# What is Acontext?



Acontext is an open-source **Agent Skills as a Memory Layer**: skill memory that **automatically** captures learnings from agent runs and stores knowledge as **Markdown files**. You can read, edit, and share those files across agents, LLMs and frameworks.

<Frame>
  <img src="/images/memory-stack.svg" alt="Acontext — Session Storage, Task Tracking, Skill Memory" />
</Frame>

## Why Skill Memory? [#why-skill-memory]

Other memory (e.g. Mem0, Zep, vector stores, RAG) stores or retrieves from **conversations** or **static docs**. If you need a layer that learns from **what the agent did** (task outcomes) and turns that into reusable SOPs and warnings, that's **Agent Skills as a memory layer**.

|               | Skill memory (Agent Skills layer)                                  | Other memory (e.g. Mem0 / Zep)       | Vector store / RAG            |
| ------------- | ------------------------------------------------------------------ | ------------------------------------ | ----------------------------- |
| **Captures**  | What the agent *did* + outcome → procedures, preferences, warnings | What was *said* → facts, preferences | What you *ingested* → chunks  |
| **Stored as** | Markdown files (human-readable, portable)                          | Embeddings / graph                   | Embeddings                    |
| **Retrieval** | Agent calls tools, gets full units (e.g. whole files)              | Semantic or chat recall              | Similarity search over chunks |

**In short:** Other memory stores and retrieves chat or ingested docs as-is. Acontext also takes session messages (and execution) as input, but distills from them *what the agent did and how it turned out* — then writes that as structured skill files that grow with every run.

<CardGroup>
  <Card title="Session Storage" icon="database" href="/store/overview">
    Session-scoped storage for messages, files, artifacts, and sandboxes — with context engineering built in.
  </Card>

  <Card title="Task Tracking" icon="radar" href="/observe/whatis">
    Task status, progress tracking, and automatic summaries.
  </Card>

  <Card title="Skill Memory" icon="brain" href="/learn/quick">
    Persistent skills learned across sessions — Markdown-native, configurable, and human-readable.
  </Card>
</CardGroup>

## Product Overview [#product-overview]

<CardGroup cols="2">
  <Card title="Long-term Skill" icon="brain" href="https://acontext.io/product/long-term-skill">
    Agent memory stored as skills — filesystem-compatible, configurable, human-readable.
  </Card>

  <Card title="Short-term Memory" icon="database" href="https://acontext.io/product/short-term-memory">
    Multi-provider messages, S3-backed disk storage, and reusable skill packages.
  </Card>

  <Card title="Mid-term State" icon="radar" href="https://acontext.io/product/mid-term-state">
    Track agent tasks, traces, token usage, and session analytics.
  </Card>

  <Card title="Sandbox" icon="terminal" href="https://acontext.io/product/sandbox">
    Secure code execution with mountable skills.
  </Card>
</CardGroup>


---

# Quickstart for AI



Use [llms.txt](https://docs.acontext.io/llms.txt) to give any coding agent full context on Acontext in \~2k tokens.

<Steps>
  <Step title="Copy the llms.txt content">
    Open [https://docs.acontext.io/llms.txt](https://docs.acontext.io/llms.txt) and copy all content, or just paste the link directly.
  </Step>

  <Step title="Prompt your coding agent">
    Paste the content (or link) into your coding agent — Cursor, Claude Code, Windsurf, or any AI assistant — along with your request:

    ```
    Build a backend for an agent application with Acontext
    ```
  </Step>
</Steps>

<Tip>
  Most coding agents can fetch URL content directly. Try pasting just the link `https://docs.acontext.io/llms.txt` into your prompt.
</Tip>


---

# Quickstart



Get up and running with Acontext in under 5 minutes. This guide walks you through setting up the server, installing the SDK, and storing your first messages.

<Steps>
  <Step title="Start Acontext server">
    Choose between hosted or self-hosted Acontext:

    <Tabs>
      <Tab title="Hosted (Recommended)">
        Go to [Acontext Dashboard](https://dash.acontext.io) and sign up for a free account. The onboarding process will guide you to get an API key.

        Set your API key as an environment variable:

        ```bash
        export ACONTEXT_API_KEY="your-api-key"
        ```
      </Tab>

      <Tab title="Self-hosted">
        Refer to [local deployment](/settings/local) to start Acontext server in 2 commands. This launches:

        * **API**: [http://localhost:8029/api/v1](http://localhost:8029/api/v1)
        * **Dashboard**: [http://localhost:3000/](http://localhost:3000/)

        The default API key is `sk-ac-your-root-api-bearer-token`.
      </Tab>
    </Tabs>
  </Step>

  <Step title="Install the SDK">
    <Tabs>
      <Tab title="Python">
        ```bash
        pip install acontext
        ```

        <Info>
          Requires Python 3.10 or newer.
        </Info>
      </Tab>

      <Tab title="TypeScript/JavaScript">
        <CodeGroup>
          ```bash title="npm"
          npm install @acontext/acontext
          ```

          ```bash title="yarn"
          yarn add @acontext/acontext
          ```

          ```bash title="pnpm"
          pnpm add @acontext/acontext
          ```
        </CodeGroup>

        <Info>
          Requires Node.js 16.x or newer.
        </Info>
      </Tab>
    </Tabs>
  </Step>

  <Step title="Verify your setup">
    Run this script to test your connection and store your first messages:

    <CodeGroup>
      ```python title="Python"
      import os
      from acontext import AcontextClient

      client = AcontextClient(
          api_key=os.getenv("ACONTEXT_API_KEY"),
      )

      # If you're using self-hosted Acontext:
      # client = AcontextClient(
      #     base_url="http://localhost:8029/api/v1",
      #     api_key="sk-ac-your-root-api-bearer-token",
      # )

      print(client.ping())

      # Create a session (optionally associate with a user)
      session = client.sessions.create(user="user@example.com")

      # Store messages
      client.sessions.store_message(
          session_id=session.id,
          blob={
              "role": "assistant",
              "content": """Here is my plan:
      1. Use Next.js for the frontend
      2. Use Supabase for the database
      3. Deploy to Cloudflare Pages
      """,
          },
      )
      client.sessions.store_message(
          session_id=session.id,
          blob={
              "role": "user",
              "content": "Confirm, go ahead. Use tailwind for frontend styling.",
          },
      )

      # Retrieve messages
      messages = client.sessions.get_messages(session_id=session.id)
      print(messages.items)
      ```

      ```typescript title="TypeScript"
      import { AcontextClient } from '@acontext/acontext';

      const client = new AcontextClient({
          apiKey: process.env.ACONTEXT_API_KEY,
      });

      // If you're using self-hosted Acontext:
      // const client = new AcontextClient({
      //     baseUrl: "http://localhost:8029/api/v1",
      //     apiKey: "sk-ac-your-root-api-bearer-token",
      // });

      async function main() {
          console.log(await client.ping());

          // Create a session (optionally associate with a user)
          const session = await client.sessions.create({ user: 'user@example.com' });

          // Store messages
          await client.sessions.storeMessage(session.id, {
              role: 'assistant',
              content: `Here is my plan:
      1. Use Next.js for the frontend
      2. Use Supabase for the database
      3. Deploy to Cloudflare Pages
      `,
          });
          await client.sessions.storeMessage(session.id, {
              role: 'user',
              content: 'Confirm, go ahead. Use tailwind for frontend styling.',
          });

          // Retrieve messages
          const messages = await client.sessions.getMessages(session.id);
          console.log(messages.items);
      }

      main();
      ```
    </CodeGroup>

    <Check>
      If you see the success message and your stored messages, you're ready to start using Acontext!
    </Check>
  </Step>
</Steps>

## Troubleshooting [#troubleshooting]

<AccordionGroup>
  <Accordion title="Connection refused or timeout errors">
    * Verify your Acontext server is running
    * Check that the `base_url` is correct
    * Ensure no firewall is blocking the connection
    * For local development, confirm you're using `http://localhost:8029/api/v1`
  </Accordion>

  <Accordion title="Authentication errors">
    * Verify your API key is correct
    * Check that the API key has the necessary permissions
    * Ensure you're passing the API key in the correct format
  </Accordion>

  <Accordion title="Module not found errors">
    * Confirm the package is installed: `pip list | grep acontext` (Python) or `npm list @acontext/acontext` (TypeScript)
    * Try reinstalling the package
    * Check your Python version is 3.10+ or Node.js version is 16+
  </Accordion>

  <Accordion title="Type errors in TypeScript">
    * Ensure you're using TypeScript 4.5 or newer
    * Run `npm install` to ensure all type definitions are installed
    * Check that your `tsconfig.json` has proper settings for module resolution
  </Accordion>
</AccordionGroup>

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Session Management" icon="folder" href="/store/messages/multi-provider">
    Create and manage sessions for organizing your conversations
  </Card>

  <Card title="Context Engineering" icon="gear" href="/engineering/editing">
    Build a compact context for agents in one API call
  </Card>

  <Card title="Disk Storage" icon="database" href="/store/disk">
    Store and manage files and artifacts in Acontext
  </Card>

  <Card title="Skill Memory" icon="brain" href="/learn/quick">
    Understand how skill memory works
  </Card>

  <Card title="API Reference" icon="code" href="/api-reference/introduction">
    Explore the complete API documentation
  </Card>
</CardGroup>


---

# One-command Setup



Acontext ships a [`SKILL.md`](https://acontext.io/SKILL.md) that any coding agent can read and execute. One prompt is all it takes — the agent installs the CLI, logs you in, and wires up the plugin automatically.

## How It Works [#how-it-works]

Copy the prompt for your platform and paste it into your agent. The agent reads `SKILL.md`, follows the instructions, and sets everything up.

<Tabs>
  <Tab title="Claude Code">
    ```
    Read https://acontext.io/SKILL.md and follow the instructions to install and configure Acontext for Claude Code
    ```

    Paste this into Claude Code. The agent will:

    1. Install the Acontext CLI
    2. Log you in via browser OAuth
    3. Install the Claude Code plugin
    4. Configure environment variables

    For full configuration, MCP tools, and troubleshooting, see the [Claude Code integration guide](/integrations/claude-code).
  </Tab>

  <Tab title="OpenClaw">
    ```
    Read https://acontext.io/SKILL.md and follow the instructions to install and configure Acontext for OpenClaw
    ```

    Paste this into your OpenClaw agent. The agent will:

    1. Install the Acontext CLI
    2. Log you in via browser OAuth
    3. Install the `@acontext/openclaw` plugin
    4. Configure `openclaw.json`

    For full configuration, agent tools, and troubleshooting, see the [OpenClaw integration guide](/integrations/openclaw).
  </Tab>
</Tabs>

<Tip>
  You can also copy this prompt directly from the [landing page](https://acontext.io) hero — click the subtitle text to copy.
</Tip>

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Claude Code Integration" icon="terminal" href="/integrations/claude-code">
    Full configuration reference for the Claude Code plugin
  </Card>

  <Card title="OpenClaw Integration" icon="plug" href="/integrations/openclaw">
    Full configuration reference for the OpenClaw plugin
  </Card>
</CardGroup>


---

# Introduction



## Overview [#overview]

Acontext provides a REST API for building AI agents with persistent context.

<CardGroup cols="2">
  <Card title="REST API" icon="code" iconType="solid">
    RESTful endpoints with predictable URLs
  </Card>

  <Card title="OpenAPI 3.0" icon="file-code" iconType="solid">
    Full OpenAPI specification available
  </Card>

  <Card title="Multi-format Support" icon="layer-group" iconType="solid">
    OpenAI, Anthropic, Gemini, and native formats
  </Card>

  <Card title="Client SDKs" icon="puzzle-piece" iconType="solid">
    Python and TypeScript libraries
  </Card>
</CardGroup>

## Authentication [#authentication]

All API requests require a Bearer token in the `Authorization` header:

```bash
Authorization: Bearer YOUR_API_KEY
```

<Warning>
  Keep your API keys secure and never expose them in client-side code.
</Warning>

## Install SDKs [#install-sdks]

<CodeGroup>
  ```bash title="Python"
  pip install acontext
  ```

  ```bash title="TypeScript"
  npm install @acontext/acontext
  ```
</CodeGroup>

## Quick Start [#quick-start]

<CodeGroup>
  ```python title="Python"
  import os
  from acontext import AcontextClient

  # Initialize client
  client = AcontextClient(
      api_key=os.getenv("ACONTEXT_API_KEY"),
  )

  # If you're using self-hosted Acontext:
  # client = AcontextClient(
  #     base_url="http://localhost:8029/api/v1",
  #     api_key="sk-ac-your-root-api-bearer-token",
  # )

  # Create a session
  session = client.sessions.create()

  # Store a message
  client.sessions.store_message(
      session.id,
      blob={'role': 'user', 'content': 'How do I reset my password?'}
  )
  ```

  ```typescript title="TypeScript"
  import { AcontextClient } from '@acontext/acontext';

  // Initialize client
  const client = new AcontextClient({
      apiKey: process.env.ACONTEXT_API_KEY,
  });

  // If you're using self-hosted Acontext:
  // const client = new AcontextClient({
  //     baseUrl: "http://localhost:8029/api/v1",
  //     apiKey: "sk-ac-your-root-api-bearer-token",
  // });

  // Create a session
  const session = await client.sessions.create();

  // Store a message
  await client.sessions.storeMessage(
    session.id,
    { role: 'user', content: 'How do I reset my password?' }
  );
  ```
</CodeGroup>

## Core Concepts [#core-concepts]

<AccordionGroup>
  <Accordion title="Sessions" icon="messages">
    Sessions represent conversation threads with message history.
  </Accordion>

  <Accordion title="Disks" icon="hard-drive">
    Isolated storage groups for organizing files and artifacts within a project.
  </Accordion>

  <Accordion title="Tools" icon="wrench">
    Capabilities that AI agents can use. Manage and configure tools across your project.
  </Accordion>
</AccordionGroup>

## API Resources [#api-resources]

<CardGroup cols="2">
  <Card title="Sessions" icon="messages" href="#session">
    Handle conversations and messages
  </Card>

  <Card title="Disks" icon="hard-drive" href="#disk">
    Manage storage groups
  </Card>

  <Card title="Tools" icon="wrench" href="#tool">
    Configure agent capabilities
  </Card>
</CardGroup>

## Message Formats [#message-formats]

Acontext supports multiple message formats for compatibility:

* **OpenAI** - Compatible with OpenAI Chat Completion format (default)
* **Anthropic** - Compatible with Anthropic Messages format
* **Gemini** - Compatible with Google Gemini Messages format

Convert between formats when retrieving or storing messages using the `format` parameter.

## Community [#community]

<CardGroup cols="2">
  <Card title="GitHub" icon="github" href="https://github.com/memodb-io/Acontext">
    Report issues and contribute
  </Card>

  <Card title="Discord" icon="discord" href="https://discord.acontext.io">
    Join our community
  </Card>
</CardGroup>


---

# Claude Code



The Acontext plugin for Claude Code gives your agent cross-session skill memory. It captures conversations, extracts tasks, distills reusable Markdown skills, and syncs them to Claude Code's skill directory for automatic loading.

## Quick Start [#quick-start]

<Steps>
  <Step title="Add the marketplace and install the plugin">
    Inside Claude Code, run:

    ```
    /plugin marketplace add memodb-io/Acontext
    /plugin install acontext
    ```
  </Step>

  <Step title="Set your environment variables">
    Get an API key from [dash.acontext.io](https://dash.acontext.io/) and export it in your shell profile (`~/.bashrc` or `~/.zshrc`):

    ```bash
    export ACONTEXT_API_KEY="<your-api-key>"
    export ACONTEXT_USER_IDENTIFIER="<your-identifier>"
    ```
  </Step>

  <Step title="Restart Claude Code">
    The plugin loads automatically. Every conversation is captured, tasks are extracted, and skills are distilled and synced to `~/.claude/skills/`.
  </Step>
</Steps>

## How It Works [#how-it-works]

```
Session 1: User talks to agent
  └→ Messages stored to Acontext session (incremental capture)
  └→ CORE extracts structured tasks
  └→ Learning Space distills skills as Markdown

Session 2: User returns
  └→ Skills synced to ~/.claude/skills/ (native loading)
  └→ Claude Code auto-loads skill files from its skill directory
  └→ New messages captured (incremental capture)
  └→ Skills updated (auto-learn)
```

The plugin hooks into Claude Code's lifecycle via MCP:

* **On session start** — Syncs learned skills to `~/.claude/skills/` so Claude Code loads them natively
* **On session end** — Stores new conversation messages, triggers task extraction and skill distillation
* **On demand** — Exposes MCP tools the agent can call mid-session (search skills, trigger learning, etc.)

<Note>
  Skills are synced as Markdown files that Claude Code loads natively. You can inspect, modify, and share these files directly in `~/.claude/skills/`.
</Note>

## Configuration [#configuration]

All settings are via environment variables:

| Env Var                        | Default                           | Description                                |
| ------------------------------ | --------------------------------- | ------------------------------------------ |
| `ACONTEXT_API_KEY`             | —                                 | **Required.** Acontext API key             |
| `ACONTEXT_BASE_URL`            | `https://api.acontext.app/api/v1` | API base URL                               |
| `ACONTEXT_USER_IDENTIFIER`     | `"claude_code"`                   | User identifier for session scoping        |
| `ACONTEXT_LEARNING_SPACE_ID`   | auto-created                      | Explicit Learning Space ID                 |
| `ACONTEXT_SKILLS_DIR`          | `~/.claude/skills`                | Directory where skills are synced          |
| `ACONTEXT_AUTO_CAPTURE`        | `true`                            | Store messages after each agent turn       |
| `ACONTEXT_AUTO_LEARN`          | `true`                            | Trigger skill distillation after sessions  |
| `ACONTEXT_MIN_TURNS_FOR_LEARN` | `4`                               | Minimum turns before triggering auto-learn |

<Tip>
  For self-hosted Acontext, set `ACONTEXT_BASE_URL` to your local API endpoint (e.g. `http://localhost:8029/api/v1`).
</Tip>

## MCP Tools [#mcp-tools]

The plugin registers these tools that your agent can call during conversations:

| Tool                       | Description                                              |
| -------------------------- | -------------------------------------------------------- |
| `acontext_search_skills`   | Search through skill files by keyword                    |
| `acontext_get_skill`       | Read the content of a specific skill file                |
| `acontext_session_history` | Get task summaries from recent past sessions             |
| `acontext_stats`           | Show memory statistics (sessions, skills, configuration) |
| `acontext_learn_now`       | Trigger skill learning from the current session          |

## Troubleshooting [#troubleshooting]

### Plugin not loading [#plugin-not-loading]

* Ensure `ACONTEXT_API_KEY` is exported in your shell profile
* Verify the plugin is installed: `/plugin list`
* Check Claude Code logs for `[info] acontext:` or `[warn] acontext:` messages

### No skills appearing [#no-skills-appearing]

* Skills appear in `~/.claude/skills/` after the first session ends
* Check that `ACONTEXT_AUTO_LEARN` is not set to `false`
* Ensure at least `ACONTEXT_MIN_TURNS_FOR_LEARN` turns have occurred

### API returns 401 [#api-returns-401]

* Verify your API key with `acontext whoami`
* Re-login with `acontext login`

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Skill Memory" icon="brain" href="/learn/quick">
    How Learning Spaces distill skills from sessions
  </Card>

  <Card title="Session Storage" icon="database" href="/store/overview">
    Session persistence and message storage
  </Card>

  <Card title="Task Tracking" icon="radar" href="/observe/whatis">
    Automatic task extraction from conversations
  </Card>

  <Card title="Dashboard" icon="chart-simple" href="/observe/dashboard">
    View all agent interactions and skills
  </Card>
</CardGroup>


---

# OpenClaw



The `@acontext/openclaw` plugin gives OpenClaw agents cross-session skill memory. It captures conversations, extracts tasks, distills reusable Markdown skills, and syncs them to OpenClaw's native skill directory for automatic loading.

## Quick Start [#quick-start]

<Steps>
  <Step title="Install the plugin">
    ```bash
    openclaw plugins install @acontext/openclaw
    ```
  </Step>

  <Step title="Set your API key">
    Get an API key from [dash.acontext.io](https://dash.acontext.io/) and export it:

    ```bash
    export ACONTEXT_API_KEY=sk-ac-your-api-key
    ```
  </Step>

  <Step title="Configure openclaw.json">
    Add the Acontext plugin to your `openclaw.json` config:

    ```json5
    {
      plugins: {
        // Select Acontext as the active memory plugin
        slots: {
          memory: "acontext"
        },
        entries: {
          "acontext": {
            enabled: true,
            config: {
              "apiKey": "${ACONTEXT_API_KEY}",
              "userIdentifier": "your-identifier"
            }
          }
        }
      }
    }
    ```

    <Note>
      OpenClaw only loads one memory plugin at a time. Setting `plugins.slots.memory` to `"acontext"` replaces the default (`memory-core`). To switch back, set it to `"memory-core"` or `"none"`.
    </Note>
  </Step>

  <Step title="Start the gateway">
    ```bash
    openclaw gateway
    ```

    Your agent now has skill memory. Every conversation is captured, tasks are extracted, and skills are distilled and synced to `~/.openclaw/skills/` for native loading.
  </Step>
</Steps>

## How It Works [#how-it-works]

```
Session 1: User talks to agent
  └→ Messages stored to Acontext session (incremental capture)
  └→ CORE extracts structured tasks
  └→ Learning Space distills skills as Markdown

  ── Compaction or reset occurs ──
  └→ Learning triggered
  └→ Message cursor resets for next capture

Session 2: User returns
  └→ Skills synced to ~/.openclaw/skills/ (native loading)
  └→ OpenClaw auto-loads skill files from its skill directory
  └→ New messages captured (incremental capture)
  └→ Skills updated (auto-learn)
```

The plugin hooks into four OpenClaw lifecycle events:

* **`before_agent_start`** — Ensures learned skills are synced to `~/.openclaw/skills/` so OpenClaw can load them natively
* **`agent_end`** — Stores new conversation messages to Acontext incrementally, triggers task extraction, and conditionally triggers skill distillation
* **`before_compaction`** — Triggers learning before OpenClaw compacts the conversation history, then flags the message cursor for reset
* **`before_reset`** — Triggers learning before a session reset clears the transcript, then flags the message cursor for reset

<Note>
  Message capture is incremental — the plugin tracks a cursor so only new messages are stored on each `agent_end`. When compaction or reset rewrites the transcript, the cursor resets automatically to avoid gaps or duplicates.
</Note>

## Configuration [#configuration]

| Key                | Type      | Default                           | Description                                                     |
| ------------------ | --------- | --------------------------------- | --------------------------------------------------------------- |
| `apiKey`           | `string`  | —                                 | **Required.** Acontext API key (supports `${ACONTEXT_API_KEY}`) |
| `baseUrl`          | `string`  | `https://api.acontext.app/api/v1` | Acontext API base URL                                           |
| `userIdentifier`   | `string`  | `"openclaw"`                      | User identifier for session scoping                             |
| `learningSpaceId`  | `string`  | auto-created                      | Explicit Learning Space ID                                      |
| `skillsDir`        | `string`  | `~/.openclaw/skills`              | Directory where skills are synced for native loading            |
| `autoCapture`      | `boolean` | `true`                            | Store messages after each turn                                  |
| `autoLearn`        | `boolean` | `true`                            | Trigger skill distillation after sessions                       |
| `minTurnsForLearn` | `number`  | `4`                               | Minimum conversation turns before triggering auto-learn         |

<Tip>
  For self-hosted Acontext, set `baseUrl` to your local API endpoint (e.g. `http://localhost:8029/api/v1`).
</Tip>

## Agent Tools [#agent-tools]

The plugin registers three tools your agent can call explicitly during conversations:

| Tool                       | Description                                                 |
| -------------------------- | ----------------------------------------------------------- |
| `acontext_search_skills`   | Search through learned skill files by keyword               |
| `acontext_session_history` | Get task summaries from recent past sessions                |
| `acontext_learn_now`       | Trigger skill learning from the current session immediately |

## CLI Commands [#cli-commands]

```bash
# List all learned skills in the Learning Space
openclaw acontext skills

# Show memory statistics (sessions, tasks, skills)
openclaw acontext stats
```

## Skill Sync [#skill-sync]

Skills are synced as Markdown files to OpenClaw's native skill directory (`~/.openclaw/skills/` by default). OpenClaw auto-loads them — no prompt injection needed.

* **Incremental sync** — Only changed skills are re-downloaded, using server-side `updated_at` timestamps
* **Sync triggers** — On service start, before each agent turn (if stale), and after learning

<Note>
  Unlike plugins that inject context into prompts, Acontext syncs skills as files that OpenClaw loads natively. You can inspect, modify, and share these Markdown skill files directly in `~/.openclaw/skills/`.
</Note>

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Skill Memory" icon="brain" href="/learn/quick">
    How Learning Spaces distill skills from sessions
  </Card>

  <Card title="Session Storage" icon="database" href="/store/overview">
    Session persistence and message storage
  </Card>

  <Card title="Task Tracking" icon="radar" href="/observe/whatis">
    Automatic task extraction from conversations
  </Card>

  <Card title="Dashboard" icon="chart-simple" href="/observe/dashboard">
    View all agent interactions and skills
  </Card>
</CardGroup>


---

# Using Agent Skills



[Agent Skills](https://agentskills.io/home) are folders of instructions and resources that agents can use. This guide shows how to upload a skill and build an agent that reads it.

## Step 1: Get a Skill [#step-1-get-a-skill]

Download from [Anthropic Skills Repository](https://github.com/anthropics/skills/tree/main/skills):

```bash
git clone https://github.com/anthropics/skills.git
cd skills/skills/internal-comms
zip -r internal-comms.zip .
```

## Step 2: Upload the Skill [#step-2-upload-the-skill]

<CodeGroup>
  ```python title="Python"
  import os
  from acontext import AcontextClient, FileUpload

  client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))

  with open("internal-comms.zip", "rb") as f:
      skill = client.skills.create(
          file=FileUpload(filename="internal-comms.zip", content=f.read())
      )
  print(f"Skill ID: {skill.id}")
  ```

  ```typescript title="TypeScript"
  import { AcontextClient, FileUpload } from '@acontext/acontext';
  import * as fs from 'fs';

  const client = new AcontextClient({
      apiKey: process.env.ACONTEXT_API_KEY,
  });

  const fileContent = fs.readFileSync("internal-comms.zip");
  const skill = await client.skills.create({
      file: new FileUpload({ filename: "internal-comms.zip", content: fileContent }),
  });
  console.log(`Skill ID: ${skill.id}`);
  ```
</CodeGroup>

## Step 3: Build an Agent with Sandbox Tools [#step-3-build-an-agent-with-sandbox-tools]

<Accordion title="Complete example with sandbox tools">
  <CodeGroup>
    ```python title="Python"
    import json
    import os
    from acontext import AcontextClient
    from acontext.agent.sandbox import SANDBOX_TOOLS
    from openai import OpenAI

    client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))
    openai_client = OpenAI()

    skill_id = "your-skill-id"

    # Create sandbox and disk
    sandbox = client.sandboxes.create()
    disk = client.disks.create()

    # Mount skill in sandbox
    ctx = SANDBOX_TOOLS.format_context(
        client,
        sandbox_id=sandbox.sandbox_id,
        disk_id=disk.id,
        mount_skills=[skill_id]
    )

    tools = SANDBOX_TOOLS.to_openai_tool_schema()
    context_prompt = ctx.get_context_prompt()

    messages = [
        {"role": "system", "content": f"You have sandbox tools.\n\n{context_prompt}"},
        {"role": "user", "content": "What communication guidelines should I follow?"}
    ]

    # Agent loop
    while True:
        response = openai_client.chat.completions.create(
            model="gpt-4.1", messages=messages, tools=tools
        )
        message = response.choices[0].message
        messages.append(message)

        if not message.tool_calls:
            print(f"Assistant: {message.content}")
            break

        for tc in message.tool_calls:
            result = SANDBOX_TOOLS.execute_tool(ctx, tc.function.name, json.loads(tc.function.arguments))
            messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})

    # Cleanup
    client.sandboxes.kill(sandbox.sandbox_id)
    client.disks.delete(disk.id)
    ```

    ```typescript title="TypeScript"
    import { AcontextClient, SANDBOX_TOOLS } from '@acontext/acontext';
    import OpenAI from 'openai';

    const client = new AcontextClient({
        apiKey: process.env.ACONTEXT_API_KEY,
    });
    const openai = new OpenAI();

    const skillId = "your-skill-id";

    // Create sandbox and disk
    const sandbox = await client.sandboxes.create();
    const disk = await client.disks.create();

    // Mount skill in sandbox
    const ctx = await SANDBOX_TOOLS.formatContext(
        client,
        sandbox.sandbox_id,
        disk.id,
        [skillId]
    );

    const tools = SANDBOX_TOOLS.toOpenAIToolSchema();
    const contextPrompt = ctx.getContextPrompt();

    const messages: OpenAI.ChatCompletionMessageParam[] = [
        { role: "system", content: `You have sandbox tools.\n\n${contextPrompt}` },
        { role: "user", content: "What communication guidelines should I follow?" },
    ];

    // Agent loop
    while (true) {
        const response = await openai.chat.completions.create({
            model: "gpt-4.1",
            messages,
            tools,
        });
        const message = response.choices[0].message;
        messages.push(message);

        if (!message.tool_calls) {
            console.log(`Assistant: ${message.content}`);
            break;
        }

        for (const tc of message.tool_calls) {
            const result = await SANDBOX_TOOLS.executeTool(ctx, tc.function.name, JSON.parse(tc.function.arguments));
            messages.push({ role: "tool", tool_call_id: tc.id, content: result });
        }
    }

    // Cleanup
    await client.sandboxes.kill(sandbox.sandbox_id);
    await client.disks.delete(disk.id);
    ```
  </CodeGroup>
</Accordion>

## Alternative: Skill Content Tools [#alternative-skill-content-tools]

For read-only skills (no scripts to execute):

<Accordion title="Complete example with skill tools">
  <CodeGroup>
    ```python title="Python"
    import json
    import os
    from acontext import AcontextClient
    from acontext.agent.skill import SKILL_TOOLS
    from openai import OpenAI

    client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))
    openai_client = OpenAI()

    ctx = SKILL_TOOLS.format_context(client, ["your-skill-id"])
    tools = SKILL_TOOLS.to_openai_tool_schema()

    messages = [
        {"role": "system", "content": f"You have skill access.\n\n{ctx.get_context_prompt()}"},
        {"role": "user", "content": "What are the guidelines?"}
    ]

    while True:
        response = openai_client.chat.completions.create(
            model="gpt-4.1", messages=messages, tools=tools
        )
        message = response.choices[0].message
        messages.append(message)

        if not message.tool_calls:
            print(f"Assistant: {message.content}")
            break

        for tc in message.tool_calls:
            result = SKILL_TOOLS.execute_tool(ctx, tc.function.name, json.loads(tc.function.arguments))
            messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})
    ```

    ```typescript title="TypeScript"
    import { AcontextClient, SKILL_TOOLS } from '@acontext/acontext';
    import OpenAI from 'openai';

    const client = new AcontextClient({
        apiKey: process.env.ACONTEXT_API_KEY,
    });
    const openai = new OpenAI();

    const ctx = await SKILL_TOOLS.formatContext(client, ["your-skill-id"]);
    const tools = SKILL_TOOLS.toOpenAIToolSchema();

    const messages: OpenAI.ChatCompletionMessageParam[] = [
        { role: "system", content: `You have skill access.\n\n${ctx.getContextPrompt()}` },
        { role: "user", content: "What are the guidelines?" },
    ];

    while (true) {
        const response = await openai.chat.completions.create({
            model: "gpt-4.1",
            messages,
            tools,
        });
        const message = response.choices[0].message;
        messages.push(message);

        if (!message.tool_calls) {
            console.log(`Assistant: ${message.content}`);
            break;
        }

        for (const tc of message.tool_calls) {
            const result = await SKILL_TOOLS.executeTool(ctx, tc.function.name, JSON.parse(tc.function.arguments));
            messages.push({ role: "tool", tool_call_id: tc.id, content: result });
        }
    }
    ```
  </CodeGroup>
</Accordion>

## When to Use Each [#when-to-use-each]

| Approach                | Use When                     |
| ----------------------- | ---------------------------- |
| **Sandbox Tools**       | Skill has executable scripts |
| **Skill Content Tools** | Skill is read-only reference |

## Next Steps [#next-steps]

<CardGroup cols="3">
  <Card title="Skill API" icon="book" href="/store/skill">
    Skills storage API
  </Card>

  <Card title="Sandbox Tools" icon="terminal" href="/tool/bash_tools">
    Full sandbox API
  </Card>

  <Card title="Skill Memory" icon="brain" href="/learn/quick">
    Build skills from agent sessions
  </Card>
</CardGroup>


---

# Prompt Cache Stability



Edit strategies change the message prefix, breaking prompt caching. Use `pin_editing_strategies_at_message` to keep the prefix stable.

## The Problem [#the-problem]

```
Round 1: [a, b, c]
Round 2: [a(edited), b, c, d]           # cache miss
Round 3: [a(edited), b(edited), c, d, e] # cache miss
```

## The Solution [#the-solution]

Pin where strategies are applied:

```
Round 1: [z(edited), a, b, c]           # edit_at_message_id = c
Round 2: [z(edited), a, b, c, d]        # stable prefix ✓
Round 3: [z(edited), a, b, c, d, e]     # stable prefix ✓
```

## Usage [#usage]

<CodeGroup>
  ```python title="Python"
  from acontext import AcontextClient
  import os

  client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))

  # First call - get the pin ID
  result = client.sessions.get_messages(
      session_id="session-uuid",
      edit_strategies=[{"type": "remove_tool_result", "params": {"keep_recent_n_tool_results": 3}}]
  )
  cache_pin_id = result.edit_at_message_id

  # Subsequent calls - pin to maintain cache
  result = client.sessions.get_messages(
      session_id="session-uuid",
      pin_editing_strategies_at_message=cache_pin_id,
      edit_strategies=[{"type": "remove_tool_result", "params": {"keep_recent_n_tool_results": 3}}]
  )
  ```

  ```typescript title="TypeScript"
  import { AcontextClient } from '@acontext/acontext';

  const client = new AcontextClient({
      apiKey: process.env.ACONTEXT_API_KEY,
  });

  // First call - get the pin ID
  let result = await client.sessions.getMessages("session-uuid", {
      editStrategies: [{ type: "remove_tool_result", params: { keep_recent_n_tool_results: 3 } }],
  });
  let cachePinId = result.editAtMessageId;

  // Subsequent calls - pin to maintain cache
  result = await client.sessions.getMessages("session-uuid", {
      pinEditingStrategiesAtMessage: cachePinId,
      editStrategies: [{ type: "remove_tool_result", params: { keep_recent_n_tool_results: 3 } }],
  });
  ```
</CodeGroup>

## When to Reset [#when-to-reset]

When context grows too large, reset by omitting the pin:

<CodeGroup>
  ```python title="Python"
  if result.this_time_tokens > 50000:
      result = client.sessions.get_messages(
          session_id="session-uuid",
          edit_strategies=[
              {"type": "remove_tool_result", "params": {"keep_recent_n_tool_results": 3}},
              {"type": "token_limit", "params": {"limit_tokens": 30000}}
          ]
      )
      cache_pin_id = result.edit_at_message_id  # new pin
  ```

  ```typescript title="TypeScript"
  if (result.thisTimeTokens > 50000) {
      result = await client.sessions.getMessages("session-uuid", {
          editStrategies: [
              { type: "remove_tool_result", params: { keep_recent_n_tool_results: 3 } },
              { type: "token_limit", params: { limit_tokens: 30000 } },
          ],
      });
      cachePinId = result.editAtMessageId;  // new pin
  }
  ```
</CodeGroup>

## Next Steps [#next-steps]

<Card title="Context Editing" icon="scissors" href="/engineering/editing">
  Available edit strategies
</Card>


---

# Copy Session



Create independent copies of sessions with all messages, tasks, and configurations intact. Perfect for experimentation, checkpointing, and branching workflows.

## Use Cases [#use-cases]

* **Experimentation**: Try different approaches without affecting the original conversation
* **Checkpointing**: Save progress at key decision points
* **Branching workflows**: Explore parallel conversation paths from a common starting point
* **Template sessions**: Reuse successful session patterns

## Basic Usage [#basic-usage]

<CodeGroup>
  ```python title="Python"
  from acontext import AcontextClient
  import os

  client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))

  # Copy a session
  result = client.sessions.copy(session_id="session-uuid")

  print(f"Original session: {result.old_session_id}")
  print(f"Copied session: {result.new_session_id}")

  # Now you can modify the copied session independently
  client.sessions.store_message(
      session_id=result.new_session_id,
      blob={"role": "user", "content": "Let's try a different approach"},
      format="openai"
  )
  ```

  ```typescript title="TypeScript"
  import { AcontextClient } from '@acontext/acontext';

  const client = new AcontextClient({
      apiKey: process.env.ACONTEXT_API_KEY,
  });

  // Copy a session
  const result = await client.sessions.copy("session-uuid");

  console.log(`Original session: ${result.oldSessionId}`);
  console.log(`Copied session: ${result.newSessionId}`);

  // Now you can modify the copied session independently
  await client.sessions.storeMessage(
      result.newSessionId,
      { role: "user", content: "Let's try a different approach" },
      { format: "openai" }
  );
  ```
</CodeGroup>

## Async Usage [#async-usage]

<CodeGroup>
  ```python title="Python (Async)"
  from acontext import AsyncAcontextClient
  import os
  import asyncio

  async def copy_example():
      async with AsyncAcontextClient(api_key=os.getenv("ACONTEXT_API_KEY")) as client:
          result = await client.sessions.copy(session_id="session-uuid")
          print(f"Copied session: {result.new_session_id}")

  asyncio.run(copy_example())
  ```

  ```typescript title="TypeScript"
  import { AcontextClient } from '@acontext/acontext';

  const client = new AcontextClient({
      apiKey: process.env.ACONTEXT_API_KEY,
  });

  const result = await client.sessions.copy("session-uuid");
  console.log(`Copied session: ${result.newSessionId}`);
  ```
</CodeGroup>

## What Gets Copied [#what-gets-copied]

When you copy a session, the following are duplicated:

* **Messages**: All messages with preserved parent-child relationships
* **Tasks**: All tasks with their order, status, and data
* **Configurations**: Session configs and settings
* **User association**: The copied session belongs to the same project

## What Doesn't Get Copied [#what-doesnt-get-copied]

* **Asset files**: S3 assets are shared (not duplicated) between sessions for efficiency
* **Message processing status**: Copied messages are marked as "pending" for reprocessing
* **Task-message links**: These are cleared and will be reassigned by the core service

## Independence [#independence]

The copied session is completely independent:

* Changes to the original session don't affect the copy
* Changes to the copy don't affect the original
* Each session can have different messages, tasks, and configs going forward

## Limitations [#limitations]

### Size Limit [#size-limit]

Sessions with more than **5,000 messages** cannot be copied synchronously. You'll receive an error:

```json
{
  "error": "SESSION_TOO_LARGE",
  "message": "Session exceeds maximum copyable size (5000 messages). Consider using async copy."
}
```

For large sessions, async copy support is planned for a future release.

## Error Handling [#error-handling]

<CodeGroup>
  ```python title="Python"
  from acontext import AcontextClient
  from acontext.exceptions import AcontextError

  client = AcontextClient(api_key="...")

  try:
      result = client.sessions.copy(session_id="invalid-uuid")
  except AcontextError as e:
      if "SESSION_NOT_FOUND" in str(e):
          print("Session does not exist or you don't have access")
      elif "SESSION_TOO_LARGE" in str(e):
          print("Session is too large to copy synchronously")
      else:
          print(f"Copy failed: {e}")
  ```

  ```typescript title="TypeScript"
  import { AcontextClient } from '@acontext/acontext';

  const client = new AcontextClient({ apiKey: "..." });

  try {
      const result = await client.sessions.copy("invalid-uuid");
  } catch (error) {
      const message = error.message || String(error);
      
      if (message.includes("SESSION_NOT_FOUND")) {
          console.log("Session does not exist or you don't have access");
      } else if (message.includes("SESSION_TOO_LARGE")) {
          console.log("Session is too large to copy synchronously");
      } else {
          console.log(`Copy failed: ${error}`);
      }
  }
  ```
</CodeGroup>

## Common Patterns [#common-patterns]

### Experiment and Compare [#experiment-and-compare]

Copy a session to try different approaches, then compare results:

<CodeGroup>
  ```python title="Python"
  result = client.sessions.copy(session_id=original_id)
  experimental_id = result.new_session_id

  client.sessions.store_message(
      session_id=experimental_id,
      blob={"role": "user", "content": "Alternative approach..."},
      format="openai"
  )

  original = client.sessions.get_token_counts(session_id=original_id)
  experimental = client.sessions.get_token_counts(session_id=experimental_id)

  print(f"Original: {original.total_tokens} tokens")
  print(f"Experimental: {experimental.total_tokens} tokens")
  ```

  ```typescript title="TypeScript"
  const result = await client.sessions.copy(originalId);
  const experimentalId = result.newSessionId;

  await client.sessions.storeMessage(
      experimentalId,
      { role: "user", content: "Alternative approach..." },
      { format: "openai" }
  );

  const original = await client.sessions.getTokenCounts(originalId);
  const experimental = await client.sessions.getTokenCounts(experimentalId);

  console.log(`Original: ${original.totalTokens} tokens`);
  console.log(`Experimental: ${experimental.totalTokens} tokens`);
  ```
</CodeGroup>

### Checkpoint Before Major Changes [#checkpoint-before-major-changes]

Create a checkpoint before making significant changes:

<CodeGroup>
  ```python title="Python"
  checkpoint = client.sessions.copy(session_id=session_id)
  print(f"Checkpoint created: {checkpoint.new_session_id}")

  client.sessions.store_message(
      session_id=session_id,
      blob={"role": "user", "content": "Major change..."},
      format="openai"
  )
  ```

  ```typescript title="TypeScript"
  const checkpoint = await client.sessions.copy(sessionId);
  console.log(`Checkpoint created: ${checkpoint.newSessionId}`);

  await client.sessions.storeMessage(
      sessionId,
      { role: "user", content: "Major change..." },
      { format: "openai" }
  );
  ```
</CodeGroup>

### Template Sessions [#template-sessions]

Create a base session as a template and copy it for each new conversation:

<CodeGroup>
  ```python title="Python"
  template_id = "template-session-uuid"
  client.sessions.store_message(
      session_id=template_id,
      blob={"role": "system", "content": "You are a helpful assistant..."},
      format="openai"
  )

  new_conversation = client.sessions.copy(session_id=template_id)
  ```

  ```typescript title="TypeScript"
  const templateId = "template-session-uuid";
  await client.sessions.storeMessage(
      templateId,
      { role: "system", content: "You are a helpful assistant..." },
      { format: "openai" }
  );

  const newConversation = await client.sessions.copy(templateId);
  ```
</CodeGroup>

## Performance [#performance]

Copy operations typically complete in:

* **\< 1 second**: Sessions with \< 100 messages
* **\< 3 seconds**: Sessions with \< 1000 messages
* **\< 5 seconds**: Sessions with \< 5000 messages

The operation is atomic - either the entire copy succeeds or fails with no partial state.


---

# Context Editing



Apply edit strategies when retrieving messages to manage context window size. The original session remains unchanged.

## Current Context Size [#current-context-size]

The `get_messages` response includes `this_time_tokens` - the total token count of returned messages. Use this to:

* Check current context window size
* Decide when to apply edit strategies
* Determine when to [reset the prompt cache](/engineering/cache)

<CodeGroup>
  ```python title="Python"
  result = client.sessions.get_messages(session_id="session-uuid")
  print(f"Current tokens: {result.this_time_tokens}")

  if result.this_time_tokens > 50000:
      # Apply strategies to reduce context
      result = client.sessions.get_messages(
          session_id="session-uuid",
          edit_strategies=[{"type": "token_limit", "params": {"limit_tokens": 30000}}]
      )
  ```

  ```typescript title="TypeScript"
  let result = await client.sessions.getMessages("session-uuid");
  console.log(`Current tokens: ${result.thisTimeTokens}`);

  if (result.thisTimeTokens > 50000) {
      // Apply strategies to reduce context
      result = await client.sessions.getMessages("session-uuid", {
          editStrategies: [{ type: "token_limit", params: { limit_tokens: 30000 } }],
      });
  }
  ```
</CodeGroup>

## Basic Usage [#basic-usage]

<CodeGroup>
  ```python title="Python"
  from acontext import AcontextClient
  import os

  client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))

  result = client.sessions.get_messages(
      session_id="session-uuid",
      edit_strategies=[
          {"type": "token_limit", "params": {"limit_tokens": 20000}}
      ]
  )
  print(f"Messages: {len(result.items)}, Tokens: {result.this_time_tokens}")
  ```

  ```typescript title="TypeScript"
  import { AcontextClient } from '@acontext/acontext';

  const client = new AcontextClient({
      apiKey: process.env.ACONTEXT_API_KEY,
  });

  const result = await client.sessions.getMessages("session-uuid", {
      editStrategies: [
          { type: "token_limit", params: { limit_tokens: 20000 } },
      ],
  });
  console.log(`Messages: ${result.items.length}, Tokens: ${result.thisTimeTokens}`);
  ```
</CodeGroup>

## Edit Strategies [#edit-strategies]

### Token Limit [#token-limit]

Remove oldest messages until under token limit:

<CodeGroup>
  ```python title="Python"
  {"type": "token_limit", "params": {"limit_tokens": 20000}}
  ```

  ```typescript title="TypeScript"
  { type: "token_limit", params: { limit_tokens: 20000 } }
  ```
</CodeGroup>

### Remove Tool Results [#remove-tool-results]

Replace old tool results with placeholder, keep recent N, and optionally filter by token count:

<CodeGroup>
  ```python title="Python"
  {
      "type": "remove_tool_result",
      "params": {
          "keep_recent_n_tool_results": 3,
          "tool_result_placeholder": "Done",
          "keep_tools": ["important_tool"],
          "gt_token": 100  # only remove results with more than 100 tokens
      }
  }
  ```

  ```typescript title="TypeScript"
  {
      type: "remove_tool_result",
      params: {
          keep_recent_n_tool_results: 3,
          tool_result_placeholder: "Done",
          keep_tools: ["important_tool"],
          gt_token: 100 // only remove results with more than 100 tokens
      }
  }
  ```
</CodeGroup>

### Remove Tool Call Params [#remove-tool-call-params]

Remove arguments from old tool calls, keep recent N, and optionally filter by token count:

<CodeGroup>
  ```python title="Python"
  {
      "type": "remove_tool_call_params",
      "params": {
          "keep_recent_n_tool_calls": 3,
          "keep_tools": ["important_tool"],
          "gt_token": 100  # only remove params with more than 100 tokens
      }
  }
  ```

  ```typescript title="TypeScript"
  {
      type: "remove_tool_call_params",
      params: {
          keep_recent_n_tool_calls: 3,
          keep_tools: ["important_tool"],
          gt_token: 100 // only remove params with more than 100 tokens
      }
  }
  ```
</CodeGroup>

### Middle Out [#middle-out]

Remove messages from the middle, preserve head and tail:

<CodeGroup>
  ```python title="Python"
  {"type": "middle_out", "params": {"token_reduce_to": 5000}}
  ```

  ```typescript title="TypeScript"
  { type: "middle_out", params: { token_reduce_to: 5000 } }
  ```
</CodeGroup>

## Combining Strategies [#combining-strategies]

<CodeGroup>
  ```python title="Python"
  result = client.sessions.get_messages(
      session_id="session-uuid",
      edit_strategies=[
          {"type": "remove_tool_result", "params": {"keep_recent_n_tool_results": 3}},
          {"type": "token_limit", "params": {"limit_tokens": 30000}}
      ]
  )
  ```

  ```typescript title="TypeScript"
  const result = await client.sessions.getMessages("session-uuid", {
      editStrategies: [
          { type: "remove_tool_result", params: { keep_recent_n_tool_results: 3 } },
          { type: "token_limit", params: { limit_tokens: 30000 } },
      ],
  });
  ```
</CodeGroup>

## Get Raw Token Count [#get-raw-token-count]

<CodeGroup>
  ```python title="Python"
  token_counts = client.sessions.get_token_counts(session_id="session-uuid")
  print(f"Total tokens: {token_counts.total_tokens}")
  ```

  ```typescript title="TypeScript"
  const tokenCounts = await client.sessions.getTokenCounts("session-uuid");
  console.log(`Total tokens: ${tokenCounts.totalTokens}`);
  ```
</CodeGroup>

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Prompt Cache" icon="bolt" href="/engineering/cache">
    Maintain cache hits with editing
  </Card>

  <Card title="Session Summary" icon="compress" href="/engineering/session_summary">
    Compact task summaries
  </Card>
</CardGroup>


---

# Session Summary



Get a token-efficient summary of session tasks for prompt injection.

## Usage [#usage]

<CodeGroup>
  ```python title="Python"
  import os
  from acontext import AcontextClient

  client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))

  # Ensure all messages have been processed
  client.sessions.flush(session_id)

  summary = client.sessions.get_session_summary(session_id)

  system_prompt = f"""You are a helpful assistant.

  Previous tasks:
  {summary}

  Continue helping the user.
  """
  ```

  ```typescript title="TypeScript"
  import { AcontextClient } from '@acontext/acontext';

  const client = new AcontextClient({
      apiKey: process.env.ACONTEXT_API_KEY,
  });

  // Ensure all messages have been processed
  await client.sessions.flush(sessionId);

  const summary = await client.sessions.getSessionSummary(sessionId);

  const systemPrompt = `You are a helpful assistant.

  Previous tasks:
  ${summary}

  Continue helping the user.
  `;
  ```
</CodeGroup>

## Output Format [#output-format]

```xml
<task id="1" description="Search for iPhone 15 news">
<progress>
1. Searched and found key specs
2. Compiled feature comparison
</progress>
<user_preference>
1. Focus on camera capabilities
</user_preference>
</task>
```

## Options [#options]

<CodeGroup>
  ```python title="Python"
  client.sessions.flush(session_id)
  # Limit to last 5 tasks
  summary = client.sessions.get_session_summary(session_id, limit=5)
  ```

  ```typescript title="TypeScript"
  await client.sessions.flush(sessionId);
  // Limit to last 5 tasks
  const summary = await client.sessions.getSessionSummary(sessionId, { limit: 5 });
  ```
</CodeGroup>

<Tip>
  Task extraction is async and may lag 1-6 messages. Keep the 4-6 most recent messages in full when using summary as compression.
</Tip>

## Next Steps [#next-steps]

<CardGroup cols="3">
  <Card title="Task Extraction" icon="list-check" href="/observe/agent_tasks">
    How tasks are extracted
  </Card>

  <Card title="Context Editing" icon="scissors" href="/engineering/editing">
    Manage context window size
  </Card>

  <Card title="Skill Memory" icon="brain" href="/learn/quick">
    Build skills from agent sessions
  </Card>
</CardGroup>


---

# What is Context Engineering?



Context Engineering is designing and optimizing the information provided to LLMs and AI agents. While prompt engineering focuses on specific inputs, context engineering covers the entire spectrum of contextual elements.

<Frame caption="https://blog.langchain.com/context-engineering-for-agents/">
  <img src="/images/context_editing.png" alt="Context editing process" />
</Frame>

## Acontext Features [#acontext-features]

* **[Context Editing](/engineering/editing)**: Manage context window size on-the-fly
* **[Session Summary](/engineering/session_summary)**: Get compact task summaries for prompts
* **[Agent Skills](/engineering/agent_skills)**: Empower agents with reusable skills

<Frame caption="Acontext Context Editing">
  <img src="/images/acontext-context-editing.png" alt="Context editing diagram" />
</Frame>

## Further Reading [#further-reading]

* [Context Editing by Anthropic](https://platform.claude.com/docs/en/build-with-claude/context-editing)
* [Context Engineering Handbook](https://github.com/davidkimai/Context-Engineering)
* [Context Engineering by Langgraph](https://blog.langchain.com/context-engineering-for-agents/)


---

# Async Python Client



You can use async python client:

```python title="Python"
from acontext import AcontextAsyncClient

client = AcontextAsyncClient(
    api_key=os.getenv("ACONTEXT_API_KEY"),
)

# If you're using self-hosted Acontext:
# client = AcontextAsyncClient(
#     base_url="http://localhost:8029/api/v1",
#     api_key="sk-ac-your-root-api-bearer-token",
# )
print(await client.ping())
session = await client.sessions.create()
# ...
```

Every method is available in async client, just add `await` prefix to the method call.

## Async Agentic Tools [#async-agentic-tools]

The [Filesystem Tools](/tool/disk_tools), [Skill Tools](/tool/skill_tools), and [Sandbox Tools](/tool/bash_tools) also support async operations. Use `async_format_context()` and `async_execute_tool()` methods with `AcontextAsyncClient`.

### Async Disk Tools [#async-disk-tools]

```python title="Python"
import json
from acontext import AcontextAsyncClient
from acontext.agent.disk import DISK_TOOLS
from openai import AsyncOpenAI

# Initialize async clients
acontext_client = AcontextAsyncClient(
    api_key=os.getenv("ACONTEXT_API_KEY"),
)
openai_client = AsyncOpenAI()

# Create a disk and async tool context
disk = await acontext_client.disks.create()
ctx = await DISK_TOOLS.async_format_context(acontext_client, disk.id)

# Get tool schemas for OpenAI
tools = DISK_TOOLS.to_openai_tool_schema()

# Async agentic loop
messages = [
    {"role": "user", "content": "Create a todo.md file with 3 tasks"}
]

while True:
    response = await openai_client.chat.completions.create(
        model="gpt-4.1",
        messages=messages,
        tools=tools,
    )

    message = response.choices[0].message
    messages.append(message)

    if not message.tool_calls:
        print(f"Assistant: {message.content}")
        break

    # Execute each tool call asynchronously
    for tool_call in message.tool_calls:
        result = await DISK_TOOLS.async_execute_tool(
            ctx, tool_call.function.name, json.loads(tool_call.function.arguments)
        )
        messages.append(
            {"role": "tool", "tool_call_id": tool_call.id, "content": result}
        )
```

### Async Skill Tools [#async-skill-tools]

```python title="Python"
import json
from acontext import AcontextAsyncClient
from acontext.agent.skill import SKILL_TOOLS
from openai import AsyncOpenAI

# Initialize async clients
acontext_client = AcontextAsyncClient(
    api_key=os.getenv("ACONTEXT_API_KEY"),
)
openai_client = AsyncOpenAI()

# Preload skills and create async tool context
skill_ids = ["uuid-of-skill-1", "uuid-of-skill-2"]
ctx = await SKILL_TOOLS.async_format_context(acontext_client, skill_ids)

# Get tool schemas for OpenAI
tools = SKILL_TOOLS.to_openai_tool_schema()

# Async agentic loop
messages = [
    {"role": "user", "content": "List the available skills and read the SKILL.md from the first one"}
]

while True:
    response = await openai_client.chat.completions.create(
        model="gpt-4.1",
        messages=messages,
        tools=tools,
    )

    message = response.choices[0].message
    messages.append(message)

    if not message.tool_calls:
        print(f"Assistant: {message.content}")
        break

    # Execute each tool call asynchronously
    for tool_call in message.tool_calls:
        result = await SKILL_TOOLS.async_execute_tool(
            ctx, tool_call.function.name, json.loads(tool_call.function.arguments)
        )
        messages.append(
            {"role": "tool", "tool_call_id": tool_call.id, "content": result}
        )
```

### Async Sandbox Tools [#async-sandbox-tools]

```python title="Python"
import json
from acontext import AcontextAsyncClient
from acontext.agent.sandbox import SANDBOX_TOOLS
from openai import AsyncOpenAI

# Initialize async clients
acontext_client = AcontextAsyncClient(
    api_key=os.getenv("ACONTEXT_API_KEY"),
)
openai_client = AsyncOpenAI()

# Create sandbox and disk
sandbox = await acontext_client.sandboxes.create()
disk = await acontext_client.disks.create()

# Create async sandbox context (optionally mount skills)
ctx = await SANDBOX_TOOLS.async_format_context(
    acontext_client,
    sandbox_id=sandbox.sandbox_id,
    disk_id=disk.id,
    # mount_skills=["skill-uuid-1", "skill-uuid-2"]
)

# Get tool schemas for OpenAI
tools = SANDBOX_TOOLS.to_openai_tool_schema()

# Get context prompt to include in system message
context_prompt = ctx.get_context_prompt()

# Async agentic loop
messages = [
    {
        "role": "system",
        "content": f"You are a helpful assistant.\n\n{context_prompt}",
    },
    {"role": "user", "content": "Create a Python script and run it"}
]

while True:
    response = await openai_client.chat.completions.create(
        model="gpt-4.1",
        messages=messages,
        tools=tools,
    )

    message = response.choices[0].message
    messages.append(message)

    if not message.tool_calls:
        print(f"Assistant: {message.content}")
        break

    # Execute each tool call asynchronously
    for tool_call in message.tool_calls:
        result = await SANDBOX_TOOLS.async_execute_tool(
            ctx, tool_call.function.name, json.loads(tool_call.function.arguments)
        )
        messages.append(
            {"role": "tool", "tool_call_id": tool_call.id, "content": result}
        )

# Clean up
await acontext_client.sandboxes.kill(sandbox.sandbox_id)
```

<Tip>
  The async versions are identical to the sync versions except:

  * Use `AcontextAsyncClient` instead of `AcontextClient`
  * Use `await async_format_context()` instead of `format_context()`
  * Use `await async_execute_tool()` instead of `execute_tool()`
</Tip>


---

# Use Acontext Badge



![Made with Acontext](https://assets.memodb.io/Acontext/badge-made-with-acontext.svg)

```md title="Markdown"
[![Made with Acontext](https://assets.memodb.io/Acontext/badge-made-with-acontext.svg)](https://acontext.io)
```

![Made with Acontext (dark)](https://assets.memodb.io/Acontext/badge-made-with-acontext-dark.svg)

```md title="Markdown"
[![Made with Acontext](https://assets.memodb.io/Acontext/badge-made-with-acontext-dark.svg)](https://acontext.io)
```


---

# What is Skill Memory?



Skill memory is Acontext's approach to persistent agent memory: your agents store memory as **skill files** — plain Markdown organized by configurable schemas you control.

```
Session messages ──► Task extraction 
                        └► Learner ──► Skill files
                ┌─────────────────────┘
                ▼
                social-contacts/
                ├── SKILL.md          (your schema)
                ├── alice-chen.md     (learner-created)
                └── bob-martinez.md   (learner-created)
```

## Quickstart [#quickstart]

<Steps>
  <Step title="Create a learning space and run a session">
    <CodeGroup>
      ```python title="Python"
      import os
      from acontext import AcontextClient

      client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))

      # Create a learning space
      space = client.learning_spaces.create()

      # Create a session and associate it with the space
      session = client.sessions.create()
      client.learning_spaces.learn(space.id, session_id=session.id)

      # Run your agent as usual — store messages along the way
      client.sessions.store_message(session.id, blob={"role": "user", "content": "My name is Gus"})
      # ... agent runs and completes the task ...
      ```

      ```typescript title="TypeScript"
      import { AcontextClient } from '@acontext/acontext';

      const client = new AcontextClient({
          apiKey: process.env.ACONTEXT_API_KEY,
      });

      // Create a learning space
      const space = await client.learningSpaces.create();

      // Create a session and associate it with the space
      const session = await client.sessions.create();
      await client.learningSpaces.learn({ spaceId: space.id, sessionId: session.id });

      // Run your agent as usual — store messages along the way
      await client.sessions.storeMessage(session.id, { role: "user", content: "My name is Gus" });
      // ... agent runs and completes the task ...
      ```
    </CodeGroup>

    <Note>
      Call `.learn()` before the agent runs. When [tasks](/observe/agent_tasks) complete during the session, Acontext automatically picks them up for learning. A **task** is a unit of work the agent completes — Acontext extracts them from session messages automatically.
    </Note>
  </Step>

  <Step title="Learning happens automatically">
    As tasks complete, Acontext distills outcomes into skill files in the background. Use `wait_for_learning` to block until it finishes:

    <CodeGroup>
      ```python title="Python"
      result = client.learning_spaces.wait_for_learning(space.id, session_id=session.id)
      print(f"Session {result.session_id}: {result.status}")
      # "completed" or "failed"
      ```

      ```typescript title="TypeScript"
      const result = await client.learningSpaces.waitForLearning({
          spaceId: space.id,
          sessionId: session.id,
      });
      console.log(`Session ${result.session_id}: ${result.status}`);
      // "completed" or "failed"
      ```
    </CodeGroup>

    <Tip>
      `wait_for_learning` / `waitForLearning` polls every 1 second (configurable via `poll_interval` / `pollInterval`) and times out after 120 seconds by default (configurable via `timeout`).
    </Tip>
  </Step>

  <Step title="Inspect the learned skills">
    Skills are plain Markdown files you can read directly. You can also download all skill files to a local directory:

    <CodeGroup>
      ```python title="Python"
      # Ensure learning has finished (called in Step 2, repeated here for completeness)
      client.learning_spaces.wait_for_learning(space.id, session_id=session.id)

      skills = client.learning_spaces.list_skills(space.id)

      # Option A: Read file content via API
      for skill in skills:
          print(f"\n=== {skill.name} ===")
          for f in skill.file_index:
              content = client.skills.get_file(skill_id=skill.id, file_path=f.path)
              print(content.content.raw)

      # Option B: Download all skill files to local directory
      for skill in skills:
          client.skills.download(skill_id=skill.id, path=f"./skills/{skill.name}")
      ```

      ```typescript title="TypeScript"
      // Ensure learning has finished (called in Step 2, repeated here for completeness)
      await client.learningSpaces.waitForLearning({ spaceId: space.id, sessionId: session.id });

      const skills = await client.learningSpaces.listSkills(space.id);

      // Option A: Read file content via API
      for (const skill of skills) {
          console.log(`\n=== ${skill.name} ===`);
          for (const f of skill.fileIndex) {
              const content = await client.skills.getFile({ skillId: skill.id, filePath: f.path });
              console.log(content.content?.raw);
          }
      }

      // Option B: Download all skill files to local directory
      for (const skill of skills) {
          await client.skills.download(skill.id, { path: `./skills/${skill.name}` });
      }
      ```
    </CodeGroup>

    <Tip>
      Export skill files to run locally, in another agent, or with another LLM. No vendor lock-in; no re-embedding or migration step.
    </Tip>
  </Step>

  <Step title="Use learned skills in your agent">
    Give your agent access to the learned skills via [Skill Content Tools](/tool/skill_tools). The agent can then look up what it learned in previous sessions:

    <CodeGroup>
      ```python title="Python"
      from acontext.agent.skill import SKILL_TOOLS
      from openai import OpenAI
      import json

      openai_client = OpenAI()

      # Get skill IDs from the learning space
      skills = client.learning_spaces.list_skills(space.id)
      skill_ids = [s.id for s in skills]

      # Give the agent access to those skills
      ctx = SKILL_TOOLS.format_context(client, skill_ids)
      tools = SKILL_TOOLS.to_openai_tool_schema()

      messages = [
          {"role": "system", "content": f"You have access to skills from past sessions.\n\n{ctx.get_context_prompt()}"},
          {"role": "user", "content": "Do you know my name?"}
      ]

      while True:
          response = openai_client.chat.completions.create(
              model="gpt-4.1", messages=messages, tools=tools
          )
          message = response.choices[0].message
          messages.append(message)

          if not message.tool_calls:
              print(f"Assistant: {message.content}")
              break

          for tc in message.tool_calls:
              result = SKILL_TOOLS.execute_tool(ctx, tc.function.name, json.loads(tc.function.arguments))
              messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})
      ```

      ```typescript title="TypeScript"
      import { SKILL_TOOLS } from '@acontext/acontext';
      import OpenAI from 'openai';

      const openai = new OpenAI();

      // Get skill IDs from the learning space
      const skills = await client.learningSpaces.listSkills(space.id);
      const skillIds = skills.map(s => s.id);

      // Give the agent access to those skills
      const ctx = await SKILL_TOOLS.formatContext(client, skillIds);
      const tools = SKILL_TOOLS.toOpenAIToolSchema();

      const messages: OpenAI.ChatCompletionMessageParam[] = [
          { role: "system", content: `You have access to skills from past sessions.\n\n${ctx.getContextPrompt()}` },
          { role: "user", content: "Do you know my name?" },
      ];

      while (true) {
          const response = await openai.chat.completions.create({
              model: "gpt-4.1",
              messages,
              tools,
          });
          const message = response.choices[0].message;
          messages.push(message);

          if (!message.tool_calls) {
              console.log(`Assistant: ${message.content}`);
              break;
          }

          for (const tc of message.tool_calls) {
              const result = await SKILL_TOOLS.executeTool(ctx, tc.function.name, JSON.parse(tc.function.arguments));
              messages.push({ role: "tool", tool_call_id: tc.id, content: result });
          }
      }
      ```
    </CodeGroup>
  </Step>
</Steps>

## Why Skill Memory? [#why-skill-memory]

* **Plain Markdown, any framework** — Skill memories are Markdown files. Use them with LangGraph, Claude, AI SDK, or anything that reads files. No embeddings, no API lock-in. Git, grep, and mount to the sandbox.
* **You design the structure** — `SKILL.md` defines schema, naming, and file layout. Examples: one file per contact, per project, per runbook.
* **Tool-based recall, not embeddings** — The agent uses `get_skill` and `get_skill_file` to fetch what it needs. Retrieval is by tool use and reasoning, not semantic top-k. Full units (e.g. whole files), not chunked fragments.
* **Download as ZIP, reuse anywhere** — Export skill files as ZIP. Run locally, in another agent, or with another LLM. No vendor lock-in; no re-embedding or migration step.

<Accordion title="Comparison with other memory approaches">
  |                      | Skill Memory (Acontext) | Vector Store | Knowledge Graph | Plain-text Files |
  | -------------------- | ----------------------- | ------------ | --------------- | ---------------- |
  | Storage format       | Markdown files          | Embeddings   | Nodes & edges   | Text files       |
  | Human-readable       | Yes                     | No           | Partially       | Partially        |
  | Configurable schema  | Yes (SKILL.md)          | No           | Complex upfront | No               |
  | Filesystem-native    | Yes                     | No           | No              | Yes              |
  | Version controllable | Yes                     | No           | No              | No               |
</Accordion>

## Next Steps [#next-steps]

<CardGroup cols="3">
  <Card title="Custom Skills" icon="sparkles" href="/learn/custom-memory">
    Define your own memory schemas with SKILL.md
  </Card>

  <Card title="Learning Spaces" icon="brain" href="/learn/learning-spaces">
    Default skills, custom skills, and managing learning spaces
  </Card>

  <Card title="Skill Content Tools" icon="wand-magic-sparkles" href="/tool/skill_tools">
    Full reference for the skill reading tools
  </Card>
</CardGroup>


---

# What is Task Tracking?



Task tracking turns raw agent conversations into structured, queryable records of what the agent was asked to do, what it actually did, and whether it succeeded. You store messages as usual — Acontext handles the rest automatically. No tracking code, no manual annotations.

## Why Task Tracking? [#why-task-tracking]

When you run AI agents in production, you need answers to questions like:

* What tasks did my agent handle today?
* Which tasks succeeded? Which failed? Why?
* What steps did the agent take to complete each task?
* What user preferences or constraints were mentioned?

Task tracking gives you this observability out of the box. Every user request becomes a trackable task with status, progress, and metadata — all extracted automatically from the conversation.

## How It Works [#how-it-works]

```
Your Agent                         Acontext
    │                                  │
    │  store_message(msg)              │
    ├─────────────────────────────────►│  Messages saved to DB
    │  store_message(msg)              │
    ├─────────────────────────────────►│  Messages buffer accumulates
    │  store_message(msg)              │
    ├─────────────────────────────────►│
    │                                  │
    │           ┌──────────────────────┤  Buffer triggers (full or idle timeout)
    │           │  Task Agent          │
    │           │  (LLM-powered)       │
    │           │                      │
    │           │  • Reads messages    │
    │           │  • Extracts tasks    │
    │           │  • Records progress  │
    │           │  • Detects status    │
    │           └──────────────────────┤  Tasks written to DB
    │                                  │
    │  get_tasks(session_id)           │
    ├─────────────────────────────────►│  Returns structured tasks
    │◄─────────────────────────────────┤
```

### Step by step [#step-by-step]

1. **You store messages** — call `store_message()` in your agent loop. Messages are saved to the database via the API.

2. **Messages buffer** — Acontext batches messages before processing. The buffer flushes when it reaches a configured turn count (`buffer_max_turns`) or when no new messages arrive for 8 seconds. You can also call `flush()` to trigger processing immediately.

3. **The Task Agent runs** — a background LLM-powered agent in Acontext Core reads the buffered messages and performs structured analysis:
   * **Identifies user requests** — each distinct thing the user asks for becomes a separate task. The agent's sub-steps are recorded as progress within that task, not as separate tasks.
   * **Tracks progress** — specific, concrete steps the agent took (e.g., "Created login component in `src/Login.tsx`", "Navigated to `https://example.com`").
   * **Determines status** — `pending`, `running`, `success`, or `failed`, based on conversation signals or your [custom evaluation criteria](/observe/task_eval_criteria).
   * **Captures user preferences** — constraints and preferences mentioned in conversation (e.g., "The user prefers TypeScript", "The user deploys to AWS").

4. **Tasks are queryable** — retrieve structured tasks via SDK or view them in the Dashboard.

## What Gets Tracked [#what-gets-tracked]

Each extracted task contains:

| Field                | Description                          | Example                                                           |
| -------------------- | ------------------------------------ | ----------------------------------------------------------------- |
| **Task description** | The user's request, in their words   | "Deploy the new API to staging"                                   |
| **Status**           | Current state of the task            | `pending` → `running` → `success` or `failed`                     |
| **Progress**         | Step-by-step record of agent actions | "Built Docker image", "Pushed to registry", "Deployed to staging" |
| **User preferences** | Constraints or preferences mentioned | "Always run tests before deploying"                               |
| **Order**            | Sequential position in the session   | `1`, `2`, `3`                                                     |
| **Linked messages**  | Which messages belong to this task   | Message IDs mapped to the task                                    |

## Example [#example]

Your agent handles this conversation:

> **User:** Deploy the new API to staging\
> &#x2A;*Agent:** I'll build the Docker image first... Done. Now pushing to the registry... Pushed to `gcr.io/my-project/api:v2.1`. Deploying to the staging cluster... Deployment complete.

Acontext automatically extracts:

```
Task #1: "Deploy the new API to staging"
Status: success
Progress:
  1. Built Docker image from Dockerfile
  2. Pushed to registry at gcr.io/my-project/api:v2.1
  3. Deployed to staging cluster via kubectl
```

No tracking code needed — it came from the conversation.

## Quick Usage [#quick-usage]

<CodeGroup>
  ```python title="Python"
  import os
  from acontext import AcontextClient

  client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))
  session = client.sessions.create()

  # Store messages as your agent runs
  client.sessions.store_message(session.id, blob={"role": "user", "content": "Deploy the new API to staging"}, format="openai")
  client.sessions.store_message(session.id, blob={"role": "assistant", "content": "Building Docker image... Done. Pushed to registry. Deployed to staging."}, format="openai")

  # Force processing (or wait for buffer to flush automatically)
  client.sessions.flush(session.id)

  # Retrieve extracted tasks
  tasks = client.sessions.get_tasks(session.id)
  for task in tasks.items:
      print(f"Task #{task.order}: {task.data.task_description}")
      print(f"  Status: {task.status}")
      for p in (task.data.progresses or []):
          print(f"  - {p}")
  ```

  ```typescript title="TypeScript"
  import { AcontextClient } from '@acontext/acontext';

  const client = new AcontextClient({ apiKey: process.env.ACONTEXT_API_KEY });
  const session = await client.sessions.create();

  // Store messages as your agent runs
  await client.sessions.storeMessage(session.id, { role: "user", content: "Deploy the new API to staging" }, { format: "openai" });
  await client.sessions.storeMessage(session.id, { role: "assistant", content: "Building Docker image... Done. Pushed to registry. Deployed to staging." }, { format: "openai" });

  // Force processing (or wait for buffer to flush automatically)
  await client.sessions.flush(session.id);

  // Retrieve extracted tasks
  const tasks = await client.sessions.getTasks(session.id);
  for (const task of tasks.items) {
      console.log(`Task #${task.order}: ${task.data.task_description}`);
      console.log(`  Status: ${task.status}`);
      for (const p of (task.data.progresses || [])) {
          console.log(`  - ${p}`);
      }
  }
  ```
</CodeGroup>

## Disabling Task Tracking [#disabling-task-tracking]

For sessions where you don't need task extraction (testing, simple Q\&A, lightweight sub-agents), you can disable it per session:

<CodeGroup>
  ```python title="Python"
  session = client.sessions.create(disable_task_tracking=True)
  ```

  ```typescript title="TypeScript"
  const session = await client.sessions.create({ disableTaskTracking: true });
  ```
</CodeGroup>

Messages are still saved — only the automatic task extraction is skipped.

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Agent Tasks" icon="list-check" href="/observe/agent_tasks">
    Full SDK reference for extracting and querying tasks.
  </Card>

  <Card title="Session Buffer" icon="clock" href="/observe/buffer">
    How message batching and processing timing works.
  </Card>

  <Card title="Task Evaluation Criteria" icon="scale-balanced" href="/observe/task_eval_criteria">
    Define custom success and failure standards for your domain.
  </Card>

  <Card title="Dashboard" icon="chart-simple" href="/observe/dashboard">
    View messages, tasks, and analytics visually.
  </Card>
</CardGroup>


---

# Core Dependencies



## Basic Environment Variables [#basic-environment-variables]

Configure your acontext core services using these essential environment variables. All environment variables use uppercase field names corresponding to the configuration schema.

### LLM Configuration [#llm-configuration]

<ParamField path="LLM_API_KEY" type="string">
  API key for your LLM provider (OpenAI or Anthropic). This is the primary authentication credential for AI model access.
</ParamField>

<ParamField path="LLM_BASE_URL" type="string" default="null">
  Custom base URL for LLM API endpoints. Leave unset to use the provider's default endpoint.
</ParamField>

<ParamField path="LLM_SDK" type="string" default="openai">
  LLM provider to use. Supported values: `openai`, `anthropic`
</ParamField>

<ParamField path="LLM_SIMPLE_MODEL" type="string" default="gpt-4.1">
  Default model identifier for LLM operations. Examples: `gpt-4`, `gpt-3.5-turbo`, `claude-3-sonnet`
</ParamField>

<ParamField path="LLM_RESPONSE_TIMEOUT" type="float" default="60">
  Timeout in seconds for LLM API responses. Increase for longer operations.
</ParamField>

### Embedding Configuration [#embedding-configuration]

<ParamField path="BLOCK_EMBEDDING_PROVIDER" type="string" default="openai">
  Embedding provider for vector operations. Supported values: `openai`, `jina`
</ParamField>

<ParamField path="BLOCK_EMBEDDING_MODEL" type="string" default="text-embedding-3-small">
  Embedding model to use for generating vectors. Examples: `text-embedding-3-small`, `text-embedding-ada-002`
</ParamField>

<ParamField path="BLOCK_EMBEDDING_DIM" type="integer" default="1536">
  Dimension size for embedding vectors. Must match your chosen embedding model's output dimensions.
</ParamField>

<ParamField path="BLOCK_EMBEDDING_API_KEY" type="string" default="null">
  Separate API key for embedding service. If not set, uses `LLM_API_KEY`.
</ParamField>

<ParamField path="BLOCK_EMBEDDING_BASE_URL" type="string" default="null">
  Custom base URL for embedding API endpoints. Leave unset to use the provider's default.
</ParamField>

<ParamField path="BLOCK_EMBEDDING_SEARCH_COSINE_DISTANCE_THRESHOLD" type="float" default="0.8">
  Cosine distance threshold for embedding similarity searches. Lower values = more strict matching.
</ParamField>

<Warning>
  Be careful when choosing your embedding model. Changing the embedding model after data has been stored will require you to clean and rebuild your databases, as existing vector embeddings will be incompatible with the new model's output format and dimensions.
</Warning>

## `.env` Examples [#env-examples]

<CodeGroup>
  ```bash title=".env"
  # Required LLM Configuration
  LLM_API_KEY=sk-your-openai-api-key-here

  # Optional LLM Settings
  LLM_SDK=openai
  LLM_SIMPLE_MODEL=gpt-4
  LLM_RESPONSE_TIMEOUT=60

  # Embedding Configuration
  BLOCK_EMBEDDING_PROVIDER=openai
  BLOCK_EMBEDDING_MODEL=text-embedding-3-small
  BLOCK_EMBEDDING_DIM=1536
  BLOCK_EMBEDDING_SEARCH_COSINE_DISTANCE_THRESHOLD=0.8
  ```

  ```bash title="Anthropic Setup"
  # Using Anthropic Claude
  LLM_API_KEY=your-anthropic-api-key
  LLM_SDK=anthropic
  LLM_SIMPLE_MODEL=claude-3-sonnet-20240229

  # Keep OpenAI for embeddings (recommended)
  BLOCK_EMBEDDING_PROVIDER=openai
  BLOCK_EMBEDDING_API_KEY=sk-your-openai-key-for-embeddings
  ```

  ```bash title="Custom Endpoints"
  # Custom LLM endpoint (e.g., Azure OpenAI)
  LLM_API_KEY=your-azure-key
  LLM_BASE_URL=https://your-resource.openai.azure.com/
  LLM_SDK=openai

  # Custom embedding endpoint
  BLOCK_EMBEDDING_API_KEY=your-embedding-key
  BLOCK_EMBEDDING_BASE_URL=https://api.jina.ai/v1/embeddings
  BLOCK_EMBEDDING_PROVIDER=jina
  ```

  ```bash title="Local LLM (Ollama)"
  # Ollama server running locally
  LLM_API_KEY=dummy-key-not-required
  LLM_BASE_URL=http://localhost:11434/v1
  LLM_SDK=openai
  LLM_SIMPLE_MODEL=qwen3:8b

  # Local embedding with Ollama
  BLOCK_EMBEDDING_PROVIDER=openai
  BLOCK_EMBEDDING_API_KEY=dummy-key
  BLOCK_EMBEDDING_BASE_URL=http://localhost:11434/v1
  BLOCK_EMBEDDING_MODEL=qwen3-embedding:0.6b
  BLOCK_EMBEDDING_DIM=1024
  ```
</CodeGroup>

## Appendix [#appendix]

<AccordionGroup>
  <Accordion title="Ollama Setup Instructions">
    <Steps>
      <Step title="Install Ollama">
        Go to [Ollama](https://ollama.com/download) to download and install Ollama.
      </Step>

      <Step title="Start Ollama">
        ```bash
        # Pull and run a model
        ollama pull qwen3:8b
        ollama pull qwen3-embedding:0.6b
        ollama serve
        ```
      </Step>

      <Step title="Enable OpenAI compatibility">
        Ollama automatically provides OpenAI-compatible endpoints at `http://localhost:11434/v1`
      </Step>
    </Steps>

    <Info>
      Local LLM setups are perfect for development, privacy-sensitive applications, or when you want to avoid API costs. Ollama provides OpenAI-compatible APIs, making integration seamless.
    </Info>
  </Accordion>
</AccordionGroup>


---

# Run Acontext Locally



## Install `acontext-cli` [#install-acontext-cli]

```bash
curl -fsSL https://install.acontext.io | sh
```

## Start Acontext Server Locally [#start-acontext-server-locally]

Acontext requires at least an OpenAI API Key to Start.

```bash title=".env"
LLM_API_KEY="YOUR_OPENAI_API_KEY"
```

Place your `.env` file and run `acontext server up` at the same directory:

```bash server
mkdir acontext_server && cd acontext_server
acontext server up
```

`acontext server up` will create/use the existing `.env` and `config.yaml` for Acontext, and create a `db` folder to persist data.

Once it's done, you can access the following endpoints:

* Acontext Base URL: [http://localhost:8029/api/v1](http://localhost:8029/api/v1)
* Acontext Dashboard: [http://localhost:3000/](http://localhost:3000/)

<Info>
  To use different providers, check [Core Settings](/settings/core) for more details.
</Info>

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Dashboard" icon="chart-simple" href="/observe/dashboard">
    View your agent's tasks, conversations in one place
  </Card>

  <Card title="Store Messages" icon="message" href="/store/messages/multi-provider">
    Save agent conversations with context
  </Card>

  <Card title="Configuration" icon="gear" href="/settings/general">
    Configure Acontext to your needs
  </Card>
</CardGroup>


---

# Runtime



### Session Message Buffer [#session-message-buffer]

Acontext will track the agent task and user feedback in the session,
the following settings control how and when tracking is conducted.

<ParamField path="PROJECT_SESSION_MESSAGE_USE_PREVIOUS_MESSAGES_TURNS" type="integer" default="3">
  Number of previous message turns to include in the context when processing new incoming messages.

  Higher values provide more context for task maintaining but consume more tokens.
</ParamField>

<ParamField path="PROJECT_SESSION_MESSAGE_BUFFER_MAX_TURNS" type="integer" default="16">
  Maximum number of untracked message turns to keep in the session buffer.

  This controls the size of the messages we want to process at the same time, the larger the buffer,
  the longer context we can use for once for task maintaining,
  and the less total token costs.
</ParamField>

### Agent Iteration Limits [#agent-iteration-limits]

<ParamField path="DEFAULT_TASK_AGENT_MAX_ITERATIONS" type="integer" default="4">
  Maximum number of iterations a task agent can perform before stopping. Prevents infinite loops in task execution.
</ParamField>

## `.env` Examples [#env-examples]

<CodeGroup>
  ```bash title="Default Configuration"
  # Session Management
  PROJECT_SESSION_MESSAGE_USE_PREVIOUS_MESSAGES_TURNS=3
  PROJECT_SESSION_MESSAGE_BUFFER_MAX_TURNS=16
  PROJECT_SESSION_MESSAGE_BUFFER_MAX_OVERFLOW=16

  # Agent Limits
  DEFAULT_TASK_AGENT_MAX_ITERATIONS=4
  ```

  ```bash title="High-Context Setup"
  # Increased context for complex conversations
  PROJECT_SESSION_MESSAGE_USE_PREVIOUS_MESSAGES_TURNS=8
  PROJECT_SESSION_MESSAGE_BUFFER_MAX_TURNS=32
  PROJECT_SESSION_MESSAGE_BUFFER_MAX_OVERFLOW=24

  # More iterations for complex tasks
  DEFAULT_TASK_AGENT_MAX_ITERATIONS=8
  ```

  ```bash title="Performance Optimized"
  # Reduced context for faster processing
  PROJECT_SESSION_MESSAGE_USE_PREVIOUS_MESSAGES_TURNS=2
  PROJECT_SESSION_MESSAGE_BUFFER_MAX_TURNS=8
  PROJECT_SESSION_MESSAGE_BUFFER_MAX_OVERFLOW=8

  # Lower iteration limits for speed
  DEFAULT_TASK_AGENT_MAX_ITERATIONS=3
  ```
</CodeGroup>

<Warning>
  Setting iteration limits too high may lead to excessive API usage and longer response times. Setting them too low may prevent agents from completing complex tasks.
</Warning>

## Message Buffer Tuning [#message-buffer-tuning]

### Buffer Size Impact [#buffer-size-impact]

* **Small buffers** (8-16 turns):
  * ✅ **Lower update latency** - Tasks and skills update faster
  * ❌ **Higher token cost** - More frequent processing with less context sharing

* **Large buffers** (32+ turns):
  * ✅ **Lower token cost** - Batch processing with shared context
  * ❌ **Higher update latency** - Tasks and skills update less frequently

<Note>
  The buffer idle timeout is fixed at 8 seconds and is not configurable per project. The buffer flushes when it reaches `BUFFER_MAX_TURNS` or when no new messages arrive for 8 seconds, whichever comes first.
</Note>

<Tip>
  For **development**: Use smaller buffers (8-16) for faster feedback loops. For **production**: Use larger buffers (24-32) to optimize costs.
</Tip>


---

# What is Session Storage?



Acontext provides unified storage for everything your agent needs — messages, files, skills, and sandboxes — all accessible through one backend.

<CardGroup cols="2">
  <Card title="Messages" icon="messages" href="/store/messages/multi-provider">
    Store and retrieve LLM messages in OpenAI, Anthropic, or Gemini format with automatic conversion.
  </Card>

  <Card title="Disk" icon="hard-drive" href="/store/disk">
    S3-backed file storage with paths, metadata, and secure download URLs.
  </Card>

  <Card title="Agent Skills" icon="book" href="/store/skill">
    Upload and manage reusable skill packages that agents can discover and use.
  </Card>

  <Card title="Sandbox" icon="terminal" href="/store/sandbox">
    Isolated containers for executing shell commands and running code securely.
  </Card>

  <Card title="Per-User Resources" icon="users" href="/store/per-user">
    Associate sessions, disks, and skills with user identifiers for multi-tenant apps.
  </Card>

  <Card title="End-to-End Encryption" icon="lock" href="/security/encryption">
    Enable per-project encryption so stored data is encrypted with a key only you possess.
  </Card>
</CardGroup>


---

# What is Agent Tool?



Instead of writing custom tools based on Acontext low-level SDKs,
Acontext provides ready-to-use tools that integrate seamlessly with popular LLM providers.

## Available Tool Sets [#available-tool-sets]

| Tool Set                          | Description                                |
| --------------------------------- | ------------------------------------------ |
| [Sandbox Tools](/tool/bash_tools) | Execute code, edit files, export results   |
| [Disk Tools](/tool/disk_tools)    | Read and write files to persistent storage |
| [Skill Tools](/tool/skill_tools)  | Access agent skills and knowledge          |

## How It Works [#how-it-works]

<Mermaid
  chart="flowchart LR
    A[LLM] -->|Tool Call| B[Agent Tools]
    B -->|Execute| C[Sandbox/Disk/Skills]
    C -->|Result| B
    B -->|Response| A"
/>

## Quick Example [#quick-example]

<Steps>
  <Step title="Init resources">
    <CodeGroup>
      ```python title="Python"
      from acontext import AcontextClient
      from acontext.agent.sandbox import SANDBOX_TOOLS

      client = AcontextClient()
      sandbox = client.sandboxes.create()
      disk = client.disks.create()
      ```

      ```typescript title="TypeScript"
      import { AcontextClient, SANDBOX_TOOLS } from '@acontext/acontext';

      const client = new AcontextClient();
      const sandbox = await client.sandboxes.create();
      const disk = await client.disks.create();
      ```
    </CodeGroup>
  </Step>

  <Step title="Create context for this session">
    <CodeGroup>
      ```python title="Python"
      ctx = SANDBOX_TOOLS.format_context(client, sandbox.sandbox_id, disk.id)
      ```

      ```typescript title="TypeScript"
      const ctx = await SANDBOX_TOOLS.formatContext(client, sandbox.sandbox_id, disk.id);
      ```
    </CodeGroup>
  </Step>

  <Step title="Inject prompt">
    <CodeGroup>
      ```python title="Python"
      context_prompt = ctx.get_context_prompt()
      messages = [
          {"role": "system", "content": f"You have sandbox access.\n\n{context_prompt}"},
          {"role": "user", "content": "Run a hello world script"}
      ]
      ```

      ```typescript title="TypeScript"
      const contextPrompt = ctx.getContextPrompt();
      const messages = [
          { role: "system", content: `You have sandbox access.\n\n${contextPrompt}` },
          { role: "user", content: "Run a hello world script" }
      ];
      ```
    </CodeGroup>
  </Step>

  <Step title="Pass tools to LLM">
    <CodeGroup>
      ```python title="Python"
      tools = SANDBOX_TOOLS.to_openai_tool_schema()
      response = openai_client.chat.completions.create(
          model="gpt-4.1", messages=messages, tools=tools
      )
      ```

      ```typescript title="TypeScript"
      const tools = SANDBOX_TOOLS.toOpenAIToolSchema();
      const response = await openai.chat.completions.create({
          model: "gpt-4.1", messages, tools
      });
      ```
    </CodeGroup>
  </Step>

  <Step title="Execute tools">
    <CodeGroup>
      ```python title="Python"
      for tc in response.choices[0].message.tool_calls:
          result = SANDBOX_TOOLS.execute_tool(ctx, tc.function.name, json.loads(tc.function.arguments))
      ```

      ```typescript title="TypeScript"
      for (const tc of response.choices[0].message.tool_calls) {
          const result = await SANDBOX_TOOLS.executeTool(ctx, tc.function.name, JSON.parse(tc.function.arguments));
      }
      ```
    </CodeGroup>
  </Step>
</Steps>

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Sandbox Tools" icon="terminal" href="/tool/bash_tools">
    Execute code in isolated containers
  </Card>

  <Card title="Disk Tools" icon="hard-drive" href="/tool/disk_tools">
    Persistent file storage for agents
  </Card>

  <Card title="Skill Tools" icon="wand-magic-sparkles" href="/tool/skill_tools">
    Access reusable agent skills
  </Card>

  <Card title="Agent Skills" icon="book" href="/store/skill">
    Create and manage skills
  </Card>
</CardGroup>


---

# Agno



Agno's message format is OpenAI-compatible, so it works directly with Acontext.

## Quick Start [#quick-start]

```bash
acontext create my-agno-project --template-path "python/agno-basic"
```

<Note>
  Install CLI first: `curl -fsSL https://install.acontext.io | sh`
</Note>

## Manual Setup [#manual-setup]

<Steps>
  <Step title="Install dependencies">
    ```bash
    pip install agno acontext python-dotenv
    ```
  </Step>

  <Step title="Configure environment">
    ```bash
    OPENAI_API_KEY=your_openai_key_here
    ACONTEXT_API_KEY=sk-ac-your-api-key
    ```
  </Step>

  <Step title="Run agent with Acontext">
    <Accordion title="Complete example">
      ```python
      import os
      from agno.agent import Agent
      from agno.models.openai import OpenAIChat
      from agno.tools import tool
      from acontext import AcontextClient

      client = AcontextClient(
          api_key=os.getenv("ACONTEXT_API_KEY"),
      )

      # If you're using self-hosted Acontext:
      # client = AcontextClient(
      #     base_url="http://localhost:8029/api/v1",
      #     api_key="sk-ac-your-root-api-bearer-token",
      # )

      @tool
      def get_weather(city: str) -> str:
          """Returns weather info for the specified city."""
          return f"The weather in {city} is sunny"

      agent = Agent(
          name="Assistant",
          model=OpenAIChat(id="gpt-4"),
          instructions="You are a helpful assistant",
          tools=[get_weather],
      )

      # Create session
      session = client.sessions.create()
      conversation = []

      # User message
      user_msg = {"role": "user", "content": "Plan a 3-day trip to Finland"}
      conversation.append(user_msg)
      client.sessions.store_message(session_id=session.id, blob=user_msg)

      # Run agent
      response = agent.run(conversation)

      # Store response
      assistant_msg = {"role": "assistant", "content": response.content}
      conversation.append(assistant_msg)
      client.sessions.store_message(session_id=session.id, blob=assistant_msg)

      # Extract tasks
      client.sessions.flush(session.id)
      tasks = client.sessions.get_tasks(session.id)

      for task in tasks.items:
          print(f"Task: {task.data.task_description} | Status: {task.status}")
      ```
    </Accordion>
  </Step>
</Steps>

## Resume Sessions [#resume-sessions]

```python
messages = client.sessions.get_messages(session_id)
conversation = messages.items

conversation.append({"role": "user", "content": "Continue from where we left off"})
response = agent.run(conversation)
```

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Task Tracking" icon="radar" href="/observe/agent_tasks">
    Monitor agent tasks and progress
  </Card>

  <Card title="Dashboard" icon="chart-simple" href="/observe/dashboard">
    View all interactions
  </Card>
</CardGroup>


---

# Claude Agent SDK



The `ClaudeAgentStorage` integration persists messages from the Claude Agent SDK to Acontext automatically. It stores **only** `UserMessage` and `AssistantMessage` in Anthropic format — `SystemMessage`, `ResultMessage`, and `StreamEvent` are used only for session-id resolution.

## Quick Start [#quick-start]

<Tabs>
  <Tab title="Python">
    <Steps>
      <Step title="Install dependencies">
        ```bash
        pip install claude-agent-sdk acontext python-dotenv
        ```
      </Step>

      <Step title="Configure environment">
        ```bash
        ANTHROPIC_API_KEY=your_anthropic_key_here
        ACONTEXT_API_KEY=sk-ac-your-api-key
        ```
      </Step>

      <Step title="Run agent with Acontext">
        ```python
        import asyncio
        from acontext import AcontextAsyncClient, ClaudeAgentStorage
        from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient

        async def main():
            acontext_client = AcontextAsyncClient(api_key="sk-ac-your-api-key")
            storage = ClaudeAgentStorage(client=acontext_client)

            options = ClaudeAgentOptions(
                extra_args={"replay-user-messages": None},  # include UserMessage in stream
            )
            async with ClaudeSDKClient(options=options) as claude_client:
                await claude_client.query("What is the capital of France?")
                async for message in claude_client.receive_response():
                    await storage.save_message(message)

        asyncio.run(main())
        ```

        <Tip>
          The `replay-user-messages` flag ensures `UserMessage` is included in the `receive_response()` stream so that both sides of the conversation are stored. Without it, only `AssistantMessage` appears in the stream and user messages won't be persisted.
        </Tip>
      </Step>
    </Steps>
  </Tab>

  <Tab title="TypeScript">
    <Steps>
      <Step title="Install dependencies">
        ```bash
        npm install @anthropic-ai/claude-agent-sdk @acontext/acontext
        ```
      </Step>

      <Step title="Configure environment">
        ```bash
        ANTHROPIC_API_KEY=your_anthropic_key_here
        ACONTEXT_API_KEY=sk-ac-your-api-key
        ```
      </Step>

      <Step title="Run agent with Acontext">
        ```typescript
        import { AcontextClient, ClaudeAgentStorage } from '@acontext/acontext';
        import { query } from '@anthropic-ai/claude-agent-sdk';

        const client = new AcontextClient({ apiKey: 'sk-ac-your-api-key' });
        const storage = new ClaudeAgentStorage({ client });

        const q = query({ prompt: 'What is the capital of France?', 
            options: {
              extraArgs: { "replay-user-messages": null }, // include UserMessage in stream
            }});
        for await (const message of q) {
          await storage.saveMessage(message);
        }
        ```
      </Step>
    </Steps>
  </Tab>
</Tabs>

## Session Handling [#session-handling]

### Auto-discovered session id [#auto-discovered-session-id]

By default, `ClaudeAgentStorage` discovers the session id from the Claude Agent SDK.

When the Claude session id is a valid UUID, Acontext **reuses it as the Acontext session id** (via `use_uuid`). This means the Acontext session and the Claude session share the same id, making it easy to correlate them across systems.

If the Claude stream provides a non-UUID session id, it is ignored and Acontext generates its own session id instead.

<Tabs>
  <Tab title="Python">
    ```python
    storage = ClaudeAgentStorage(client=acontext_client)

    # session_id is None until the init message arrives
    async for message in claude_client.receive_response():
        await storage.save_message(message)

    # After init: storage.session_id matches the Claude session id
    print(storage.session_id)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript
    const storage = new ClaudeAgentStorage({ client });

    // sessionId is null until the init message arrives
    for await (const message of q) {
      await storage.saveMessage(message);
    }

    // After init: storage.sessionId matches the Claude session id
    console.log(storage.sessionId);
    ```
  </Tab>
</Tabs>

### Explicit Acontext session [#explicit-acontext-session]

To correlate the Claude run with a specific Acontext session:

<Tabs>
  <Tab title="Python">
    ```python
    session = await acontext_client.sessions.create()
    storage = ClaudeAgentStorage(client=acontext_client, session_id=session.id)

    async for message in claude_client.receive_response():
        await storage.save_message(message)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript
    const session = await client.sessions.create();
    const storage = new ClaudeAgentStorage({ client, sessionId: session.id });

    for await (const message of q) {
      await storage.saveMessage(message);
    }
    ```
  </Tab>
</Tabs>

## Options [#options]

### Include thinking blocks [#include-thinking-blocks]

By default, `ThinkingBlock` content is omitted from stored messages. To include it as native thinking blocks:

<Tabs>
  <Tab title="Python">
    ```python
    storage = ClaudeAgentStorage(
        client=acontext_client,
        include_thinking=True,
    )
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript
    const storage = new ClaudeAgentStorage({
      client,
      includeThinking: true,
    });
    ```
  </Tab>
</Tabs>

Thinking blocks are stored as Anthropic `thinking` content blocks with their `signature` preserved. When retrieved in Anthropic or Gemini format, they round-trip as native `thinking` blocks. When retrieved in OpenAI format, thinking content is automatically downgraded to plain text.

The stored message `meta` will contain `"has_thinking": True` for observability.

### Custom error handling [#custom-error-handling]

By default, API errors during storage are logged and swallowed so your message loop is never interrupted. To customize:

<Tabs>
  <Tab title="Python">
    ```python
    storage = ClaudeAgentStorage(
        client=acontext_client,
        on_error=lambda exc, msg: print(f"Storage failed: {exc}"),
    )
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript
    const storage = new ClaudeAgentStorage({
      client,
      onError: (err, blob) => console.error('Storage failed:', err),
    });
    ```
  </Tab>
</Tabs>

### User identifier [#user-identifier]

Pass a `user` string to associate the Acontext session with a specific user. This value is forwarded to `sessions.create()` when the session is first created:

<Tabs>
  <Tab title="Python">
    ```python
    storage = ClaudeAgentStorage(
        client=acontext_client,
        user="alice@example.com",
    )
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript
    const storage = new ClaudeAgentStorage({
      client,
      user: 'alice@example.com',
    });
    ```
  </Tab>
</Tabs>

## What gets stored [#what-gets-stored]

| Message type         | Stored? | Notes                                                                                                                                                                                           |
| -------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **UserMessage**      | Yes     | `role: "user"`. String content wrapped as text block. `ToolUseBlock` skipped (invalid in user role).                                                                                            |
| **AssistantMessage** | Yes     | `role: "assistant"`. `model` stored in message `meta`. `ToolResultBlock` skipped (invalid in assistant role). If `error` field is set, its value is included in `meta.error` for observability. |
| **SystemMessage**    | No      | Used only for session-id resolution (`subtype == "init"` in Python, `session_id` field in TypeScript).                                                                                          |
| **ResultMessage**    | No      | Fallback session-id source.                                                                                                                                                                     |
| **StreamEvent**      | No      | Fallback session-id source.                                                                                                                                                                     |

## Learn and Download Skills [#learn-and-download-skills]

Acontext can automatically learn [skills](/learn/quick) from the stored messages — extracting patterns, user preferences, and domain knowledge into structured markdown files. You can then download these skills to your local filesystem:

<Tabs>
  <Tab title="Python">
    ```python
    # Create a learning space and learn from the session
    space = await acontext_client.learning_spaces.create()
    await acontext_client.learning_spaces.learn(space.id, session_id=storage.session_id)
    # your agent runs as usual...
    # ...


    # Download learned skills to local
    skills = await acontext_client.learning_spaces.list_skills(space.id)
    for skill in skills:
        result = await acontext_client.skills.download(skill_id=skill.id, path=f"./skills/{skill.name}")
        print(f"Downloaded {result.name} to {result.dir_path}")
        print(f"Files: {result.files}")
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript
    // Create a learning space and learn from the session
    const space = await client.learningSpaces.create();
    await client.learningSpaces.learn({ spaceId: space.id, sessionId: storage.sessionId });

    // your agent runs as usual...
    // ...

    // Download learned skills to local
    const skills = await client.learningSpaces.listSkills(space.id);
    for (const skill of skills) {
        const result = await client.skills.download(skill.id, { path: `./skills/${skill.name}` });
        console.log(`Downloaded ${result.name} to ${result.dirPath}`);
        console.log(`Files: ${result.files}`);
    }
    ```
  </Tab>
</Tabs>

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Anthropic format" icon="message" href="/store/messages/special/anthropic">
    Anthropic message storage details
  </Card>

  <Card title="Message meta" icon="tag" href="/store/messages/special/message-meta">
    Working with message metadata
  </Card>

  <Card title="Skill Memory" icon="brain" href="/learn/quick">
    Learn skills from agent sessions automatically
  </Card>

  <Card title="Dashboard" icon="chart-simple" href="/observe/dashboard">
    View all interactions
  </Card>
</CardGroup>


---

# OpenAI Agents SDK



The OpenAI Agents SDK handles tool execution automatically. Use `Converter.items_to_messages()` to convert to Acontext-compatible format.

## Quick Start [#quick-start]

```bash
acontext create my-agent-project --template-path "python/openai-agent-basic"
```

<Note>
  Install CLI first: `curl -fsSL https://install.acontext.io | sh`
</Note>

## Manual Setup [#manual-setup]

<Steps>
  <Step title="Install dependencies">
    ```bash
    pip install openai-agents acontext python-dotenv
    ```
  </Step>

  <Step title="Configure environment">
    ```bash
    OPENAI_API_KEY=your_openai_key_here
    ACONTEXT_API_KEY=sk-ac-your-api-key
    ```
  </Step>

  <Step title="Run agent with Acontext">
    <Accordion title="Complete example">
      ```python
      import asyncio
      import os
      from agents import Agent, Runner, OpenAIChatCompletionsModel, AsyncOpenAI, function_tool
      from agents.models.chatcmpl_converter import Converter
      from acontext import AcontextClient

      client = AcontextClient(
          api_key=os.getenv("ACONTEXT_API_KEY"),
      )

      # If you're using self-hosted Acontext:
      # client = AcontextClient(
      #     base_url="http://localhost:8029/api/v1",
      #     api_key="sk-ac-your-root-api-bearer-token",
      # )

      @function_tool
      def get_weather(city: str) -> str:
          """Returns weather info for the specified city."""
          return f"The weather in {city} is sunny"

      agent = Agent(
          name="Assistant",
          instructions="You are a helpful assistant",
          model=OpenAIChatCompletionsModel(
              model="gpt-4o-mini",
              openai_client=AsyncOpenAI(),
          ),
          tools=[get_weather],
      )

      async def main():
          session = client.sessions.create()

          # Run agent
          result = await Runner.run(agent, "Plan a 3-day trip to Finland")

          # Continue conversation
          user_msg_2 = {"role": "user", "content": "Check the weather there"}
          new_input = result.to_input_list() + [user_msg_2]
          result = await Runner.run(agent, new_input)

          # Convert and store to Acontext
          messages = Converter.items_to_messages(result.to_input_list())
          for msg in messages:
              client.sessions.store_message(session_id=session.id, blob=msg, format="openai")

          # Extract tasks
          client.sessions.flush(session.id)
          tasks = client.sessions.get_tasks(session.id)

          for task in tasks.items:
              print(f"Task: {task.data.task_description} | Status: {task.status}")

      if __name__ == "__main__":
          asyncio.run(main())
      ```
    </Accordion>
  </Step>
</Steps>

## Resume Sessions [#resume-sessions]

```python
from helper import message_to_input_items

messages = client.sessions.get_messages(session_id, format="openai")

# Convert back to Responses API format
conversation = []
for msg in messages.items:
    items = message_to_input_items(msg)
    conversation.extend(items)

conversation.append({"role": "user", "content": "Continue"})
result = await Runner.run(agent, conversation)
```

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Task Tracking" icon="radar" href="/observe/agent_tasks">
    Monitor agent tasks and progress
  </Card>

  <Card title="Dashboard" icon="chart-simple" href="/observe/dashboard">
    View all interactions
  </Card>
</CardGroup>


---

# Vercel AI SDK



The Vercel AI SDK provides a unified interface for multiple AI providers. Store conversations to Acontext for persistence and task extraction.

## Quick Start [#quick-start]

```bash
acontext create my-ai-project --template-path "typescript/vercel-ai-basic"
```

<Note>
  Install CLI first: `curl -fsSL https://install.acontext.io | sh`
</Note>

## Manual Setup [#manual-setup]

<Steps>
  <Step title="Install dependencies">
    ```bash
    npm install ai @ai-sdk/openai @acontext/acontext dotenv zod
    ```
  </Step>

  <Step title="Configure environment">
    ```bash
    OPENAI_API_KEY=your_openai_key_here
    ACONTEXT_API_KEY=sk-ac-your-api-key
    ```
  </Step>

  <Step title="Run agent with Acontext">
    <Accordion title="Complete example">
      ```typescript
      import { generateText, tool } from 'ai';
      import { createOpenAI } from '@ai-sdk/openai';
      import { z } from 'zod';
      import { AcontextClient } from '@acontext/acontext';
      import dotenv from 'dotenv';

      dotenv.config();

      const openaiProvider = createOpenAI({
        apiKey: process.env.OPENAI_API_KEY,
      });

      const client = new AcontextClient({
        apiKey: process.env.ACONTEXT_API_KEY,
      });

      // If you're using self-hosted Acontext:
      // const client = new AcontextClient({
      //     baseUrl: "http://localhost:8029/api/v1",
      //     apiKey: "sk-ac-your-root-api-bearer-token",
      // });

      const tools = {
        get_weather: tool({
          description: 'Returns weather info for the specified city.',
          inputSchema: z.object({
            city: z.string(),
          }),
          execute: async ({ city }) => `The weather in ${city} is sunny`,
        }),
      };

      async function main() {
        const session = await client.sessions.create();
        const model = openaiProvider('gpt-4o-mini');

        // User message
        const userMsg = { role: 'user', content: "What's the weather in Helsinki?" };
        await client.sessions.storeMessage(session.id, userMsg, { format: 'openai' });

        // Generate response
        const result = await generateText({
          model,
          messages: [userMsg],
          tools,
        });

        // Store response
        const assistantMsg = { role: 'assistant', content: result.text };
        await client.sessions.storeMessage(session.id, assistantMsg, { format: 'openai' });

        // Extract tasks
        await client.sessions.flush(session.id);
        const tasks = await client.sessions.getTasks(session.id);

        for (const task of tasks.items) {
          console.log(`Task: ${task.data.task_description} | Status: ${task.status}`);
        }
      }

      main().catch(console.error);
      ```
    </Accordion>
  </Step>
</Steps>

<Warning>
  Vercel AI SDK v5 only accepts 'user' and 'assistant' roles. Tool results must be converted to user messages marked as internal.
</Warning>

## Resume Sessions [#resume-sessions]

```typescript
const messages = await client.sessions.getMessages(sessionId, { format: 'openai' });
const conversation = messages.items;

conversation.push({ role: 'user', content: 'Continue' });
const result = await generateText({ model, messages: conversation, tools });
```

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Task Tracking" icon="radar" href="/observe/agent_tasks">
    Monitor agent tasks and progress
  </Card>

  <Card title="Dashboard" icon="chart-simple" href="/observe/dashboard">
    View all interactions
  </Card>
</CardGroup>


---

# OpenAI Python SDK



OpenAI's message format is natively compatible with Acontext. Store messages directly without conversion.

## Quick Start [#quick-start]

```bash
acontext create my-openai-project --template-path "python/openai-basic"
```

<Note>
  Install CLI first: `curl -fsSL https://install.acontext.io | sh`
</Note>

## Manual Setup [#manual-setup]

<Steps>
  <Step title="Install dependencies">
    ```bash
    pip install openai acontext python-dotenv
    ```
  </Step>

  <Step title="Configure environment">
    ```bash
    OPENAI_API_KEY=your_openai_key_here
    ACONTEXT_API_KEY=sk-ac-your-api-key
    ```
  </Step>

  <Step title="Run agent with Acontext">
    <Accordion title="Complete example">
      ```python
      import os
      import json
      from openai import OpenAI
      from acontext import AcontextClient

      openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

      client = AcontextClient(
          api_key=os.getenv("ACONTEXT_API_KEY"),
      )

      # If you're using self-hosted Acontext:
      # client = AcontextClient(
      #     base_url="http://localhost:8029/api/v1",
      #     api_key="sk-ac-your-root-api-bearer-token",
      # )

      tools = [
          {
              "type": "function",
              "function": {
                  "name": "get_weather",
                  "description": "Returns weather info for the specified city.",
                  "parameters": {
                      "type": "object",
                      "properties": {"city": {"type": "string"}},
                      "required": ["city"],
                  },
              },
          },
      ]

      def get_weather(city: str) -> str:
          return f"The weather in {city} is sunny"

      def execute_tool(name: str, args: dict) -> str:
          if name == "get_weather":
              return get_weather(**args)
          return f"Unknown tool: {name}"

      def run_agent(conversation: list) -> tuple[str, list]:
          messages = list(conversation)
          new_messages = []

          while True:
              response = openai_client.chat.completions.create(
                  model="gpt-4o-mini",
                  messages=messages,
                  tools=tools,
                  tool_choice="auto",
              )

              message = response.choices[0].message
              msg_dict = {"role": message.role, "content": message.content}

              if message.tool_calls:
                  msg_dict["tool_calls"] = [
                      {"id": tc.id, "type": "function", "function": {"name": tc.function.name, "arguments": tc.function.arguments}}
                      for tc in message.tool_calls
                  ]
                  messages.append(msg_dict)
                  new_messages.append(msg_dict)

                  for tc in message.tool_calls:
                      result = execute_tool(tc.function.name, json.loads(tc.function.arguments))
                      tool_msg = {"role": "tool", "tool_call_id": tc.id, "content": result}
                      messages.append(tool_msg)
                      new_messages.append(tool_msg)
              else:
                  new_messages.append(msg_dict)
                  return message.content, new_messages

      # Create session
      session = client.sessions.create()
      conversation = []

      # User message
      user_msg = {"role": "user", "content": "What's the weather in Helsinki?"}
      conversation.append(user_msg)
      client.sessions.store_message(session_id=session.id, blob=user_msg)

      # Run agent
      response_content, new_messages = run_agent(conversation)

      # Store all messages
      for msg in new_messages:
          conversation.append(msg)
          client.sessions.store_message(session_id=session.id, blob=msg)

      # Extract tasks
      client.sessions.flush(session.id)
      tasks = client.sessions.get_tasks(session.id)

      for task in tasks.items:
          print(f"Task: {task.data.task_description} | Status: {task.status}")
      ```
    </Accordion>
  </Step>
</Steps>

## Resume Sessions [#resume-sessions]

```python
messages = client.sessions.get_messages(session_id)
conversation = messages.items

conversation.append({"role": "user", "content": "Summarize our conversation"})
response = openai_client.chat.completions.create(model="gpt-4o-mini", messages=conversation)
```

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Task Tracking" icon="radar" href="/observe/agent_tasks">
    Monitor agent tasks and progress
  </Card>

  <Card title="Dashboard" icon="chart-simple" href="/observe/dashboard">
    View all interactions
  </Card>
</CardGroup>


---

# OpenAI TypeScript SDK



OpenAI's message format is natively compatible with Acontext. Store messages directly without conversion.

## Quick Start [#quick-start]

```bash
acontext create my-openai-project --template-path "typescript/openai-basic"
```

<Note>
  Install CLI first: `curl -fsSL https://install.acontext.io | sh`
</Note>

## Manual Setup [#manual-setup]

<Steps>
  <Step title="Install dependencies">
    ```bash
    npm install openai @acontext/acontext dotenv
    ```
  </Step>

  <Step title="Configure environment">
    ```bash
    OPENAI_API_KEY=your_openai_key_here
    ACONTEXT_API_KEY=sk-ac-your-api-key
    ```
  </Step>

  <Step title="Run agent with Acontext">
    <Accordion title="Complete example">
      ```typescript
      import OpenAI from 'openai';
      import { AcontextClient } from '@acontext/acontext';
      import dotenv from 'dotenv';

      dotenv.config();

      const openaiClient = new OpenAI({
        apiKey: process.env.OPENAI_API_KEY,
      });

      const client = new AcontextClient({
        apiKey: process.env.ACONTEXT_API_KEY,
      });

      // If you're using self-hosted Acontext:
      // const client = new AcontextClient({
      //     baseUrl: "http://localhost:8029/api/v1",
      //     apiKey: "sk-ac-your-root-api-bearer-token",
      // });

      const tools = [
        {
          type: 'function' as const,
          function: {
            name: 'get_weather',
            description: 'Returns weather info for the specified city.',
            parameters: {
              type: 'object',
              properties: { city: { type: 'string' } },
              required: ['city'],
            },
          },
        },
      ];

      function getWeather(city: string): string {
        return `The weather in ${city} is sunny`;
      }

      function executeTool(name: string, args: Record<string, any>): string {
        if (name === 'get_weather') return getWeather(args.city);
        return `Unknown tool: ${name}`;
      }

      async function runAgent(conversation: any[]): Promise<[string, any[]]> {
        const messages = [...conversation];
        const newMessages: any[] = [];

        while (true) {
          const response = await openaiClient.chat.completions.create({
            model: 'gpt-4o-mini',
            messages,
            tools,
            tool_choice: 'auto',
          });

          const message = response.choices[0].message;
          const msgDict: any = { role: message.role, content: message.content };

          if (message.tool_calls) {
            msgDict.tool_calls = message.tool_calls.map((tc) => ({
              id: tc.id,
              type: 'function',
              function: { name: tc.function.name, arguments: tc.function.arguments },
            }));
            messages.push(msgDict);
            newMessages.push(msgDict);

            for (const tc of message.tool_calls) {
              const result = executeTool(tc.function.name, JSON.parse(tc.function.arguments));
              const toolMsg = { role: 'tool' as const, tool_call_id: tc.id, content: result };
              messages.push(toolMsg);
              newMessages.push(toolMsg);
            }
          } else {
            newMessages.push(msgDict);
            return [message.content || '', newMessages];
          }
        }
      }

      async function main() {
        const session = await client.sessions.create();
        let conversation: any[] = [];

        // User message
        const userMsg = { role: 'user', content: "What's the weather in Helsinki?" };
        conversation.push(userMsg);
        await client.sessions.storeMessage(session.id, userMsg, { format: 'openai' });

        // Run agent
        const [responseContent, newMessages] = await runAgent(conversation);

        // Store all messages
        for (const msg of newMessages) {
          conversation.push(msg);
          await client.sessions.storeMessage(session.id, msg, { format: 'openai' });
        }

        // Extract tasks
        await client.sessions.flush(session.id);
        const tasks = await client.sessions.getTasks(session.id);

        for (const task of tasks.items) {
          console.log(`Task: ${task.data.task_description} | Status: ${task.status}`);
        }
      }

      main().catch(console.error);
      ```
    </Accordion>
  </Step>
</Steps>

## Resume Sessions [#resume-sessions]

```typescript
const messages = await client.sessions.getMessages(sessionId, { format: 'openai' });
const conversation = messages.items;

conversation.push({ role: 'user', content: 'Summarize our conversation' });
const response = await openaiClient.chat.completions.create({
  model: 'gpt-4o-mini',
  messages: conversation,
});
```

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Task Tracking" icon="radar" href="/observe/agent_tasks">
    Monitor agent tasks and progress
  </Card>

  <Card title="Dashboard" icon="chart-simple" href="/observe/dashboard">
    View all interactions
  </Card>
</CardGroup>


---

# End-to-End Encryption



Acontext supports per-project end-to-end encryption. Your data is encrypted with a master key embedded in your API key. The server never stores your API key or plaintext encryption key — only you can decrypt your data.

## How It Works [#how-it-works]

Acontext uses **envelope encryption** with AES-256-GCM:

1. Your API key contains two parts: an **auth secret** (for authentication) and an encrypted **master key** (for encryption)
2. The **master key** is your KEK (Key Encryption Key) — it is generated once when your project is created and never changes during key rotation
3. Each stored object gets its own **DEK** (Data Encryption Key), generated server-side
4. The DEK is encrypted (wrapped) with your master key before being stored
5. To decrypt, the system extracts the master key from your API key and unwraps the DEK

### API Key Format [#api-key-format]

Your API key has the format: `sk-ac-{base64url_encoded_token}`

The token body is a single base64url-encoded blob containing:

* **version byte** (`0x01`): Identifies the compact token format
* **auth secret** (16 bytes): Used for authentication (HMAC lookup + verification)
* **wrapped master key** (40 bytes): Your master key, encrypted with a wrapping key derived from the auth secret using AES Key Wrap (RFC 3394)

This is a **zero-knowledge architecture** — the server stores only encrypted DEKs and encrypted data. Your API key is never stored on the server. Only someone who possesses the API key can decrypt the data.

## Enable Encryption [#enable-encryption]

<Steps>
  <Step title="Generate and save an API key">
    Go to **Project Settings → API Keys** and generate a new API key. Click **Save to Browser** to store it in your browser's localStorage. This key is required for encryption operations.

    <Warning>
      Copy and store your API key somewhere safe. The server does not store it — if you lose it, encrypted data **cannot be recovered**.
    </Warning>
  </Step>

  <Step title="Ensure compact key format">
    Encryption requires the compact key format (76-character token body with embedded master key). All newly created projects use this format automatically. If your project uses a legacy key, rotate it first to upgrade.
  </Step>

  <Step title="Enable encryption">
    Go to **Project Settings → Encryption** tab and toggle **Enable Encryption**.
  </Step>

  <Step title="Confirm">
    Confirm the prompt. All existing project data will be encrypted. New data will be automatically encrypted going forward.
  </Step>
</Steps>

## Key Rotation [#key-rotation]

Key rotation replaces only the auth\_secret while preserving the same master key. This means **no S3 data needs to be re-encrypted** — rotation is fast regardless of project size.

<Steps>
  <Step title="Rotate your API key">
    Go to **Project Settings → API Keys** and click **Rotate**. Make sure your current API key is saved in the browser so the system can preserve the master key.
  </Step>

  <Step title="Confirm rotation">
    Confirm the prompt. The system will generate a new auth secret and re-wrap the same master key with a new wrapping key. No data is re-encrypted — only the API key token changes.
  </Step>

  <Step title="New key auto-saved">
    If you previously saved the key to your browser, the new key is automatically saved to localStorage.
  </Step>
</Steps>

## Important Notes [#important-notes]

<Warning>
  Your API key is **never stored on the server**. In the Dashboard, you can optionally save it to your browser's localStorage for convenience — but if you clear browser data or switch browsers, you will need to re-import it. When using SDKs or the API, you manage the key yourself.
</Warning>

<Note>
  Only **project owners** can enable or disable encryption for a project.
</Note>

<Warning>
  **Text search is not available** for encrypted projects. Encrypted data cannot be indexed for search.
</Warning>

<Note>
  **Deduplication is disabled** for encrypted projects. Each object is encrypted with a unique DEK, so identical content produces different ciphertext. This may increase storage usage.
</Note>

<Warning>
  If you lose your API key, your encrypted data **cannot be recovered**. The server has no copy of your key and no recovery mechanism — this is by design.
</Warning>

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Project Settings" icon="settings" href="/settings/general">
    Configure project settings
  </Card>

  <Card title="Per-User Sessions" icon="users" href="/store/per-user">
    Manage per-user session storage
  </Card>
</CardGroup>


---

# Custom Skills for Skill Memory



Every skill in a learning space has a `SKILL.md` that tells the learner how to organize information. By writing different SKILL.md files, you define different memory algorithms — each with its own rules for what to capture, how to name files, and how to structure entries.

## How It Works [#how-it-works]

When a session completes, Acontext's learning pipeline:

1. Reads each skill's `SKILL.md` to understand its purpose and rules
2. Decides which skills are relevant to the session outcome
3. Creates or updates files **following the instructions you wrote**

This means you control the memory format. The learner is just the executor.

## Example Skills [#example-skills]

### social-contacts [#social-contacts]

One file per person — captures who the user knows, relationship context, and preferences for interacting with them.

<Accordion title="SKILL.md">
  ```markdown
  ---
  name: "social-contacts"
  description: "Track people the user interacts with — one file per person"
  ---
  # Social Contacts

  Maintain a file for each person the user mentions or interacts with.
  Capture relationship context, communication preferences, and notable interactions.

  ## File Structure

  One file per person, named [first-last].md (lowercase, hyphenated).
  Create a new file the first time a person is mentioned; update it when new information surfaces.

  File format for [first-last].md:
    # [Full Name]
    ## Basics — Relationship, Role, Company
    ## Notes — notable facts, preferences, or context about this person

  ## Guidelines

  - One person per file — do not combine people
  - Only record information explicitly mentioned by the user
  - Update existing entries when corrections are provided
  - Keep entries factual and concise
  ```
</Accordion>

### project-runbooks [#project-runbooks]

One file per project — captures deployment steps, architecture decisions, and recurring issues.

<Accordion title="SKILL.md">
  ```markdown
  ---
  name: "project-runbooks"
  description: "Operational knowledge per project — deploy steps, architecture, common issues"
  ---
  # Project Runbooks

  Maintain a runbook for each project the user works on.
  Capture deployment procedures, architecture decisions, and solutions to recurring problems.

  ## File Structure

  One file per project, named [project-name].md (lowercase, hyphenated).

  File format for [project-name].md:
    # [Project Name]
    ## Architecture — key components, services, dependencies
    ## Deploy — step-by-step deployment procedure
    ## Common Issues — each issue with Symptom and Fix

  ## Guidelines

  - One project per file
  - Update deploy steps when procedures change — do not keep stale instructions
  - Add new issues as they are encountered and resolved
  - Keep architecture notes high-level — link to external docs for details
  ```
</Accordion>

### api-error-patterns [#api-error-patterns]

Accumulated patterns organized by error category — captures what went wrong and how to fix it.

<Accordion title="SKILL.md">
  ```markdown
  ---
  name: "api-error-patterns"
  description: "Reusable patterns for handling API errors — organized by category"
  ---
  # API Error Patterns

  Collect error-handling patterns discovered during development.
  Organize by error category so the agent can quickly find relevant solutions.

  ## File Structure

  One file per error category, named [category].md (e.g., auth.md, rate-limits.md, timeouts.md).

  File format for [category].md:
    # [Category] Errors
    ## [Pattern title] (date: YYYY-MM-DD) — with Symptom, Root Cause, Fix, Prevention

  ## Guidelines

  - Group related errors in the same file — do not create one file per error
  - Include the date so patterns can be reviewed for staleness
  - Focus on reusable patterns, not one-off bugs
  ```
</Accordion>

## Walkthrough: Using social-contacts [#walkthrough-using-social-contacts]

<Steps>
  <Step title="Create the SKILL.md file">
    Create a file called `SKILL.md` with the following content:

    ```markdown title="SKILL.md"
    ---
    name: "social-contacts"
    description: "Track people the user interacts with — one file per person"
    ---
    # Social Contacts

    Maintain a file for each person the user mentions or interacts with.

    ## File Structure

    One file per person, named [first-last].md (lowercase, hyphenated).
    Each file has: # [Full Name], ## Basics (Relationship, Role, Company), ## Notes.

    ## Guidelines

    - One person per file — do not combine people
    - Only record information explicitly mentioned by the user
    - Update existing entries when corrections are provided
    - Keep entries factual and concise
    ```
  </Step>

  <Step title="Zip and upload the skill">
    <CodeGroup>
      ```python title="Python"
      import os, zipfile, io
      from acontext import AcontextClient, FileUpload

      client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))

      # Read the SKILL.md you created
      with open("SKILL.md", "r") as f:
          skill_md = f.read()

      buf = io.BytesIO()
      with zipfile.ZipFile(buf, "w") as zf:
          zf.writestr("SKILL.md", skill_md)
      buf.seek(0)

      skill = client.skills.create(
          file=FileUpload(filename="social-contacts.zip", content=buf.read()),
      )
      print(f"Created skill: {skill.name} ({skill.id})")
      ```

      ```typescript title="TypeScript"
      import { AcontextClient, FileUpload } from '@acontext/acontext';
      import * as fs from 'fs';
      import JSZip from 'jszip';

      const client = new AcontextClient({
          apiKey: process.env.ACONTEXT_API_KEY,
      });

      const skillMd = fs.readFileSync("SKILL.md", "utf-8");

      const zip = new JSZip();
      zip.file("SKILL.md", skillMd);
      const zipBuffer = await zip.generateAsync({ type: "nodebuffer" });

      const skill = await client.skills.create({
          file: new FileUpload({ filename: "social-contacts.zip", content: zipBuffer }),
      });
      console.log(`Created skill: ${skill.name} (${skill.id})`);
      ```
    </CodeGroup>
  </Step>

  <Step title="Add the skill to a learning space">
    <CodeGroup>
      ```python title="Python"
      space = client.learning_spaces.create()
      client.learning_spaces.include_skill(space.id, skill_id=skill.id)

      skills = client.learning_spaces.list_skills(space.id)
      for s in skills:
          print(f"  {s.name}: {s.description}")
      ```

      ```typescript title="TypeScript"
      const space = await client.learningSpaces.create();
      await client.learningSpaces.includeSkill({ spaceId: space.id, skillId: skill.id });

      const skills = await client.learningSpaces.listSkills(space.id);
      for (const s of skills) {
          console.log(`  ${s.name}: ${s.description}`);
      }
      ```
    </CodeGroup>
  </Step>

  <Step title="Run a session with mock messages">
    <CodeGroup>
      ```python title="Python"
      session = client.sessions.create()
      client.learning_spaces.learn(space.id, session_id=session.id)

      messages = [
          {"role": "user", "content": "Set up a meeting with Alice Chen — she's our new product manager at Acme Corp. She prefers morning slots."},
          {"role": "assistant", "content": "I've scheduled a meeting with Alice Chen for tomorrow at 10 AM."},
          {"role": "user", "content": "Also remind me that Bob Martinez from the DevOps team helped us fix the staging deploy issue last week."},
          {"role": "assistant", "content": "Noted — Bob Martinez from DevOps helped resolve the staging deployment issue."},
      ]

      for msg in messages:
          client.sessions.store_message(session.id, blob=msg)
      ```

      ```typescript title="TypeScript"
      const session = await client.sessions.create();
      await client.learningSpaces.learn({ spaceId: space.id, sessionId: session.id });

      const messages = [
          { role: "user", content: "Set up a meeting with Alice Chen — she's our new product manager at Acme Corp. She prefers morning slots." },
          { role: "assistant", content: "I've scheduled a meeting with Alice Chen for tomorrow at 10 AM." },
          { role: "user", content: "Also remind me that Bob Martinez from the DevOps team helped us fix the staging deploy issue last week." },
          { role: "assistant", content: "Noted — Bob Martinez from DevOps helped resolve the staging deployment issue." },
      ];

      for (const msg of messages) {
          await client.sessions.storeMessage(session.id, msg);
      }
      ```
    </CodeGroup>

    <Note>
      Learning is asynchronous. After the session's tasks complete, the learner reads your `SKILL.md` and creates files like `alice-chen.md` and `bob-martinez.md` following your rules.
    </Note>
  </Step>

  <Step title="Inspect what the learner created">
    <CodeGroup>
      ```python title="Python"
      client.learning_spaces.wait_for_learning(space.id, session_id=session.id)

      skills = client.learning_spaces.list_skills(space.id)
      for s in skills:
          if s.name == "social-contacts":
              for f in s.file_index:
                  if f.path == "SKILL.md":
                      continue
                  print(f"\n--- {f.path} ---")
                  content = client.skills.get_file(skill_id=s.id, file_path=f.path)
                  print(content.content.raw)
      ```

      ```typescript title="TypeScript"
      await client.learningSpaces.waitForLearning({ spaceId: space.id, sessionId: session.id });

      const updatedSkills = await client.learningSpaces.listSkills(space.id);
      for (const s of updatedSkills) {
          if (s.name === "social-contacts") {
              for (const f of s.fileIndex) {
                  if (f.path === "SKILL.md") continue;
                  console.log(`\n--- ${f.path} ---`);
                  const content = await client.skills.getFile({ skillId: s.id, filePath: f.path });
                  console.log(content.content?.raw);
              }
          }
      }
      ```
    </CodeGroup>

    Expected output — files created by the learner following your SKILL.md rules:

    ```markdown title="alice-chen.md"
    # Alice Chen

    ## Basics
    - Relationship: colleague
    - Role: Product Manager
    - Company: Acme Corp

    ## Notes
    - Prefers morning meeting slots
    ```

    ```markdown title="bob-martinez.md"
    # Bob Martinez

    ## Basics
    - Relationship: colleague
    - Role: DevOps team

    ## Notes
    - Helped resolve a staging deployment issue
    ```
  </Step>
</Steps>

## Full Code [#full-code]

<AccordionGroup>
  <Accordion title="Python — complete runnable script">
    ```python
    import os, zipfile, io
    from acontext import AcontextClient, FileUpload

    client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))

    # 1. Create the SKILL.md and ZIP it
    skill_md = """---
    name: "social-contacts"
    description: "Track people the user interacts with — one file per person"
    ---
    # Social Contacts

    Maintain a file for each person the user mentions or interacts with.

    ## File Structure

    One file per person, named [first-last].md (lowercase, hyphenated).
    Each file has: # [Full Name], ## Basics (Relationship, Role, Company), ## Notes.

    ## Guidelines

    - One person per file — do not combine people
    - Only record information explicitly mentioned by the user
    - Update existing entries when corrections are provided
    - Keep entries factual and concise
    """

    buf = io.BytesIO()
    with zipfile.ZipFile(buf, "w") as zf:
        zf.writestr("SKILL.md", skill_md)
    buf.seek(0)

    skill = client.skills.create(
        file=FileUpload(filename="social-contacts.zip", content=buf.read()),
    )
    print(f"Created skill: {skill.name} ({skill.id})")

    # 2. Create a learning space and include the skill
    space = client.learning_spaces.create()
    client.learning_spaces.include_skill(space.id, skill_id=skill.id)

    skills = client.learning_spaces.list_skills(space.id)
    for s in skills:
        print(f"  {s.name}: {s.description}")

    # 3. Create a session and run mock messages
    session = client.sessions.create()
    client.learning_spaces.learn(space.id, session_id=session.id)

    messages = [
        {"role": "user", "content": "Set up a meeting with Alice Chen — she's our new product manager at Acme Corp. She prefers morning slots."},
        {"role": "assistant", "content": "I've scheduled a meeting with Alice Chen for tomorrow at 10 AM."},
        {"role": "user", "content": "Also remind me that Bob Martinez from the DevOps team helped us fix the staging deploy issue last week."},
        {"role": "assistant", "content": "Noted — Bob Martinez from DevOps helped resolve the staging deployment issue."},
    ]

    for msg in messages:
        client.sessions.store_message(session.id, blob=msg)

    # 4. Wait for learning pipeline, then inspect results
    client.learning_spaces.wait_for_learning(space.id, session_id=session.id)

    skills = client.learning_spaces.list_skills(space.id)
    for s in skills:
        if s.name == "social-contacts":
            for f in s.file_index:
                if f.path == "SKILL.md":
                    continue
                print(f"\n--- {f.path} ---")
                content = client.skills.get_file(skill_id=s.id, file_path=f.path)
                print(content.content.raw)
    ```
  </Accordion>

  <Accordion title="TypeScript — complete runnable script">
    ```typescript
    import { AcontextClient, FileUpload } from '@acontext/acontext';
    import JSZip from 'jszip';

    const client = new AcontextClient({
        apiKey: process.env.ACONTEXT_API_KEY,
    });

    // 1. Create the SKILL.md and ZIP it
    const skillMd = `---
    name: "social-contacts"
    description: "Track people the user interacts with — one file per person"
    ---
    # Social Contacts

    Maintain a file for each person the user mentions or interacts with.

    ## File Structure

    One file per person, named [first-last].md (lowercase, hyphenated).
    Each file has: # [Full Name], ## Basics (Relationship, Role, Company), ## Notes.

    ## Guidelines

    - One person per file — do not combine people
    - Only record information explicitly mentioned by the user
    - Update existing entries when corrections are provided
    - Keep entries factual and concise
    `;

    const zip = new JSZip();
    zip.file("SKILL.md", skillMd);
    const zipBuffer = await zip.generateAsync({ type: "nodebuffer" });

    const skill = await client.skills.create({
        file: new FileUpload({ filename: "social-contacts.zip", content: zipBuffer }),
    });
    console.log(`Created skill: ${skill.name} (${skill.id})`);

    // 2. Create a learning space and include the skill
    const space = await client.learningSpaces.create();
    await client.learningSpaces.includeSkill({ spaceId: space.id, skillId: skill.id });

    const skills = await client.learningSpaces.listSkills(space.id);
    for (const s of skills) {
        console.log(`  ${s.name}: ${s.description}`);
    }

    // 3. Create a session and run mock messages
    const session = await client.sessions.create();
    await client.learningSpaces.learn({ spaceId: space.id, sessionId: session.id });

    const messages = [
        { role: "user", content: "Set up a meeting with Alice Chen — she's our new product manager at Acme Corp. She prefers morning slots." },
        { role: "assistant", content: "I've scheduled a meeting with Alice Chen for tomorrow at 10 AM." },
        { role: "user", content: "Also remind me that Bob Martinez from the DevOps team helped us fix the staging deploy issue last week." },
        { role: "assistant", content: "Noted — Bob Martinez from DevOps helped resolve the staging deployment issue." },
    ];

    for (const msg of messages) {
        await client.sessions.storeMessage(session.id, msg);
    }

    // 4. Wait for learning pipeline, then inspect results
    await client.learningSpaces.waitForLearning({ spaceId: space.id, sessionId: session.id });

    const updatedSkills = await client.learningSpaces.listSkills(space.id);
    for (const s of updatedSkills) {
        if (s.name === "social-contacts") {
            for (const f of s.fileIndex) {
                if (f.path === "SKILL.md") continue;
                console.log(`\n--- ${f.path} ---`);
                const content = await client.skills.getFile({ skillId: s.id, filePath: f.path });
                console.log(content.content?.raw);
            }
        }
    }
    ```
  </Accordion>
</AccordionGroup>

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Agent Skills" icon="sparkles" href="/store/skill">
    SKILL.md format and ZIP structure
  </Card>

  <Card title="Learning Spaces" icon="brain" href="/learn/learning-spaces">
    Default skills, custom skills, and managing learning spaces
  </Card>
</CardGroup>


---

# End-to-End Auto-Learn Demo



This guide walks through the full Acontext auto-learn cycle using a **DevOps incident resolution** scenario. By the end, you'll see an agent that:

1. Diagnoses a 502 gateway error using multiple tool calls
2. Has its session automatically distilled into a reusable SOP (skill)
3. Applies that SOP to a **new** incident — resolving it faster with fewer tool calls

```
ACT 1: Agent investigates 502 errors (no prior knowledge)
  └► 7+ tool calls: check status, read logs, read config, update, restart, verify
  └► Task extracted → Skill distilled → SOP written

ACT 2: Similar 502 incident (with learned skill)
  └► Agent reads skill first → goes straight to the fix
  └► 3-4 tool calls: targeted diagnosis + fix
```

## Prerequisites [#prerequisites]

* `ACONTEXT_API_KEY` — get one at [acontext.io](https://acontext.io)
* `OPENAI_API_KEY` — for the OpenAI agent loop
* Python SDK: `pip install acontext openai`
* TypeScript SDK: `npm install @acontext/acontext openai`

## The Mock Environment [#the-mock-environment]

The demo uses five mock DevOps tools that operate against an in-memory state — no real infrastructure needed:

| Tool                   | Purpose                     |
| ---------------------- | --------------------------- |
| `check_service_status` | Health, error rate, latency |
| `check_logs`           | Recent log entries          |
| `read_config`          | Service configuration       |
| `update_config`        | Mutate a config value       |
| `restart_service`      | Restart and clear errors    |

<Accordion title="Mock tool schemas (OpenAI function calling format)">
  ```json
  [
    {
      "type": "function",
      "function": {
        "name": "check_service_status",
        "description": "Check the health status, error rate, and latency of a service.",
        "parameters": {
          "type": "object",
          "properties": {
            "service_name": { "type": "string", "description": "Name of the service to check." }
          },
          "required": ["service_name"]
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "check_logs",
        "description": "Retrieve recent log entries for a service.",
        "parameters": {
          "type": "object",
          "properties": {
            "service_name": { "type": "string", "description": "Name of the service." },
            "lines": { "type": "integer", "description": "Number of recent log lines (max 20)." }
          },
          "required": ["service_name"]
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "read_config",
        "description": "Read the current configuration of a service.",
        "parameters": {
          "type": "object",
          "properties": {
            "service_name": { "type": "string", "description": "Name of the service." }
          },
          "required": ["service_name"]
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "update_config",
        "description": "Update a configuration value for a service.",
        "parameters": {
          "type": "object",
          "properties": {
            "service_name": { "type": "string", "description": "Name of the service." },
            "key": { "type": "string", "description": "Configuration key to update." },
            "value": { "type": "string", "description": "New value." }
          },
          "required": ["service_name", "key", "value"]
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "restart_service",
        "description": "Restart a service to apply configuration changes.",
        "parameters": {
          "type": "object",
          "properties": {
            "service_name": { "type": "string", "description": "Name of the service to restart." }
          },
          "required": ["service_name"]
        }
      }
    }
  ]
  ```
</Accordion>

## Act 1 — The Learning Run [#act-1--the-learning-run]

The agent has **no prior knowledge**. It must explore, diagnose, and fix a 502 gateway error from scratch.

**Scenario**: The API gateway connects to six services (auth, backend-api, notifications, redis, database). The `upstream_timeout` is set to 5 seconds, but the backend-api's p99 latency is 4.2 seconds. Gateway logs show 502 errors and slow upstream warnings — but don't reveal the configured timeout value. The notification service is also degraded with SMTP failures (a convincing red herring).

### Step 1: Set up learning space and session [#step-1-set-up-learning-space-and-session]

<CodeGroup>
  ```python title="Python"
  space = ac.learning_spaces.create()
  session = ac.sessions.create()
  ac.learning_spaces.learn(space.id, session_id=session.id)
  ```

  ```typescript title="TypeScript"
  const space = await ac.learningSpaces.create();
  const session = await ac.sessions.create();
  await ac.learningSpaces.learn({ spaceId: space.id, sessionId: session.id });
  ```
</CodeGroup>

### Step 2: Run the agent and record messages [#step-2-run-the-agent-and-record-messages]

Run your OpenAI agent loop as usual. The key is storing every message (user, assistant, tool calls, tool results) to Acontext via `store_message`:

<CodeGroup>
  ```python title="Python"
  ac.sessions.store_message(session.id, blob={"role": "user", "content": "..."})
  # ... agent calls tools, produces responses ...
  ac.sessions.store_message(session.id, blob={"role": "assistant", "tool_calls": [...]})
  ac.sessions.store_message(session.id, blob={"role": "tool", "tool_call_id": "...", "content": "..."})
  ```

  ```typescript title="TypeScript"
  await ac.sessions.storeMessage(session.id, { role: "user", content: "..." });
  // ... agent calls tools, produces responses ...
  await ac.sessions.storeMessage(session.id, { role: "assistant", tool_calls: [...] });
  await ac.sessions.storeMessage(session.id, { role: "tool", tool_call_id: "...", content: "..." });
  ```
</CodeGroup>

**What the agent typically does** (9-12 tool calls — investigating + fixing):

```
 1. check_service_status("api-gateway")         → unhealthy, 34% error rate
 2. check_logs("api-gateway")                   → 502s, slow upstream, notification-service failures
 3. check_service_status("notification-service") → degraded, 5% errors (investigates red herring)
 4. check_logs("notification-service")           → SMTP connection refused (unrelated to 502s)
 5. check_service_status("backend-api")          → healthy, but p99 = 4200ms
 6. check_logs("backend-api")                    → requests taking 4000-4900ms, slow DB queries
 7. read_config("api-gateway")                   → upstream_timeout_ms: 5000 — aha!
 8. update_config("api-gateway", "upstream_timeout_ms", "10000")
 9. restart_service("api-gateway")
10. check_service_status("api-gateway")          → healthy, 0% error rate ✓
```

### Step 3: Flush and inspect extracted tasks [#step-3-flush-and-inspect-extracted-tasks]

<CodeGroup>
  ```python title="Python"
  ac.sessions.flush(session.id)

  # Poll until all messages are processed
  status = ac.sessions.messages_observing_status(session.id)
  # status.pending / status.in_process → wait until both are 0

  result = ac.sessions.get_tasks(session.id)
  # result.items → list of Task objects
  # result.items[0].status → "success"
  # result.items[0].data.task_description → "Investigate and fix 502 errors..."
  ```

  ```typescript title="TypeScript"
  await ac.sessions.flush(session.id);

  const status = await ac.sessions.messagesObservingStatus(session.id);
  // status.pending / status.in_process → wait until both are 0

  const result = await ac.sessions.getTasks(session.id);
  // result.items → list of Task objects
  // result.items[0].status → "success"
  ```
</CodeGroup>

**Example extracted task:**

```
[success] Diagnose and fix 502 errors on the API gateway
  > Gateway logs show 502s from backend-api, also notification-service health failures
  > Investigated notification-service: SMTP issue, unrelated to 502 errors
  > backend-api healthy but p99 latency 4200ms; gateway config upstream_timeout_ms=5000
  > Root cause: backend-api latency exceeds gateway timeout threshold
  > Increased upstream_timeout_ms to 10000, restarted gateway → 0% error rate
```

### Step 4: Wait for skill learning and inspect results [#step-4-wait-for-skill-learning-and-inspect-results]

<CodeGroup>
  ```python title="Python"
  result = ac.learning_spaces.wait_for_learning(space.id, session_id=session.id)
  # result.status → "completed"

  skills = ac.learning_spaces.list_skills(space.id)
  for s in skills:
      for f in s.file_index:
          content = ac.skills.get_file(skill_id=s.id, file_path=f.path)
  ```

  ```typescript title="TypeScript"
  const result = await ac.learningSpaces.waitForLearning({
      spaceId: space.id, sessionId: session.id,
  });

  const skills = await ac.learningSpaces.listSkills(space.id);
  for (const s of skills) {
      for (const f of s.fileIndex) {
          const content = await ac.skills.getFile({ skillId: s.id, filePath: f.path });
      }
  }
  ```
</CodeGroup>

**Example generated skill** (auto-created by the learning pipeline):

```markdown title="SKILL.md"
---
name: "devops-incident-patterns"
description: "Reusable patterns for diagnosing and resolving infrastructure incidents"
---
# DevOps Incident Patterns

Collected SOPs and warnings from past incident resolutions.

## File Structure

One file per incident category (e.g., gateway-errors.md, database-issues.md).

## Guidelines

- One category per file
- Include date, symptoms, and numbered resolution steps
- Update existing entries when better approaches are discovered
```

```markdown title="gateway-errors.md"
## API Gateway 502 — Upstream Timeout Mismatch (date: 2026-03-26)
- Principle: 502 errors from a reverse proxy often indicate the upstream
  timeout is lower than the backend's actual response time.
- When to Apply: API gateway returning 502s with upstream timeout errors in logs.
- Steps:
  1. Check gateway logs for "upstream timeout" errors — note which backend service
  2. Compare gateway's upstream_timeout_ms config against the backend's p99 latency
  3. If p99 > timeout, increase timeout to at least 3-5x the p99 latency
  4. Restart the gateway to apply the new config
  5. Verify error rate drops to 0%
```

<Note>
  The exact skill output varies between runs since the learning pipeline uses LLM-based distillation. The structure and content will be similar, but wording may differ.
</Note>

## Act 2 — The Recall Run [#act-2--the-recall-run]

Now a **new** incident occurs after a deployment. The freshly added `payment-service` has a p99 latency of 45 seconds, exceeding the gateway's 30-second timeout. The user doesn't know which service is causing it — they just see 502s again.

The agent has access to the skill learned in Act 1 via [Skill Content Tools](/tool/skill_tools).

### Step 5: Run the agent with learned skills [#step-5-run-the-agent-with-learned-skills]

<CodeGroup>
  ```python title="Python"
  from acontext.agent.skill import SKILL_TOOLS

  session2 = ac.sessions.create()
  ac.learning_spaces.learn(space.id, session_id=session2.id)

  skill_ids = [s.id for s in skills]
  skill_ctx = SKILL_TOOLS.format_context(ac, skill_ids)

  # Add skill tools alongside your existing agent tools
  tools = your_tools + SKILL_TOOLS.to_openai_tool_schema()
  system = "..." + skill_ctx.get_context_prompt()
  ```

  ```typescript title="TypeScript"
  import { SKILL_TOOLS } from '@acontext/acontext';

  const session2 = await ac.sessions.create();
  await ac.learningSpaces.learn({ spaceId: space.id, sessionId: session2.id });

  const skillIds = skills.map(s => s.id);
  const skillCtx = await SKILL_TOOLS.formatContext(ac, skillIds);

  const tools = [...yourTools, ...SKILL_TOOLS.toOpenAIToolSchema()];
  const system = "..." + skillCtx.getContextPrompt();
  ```
</CodeGroup>

**What the agent typically does** (4-6 DevOps tool calls — skips exploration):

```
1. get_skill(...)                → reads the SOP from Act 1
2. get_skill_file(...)           → learns the upstream-timeout diagnostic pattern
3. check_logs("api-gateway")     → "upstream timeout: payment-service...within 30000 ms" — pattern match!
4. read_config("api-gateway")    → upstream_timeout_ms: 30000
5. update_config("api-gateway", "upstream_timeout_ms", "60000")
6. restart_service("api-gateway")
7. check_service_status("api-gateway") → healthy, 0% error rate ✓
```

The agent skips the exploratory phase entirely. In Act 1 it had to investigate notification-service (red herring), check backend-api logs, and discover the timeout mismatch. With the SOP, it recognizes the `upstream timeout` pattern in the logs and goes straight to gateway config → fix.

### Result [#result]

```
Act 1 (no prior knowledge):  9-12 DevOps tool calls (investigates multiple services)
Act 2 (with learned skills): 4-6 DevOps tool calls (targeted fix)
Improvement: ~40-55% fewer tool calls
```

## Full Runnable Scripts [#full-runnable-scripts]

<AccordionGroup>
  <Accordion title="Python — complete runnable script">
    ```python
    """
    End-to-end auto-learn demo: DevOps incident resolution.

    Requires: ACONTEXT_API_KEY, OPENAI_API_KEY
      pip install acontext openai
    """

    import json
    import os
    import time
    from copy import deepcopy

    from openai import OpenAI

    from acontext import AcontextClient
    from acontext.agent.skill import SKILL_TOOLS

    # ── Mock environment ──────────────────────────────────────────────────

    ACT1_ENV = {
        "services": {
            "api-gateway": {"status": "unhealthy", "error_rate_pct": 34, "p99_latency_ms": 55, "type": "reverse-proxy"},
            "auth-service": {"status": "healthy", "error_rate_pct": 0, "p99_latency_ms": 85, "type": "application"},
            "backend-api": {"status": "healthy", "error_rate_pct": 0, "p99_latency_ms": 4200, "type": "application"},
            "notification-service": {"status": "degraded", "error_rate_pct": 5, "p99_latency_ms": 320, "type": "application"},
            "redis-cache": {"status": "healthy", "error_rate_pct": 0, "p99_latency_ms": 3, "type": "cache"},
            "user-db": {"status": "healthy", "error_rate_pct": 0, "connections_active": 45, "type": "database"},
        },
        "configs": {
            "api-gateway": {
                "upstream_timeout_ms": 5000, "max_retries": 3,
                "routes": {"/api/*": "http://backend-api:8080", "/auth/*": "http://auth-service:8080", "/notifications/*": "http://notification-service:8080"},
            },
        },
        "logs": {
            "api-gateway": [
                "[2026-03-26 09:12:03] ERROR  502 Bad Gateway returned to client  path=/api/orders  upstream=backend-api",
                "[2026-03-26 09:12:01] ERROR  502 Bad Gateway returned to client  path=/api/users  upstream=backend-api",
                "[2026-03-26 09:11:59] WARN   upstream response slow  service=backend-api  duration=4850ms",
                "[2026-03-26 09:11:58] WARN   retrying upstream request  attempt=3  service=backend-api",
                "[2026-03-26 09:11:55] ERROR  upstream health check failed  service=notification-service  consecutive_failures=5",
                "[2026-03-26 09:11:52] ERROR  502 Bad Gateway returned to client  path=/api/users  upstream=backend-api",
                "[2026-03-26 09:11:50] WARN   retrying upstream request  attempt=2  service=backend-api",
                "[2026-03-26 09:11:48] INFO   health check passed  service=auth-service  latency=80ms",
                "[2026-03-26 09:11:45] INFO   health check passed  service=redis-cache  latency=2ms",
            ],
            "auth-service": ["[2026-03-26 09:12:00] INFO   request completed  path=/auth/verify  duration=78ms"],
            "backend-api": [
                "[2026-03-26 09:12:00] INFO   request completed  path=/api/users  duration=4150ms",
                "[2026-03-26 09:11:55] INFO   request completed  path=/api/orders  duration=4890ms",
                "[2026-03-26 09:11:50] WARN   DB query slow  table=orders  duration=3200ms",
            ],
            "notification-service": [
                "[2026-03-26 09:11:55] ERROR  SMTP relay connection refused  host=smtp.internal  retrying in 30s",
                "[2026-03-26 09:11:50] WARN   message queue backlog  depth=1240  oldest=45s",
                "[2026-03-26 09:11:45] ERROR  failed to deliver notification  channel=email  error=connection_refused",
            ],
            "redis-cache": ["[2026-03-26 09:12:00] INFO   keyspace hits=12450  misses=23"],
        },
    }

    ACT2_ENV = {
        "services": {
            "api-gateway": {"status": "unhealthy", "error_rate_pct": 22, "p99_latency_ms": 60, "type": "reverse-proxy"},
            "auth-service": {"status": "healthy", "error_rate_pct": 0, "p99_latency_ms": 90, "type": "application"},
            "backend-api": {"status": "healthy", "error_rate_pct": 0, "p99_latency_ms": 120, "type": "application"},
            "payment-service": {"status": "healthy", "error_rate_pct": 0, "p99_latency_ms": 45000, "type": "application"},
            "notification-service": {"status": "healthy", "error_rate_pct": 0, "p99_latency_ms": 200, "type": "application"},
            "redis-cache": {"status": "healthy", "error_rate_pct": 0, "p99_latency_ms": 3, "type": "cache"},
            "user-db": {"status": "healthy", "error_rate_pct": 0, "connections_active": 52, "type": "database"},
        },
        "configs": {
            "api-gateway": {
                "upstream_timeout_ms": 30000, "max_retries": 3,
                "routes": {"/api/*": "http://backend-api:8080", "/auth/*": "http://auth-service:8080", "/notifications/*": "http://notification-service:8080", "/payments/*": "http://payment-service:8080"},
            },
        },
        "logs": {
            "api-gateway": [
                "[2026-03-26 14:30:12] ERROR  upstream timeout: payment-service did not respond within 30000 ms",
                "[2026-03-26 14:30:12] ERROR  502 Bad Gateway returned to client  path=/payments/checkout",
                "[2026-03-26 14:30:10] ERROR  upstream timeout: payment-service did not respond within 30000 ms",
                "[2026-03-26 14:30:10] ERROR  502 Bad Gateway returned to client  path=/payments/refund",
                "[2026-03-26 14:30:05] WARN   retrying upstream request  attempt=3  service=payment-service",
                "[2026-03-26 14:30:00] INFO   health check passed  service=backend-api  latency=110ms",
                "[2026-03-26 14:29:58] INFO   health check passed  service=auth-service  latency=85ms",
            ],
            "payment-service": [
                "[2026-03-26 14:30:11] INFO   request completed  path=/payments/checkout  duration=44200ms",
                "[2026-03-26 14:30:05] INFO   request completed  path=/payments/refund  duration=42800ms",
                "[2026-03-26 14:29:50] WARN   external payment gateway slow  provider=stripe  duration=41000ms",
            ],
        },
    }

    MOCK_TOOL_SCHEMAS = [
        {"type": "function", "function": {"name": "check_service_status", "description": "Check the health status, error rate, and latency of a service.", "parameters": {"type": "object", "properties": {"service_name": {"type": "string", "description": "Name of the service to check."}}, "required": ["service_name"]}}},
        {"type": "function", "function": {"name": "check_logs", "description": "Retrieve recent log entries for a service.", "parameters": {"type": "object", "properties": {"service_name": {"type": "string", "description": "Name of the service."}, "lines": {"type": "integer", "description": "Number of log lines (max 20)."}}, "required": ["service_name"]}}},
        {"type": "function", "function": {"name": "read_config", "description": "Read the current configuration of a service.", "parameters": {"type": "object", "properties": {"service_name": {"type": "string", "description": "Name of the service."}}, "required": ["service_name"]}}},
        {"type": "function", "function": {"name": "update_config", "description": "Update a configuration value for a service.", "parameters": {"type": "object", "properties": {"service_name": {"type": "string", "description": "Name of the service."}, "key": {"type": "string", "description": "Config key."}, "value": {"type": "string", "description": "New value."}}, "required": ["service_name", "key", "value"]}}},
        {"type": "function", "function": {"name": "restart_service", "description": "Restart a service.", "parameters": {"type": "object", "properties": {"service_name": {"type": "string", "description": "Name of the service."}}, "required": ["service_name"]}}},
    ]
    MOCK_TOOL_NAMES = {t["function"]["name"] for t in MOCK_TOOL_SCHEMAS}

    SYSTEM_PROMPT = (
        "You are a DevOps incident response agent with full authority to make changes. "
        "Diagnose the root cause, apply the fix immediately, and verify it works. "
        "Act autonomously — never ask for permission or confirmation. "
        "Complete the full resolution cycle: diagnose → fix → verify."
    )


    def exec_mock_tool(env, name, args):
        if name == "check_service_status":
            svc = args.get("service_name", "")
            info = env["services"].get(svc)
            if not info:
                return f"Service '{svc}' not found. Available: {', '.join(env['services'])}"
            return "\n".join([f"Service: {svc}"] + [f"  {k}: {v}" for k, v in info.items()])
        if name == "check_logs":
            svc = args.get("service_name", "")
            n = min(int(args.get("lines", 10)), 20)
            entries = env["logs"].get(svc, [])
            return "\n".join(entries[:n]) if entries else f"No logs for '{svc}'."
        if name == "read_config":
            svc = args.get("service_name", "")
            cfg = env["configs"].get(svc)
            return json.dumps(cfg, indent=2) if cfg else f"No config for '{svc}'."
        if name == "update_config":
            svc, key, value = args.get("service_name", ""), args.get("key", ""), args.get("value", "")
            cfg = env["configs"].get(svc)
            if not cfg:
                return f"No config for '{svc}'."
            try:
                value = int(value)
            except (ValueError, TypeError):
                pass
            cfg[key] = value
            return f"Updated {svc}: {key} = {value}"
        if name == "restart_service":
            svc = args.get("service_name", "")
            info = env["services"].get(svc)
            if not info:
                return f"Service '{svc}' not found."
            info["status"] = "healthy"
            info["error_rate_pct"] = 0
            return f"Service '{svc}' restarted. Status: healthy"
        return f"Unknown tool: {name}"


    def run_agent(oai, ac, session_id, env, user_message, extra_system="", extra_tools=None, skill_ctx=None):
        system = SYSTEM_PROMPT + ("\n\n" + extra_system if extra_system else "")
        tools = list(MOCK_TOOL_SCHEMAS) + (extra_tools or [])
        messages = [{"role": "system", "content": system}, {"role": "user", "content": user_message}]
        ac.sessions.store_message(session_id, blob={"role": "user", "content": user_message})
        devops_calls = 0

        for _ in range(15):
            resp = oai.chat.completions.create(model="gpt-4.1", messages=messages, tools=tools)
            msg = resp.choices[0].message
            if msg.tool_calls:
                blob = {"role": "assistant", "tool_calls": [
                    {"id": tc.id, "type": "function", "function": {"name": tc.function.name, "arguments": tc.function.arguments}}
                    for tc in msg.tool_calls
                ]}
                if msg.content:
                    blob["content"] = msg.content
            else:
                blob = {"role": "assistant", "content": msg.content or ""}
            messages.append(blob)
            ac.sessions.store_message(session_id, blob=blob)

            if not msg.tool_calls:
                print(f"  Agent: {msg.content}")
                break

            for tc in msg.tool_calls:
                fn, args = tc.function.name, json.loads(tc.function.arguments)
                if fn in MOCK_TOOL_NAMES:
                    result = exec_mock_tool(env, fn, args)
                    devops_calls += 1
                    label = ", ".join(f"{k}={v}" for k, v in args.items())
                    print(f"    → {fn}({label})")
                elif skill_ctx and SKILL_TOOLS.tool_exists(fn):
                    result = SKILL_TOOLS.execute_tool(skill_ctx, fn, args)
                    print(f"    ★ {fn}({args.get('skill_name', args.get('file_path', fn))})")
                else:
                    result = f"Unknown tool: {fn}"
                tool_msg = {"role": "tool", "tool_call_id": tc.id, "content": result}
                messages.append(tool_msg)
                ac.sessions.store_message(session_id, blob=tool_msg)

        return devops_calls


    def wait_for_tasks(ac, session_id, timeout=90):
        deadline = time.time() + timeout
        while time.time() < deadline:
            s = ac.sessions.messages_observing_status(session_id)
            if s.pending == 0 and s.in_process == 0:
                break
            time.sleep(2)
        return ac.sessions.get_tasks(session_id).items


    def main():
        ac = AcontextClient(api_key=os.environ["ACONTEXT_API_KEY"], base_url=os.getenv("ACONTEXT_BASE_URL"))
        oai = OpenAI()
        out_dir = os.path.join(os.path.dirname(__file__), "learned_skills")

        # ── ACT 1 ────────────────────────────────────────────────────────
        print("\n" + "=" * 60)
        print("  ACT 1 — Learning Run: Diagnose 502 errors")
        print("=" * 60)

        space = ac.learning_spaces.create()
        s1 = ac.sessions.create()
        ac.learning_spaces.learn(space.id, session_id=s1.id)

        act1 = run_agent(oai, ac, s1.id, deepcopy(ACT1_ENV),
            "Our API gateway is returning 502 errors on /api/* routes — "
            "about 34% of requests are failing. "
            "Diagnose the root cause and fix it.")

        print("\n  Waiting for task extraction...")
        ac.sessions.flush(s1.id)
        tasks = wait_for_tasks(ac, s1.id)
        print(f"  Extracted {len(tasks)} task(s):")
        for t in tasks:
            print(f"    [{t.status}] {t.data.task_description}")

        print("\n  Waiting for skill learning pipeline...")
        ac.learning_spaces.wait_for_learning(space.id, session_id=s1.id, timeout=180)

        skills = ac.learning_spaces.list_skills(space.id)
        os.makedirs(out_dir, exist_ok=True)
        print(f"\n  Generated {len(skills)} skill(s) — downloading to learned_skills/")
        for s in skills:
            skill_dir = os.path.join(out_dir, s.name)
            os.makedirs(skill_dir, exist_ok=True)
            for f in s.file_index:
                c = ac.skills.get_file(skill_id=s.id, file_path=f.path)
                if c.content:
                    with open(os.path.join(skill_dir, f.path), "w") as fh:
                        fh.write(c.content.raw)
            print(f"    • {s.name}: {s.description}")

        # ── ACT 2 ────────────────────────────────────────────────────────
        print("\n" + "=" * 60)
        print("  ACT 2 — Recall Run: New 502 (with learned skills)")
        print("=" * 60)

        s2 = ac.sessions.create()
        ac.learning_spaces.learn(space.id, session_id=s2.id)
        skill_ctx = SKILL_TOOLS.format_context(ac, [s.id for s in skills])

        act2 = run_agent(oai, ac, s2.id, deepcopy(ACT2_ENV),
            "The API gateway started returning 502 errors again after today's deployment. "
            "About 22% of requests are failing. Please diagnose and fix.",
            extra_system="You have skills from previous incidents. Check skills first.\n\n" + skill_ctx.get_context_prompt(),
            extra_tools=SKILL_TOOLS.to_openai_tool_schema(),
            skill_ctx=skill_ctx)

        # ── Results ──────────────────────────────────────────────────────
        print("\n" + "=" * 60)
        print(f"  Act 1 (no prior knowledge):  {act1} tool calls")
        print(f"  Act 2 (with learned skills): {act2} tool calls")
        if act2 < act1:
            print(f"  → {round((1 - act2/act1) * 100)}% fewer tool calls with learned skills")
        print(f"\n  Skills saved to: {out_dir}/")
        print("=" * 60)

        ac.sessions.delete(s1.id)
        ac.sessions.delete(s2.id)
        ac.learning_spaces.delete(space.id)
        for s in skills:
            ac.skills.delete(s.id)
        print("  Cleaned up remote resources.")

    if __name__ == "__main__":
        main()
    ```
  </Accordion>

  <Accordion title="TypeScript — complete runnable script">
    ```typescript
    /**
     * End-to-end auto-learn demo: DevOps incident resolution.
     * Requires: ACONTEXT_API_KEY, OPENAI_API_KEY
     *   npm install @acontext/acontext openai
     */

    import fs from 'node:fs/promises';
    import path from 'node:path';
    import { AcontextClient } from '@acontext/acontext';
    import { SKILL_TOOLS, SkillContext } from '@acontext/acontext';
    import OpenAI from 'openai';

    // ── Mock environment ────────────────────────────────────────────

    interface MockEnv {
      services: Record<string, Record<string, unknown>>;
      configs: Record<string, Record<string, unknown>>;
      logs: Record<string, string[]>;
    }

    function createAct1Env(): MockEnv {
      return {
        services: {
          'api-gateway': { status: 'unhealthy', error_rate_pct: 34, p99_latency_ms: 55, type: 'reverse-proxy' },
          'auth-service': { status: 'healthy', error_rate_pct: 0, p99_latency_ms: 85, type: 'application' },
          'backend-api': { status: 'healthy', error_rate_pct: 0, p99_latency_ms: 4200, type: 'application' },
          'notification-service': { status: 'degraded', error_rate_pct: 5, p99_latency_ms: 320, type: 'application' },
          'redis-cache': { status: 'healthy', error_rate_pct: 0, p99_latency_ms: 3, type: 'cache' },
          'user-db': { status: 'healthy', error_rate_pct: 0, connections_active: 45, type: 'database' },
        },
        configs: {
          'api-gateway': {
            upstream_timeout_ms: 5000, max_retries: 3,
            routes: { '/api/*': 'http://backend-api:8080', '/auth/*': 'http://auth-service:8080', '/notifications/*': 'http://notification-service:8080' },
          },
        },
        logs: {
          'api-gateway': [
            '[2026-03-26 09:12:03] ERROR  502 Bad Gateway returned to client  path=/api/orders  upstream=backend-api',
            '[2026-03-26 09:12:01] ERROR  502 Bad Gateway returned to client  path=/api/users  upstream=backend-api',
            '[2026-03-26 09:11:59] WARN   upstream response slow  service=backend-api  duration=4850ms',
            '[2026-03-26 09:11:58] WARN   retrying upstream request  attempt=3  service=backend-api',
            '[2026-03-26 09:11:55] ERROR  upstream health check failed  service=notification-service  consecutive_failures=5',
            '[2026-03-26 09:11:52] ERROR  502 Bad Gateway returned to client  path=/api/users  upstream=backend-api',
            '[2026-03-26 09:11:50] WARN   retrying upstream request  attempt=2  service=backend-api',
            '[2026-03-26 09:11:48] INFO   health check passed  service=auth-service  latency=80ms',
            '[2026-03-26 09:11:45] INFO   health check passed  service=redis-cache  latency=2ms',
          ],
          'auth-service': ['[2026-03-26 09:12:00] INFO   request completed  path=/auth/verify  duration=78ms'],
          'backend-api': [
            '[2026-03-26 09:12:00] INFO   request completed  path=/api/users  duration=4150ms',
            '[2026-03-26 09:11:55] INFO   request completed  path=/api/orders  duration=4890ms',
            '[2026-03-26 09:11:50] WARN   DB query slow  table=orders  duration=3200ms',
          ],
          'notification-service': [
            '[2026-03-26 09:11:55] ERROR  SMTP relay connection refused  host=smtp.internal  retrying in 30s',
            '[2026-03-26 09:11:50] WARN   message queue backlog  depth=1240  oldest=45s',
            '[2026-03-26 09:11:45] ERROR  failed to deliver notification  channel=email  error=connection_refused',
          ],
          'redis-cache': ['[2026-03-26 09:12:00] INFO   keyspace hits=12450  misses=23'],
        },
      };
    }

    function createAct2Env(): MockEnv {
      return {
        services: {
          'api-gateway': { status: 'unhealthy', error_rate_pct: 22, p99_latency_ms: 60, type: 'reverse-proxy' },
          'auth-service': { status: 'healthy', error_rate_pct: 0, p99_latency_ms: 90, type: 'application' },
          'backend-api': { status: 'healthy', error_rate_pct: 0, p99_latency_ms: 120, type: 'application' },
          'payment-service': { status: 'healthy', error_rate_pct: 0, p99_latency_ms: 45000, type: 'application' },
          'notification-service': { status: 'healthy', error_rate_pct: 0, p99_latency_ms: 200, type: 'application' },
          'redis-cache': { status: 'healthy', error_rate_pct: 0, p99_latency_ms: 3, type: 'cache' },
          'user-db': { status: 'healthy', error_rate_pct: 0, connections_active: 52, type: 'database' },
        },
        configs: {
          'api-gateway': {
            upstream_timeout_ms: 30000, max_retries: 3,
            routes: { '/api/*': 'http://backend-api:8080', '/auth/*': 'http://auth-service:8080', '/notifications/*': 'http://notification-service:8080', '/payments/*': 'http://payment-service:8080' },
          },
        },
        logs: {
          'api-gateway': [
            '[2026-03-26 14:30:12] ERROR  upstream timeout: payment-service did not respond within 30000 ms',
            '[2026-03-26 14:30:12] ERROR  502 Bad Gateway returned to client  path=/payments/checkout',
            '[2026-03-26 14:30:10] ERROR  upstream timeout: payment-service did not respond within 30000 ms',
            '[2026-03-26 14:30:10] ERROR  502 Bad Gateway returned to client  path=/payments/refund',
            '[2026-03-26 14:30:05] WARN   retrying upstream request  attempt=3  service=payment-service',
            '[2026-03-26 14:30:00] INFO   health check passed  service=backend-api  latency=110ms',
            '[2026-03-26 14:29:58] INFO   health check passed  service=auth-service  latency=85ms',
          ],
          'payment-service': [
            '[2026-03-26 14:30:11] INFO   request completed  path=/payments/checkout  duration=44200ms',
            '[2026-03-26 14:30:05] INFO   request completed  path=/payments/refund  duration=42800ms',
            '[2026-03-26 14:29:50] WARN   external payment gateway slow  provider=stripe  duration=41000ms',
          ],
        },
      };
    }

    function execMockTool(env: MockEnv, name: string, args: Record<string, unknown>): string {
      if (name === 'check_service_status') {
        const svc = args.service_name as string;
        const info = env.services[svc];
        if (!info) return `Service '${svc}' not found. Available: ${Object.keys(env.services).join(', ')}`;
        return ['Service: ' + svc, ...Object.entries(info).map(([k, v]) => `  ${k}: ${v}`)].join('\n');
      }
      if (name === 'check_logs') {
        const svc = args.service_name as string;
        const n = Math.min(Number(args.lines ?? 10), 20);
        const entries = env.logs[svc];
        return entries?.length ? entries.slice(0, n).join('\n') : `No logs for '${svc}'.`;
      }
      if (name === 'read_config') {
        const svc = args.service_name as string;
        const cfg = env.configs[svc];
        return cfg ? JSON.stringify(cfg, null, 2) : `No config for '${svc}'.`;
      }
      if (name === 'update_config') {
        const svc = args.service_name as string;
        const key = args.key as string;
        let value: unknown = args.value;
        const cfg = env.configs[svc];
        if (!cfg) return `No config for '${svc}'.`;
        const num = Number(value);
        if (!isNaN(num) && value !== '') value = num;
        cfg[key] = value;
        return `Updated ${svc}: ${key} = ${value}`;
      }
      if (name === 'restart_service') {
        const svc = args.service_name as string;
        const info = env.services[svc];
        if (!info) return `Service '${svc}' not found.`;
        info.status = 'healthy';
        info.error_rate_pct = 0;
        return `Service '${svc}' restarted. Status: healthy`;
      }
      return `Unknown tool: ${name}`;
    }

    const MOCK_TOOL_SCHEMAS: OpenAI.ChatCompletionTool[] = [
      { type: 'function', function: { name: 'check_service_status', description: 'Check health status, error rate, and latency.', parameters: { type: 'object', properties: { service_name: { type: 'string', description: 'Service name.' } }, required: ['service_name'] } } },
      { type: 'function', function: { name: 'check_logs', description: 'Retrieve recent log entries.', parameters: { type: 'object', properties: { service_name: { type: 'string', description: 'Service name.' }, lines: { type: 'integer', description: 'Number of lines (max 20).' } }, required: ['service_name'] } } },
      { type: 'function', function: { name: 'read_config', description: 'Read service configuration.', parameters: { type: 'object', properties: { service_name: { type: 'string', description: 'Service name.' } }, required: ['service_name'] } } },
      { type: 'function', function: { name: 'update_config', description: 'Update a config value.', parameters: { type: 'object', properties: { service_name: { type: 'string', description: 'Service name.' }, key: { type: 'string', description: 'Config key.' }, value: { type: 'string', description: 'New value.' } }, required: ['service_name', 'key', 'value'] } } },
      { type: 'function', function: { name: 'restart_service', description: 'Restart a service.', parameters: { type: 'object', properties: { service_name: { type: 'string', description: 'Service name.' } }, required: ['service_name'] } } },
    ];
    const MOCK_TOOL_NAMES = new Set(MOCK_TOOL_SCHEMAS.map(t => t.function.name));

    const SYSTEM_PROMPT = 'You are a DevOps incident response agent with full authority. ' +
      'Diagnose the root cause, apply the fix immediately, and verify it works. ' +
      'Act autonomously — never ask for permission. Complete: diagnose → fix → verify.';

    async function runAgent(
      openai: OpenAI, ac: AcontextClient, sessionId: string, env: MockEnv,
      userMessage: string, extraSystem = '', extraTools: OpenAI.ChatCompletionTool[] = [],
      skillCtx?: SkillContext,
    ): Promise<number> {
      const system = SYSTEM_PROMPT + (extraSystem ? '\n\n' + extraSystem : '');
      const tools = [...MOCK_TOOL_SCHEMAS, ...extraTools];
      const messages: OpenAI.ChatCompletionMessageParam[] = [
        { role: 'system', content: system },
        { role: 'user', content: userMessage },
      ];
      await ac.sessions.storeMessage(sessionId, { role: 'user', content: userMessage });
      let devopsCalls = 0;

      for (let i = 0; i < 15; i++) {
        const resp = await openai.chat.completions.create({ model: 'gpt-4.1', messages, tools });
        const msg = resp.choices[0].message;
        let blob: Record<string, unknown>;
        if (msg.tool_calls?.length) {
          blob = { role: 'assistant', tool_calls: msg.tool_calls.map(tc => ({ id: tc.id, type: 'function', function: { name: tc.function.name, arguments: tc.function.arguments } })) };
          if (msg.content) blob.content = msg.content;
        } else {
          blob = { role: 'assistant', content: msg.content ?? '' };
        }
        messages.push(msg as OpenAI.ChatCompletionMessageParam);
        await ac.sessions.storeMessage(sessionId, blob);

        if (!msg.tool_calls?.length) { console.log(`  Agent: ${msg.content}`); break; }

        for (const tc of msg.tool_calls) {
          const fn = tc.function.name;
          const args = JSON.parse(tc.function.arguments);
          let result: string;
          if (MOCK_TOOL_NAMES.has(fn)) {
            result = execMockTool(env, fn, args);
            devopsCalls++;
            const label = Object.entries(args).map(([k, v]) => `${k}=${v}`).join(', ');
            console.log(`    → ${fn}(${label})`);
          } else if (skillCtx && SKILL_TOOLS.toolExists(fn)) {
            result = await SKILL_TOOLS.executeTool(skillCtx, fn, args);
            console.log(`    ★ ${fn}(${args.skill_name ?? args.file_path ?? fn})`);
          } else {
            result = `Unknown tool: ${fn}`;
          }
          messages.push({ role: 'tool', tool_call_id: tc.id, content: result });
          await ac.sessions.storeMessage(sessionId, { role: 'tool', tool_call_id: tc.id, content: result });
        }
      }
      return devopsCalls;
    }

    const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));

    async function waitForTasks(ac: AcontextClient, sessionId: string, timeout = 90000) {
      const deadline = Date.now() + timeout;
      while (Date.now() < deadline) {
        const s = await ac.sessions.messagesObservingStatus(sessionId);
        if (s.pending === 0 && s.in_process === 0) break;
        await sleep(2000);
      }
      const r = await ac.sessions.getTasks(sessionId);
      return r.items;
    }

    async function main() {
      const ac = new AcontextClient({ apiKey: process.env.ACONTEXT_API_KEY!, baseUrl: process.env.ACONTEXT_BASE_URL });
      const openai = new OpenAI();

      // ACT 1
      console.log('\n' + '='.repeat(60));
      console.log('  ACT 1 — Learning Run: Diagnose 502 errors');
      console.log('='.repeat(60));

      const space = await ac.learningSpaces.create();
      const s1 = await ac.sessions.create();
      await ac.learningSpaces.learn({ spaceId: space.id, sessionId: s1.id });

      const act1 = await runAgent(openai, ac, s1.id, createAct1Env(),
        'Our API gateway is returning 502 errors on /api/* routes — ' +
        'about 34% of requests are failing. ' +
        'Diagnose the root cause and fix it.');

      console.log('\n  Waiting for task extraction...');
      await ac.sessions.flush(s1.id);
      const tasks = await waitForTasks(ac, s1.id);
      console.log(`  Extracted ${tasks.length} task(s):`);
      for (const t of tasks) console.log(`    [${t.status}] ${t.data.task_description}`);

      console.log('\n  Waiting for skill learning pipeline...');
      await ac.learningSpaces.waitForLearning({ spaceId: space.id, sessionId: s1.id, timeout: 180 });

      const skills = await ac.learningSpaces.listSkills(space.id);
      const outDir = path.join(path.dirname(new URL(import.meta.url).pathname), 'learned_skills');
      await fs.mkdir(outDir, { recursive: true });
      console.log(`\n  Generated ${skills.length} skill(s) — downloading to learned_skills/`);
      for (const s of skills) {
        const skillDir = path.join(outDir, s.name);
        await fs.mkdir(skillDir, { recursive: true });
        for (const f of s.fileIndex) {
          const c = await ac.skills.getFile({ skillId: s.id, filePath: f.path });
          if (c.content) await fs.writeFile(path.join(skillDir, f.path), c.content.raw);
        }
        console.log(`    • ${s.name}: ${s.description}`);
      }

      // ACT 2
      console.log('\n' + '='.repeat(60));
      console.log('  ACT 2 — Recall Run: New 502 (with learned skills)');
      console.log('='.repeat(60));

      const s2 = await ac.sessions.create();
      await ac.learningSpaces.learn({ spaceId: space.id, sessionId: s2.id });
      const skillCtx = await SKILL_TOOLS.formatContext(ac, skills.map(s => s.id));

      const act2 = await runAgent(openai, ac, s2.id, createAct2Env(),
        'The API gateway started returning 502 errors again after today\'s deployment. ' +
        'About 22% of requests are failing. Please diagnose and fix.',
        'You have skills from previous incidents. Check skills first.\n\n' + skillCtx.getContextPrompt(),
        SKILL_TOOLS.toOpenAIToolSchema() as OpenAI.ChatCompletionTool[], skillCtx);

      // Results
      console.log('\n' + '='.repeat(60));
      console.log(`  Act 1 (no prior knowledge):  ${act1} tool calls`);
      console.log(`  Act 2 (with learned skills): ${act2} tool calls`);
      if (act2 < act1) console.log(`  → ${Math.round((1 - act2 / act1) * 100)}% fewer tool calls with learned skills`);
      console.log(`\n  Skills saved to: learned_skills/`);
      console.log('='.repeat(60));

      await ac.sessions.delete(s1.id);
      await ac.sessions.delete(s2.id);
      await ac.learningSpaces.delete(space.id);
      for (const s of skills) await ac.skills.delete(s.id);
      console.log('  Cleaned up remote resources.');
    }

    main().catch(e => { console.error(e); process.exit(1); });
    ```
  </Accordion>
</AccordionGroup>

## How It Works Under the Hood [#how-it-works-under-the-hood]

The auto-learn pipeline that runs between Act 1 and Act 2:

```
Session messages
  └► Task Agent extracts structured tasks (goal, progress, status)
      └► Distillation LLM analyzes the completed task:
          - Was it trivial? → skip
          - Multi-step procedure? → report_success_analysis (SOP)
          - Factual content? → report_factual_content
          - Failed task? → report_failure_analysis (anti-pattern)
              └► Skill Learner Agent reads distilled context + existing skills
                  → Creates/updates skill files (SKILL.md + data files)
```

The DevOps scenario works well because it triggers `report_success_analysis` — the multi-step debugging process with clear decisions produces a rich SOP that the skill learner formats into actionable steps.

## Next Steps [#next-steps]

<CardGroup cols="3">
  <Card title="Custom Skills" icon="sparkles" href="/learn/custom-memory">
    Define your own memory schemas
  </Card>

  <Card title="Learning Spaces" icon="brain" href="/learn/learning-spaces">
    Default skills, custom skills, and managing spaces
  </Card>

  <Card title="Skill Content Tools" icon="wand-magic-sparkles" href="/tool/skill_tools">
    Full reference for skill reading tools
  </Card>
</CardGroup>


---

# Managing Learning Spaces



A learning space is the container where Acontext organizes skills for your agent. Each space comes with default skills and supports custom ones.

## Default Skills [#default-skills]

Every learning space starts with two built-in skills:

| Skill                  | What it captures                                |
| ---------------------- | ----------------------------------------------- |
| **daily-logs**         | Daily activity summaries — one file per day     |
| **user-general-facts** | User preferences and facts — one file per topic |

<Accordion title="Example: daily-logs output">
  ```markdown
  # 2025-06-15

  ## Deploy API v2 to staging
  - **Outcome**: success
  - **Summary**: Blue-green deploy completed, zero downtime
  - **Key Decisions**: Ran DB migration before switching traffic
  - **Learnings**: Set health-check timeout to 30s, not 10s

  ## Roll back staging after test failure
  - **Outcome**: failed
  - **Summary**: Rollback script timed out on large DB
  - **Key Decisions**: Attempted manual rollback
  - **Learnings**: Add rollback timeout flag, pre-test with smaller dataset
  ```
</Accordion>

<Accordion title="Example: user-general-facts output">
  ```markdown
  # Personal Info
  - username: gus
  ```
</Accordion>

## Adding Custom Skills [#adding-custom-skills]

Include your own skills alongside the defaults. The learner updates them too — as long as they have a valid `SKILL.md`.

<CodeGroup>
  ```python title="Python"
  skill = client.skills.create(file=FileUpload(...))
  client.learning_spaces.include_skill(space.id, skill_id=skill.id)

  # Remove a skill (idempotent)
  client.learning_spaces.exclude_skill(space.id, skill_id="skill-uuid")
  ```

  ```typescript title="TypeScript"
  const skill = await client.skills.create({ file: new FileUpload(...) });
  await client.learningSpaces.includeSkill({ spaceId: space.id, skillId: skill.id });

  // Remove a skill (idempotent)
  await client.learningSpaces.excludeSkill({ spaceId: space.id, skillId: "skill-uuid" });
  ```
</CodeGroup>

See [Agent Skills](/store/skill) for the SKILL.md format and ZIP structure.

## Managing Learning Spaces [#managing-learning-spaces]

<CodeGroup>
  ```python title="Python"
  # List, filter, paginate
  spaces = client.learning_spaces.list(user="alice@example.com", limit=10)

  # Filter by meta
  spaces = client.learning_spaces.list(filter_by_meta={"domain": "deployments"})

  # Update meta
  client.learning_spaces.update(space.id, meta={"version": "2.0"})

  # Delete (skills and sessions are NOT deleted)
  client.learning_spaces.delete(space.id)
  ```

  ```typescript title="TypeScript"
  // List, filter, paginate
  const spaces = await client.learningSpaces.list({ user: "alice@example.com", limit: 10 });

  // Filter by meta
  const filtered = await client.learningSpaces.list({ filterByMeta: { domain: "deployments" } });

  // Update meta
  await client.learningSpaces.update(space.id, { meta: { version: "2.0" } });

  // Delete (skills and sessions are NOT deleted)
  await client.learningSpaces.delete(space.id);
  ```
</CodeGroup>

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Skill Memory" icon="brain" href="/learn/quick">
    Understand how skill memory compares to other approaches
  </Card>

  <Card title="Agent Skills" icon="sparkles" href="/store/skill">
    Upload and manage skills
  </Card>
</CardGroup>


---

# Agent Tasks



Acontext extracts tasks from agent conversations based on **user requirements**. Each distinct request the user makes becomes a separate, trackable task — the agent's plan steps are tracked as progress within that task.

## Quick Start [#quick-start]

<CodeGroup>
  ```python title="Python"
  import os
  from acontext import AcontextClient

  client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))
  session = client.sessions.create()

  # Store conversation — each user request becomes a separate task
  messages = [
      {"role": "user", "content": "Research iPhone 15 specs and summarize the key features"},
      {"role": "assistant", "content": "Here are the key iPhone 15 specs:\n- A16 chip, 48MP camera, USB-C..."},
      {"role": "user", "content": "Now create a comparison table with iPhone 14"},
      {"role": "assistant", "content": "| Feature | iPhone 14 | iPhone 15 |\n|---|---|---|\n| Chip | A15 | A16 |..."},
  ]
  for msg in messages:
      client.sessions.store_message(session_id=session.id, blob=msg, format="openai")

  # Wait for extraction
  client.sessions.flush(session.id)

  # Get tasks — one per user request
  tasks = client.sessions.get_tasks(session.id)
  for task in tasks.items:
      print(f"Task #{task.order}: {task.data.task_description} | Status: {task.status}")
  ```

  ```typescript title="TypeScript"
  import { AcontextClient } from '@acontext/acontext';

  const client = new AcontextClient({
      apiKey: process.env.ACONTEXT_API_KEY,
  });
  const session = await client.sessions.create();

  // Store conversation — each user request becomes a separate task
  const messages = [
      { role: "user", content: "Research iPhone 15 specs and summarize the key features" },
      { role: "assistant", content: "Here are the key iPhone 15 specs:\n- A16 chip, 48MP camera, USB-C..." },
      { role: "user", content: "Now create a comparison table with iPhone 14" },
      { role: "assistant", content: "| Feature | iPhone 14 | iPhone 15 |\n|---|---|---|\n| Chip | A15 | A16 |..." },
  ];
  for (const msg of messages) {
      await client.sessions.storeMessage(session.id, msg, { format: "openai" });
  }

  // Wait for extraction
  await client.sessions.flush(session.id);

  // Get tasks — one per user request
  const tasks = await client.sessions.getTasks(session.id);
  for (const task of tasks.items) {
      console.log(`Task #${task.order}: ${task.data.task_description} | Status: ${task.status}`);
  }
  ```
</CodeGroup>

## Task Data [#task-data]

Each task contains:

* `task_description`: What the task is
* `status`: `pending`, `running`, `success`, or `failed`
* `progresses`: Agent's progress updates
* `user_preferences`: User requirements mentioned

<CodeGroup>
  ```python title="Python"
  for task in tasks.items:
      print(f"Task: {task.data.task_description}")
      
      if task.data.progresses:
          for p in task.data.progresses:
              print(f"  Progress: {p}")
      
      if task.data.user_preferences:
          for pref in task.data.user_preferences:
              print(f"  Preference: {pref}")
  ```

  ```typescript title="TypeScript"
  for (const task of tasks.items) {
      console.log(`Task: ${task.data.task_description}`);
      
      if (task.data.progresses) {
          for (const p of task.data.progresses) {
              console.log(`  Progress: ${p}`);
          }
      }
      
      if (task.data.user_preferences) {
          for (const pref of task.data.user_preferences) {
              console.log(`  Preference: ${pref}`);
          }
      }
  }
  ```
</CodeGroup>

## View in Dashboard [#view-in-dashboard]

<Frame caption="Task viewer showing extracted tasks">
  <img src="/images/dashboard/session_task_viewer.png" alt="Task list view" />
</Frame>

<Frame caption="Task details with progress and preferences">
  <img src="/images/dashboard/task_viewer.png" alt="Task detail view" />
</Frame>

## Next Steps [#next-steps]

<CardGroup cols="3">
  <Card title="Buffer" icon="clock" href="/observe/buffer">
    Understand task extraction timing
  </Card>

  <Card title="Dashboard" icon="chart-simple" href="/observe/dashboard">
    View all agent data
  </Card>

  <Card title="Skill Memory" icon="brain" href="/learn/quick">
    Build skills from agent sessions
  </Card>
</CardGroup>


---

# Session Buffer



Acontext batches messages before processing to reduce LLM costs.

## When Processing Happens [#when-processing-happens]

**Buffer full**: When unprocessed messages reach `PROJECT_SESSION_MESSAGE_BUFFER_MAX_TURNS`

**Idle timeout**: When no new messages arrive for 8 seconds

## Force Processing [#force-processing]

<CodeGroup>
  ```python title="Python"
  client.sessions.flush(session_id)
  ```

  ```typescript title="TypeScript"
  await client.sessions.flush(sessionId);
  ```
</CodeGroup>

## Check Status [#check-status]

<CodeGroup>
  ```python title="Python"
  status = client.sessions.messages_observing_status(session_id)
  print(f"Observed: {status.observed}")
  print(f"In Process: {status.in_process}")
  print(f"Pending: {status.pending}")
  ```

  ```typescript title="TypeScript"
  const status = await client.sessions.messagesObservingStatus(sessionId);
  console.log(`Observed: ${status.observed}`);
  console.log(`In Process: ${status.inProcess}`);
  console.log(`Pending: ${status.pending}`);
  ```
</CodeGroup>

* **Observed**: Fully processed
* **In Process**: Currently being analyzed
* **Pending**: Waiting in buffer

## Next Steps [#next-steps]

<Card title="Settings" icon="gear" href="/settings/general">
  Customize buffer behavior
</Card>


---

# Dashboard



The dashboard provides visibility into your agent's operations through specialized views.

## Metrics [#metrics]

<Frame caption="Analytics and performance metrics">
  <img src="/images/dashboard/BI.png" alt="BI dashboard" />
</Frame>

## Traces [#traces]

<Frame caption="Distributed traces across services">
  <img src="/images/dashboard/traces_viewer.png" alt="Traces viewer" />
</Frame>

## Messages [#messages]

<Frame caption="Message contents and metadata">
  <img src="/images/dashboard/message_viewer.png" alt="Message viewer" />
</Frame>

## Artifacts [#artifacts]

<Frame caption="File artifacts in tree view">
  <img src="/images/dashboard/artifact_viewer.png" alt="Artifact viewer" />
</Frame>

## Tasks [#tasks]

<Frame caption="Session tasks and execution status">
  <img src="/images/dashboard/session_task_viewer.png" alt="Task viewer" />
</Frame>

<Frame caption="Task details with progress">
  <img src="/images/dashboard/task_viewer.png" alt="Task details" />
</Frame>

## Skills [#skills]

<Frame caption="Agent skills and usage">
  <img src="/images/dashboard/skill_viewer.png" alt="Skill viewer" />
</Frame>

## Next Steps [#next-steps]

<Card title="Agent Tasks" icon="list-check" href="/observe/agent_tasks">
  Get tasks via SDK
</Card>


---

# Disable Task Tracking



Disable task tracking for testing, simple Q\&A, or lightweight sub-agents.

<CodeGroup>
  ```python title="Python"
  session = client.sessions.create(disable_task_tracking=True)
  ```

  ```typescript title="TypeScript"
  const session = await client.sessions.create({ disableTaskTracking: true });
  ```
</CodeGroup>

**What happens:**

* Messages are still saved
* No automatic task extraction
* Session won't appear in task analytics

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Agent Tasks" icon="list-check" href="/observe/agent_tasks">
    How task extraction works
  </Card>

  <Card title="Dashboard" icon="chart-simple" href="/observe/dashboard">
    View analytics
  </Card>
</CardGroup>


---

# Task Evaluation Criteria



Define what "success" and "failure" mean for your project's tasks. By default, Acontext uses generic criteria — configure custom ones to match your agent's domain (e.g. customer support, coding, research).

## Configure via SDK [#configure-via-sdk]

<CodeGroup>
  ```python title="Python"
  import os
  from acontext import AcontextClient

  client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))

  # Set custom task evaluation criteria
  client.project.update_configs({
      "task_success_criteria": "The customer's issue is fully resolved and they confirm satisfaction, or the agent provides a verified solution with supporting documentation.",
      "task_failure_criteria": "The agent cannot resolve the issue after exhausting available options, the customer explicitly reports dissatisfaction, or the conversation is abandoned without resolution.",
  })

  # View current config
  configs = client.project.get_configs()
  print(configs)
  ```

  ```typescript title="TypeScript"
  import { AcontextClient } from '@acontext/acontext';

  const client = new AcontextClient({
      apiKey: process.env.ACONTEXT_API_KEY,
  });

  // Set custom task evaluation criteria
  await client.project.updateConfigs({
    task_success_criteria: "The customer's issue is fully resolved and they confirm satisfaction, or the agent provides a verified solution with supporting documentation.",
    task_failure_criteria: "The agent cannot resolve the issue after exhausting available options, the customer explicitly reports dissatisfaction, or the conversation is abandoned without resolution.",
  });

  // View current config
  const configs = await client.project.getConfigs();
  console.log(configs);
  ```
</CodeGroup>

## Reset to Defaults [#reset-to-defaults]

Pass `null` to remove a custom criteria and revert to the built-in default.

<CodeGroup>
  ```python title="Python"
  client.project.update_configs({
      "task_success_criteria": None,
      "task_failure_criteria": None,
  })
  ```

  ```typescript title="TypeScript"
  await client.project.updateConfigs({
    task_success_criteria: null,
    task_failure_criteria: null,
  });
  ```
</CodeGroup>

## Configure via Dashboard [#configure-via-dashboard]

You can also set task evaluation criteria from the Dashboard UI.

<Tabs>
  <Tab title="Hosted Dashboard">
    Go to **Project Settings → Task Tracking** tab. Edit the **Task Success Criteria** and **Task Failure Criteria** text fields, then click **Save**.
  </Tab>

  <Tab title="Self-Hosted (OSS)">
    Go to **Settings** in the sidebar. Edit the **Task Success Criteria** and **Task Failure Criteria** text fields, then click **Save**.
  </Tab>
</Tabs>

<Note>
  Custom criteria are injected into the task agent's input prompt. The system prompt stays identical across all projects, so LLM prompt caching (Anthropic and OpenAI) is fully preserved regardless of your custom criteria.
</Note>

## Next Steps [#next-steps]

<CardGroup cols="3">
  <Card title="Agent Tasks" icon="list-check" href="/observe/agent_tasks">
    How task extraction works
  </Card>

  <Card title="Disable Tasks" icon="circle-xmark" href="/observe/disable_tasks">
    Skip task tracking for specific sessions
  </Card>

  <Card title="Dashboard" icon="chart-simple" href="/observe/dashboard">
    View task analytics
  </Card>
</CardGroup>


---

# Distributed Tracing



Acontext traces requests through API, Core, database, cache, storage, and LLM calls using OpenTelemetry and Jaeger.

## What's Traced [#whats-traced]

* HTTP requests
* Database queries
* Redis cache operations
* S3 storage operations
* RabbitMQ messages
* LLM embedding/completion calls

## View Traces [#view-traces]

Access traces from the dashboard:

<Frame caption="Traces with hierarchical spans">
  <img src="/images/dashboard/traces_viewer.png" alt="Traces viewer" />
</Frame>

* **Time filtering**: 15min, 1h, 6h, 24h, 7d
* **Auto-refresh**: Every 30 seconds
* **Service colors**: Teal (api), Blue (core)
* **Jaeger link**: Click trace ID for detailed view

## Configuration [#configuration]

**Core (Python):**

```bash
TELEMETRY_ENABLED=true
TELEMETRY_OTLP_ENDPOINT=http://localhost:4317
TELEMETRY_SAMPLE_RATIO=1.0
```

**API (Go):**

```yaml
telemetry:
  enabled: true
  otlp_endpoint: "localhost:4317"
  sample_ratio: 1.0
```

<Warning>
  Use sampling ratio \< 1.0 in production (e.g., 0.1 for 10%) to reduce storage costs.
</Warning>

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Dashboard" icon="chart-simple" href="/observe/dashboard">
    View all task tracking data
  </Card>

  <Card title="Settings" icon="gear" href="/settings/general">
    Configure tracing
  </Card>
</CardGroup>


---

# Disk



Disk provides persistent file storage with paths, metadata, and secure download URLs. Projects with [end-to-end encryption](/security/encryption) enabled will automatically encrypt all disk files at rest.

<Card title="Agentic Disk SDK" icon="box" href="/tool/disk_tools">
  Bring filesystem to your agent with one-line code.
</Card>

## Quick Start [#quick-start]

<Steps>
  <Step title="Create a disk">
    <CodeGroup>
      ```python title="Python"
      import os
      from acontext import AcontextClient, FileUpload

      client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))
      disk = client.disks.create()
      ```

      ```typescript title="TypeScript"
      import { AcontextClient, FileUpload } from '@acontext/acontext';

      const client = new AcontextClient({
          apiKey: process.env.ACONTEXT_API_KEY,
      });
      const disk = await client.disks.create();
      ```
    </CodeGroup>
  </Step>

  <Step title="Upload a file">
    <CodeGroup>
      ```python title="Python"
      artifact = client.disks.artifacts.upsert(
          disk.id,
          file=FileUpload(filename="notes.md", content=b"# Meeting Notes"),
          file_path="/documents/",
          meta={"author": "alice"}
      )
      ```

      ```typescript title="TypeScript"
      const artifact = await client.disks.artifacts.upsert(disk.id, {
          file: new FileUpload({
              filename: "notes.md",
              content: Buffer.from("# Meeting Notes"),
          }),
          filePath: "/documents/",
          meta: { author: "alice" },
      });
      ```
    </CodeGroup>
  </Step>

  <Step title="Get file with URL">
    <CodeGroup>
      ```python title="Python"
      result = client.disks.artifacts.get(
          disk.id,
          file_path="/documents/",
          filename="notes.md",
          with_public_url=True,
          with_content=True
      )
      print(result.public_url)
      print(result.content.raw)
      ```

      ```typescript title="TypeScript"
      const result = await client.disks.artifacts.get(disk.id, {
          filePath: "/documents/",
          filename: "notes.md",
          withPublicUrl: true,
          withContent: true,
      });
      console.log(result.publicUrl);
      console.log(result.content?.raw);
      ```
    </CodeGroup>
  </Step>

  <Step title="List files">
    <CodeGroup>
      ```python title="Python"
      result = client.disks.artifacts.list(disk.id, path="/documents/")
      for artifact in result.artifacts:
          print(f"{artifact.filename} - {artifact.meta}")
      ```

      ```typescript title="TypeScript"
      const listResult = await client.disks.artifacts.list(disk.id, { path: "/documents/" });
      for (const artifact of listResult.artifacts) {
          console.log(`${artifact.filename} - ${JSON.stringify(artifact.meta)}`);
      }
      ```
    </CodeGroup>
  </Step>

  <Step title="Search files">
    <CodeGroup>
      ```python title="Python"
      # Search content with regex
      results = client.disks.artifacts.grep_artifacts(disk.id, query="TODO.*")

      # Find by path pattern
      results = client.disks.artifacts.glob_artifacts(disk.id, query="**/*.md")
      ```

      ```typescript title="TypeScript"
      // Search content with regex
      const grepResults = await client.disks.artifacts.grepArtifacts(disk.id, { query: "TODO.*" });

      // Find by path pattern
      const globResults = await client.disks.artifacts.globArtifacts(disk.id, { query: "**/*.md" });
      ```
    </CodeGroup>
  </Step>

  <Step title="Update metadata">
    <CodeGroup>
      ```python title="Python"
      client.disks.artifacts.update(
          disk.id,
          file_path="/documents/",
          filename="notes.md",
          meta={"author": "alice", "reviewed": True}
      )
      ```

      ```typescript title="TypeScript"
      await client.disks.artifacts.update(disk.id, {
          filePath: "/documents/",
          filename: "notes.md",
          meta: { author: "alice", reviewed: true },
      });
      ```
    </CodeGroup>
  </Step>

  <Step title="Clean up">
    <CodeGroup>
      ```python title="Python"
      client.disks.artifacts.delete(disk.id, file_path="/documents/", filename="notes.md")
      client.disks.delete(disk.id)
      ```

      ```typescript title="TypeScript"
      await client.disks.artifacts.delete(disk.id, { filePath: "/documents/", filename: "notes.md" });
      await client.disks.delete(disk.id);
      ```
    </CodeGroup>
  </Step>
</Steps>

## Complete Example [#complete-example]

<Accordion title="Full workflow">
  <CodeGroup>
    ```python title="Python"
    import os
    from acontext import AcontextClient, FileUpload

    client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))

    # Create disk
    disk = client.disks.create()

    # Upload file
    artifact = client.disks.artifacts.upsert(
        disk.id,
        file=FileUpload(filename="notes.md", content=b"# Meeting Notes\nQ4 goals discussed."),
        file_path="/meetings/",
        meta={"date": "2024-01-15"}
    )

    # Get with URL
    result = client.disks.artifacts.get(
        disk.id, file_path="/meetings/", filename="notes.md",
        with_public_url=True
    )
    print(f"URL: {result.public_url}")

    # List files
    files = client.disks.artifacts.list(disk.id, path="/meetings/")
    print(f"Found {len(files.artifacts)} files")

    # Cleanup
    client.disks.delete(disk.id)
    ```

    ```typescript title="TypeScript"
    import { AcontextClient, FileUpload } from '@acontext/acontext';

    const client = new AcontextClient({
        apiKey: process.env.ACONTEXT_API_KEY,
    });

    // Create disk
    const disk = await client.disks.create();

    // Upload file
    const artifact = await client.disks.artifacts.upsert(disk.id, {
        file: new FileUpload({
            filename: "notes.md",
            content: Buffer.from("# Meeting Notes\nQ4 goals discussed."),
        }),
        filePath: "/meetings/",
        meta: { date: "2024-01-15" },
    });

    // Get with URL
    const result = await client.disks.artifacts.get(disk.id, {
        filePath: "/meetings/",
        filename: "notes.md",
        withPublicUrl: true,
    });
    console.log(`URL: ${result.publicUrl}`);

    // List files
    const files = await client.disks.artifacts.list(disk.id, { path: "/meetings/" });
    console.log(`Found ${files.artifacts.length} files`);

    // Cleanup
    await client.disks.delete(disk.id);
    ```
  </CodeGroup>
</Accordion>

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Sandbox" icon="terminal" href="/store/sandbox">
    Execute code with disk file transfer
  </Card>

  <Card title="Dashboard" icon="chart-simple" href="/observe/dashboard">
    View artifacts in the dashboard
  </Card>
</CardGroup>


---

# Store with User



Associate resources with user identifiers for multi-tenant apps, per-user isolation, and simplified cleanup.

## Supported Resources [#supported-resources]

| Resource        | Create                     | List                       | Cascade Delete |
| --------------- | -------------------------- | -------------------------- | -------------- |
| Sessions        | `user="alice@example.com"` | `user="alice@example.com"` | ✓              |
| Disks           | `user="alice@example.com"` | `user="alice@example.com"` | ✓              |
| Skills          | `user="alice@example.com"` | `user="alice@example.com"` | ✓              |
| Learning Spaces | `user="alice@example.com"` | `user="alice@example.com"` | ✓              |

## Create Resources with User [#create-resources-with-user]

<CodeGroup>
  ```python title="Python"
  import os
  from acontext import AcontextClient, FileUpload

  client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))

  # Session
  session = client.sessions.create(user="alice@example.com")

  # Disk
  disk = client.disks.create(user="alice@example.com")

  # Skill
  with open("skill.zip", "rb") as f:
      skill = client.skills.create(
          file=FileUpload(filename="skill.zip", content=f.read()),
          user="alice@example.com"
      )

  # Learning Space
  space = client.learning_spaces.create(user="alice@example.com")
  ```

  ```typescript title="TypeScript"
  import { AcontextClient, FileUpload } from '@acontext/acontext';
  import * as fs from 'fs';

  const client = new AcontextClient({
      apiKey: process.env.ACONTEXT_API_KEY,
  });

  // Session
  const session = await client.sessions.create({ user: "alice@example.com" });

  // Disk
  const disk = await client.disks.create({ user: "alice@example.com" });

  // Skill
  const fileContent = fs.readFileSync("skill.zip");
  const skill = await client.skills.create({
      file: new FileUpload({ filename: "skill.zip", content: fileContent }),
      user: "alice@example.com",
  });

  // Learning Space
  const space = await client.learningSpaces.create({ user: "alice@example.com" });
  ```
</CodeGroup>

<Note>
  Users are created automatically when first referenced. No explicit user creation needed.
</Note>

## Filter by User [#filter-by-user]

<CodeGroup>
  ```python title="Python"
  # Sessions
  sessions = client.sessions.list(user="alice@example.com")

  # Disks
  disks = client.disks.list(user="alice@example.com")

  # Skills
  skills = client.skills.list_catalog(user="alice@example.com")

  # Learning Spaces
  spaces = client.learning_spaces.list(user="alice@example.com")
  ```

  ```typescript title="TypeScript"
  // Sessions
  const sessions = await client.sessions.list({ user: "alice@example.com" });

  // Disks
  const disks = await client.disks.list({ user: "alice@example.com" });

  // Skills
  const skills = await client.skills.listCatalog({ user: "alice@example.com" });

  // Learning Spaces
  const spaces = await client.learningSpaces.list({ user: "alice@example.com" });
  ```
</CodeGroup>

## Get User Resources [#get-user-resources]

Check how many resources a user has before cleanup or for analytics:

<CodeGroup>
  ```python title="Python"
  resources = client.users.get_resources("alice@example.com")
  print(f"Sessions: {resources.counts.sessions_count}")
  print(f"Disks: {resources.counts.disks_count}")
  print(f"Skills: {resources.counts.skills_count}")
  ```

  ```typescript title="TypeScript"
  const resources = await client.users.getResources("alice@example.com");
  console.log(`Sessions: ${resources.counts.sessions_count}`);
  console.log(`Disks: ${resources.counts.disks_count}`);
  console.log(`Skills: ${resources.counts.skills_count}`);
  ```
</CodeGroup>

## Delete User (Cascade) [#delete-user-cascade]

Deleting a user removes all associated Sessions, Disks, and Skills:

<CodeGroup>
  ```python title="Python"
  client.users.delete("alice@example.com")
  ```

  ```typescript title="TypeScript"
  await client.users.delete("alice@example.com");
  ```
</CodeGroup>

<Warning>
  This permanently deletes all resources associated with the user.
</Warning>

## Complete Example [#complete-example]

<Accordion title="Full workflow">
  <CodeGroup>
    ```python title="Python"
    import os
    from acontext import AcontextClient

    client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))
    user_id = "demo@example.com"

    # Create resources
    session = client.sessions.create(user=user_id)
    disk = client.disks.create(user=user_id)

    # List resources
    sessions = client.sessions.list(user=user_id)
    disks = client.disks.list(user=user_id)
    print(f"User has {len(sessions.items)} sessions, {len(disks.items)} disks")

    # Get resource counts
    resources = client.users.get_resources(user_id)
    print(f"Total: {resources.counts.sessions_count} sessions, {resources.counts.disks_count} disks, {resources.counts.skills_count} skills")

    # Delete user and all resources
    client.users.delete(user_id)
    ```

    ```typescript title="TypeScript"
    import { AcontextClient } from '@acontext/acontext';

    const client = new AcontextClient({
        apiKey: process.env.ACONTEXT_API_KEY,
    });
    const userId = "demo@example.com";

    // Create resources
    const session = await client.sessions.create({ user: userId });
    const disk = await client.disks.create({ user: userId });

    // List resources
    const sessions = await client.sessions.list({ user: userId });
    const disks = await client.disks.list({ user: userId });
    console.log(`User has ${sessions.items.length} sessions, ${disks.items.length} disks`);

    // Get resource counts
    const resources = await client.users.getResources(userId);
    console.log(`Total: ${resources.counts.sessions_count} sessions, ${resources.counts.disks_count} disks, ${resources.counts.skills_count} skills`);

    // Delete user and all resources
    await client.users.delete(userId);
    ```
  </CodeGroup>
</Accordion>

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Disk Storage" icon="database" href="/store/disk">
    File storage API
  </Card>

  <Card title="Sessions" icon="folder" href="/store/messages/multi-provider">
    Message storage API
  </Card>

  <Card title="Skills" icon="wand-magic-sparkles" href="/store/skill">
    Skills storage API
  </Card>

  <Card title="Learning Spaces" icon="brain" href="/learn/learning-spaces">
    Organize agent knowledge by user
  </Card>
</CardGroup>


---

# Sandbox



Sandbox provides secure, isolated containers for executing shell commands.

<Card title="Agentic Sandbox Tools" icon="terminal" href="/tool/bash_tools">
  Give your agent command execution with pre-built tools.
</Card>

## Quick Start [#quick-start]

<Steps>
  <Step title="Create a sandbox">
    <CodeGroup>
      ```python title="Python"
      import os
      from acontext import AcontextClient

      client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))
      sandbox = client.sandboxes.create()
      ```

      ```typescript title="TypeScript"
      import { AcontextClient } from '@acontext/acontext';

      const client = new AcontextClient({
          apiKey: process.env.ACONTEXT_API_KEY,
      });
      const sandbox = await client.sandboxes.create();
      ```
    </CodeGroup>
  </Step>

  <Step title="Execute commands">
    <CodeGroup>
      ```python title="Python"
      result = client.sandboxes.exec_command(
          sandbox_id=sandbox.sandbox_id,
          command="echo 'Hello!' && python3 --version"
      )
      print(f"stdout: {result.stdout}")
      print(f"exit_code: {result.exit_code}")
      ```

      ```typescript title="TypeScript"
      const result = await client.sandboxes.execCommand({
          sandboxId: sandbox.sandbox_id,
          command: "echo 'Hello!' && python3 --version",
      });
      console.log(`stdout: ${result.stdout}`);
      console.log(`exit_code: ${result.exit_code}`);
      ```
    </CodeGroup>
  </Step>

  <Step title="Kill sandbox">
    <CodeGroup>
      ```python title="Python"
      client.sandboxes.kill(sandbox.sandbox_id)
      ```

      ```typescript title="TypeScript"
      await client.sandboxes.kill(sandbox.sandbox_id);
      ```
    </CodeGroup>
  </Step>
</Steps>

## Disk Integration [#disk-integration]

Transfer files between disk and sandbox:

<Steps>
  <Step title="Setup">
    <CodeGroup>
      ```python title="Python"
      from acontext import FileUpload

      disk = client.disks.create()
      sandbox = client.sandboxes.create()

      # Upload file to disk
      client.disks.artifacts.upsert(
          disk.id,
          file=FileUpload(filename="input.txt", content=b"Line 1\nLine 2"),
          file_path="/input/"
      )
      ```

      ```typescript title="TypeScript"
      import { FileUpload } from '@acontext/acontext';

      const disk = await client.disks.create();
      const sandbox = await client.sandboxes.create();

      // Upload file to disk
      await client.disks.artifacts.upsert(disk.id, {
          file: new FileUpload({
              filename: "input.txt",
              content: Buffer.from("Line 1\nLine 2"),
          }),
          filePath: "/input/",
      });
      ```
    </CodeGroup>
  </Step>

  <Step title="Download to sandbox">
    <CodeGroup>
      ```python title="Python"
      client.disks.artifacts.download_to_sandbox(
          disk_id=disk.id,
          file_path="/input/",
          filename="input.txt",
          sandbox_id=sandbox.sandbox_id,
          sandbox_path="/workspace/"
      )
      ```

      ```typescript title="TypeScript"
      await client.disks.artifacts.downloadToSandbox(disk.id, {
          filePath: "/input/",
          filename: "input.txt",
          sandboxId: sandbox.sandbox_id,
          sandboxPath: "/workspace/",
      });
      ```
    </CodeGroup>
  </Step>

  <Step title="Process in sandbox">
    <CodeGroup>
      ```python title="Python"
      client.sandboxes.exec_command(
          sandbox_id=sandbox.sandbox_id,
          command="wc -l /workspace/input.txt > /workspace/output.txt"
      )
      ```

      ```typescript title="TypeScript"
      await client.sandboxes.execCommand({
          sandboxId: sandbox.sandbox_id,
          command: "wc -l /workspace/input.txt > /workspace/output.txt",
      });
      ```
    </CodeGroup>
  </Step>

  <Step title="Upload result to disk">
    <CodeGroup>
      ```python title="Python"
      client.disks.artifacts.upload_from_sandbox(
          disk_id=disk.id,
          sandbox_id=sandbox.sandbox_id,
          sandbox_path="/workspace/",
          sandbox_filename="output.txt",
          file_path="/output/"
      )
      ```

      ```typescript title="TypeScript"
      await client.disks.artifacts.uploadFromSandbox(disk.id, {
          sandboxId: sandbox.sandbox_id,
          sandboxPath: "/workspace/",
          sandboxFilename: "output.txt",
          filePath: "/output/",
      });
      ```
    </CodeGroup>
  </Step>

  <Step title="Clean up">
    <CodeGroup>
      ```python title="Python"
      client.sandboxes.kill(sandbox.sandbox_id)
      # Disk persists after sandbox is killed
      ```

      ```typescript title="TypeScript"
      await client.sandboxes.kill(sandbox.sandbox_id);
      // Disk persists after sandbox is killed
      ```
    </CodeGroup>
  </Step>
</Steps>

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Disk" icon="hard-drive" href="/store/disk">
    Persistent file storage
  </Card>

  <Card title="Bash Tools" icon="terminal" href="/tool/bash_tools">
    Agent sandbox tools
  </Card>
</CardGroup>


---

# Session Events



Add structured event markers to sessions that are stored alongside messages and rendered chronologically. Events support any type with a JSON payload, with built-in SDK helpers for common types like disk operations and free-text notes.

## Event Types [#event-types]

The SDK provides two built-in event types with validation:

* **DiskEvent**: Track disk operations (file uploads, downloads, etc.) with `disk_id`, `path`, and optional `note`
* **TextEvent**: Add free-text annotations with a `text` field

The API accepts any `type` string — you can create custom event types without API changes.

## Adding Events [#adding-events]

<CodeGroup>
  ```python title="Python"
  import os
  from acontext import AcontextClient
  from acontext.event import DiskEvent, TextEvent

  client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))

  session = client.sessions.create()

  # Add a disk-related event
  client.sessions.add_event(
      session.id,
      DiskEvent(disk_id="disk-uuid", path="/data/report.csv", note="Uploaded report"),
  )

  # Add a free-text event
  client.sessions.add_event(
      session.id,
      TextEvent(text="User switched to dark mode"),
  )
  ```

  ```typescript title="TypeScript"
  import { AcontextClient, DiskEvent, TextEvent } from '@acontext/acontext';

  const client = new AcontextClient({ apiKey: process.env.ACONTEXT_API_KEY });

  const session = await client.sessions.create();

  // Add a disk-related event
  await client.sessions.addEvent(
      session.id,
      new DiskEvent({ diskId: 'disk-uuid', path: '/data/report.csv', note: 'Uploaded report' }),
  );

  // Add a free-text event
  await client.sessions.addEvent(
      session.id,
      new TextEvent({ text: 'User switched to dark mode' }),
  );
  ```
</CodeGroup>

## Retrieving Events [#retrieving-events]

<CodeGroup>
  ```python title="Python"
  # Get events only
  events = client.sessions.get_events(session.id, limit=50)
  for event in events.items:
      print(f"{event.type}: {event.data}")

  # Get events alongside messages (unified timeline)
  timeline = client.sessions.get_messages(session.id, with_events=True)
  print(f"Messages: {len(timeline.items)}")
  print(f"Events: {len(timeline.events or [])}")
  ```

  ```typescript title="TypeScript"
  // Get events only
  const events = await client.sessions.getEvents(session.id, { limit: 50 });
  for (const event of events.items) {
      console.log(`${event.type}: ${JSON.stringify(event.data)}`);
  }

  // Get events alongside messages (unified timeline)
  const timeline = await client.sessions.getMessages(session.id, { withEvents: true });
  console.log(`Messages: ${timeline.items.length}`);
  console.log(`Events: ${(timeline.events || []).length}`);
  ```
</CodeGroup>

## Unified Timeline [#unified-timeline]

When you pass `with_events=true` to `get_messages`, events are returned alongside messages. Events within the time window of the returned messages page are included. The Dashboard renders both in a single chronological timeline.

## Lifecycle [#lifecycle]

* **Cascade delete**: Events are automatically deleted when their session is deleted
* **Session copy**: Events are included when copying a session
* **No MQ processing**: Events are simple CRUD — no background processing needed

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Session Overview" href="/docs/engineering/whatis">
    Learn about session fundamentals and message management.
  </Card>

  <Card title="Copy Session" href="/docs/engineering/copy_session">
    Duplicate sessions including events for experimentation.
  </Card>
</CardGroup>


---

# Agent Skill



Skills are folders of instructions, scripts, and resources that agents can discover and use. Upload skills as ZIP files containing a `SKILL.md` with name and description.

<Note>
  Skills can also be auto-generated by the skill memory pipeline. See [Skill Memory Quickstart](/learn/quick).
</Note>

<CardGroup cols="2">
  <Card title="Sandbox with Skills" icon="terminal" href="/tool/bash_tools#mounting-agent-skills">
    Mount skills in sandbox
  </Card>

  <Card title="Skill Content Tools" icon="wand-magic-sparkles" href="/tool/skill_tools">
    Read skill files without sandbox
  </Card>
</CardGroup>

## Where to Find Skills [#where-to-find-skills]

<CardGroup cols="2">
  <Card title="Agent Skills Directory" icon="sparkles" href="https://agentskills.io/home">
    Browse community skills in the open format
  </Card>

  <Card title="Anthropic Skills" icon="github" href="https://github.com/anthropics/skills/tree/main/skills">
    Pre-built skills from Anthropic
  </Card>
</CardGroup>

## Skill ZIP Structure [#skill-zip-structure]

```
my-skill.zip
├── SKILL.md          # Required: name, description, instructions
├── scripts/          # Optional: executable scripts
│   └── extract.py
└── resources/        # Optional: data files, templates
    └── template.json
```

The `SKILL.md` file must include frontmatter with name and description:

```markdown
---
name: data-extraction
description: Extract structured data from documents
---

# Data Extraction Skill
Instructions for the agent...
```

## Quick Start [#quick-start]

<Steps>
  <Step title="Upload a skill">
    <CodeGroup>
      ```python title="Python"
      import os
      from acontext import AcontextClient, FileUpload

      client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))

      with open("my-skill.zip", "rb") as f:
          skill = client.skills.create(
              file=FileUpload(filename="my-skill.zip", content=f.read()),
              meta={"version": "1.0"}
          )
      print(f"Created: {skill.name} ({skill.id})")
      ```

      ```typescript title="TypeScript"
      import { AcontextClient, FileUpload } from '@acontext/acontext';
      import * as fs from 'fs';

      const client = new AcontextClient({
          apiKey: process.env.ACONTEXT_API_KEY,
      });

      const fileContent = fs.readFileSync("my-skill.zip");
      const skill = await client.skills.create({
          file: new FileUpload({ filename: "my-skill.zip", content: fileContent }),
          meta: { version: "1.0" },
      });
      console.log(`Created: ${skill.name} (${skill.id})`);
      ```
    </CodeGroup>
  </Step>

  <Step title="Browse catalog">
    <CodeGroup>
      ```python title="Python"
      catalog = client.skills.list_catalog()
      for item in catalog.items:
          print(f"{item.name}: {item.description}")
      ```

      ```typescript title="TypeScript"
      const catalog = await client.skills.listCatalog();
      for (const item of catalog.items) {
          console.log(`${item.name}: ${item.description}`);
      }
      ```
    </CodeGroup>
  </Step>

  <Step title="Get skill details">
    <CodeGroup>
      ```python title="Python"
      skill = client.skills.get(skill_id)
      for file_info in skill.file_index:
          print(f"{file_info.path} ({file_info.mime})")
      ```

      ```typescript title="TypeScript"
      const skillDetails = await client.skills.get(skillId);
      for (const fileInfo of skillDetails.fileIndex || []) {
          console.log(`${fileInfo.path} (${fileInfo.mime})`);
      }
      ```
    </CodeGroup>
  </Step>

  <Step title="Read files">
    <CodeGroup>
      ```python title="Python"
      result = client.skills.get_file(skill_id=skill.id, file_path="SKILL.md")
      print(result.content.raw)

      # Binary files return a URL instead
      result = client.skills.get_file(skill_id=skill.id, file_path="images/diagram.png")
      print(result.url)
      ```

      ```typescript title="TypeScript"
      const fileResult = await client.skills.getFile({ skillId: skill.id, filePath: "SKILL.md" });
      console.log(fileResult.content?.raw);

      // Binary files return a URL instead
      const imageResult = await client.skills.getFile({ skillId: skill.id, filePath: "images/diagram.png" });
      console.log(imageResult.url);
      ```
    </CodeGroup>
  </Step>

  <Step title="Delete skill">
    <CodeGroup>
      ```python title="Python"
      client.skills.delete(skill.id)
      ```

      ```typescript title="TypeScript"
      await client.skills.delete(skill.id);
      ```
    </CodeGroup>
  </Step>
</Steps>

## Disk Storage [#disk-storage]

Each skill stores its files on an internal [Disk](/store/disk). The `disk_id` is returned when you create or get a skill.

<CodeGroup>
  ```python title="Python"
  skill = client.skills.get(skill_id)
  print(skill.disk_id)  # UUID of the backing Disk
  ```

  ```typescript title="TypeScript"
  const skill = await client.skills.get(skillId);
  console.log(skill.diskId); // UUID of the backing Disk
  ```
</CodeGroup>

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Sandbox" icon="terminal" href="/store/sandbox">
    Execute skill scripts
  </Card>

  <Card title="Skill Tools" icon="wand-magic-sparkles" href="/tool/skill_tools">
    Agent skill reading tools
  </Card>
</CardGroup>


---

# Sandbox Tools



Pre-built tools for LLMs to execute bash commands, edit files, and export results in isolated containers.

## Available Tools [#available-tools]

| Tool                     | Description                            |
| ------------------------ | -------------------------------------- |
| `bash_execution_sandbox` | Execute bash commands                  |
| `text_editor_sandbox`    | View, create, edit text files          |
| `export_file_sandbox`    | Export files to disk with download URL |

<Tip>
  Sandbox tools support mounting [Agent Skills](/store/skill) directly into the sandbox filesystem, allowing LLMs to access skill files and execute skill scripts.
</Tip>

## Quick Start [#quick-start]

<CodeGroup>
  ```python title="Python"
  import json
  import os
  from acontext import AcontextClient
  from acontext.agent.sandbox import SANDBOX_TOOLS
  from openai import OpenAI

  client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))
  openai_client = OpenAI()

  # Create sandbox and disk
  sandbox = client.sandboxes.create()
  disk = client.disks.create()

  # Create context (optionally mount skills)
  ctx = SANDBOX_TOOLS.format_context(
      client,
      sandbox_id=sandbox.sandbox_id,
      disk_id=disk.id,
      # mount_skills=["skill-uuid"]
  )

  tools = SANDBOX_TOOLS.to_openai_tool_schema()
  context_prompt = ctx.get_context_prompt()

  messages = [
      {"role": "system", "content": f"You have sandbox access.\n\n{context_prompt}"},
      {"role": "user", "content": "Create and run a Python hello world script"}
  ]

  # Agent loop
  while True:
      response = openai_client.chat.completions.create(
          model="gpt-4.1", messages=messages, tools=tools
      )
      message = response.choices[0].message
      messages.append(message)

      if not message.tool_calls:
          print(f"Assistant: {message.content}")
          break

      for tc in message.tool_calls:
          result = SANDBOX_TOOLS.execute_tool(ctx, tc.function.name, json.loads(tc.function.arguments))
          messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})

  client.sandboxes.kill(sandbox.sandbox_id)
  ```

  ```typescript title="TypeScript"
  import { AcontextClient, SANDBOX_TOOLS } from '@acontext/acontext';
  import OpenAI from 'openai';

  const client = new AcontextClient({
      apiKey: process.env.ACONTEXT_API_KEY,
  });
  const openai = new OpenAI();

  // Create sandbox and disk
  const sandbox = await client.sandboxes.create();
  const disk = await client.disks.create();

  // Create context (optionally mount skills)
  const ctx = await SANDBOX_TOOLS.formatContext(
      client,
      sandbox.sandbox_id,
      disk.id,
      // ["skill-uuid"]
  );

  const tools = SANDBOX_TOOLS.toOpenAIToolSchema();
  const contextPrompt = ctx.getContextPrompt();

  const messages: OpenAI.ChatCompletionMessageParam[] = [
      { role: "system", content: `You have sandbox access.\n\n${contextPrompt}` },
      { role: "user", content: "Create and run a Python hello world script" },
  ];

  // Agent loop
  while (true) {
      const response = await openai.chat.completions.create({
          model: "gpt-4.1",
          messages,
          tools,
      });
      const message = response.choices[0].message;
      messages.push(message);

      if (!message.tool_calls) {
          console.log(`Assistant: ${message.content}`);
          break;
      }

      for (const tc of message.tool_calls) {
          const result = await SANDBOX_TOOLS.executeTool(ctx, tc.function.name, JSON.parse(tc.function.arguments));
          messages.push({ role: "tool", tool_call_id: tc.id, content: result });
      }
  }

  await client.sandboxes.kill(sandbox.sandbox_id);
  ```
</CodeGroup>

## Mounting Agent Skills [#mounting-agent-skills]

Mount [Agent Skills](/store/skill) into sandbox at `/skills/{skill_name}/`:

<CodeGroup>
  ```python title="Python"
  ctx = SANDBOX_TOOLS.format_context(
      client,
      sandbox_id=sandbox.sandbox_id,
      disk_id=disk.id,
      mount_skills=["skill-uuid-1", "skill-uuid-2"]
  )

  # Or mount after creation
  ctx.mount_skills(["skill-uuid-3"])
  ```

  ```typescript title="TypeScript"
  const ctx = await SANDBOX_TOOLS.formatContext(
      client,
      sandbox.sandbox_id,
      disk.id,
      ["skill-uuid-1", "skill-uuid-2"]
  );

  // Or mount after creation
  await ctx.mountSkills(["skill-uuid-3"]);
  ```
</CodeGroup>

<Note>
  Mounting skills modifies the context prompt. Always call `get_context_prompt()` after mounting to include the updated skill information in your system message.
</Note>

## Tool Reference [#tool-reference]

### bash\_execution\_sandbox [#bash_execution_sandbox]

```json
{"command": "python3 script.py", "timeout": 30}
```

Returns: `{"stdout": "...", "stderr": "...", "exit_code": 0}`

### text\_editor\_sandbox [#text_editor_sandbox]

| Command       | Parameters                      | Description           |
| ------------- | ------------------------------- | --------------------- |
| `view`        | `path`, `view_range` (optional) | Read file             |
| `create`      | `path`, `file_text`             | Create/overwrite file |
| `str_replace` | `path`, `old_str`, `new_str`    | Find and replace      |

### export\_file\_sandbox [#export_file_sandbox]

```json
{"sandbox_path": "/workspace/", "sandbox_filename": "output.txt"}
```

Returns: `{"message": "...", "public_url": "https://..."}`

## Sandbox Environment [#sandbox-environment]

Pre-installed: Python 3, ripgrep, fd, sqlite3, jq, imagemagick, standard Unix tools.

<Warning>
  **Limitations:** No blocking calls (`plt.show()`, `input()`), expires after 30 minutes.
</Warning>

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Sandbox API" icon="terminal" href="/store/sandbox">
    Low-level sandbox API
  </Card>

  <Card title="Skill Tools" icon="wand-magic-sparkles" href="/tool/skill_tools">
    Read-only skill access
  </Card>
</CardGroup>


---

# Disk Tools



Pre-built tools for LLMs to read, write, and search files on disks through function calling.

## Available Tools [#available-tools]

| Tool                  | Description                    |
| --------------------- | ------------------------------ |
| `write_file_disk`     | Create or overwrite text files |
| `read_file_disk`      | Read file contents             |
| `replace_string_disk` | Find and replace text          |
| `list_disk`           | List files and directories     |
| `download_file_disk`  | Get public download URL        |
| `grep_disk`           | Search contents with regex     |
| `glob_disk`           | Find files by path pattern     |

## Quick Start [#quick-start]

<CodeGroup>
  ```python title="Python"
  import json
  import os
  from acontext import AcontextClient
  from acontext.agent.disk import DISK_TOOLS
  from openai import OpenAI

  client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))
  openai_client = OpenAI()

  # Create disk and context
  disk = client.disks.create()
  ctx = DISK_TOOLS.format_context(client, disk.id)

  tools = DISK_TOOLS.to_openai_tool_schema()
  context_prompt = ctx.get_context_prompt()

  messages = [
      {"role": "system", "content": f"You have disk access.\n\n{context_prompt}"},
      {"role": "user", "content": "Create a todo.md file with 3 tasks"}
  ]

  # Agent loop
  while True:
      response = openai_client.chat.completions.create(
          model="gpt-4.1", messages=messages, tools=tools
      )
      message = response.choices[0].message
      messages.append(message)

      if not message.tool_calls:
          print(f"Assistant: {message.content}")
          break

      for tc in message.tool_calls:
          result = DISK_TOOLS.execute_tool(ctx, tc.function.name, json.loads(tc.function.arguments))
          messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})
  ```

  ```typescript title="TypeScript"
  import { AcontextClient, DISK_TOOLS } from '@acontext/acontext';
  import OpenAI from 'openai';

  const client = new AcontextClient({
      apiKey: process.env.ACONTEXT_API_KEY,
  });
  const openai = new OpenAI();

  // Create disk and context
  const disk = await client.disks.create();
  const ctx = DISK_TOOLS.formatContext(client, disk.id);

  const tools = DISK_TOOLS.toOpenAIToolSchema();
  const contextPrompt = ctx.getContextPrompt();

  const messages: OpenAI.ChatCompletionMessageParam[] = [
      { role: "system", content: `You have disk access.\n\n${contextPrompt}` },
      { role: "user", content: "Create a todo.md file with 3 tasks" },
  ];

  // Agent loop
  while (true) {
      const response = await openai.chat.completions.create({
          model: "gpt-4.1",
          messages,
          tools,
      });
      const message = response.choices[0].message;
      messages.push(message);

      if (!message.tool_calls) {
          console.log(`Assistant: ${message.content}`);
          break;
      }

      for (const tc of message.tool_calls) {
          const result = await DISK_TOOLS.executeTool(ctx, tc.function.name, JSON.parse(tc.function.arguments));
          messages.push({ role: "tool", tool_call_id: tc.id, content: result });
      }
  }
  ```
</CodeGroup>

## Tool Reference [#tool-reference]

### write\_file\_disk [#write_file_disk]

```json
{"filename": "notes.md", "content": "# Notes", "file_path": "/docs/"}
```

### read\_file\_disk [#read_file_disk]

```json
{"filename": "notes.md", "file_path": "/docs/", "line_offset": 0, "line_limit": 100}
```

### replace\_string\_disk [#replace_string_disk]

```json
{"filename": "notes.md", "old_string": "TODO", "new_string": "DONE", "file_path": "/"}
```

### list\_disk [#list_disk]

```json
{"file_path": "/docs/"}
```

### download\_file\_disk [#download_file_disk]

```json
{"filename": "report.pdf", "file_path": "/", "expire": 3600}
```

### grep\_disk [#grep_disk]

Search file contents with regex:

```json
{"query": "TODO.*", "limit": 100}
```

<Warning>
  Uses **regex** syntax: `.*` means any characters, `*` alone means zero or more of preceding character.
</Warning>

### glob\_disk [#glob_disk]

Find files by path pattern:

```json
{"query": "**/*.md", "limit": 100}
```

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Disk API" icon="hard-drive" href="/store/disk">
    Low-level disk API
  </Card>

  <Card title="Sandbox Tools" icon="terminal" href="/tool/bash_tools">
    Execute code in sandbox
  </Card>
</CardGroup>


---

# Skill Content Tools



Lightweight tools for LLMs to access skill content through function calling. Use when skills contain only reference content (no scripts to execute).

<Note>
  For skills with executable scripts, use [Sandbox Tools with mounted skills](/tool/bash_tools#mounting-agent-skills) instead.
</Note>

## Available Tools [#available-tools]

| Tool             | Description                       |
| ---------------- | --------------------------------- |
| `get_skill`      | Get skill metadata and file index |
| `get_skill_file` | Read a file from a skill          |

## Quick Start [#quick-start]

<CodeGroup>
  ```python title="Python"
  import json
  import os
  from acontext import AcontextClient
  from acontext.agent.skill import SKILL_TOOLS
  from openai import OpenAI

  client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))
  openai_client = OpenAI()

  # Preload skills by UUID
  skill_ids = ["uuid-of-skill-1", "uuid-of-skill-2"]
  ctx = SKILL_TOOLS.format_context(client, skill_ids)

  tools = SKILL_TOOLS.to_openai_tool_schema()
  skills_context = ctx.get_context_prompt()

  messages = [
      {"role": "system", "content": f"You have skill access.\n\n{skills_context}"},
      {"role": "user", "content": "What guidelines are in the internal-comms skill?"}
  ]

  # Agent loop
  while True:
      response = openai_client.chat.completions.create(
          model="gpt-4.1", messages=messages, tools=tools
      )
      message = response.choices[0].message
      messages.append(message)

      if not message.tool_calls:
          print(f"Assistant: {message.content}")
          break

      for tc in message.tool_calls:
          result = SKILL_TOOLS.execute_tool(ctx, tc.function.name, json.loads(tc.function.arguments))
          messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})
  ```

  ```typescript title="TypeScript"
  import { AcontextClient, SKILL_TOOLS } from '@acontext/acontext';
  import OpenAI from 'openai';

  const client = new AcontextClient({
      apiKey: process.env.ACONTEXT_API_KEY,
  });
  const openai = new OpenAI();

  // Preload skills by UUID
  const skillIds = ["uuid-of-skill-1", "uuid-of-skill-2"];
  const ctx = await SKILL_TOOLS.formatContext(client, skillIds);

  const tools = SKILL_TOOLS.toOpenAIToolSchema();
  const skillsContext = ctx.getContextPrompt();

  const messages: OpenAI.ChatCompletionMessageParam[] = [
      { role: "system", content: `You have skill access.\n\n${skillsContext}` },
      { role: "user", content: "What guidelines are in the internal-comms skill?" },
  ];

  // Agent loop
  while (true) {
      const response = await openai.chat.completions.create({
          model: "gpt-4.1",
          messages,
          tools,
      });
      const message = response.choices[0].message;
      messages.push(message);

      if (!message.tool_calls) {
          console.log(`Assistant: ${message.content}`);
          break;
      }

      for (const tc of message.tool_calls) {
          const result = await SKILL_TOOLS.executeTool(ctx, tc.function.name, JSON.parse(tc.function.arguments));
          messages.push({ role: "tool", tool_call_id: tc.id, content: result });
      }
  }
  ```
</CodeGroup>

## Context Prompt [#context-prompt]

The context includes available skills as XML:

```xml
<available_skills>
<skill>
<name>data-extraction</name>
<description>Extract structured data from documents</description>
</skill>
</available_skills>
```

## Tool Reference [#tool-reference]

### get\_skill [#get_skill]

```json
{"skill_name": "data-extraction"}
```

Returns: Skill ID, description, and file index with MIME types.

### get\_skill\_file [#get_skill_file]

```json
{"skill_name": "data-extraction", "file_path": "SKILL.md"}
```

Returns: File content (text) or presigned URL (binary).

<Warning>
  **Read-only:** These tools can only read skill content. To execute skill scripts, use [Sandbox Tools](/tool/bash_tools#mounting-agent-skills).
</Warning>

## Next Steps [#next-steps]

<CardGroup cols="3">
  <Card title="Skills API" icon="wand-magic-sparkles" href="/store/skill">
    Upload and manage skills
  </Card>

  <Card title="Sandbox Tools" icon="terminal" href="/tool/bash_tools">
    Execute skill scripts
  </Card>

  <Card title="Skill Memory" icon="brain" href="/learn/quick">
    Build skills from agent sessions
  </Card>
</CardGroup>


---

# Session Configs



Sessions can store arbitrary configuration data that persists for the session lifetime. Use configs to track agent settings, model parameters, or any session-level metadata.

## Create Session with Configs [#create-session-with-configs]

<CodeGroup>
  ```python title="Python"
  session = client.sessions.create(
      configs={"agent": "bot1", "temperature": 0.7, "model": "gpt-4"}
  )
  ```

  ```typescript title="TypeScript"
  const session = await client.sessions.create({
    configs: { agent: "bot1", temperature: 0.7, model: "gpt-4" }
  });
  ```
</CodeGroup>

## Update Configs (Full Replacement) [#update-configs-full-replacement]

Use `update_configs` to completely replace all configs. Any keys not included will be removed.

<CodeGroup>
  ```python title="Python"
  # Replaces ALL configs - any missing keys are deleted
  client.sessions.update_configs(
      session_id=session.id,
      configs={"agent": "bot2", "temperature": 0.9}  # "model" key is now gone
  )
  ```

  ```typescript title="TypeScript"
  // Replaces ALL configs - any missing keys are deleted
  await client.sessions.updateConfigs(session.id, {
    configs: { agent: "bot2", temperature: 0.9 }  // "model" key is now gone
  });
  ```
</CodeGroup>

## Patch Configs (Partial Update) [#patch-configs-partial-update]

Use `patch_configs` to update only specific keys while preserving others. Pass `null`/`None` to delete a key.

<CodeGroup>
  ```python title="Python"
  # Add or update keys (preserves existing keys)
  updated = client.sessions.patch_configs(
      session_id=session.id,
      configs={"agent": "bot2", "new_setting": True}
  )
  print(updated)  # {"agent": "bot2", "temperature": 0.7, "model": "gpt-4", "new_setting": True}

  # Delete a key by passing None
  updated = client.sessions.patch_configs(
      session_id=session.id,
      configs={"model": None}  # Deletes "model" key
  )
  print(updated)  # {"agent": "bot2", "temperature": 0.7, "new_setting": True}
  ```

  ```typescript title="TypeScript"
  // Add or update keys (preserves existing keys)
  const updated = await client.sessions.patchConfigs(
    session.id,
    { agent: "bot2", new_setting: true }
  );
  console.log(updated);  // {agent: "bot2", temperature: 0.7, model: "gpt-4", new_setting: true}

  // Delete a key by passing null
  const result = await client.sessions.patchConfigs(
    session.id,
    { model: null }  // Deletes "model" key
  );
  console.log(result);  // {agent: "bot2", temperature: 0.7, new_setting: true}
  ```
</CodeGroup>

## PUT vs PATCH Comparison [#put-vs-patch-comparison]

| Aspect       | PUT (update\_configs) | PATCH (patch\_configs) |
| ------------ | --------------------- | ---------------------- |
| Semantics    | Full replacement      | Partial update         |
| Missing keys | Removed               | Preserved              |
| Null values  | Set to null           | Delete the key         |
| Return value | None                  | Updated configs        |

## Get Configs [#get-configs]

<CodeGroup>
  ```python title="Python"
  session = client.sessions.get_configs(session_id=session.id)
  print(session.configs)
  ```

  ```typescript title="TypeScript"
  const session = await client.sessions.getConfigs(session.id);
  console.log(session.configs);
  ```
</CodeGroup>

## Filter Sessions by Configs [#filter-sessions-by-configs]

Filter sessions by their `configs` metadata using JSONB containment. Only sessions where `configs` contains all key-value pairs in your filter will be returned.

### Basic Usage [#basic-usage]

<CodeGroup>
  ```python title="Python"
  import os
  from acontext import AcontextClient

  client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))

  # Create sessions with configs
  session1 = client.sessions.create(configs={"agent": "bot1", "env": "prod"})
  session2 = client.sessions.create(configs={"agent": "bot2", "env": "dev"})

  # Filter by single key
  sessions = client.sessions.list(filter_by_configs={"agent": "bot1"})
  # Returns: session1
  ```

  ```typescript title="TypeScript"
  import { AcontextClient } from '@acontext/acontext';

  const client = new AcontextClient({
      apiKey: process.env.ACONTEXT_API_KEY,
  });

  // Create sessions with configs
  const session1 = await client.sessions.create({ configs: { agent: "bot1", env: "prod" } });
  const session2 = await client.sessions.create({ configs: { agent: "bot2", env: "dev" } });

  // Filter by single key
  const sessions = await client.sessions.list({
      filterByConfigs: { agent: "bot1" }
  });
  // Returns: session1
  ```
</CodeGroup>

## Filter Examples [#filter-examples]

### Multiple Keys [#multiple-keys]

Sessions must match **all** key-value pairs:

<CodeGroup>
  ```python title="Python"
  # Only returns sessions with BOTH agent="bot1" AND env="prod"
  sessions = client.sessions.list(
      filter_by_configs={"agent": "bot1", "env": "prod"}
  )
  ```

  ```typescript title="TypeScript"
  // Only returns sessions with BOTH agent="bot1" AND env="prod"
  const sessions = await client.sessions.list({
      filterByConfigs: { agent: "bot1", env: "prod" }
  });
  ```
</CodeGroup>

### Nested Objects [#nested-objects]

Filter by nested config values:

<CodeGroup>
  ```python title="Python"
  # Create session with nested config
  session = client.sessions.create(
      configs={"agent": {"name": "bot1", "version": "2.0"}}
  )

  # Filter by nested object
  sessions = client.sessions.list(
      filter_by_configs={"agent": {"name": "bot1"}}
  )
  ```

  ```typescript title="TypeScript"
  // Create session with nested config
  const session = await client.sessions.create({
      configs: { agent: { name: "bot1", version: "2.0" } }
  });

  // Filter by nested object
  const sessions = await client.sessions.list({
      filterByConfigs: { agent: { name: "bot1" } }
  });
  ```
</CodeGroup>

### Combine with User Filter [#combine-with-user-filter]

<CodeGroup>
  ```python title="Python"
  # Filter by both user and configs
  sessions = client.sessions.list(
      user="alice@example.com",
      filter_by_configs={"agent": "bot1"}
  )
  ```

  ```typescript title="TypeScript"
  // Filter by both user and configs
  const sessions = await client.sessions.list({
      user: "alice@example.com",
      filterByConfigs: { agent: "bot1" }
  });
  ```
</CodeGroup>

## Important Behaviors [#important-behaviors]

<Note>
  * **Case-sensitive**: `{"Agent": "x"}` won't match `{"agent": "x"}`
  * **Type-sensitive**: `{"count": 1}` won't match `{"count": "1"}`
  * **Partial matching**: filter `{"a": 1}` matches configs `{"a": 1, "b": 2}`
  * **NULL excluded**: Sessions with `configs=null` are excluded from filtered results
</Note>

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Message Metadata" icon="tag" href="/store/messages/special/message-meta">
    Attach metadata to individual messages
  </Card>

  <Card title="Multi-provider Messages" icon="arrows-rotate" href="/store/messages/multi-provider">
    Store messages in any format
  </Card>
</CardGroup>


---

# Message Status



Every message has a `session_task_process_status` field that tells you where it is in the task extraction pipeline.

## Status Values [#status-values]

| Status             | Meaning                                                                                                |
| ------------------ | ------------------------------------------------------------------------------------------------------ |
| `pending`          | Message is queued for task extraction. The Task Agent hasn't processed it yet.                         |
| `running`          | The Task Agent is currently processing this message.                                                   |
| `success`          | Task extraction completed successfully.                                                                |
| `failed`           | Task extraction was attempted but failed (e.g., LLM error, timeout).                                   |
| `disable_tracking` | Task tracking is disabled for this session. The message was saved but never sent for extraction.       |
| `limit_exceed`     | The project's task creation usage limit was reached. The message was saved but extraction was skipped. |

## Lifecycle [#lifecycle]

```
store_message()
       │
       ├─ disable_task_tracking = true ──────► disable_tracking
       │
       ▼
   ┌────────┐     usage limit hit
   │pending │──────────────────────────────────► limit_exceed
   └───┬────┘
       │  Core picks up from MQ
       ▼
   ┌────────┐
   │running │
   └───┬────┘
       │  Task Agent processes
       ▼
  success / failed
```

## Checking Status [#checking-status]

<CodeGroup>
  ```python title="Python"
  import os
  from acontext import AcontextClient

  client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))

  messages = client.sessions.get_messages(session_id="<session-id>", format="acontext")
  for msg, msg_id in zip(messages.items, messages.ids):
      print(f"{msg_id}: {msg['session_task_process_status']}")
  ```

  ```typescript title="TypeScript"
  import { AcontextClient } from '@acontext/acontext';

  const client = new AcontextClient({ apiKey: process.env.ACONTEXT_API_KEY });

  const messages = await client.sessions.getMessages('<session-id>', { format: 'acontext' });
  for (const item of messages.items) {
      console.log(`${item.id}: ${item.session_task_process_status}`);
  }
  ```
</CodeGroup>

## Observing Status [#observing-status]

Use `messages_observing_status` to get aggregated counts of `observed` (success), `in_process` (running), and `pending` messages for a session. Messages with `disable_tracking` or `limit_exceed` are not included in these counts.

<CodeGroup>
  ```python title="Python"
  status = client.sessions.messages_observing_status(session_id="<session-id>")
  print(f"Observed: {status.observed}, In process: {status.in_process}, Pending: {status.pending}")
  ```

  ```typescript title="TypeScript"
  const status = await client.sessions.messagesObservingStatus('<session-id>');
  console.log(`Observed: ${status.observed}, In process: ${status.in_process}, Pending: ${status.pending}`);
  ```
</CodeGroup>

## Next Steps [#next-steps]

<CardGroup cols="3">
  <Card title="Agent Tasks" icon="list-check" href="/observe/agent_tasks">
    How task extraction works
  </Card>

  <Card title="Disable Tracking" icon="toggle-off" href="/observe/disable_tasks">
    Skip extraction for specific sessions
  </Card>

  <Card title="Session Buffer" icon="clock" href="/observe/buffer">
    How message batching works
  </Card>
</CardGroup>


---

# Multi-modal Messages



Acontext stores multi-modal content (images, audio, PDFs) as base64 within message parts. Format conversion between OpenAI and Anthropic is automatic.

## Images [#images]

<Tabs>
  <Tab title="OpenAI Format">
    <CodeGroup>
      ```python title="Python"
      import os
      import base64
      from acontext import AcontextClient

      client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))
      session = client.sessions.create()

      # From URL
      client.sessions.store_message(
          session_id=session.id,
          blob={
              "role": "user",
              "content": [
                  {"type": "text", "text": "What's in this image?"},
                  {"type": "image_url", "image_url": {"url": "https://example.com/image.png"}}
              ]
          },
          format="openai"
      )

      # From base64
      with open("image.png", "rb") as f:
          image_data = base64.b64encode(f.read()).decode("utf-8")

      client.sessions.store_message(
          session_id=session.id,
          blob={
              "role": "user",
              "content": [
                  {"type": "text", "text": "Describe this"},
                  {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_data}"}}
              ]
          },
          format="openai"
      )
      ```

      ```typescript title="TypeScript"
      import { AcontextClient } from '@acontext/acontext';
      import * as fs from 'fs';

      const client = new AcontextClient({ apiKey: process.env.ACONTEXT_API_KEY });
      const session = await client.sessions.create();

      // From URL
      await client.sessions.storeMessage(session.id, {
          role: "user",
          content: [
              { type: "text", text: "What's in this image?" },
              { type: "image_url", image_url: { url: "https://example.com/image.png" } }
          ]
      }, { format: "openai" });

      // From base64
      const imageData = fs.readFileSync("image.png").toString("base64");

      await client.sessions.storeMessage(session.id, {
          role: "user",
          content: [
              { type: "text", text: "Describe this" },
              { type: "image_url", image_url: { url: `data:image/png;base64,${imageData}` } }
          ]
      }, { format: "openai" });
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Anthropic Format">
    <CodeGroup>
      ```python title="Python"
      import os
      import base64
      from acontext import AcontextClient

      client = AcontextClient(api_key=os.getenv("ACONTEXT_API_KEY"))
      session = client.sessions.create()

      with open("image.png", "rb") as f:
          image_data = base64.b64encode(f.read()).decode("utf-8")

      client.sessions.store_message(
          session_id=session.id,
          blob={
              "role": "user",
              "content": [
                  {"type": "text", "text": "Describe this image"},
                  {
                      "type": "image",
                      "source": {"type": "base64", "media_type": "image/png", "data": image_data}
                  }
              ]
          },
          format="anthropic"
      )
      ```

      ```typescript title="TypeScript"
      import { AcontextClient } from '@acontext/acontext';
      import * as fs from 'fs';

      const client = new AcontextClient({ apiKey: process.env.ACONTEXT_API_KEY });
      const session = await client.sessions.create();

      const imageData = fs.readFileSync("image.png").toString("base64");

      await client.sessions.storeMessage(session.id, {
          role: "user",
          content: [
              { type: "text", text: "Describe this image" },
              {
                  type: "image",
                  source: { type: "base64", media_type: "image/png", data: imageData }
              }
          ]
      }, { format: "anthropic" });
      ```
    </CodeGroup>
  </Tab>
</Tabs>

## Audio [#audio]

<CodeGroup>
  ```python title="Python"
  import base64

  with open("audio.wav", "rb") as f:
      audio_data = base64.b64encode(f.read()).decode("utf-8")

  client.sessions.store_message(
      session_id=session.id,
      blob={
          "role": "user",
          "content": [
              {"type": "text", "text": "Transcribe this audio"},
              {"type": "input_audio", "input_audio": {"data": audio_data, "format": "wav"}}
          ]
      },
      format="openai"
  )
  ```

  ```typescript title="TypeScript"
  import * as fs from 'fs';

  const audioData = fs.readFileSync("audio.wav").toString("base64");

  await client.sessions.storeMessage(session.id, {
      role: "user",
      content: [
          { type: "text", text: "Transcribe this audio" },
          { type: "input_audio", input_audio: { data: audioData, format: "wav" } }
      ]
  }, { format: "openai" });
  ```
</CodeGroup>

## Documents [#documents]

<Tabs>
  <Tab title="OpenAI Format">
    <CodeGroup>
      ```python title="Python"
      import base64

      with open("document.pdf", "rb") as f:
          pdf_data = base64.b64encode(f.read()).decode("utf-8")

      client.sessions.store_message(
          session_id=session.id,
          blob={
              "role": "user",
              "content": [
                  {"type": "text", "text": "Summarize this PDF"},
                  {"type": "file", "file": {"file_data": pdf_data, "filename": "document.pdf"}}
              ]
          },
          format="openai"
      )
      ```

      ```typescript title="TypeScript"
      import * as fs from 'fs';

      const pdfData = fs.readFileSync("document.pdf").toString("base64");

      await client.sessions.storeMessage(session.id, {
          role: "user",
          content: [
              { type: "text", text: "Summarize this PDF" },
              { type: "file", file: { file_data: pdfData, filename: "document.pdf" } }
          ]
      }, { format: "openai" });
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Anthropic Format">
    <CodeGroup>
      ```python title="Python"
      import base64

      with open("report.pdf", "rb") as f:
          pdf_data = base64.b64encode(f.read()).decode("utf-8")

      client.sessions.store_message(
          session_id=session.id,
          blob={
              "role": "user",
              "content": [
                  {"type": "document", "source": {"type": "base64", "media_type": "application/pdf", "data": pdf_data}},
                  {"type": "text", "text": "Summarize the key findings"}
              ]
          },
          format="anthropic"
      )
      ```

      ```typescript title="TypeScript"
      import * as fs from 'fs';

      const pdfData = fs.readFileSync("report.pdf").toString("base64");

      await client.sessions.storeMessage(session.id, {
          role: "user",
          content: [
              { type: "document", source: { type: "base64", media_type: "application/pdf", data: pdfData } },
              { type: "text", text: "Summarize the key findings" }
          ]
      }, { format: "anthropic" });
      ```
    </CodeGroup>
  </Tab>
</Tabs>

## Retrieve Messages [#retrieve-messages]

Base64 content is returned as-is. Format conversion is automatic:

<CodeGroup>
  ```python title="Python"
  # Store as Anthropic, retrieve as OpenAI
  result = client.sessions.get_messages(session_id=session.id, format="openai")

  for msg in result.items:
      for part in msg.content:
          print(f"Type: {part.get('type')}")
  ```

  ```typescript title="TypeScript"
  // Store as Anthropic, retrieve as OpenAI
  const result = await client.sessions.getMessages(session.id, { format: "openai" });

  for (const msg of result.items) {
      for (const part of msg.content as any[]) {
          console.log(`Type: ${part.type}`);
      }
  }
  ```
</CodeGroup>

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Store Artifacts" icon="box" href="/store/disk">
    Store file artifacts alongside messages
  </Card>

  <Card title="Dashboard" icon="chart-simple" href="/observe/dashboard">
    View messages in the dashboard
  </Card>
</CardGroup>


---

# Multi-provider Messages



Acontext stores messages in OpenAI, Anthropic, or Gemini format and automatically converts between them on retrieval.

## Store Messages [#store-messages]

<Steps>
  <Step title="Create a session">
    <CodeGroup>
      ```python title="Python"
      import os
      from acontext import AcontextClient

      client = AcontextClient(
          api_key=os.getenv("ACONTEXT_API_KEY"),
      )
      session = client.sessions.create()
      ```

      ```typescript title="TypeScript"
      import { AcontextClient } from '@acontext/acontext';

      const client = new AcontextClient({
          apiKey: process.env.ACONTEXT_API_KEY,
      });
      const session = await client.sessions.create();
      ```
    </CodeGroup>

    <Tip>
      You can create a session with a specific UUID:

      <CodeGroup>
        ```python title="Python"
        session = client.sessions.create(use_uuid="123e4567-e89b-12d3-a456-426614174000")
        ```

        ```typescript title="TypeScript"
        const session = await client.sessions.create({ useUuid: "123e4567-e89b-12d3-a456-426614174000" });
        ```
      </CodeGroup>

      This is useful for correlating sessions with external systems. A 409 Conflict error is returned if the UUID already exists.
    </Tip>
  </Step>

  <Step title="Store messages in any format">
    <Tabs>
      <Tab title="OpenAI">
        <CodeGroup>
          ```python title="Python"
          client.sessions.store_message(
              session_id=session.id,
              blob={"role": "user", "content": "What is the capital of France?"},
              format="openai"
          )
          ```

          ```typescript title="TypeScript"
          await client.sessions.storeMessage(session.id,
              { role: "user", content: "What is the capital of France?" },
              { format: "openai" }
          );
          ```
        </CodeGroup>
      </Tab>

      <Tab title="Anthropic">
        <CodeGroup>
          ```python title="Python"
          client.sessions.store_message(
              session_id=session.id,
              blob={
                  "role": "user",
                  "content": [{"type": "text", "text": "Explain quantum computing"}]
              },
              format="anthropic"
          )
          ```

          ```typescript title="TypeScript"
          await client.sessions.storeMessage(session.id, {
              role: "user",
              content: [{ type: "text", text: "Explain quantum computing" }]
          }, { format: "anthropic" });
          ```
        </CodeGroup>
      </Tab>

      <Tab title="Gemini">
        <CodeGroup>
          ```python title="Python"
          client.sessions.store_message(
              session_id=session.id,
              blob={
                  "role": "user",
                  "parts": [{"text": "Explain quantum computing"}]
              },
              format="gemini"
          )
          ```

          ```typescript title="TypeScript"
          await client.sessions.storeMessage(session.id, {
              role: "user",
              parts: [{ text: "Explain quantum computing" }]
          }, { format: "gemini" });
          ```
        </CodeGroup>
      </Tab>
    </Tabs>

    <Tip>
      You can attach custom metadata to any message using the `meta` parameter:

      <CodeGroup>
        ```python title="Python"
        client.sessions.store_message(
            session_id=session.id,
            blob={"role": "user", "content": "Hello"},
            format="openai",
            meta={"source": "web", "request_id": "abc123"}
        )
        ```

        ```typescript title="TypeScript"
        await client.sessions.storeMessage(session.id, {
            blob: { role: "user", content: "Hello" },
            format: "openai",
            meta: { source: "web", requestId: "abc123" }
        });
        ```
      </CodeGroup>

      This is useful for tracking message origin, request IDs, or any application-specific data. See [Message Meta](/store/messages/special/message-meta) for more details.
    </Tip>
  </Step>
</Steps>

## Retrieve Messages [#retrieve-messages]

Retrieve in any format—Acontext converts automatically:

<CodeGroup>
  ```python title="Python"
  # Stored as OpenAI, retrieve as Anthropic
  result = client.sessions.get_messages(session_id=session.id, format="anthropic")

  for msg in result.items:
      print(f"{msg.role}: {msg.content}")
  ```

  ```typescript title="TypeScript"
  // Stored as OpenAI, retrieve as Anthropic
  const result = await client.sessions.getMessages(session.id, { format: "anthropic" });

  for (const msg of result.items) {
      console.log(`${msg.role}: ${msg.content}`);
  }
  ```
</CodeGroup>

## Delete Session [#delete-session]

<CodeGroup>
  ```python title="Python"
  client.sessions.delete(session_id)
  ```

  ```typescript title="TypeScript"
  await client.sessions.delete(sessionId);
  ```
</CodeGroup>

<Warning>
  Deleting a session permanently removes all messages.
</Warning>

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Filter by Configs" icon="filter" href="/store/messages/filter-by-configs">
    Query sessions using metadata filters
  </Card>

  <Card title="Multi-modal Messages" icon="image" href="/store/messages/multi-modal">
    Store images, audio, and documents
  </Card>
</CardGroup>


---

# Anthropic Special Flags



Acontext preserves Anthropic-specific flags like `cache_control` for [prompt caching](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching) and `thinking` blocks for [extended thinking](https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking).

## Prompt Cache [#prompt-cache]

<CodeGroup>
  ```python title="Python"
  client.sessions.store_message(
      session_id=session.id,
      blob={
          "role": "user",
          "content": [
              {
                  "type": "text",
                  "text": "<the entire contents of Pride and Prejudice>",
                  "cache_control": {"type": "ephemeral"}
              }
          ]
      },
      format="anthropic"
  )

  # Retrieved messages preserve the cache_control flag
  messages = client.sessions.get_messages(session_id=session.id, format="anthropic")
  ```

  ```typescript title="TypeScript"
  await client.sessions.storeMessage(session.id, {
      role: "user",
      content: [
          {
              type: "text",
              text: "<the entire contents of Pride and Prejudice>",
              cache_control: { type: "ephemeral" }
          }
      ]
  }, { format: "anthropic" });

  // Retrieved messages preserve the cache_control flag
  const messages = await client.sessions.getMessages(session.id, { format: "anthropic" });
  ```
</CodeGroup>

## Thinking Blocks [#thinking-blocks]

Acontext natively stores `thinking` blocks from Claude's extended thinking feature. The `signature` is preserved for multi-turn conversation continuity.

<CodeGroup>
  ```python title="Python"
  client.sessions.store_message(
      session_id=session.id,
      blob={
          "role": "assistant",
          "content": [
              {
                  "type": "thinking",
                  "thinking": "Let me reason step by step...",
                  "signature": "EqoBCkgIAxgCIkD..."
              },
              {
                  "type": "text",
                  "text": "The answer is 42."
              }
          ]
      },
      format="anthropic"
  )

  # Retrieved in Anthropic or Gemini format: thinking blocks round-trip as native thinking blocks
  # Retrieved in OpenAI format: thinking content is downgraded to plain text
  ```

  ```typescript title="TypeScript"
  await client.sessions.storeMessage(session.id, {
      role: "assistant",
      content: [
          {
              type: "thinking",
              thinking: "Let me reason step by step...",
              signature: "EqoBCkgIAxgCIkD..."
          },
          {
              type: "text",
              text: "The answer is 42."
          }
      ]
  }, { format: "anthropic" });

  // Retrieved in Anthropic or Gemini format: thinking blocks round-trip as native thinking blocks
  // Retrieved in OpenAI format: thinking content is downgraded to plain text
  ```
</CodeGroup>

<Tip>
  If you're using the Claude Agent SDK, see [ClaudeAgentStorage](/integrations/claude-agent#include-thinking-blocks) for automatic thinking block handling.
</Tip>


---

# Format Conversion



Acontext normalizes messages to a unified internal schema on store, and converts back to the target provider format on retrieval.

## Conversion Flow [#conversion-flow]

<Mermaid
  chart="flowchart LR
    subgraph Store[&#x22;Store (Normalize)&#x22;]
        direction TB
        OI[&#x22;OpenAI&#x22;] --> N[&#x22;Acontext Internal Format&#x22;]
        AI[&#x22;Anthropic&#x22;] --> N
        GI[&#x22;Gemini&#x22;] --> N
    end

    N --> C{{&#x22;Retrieve (Convert)&#x22;}}

    subgraph Retrieve[&#x22;Output Format&#x22;]
        direction TB
        OO[&#x22;OpenAI&#x22;]
        AO[&#x22;Anthropic&#x22;]
        GO[&#x22;Gemini&#x22;]
        CO[&#x22;Acontext&#x22;]
    end

    C --> OO
    C --> AO
    C --> GO
    C --> CO"
/>

## Part Type Conversion Matrix [#part-type-conversion-matrix]

Each message is composed of typed parts. The table below shows how each internal part type maps to provider-specific structures on retrieval:

| Internal Part Type | OpenAI                                  | Anthropic                                | Gemini                                        |
| ------------------ | --------------------------------------- | ---------------------------------------- | --------------------------------------------- |
| `text`             | `content` string or `text` content part | `text` block                             | `text` part                                   |
| `image`            | `image_url` content part                | `image` block (base64)                   | `InlineData` part                             |
| `tool-call`        | `tool_calls` array                      | `tool_use` block                         | `FunctionCall` part                           |
| `tool-result`      | `tool` role message                     | `tool_result` block                      | `FunctionResponse` part                       |
| `thinking`         | downgraded to `text` content part       | native `thinking` block with `signature` | native `Thought` part with `ThoughtSignature` |
| `audio`            | `input_audio` content part              | —                                        | —                                             |
| `file`             | `file` content part                     | `document` block                         | —                                             |

<Tip>
  Thinking blocks from Anthropic and Gemini are stored identically and are fully cross-compatible. A thinking block stored from Claude can be retrieved in Gemini format as a native `Thought` part, and vice versa.
</Tip>

## Role Mapping [#role-mapping]

| Provider  | Provider Roles                          | Internal Role         |
| --------- | --------------------------------------- | --------------------- |
| OpenAI    | `user`, `assistant`, `tool`, `function` | `user` or `assistant` |
| Anthropic | `user`, `assistant`                     | `user` or `assistant` |
| Gemini    | `user`, `model`                         | `user` or `assistant` |

On retrieval, `assistant` is converted back to `model` for Gemini format.

## Provider-specific Handling [#provider-specific-handling]

<CardGroup cols="2">
  <Card title="Anthropic Flags" icon="a" href="/store/messages/special/anthropic">
    Cache control and thinking blocks
  </Card>

  <Card title="Gemini Handling" icon="g" href="/store/messages/special/gemini">
    Thinking parts and function call ID generation
  </Card>
</CardGroup>


---

# Gemini Special Handling



Acontext handles Gemini-specific features including `thinking` parts for [Gemini thinking](https://ai.google.dev/gemini-api/docs/thinking) and automatic function call ID generation.

## Thinking Parts [#thinking-parts]

Acontext natively stores Gemini thinking parts. The `ThoughtSignature` is base64-encoded and preserved for multi-turn continuity.

<CodeGroup>
  ```python title="Python"
  client.sessions.store_message(
      session_id=session.id,
      blob={
          "role": "model",
          "parts": [
              {
                  "text": "Let me reason step by step...",
                  "thought": True,
                  "thoughtSignature": "Z2VtaW5pLXRob3VnaHQtc2ln..."
              },
              {
                  "text": "The answer is 42."
              }
          ]
      },
      format="gemini"
  )

  # Retrieved in Gemini format: thinking parts round-trip with Thought flag and ThoughtSignature
  # Retrieved in Anthropic format: thinking parts round-trip as native thinking blocks
  # Retrieved in OpenAI format: thinking content is downgraded to plain text
  ```

  ```typescript title="TypeScript"
  await client.sessions.storeMessage(session.id, {
      role: "model",
      parts: [
          {
              text: "Let me reason step by step...",
              thought: true,
              thoughtSignature: "Z2VtaW5pLXRob3VnaHQtc2ln..."
          },
          {
              text: "The answer is 42."
          }
      ]
  }, { format: "gemini" });

  // Retrieved in Gemini format: thinking parts round-trip with Thought flag and ThoughtSignature
  // Retrieved in Anthropic format: thinking parts round-trip as native thinking blocks
  // Retrieved in OpenAI format: thinking content is downgraded to plain text
  ```
</CodeGroup>

## Function Call ID Generation [#function-call-id-generation]

Gemini `FunctionCall` parts may omit the `id` field. Acontext automatically generates a unique ID (format: `call_<8-char-hex>`) for each function call without one, and stores the ID-to-name mapping in message metadata so that `FunctionResponse` parts can be matched back correctly.

<CodeGroup>
  ```python title="Python"
  # Store a model message with a FunctionCall (no ID needed)
  client.sessions.store_message(
      session_id=session.id,
      blob={
          "role": "model",
          "parts": [
              {
                  "functionCall": {
                      "name": "get_weather",
                      "args": {"city": "San Francisco"}
                  }
              }
          ]
      },
      format="gemini"
  )

  # Store the FunctionResponse — Acontext resolves the tool_call_id automatically
  client.sessions.store_message(
      session_id=session.id,
      blob={
          "role": "user",
          "parts": [
              {
                  "functionResponse": {
                      "name": "get_weather",
                      "response": {"temperature": "72°F", "condition": "sunny"}
                  }
              }
          ]
      },
      format="gemini"
  )
  ```

  ```typescript title="TypeScript"
  // Store a model message with a FunctionCall (no ID needed)
  await client.sessions.storeMessage(session.id, {
      role: "model",
      parts: [
          {
              functionCall: {
                  name: "get_weather",
                  args: { city: "San Francisco" }
              }
          }
      ]
  }, { format: "gemini" });

  // Store the FunctionResponse — Acontext resolves the tool_call_id automatically
  await client.sessions.storeMessage(session.id, {
      role: "user",
      parts: [
          {
              functionResponse: {
                  name: "get_weather",
                  response: { temperature: "72°F", condition: "sunny" }
              }
          }
      ]
  }, { format: "gemini" });
  ```
</CodeGroup>

<Tip>
  Thinking parts stored from any provider (Anthropic or Gemini) are cross-compatible. Retrieve in any format and Acontext converts automatically. See [Multi-provider Messages](/store/messages/multi-provider) for details.
</Tip>


---

# Message Metadata



Attach user-defined metadata to any message regardless of format. Metadata is stored separately and can be retrieved or updated independently.

## Store Message with Metadata [#store-message-with-metadata]

<CodeGroup>
  ```python title="Python"
  client.sessions.store_message(
      session_id=session.id,
      blob={"role": "user", "content": "Hello!"},
      format="openai",
      meta={"source": "web", "request_id": "abc123", "user_agent": "mobile"}
  )
  ```

  ```typescript title="TypeScript"
  await client.sessions.storeMessage(
    session.id,
    { role: "user", content: "Hello!" },
    { format: "openai", meta: { source: "web", request_id: "abc123" } }
  );
  ```
</CodeGroup>

## Retrieve Metadata [#retrieve-metadata]

The `metas` array in `get_messages` response contains metadata for each message in the same order as `items` and `ids`.

<CodeGroup>
  ```python title="Python"
  result = client.sessions.get_messages(session_id=session.id, format="openai")

  for i, msg in enumerate(result.items):
      meta = result.metas[i]
      print(f"Message: {msg}, Meta: {meta}")
  ```

  ```typescript title="TypeScript"
  const result = await client.sessions.getMessages(session.id, { format: "openai" });

  for (let i = 0; i < result.items.length; i++) {
      const msg = result.items[i];
      const meta = result.metas[i];
      console.log(`Message: ${JSON.stringify(msg)}, Meta: ${JSON.stringify(meta)}`);
  }
  ```
</CodeGroup>

## Update Metadata [#update-metadata]

Use `patch_message_meta` to add, update, or delete metadata keys after message creation.

<CodeGroup>
  ```python title="Python"
  # Add or update keys
  updated = client.sessions.patch_message_meta(
      session_id=session.id,
      message_id=msg.id,
      meta={"status": "processed", "score": 0.95}
  )

  # Delete a key by passing None
  updated = client.sessions.patch_message_meta(
      session_id=session.id,
      message_id=msg.id,
      meta={"old_key": None}
  )
  ```

  ```typescript title="TypeScript"
  // Add or update keys
  const updated = await client.sessions.patchMessageMeta(
    session.id,
    msg.id,
    { status: "processed", score: 0.95 }
  );

  // Delete a key by passing null
  await client.sessions.patchMessageMeta(
    session.id,
    msg.id,
    { old_key: null }
  );
  ```
</CodeGroup>

## Next Steps [#next-steps]

<CardGroup cols="2">
  <Card title="Multi-provider Messages" icon="arrows-rotate" href="/store/messages/multi-provider">
    Store and convert between OpenAI, Anthropic, and Gemini formats
  </Card>

  <Card title="Anthropic Special Flags" icon="bolt" href="/store/messages/special/anthropic">
    Preserve Anthropic-specific flags like prompt caching
  </Card>
</CardGroup>


---

# Create a new project

Create a new project with a randomly generated secret key

---

# Delete a project

Delete a project by ID

---

# Analyze project metrics

Get metrics for a project by querying Jaeger API with project_id filter

---

# Rotate project secret key (admin)

Generate a new secret key for a non-encrypted project. Blocked for encrypted projects.

---

# Analyze project statistics

Get statistics for a project

---

# Analyze project usages

Get usage analytics for a project

---

# List agent skills

List all agent skills under a project

---

# Create agent skill

Upload a zip file containing agent skill. The zip file must contain a SKILL.md file (case-insensitive) with YAML format containing 'name' and 'description' fields. The name and description will be extracted from SKILL.md. Files are stored as Disk Artifacts. Optionally associate with a user identifier.

---

# Get agent skill by ID

Get agent skill by its UUID

---

# Delete agent skill

Delete agent skill and all associated files

---

# Download skill to sandbox

Download all files from an agent skill to a sandbox environment. Files are placed at /skills/{skill_name}/.

---

# Download skill as ZIP file

Download all files from an agent skill as a ZIP archive. Files are streamed directly with their relative paths preserved.

---

# Get file from agent skill

Get file content or download URL from agent skill. If the file is text-based (parseable), returns parsed content. Otherwise, returns a presigned download URL.

---

# List disks

List all disks under a project

---

# Create disk

Create a disk group under a project. Optionally associate with a user identifier.

---

# Delete disk

Delete a disk by its UUID

---

# Get artifact

Get artifact information by path and filename. Optionally include a presigned URL for downloading and parsed file content.

---

# Upsert artifact

Upload a file and create or update an artifact record under a disk. File size must not exceed the configured maximum upload size limit (default: 16MB).

---

# Delete artifact

Delete an artifact by path and filename

---

# Update artifact meta

Update an artifact's metadata (user-defined metadata only)

---

# Download artifact content

Download raw artifact file content. Decrypts content if encryption is enabled.

---

# Download artifact to sandbox

Download an artifact from disk storage to a sandbox environment

---

# Search artifact paths with glob patterns

Search through artifact file paths using glob patterns (*, ?, etc.)

---

# Search artifact content with regex

Search through text-based artifact content using regex patterns

---

# List artifacts

List artifacts in a specific path or all artifacts in a disk

---

# Upload file from sandbox to disk

Upload a file from a sandbox environment to disk storage as an artifact

---

# List learning spaces

List learning spaces with optional user, meta filter, and cursor pagination.

---

# Create learning space

Create a new learning space. Optionally associate with a user.

---

# Get learning space

Get a learning space by ID.

---

# Update learning space (patch meta)

Merge provided meta into existing meta. Existing keys not in the request are preserved.

---

# Delete learning space

Delete a learning space. Junction records are cascade-deleted by the DB.

---

# Learn from session

Create an async learning record from a session. Initially stays in pending status.

---

# List sessions in space

List all learning session records for a space, including their processing status.

---

# Get session in space

Get a single learning session record by session ID within a learning space.

---

# List skills in space

List all skills associated with a learning space. Returns full skill data.

---

# Include skill in space

Add a skill to a learning space.

---

# Exclude skill from space

Remove a skill from a learning space. Idempotent — silently succeeds if not associated.

---

# Serve material content

Download file content via a material token. No authentication required. Returns the file content with appropriate Content-Type header.

---

# Push metrics to external API

Create storage metrics, fetch metrics, push to external API, and store quota status in Redis

---

# Get project quota

Get quota information for a project based on path and method

---

# Get project configs

Returns the project-level configuration (stored under the "project_config" key).

---

# Patch project configs

Merges the provided keys into the project-level configuration. Keys with null values are deleted (reset to default).

---

# Disable project encryption

Decrypts all existing S3 data for the project and disables encryption for future writes.

---

# Enable project encryption

Encrypts all existing S3 data for the project and enables encryption for future writes.

---

# Create a new sandbox

Create and start a new sandbox for the project

---

# Get sandbox logs

Get sandbox logs for the project with cursor-based pagination

---

# Kill a sandbox

Kill a running sandbox

---

# Execute command in sandbox

Execute a shell command in the specified sandbox

---

# Get sessions

Get all sessions under a project, optionally filtered by user or configs

---

# Create session

Create a new session. Optionally associate with a user identifier. You can also specify a custom UUID using use_uuid.

---

# Delete session

Delete a session by id

---

# Download session asset

Download a session asset (file attachment) by its S3 key. Decrypts if encryption is enabled.

---

# Get session configs

Get session configs by id

---

# Patch session configs

Update session configs using patch semantics. Only updates keys present in the request. Pass null as value to delete a key. Returns the complete configs after patch.

---

# Update session configs

Update session configs by id

---

# Copy session

Create a complete copy of a session with all its messages and tasks. The copied session will be independent and can be modified without affecting the original.

---

# Get events for session

Get events for a session with cursor-based pagination.

---

# Add event to session

Add a structured event to a session. Events are stored alongside messages and can be retrieved chronologically.

---

# Flush session

Flush the session buffer for a given session

---

# Get messages from session

Get messages from session. Default format is openai. Can convert to acontext (original), anthropic, or gemini format.

---

# Store message to session

Supports JSON and multipart/form-data. In multipart mode: the payload is a JSON string placed in a form field. The format parameter indicates the format of the input message (default: openai, same as GET). The blob field should be a complete message object: for openai, use OpenAI ChatCompletionMessageParam format (with role and content); for anthropic, use Anthropic MessageParam format (with role and content); for acontext (internal), use {role, parts} format. The optional meta field allows attaching user-provided metadata to the message, which can be retrieved via get_messages().metas or updated via patch_message_meta().

---

# Patch message metadata

Update message metadata using patch semantics. Only updates keys present in the request. Pass null as value to delete a key.

---

# Get message observing status for a session

Returns the count of observed, in_process, and pending messages

---

# Get tasks from session

Get tasks from session with cursor-based pagination

---

# Get token counts for session

Get total token counts for all text and tool-call parts in a session

---

# List users

Get all users under a project. If limit is not provided or 0, all users will be returned.

---

# Delete user

Delete a user by identifier and cascade delete all associated resources (Session, Disk, Skill)

---

# Get user resources

Get the resource counts (Sessions, Disks, Skills) associated with a user