Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions website/src/content/docs/docs/tutorials/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 SQLite](/docs/tutorials/tapestore-sqlalchemy/) — replace the file-based tape store with a local SQLite database.

## Next steps

Expand Down
118 changes: 118 additions & 0 deletions website/src/content/docs/docs/tutorials/tapestore-sqlalchemy.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
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
---

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.

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.

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 tape store changes.

## 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 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
```

Confirm Bub picked up the entry point:

```bash
uv run bub hooks
```

```text
provide_tape_store: builtin, tapestore-sqlalchemy
```

## 2. Point Bub at a local SQLite database

The plugin uses `<BUB_HOME>/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
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
```

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
sqlite3 "$PWD/bub-tapes.db" "SELECT name, last_entry_id FROM tapes;"
```

```text
86774b31b96845a4__0b871d5e50e7c192|9
```

The `last_entry_id` tracks the highest entry id written to each tape. Inspect the anchor rows as well:

```bash
sqlite3 "$PWD/bub-tapes.db" \
"SELECT entry_id, anchor_name, entry_date FROM tape_entries WHERE kind = 'anchor';"
```

```text
1|session/start|2026-05-15T01:27:57Z
```

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.

## 5. Extend the backend

`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.

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
unset BUB_TAPESTORE_SQLALCHEMY_URL BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS
rm -f "$PWD/bub-tapes.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.
1 change: 1 addition & 0 deletions website/src/content/docs/zh-cn/docs/tutorials/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 与 SQLite 持久化 tape](/zh-cn/docs/tutorials/tapestore-sqlalchemy/) — 把基于文件的 tape store 换成本地 SQLite 数据库。

## 下一步

Expand Down
118 changes: 118 additions & 0 deletions website/src/content/docs/zh-cn/docs/tutorials/tapestore-sqlalchemy.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
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 记录,也可以承载偏记忆的事实,并成为出问题时你要去查的运行日志。

这不要求你一定使用数据库。默认 store 会在 `~/.bub/tapes/` 下为每条 tape 写一个 JSONL 文件,这很简单,也便于迁移。但这个抽象同样适合放进数据库:当 tape entries 进入数据库之后,你可以开始利用数据库在查询规划、索引、事务写入、备份流程和存储扩展上的能力,而不需要改 Bub 的 turn pipeline。

本教程使用 [`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` 流程不变:变化的只有 tape store。

## 前置条件

你需要:

- Bub 已安装,且 `uv run bub --help` 可以运行(参考 [安装](/zh-cn/docs/getting-started/install/))。
- 一个 workspace,其中 `uv run bub run "What tools do you have?"` 能调用已配置的模型。
- `sqlite3` CLI,用于可选的数据库检查。

## 1. 安装插件

如果还没有配置过 `BUB_HOME`,先把它设成绝对路径:

```bash
export BUB_HOME="${BUB_HOME:-$HOME/.bub}"
```

```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. 把 Bub 指向本地 SQLite 数据库

插件默认使用 `<BUB_HOME>/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
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
```

具体 tape name 和数量取决于你的 workspace 与模型调用,但命令应当输出 tape name、至少一个 anchor,以及 `session/start` 之后写入的 entries。

## 4. 检查数据库

用标准 CLI 打开 SQLite 文件:

```bash
sqlite3 "$PWD/bub-tapes.db" "SELECT name, last_entry_id FROM tapes;"
```

```text
86774b31b96845a4__0b871d5e50e7c192|9
```

`last_entry_id` 记录每条 tape 当前写到的最高 entry id。也可以检查 anchor 行:

```bash
sqlite3 "$PWD/bub-tapes.db" \
"SELECT entry_id, anchor_name, entry_date FROM tape_entries WHERE kind = 'anchor';"
```

```text
1|session/start|2026-05-15T01:27:57Z
```

这就是完整的存储切换:Bub 仍然写 append-only tape entries,只是它们现在进入 SQL 数据库,而不是每条 tape 一个 JSONL 文件。

## 5. 扩展后端

`bub-tapestore-sqlalchemy` 通过 `BUB_TAPESTORE_SQLALCHEMY_URL` 配置,因此同一个插件也可以指向其他 SQLAlchemy 支持的数据库 URL,前提是运行 `bub` 的环境中安装了对应 dialect 与 driver。不同 driver 可能需要兼容性设置或特定处理;用于长期 tape 之前,请先用本教程里的 `,tape.info` 流程验证后端行为。

如果你需要更专门的 SQLite store,可以参考 [`bub-tapestore-sqlite`](https://github.com/bubbuild/bub-contrib/tree/main/packages/bub-tapestore-sqlite)。它基于 `sqlite-vec`,并支持利用 embedding 做 tape 检索。

## 清理

```bash
unset BUB_TAPESTORE_SQLALCHEMY_URL BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS
rm -f "$PWD/bub-tapes.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` 遵循的插件契约。
Loading