From 4c397a0a4e87aa3aea5d83c59d60bddd9389b984 Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Fri, 15 May 2026 02:08:58 +0800 Subject: [PATCH 1/2] docs: add tapestore tutorial Signed-off-by: Chojan Shang --- .../src/content/docs/docs/tutorials/index.mdx | 1 + .../docs/tutorials/tapestore-sqlalchemy.mdx | 163 ++++++++++++++++++ .../docs/zh-cn/docs/tutorials/index.mdx | 1 + .../docs/tutorials/tapestore-sqlalchemy.mdx | 163 ++++++++++++++++++ 4 files changed, 328 insertions(+) create mode 100644 website/src/content/docs/docs/tutorials/tapestore-sqlalchemy.mdx create mode 100644 website/src/content/docs/zh-cn/docs/tutorials/tapestore-sqlalchemy.mdx diff --git a/website/src/content/docs/docs/tutorials/index.mdx b/website/src/content/docs/docs/tutorials/index.mdx index 29e3740f..090c6d80 100644 --- a/website/src/content/docs/docs/tutorials/index.mdx +++ b/website/src/content/docs/docs/tutorials/index.mdx @@ -11,6 +11,7 @@ Tutorials are hands-on lessons. Use this section when you want to learn a workfl 1. [Observe Bub with tapes and Jaeger](/docs/tutorials/observability/) — inspect Bub's own tape first, then export Logfire/OpenTelemetry traces to Jaeger. 2. [Connect MCP Servers with bub-mcp](/docs/tutorials/mcp/) — install the MCP plugin, wire up a time server, and call it from a Bub turn. +3. [Persist tapes in SQLAlchemy with local SQLite or Turso](/docs/tutorials/tapestore-sqlalchemy/) — replace the file-based tape store with a SQLAlchemy database, then push the same workflow to a Turso embedded replica. ## Next steps diff --git a/website/src/content/docs/docs/tutorials/tapestore-sqlalchemy.mdx b/website/src/content/docs/docs/tutorials/tapestore-sqlalchemy.mdx new file mode 100644 index 00000000..e3dd68c0 --- /dev/null +++ b/website/src/content/docs/docs/tutorials/tapestore-sqlalchemy.mdx @@ -0,0 +1,163 @@ +--- +title: Persist tapes in SQLAlchemy with local SQLite or Turso +description: Replace the file-based tape store with one SQLAlchemy database. Run it against local SQLite, then push the same workflow to a Turso embedded replica. +sidebar: + order: 2 +--- + +A growing share of agent runtimes are not database-centric. Many of them lean on plain files — JSONL transcripts, Markdown notes — for context and memory, and reach for a separate hosted service whenever they need observability. Bub's [tape model](/docs/concepts/tape-and-context/) (see [tape.systems](https://tape.systems)) frames the same surface differently: the same append-only record that rebuilds context is also the operational log you query when something goes wrong. + +This tutorial uses [`bub-tapestore-sqlalchemy`](https://github.com/bubbuild/bub-contrib/tree/main/packages/bub-tapestore-sqlalchemy) to replace Bub's per-tape JSONL files with one SQLAlchemy database, twice: + +1. **Local SQLite** — one file you can back up, copy, or open in any SQL client. +2. **[Turso](https://turso.tech) embedded replica (libSQL)** — the same local file, transparently synced to a managed cloud database. Local writes stay fast and transactional; the cloud copy is the durable, multi-machine store. + +The [`,tape.info`](/docs/tutorials/observability/) and `,tape.search` workflow stays unchanged: only the storage URL — and, for Turso, an auth token — moves between modes. + +## Before you begin + +You need: + +- Bub installed and runnable with `uv run bub --help` (see [Install](/docs/getting-started/install/)). +- A workspace where `uv run bub run "What tools do you have?"` can call your configured model. +- The `sqlite3` CLI for the optional local check. +- A free [Turso](https://turso.tech/) account and the [`turso`](https://docs.turso.tech/cli/installation) CLI authenticated with `turso auth login`. + +## 1. Install the plugin + +```bash +uv run bub install bub-tapestore-sqlalchemy@main +``` + +Confirm Bub picked up the entry point: + +```bash +uv run bub hooks +``` + +```text +provide_tape_store: builtin, tapestore-sqlalchemy +``` + +## 2. Run a turn against local SQLite + +The plugin uses `/tapes.db` by default. Override it for this tutorial: + +```bash +export BUB_TAPESTORE_SQLALCHEMY_URL="sqlite+pysqlite:///$PWD/bub-tapes.db" +``` + +Run a small natural-language task, then ask Bub to inspect the tape it just wrote: + +```bash +uv run bub run "Reply with one short sentence: hello from local SQLAlchemy." +uv run bub run ",tape.info" +``` + +```text +name: 86774b31b96845a4__0b871d5e50e7c192 +entries: 9 +anchors: 1 +last_anchor: session/start +entries_since_last_anchor: 8 +last_token_usage: 4106 +``` + +Open the SQLite file with the standard CLI: + +```bash +sqlite3 "$PWD/bub-tapes.db" "SELECT name, last_entry_id FROM tapes;" +``` + +```text +86774b31b96845a4__0b871d5e50e7c192|9 +``` + +The `last_entry_id` matches `entries` from `,tape.info` because the plugin uses a per-tape monotonic counter — the same counter Bub uses to replay context. + +## 3. Provision a Turso database + +```bash +turso db create bub-tapes +export TURSO_DB_URL="$(turso db show bub-tapes --url)" +export TURSO_AUTH_TOKEN="$(turso db tokens create bub-tapes)" +``` + +Install the libSQL SQLAlchemy dialect into the same Python environment that runs `bub`: + +```bash +uv pip install sqlalchemy-libsql +``` + +## 4. Point Bub at a Turso embedded replica + +Embedded replicas keep a local libSQL file in sync with Turso. You write locally — fast, transactional, no network round-trip per `append` — and libSQL streams the same bytes to the cloud. That is the natural fit for an append-only tape: the agent never waits on the network, and the cloud copy is always catching up. + +Switch the SQLAlchemy URL to the libSQL dialect and pass the auth token plus sync URL through `BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS`: + +```bash +HOST="${TURSO_DB_URL#libsql://}" +export BUB_TAPESTORE_SQLALCHEMY_URL="sqlite+libsql:///$PWD/bub-tapes-replica.db" +export BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS="$(printf '{"auth_token": "%s", "sync_url": "https://%s"}' "$TURSO_AUTH_TOKEN" "$HOST")" +``` + +`BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS` is a JSON object forwarded straight to `sqlalchemy.create_engine(connect_args=...)`. For libSQL, `auth_token` and `sync_url` are the two keys that turn a local file into a cloud-backed replica. + +## 5. Run a turn against Turso + +```bash +uv run bub run "Reply with one short sentence: hello via embedded replica." +uv run bub run ",tape.info" +``` + +```text +name: f8d663d1fa2fa3e9__0b871d5e50e7c192 +entries: 9 +anchors: 1 +last_anchor: session/start +entries_since_last_anchor: 8 +last_token_usage: 2546 +``` + +The output matches step 2 — only the storage backend changed. + +## 6. Verify the data landed in Turso + +Open a SQL shell against the remote database: + +```bash +turso db shell bub-tapes +``` + +```sql +.tables +-- tape_entries tapes + +SELECT name, last_entry_id FROM tapes; +-- f8d663d1fa2fa3e9__0b871d5e50e7c192|10 + +SELECT entry_id, anchor_name, entry_date + FROM tape_entries + WHERE kind = 'anchor' + ORDER BY tape_id DESC, entry_id DESC LIMIT 1; +-- 1|session/start|2026-05-15T01:27:57Z +``` + +![A Turso shell showing the tapes and tape_entries tables populated by Bub](/docs/tapestore-sqlalchemy/turso-shell.png) + +This is the payoff of letting the tape live in a real database: context, memory, and the operational log are all the same record, and that record is now reachable from any machine that exports the same `BUB_TAPESTORE_SQLALCHEMY_URL` and `BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS`. + +## Clean up + +```bash +turso db destroy bub-tapes --yes +unset BUB_TAPESTORE_SQLALCHEMY_URL BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS \ + TURSO_DB_URL TURSO_AUTH_TOKEN +rm -f "$PWD/bub-tapes.db" "$PWD/bub-tapes-replica.db"* +``` + +## Next steps + +- [Observe Bub with tapes and Jaeger](/docs/tutorials/observability/) — pair the durable tape with process-level telemetry. +- [Tape and context](/docs/concepts/tape-and-context/) — what Bub records and how context is rebuilt. +- [Plugins](/docs/build/plugins/) — the plugin contract `bub-tapestore-sqlalchemy` follows. diff --git a/website/src/content/docs/zh-cn/docs/tutorials/index.mdx b/website/src/content/docs/zh-cn/docs/tutorials/index.mdx index bbdd2011..2d9026a1 100644 --- a/website/src/content/docs/zh-cn/docs/tutorials/index.mdx +++ b/website/src/content/docs/zh-cn/docs/tutorials/index.mdx @@ -11,6 +11,7 @@ sidebar: 1. [使用 tape 与 Jaeger 观察 Bub](/zh-cn/docs/tutorials/observability/) — 先检查 Bub 自身的 tape,再把 Logfire/OpenTelemetry trace 导出到 Jaeger。 2. [使用 bub-mcp 连接 MCP 服务器](/zh-cn/docs/tutorials/mcp/) — 安装 MCP 插件,接入时间服务器,并在 Bub turn 中调用。 +3. [用 SQLAlchemy 持久化 tape:本地 SQLite 与 Turso](/zh-cn/docs/tutorials/tapestore-sqlalchemy/) — 把基于文件的 tape store 换成 SQLAlchemy 数据库,再把同一套流程接到 Turso 嵌入式副本。 ## 下一步 diff --git a/website/src/content/docs/zh-cn/docs/tutorials/tapestore-sqlalchemy.mdx b/website/src/content/docs/zh-cn/docs/tutorials/tapestore-sqlalchemy.mdx new file mode 100644 index 00000000..588c073e --- /dev/null +++ b/website/src/content/docs/zh-cn/docs/tutorials/tapestore-sqlalchemy.mdx @@ -0,0 +1,163 @@ +--- +title: 用 SQLAlchemy 持久化 tape:本地 SQLite 与 Turso +description: 用一份 SQLAlchemy 数据库替代基于文件的 tape store,先在本地 SQLite 上跑通,再把同一套流程接到 Turso 嵌入式副本。 +sidebar: + order: 2 +--- + +近年来流行的一部分 Agent 实现并不以数据库为中心。它们更习惯用文件——JSONL 记录、Markdown 笔记——承载上下文与记忆,需要可观测性时再去接入独立的服务。Bub 的 [tape 模型](/zh-cn/docs/concepts/tape-and-context/)(参见 [tape.systems](https://tape.systems))则提供了另一种表达:用来重建 context 的同一份 append-only 记录,本身就是出问题时你要去查的运行日志。 + +本教程使用 [`bub-tapestore-sqlalchemy`](https://github.com/bubbuild/bub-contrib/tree/main/packages/bub-tapestore-sqlalchemy) 把 Bub 默认的每个 tape 一个 JSONL 的写入方式,换成一份 SQLAlchemy 数据库,并把同一个流程跑两次: + +1. **本地 SQLite** —— 一个可以备份、复制、用任意 SQL 客户端打开的文件。 +2. **[Turso](https://turso.tech) 嵌入式副本(libSQL)** —— 同样是本地文件,并由 libSQL 透明同步到托管云端。本地写入保持快速且事务安全,云端副本是持久化、可跨机器访问的真实存储。 + +[`,tape.info`](/zh-cn/docs/tutorials/observability/) 与 `,tape.search` 流程不变:在两种模式之间切换,只需要更换 storage URL,以及——对于 Turso ——一个 auth token。 + +## 前置条件 + +你需要: + +- Bub 已安装,且 `uv run bub --help` 可以运行(参考 [安装](/zh-cn/docs/getting-started/install/))。 +- 一个 workspace,其中 `uv run bub run "What tools do you have?"` 能调用已配置的模型。 +- `sqlite3` CLI,用于可选的本地检查。 +- 一个免费的 [Turso](https://turso.tech/) 账号,并已通过 `turso auth login` 完成 [`turso`](https://docs.turso.tech/cli/installation) CLI 鉴权。 + +## 1. 安装插件 + +```bash +uv run bub install bub-tapestore-sqlalchemy@main +``` + +确认 Bub 加载了 entry point: + +```bash +uv run bub hooks +``` + +```text +provide_tape_store: builtin, tapestore-sqlalchemy +``` + +## 2. 在本地 SQLite 上跑一个 turn + +插件默认使用 `/tapes.db`。教程里换一个方便检查的位置: + +```bash +export BUB_TAPESTORE_SQLALCHEMY_URL="sqlite+pysqlite:///$PWD/bub-tapes.db" +``` + +跑一个小的自然语言任务,然后让 Bub 检查刚刚写入的 tape: + +```bash +uv run bub run "Reply with one short sentence: hello from local SQLAlchemy." +uv run bub run ",tape.info" +``` + +```text +name: 86774b31b96845a4__0b871d5e50e7c192 +entries: 9 +anchors: 1 +last_anchor: session/start +entries_since_last_anchor: 8 +last_token_usage: 4106 +``` + +用标准 CLI 打开 SQLite 文件: + +```bash +sqlite3 "$PWD/bub-tapes.db" "SELECT name, last_entry_id FROM tapes;" +``` + +```text +86774b31b96845a4__0b871d5e50e7c192|9 +``` + +`last_entry_id` 与 `,tape.info` 中的 `entries` 一致——插件使用的是每条 tape 单调递增的计数器,也就是 Bub 重放 context 时使用的那个计数器。 + +## 3. 创建 Turso 数据库 + +```bash +turso db create bub-tapes +export TURSO_DB_URL="$(turso db show bub-tapes --url)" +export TURSO_AUTH_TOKEN="$(turso db tokens create bub-tapes)" +``` + +把 libSQL 的 SQLAlchemy 方言安装到运行 `bub` 的同一个 Python 环境: + +```bash +uv pip install sqlalchemy-libsql +``` + +## 4. 把 Bub 指向 Turso 嵌入式副本 + +嵌入式副本会把一份本地 libSQL 文件与 Turso 保持同步。你在本地写入——速度快、事务安全、每次 `append` 都不需要往返网络——再由 libSQL 把同样的字节流同步到云端。这正好契合 append-only 的 tape:Agent 永远不需要等待网络,云端副本在后台追上。 + +把 SQLAlchemy URL 换成 libSQL 方言,并通过 `BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS` 传入 auth token 与 sync URL: + +```bash +HOST="${TURSO_DB_URL#libsql://}" +export BUB_TAPESTORE_SQLALCHEMY_URL="sqlite+libsql:///$PWD/bub-tapes-replica.db" +export BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS="$(printf '{"auth_token": "%s", "sync_url": "https://%s"}' "$TURSO_AUTH_TOKEN" "$HOST")" +``` + +`BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS` 是一个 JSON 对象,会原样转给 `sqlalchemy.create_engine(connect_args=...)`。对 libSQL 来说,`auth_token` 与 `sync_url` 这两个键就足以让本地文件变成一份云端副本。 + +## 5. 在 Turso 上跑一个 turn + +```bash +uv run bub run "Reply with one short sentence: hello via embedded replica." +uv run bub run ",tape.info" +``` + +```text +name: f8d663d1fa2fa3e9__0b871d5e50e7c192 +entries: 9 +anchors: 1 +last_anchor: session/start +entries_since_last_anchor: 8 +last_token_usage: 2546 +``` + +输出与第 2 节一致——只是后端换了一个。 + +## 6. 在 Turso 上验证数据 + +打开远程数据库的 SQL shell: + +```bash +turso db shell bub-tapes +``` + +```sql +.tables +-- tape_entries tapes + +SELECT name, last_entry_id FROM tapes; +-- f8d663d1fa2fa3e9__0b871d5e50e7c192|10 + +SELECT entry_id, anchor_name, entry_date + FROM tape_entries + WHERE kind = 'anchor' + ORDER BY tape_id DESC, entry_id DESC LIMIT 1; +-- 1|session/start|2026-05-15T01:27:57Z +``` + +![Turso shell 中显示由 Bub 写入的 tapes 与 tape_entries 表](/docs/tapestore-sqlalchemy/turso-shell.png) + +把 tape 放进真正的数据库的好处就在这里:context、记忆、运行日志是同一份记录,而这份记录现在可以从任何一台 export 了相同 `BUB_TAPESTORE_SQLALCHEMY_URL` 与 `BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS` 的机器上访问。 + +## 清理 + +```bash +turso db destroy bub-tapes --yes +unset BUB_TAPESTORE_SQLALCHEMY_URL BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS \ + TURSO_DB_URL TURSO_AUTH_TOKEN +rm -f "$PWD/bub-tapes.db" "$PWD/bub-tapes-replica.db"* +``` + +## 下一步 + +- [使用 tape 与 Jaeger 观察 Bub](/zh-cn/docs/tutorials/observability/) —— 把持久化 tape 和进程级 telemetry 配合使用。 +- [Tape 与 context](/zh-cn/docs/concepts/tape-and-context/) —— 理解 Bub 记录什么,以及如何重建 context。 +- [Plugins](/zh-cn/docs/build/plugins/) —— `bub-tapestore-sqlalchemy` 遵循的插件契约。 From 02b6a92a816768b2c037a5f64fb37e6e8d3ad5b3 Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Mon, 18 May 2026 00:30:24 +0800 Subject: [PATCH 2/2] docs: only keep sqlite Signed-off-by: Chojan Shang --- .../src/content/docs/docs/tutorials/index.mdx | 2 +- .../docs/tutorials/tapestore-sqlalchemy.mdx | 111 ++++++------------ .../docs/zh-cn/docs/tutorials/index.mdx | 2 +- .../docs/tutorials/tapestore-sqlalchemy.mdx | 111 ++++++------------ 4 files changed, 68 insertions(+), 158 deletions(-) diff --git a/website/src/content/docs/docs/tutorials/index.mdx b/website/src/content/docs/docs/tutorials/index.mdx index 090c6d80..61f5164f 100644 --- a/website/src/content/docs/docs/tutorials/index.mdx +++ b/website/src/content/docs/docs/tutorials/index.mdx @@ -11,7 +11,7 @@ Tutorials are hands-on lessons. Use this section when you want to learn a workfl 1. [Observe Bub with tapes and Jaeger](/docs/tutorials/observability/) — inspect Bub's own tape first, then export Logfire/OpenTelemetry traces to Jaeger. 2. [Connect MCP Servers with bub-mcp](/docs/tutorials/mcp/) — install the MCP plugin, wire up a time server, and call it from a Bub turn. -3. [Persist tapes in SQLAlchemy with local SQLite or Turso](/docs/tutorials/tapestore-sqlalchemy/) — replace the file-based tape store with a SQLAlchemy database, then push the same workflow to a Turso embedded replica. +3. [Persist tapes in SQLAlchemy with SQLite](/docs/tutorials/tapestore-sqlalchemy/) — replace the file-based tape store with a local SQLite database. ## Next steps diff --git a/website/src/content/docs/docs/tutorials/tapestore-sqlalchemy.mdx b/website/src/content/docs/docs/tutorials/tapestore-sqlalchemy.mdx index e3dd68c0..4d2f9121 100644 --- a/website/src/content/docs/docs/tutorials/tapestore-sqlalchemy.mdx +++ b/website/src/content/docs/docs/tutorials/tapestore-sqlalchemy.mdx @@ -1,18 +1,17 @@ --- -title: Persist tapes in SQLAlchemy with local SQLite or Turso -description: Replace the file-based tape store with one SQLAlchemy database. Run it against local SQLite, then push the same workflow to a Turso embedded replica. +title: Persist tapes in SQLAlchemy with SQLite +description: Replace Bub's file-based tape store with one local SQLite database through the SQLAlchemy tape store plugin. sidebar: order: 2 --- -A growing share of agent runtimes are not database-centric. Many of them lean on plain files — JSONL transcripts, Markdown notes — for context and memory, and reach for a separate hosted service whenever they need observability. Bub's [tape model](/docs/concepts/tape-and-context/) (see [tape.systems](https://tape.systems)) frames the same surface differently: the same append-only record that rebuilds context is also the operational log you query when something goes wrong. +Many agent implementations lean on files — JSONL transcripts, Markdown notes, local caches — to carry context and memory, then attach a separate service when they need observability. Bub's [tape model](/docs/concepts/tape-and-context/) (see [tape.systems](https://tape.systems)) names the underlying abstraction instead of the medium: the same append-only record can rebuild context, carry memory-oriented facts, and serve as the operational log you inspect when something goes wrong. -This tutorial uses [`bub-tapestore-sqlalchemy`](https://github.com/bubbuild/bub-contrib/tree/main/packages/bub-tapestore-sqlalchemy) to replace Bub's per-tape JSONL files with one SQLAlchemy database, twice: +That does not require a database. The default store writes one JSONL file per tape under `~/.bub/tapes/`, which is simple and portable. But the abstraction is also a natural fit for databases: when tape entries live in a database, you can start using database strengths — query planning, indexes, transactional writes, backup workflows, and storage scaling — without changing Bub's turn pipeline. -1. **Local SQLite** — one file you can back up, copy, or open in any SQL client. -2. **[Turso](https://turso.tech) embedded replica (libSQL)** — the same local file, transparently synced to a managed cloud database. Local writes stay fast and transactional; the cloud copy is the durable, multi-machine store. +This tutorial uses [`bub-tapestore-sqlalchemy`](https://github.com/bubbuild/bub-contrib/tree/main/packages/bub-tapestore-sqlalchemy) to replace Bub's file store with one local SQLite database that you can inspect with standard SQL tooling. -The [`,tape.info`](/docs/tutorials/observability/) and `,tape.search` workflow stays unchanged: only the storage URL — and, for Turso, an auth token — moves between modes. +The [`,tape.info`](/docs/tutorials/observability/) and `,tape.search` workflow stays unchanged. Only the tape store changes. ## Before you begin @@ -20,11 +19,16 @@ You need: - Bub installed and runnable with `uv run bub --help` (see [Install](/docs/getting-started/install/)). - A workspace where `uv run bub run "What tools do you have?"` can call your configured model. -- The `sqlite3` CLI for the optional local check. -- A free [Turso](https://turso.tech/) account and the [`turso`](https://docs.turso.tech/cli/installation) CLI authenticated with `turso auth login`. +- The `sqlite3` CLI for the optional database check. ## 1. Install the plugin +Set `BUB_HOME` to an absolute path if you have not configured it already: + +```bash +export BUB_HOME="${BUB_HOME:-$HOME/.bub}" +``` + ```bash uv run bub install bub-tapestore-sqlalchemy@main ``` @@ -39,14 +43,18 @@ uv run bub hooks provide_tape_store: builtin, tapestore-sqlalchemy ``` -## 2. Run a turn against local SQLite +## 2. Point Bub at a local SQLite database -The plugin uses `/tapes.db` by default. Override it for this tutorial: +The plugin uses `/tapes.db` by default. Override it for this tutorial so the database is easy to find and remove: ```bash export BUB_TAPESTORE_SQLALCHEMY_URL="sqlite+pysqlite:///$PWD/bub-tapes.db" ``` +This URL is passed to SQLAlchemy. The `sqlite+pysqlite` dialect uses Python's standard SQLite driver, so no extra database driver is required. + +## 3. Run a turn + Run a small natural-language task, then ask Bub to inspect the tape it just wrote: ```bash @@ -63,6 +71,10 @@ entries_since_last_anchor: 8 last_token_usage: 4106 ``` +The exact tape name and counts depend on your workspace and model call, but the command should report a tape name, at least one anchor, and entries written after `session/start`. + +## 4. Inspect the database + Open the SQLite file with the standard CLI: ```bash @@ -73,87 +85,30 @@ sqlite3 "$PWD/bub-tapes.db" "SELECT name, last_entry_id FROM tapes;" 86774b31b96845a4__0b871d5e50e7c192|9 ``` -The `last_entry_id` matches `entries` from `,tape.info` because the plugin uses a per-tape monotonic counter — the same counter Bub uses to replay context. - -## 3. Provision a Turso database - -```bash -turso db create bub-tapes -export TURSO_DB_URL="$(turso db show bub-tapes --url)" -export TURSO_AUTH_TOKEN="$(turso db tokens create bub-tapes)" -``` - -Install the libSQL SQLAlchemy dialect into the same Python environment that runs `bub`: - -```bash -uv pip install sqlalchemy-libsql -``` - -## 4. Point Bub at a Turso embedded replica - -Embedded replicas keep a local libSQL file in sync with Turso. You write locally — fast, transactional, no network round-trip per `append` — and libSQL streams the same bytes to the cloud. That is the natural fit for an append-only tape: the agent never waits on the network, and the cloud copy is always catching up. - -Switch the SQLAlchemy URL to the libSQL dialect and pass the auth token plus sync URL through `BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS`: +The `last_entry_id` tracks the highest entry id written to each tape. Inspect the anchor rows as well: ```bash -HOST="${TURSO_DB_URL#libsql://}" -export BUB_TAPESTORE_SQLALCHEMY_URL="sqlite+libsql:///$PWD/bub-tapes-replica.db" -export BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS="$(printf '{"auth_token": "%s", "sync_url": "https://%s"}' "$TURSO_AUTH_TOKEN" "$HOST")" -``` - -`BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS` is a JSON object forwarded straight to `sqlalchemy.create_engine(connect_args=...)`. For libSQL, `auth_token` and `sync_url` are the two keys that turn a local file into a cloud-backed replica. - -## 5. Run a turn against Turso - -```bash -uv run bub run "Reply with one short sentence: hello via embedded replica." -uv run bub run ",tape.info" +sqlite3 "$PWD/bub-tapes.db" \ + "SELECT entry_id, anchor_name, entry_date FROM tape_entries WHERE kind = 'anchor';" ``` ```text -name: f8d663d1fa2fa3e9__0b871d5e50e7c192 -entries: 9 -anchors: 1 -last_anchor: session/start -entries_since_last_anchor: 8 -last_token_usage: 2546 +1|session/start|2026-05-15T01:27:57Z ``` -The output matches step 2 — only the storage backend changed. - -## 6. Verify the data landed in Turso - -Open a SQL shell against the remote database: +That is the full storage switch: Bub still records append-only tape entries, but they now live in a SQL database instead of per-tape JSONL files. -```bash -turso db shell bub-tapes -``` - -```sql -.tables --- tape_entries tapes - -SELECT name, last_entry_id FROM tapes; --- f8d663d1fa2fa3e9__0b871d5e50e7c192|10 - -SELECT entry_id, anchor_name, entry_date - FROM tape_entries - WHERE kind = 'anchor' - ORDER BY tape_id DESC, entry_id DESC LIMIT 1; --- 1|session/start|2026-05-15T01:27:57Z -``` +## 5. Extend the backend -![A Turso shell showing the tapes and tape_entries tables populated by Bub](/docs/tapestore-sqlalchemy/turso-shell.png) +`bub-tapestore-sqlalchemy` is configured with `BUB_TAPESTORE_SQLALCHEMY_URL`, so the same plugin can target another SQLAlchemy-supported database URL when the matching dialect and driver are installed in the environment that runs `bub`. Depending on the driver, you may need compatibility settings or driver-specific handling; validate the backend with this same `,tape.info` flow before using it for long-lived tapes. -This is the payoff of letting the tape live in a real database: context, memory, and the operational log are all the same record, and that record is now reachable from any machine that exports the same `BUB_TAPESTORE_SQLALCHEMY_URL` and `BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS`. +For a SQLite-focused store with vector-search support, see [`bub-tapestore-sqlite`](https://github.com/bubbuild/bub-contrib/tree/main/packages/bub-tapestore-sqlite). It builds on `sqlite-vec` and can use embeddings for tape retrieval. ## Clean up ```bash -turso db destroy bub-tapes --yes -unset BUB_TAPESTORE_SQLALCHEMY_URL BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS \ - TURSO_DB_URL TURSO_AUTH_TOKEN -rm -f "$PWD/bub-tapes.db" "$PWD/bub-tapes-replica.db"* +unset BUB_TAPESTORE_SQLALCHEMY_URL BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS +rm -f "$PWD/bub-tapes.db" ``` ## Next steps diff --git a/website/src/content/docs/zh-cn/docs/tutorials/index.mdx b/website/src/content/docs/zh-cn/docs/tutorials/index.mdx index 2d9026a1..8c511051 100644 --- a/website/src/content/docs/zh-cn/docs/tutorials/index.mdx +++ b/website/src/content/docs/zh-cn/docs/tutorials/index.mdx @@ -11,7 +11,7 @@ sidebar: 1. [使用 tape 与 Jaeger 观察 Bub](/zh-cn/docs/tutorials/observability/) — 先检查 Bub 自身的 tape,再把 Logfire/OpenTelemetry trace 导出到 Jaeger。 2. [使用 bub-mcp 连接 MCP 服务器](/zh-cn/docs/tutorials/mcp/) — 安装 MCP 插件,接入时间服务器,并在 Bub turn 中调用。 -3. [用 SQLAlchemy 持久化 tape:本地 SQLite 与 Turso](/zh-cn/docs/tutorials/tapestore-sqlalchemy/) — 把基于文件的 tape store 换成 SQLAlchemy 数据库,再把同一套流程接到 Turso 嵌入式副本。 +3. [用 SQLAlchemy 与 SQLite 持久化 tape](/zh-cn/docs/tutorials/tapestore-sqlalchemy/) — 把基于文件的 tape store 换成本地 SQLite 数据库。 ## 下一步 diff --git a/website/src/content/docs/zh-cn/docs/tutorials/tapestore-sqlalchemy.mdx b/website/src/content/docs/zh-cn/docs/tutorials/tapestore-sqlalchemy.mdx index 588c073e..0a4c69d7 100644 --- a/website/src/content/docs/zh-cn/docs/tutorials/tapestore-sqlalchemy.mdx +++ b/website/src/content/docs/zh-cn/docs/tutorials/tapestore-sqlalchemy.mdx @@ -1,18 +1,17 @@ --- -title: 用 SQLAlchemy 持久化 tape:本地 SQLite 与 Turso -description: 用一份 SQLAlchemy 数据库替代基于文件的 tape store,先在本地 SQLite 上跑通,再把同一套流程接到 Turso 嵌入式副本。 +title: 用 SQLAlchemy 与 SQLite 持久化 tape +description: 通过 SQLAlchemy tape store 插件,把 Bub 基于文件的 tape store 换成本地 SQLite 数据库。 sidebar: order: 2 --- -近年来流行的一部分 Agent 实现并不以数据库为中心。它们更习惯用文件——JSONL 记录、Markdown 笔记——承载上下文与记忆,需要可观测性时再去接入独立的服务。Bub 的 [tape 模型](/zh-cn/docs/concepts/tape-and-context/)(参见 [tape.systems](https://tape.systems))则提供了另一种表达:用来重建 context 的同一份 append-only 记录,本身就是出问题时你要去查的运行日志。 +近年来流行的一部分 Agent 实现更习惯用文件——JSONL 记录、Markdown 笔记、本地缓存——承载上下文与记忆,需要可观测性时再去接入独立的服务。Bub 的 [tape 模型](/zh-cn/docs/concepts/tape-and-context/)(参见 [tape.systems](https://tape.systems))描述的不是某一种存储介质,而是底层抽象:用来重建 context 的同一份 append-only 记录,也可以承载偏记忆的事实,并成为出问题时你要去查的运行日志。 -本教程使用 [`bub-tapestore-sqlalchemy`](https://github.com/bubbuild/bub-contrib/tree/main/packages/bub-tapestore-sqlalchemy) 把 Bub 默认的每个 tape 一个 JSONL 的写入方式,换成一份 SQLAlchemy 数据库,并把同一个流程跑两次: +这不要求你一定使用数据库。默认 store 会在 `~/.bub/tapes/` 下为每条 tape 写一个 JSONL 文件,这很简单,也便于迁移。但这个抽象同样适合放进数据库:当 tape entries 进入数据库之后,你可以开始利用数据库在查询规划、索引、事务写入、备份流程和存储扩展上的能力,而不需要改 Bub 的 turn pipeline。 -1. **本地 SQLite** —— 一个可以备份、复制、用任意 SQL 客户端打开的文件。 -2. **[Turso](https://turso.tech) 嵌入式副本(libSQL)** —— 同样是本地文件,并由 libSQL 透明同步到托管云端。本地写入保持快速且事务安全,云端副本是持久化、可跨机器访问的真实存储。 +本教程使用 [`bub-tapestore-sqlalchemy`](https://github.com/bubbuild/bub-contrib/tree/main/packages/bub-tapestore-sqlalchemy),把 Bub 的文件 store 换成一个可以用标准 SQL 工具检查的本地 SQLite 数据库。 -[`,tape.info`](/zh-cn/docs/tutorials/observability/) 与 `,tape.search` 流程不变:在两种模式之间切换,只需要更换 storage URL,以及——对于 Turso ——一个 auth token。 +[`,tape.info`](/zh-cn/docs/tutorials/observability/) 与 `,tape.search` 流程不变:变化的只有 tape store。 ## 前置条件 @@ -20,11 +19,16 @@ sidebar: - Bub 已安装,且 `uv run bub --help` 可以运行(参考 [安装](/zh-cn/docs/getting-started/install/))。 - 一个 workspace,其中 `uv run bub run "What tools do you have?"` 能调用已配置的模型。 -- `sqlite3` CLI,用于可选的本地检查。 -- 一个免费的 [Turso](https://turso.tech/) 账号,并已通过 `turso auth login` 完成 [`turso`](https://docs.turso.tech/cli/installation) CLI 鉴权。 +- `sqlite3` CLI,用于可选的数据库检查。 ## 1. 安装插件 +如果还没有配置过 `BUB_HOME`,先把它设成绝对路径: + +```bash +export BUB_HOME="${BUB_HOME:-$HOME/.bub}" +``` + ```bash uv run bub install bub-tapestore-sqlalchemy@main ``` @@ -39,14 +43,18 @@ uv run bub hooks provide_tape_store: builtin, tapestore-sqlalchemy ``` -## 2. 在本地 SQLite 上跑一个 turn +## 2. 把 Bub 指向本地 SQLite 数据库 -插件默认使用 `/tapes.db`。教程里换一个方便检查的位置: +插件默认使用 `/tapes.db`。教程里换一个方便找到和清理的位置: ```bash export BUB_TAPESTORE_SQLALCHEMY_URL="sqlite+pysqlite:///$PWD/bub-tapes.db" ``` +这个 URL 会传给 SQLAlchemy。`sqlite+pysqlite` 方言使用 Python 标准库里的 SQLite driver,因此不需要额外安装数据库驱动。 + +## 3. 跑一个 turn + 跑一个小的自然语言任务,然后让 Bub 检查刚刚写入的 tape: ```bash @@ -63,6 +71,10 @@ entries_since_last_anchor: 8 last_token_usage: 4106 ``` +具体 tape name 和数量取决于你的 workspace 与模型调用,但命令应当输出 tape name、至少一个 anchor,以及 `session/start` 之后写入的 entries。 + +## 4. 检查数据库 + 用标准 CLI 打开 SQLite 文件: ```bash @@ -73,87 +85,30 @@ sqlite3 "$PWD/bub-tapes.db" "SELECT name, last_entry_id FROM tapes;" 86774b31b96845a4__0b871d5e50e7c192|9 ``` -`last_entry_id` 与 `,tape.info` 中的 `entries` 一致——插件使用的是每条 tape 单调递增的计数器,也就是 Bub 重放 context 时使用的那个计数器。 - -## 3. 创建 Turso 数据库 - -```bash -turso db create bub-tapes -export TURSO_DB_URL="$(turso db show bub-tapes --url)" -export TURSO_AUTH_TOKEN="$(turso db tokens create bub-tapes)" -``` - -把 libSQL 的 SQLAlchemy 方言安装到运行 `bub` 的同一个 Python 环境: - -```bash -uv pip install sqlalchemy-libsql -``` - -## 4. 把 Bub 指向 Turso 嵌入式副本 - -嵌入式副本会把一份本地 libSQL 文件与 Turso 保持同步。你在本地写入——速度快、事务安全、每次 `append` 都不需要往返网络——再由 libSQL 把同样的字节流同步到云端。这正好契合 append-only 的 tape:Agent 永远不需要等待网络,云端副本在后台追上。 - -把 SQLAlchemy URL 换成 libSQL 方言,并通过 `BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS` 传入 auth token 与 sync URL: +`last_entry_id` 记录每条 tape 当前写到的最高 entry id。也可以检查 anchor 行: ```bash -HOST="${TURSO_DB_URL#libsql://}" -export BUB_TAPESTORE_SQLALCHEMY_URL="sqlite+libsql:///$PWD/bub-tapes-replica.db" -export BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS="$(printf '{"auth_token": "%s", "sync_url": "https://%s"}' "$TURSO_AUTH_TOKEN" "$HOST")" -``` - -`BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS` 是一个 JSON 对象,会原样转给 `sqlalchemy.create_engine(connect_args=...)`。对 libSQL 来说,`auth_token` 与 `sync_url` 这两个键就足以让本地文件变成一份云端副本。 - -## 5. 在 Turso 上跑一个 turn - -```bash -uv run bub run "Reply with one short sentence: hello via embedded replica." -uv run bub run ",tape.info" +sqlite3 "$PWD/bub-tapes.db" \ + "SELECT entry_id, anchor_name, entry_date FROM tape_entries WHERE kind = 'anchor';" ``` ```text -name: f8d663d1fa2fa3e9__0b871d5e50e7c192 -entries: 9 -anchors: 1 -last_anchor: session/start -entries_since_last_anchor: 8 -last_token_usage: 2546 +1|session/start|2026-05-15T01:27:57Z ``` -输出与第 2 节一致——只是后端换了一个。 - -## 6. 在 Turso 上验证数据 - -打开远程数据库的 SQL shell: +这就是完整的存储切换:Bub 仍然写 append-only tape entries,只是它们现在进入 SQL 数据库,而不是每条 tape 一个 JSONL 文件。 -```bash -turso db shell bub-tapes -``` - -```sql -.tables --- tape_entries tapes - -SELECT name, last_entry_id FROM tapes; --- f8d663d1fa2fa3e9__0b871d5e50e7c192|10 - -SELECT entry_id, anchor_name, entry_date - FROM tape_entries - WHERE kind = 'anchor' - ORDER BY tape_id DESC, entry_id DESC LIMIT 1; --- 1|session/start|2026-05-15T01:27:57Z -``` +## 5. 扩展后端 -![Turso shell 中显示由 Bub 写入的 tapes 与 tape_entries 表](/docs/tapestore-sqlalchemy/turso-shell.png) +`bub-tapestore-sqlalchemy` 通过 `BUB_TAPESTORE_SQLALCHEMY_URL` 配置,因此同一个插件也可以指向其他 SQLAlchemy 支持的数据库 URL,前提是运行 `bub` 的环境中安装了对应 dialect 与 driver。不同 driver 可能需要兼容性设置或特定处理;用于长期 tape 之前,请先用本教程里的 `,tape.info` 流程验证后端行为。 -把 tape 放进真正的数据库的好处就在这里:context、记忆、运行日志是同一份记录,而这份记录现在可以从任何一台 export 了相同 `BUB_TAPESTORE_SQLALCHEMY_URL` 与 `BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS` 的机器上访问。 +如果你需要更专门的 SQLite store,可以参考 [`bub-tapestore-sqlite`](https://github.com/bubbuild/bub-contrib/tree/main/packages/bub-tapestore-sqlite)。它基于 `sqlite-vec`,并支持利用 embedding 做 tape 检索。 ## 清理 ```bash -turso db destroy bub-tapes --yes -unset BUB_TAPESTORE_SQLALCHEMY_URL BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS \ - TURSO_DB_URL TURSO_AUTH_TOKEN -rm -f "$PWD/bub-tapes.db" "$PWD/bub-tapes-replica.db"* +unset BUB_TAPESTORE_SQLALCHEMY_URL BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS +rm -f "$PWD/bub-tapes.db" ``` ## 下一步