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

# 常用 API 串讲

> 写出一条 Memory，搜到它，再拿到带 Citation 的 PreparedContext。

这一页用最小 Server。没有生成模型，也不需要向量搜索。

先做一件能当场验证的事：写出一条项目决策，搜到它，再把它装进这一次请求的 PreparedContext (这一次请求用的有界视图。带 Citation，不是长期资产。)。然后修订、停用，并确认历史还在。

![第一条闭环：写出 Memory，搜到它，再拿到带 Citation 的 PreparedContext。](https://mintcdn.com/powercontext/1t_ajifOEOG5YQjP/assets/basic-first-loop-zh.svg?fit=max\&auto=format\&n=1t_ajifOEOG5YQjP\&q=85\&s=dcaffb0b9aafe944300998e3d8c083d8)

> [!NOTE]
>
> 接口字段和错误码以 [官方文档](https://oceanbase.github.io/powercontext/) 的最新发布版本为准。这页帮你把第一条闭环跑出来。

## 四个入口，同一套数据

HTTP、Python Client、MCP 和 CLI 都连同一个 Server。覆盖面不一样。

| 入口 | 适合什么 |
| - | - |
| HTTP | 跨语言服务接入。完整字段以官方文档为准 |
| Python Client | 在 Python 里做类型化异步调用 |
| MCP | 给 Agent 明确调用的工具，不是完整 HTTP API |
| CLI | 安装、诊断、管理 Server，以及人来做 Review |

本地 Server 也可以打开接口说明：

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
http://127.0.0.1:8000/docs
http://127.0.0.1:8000/openapi.json
```

`/docs` 是公开的接口说明页。开了认证它仍能打开。`/openapi.json` 和实际调用需要 Bearer 请求头。不要把令牌放进 URL。完整字段仍以 [官方文档](https://oceanbase.github.io/powercontext/) 为准。

## 准备示例项目

上一页已经安装了客户端附加依赖。新建 `api_tour.py`，使用下面的完整脚本。

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import asyncio
import os
from uuid import uuid4

from powercontext.client import PowerContextClient, ServerResponseError
from powercontext.http import (
    GetMemoryEntryRequest,
    ListMemoryEntriesRequest,
    PrepareContextRequest,
    RememberMemoryRequest,
    RetireMemoryEntryRequest,
    ReviseMemoryEntryRequest,
    SearchMemoryRequest,
)


SERVER_URL = os.getenv(
    "POWERCONTEXT_CLIENT_SERVER_URL",
    "http://127.0.0.1:8000",
)
API_TOKEN = os.getenv("POWERCONTEXT_CLIENT_API_TOKEN")


async def main() -> None:
    scope_id = f"project:powercontext-101:{uuid4().hex}"

    async with PowerContextClient(
        SERVER_URL,
        token=API_TOKEN,
        timeout=10.0,
    ) as client:
        capabilities = await client.get_capabilities()
        print("search modes:", [mode.value for mode in capabilities.search_modes])
        print("scope:", scope_id)

        request = RememberMemoryRequest(
            scope_id=scope_id,
            kind="decision",
            text=(
                "Regenerate and inspect the checked-in client before "
                "running contract tests."
            ),
            reason="PowerContext 101 API tour",
        )

        remembered = await client.remember_memory(request)
        assert remembered.entry is not None
        original = remembered.entry
        print("memory:", remembered.memory)
        print("entry citation:", original.citation)
        print("source refs:", original.source_refs)

        duplicate = await client.remember_memory(request)
        assert duplicate.memory == remembered.memory
        assert duplicate.entry is None
        print("duplicate write: no-op")

        search = await client.search_memory(
            SearchMemoryRequest(
                scope_id=scope_id,
                query="What should happen before contract tests?",
                mode="auto",
                limit=5,
            )
        )
        assert search.hits
        print("used search mode:", search.mode)
        for hit in search.hits:
            print(
                "hit:",
                hit.text,
                hit.score,
                [item.value for item in hit.matched_by],
                hit.citation,
            )

        prepared = await client.prepare_context(
            PrepareContextRequest(
                scope_id=scope_id,
                query="What should happen before contract tests?",
                max_bytes=8000,
            )
        )
        assert prepared.status == "ready"
        assert prepared.content is not None
        assert len(prepared.content.encode("utf-8")) == prepared.content_bytes
        print("context schema:", prepared.schema_)
        print("context bytes:", prepared.content_bytes)
        print(prepared.content)

        exact = await client.get_memory_entry(
            GetMemoryEntryRequest(
                scope_id=scope_id,
                citation=original.citation,
            )
        )
        assert exact == original

        revised = await client.revise_memory_entry(
            ReviseMemoryEntryRequest(
                scope_id=scope_id,
                citation=original.citation,
                kind="decision",
                text=(
                    "Regenerate the checked-in client, inspect the diff, "
                    "and then run contract tests."
                ),
                reason="Add the generated-diff check",
            )
        )
        assert revised.entry is not None
        current = revised.entry
        print("revised memory:", revised.memory)
        print("current citation:", current.citation)

        historical = await client.get_memory_entry(
            GetMemoryEntryRequest(
                scope_id=scope_id,
                citation=original.citation,
            )
        )
        assert historical.text == original.text
        print("old citation remains readable")

        try:
            await client.retire_memory_entry(
                RetireMemoryEntryRequest(
                    scope_id=scope_id,
                    citation=original.citation,
                    reason="Demonstrate stale citation",
                )
            )
        except ServerResponseError as exc:
            assert exc.status_code == 409
            assert exc.code == "revision_conflict"
            print("stale citation rejected:", exc.code, exc.request_id)
        else:
            raise AssertionError("the stale citation should have conflicted")

        retired = await client.retire_memory_entry(
            RetireMemoryEntryRequest(
                scope_id=scope_id,
                citation=current.citation,
                reason="The tutorial lifecycle is complete",
            )
        )
        assert retired.entry is not None
        assert retired.entry.state == "inactive"

        active = await client.list_memory_entries(
            ListMemoryEntriesRequest(scope_id=scope_id)
        )
        assert active.entries == []

        audit = await client.list_memory_entries(
            ListMemoryEntriesRequest(
                scope_id=scope_id,
                include_inactive=True,
            )
        )
        assert len(audit.entries) == 1
        assert audit.entries[0].state == "inactive"
        print("inactive history remains readable")


if __name__ == "__main__":
    asyncio.run(main())
```

运行：

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv run python api_tour.py
```

每次运行使用唯一 Scope，避免上一次教程数据让重复写入、条目计数或 Revision 断言失效。真实项目应改用稳定的项目 Scope，不要用随机值。

## 这段脚本在证明什么

### 写下 Memory，不需要先有 Source

`remember_memory` 直接写下你已经确认的长期知识。示例里 `source_refs` 通常是空的。

首次有效写入返回：

* 精确 Memory Artifact 版本引用；
* 逻辑 `entry_id`；
* 精确 `entry_version_id`；
* 当前条目内容和状态。

完全重复的启用状态内容是无变更。它返回当前 Memory 版本引用，但 `entry` 是 `null`。所以不要假定每次 `remember_memory` 都有 `response.entry.citation`。

### 搜索为空，有两种正常情况

Scope 从未有过 Memory：

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
memory = null
mode = null
hits = []
```

Scope 有 Memory，但查询没匹配：

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
memory = current exact ref
mode = fts/vector/hybrid
hits = []
```

两者都是 HTTP 200 下的正常结果。

### PreparedContext 已经是最终封装

`prepared.content` 已包含信任标记、边界和 Citation。Agent 可以把它当作不可信历史证据放进这一次请求，不必再拼搜索命中。

`status="empty"` 时：

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
content = null
content_bytes = 0
```

`status="ready"` 时，实际 UTF-8 字节必须等于 `content_bytes`，并且不超过请求的 `max_bytes`。

### 读取历史和改内容，用的不是同一套版本规则

旧 Citation 还能用 `get_memory_entry` 读历史。

`revise_memory_entry` 和 `retire_memory_entry` 只接受当前头版本 Citation。Memory 一改，Revision 往前走，旧 Citation 再改会返回 HTTP 409 `revision_conflict`。

下一步用修改响应里的新 Citation。

### 停用不等于删除

停用后：

* 默认列出不再显示条目；
* 启用状态搜索 / 召回不再选择它；
* `include_inactive=True` 仍能审计；
* 精确历史读取仍然有效。

## 出错时看什么

Python Client 保留 HTTP 状态、稳定错误 code、Server 消息和 `X-PowerContext-Request-ID`。

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from powercontext.client import (
    InvalidResponseError,
    ServerResponseError,
    TransportError,
)

try:
    ...
except ServerResponseError as exc:
    print(exc.status_code, exc.code, exc.server_message, exc.request_id)
except TransportError as exc:
    print("HTTP transport failed:", exc)
except InvalidResponseError as exc:
    print("Response violated the generated contract:", exc)
```

常见状态：

| HTTP | 含义 |
| - | - |
| 401 | Bearer 令牌缺失或错误 |
| 404 | 精确不可变值不存在 |
| 409 | 当前头版本、Citation 或预期版本冲突 |
| 422 | 请求或能力不满足契约 |
| 503 | 必需运行时绑定 / 依赖不可用 |

遇到 409，先重新读取当前值，再决定要不要重试。不要猜 Revision。

## MCP 和 HTTP 覆盖面不一样

MCP 是给 Agent 明确调用的工具。HTTP 才是完整接口。

`POST /v1/context/prepare` 和 Python Client 的 `prepare_context` 不会作为 MCP 工具出现。最终 PreparedContext 通常由 Hook 或框架适配器在请求时准备。MCP 也不等于每个集成自己的自动召回、采集和刷新。

要连上客户端、核对自己能调哪些工具，看[用 MCP 连接 Server](https://powercontext.mintlify.site/zh/basic-usage/mcp.md)。

## 怎么知道跑通了

终端里应能看到这几件事：

* 第一次写入返回精确条目 Citation。
* 重复写入是 `entry=None` 的无变更。
* 全文搜索 / `auto` 能找到这条启用状态条目。
* PreparedContext 是 `ready`，字节数对得上。
* 修订后，旧 Citation 还能精确读到。
* 用旧 Citation 再改，返回 409。
* 停用后，默认列表是空的；带上停用状态，历史还在。

对不上时，先对[排错](https://powercontext.mintlify.site/zh/start/troubleshooting.md)。

## 下一步

> [card] [用 Dashboard 核对数字](https://powercontext.mintlify.site/zh/basic-usage/dashboard.md)
>
> Dashboard 不显示 Memory 正文。配上刚才那个 Scope，核对 `total=1`、`active=0`、`inactive=1`。

> [card] [Source 与 Memory](https://powercontext.mintlify.site/zh/basic-usage/source-and-memory.md)
>
> 下一条路是先采集 Source，再抽取 Memory。采集成功不等于留下知识。


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