跳转到内容

包

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。完整指南请看协议。