diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 370b911e7..25b414ec4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,6 @@ +# Standalone examples vendor frontend assets and serve as teaching material; skip all hooks. +exclude: ^website/public/examples/ + repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: "v5.0.0" diff --git a/README.md b/README.md index 7874e2c07..ff1a40eff 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

Build your Bub.

-[Website](https://bub.build) · [Docs](https://bub.build/docs/getting-started/) · [Hub](https://hub.bub.build) +[Website](https://bub.build) · [Docs](https://bub.build/docs/) · [Hub](https://hub.bub.build) @@ -25,9 +25,9 @@ In a Python 3.12+ project: uv add bub ``` -Run your first agent with the [SDK guide](https://bub.build/docs/build/sdk/), then [serve it with FastAPI](https://bub.build/docs/tutorials/fastapi/) when you need an HTTP endpoint. +Run your first agent with the [SDK guide](https://bub.build/docs/sdk/), then [serve it with FastAPI](https://bub.build/docs/sdk/web-service/) when you need an HTTP endpoint. -You can also embed `BubAgent` to reuse the application's settings and plugin hooks in your service. This uses `bub[app]`; [Reuse application plugins](https://bub.build/docs/build/reuse-plugins/) explains the integration choices and their requirements. +You can also embed `BubAgent` to reuse the application's settings and plugin hooks in your service. This uses `bub[app]`; [Reuse application plugins](https://bub.build/docs/sdk/reuse-plugins/) explains the integration choices and their requirements. ## Customize the Bub app @@ -51,11 +51,11 @@ Follow the prompts to choose plugins and configure your model, then start a conv bub chat ``` -[Run your first conversation](https://bub.build/docs/getting-started/first-turn/) walks through a task in your workspace. [Customize the Bub app](https://bub.build/docs/build/) covers tools, plugins, and packaging. For `uv tool install` and other setup options, see [Install Bub](https://bub.build/docs/getting-started/install/#app-install). +[Run your first conversation](https://bub.build/docs/app/workspace/) walks through a task in your workspace. [Customize the Bub app](https://bub.build/docs/app/workspace/) covers tools, plugins, and packaging. For `uv tool install` and other setup options, see [Install Bub](https://bub.build/docs/getting-started/install/#app-install). ## Documentation -- [Choose a starting point](https://bub.build/docs/getting-started/) +- [Choose a starting point](https://bub.build/docs/) - [Shared concepts and capabilities](https://bub.build/docs/concepts/) - [API and application reference](https://bub.build/docs/reference/) diff --git a/website/TRANSLATING.md b/website/TRANSLATING.md new file mode 100644 index 000000000..e29d7a7cc --- /dev/null +++ b/website/TRANSLATING.md @@ -0,0 +1,34 @@ +# Translating the docs + +Applies to every locale under `src/content/docs/`. The English source is `src/content/docs/docs/`. Keep its paragraph and heading structure; do not merge, split, or reorder. + +## Glossary + +A term that names code keeps its spelling. Do not invent a local name. A new locale adds a column. + +Anything not listed is an ordinary technical word. Translate it the usual way; judge each case. Keep one rendering per page. + +| Term | zh-cn | Note | +| --- | --- | --- | +| `turn` | `turn` | One inbound message and all its work. Never 轮次, 回合. | +| `tape` | `tape` | The conversation record. Never 磁带. | +| `anchor` | `anchor` | Where the context starts. | +| `handoff` | `handoff` | Writes a new anchor. | +| `state` | `state` | The dictionary a turn shares. Type `TurnState`. | +| `steering` | `steering` | Never 引导. | +| `queue up` | `queue up` | Never 排队. | +| `hook` | `hook` | Never 钩子. | +| `envelope` | `envelope` | Never 信封. | +| `trace` | `trace` | Never 轨迹, 追踪. | +| `CodeMode` | `CodeMode` | The class. The feature is `code mode`. Never 代码模式. | +| `extra` | `extra` | User-facing prose says "`app` extra" and "`trace` extra"; keep `extra` for the packaging mechanism, `--extra`, and `package[extra]`. | +| `harness` | `harness` | The app layer around the loop. | + +Docs pages explain the `steering` and `queue up` difference where it changes the reader's choice. A steering message that the current turn does not consume falls back to the pending queue; pages that mention steering keep that fallback rule. For the general statement, link [Steering and queueing](https://docs.github.com/en/copilot/how-tos/copilot-sdk/features/steering-and-queueing) instead of translating it. + +## Punctuation (zh-cn) + +- Use full-width `,。;:!?、` and `「」`. +- Put one space between Chinese and Latin letters, digits, or inline code. +- Use `……` for an ellipsis and `——` for a dash. +- Put no space next to full-width punctuation, and none inside bold or italic text. diff --git a/website/astro.config.mjs b/website/astro.config.mjs index cd99e3440..43d142914 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -4,6 +4,7 @@ import { defineConfig, envField } from 'astro/config'; import tailwindcss from '@tailwindcss/vite'; import starlight from '@astrojs/starlight'; import cloudflare from '@astrojs/cloudflare'; +import headingAnchors from './src/integrations/heading-anchors.mjs'; import peleDiagrams from './src/integrations/pele.mjs'; const astro_image_mode = @@ -26,6 +27,10 @@ export default defineConfig({ imageService: image_service, prerenderEnvironment: 'node', }), + redirects: { + '/docs/sdk/options': '/docs/sdk/tape', + '/zh-cn/docs/sdk/options': '/zh-cn/docs/sdk/tape', + }, site: process.env.SITE_URL ?? 'https://bub.build', env: { schema: { @@ -39,6 +44,13 @@ export default defineConfig({ }, }, vite: { + // Keep the dependency cache of `astro build` away from the one `astro dev` + // uses. When both share `node_modules/.vite`, every build rewrites the + // optimized deps and a running dev server answers with "There is a new + // version of the pre-bundle" instead of hot-reloading. + cacheDir: + process.env.VITE_CACHE_DIR ?? + (astro_image_mode === 'dev' ? 'node_modules/.vite' : 'node_modules/.vite-build'), plugins: [tailwindcss()], resolve: { alias: { @@ -47,6 +59,8 @@ export default defineConfig({ }, }, integrations: [ + // Let headings carry an explicit id so every locale shares the same anchor. + headingAnchors(), // Transform Mermaid fences before Starlight highlights code blocks. peleDiagrams(), starlight({ @@ -111,104 +125,103 @@ export default defineConfig({ social: [{ icon: 'github', label: 'GitHub', href: 'https://github.com/bubbuild/bub' }], sidebar: [ { - label: 'Start here', - translations: { 'zh-CN': '从这里开始' }, - items: ['docs/getting-started', 'docs/getting-started/install'], + label: 'Get started', + translations: { 'zh-CN': '开始' }, + items: ['docs/getting-started', 'docs/getting-started/install', 'docs/getting-started/quickstart'], }, { - label: 'Build your own agent', - translations: { 'zh-CN': '构建自己的 Agent' }, - items: ['docs/build/sdk', 'docs/build/reuse-plugins', 'docs/tutorials/fastapi'], + label: 'Concepts', + translations: { 'zh-CN': '核心概念' }, + items: [ + 'docs/concepts', + 'docs/concepts/kernel', + 'docs/concepts/turn', + 'docs/concepts/tape', + 'docs/concepts/events', + 'docs/concepts/extension-points', + ], }, { - label: 'Customize the Bub app', - translations: { 'zh-CN': '定制 Bub 应用' }, + label: 'Use Bub App', + translations: { 'zh-CN': '使用 Bub 应用' }, items: [ - 'docs/build', - 'docs/getting-started/first-turn', - 'docs/concepts/turn-pipeline', { - label: 'Extend the app', - translations: { 'zh-CN': '扩展应用' }, - collapsed: true, + label: 'Run', + translations: { 'zh-CN': '运行' }, + collapsed: false, items: [ - 'docs/build/app-tools', - 'docs/getting-started/first-skill', - 'docs/build/skills', - 'docs/getting-started/first-plugin', - 'docs/build/plugins', - 'docs/build/hooks', - 'docs/build/distribution', + 'docs/app/workspace', + 'docs/app/configure', + 'docs/app/terminal', + 'docs/app/gateway', ], }, { - label: 'Configure and run', - translations: { 'zh-CN': '配置与运行' }, - collapsed: true, + label: 'Extend', + translations: { 'zh-CN': '扩展' }, + collapsed: false, items: [ - 'docs/operate', - 'docs/operate/configure', - 'docs/operate/channels', - 'docs/operate/channels/cli', - 'docs/operate/channels/telegram', - 'docs/operate/deploy', + 'docs/app/tools', + 'docs/app/skills', + 'docs/app/subagents', + 'docs/app/plugins', ], }, { - label: 'Integrations', - translations: { 'zh-CN': '接入集成' }, - collapsed: true, + label: 'Distribute and deploy', + translations: { 'zh-CN': '分发与部署' }, + collapsed: false, items: [ - { - slug: 'docs/tutorials', - label: 'Integration index', - translations: { 'zh-CN': '集成索引' }, - }, - 'docs/tutorials/mcp', - 'docs/tutorials/agent-plugins', - 'docs/tutorials/language-extensions', - 'docs/tutorials/acp-server', - 'docs/tutorials/tapestore-sqlalchemy', - 'docs/tutorials/observability', - 'docs/tutorials/local-llama-cpp', + 'docs/app/distribution', + 'docs/app/deploy', ], }, ], }, { - label: 'Shared concepts and capabilities', - translations: { 'zh-CN': '公共概念与能力' }, + label: 'Build your own Bub Agent', + translations: { 'zh-CN': '构建你的 Bub Agent' }, items: [ - 'docs/concepts', - 'docs/build/tools', - 'docs/concepts/tape-and-context', - 'docs/concepts/events', - 'docs/concepts/surfaces', - 'docs/concepts/philosophy', + 'docs/sdk', + 'docs/sdk/tools', + 'docs/sdk/tape', + 'docs/sdk/events', + 'docs/sdk/web-service', ], }, { - label: 'Reference', - translations: { 'zh-CN': '参考' }, + label: 'Integrations', + translations: { 'zh-CN': '集成' }, items: [ + 'docs/integrations/reuse-app', + 'docs/integrations/mcp', { - slug: 'docs/reference', - label: 'Agent and app APIs', - translations: { 'zh-CN': 'Agent 与应用接口' }, + label: 'Portable Agent Plugins', + translations: { 'zh-CN': '使用 Agent Plugins' }, + slug: 'docs/integrations/agent-plugins', }, - { - label: 'Application reference', - translations: { 'zh-CN': '应用参考' }, - items: [ - 'docs/reference/hooks', - 'docs/reference/cli', - 'docs/reference/settings', - 'docs/reference/types', - ], - }, - 'docs/reference/versions', + 'docs/integrations/acp', + 'docs/integrations/local-models', + 'docs/integrations/storage', + 'docs/integrations/observability', + ], + }, + { + label: 'Reference', + translations: { 'zh-CN': '参考' }, + items: [ + 'docs/reference/cli', + 'docs/reference/settings', + 'docs/reference/tools', + 'docs/reference/python-api', + 'docs/reference/hooks', ], }, + { + label: 'Migration', + translations: { 'zh-CN': '迁移' }, + items: ['docs/migration/v0-6'], + }, ], }), ], diff --git a/website/concepts-revision-plan.md b/website/concepts-revision-plan.md new file mode 100644 index 000000000..ab3303c52 --- /dev/null +++ b/website/concepts-revision-plan.md @@ -0,0 +1,158 @@ +# Concepts 核心概念章节修订纲要 + +本纲要用于指导 Bub 文档中 **Concepts(核心概念)** 模块的重构与优化。 + +--- + +## 一、 设计原则与定位 + +1. **侧栏保持概念本质**:侧栏直接展示纯粹、凝练的概念名词(内核、Turn、Tape、事件、扩展点),页面标题再承担具体描述性说明。 +2. **心智模型优先于实现细节**:聚焦架构哲学(Mental Model)与设计边界(Design Philosophy),解释清楚“是什么”、“为什么这样设计”以及“它解决了传统 Agent 的什么痛点”,避免过早陷入 API 参数细节。 +3. **建立全景贯穿感**:各概念之间通过一条统一的数据与控制流串联,形成完整体系。 + +--- + +## 二、 侧栏结构与页面规划 + +| 序号 | 侧栏展示 (Sidebar Label) | 页面标题 (Document Title) | 路径与 Slug | 概念定位 | +| :--- | :--- | :--- | :--- | :--- | +| 0 | **概览** | 概览:Bub 的设计哲学与职责边界 | `docs/concepts/index.mdx` | 全景心智模型与各概念串联 | +| 1 | **内核** *(新增)* | 内核:微内核架构与纯粹组装契约 | `docs/concepts/kernel.mdx` | `bub.core` 哲学、无配置/显式组装 | +| 2 | **Turn** | Turn:消息处理流程与确定性管道 | `docs/concepts/turn.mdx` | Turn / Run / Step 层次与 7 阶段状态机 | +| 3 | **Tape** | Tape:持久存储、不可变事实与上下文投影 | `docs/concepts/tape.mdx` | 事实流 (Write) 与上下文 (Read) 的正交解耦 | +| 4 | **事件** | 事件:可观测与同步决策拦截总线 | `docs/concepts/events.mdx` | 具备裁决权(Decision)的 EventBus | +| 5 | **扩展点** | 扩展点:能力切入边界与单一职责原则 | `docs/concepts/extension-points.mdx` | 架构扩展机制分类与决策树 | + +--- + +## 三、 全景心智流转模型 + +以下流转图作为统领所有概念的核心心智图,贯穿于《概览》与《内核》章节: + +```mermaid +flowchart TD + subgraph AppRuntime["应用运行时 (Application Runtime)"] + Ch["外部渠道 (Channels)"] -->|"接收消息"| Env["信封 (Envelope)"] + Env --> Turn["Turn 管道 (7 阶段生命周期)"] + Turn -->|"生成并分发"| Out["回复出站 (Outbounds)"] + end + + subgraph KernelCore["微内核 (bub.core.Agent)"] + Turn -->|"触发驱动"| Run["Agent 循环 (Run / Steps)"] + Run <-->|"同步拦截与决策"| Bus[("事件总线 EventBus")] + Run -->|"调用执行"| Tools["工具执行 (Tools / Code Mode)"] + Run -->|"记录操作事实"| TapeRec["Tape 记录器"] + end + + subgraph StateStorage["存储与投影 (Memory & Storage)"] + TapeRec -->|"追加不可变条目"| Tape[("Tape (历史事实流水账)")] + Tape -->|"由 Anchor 截断,投影转换"| Context["TapeContext (模型上下文)"] + Context -->|"注入本次请求"| Run + end +``` + +--- + +## 四、 各章节修订大纲与核心内容 + +### 0. 概览 (`index.mdx`) +- **修订方向**:在现有已润色的背景故事(自我进化智能体、阿西莫夫三定律、机器人中立契约)基础上,强化概念之间的串联感。 +- **核心要点**: + 1. **工程化使命**:大模型交互本身具备不确定性,框架的核心使命是在外围建立**确定性的约束、存储、安全审计与流程控制**。 + 2. **全景心智地图**:展示上述 Mermaid 架构全景图,明确标定内核、Turn、Tape、事件、扩展点各自处在消息处理与执行的哪个环节。 + 3. **导航卡片扩展**:卡片网格补充新增的《内核》页面,形成 5 大支柱概览。 + +--- + +### 1. 内核 (`kernel.mdx`) —— *【新增页面】* +- **概念本质**:`bub.core` 的**微内核哲学(Microkernel Design)**。 +- **解决痛点**:为什么不能把所有开关、渠道、配置一股脑塞进 Agent 类里? +- **核心要点**: + 1. **零环境污染与显式依赖注入**: + - `bub.core.Agent` 内部**没有任何 `os.environ` 依赖、没有全局配置项、没有隐藏的全局单例**。 + - 一切能力(Model、Store、Tools、Context、Sidecars)纯粹依靠构造函数显式组装。 + 2. **双层架构的边界划分**: + - **内核层 (`bub.core`)**:只关心最纯粹的单次循环(LLM 调用、工具调度、事件派发、Tape 读写)。可作为极简 Python 库嵌入已有 Web 服务或异步队列。 + - **应用层 (`bub.framework` / `builtin`)**:处理生产级完整应用的外部复杂度(配置加载、渠道网关、会话解析路由、插件自动发现)。 + 3. **中立性协议**:内核不假定特定的模型供应商或数据库存储,纯粹依赖协议(Protocol)与适配器。 + +--- + +### 2. Turn (`turn.mdx`) +- **概念本质**:消息交互的**确定性状态机与生命周期管道**。 +- **解决痛点**:多轮会话中,状态何时加载/保存?出错时如何避免状态悬空?回复怎样可靠路由? +- **核心要点**: + 1. **三层执行颗粒度边界**: + - `Turn`:外部一次消息交互的完整生命周期(接收 -> 保存 -> 发送)。 + - `Run`:当前 Turn 内部启动的模型与工具迭代循环(直到模型满意或达到上限)。 + - `Step`:一次具体的模型推理与随后的所有并发工具调用。 + 2. **7 阶段确定性管道**: + - 规范阶段流转:`resolve_session` -> `load_state` -> `build_prompt` -> `run_model` -> `save_state` -> `render_outbound` -> `dispatch_outbound`。 + - 强调异常保障机制:模型阶段进入 `finally` 确保 `save_state` 执行,保障即使 LLM 出错或超时,会话状态也不会悬空。 + 3. **TurnState 的瞬态契约**:明确它是单次管道共享的内存字典,生命周期不跨 Turn。 + +--- + +### 3. Tape (`tape.mdx`) +- **概念本质**:**不可变事实追加流 (Write) 与 上下文投影 (Read) 的正交解耦**。 +- **解决痛点**:传统框架将会话等同于 `messages: list[Message]` 数组,导致上下文超长时只能盲目截断或粗暴 summary,丧失审计回溯能力。 +- **核心要点**: + 1. **事件溯源(Event Sourcing)视角下的 Tape**: + - Tape 是 Write Model:按时间戳线性追加所有客观事实(对话、工具调用入参、执行返回值、业务事件)。 + - 正常运行只追加不篡改,保障系统的确定性、可调试性与合规审计。 + 2. **上下文投影(Projection via TapeContext)**: + - TapeContext 是 Read Model:从全量 Tape 条目中,按规则过滤转换出当前模型推理所必需的视界。 + 3. **Anchor(锚点)与认知交接(Handoff)**: + - Anchor 不是简单的物理清空,而是**阶段性认知里程碑**(例如从“信息收集”走向“订单确认”)。 + - 剖析显式 Handoff 与超长自动 Handoff 的触发机制。 + +--- + +### 4. 事件 (`events.mdx`) +- **概念本质**:**从只读可观测,演进为具备裁决权(Decision)的同步总线**。 +- **解决痛点**:如何在不污染业务工具代码的前提下,统一实现权限检查、人工介入(Human-in-the-loop)、数据脱敏与调用替换? +- **核心要点**: + 1. **三类数据流的区别**:清楚界定 `AgentEvent`(生命周期与控制)、`StreamEvent`(前端流式输出)与 `TapeEntry`(持久化记录)。 + 2. **同步阻塞与决策系统 (`ToolCallDecision`)**: + - 为什么事件总线是同步等待的?因为必须保证“策略控制先于动作执行”。 + - 阐释三种裁决手段:`deny()`(拒绝调用并反馈给模型)、`replace()`(Mock 或直接返回替代结果)、改写参数(替换 `event.call`)。 + 3. **控制反转(IoC)哲学**:业务工具只需老老实实写执行逻辑,风控、权限、预算等跨切面逻辑交由事件监听器负责。 + +--- + +### 5. 扩展点 (`extension-points.mdx`) +- **概念本质**:Bub 系统的**能力切入分类体系与单一职责原则**。 +- **解决痛点**:开发者面对新需求时,不知道该写成 Tool、Skill、Hook、Event 还是 Channel。 +- **核心要点**: + 1. **正交的五大扩展维度**: + - **动作层 (Action)**:工具(Tools,负责可执行代码)。 + - **认知与流程层 (Instruction)**:技能(Skills,提供可被模型阅读的任务说明书)。 + - **策略与约束层 (Policy)**:事件总线(Events,负责拦截改写)。 + - **生命周期层 (Lifecycle)**:插件与 Turn Hooks(注入阶段逻辑)。 + - **通信与传输层 (Transport)**:渠道(Channels,连接各类 IM 和外部协议)。 + 2. **决策树(选择心智)**: + - “我要调外部系统或数据库” 👉 工具。 + - “我要教模型某个复杂业务 SOP” 👉 技能。 + - “我要在模型调用敏感操作前校验权限” 👉 事件处理器。 + - “我要改变消息进入模型的组装方式” 👉 Turn Hook。 + - “我要对接钉钉、飞书或 Telegram” 👉 渠道。 + 3. **黄金法则:单一所有权(One Owner)**: + - 避免在工具里硬编码风控规则,避免在技能里塞大段待执行脚本,保持扩展正交。 + +--- + +## 五、 后续实施与执行路线 + +1. **配置调整**: + - 更新 `website/astro.config.mjs` 中的 `sidebar` 配置,在 `Concepts` 分组下加入 `docs/concepts/kernel`。 +2. **新增页面**: + - 编写 `src/content/docs/zh-cn/docs/concepts/kernel.mdx`(中文)。 + - 编写 `src/content/docs/docs/concepts/kernel.mdx`(英文)。 +3. **润色与强化现有各篇**: + - 更新 `index.mdx`:加入全景架构图与内核导航卡片。 + - 润色 `turn.mdx`:强化管道防腐层与确定性状态流转。 + - 润色 `tape.mdx`:深化 Event Sourcing / 投影解耦哲学。 + - 润色 `events.mdx`:突出同步决策拦截与控制反转。 + - 润色 `extension-points.mdx`:梳理决策树并强化单一所有权。 +4. **验证与校验**: + - 运行 `pnpm build` 或 `make docs-test`,确保两套语言的链接、Mermaid 图表及路由无死链。 diff --git a/website/docs-audit-vue-style.md b/website/docs-audit-vue-style.md new file mode 100644 index 000000000..3700f5550 --- /dev/null +++ b/website/docs-audit-vue-style.md @@ -0,0 +1,114 @@ +# Vue 中文文档写作风格调研(给 Bub 中文文档改写用) + +Sources (primary only): + +- https://cn.vuejs.org/guide/introduction.html +- https://cn.vuejs.org/guide/essentials/application.html +- https://cn.vuejs.org/guide/essentials/reactivity-fundamentals.html +- https://cn.vuejs.org/api/ (结构) +- https://cn.vuejs.org/api/application.html (API 词条形态) +- https://github.com/vuejs/docs/blob/main/.github/contributing/writing-guide.md +- https://github.com/vuejs/docs/blob/main/.vitepress/config.ts (信息架构) +- https://github.com/vuejs-translations/docs-zh-cn/wiki/翻译须知 (中文排版/术语) + +--- + +## 1. 信息架构:Guide vs API + +| 层 | 目的 | 读法 | 内容密度 | +|---|---|---|---| +| **指南 Guide** | 按问题讲“为什么 → 怎么用” | 顺序读 | 故事流;先 80% 场景,再进阶 | +| **API 参考** | 完整、可检索的词条 | 跳读/查字典 | 类型 → 详细信息 → 示例 → 参考链接 | +| **快速上手 / 教程 / 示例** | 低摩擦上手 | 可选路径 | 简介页明确分流 | + +侧栏分层(config):开始 → 基础 → 深入组件 → 逻辑复用 → 内置 → 规模化 → 最佳实践 → TS → 进阶。API 按全局 / 组合式 / 选项式 / 内置 / SFC / 进阶分桶。 + +**Bub 映射**:概念页走 Guide 叙事;CLI/配置/类型签名走 API 词条;别把完整参数表塞进入门页。 + +--- + +## 2. 开篇模式(到第一段代码前有几句) + +实测: + +- **简介**:1–2 句定义 + 一句能力边界 → 立刻最小示例(约 2–3 句后出码)。 +- **创建一个应用**:H1 后几乎无导语,第一节标题下 **1 句** 就出 `createApp`。 +- **响应式基础**:页首一句“本页含两种 API 风格”→ 小节下 **2–3 句** 说明意图 → 代码。 +- **API 词条**:一句功能说明 → **类型** → **详细信息** → **示例**(几乎无散文铺垫)。 + +规则:**先一句话说明“这是什么 / 解决什么”,尽快给最小可运行片段**;不要用半页背景才出码。 + +--- + +## 3. 同一页如何同时服务初学者与高手 + +1. **主线只讲当前概念**;细节、边界、深层机制用链接推到后文/API/进阶(“后续章节会讲”“先别急”)。 +2. **声明前提**(如需 HTML/JS),并给补课链接;不假装零基础也能硬啃框架。 +3. **双轨示例**(选项式 / 组合式)并排或可切换,核心概念声明为共享。 +4. **按角色分流**:“如果你是初学者… / 如果你是有经验的开发者…” 各给一条路径。 +5. **进阶块后置**(caveats、性能、与外部系统集成),主路径保持顺读。 +6. 首次阅读时,**少链到会打断心流的深链**;若必须链,说明“第一次读是否需要点开”。 + +--- + +## 4. 刻意避开的反模式 + +来自 Writing Guide + 中文页实操: + +- **套话 / 权威口号**:不说“最佳实践所以该用 X”,用具体问题与例子证明。 +- **循环复述**:不把同一句换措辞再说一遍;用链接代替大段重复。 +- **一次塞多个新概念**(文+码同时引入两套新东西)。 +- **标题写解法不写问题**(差:“使用 props”;好:“用 Props 向子组件传递数据”)。 +- **滥用 tip/warning 盒子**;每页 tip/caveat 不宜超过约 2 个;相邻两个 alert 视为失败信号。 +- **否定读者吃力**:“很简单”“显然”“只需”。 +- **空洞概括 + 无代码**:Show, don’t tell。 +- **示例过抽象**(`ComponentA`);优先具体、可共鸣(`BlogPost`、待办树)。 +- **中文侧**:直译英文语序、夹带无信息的完句填充词、中英空格/标点混乱、术语前后不一。 + +--- + +## 5. Bub 中文文档可执行清单 + +### 结构 + +- [ ] 页面是 **Guide**(顺序叙事)还是 **API**(词条)?两者版式不要混。 +- [ ] Guide:问题 → 最小例子 → 解释 → 边界/进阶链接。 +- [ ] API:一句话功能 → 类型/签名 → 详细信息 → 示例 → 互链指南。 +- [ ] 侧栏按“功率/成本比”排序:先高频刚需,后niche。 + +### 开篇与节奏 + +- [ ] H1/小节后 **≤3 句** 出现第一段代码(概念页);API 词条可 **1 句**。 +- [ ] 一次只引入一个新概念;下一概念用新小节。 +- [ ] 代码后用 **短列表** 点出刚看到的 1–2 个要点,不另起长散文复述。 + +### 读者分流 + +- [ ] 页首声明前提知识(若有)。 +- [ ] 初学者主路径可独立读完;专家细节用链接/后置小节。 +- [ ] 需要“两种写法”时并排示例,并写明共同点。 + +### 文风(中文) + +- [ ] 标题描述 **问题/任务**,不是 API 名堆砌。 +- [ ] 用语具体、短句;避免“赋能”“最佳实践”空话。 +- [ ] 不用“简单/显然/只需”;用“你 / 我们”,不用“您”。 +- [ ] 术语前后一致;专有 API/品牌按约定保留英文(参考 Vue 译法表精神:统一表驱动)。 +- [ ] 中英文之间空格;全角 `,。;:!?`;括号用半角 `()`;行内代码用反引号。 +- [ ] 代码、注释、docstring、system prompt 和日志按示例场景选择语言,不机械中文化;真实 SDK 与跨语言插件可使用自然英文。 +- [ ] 先问题后方案;用例子证明取舍,不堆 tip 盒。 +- [ ] 优先互链,避免多页复制同一大段说明。 + +### 改写自检(删改时) + +- [ ] 删掉对理解无增益的开场白与同义反复。 +- [ ] 每个小节能否回答:“为什么要关心 → 怎么做 → 下一步去哪”。 +- [ ] 若出现第三个 tip/警告:改为独立“注意事项”小节或链到参考页。 + +--- + +## 附:中文译写习惯(摘自译须,Bub 可对齐) + +- 应用(不写“应用程序”)、开发者、其他、举例来说。 +- 关联词可补(然而 / 并且 / 只要…就…)让逻辑更顺,但勿注水。 +- 调整为中文语序;省略仅服务英文句式的虚词。 diff --git a/website/public/examples/bub-todo-agent/.env.example b/website/public/examples/bub-todo-agent/.env.example new file mode 100644 index 000000000..2fbfd6789 --- /dev/null +++ b/website/public/examples/bub-todo-agent/.env.example @@ -0,0 +1,12 @@ +# Copy to .env and fill in your own values. Existing environment variables win over .env. +# cp .env.example .env + +BUB_MODEL=openai:gpt-4o-mini +BUB_API_BASE=https://api.openai.com/v1 + +# Required. REPUBLIC_OPENAI_API_KEY works as a fallback. +BUB_API_KEY=sk-... + +# Server bind address +BUB_HOST=127.0.0.1 +BUB_PORT=8000 diff --git a/website/public/examples/bub-todo-agent/.gitignore b/website/public/examples/bub-todo-agent/.gitignore new file mode 100644 index 000000000..156b47ee4 --- /dev/null +++ b/website/public/examples/bub-todo-agent/.gitignore @@ -0,0 +1,6 @@ +.venv/ +__pycache__/ +sessions/ +*.pyc +.DS_Store +.env diff --git a/website/public/examples/bub-todo-agent/README.md b/website/public/examples/bub-todo-agent/README.md new file mode 100644 index 000000000..ab2259516 --- /dev/null +++ b/website/public/examples/bub-todo-agent/README.md @@ -0,0 +1,69 @@ +# Bub Todo Agent + +A runnable end-to-end Bub SDK example: FastAPI + agent tools + Tape + agent events + SSE, with a frontend built on self-hosted UnoCSS and Alpine.js. + +## Run + +Requires Python 3.12 or newer and [uv](https://docs.astral.sh/uv/). + +```bash +export BUB_MODEL=openai:gpt-4o-mini +export BUB_API_BASE=https://api.openai.com/v1 +export BUB_API_KEY=sk-... +uv run main.py +``` + +You can also run `./main.py` directly (after `chmod +x main.py`). Dependencies are declared in the PEP 723 header at the top of `main.py`, so uv prepares the environment automatically — no `pyproject.toml` and no manual installs. + +If you would rather not export variables every time, copy the example file and fill it in. It is loaded at startup, and existing environment variables take precedence over `.env`: + +```bash +cp .env.example .env +# then edit .env: BUB_MODEL / BUB_API_BASE / BUB_API_KEY ... +``` + +## Configuration + +| Variable | Default | Description | +| --- | --- | --- | +| `BUB_MODEL` | `openai:gpt-4o-mini` | Model identifier passed to `republic.get_model()` | +| `BUB_API_BASE` | `https://api.openai.com/v1` | API base URL for OpenAI or a compatible service, mapped to `api_base` | +| `BUB_API_KEY` | none | API key; falls back to `REPUBLIC_OPENAI_API_KEY` when unset | +| `BUB_HOST` | `127.0.0.1` | Server bind address | +| `BUB_PORT` | `8000` | Server port | + +These defaults live at the top of `main.py` and can be edited in place; environment variables and `.env` override them. When switching providers, adjust `MODEL`, `BASE_URL`, and `API_KEY` together. The composer reads `GET /config` to show the model, base URL, and key status — the endpoint never returns the key itself. + +`Think level` under the composer can be `Auto`, `Low`, `Medium`, or `High`. `Auto` sends no reasoning options; the others pass `reasoning_effort`, and whether that is honored depends on the model and provider. The button in the top-right corner collapses or expands the todo panel. + +Open . + +## What to try + +1. Click “Add a reading todo” and watch the tool call and the todo panel update. +2. Click the chip that marks “Write the weekly report” as complete to verify multi-turn memory. +3. Ask the agent to delete a todo: the run pauses and a prompt above the composer asks you to confirm. Cancel keeps the todo and tells the agent to suggest completing it; Delete lets `delete_task` run. +4. Refresh the page and ask “What todos did I add earlier?” to verify `FileTapeStore` persistence. The todo list resets on refresh, but the agent still remembers the conversation. + +The confirmation is a `ToolCallStarted` hook. Bub awaits async handlers, so the hook can hold the run until the page answers `POST /chat/confirm`; an unanswered prompt fails closed after two minutes and the deletion is denied. + +Todos live in memory only; every page load calls `POST /tasks/reset` to restore the seed data. Conversations are stored under `sessions/`. + +## API + +- `GET /` — chat page +- `GET /config` — model, base URL, and key status +- `GET /tasks` — real todo state, without calling the model +- `POST /tasks/reset` — restore the seed todos (called on page load) +- `PATCH /tasks/{task_id}` — change a todo's completion state +- `POST /chat/stream` — text, tool status, and confirmation requests over SSE +- `POST /chat/confirm` — approve or cancel the pending `delete_task` call + +## Frontend dependencies + +- UnoCSS runtime 66.12.0: `static/uno.global.js` +- Alpine.js 3.17.5: `static/alpine.min.js` + +The page never loads anything from a third-party CDN. Third-party licenses are listed in `static/THIRD_PARTY_NOTICES.md`. + +This is a single-process development example. It does not include the authentication, database, multi-instance locking, or session-lock reclamation a production deployment would need. diff --git a/website/public/examples/bub-todo-agent/index.html b/website/public/examples/bub-todo-agent/index.html new file mode 100644 index 000000000..59bff2983 --- /dev/null +++ b/website/public/examples/bub-todo-agent/index.html @@ -0,0 +1,496 @@ + + + + + + Bub Todo Agent + + + + +
+
+
+
+
+ Bub +

Bub Todo Agent

+
+ +
+ +
+
+
+ +
+

What are you working on?

+

Tell me your plan. I will use tools to add, look up, and complete todos for you.

+
+ + + +
+
+ +
+ + +
+
+ + +
+
+
+
+ + +
+
+
+ + + +

+ Delete “”? + This cannot be undone. +

+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+ + +
+
+
+
+ + +
+
+ + + + + diff --git a/website/public/examples/bub-todo-agent/main.py b/website/public/examples/bub-todo-agent/main.py new file mode 100755 index 000000000..0d8b88575 --- /dev/null +++ b/website/public/examples/bub-todo-agent/main.py @@ -0,0 +1,324 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# "bub @ git+https://github.com/bubbuild/bub@main", +# "fastapi>=0.135", +# "python-dotenv", +# "uvicorn", +# ] +# /// + +import asyncio +import logging +import os +from collections import defaultdict +from collections.abc import AsyncIterable, Awaitable, Callable +from contextlib import asynccontextmanager, suppress +from datetime import UTC, datetime, timedelta +from itertools import count +from pathlib import Path +from typing import Literal +from uuid import uuid4 + +import republic +from dotenv import load_dotenv +from fastapi import FastAPI, HTTPException, Request +from fastapi.responses import FileResponse +from fastapi.sse import EventSourceResponse, ServerSentEvent +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel, Field + +from bub.core import Agent +from bub.core.events import ToolCallDecision, ToolCallFinished, ToolCallStarted +from bub.core.store import FileTapeStore +from bub.core.tools import tool + + +logger = logging.getLogger(__name__) +BASE_DIR = Path(__file__).resolve().parent + +load_dotenv() + +# ===== App config: override via environment variables or .env, or edit the defaults here ===== +MODEL = os.getenv("BUB_MODEL", "openai:gpt-4.1") +BASE_URL = os.getenv("BUB_API_BASE", "https://api.openai.com/v1") +API_KEY = os.getenv("BUB_API_KEY") or os.getenv("REPUBLIC_OPENAI_API_KEY") +HOST = os.getenv("BUB_HOST", "127.0.0.1") +PORT = int(os.getenv("BUB_PORT", "8000")) +# ========================================================== + +# Called from the ToolCallStarted hook; resolves to True only if the person approves in the UI. +ConfirmDelete = Callable[[dict[str, object]], Awaitable[bool]] + + +def _ago(**delta: float) -> str: + return (datetime.now(UTC) - timedelta(**delta)).isoformat(timespec="seconds") + + +def initial_tasks() -> dict[str, dict[str, object]]: + return { + "task-1": { + "task_id": "task-1", + "title": "Write the weekly report", + "completed": False, + "created_at": _ago(days=2), + "completed_at": None, + }, + "task-2": { + "task_id": "task-2", + "title": "Read the Bub docs", + "completed": True, + "created_at": _ago(days=3), + "completed_at": _ago(hours=5), + }, + } + + +# Demo task store (not a Tape). Each page load resets it via POST /tasks/reset. +TASKS: dict[str, dict[str, object]] = initial_tasks() +TASK_IDS = count(3) + + +def reset_tasks() -> None: + global TASK_IDS + TASKS.clear() + TASKS.update(initial_tasks()) + TASK_IDS = count(3) + + +def all_tasks() -> list[dict[str, object]]: + return [dict(task) for task in TASKS.values()] + + +@tool +def list_tasks() -> list[dict[str, object]]: + """List every todo with its task ID, title, and completion state.""" + return all_tasks() + + +class AddTaskInput(BaseModel): + title: str = Field(min_length=1, max_length=100, description="Short, specific todo title") + + +@tool(name="add_task", model=AddTaskInput) +def add_task(param: AddTaskInput) -> dict[str, object]: + """Add a new incomplete todo.""" + task_id = f"task-{next(TASK_IDS)}" + task = { + "task_id": task_id, + "title": param.title, + "completed": False, + "created_at": _ago(), + "completed_at": None, + } + TASKS[task_id] = task + return dict(task) + + +@tool +def complete_task(task_id: str) -> dict[str, object]: + """Mark a todo complete by task ID. Call list_tasks first if the ID is unknown.""" + if task := TASKS.get(task_id): + task["completed"] = True + task["completed_at"] = _ago() + return dict(task) + return {"task_id": task_id, "error": "task_not_found"} + + +@tool +def delete_task(task_id: str) -> dict[str, object]: + """Permanently delete a todo by task ID. The host must authorize it first.""" + if task := TASKS.pop(task_id, None): + return {"task_id": task_id, "status": "deleted", "title": task["title"]} + return {"task_id": task_id, "error": "task_not_found"} + + +@asynccontextmanager +async def lifespan(app: FastAPI): + async with republic.get_model(MODEL, api_base=BASE_URL, api_key=API_KEY) as model: + agent = Agent( + store=FileTapeStore(BASE_DIR / "sessions"), + tools=[list_tasks, add_task, complete_task, delete_task], + system_prompt=( + "You are a personal todo assistant. Always use tools to read or change todos; " + "never guess task state. If a request is ambiguous, look it up or ask the user " + "first, and respect tool errors and denial reasons." + ), + max_steps=6, + max_tool_concurrency=2, + ) + + @agent.events.on(ToolCallStarted) + async def authorize_tool(event: ToolCallStarted) -> None: + # Async handlers are awaited, so this hook pauses the run until the person answers + # the prompt. Denying is the safe default when nobody answers. + if event.call.tool != "delete_task": + return + confirm: ConfirmDelete | None = event.state.get("confirm_delete") + if confirm is None or not await confirm(event.call.arguments): + event.decision = ToolCallDecision.deny( + "The user did not approve deleting this todo. Do not retry; offer to mark " + "the todo complete instead." + ) + + @agent.events.on(ToolCallFinished) + def audit_tool(event: ToolCallFinished) -> None: + logger.info( + "tool=%s status=%s duration_ms=%s", + event.result.tool, + "error" if event.result.error else "ok", + event.result.duration_ms, + ) + + app.state.model = model + app.state.agent = agent + app.state.session_locks = defaultdict(asyncio.Lock) + # request_id -> future that the delete hook is waiting on; answered by POST /chat/confirm. + app.state.pending_confirms = {} + yield + + +app = FastAPI(title="Bub Todo Agent", lifespan=lifespan) +app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static") + + +class ChatRequest(BaseModel): + # The tape name becomes a filename; namespace it per tenant/user in production. + session_id: str = Field(pattern=r"^[A-Za-z0-9_-]{1,64}$") + prompt: str = Field(min_length=1, max_length=10_000) + think_level: Literal["auto", "low", "medium", "high"] = "auto" + + +def agent_run(agent: Agent, model, req: ChatRequest, confirm_delete: ConfirmDelete): + # Auto sends no reasoning options; other levels depend on the current provider. + options = None if req.think_level == "auto" else {"reasoning_effort": req.think_level} + return agent.run( + req.prompt, + model=model, + options=options, + tape=req.session_id, + # Turn state reaches event hooks and tools; the delete hook awaits this callback. + state={"session_id": req.session_id, "confirm_delete": confirm_delete}, + ) + + +@app.get("/") +async def index(): + return FileResponse(BASE_DIR / "index.html") + + +@app.get("/config") +async def config(): + # Only expose display-safe config; never return the API key. + return {"model": MODEL, "base_url": BASE_URL, "api_key_configured": bool(API_KEY)} + + +@app.get("/tasks") +async def tasks(): + # Deterministic business query; no model call. + return {"tasks": all_tasks()} + + +@app.post("/tasks/reset") +async def reset_demo_tasks(): + # Called on every page load so a refresh starts from the seed data. + reset_tasks() + return {"tasks": all_tasks()} + + +class TaskUpdate(BaseModel): + completed: bool + + +@app.patch("/tasks/{task_id}") +async def update_task(task_id: str, req: TaskUpdate): + # Manual toggle from the page; shares the same TASKS store as the agent tools. + task = TASKS.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail="task_not_found") + task["completed"] = req.completed + task["completed_at"] = _ago() if req.completed else None + return dict(task) + + +class Confirmation(BaseModel): + request_id: str + approved: bool + + +@app.post("/chat/confirm") +async def confirm_tool_call(req: Confirmation, request: Request): + # Answer the delete confirmation a running hook is awaiting; unknown means already gone. + future = request.app.state.pending_confirms.pop(req.request_id, None) + if future is None or future.done(): + raise HTTPException(status_code=404, detail="confirmation_not_found") + future.set_result(req.approved) + return {"approved": req.approved} + + +@app.post("/chat/stream", response_class=EventSourceResponse) +async def chat_stream(req: ChatRequest, request: Request) -> AsyncIterable[ServerSentEvent]: + state = request.app.state + lock: asyncio.Lock = state.session_locks[req.session_id] + # The run is pumped in a task so a hook can push frames (the confirmation request) because + # it runs inside the agent stream, where the response generator itself cannot yield. + queue: asyncio.Queue[ServerSentEvent | None] = asyncio.Queue() + + async def confirm_delete(arguments: dict[str, object]) -> bool: + task_id = str(arguments.get("task_id", "")) + request_id = uuid4().hex + future = asyncio.get_running_loop().create_future() + state.pending_confirms[request_id] = future + queue.put_nowait( + ServerSentEvent( + data={ + "request_id": request_id, + "task_id": task_id, + "title": (TASKS.get(task_id) or {}).get("title"), + }, + event="confirm", + ) + ) + try: + # Fail closed: no answer within two minutes, or a dropped client, means no delete. + return await asyncio.wait_for(future, timeout=120) + except TimeoutError: + return False + finally: + state.pending_confirms.pop(request_id, None) + + async def pump() -> None: + try: + async with lock, agent_run(state.agent, state.model, req, confirm_delete) as stream: + async for event in stream: + if event.kind == "text": + queue.put_nowait(ServerSentEvent(data={"delta": event.data["delta"]}, event="message")) + elif event.kind == "tool_call": + names = [call["name"] for call in event.data["tool_calls"]] + queue.put_nowait(ServerSentEvent(data={"names": names}, event="tool")) + + # The run has ended; usage is for the last model step only. + queue.put_nowait(ServerSentEvent(data={"last_step_usage": stream.usage}, event="done")) + except asyncio.CancelledError: + raise + except Exception: + logger.exception("Streaming agent run failed: session=%s", req.session_id) + queue.put_nowait(ServerSentEvent(data={"message": "Agent run failed"}, event="error")) + finally: + queue.put_nowait(None) + + task = asyncio.create_task(pump()) + try: + while (frame := await queue.get()) is not None: + yield frame + finally: + task.cancel() + with suppress(asyncio.CancelledError): + await task + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host=HOST, port=PORT) diff --git a/website/public/examples/bub-todo-agent/static/THIRD_PARTY_NOTICES.md b/website/public/examples/bub-todo-agent/static/THIRD_PARTY_NOTICES.md new file mode 100644 index 000000000..f3dd1438c --- /dev/null +++ b/website/public/examples/bub-todo-agent/static/THIRD_PARTY_NOTICES.md @@ -0,0 +1,60 @@ +# Third-party notices + +This example bundles the following unmodified browser runtimes. Their license texts are reproduced in full below, as the MIT License requires. + +| Bundled file | Package | Version | License | +| --- | --- | --- | --- | +| `uno.global.js` | [UnoCSS runtime](https://github.com/unocss/unocss) | 66.12.0 | MIT | +| `alpine.min.js` | [Alpine.js](https://github.com/alpinejs/alpine) | 3.17.5 | MIT | + +## UnoCSS runtime 66.12.0 + +Project: https://github.com/unocss/unocss + +MIT License + +Copyright (c) 2021-PRESENT Anthony Fu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +## Alpine.js 3.17.5 + +Project: https://github.com/alpinejs/alpine + +MIT License + +Copyright © 2019-2025 Caleb Porzio and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/website/public/examples/bub-todo-agent/static/alpine.min.js b/website/public/examples/bub-todo-agent/static/alpine.min.js new file mode 100644 index 000000000..1a46daae6 --- /dev/null +++ b/website/public/examples/bub-todo-agent/static/alpine.min.js @@ -0,0 +1,21 @@ +(()=>{var bt=!1,Et=!1,B=[],vt=-1,Fe=!1,wt=!1;function gr(e){ai(e)}function xr(){wt=!0}function yr(){wt=!1,br()}function ai(e){B.includes(e)||(B.push(e),e._x_schedulerPriority!==void 0&&(Fe=!0)),br()}function ue(e){let t=B.indexOf(e);t!==-1&&t>vt&&B.splice(t,1)}function br(){if(!Et&&!bt){if(wt)return;bt=!0,queueMicrotask(ci)}}function ci(){bt=!1,Et=!0;for(let e=0;efi(n,i,t));for(let n=0;ne.effect(t,{scheduler:r=>{St?gr(r):r()}}),At=e.raw}function Tt(e){P=e}function wr(e){let t=()=>{};return[(n,i)=>{let o=i?.priority==="structural"?ui++:void 0,s=P(n);return o!==void 0&&s!==void 0&&(s._x_schedulerPriority={el:e,order:o}),e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(a=>a())}),e._x_effects.add(s),t=()=>{s!==void 0&&(e._x_effects.delete(s),H(s))},s},()=>{t()}]}function Ve(e,t){let r=!0,n,i,o=P(()=>{let s=e(),a=JSON.stringify(s);if(!r&&(typeof s=="object"||s!==n)){let c=typeof n=="object"?JSON.parse(i):n;queueMicrotask(()=>{t(s,c)})}n=s,i=a,r=!1});return()=>{ue(o),H(o)}}async function Sr(e){xr();try{await e(),await Promise.resolve()}finally{yr()}}var Ar=[],Tr=[],Or=[];function Nr(e){Or.push(e)}function de(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,Tr.push(t))}function Be(e){Ar.push(e)}function He(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function Ot(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}function Cr(e){for(e._x_effects?.forEach(ue);e._x_cleanups?.length;)e._x_cleanups.pop()()}var Ue=new MutationObserver(qe),Nt=!1;function we(){Ue.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),Nt=!0}function Ct(){di(),Ue.disconnect(),Nt=!1}var Z=[];function di(){let e=Ue.takeRecords();Z.push(()=>e.length>0&&qe(e));let t=Z.length;queueMicrotask(()=>{if(Z.length===t)for(;Z.length>0;)Z.shift()()})}function Rr(){for(;Z.length>0;)Z.shift()();let e=Ue.takeRecords();e.length>0&&qe(e)}function h(e){if(!Nt)return e();Ct();try{return e()}finally{we()}}var Rt=!1,$e=[];function Dr(){Rt=!0}function Mr(){Rt=!1,qe($e),$e=[]}function qe(e){if(Rt){$e=$e.concat(e);return}let t=[],r=new Set,n=new Map,i=new Map;for(let o=0;o{s.nodeType===1&&s._x_marker&&r.add(s)}),e[o].addedNodes.forEach(s=>{if(s.nodeType===1){if(r.has(s)){r.delete(s);return}s._x_marker||t.push(s)}})),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},f=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(f(),l()):f()}i.forEach((o,s)=>{Ot(s,o)}),n.forEach((o,s)=>{Ar.forEach(a=>a(s,o))});for(let o of r)t.some(s=>s.contains(o))||Tr.forEach(s=>s(o));for(let o of t)o.isConnected&&Or.forEach(s=>s(o));t=null,r=null,n=null,i=null}function ze(e){return L(U(e))}function I(e,t,r){return e._x_dataStack=[t,...U(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function U(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?U(e.host):e.parentNode?U(e.parentNode):[]}function L(e){return new Proxy({objects:e},pi)}function kr(e,t){return e===null||e===Object.prototype?null:Object.prototype.hasOwnProperty.call(e,t)?e:kr(Object.getPrototypeOf(e),t)}var pi={ownKeys({objects:e}){return Array.from(new Set(e.flatMap(t=>Object.keys(t))))},has({objects:e},t){return t==Symbol.unscopables?!1:e.some(r=>Object.prototype.hasOwnProperty.call(r,t)||Reflect.has(r,t))},get({objects:e},t,r){return t=="toJSON"?hi:Reflect.get(e.find(n=>Reflect.has(n,t))||{},t,r)},set({objects:e},t,r,n){let i;for(let s of e)if(i=kr(s,t),i)break;i||(i=e[e.length-1]);let o=Object.getOwnPropertyDescriptor(i,t);return o?.set&&o?.get?o.set.call(n,r)||!0:Reflect.set(i,t,r)}};function hi(){return Reflect.ownKeys(this).reduce((t,r)=>(t[r]=Reflect.get(this,r),t),{})}function pe(e,t=()=>{}){let r=i=>typeof i=="object"&&!Array.isArray(i)&&i!==null,n=(i,o="")=>{Object.entries(Object.getOwnPropertyDescriptors(i)).forEach(([s,{value:a,enumerable:c}])=>{if(c===!1||a===void 0||typeof a=="object"&&a!==null&&a.__v_skip)return;let l=o===""?s:`${o}.${s}`;typeof a=="object"&&a!==null&&a._x_interceptor?i[s]=a.initialize(e,l,s,t):r(a)&&a!==i&&!(a instanceof Element)&&n(a,l)})};return n(e)}function Ke(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o,s){return e(this.initialValue,()=>mi(n,i),a=>Dt(n,i,a),i,o,s)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a,c)=>{let l=n.initialize(o,s,a,c);return r.initialValue=l,i(o,s,a,c)}}else r.initialValue=n;return r}}function mi(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function Dt(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),Dt(e[t[0]],t.slice(1),r)}}var Pr={};function b(e,t){Pr[e]=t}function G(e,t){let r=_i(t);return Object.entries(Pr).forEach(([n,i])=>{Object.defineProperty(e,`$${n}`,{get(){return i(t,r)},enumerable:!1})}),e}function _i(e){let[t,r]=Mt(e),n={interceptor:Ke,...t};return de(e,r),n}function Ir(e,t,r,...n){try{return r(...n)}catch(i){Y(i,e,t)}}function Y(...e){return Lr(...e)}var Lr=gi;function jr(e){Lr=e}function gi(e,t,r=void 0){e=Object.assign(e??{message:"No error message given."},{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message} + +${r?'Expression: "'+r+`" + +`:""}`,t),setTimeout(()=>{throw e},0)}var he=!0;function We(e){let t=he;he=!1;let r=e();return he=t,r}function M(e,t,r={}){let n;return x(e,t)(i=>n=i,r),n}function x(...e){return Fr(...e)}var Fr=()=>{};function Vr(e){Fr=e}var $r;function Br(e){$r=e}function Hr(e,t){let r={};G(r,e);let n=[r,...U(e)],i=typeof t=="function"?xi(n,t):bi(n,t,e);return Ir.bind(null,e,t,i)}function xi(e,t){return(r=()=>{},{scope:n={},params:i=[],context:o}={})=>{if(!he){Se(r,t,L([n,...e]),i);return}let s=t.apply(L([n,...e]),i);Se(r,s)}}var kt={};function yi(e,t){if(kt[e])return kt[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(async()=>{ ${e} })()`:e,o=(()=>{try{let s=new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`);return Object.defineProperty(s,"name",{value:`[Alpine] ${e}`}),s}catch(s){return Y(s,t,e),Promise.resolve()}})();return kt[e]=o,o}function bi(e,t,r){let n=yi(t,r);return(i=()=>{},{scope:o={},params:s=[],context:a}={})=>{n.result=void 0,n.finished=!1;let c=L([o,...e]);if(typeof n=="function"){let l=n.call(a,n,c).catch(f=>Y(f,r,t));n.finished?(Se(i,n.result,c,s,r),n.result=void 0):l.then(f=>{Se(i,f,c,s,r)}).catch(f=>Y(f,r,t)).finally(()=>n.result=void 0)}}}function Se(e,t,r,n,i){if(he&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>Se(e,s,r,n)).catch(s=>Y(s,i,t)):e(o)}else typeof t=="object"&&t instanceof Promise?t.then(o=>e(o)):e(t)}function Ur(...e){return $r(...e)}function qr(e,t,r={}){let n={};G(n,e);let i=[n,...U(e)],o=L([r.scope??{},...i]),s=r.params??[];if(t.includes("await")){let a=Object.getPrototypeOf(async function(){}).constructor,c=/^[\n\s]*if.*\(.*\)/.test(t.trim())||/^(let|const)\s/.test(t.trim())?`(async()=>{ ${t} })()`:t;return new a(["scope"],`with (scope) { let __result = ${c}; return __result }`).call(r.context,o)}else{let a=/^[\n\s]*if.*\(.*\)/.test(t.trim())||/^(let|const)\s/.test(t.trim())?`(()=>{ ${t} })()`:t,l=new Function(["scope"],`with (scope) { let __result = ${a}; return __result }`).call(r.context,o);return typeof l=="function"&&he?l.apply(o,s):l}}var Lt="x-";function N(e=""){return Lt+e}function zr(e){Lt=e}var Ge={};function p(e,t){return Ge[e]=t,{before(r){if(!Ge[r]){console.warn(String.raw`Cannot find directive \`${r}\`. \`${e}\` will use the default order of execution`);return}let n=Q.indexOf(r);Q.splice(n>=0?n:Q.indexOf("DEFAULT"),0,e)}}}function Kr(e){return Object.keys(Ge).includes(e)}function ee(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=jt(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(Yr((o,s)=>n[o]=s)).filter(Xr).map(vi(n,r)).sort(wi).map(o=>Ei(e,o))}function jt(e){return Array.from(e).map(Yr()).filter(t=>!Xr(t))}var Pt=!1,Ae=new Map,Wr=Symbol();function Gr(e){Pt=!0;let t=Symbol();Wr=t,Ae.set(t,[]);let r=()=>{for(;Ae.get(t).length;)Ae.get(t).shift()();Ae.delete(t)},n=()=>{Pt=!1,r()};e(r),n()}function Mt(e){let t=[],r=a=>t.push(a),[n,i]=wr(e);return t.push(i),[{Alpine:q,effect:n,cleanup:r,evaluateLater:x.bind(x,e),evaluate:M.bind(M,e)},()=>t.forEach(a=>a())]}function Ei(e,t){let r=()=>{},n=Ge[t.type]||r,[i,o]=Mt(e);He(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),Pt?Ae.get(Wr).push(n):n())};return s.runCleanups=o,s}var Ye=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),Je=e=>e;function Yr(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=Jr.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var Jr=[];function me(e){Jr.push(e)}function Xr({name:e}){return Zr().test(e)}var Zr=()=>new RegExp(`^${Lt}([^:^.]+)\\b`);function vi(e,t){return({name:r,value:n})=>{r===n&&(n="");let i=r.match(Zr()),o=r.match(/:([a-zA-Z0-9\-_:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var It="DEFAULT",Q=["ignore","ref","id","data","anchor","bind","init","for","model","modelable","transition","show","if",It,"teleport"];function wi(e,t){let r=Q.indexOf(e.type)===-1?It:e.type,n=Q.indexOf(t.type)===-1?It:t.type;return Q.indexOf(r)-Q.indexOf(n)}function j(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>j(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)j(n,t,!1),n=n.nextElementSibling}var C=!1;function w(e,t=()=>{}){return(...r)=>C?t(...r):e(...r)}function Qr(e){return(...t)=>C&&e(...t)}var en=[];function F(e){en.push(e)}function tn(e,t){en.forEach(r=>r(e,t)),C=!0,nn(()=>{S(t,(r,n)=>{n(r,()=>{})})}),C=!1}var Xe=!1;function rn(e,t){t._x_dataStack||(t._x_dataStack=e._x_dataStack),C=!0,Xe=!0,nn(()=>{Si(t)}),C=!1,Xe=!1}function Si(e){let t=!1;S(e,(n,i)=>{j(n,(o,s)=>{if(t&&on(o))return s();t=!0,i(o,s)})})}function nn(e){let t=P;Tt((r,n)=>{let i=t(r);return H(i),()=>{}}),e(),Tt(t)}var Ze=0;function sn(e,t){let r=e._x_deferInit;r||(r=e._x_deferInit={pending:0,ownsIgnore:!e._x_ignore,queuedAttributes:new Map},r.ownsIgnore&&(e._x_ignore=!0),Ze++),r.pending++,Promise.resolve(t).catch(n=>{try{Y(n,e)}catch(i){setTimeout(()=>{throw i},0)}}).then(()=>Ai(e,r))}function Ai(e,t){t.pending--,!(t.pending>0)&&(Rr(),!(t.pending>0)&&e._x_deferInit===t&&(delete e._x_deferInit,t.ownsIgnore&&delete e._x_ignore,Ze--,e.isConnected&&(Ti(t),S(e))))}function an(e,t){if(Ze===0)return!1;let r=A(e,n=>n._x_deferInit);return r?(cn(r._x_deferInit,e,t.map(({name:n})=>n)),!0):!1}function cn(e,t,r){let n=e.queuedAttributes.get(t);(!n||n.marker!==t._x_marker)&&(n={marker:t._x_marker,names:new Set},e.queuedAttributes.set(t,n)),r.forEach(i=>n.names.add(i))}function Ti(e){e.queuedAttributes.forEach((t,r)=>{if(!r.isConnected||!r._x_marker||r._x_marker!==t.marker)return;let n=A(r,o=>o._x_deferInit);if(n){cn(n._x_deferInit,r,Array.from(t.names));return}let i=Array.from(t.names).filter(o=>r.hasAttribute(o)).map(o=>({name:o,value:r.getAttribute(o)}));i.length!==0&&ee(r,i).forEach(o=>o())})}F((e,t)=>{Ze!==0&&(!e||e.nodeType!==1||!t||t.nodeType!==1||A(e,r=>r._x_deferInit)&&(t._x_ignore=!0))});function te(e,t,r={},n={}){return e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0,...n}))}function T(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var ln=!1;function fn(){ln&&T("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),ln=!0,document.body||T("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's `