包
OpenBot 是一个 pnpm 工作区。每个包只做一件事。openbot 把各部分组装成服务器。其他库可以单独使用。
所有包都命名为 @openbot-build/<name>,版本为 0.1.0,并使用 effect@4.0.5。它们还没有发布到 npm。请在工作区中用 "workspace:*" 使用它们。
概览
| 包 | 用途 |
|---|---|
openbot |
服务器、它的插件和它的模型提供方。请看从代码中运行。 |
api |
带类型的 HTTP 契约。服务器、客户端和 API 参考都使用它。 |
client |
TypeScript 客户端。请看客户端。 |
core |
Agent 核心:Agent、Session、Turn、Timeline、存储、插件和技能。 |
protocol |
OpenBot App Protocol:发布、查找、连接 App,并为 App 建立索引。 |
protocol-test |
App Protocol 的公开测试服务。请看协议测试。 |
docs |
本文档网站。 |
下面的示例省略了模型。要运行它们,请提供一个 LanguageModel Layer,例如来自 openbot 中的某个模型提供方。
core
@openbot-build/core 负责运行 Agent。它有以下主要部分:
| 模块 | 作用 |
|---|---|
Agent |
Agent 定义:工具、插件、提供方和压缩。一个定义可以服务多个 Session。 |
SessionRuntime |
运行 Turn:execute、stream、start、steer、approve、interrupt 等。 |
SessionStore、Store |
保存 Session 及其 Timeline。 |
SessionStoreMemory |
把所有内容都放在内存中的存储,用于测试和脚本。 |
Timeline、Item、Turn、Session |
数据类型。 |
Plugin、PluginComposition、PluginState |
创建和组合插件,并为插件提供保存的状态。 |
Skill、SkillMap |
描述技能,并维护技能列表。 |
一个 Session(会话)是一条工作线。每次输入都会启动一个 Turn(轮次)。一个 Turn 可以包含多个模型步骤和工具调用。 Timeline(时间线)是 Session 保存下来的历史。每次改变都会生成一个新的 Timeline 版本,所以你可以读取较早的某个时间点,或者从那里分叉。
import { SessionRuntime, SessionStoreMemory, Store } from "@openbot-build/core"import { Effect } from "effect"import { Toolkit } from "effect/ai"
const program = Effect.scoped( Effect.gen(function* () { const definition = yield* Store.define({ stores: {}, schemaVersion: "timeline/1", }) const store = yield* SessionStoreMemory.make(definition) const runtime = yield* SessionRuntime.make({ store, agent: { toolkit: Toolkit.empty }, }) const view = yield* store.create({ metadata: { title: "Closures" } }) return yield* runtime.execute({ base: { session: view.session.id, revision: view.session.revision }, input: "Explain closures", }) }),)execute 等待 Turn 结束,然后返回保存后的 Session。stream 在 Turn 运行时提供事件。
每次改变都需要当前的 revision。这样可以防止两个写入方同时修改同一个 Session。
存储还可以在不使用模型的情况下分叉、重命名、导出和导入 Session:
const fork = yield * store.fork({ source: view.session.id, metadata: { title: "Another try" }, })const record = yield * store.export(fork.session.id)插件
插件包裹模型循环。用 Plugin.make 创建插件。它的构造函数只运行一次。它返回的中间件包裹每个 Turn 的事件流:
import { Plugin } from "@openbot-build/core"import { Effect, Stream } from "effect"
const logging = Plugin.make({ id: Plugin.Id.make("logging") })(() => Effect.succeed((next, options) => next(options).pipe( Stream.tap((event) => event._tag === "ModelStepStarted" ? Effect.log("A model step started") : Effect.void, ), ), ),)PluginComposition.merge(a, b) 组合两个插件。a 在 b 的外层运行。
插件还可以把自己的数据保存在 Session 或 Timeline 中。把 plugin.stores 传给 Store.define,然后使用 PluginState.make。
技能
Skill.Descriptor 描述一个技能。SkillMap.Catalog 是一个技能列表。当两个列表中有相同的技能 ID 时,后面的那个生效。
查找、加载和运行技能由你的程序负责。
openbot 中的插件
@openbot-build/openbot 包含 OpenBot 的所有插件。服务器在每个 Turn 中都使用 Agent、App、工作和记忆插件。工作插件为 Agent 提供待办列表。你也可以在自己的 SessionRuntime 中使用记忆插件和 Turn 限制插件。
记忆
记忆插件保存学到的记忆:Agent 在 Session 中学到、并在之后的 Turn 中使用的简短事实。
服务器用它实现记忆 API。请看 Memory。记忆 API 的 schema 在 api 中,名为 Memory 和 AgentMemory。
import { SessionStoreMemory, Store } from "@openbot-build/core"import { LanguageModelMemory, Memory, MemoryOperations, MemoryRules,} from "@openbot-build/openbot"import { Effect } from "effect"
const program = Effect.scoped( Effect.gen(function* () { const definition = yield* Store.define({ stores: Memory.stores, schemaVersion: "timeline/1", }) const store = yield* SessionStoreMemory.make(definition) const memory = MemoryOperations.make({ store, policy: MemoryRules.policy, model: LanguageModelMemory.model, }) return yield* memory.read(sessionId) }),)read 返回学到的条目。update 执行一次 Remember 或 Forget 编辑。consolidate 让模型从新的历史中学习新条目。
MemoryRules 和 LanguageModelMemory 是默认的规则和模型。你可以替换它们。
MemoryRules.policy 把记忆文本限制在 8000 个字符以内。要使用其他限制,请用 MemoryRules.make({ maxLength })。
MemoryPlugin.make 提供在每个 Turn 中使用记忆的插件。它把有效的条目加入指令中。一个 Turn 完成后,它会学习新条目。你需要给它两个在每个 Turn 中运行的 Effect:
settings返回{ use, generate }。use打开指令功能。generate打开学习功能。languageModel返回用来学习条目的模型。
这些值从哪里来,由宿主决定。把 plugin.stores 传给 Store.define,并用 PluginState.make 提供 plugin.store。
这里的 learner 是你之前构建好的 LanguageModel 服务:
const plugin = MemoryPlugin.make({ settings: Effect.succeed({ use: true, generate: true }), languageModel: Effect.succeed(learner),})Turn 限制
有三个插件可以限制每个 Turn。你可以只用其中一个,也可以用 PluginComposition.merge 把它们合在一起。每个插件只计算自己的 Turn。
模型步骤
TurnLimiter 在达到设定的模型步骤数后停止 Turn。第一次模型请求算作一步。
import { TurnLimiter } from "@openbot-build/openbot"
const runtime = yield * SessionRuntime.make({ store, agent: { plugin: TurnLimiter.make({ maxSteps: 4 }) }, })它会以以下两种错误之一失败:
| 错误 | 何时发生 |
|---|---|
InvalidMaxSteps |
maxSteps 不是正整数。 |
ModelStepLimitExceeded |
达到限制后,Turn 还需要再走一个模型步骤。它带有 maxSteps 和 history。 |
Token
TokenLimiter 在 Turn 用掉设定数量的 token 后停止它。token 是模型读或写的一小段文字。插件把每个模型步骤的输入和输出 token 加在一起。已经开始的模型步骤可以完成。当总数达到 maxTokens 时,下一个模型步骤会失败。
import { TokenLimiter } from "@openbot-build/openbot"
const plugin = TokenLimiter.make({ maxTokens: 100_000 })| 错误 | 何时发生 |
|---|---|
InvalidMaxTokens |
maxTokens 不是正整数。 |
TokenLimitExceeded |
达到限制后,Turn 还需要再走一个模型步骤。它带有 maxTokens、usedTokens 和 history。 |
时间
TimeLimiter 在设定的时间过后停止 Turn。maxDuration 可以是毫秒数,也可以是 "5 minutes" 这样的文字。时间一到,插件会马上停止 Turn。模型步骤或工具调用进行到一半时也会被停止。
import { TimeLimiter } from "@openbot-build/openbot"
const plugin = TimeLimiter.make({ maxDuration: "5 minutes" })| 错误 | 何时发生 |
|---|---|
InvalidMaxDuration |
maxDuration 不是一个有限的正时间。 |
TimeLimitExceeded |
Turn 运行的时间超过了限制。它带有 maxDuration 和 history。 |
模型提供方
@openbot-build/openbot 有三个模型提供方。每个提供方把一个模型选择变成一个 LanguageModel Layer。API 密钥由你提供。OpenBot 如何使用它们,请看模型。
OpenAiCompatProvider
import { Config, Model, Provider } from "@openbot-build/core"import { OpenAiCompatProvider } from "@openbot-build/openbot"import { Effect, Layer, Redacted } from "effect"import { LanguageModel } from "effect/ai"import { FetchHttpClient } from "effect/http"
const selection = Config.Selection.make({ provider: Provider.Id.make("openai-compat"), model: Model.Id.make("gpt-4.1"),})const model = Layer.unwrap( OpenAiCompatProvider.make({ max_output_tokens: 1024 }).model(selection),).pipe( Layer.provide( OpenAiCompatProvider.layerClient({ apiKey: Redacted.make(process.env.OPENAI_API_KEY ?? ""), apiUrl: "https://api.openai.com/v1", }), ), Layer.provide(FetchHttpClient.layer),)
const program = LanguageModel.generateText({ prompt: "Hello" }).pipe( Effect.provide(model),)省略 apiUrl 时,使用 https://api.openai.com/v1。模型选择中的 reasoningEffort 和 serviceTier 会替换默认值中的 reasoning_effort 和 service_tier。
OpenAiProvider
用法相同,只是换成 OpenAiProvider 和提供方 ID openai。它使用 Responses API。
它还有 ResponsesCompaction.layer(),用 Responses API 自带的压缩功能缩短很长的历史。
import { OpenAiProvider, ResponsesCompaction } from "@openbot-build/openbot"
const services = Layer.merge( Layer.unwrap(OpenAiProvider.make().model(selection)), ResponsesCompaction.layer(),).pipe( Layer.provide(OpenAiProvider.layerClient({ apiKey })), Layer.provide(FetchHttpClient.layer),)AnthropicProvider
用法相同,只是换成 AnthropicProvider 和提供方 ID anthropic。它使用 Messages API。
import { AnthropicProvider } from "@openbot-build/openbot"
const provider = AnthropicProvider.make({ max_tokens: 1024 })reasoningEffort 可以是 low、medium 或 high。serviceTier 可以是 auto 或 standard_only。其他值会在发出任何请求之前就失败。
protocol
@openbot-build/protocol 实现了 OpenBot App Protocol。发布者用它把 App 放到网上。宿主用它查找、连接和调用 App。
import { Document, McpPublisher } from "@openbot-build/protocol"import { Effect, Schema } from "effect"import { McpServer, Tool, Toolkit } from "effect/ai"
const tools = Toolkit.make( Tool.make("Hello", { parameters: Schema.Struct({ name: Schema.String }), success: Schema.Struct({ greeting: Schema.String }), }),)const handlers = tools.toLayer({ Hello: ({ name }) => Effect.succeed({ greeting: `Hello, ${name}` }),})const origin = "https://apps.example.com"
const publisher = McpPublisher.layer({ publication: { card: { $schema: Document.cardSchemaUrl, name: "com.example/hello", version: "1.0.0", description: "Say hello", remotes: [ { type: "streamable-http", url: origin + "/mcp", supportedProtocolVersions: [Document.protocolVersion], }, ], }, cardUrl: origin + "/mcp/server-card", catalogUrl: origin + "/.well-known/ai-catalog.json", mcpUrl: origin + "/mcp", allowedOrigins: ["https://host.example.com"], }, register: McpServer.registerToolkit(tools).pipe(Effect.provide(handlers)),})用 HttpRouter.serve 和一个 HTTP 服务器 Layer 来提供 publisher。完整指南请看协议。