feat(agent-runner): align protocol adapter terminology

This commit is contained in:
huanghuoguoguo
2026-05-24 09:13:15 +08:00
parent ea6c8fba57
commit 90dffa7cd8
26 changed files with 471 additions and 342 deletions

View File

@@ -2,6 +2,20 @@
本文档描述插件化 AgentRunner 场景下的上下文边界。结论先行LangBot 不应成为最终 agentic context managerLangBot 应提供 context substrateAgentRunner 或其背后的 agent runtime 自己决定如何管理历史、压缩、召回和 KV cache。
## 当前状态
**当前分支已落地**
-`AgentRunContext` — event-first context 模型
-`ContextAccess` — cursor、inline policy、available APIs
-`AgentRunAPIProxy.history` — page/search API
-`AgentRunAPIProxy.events` — get/page API
-`AgentRunAPIProxy.artifacts` — metadata/read_range API
-`AgentRunAPIProxy.state` — get/set/delete API
- ✅ EventLog / Transcript / ArtifactStore — host 事实源
- ✅ PersistentStateStore — 持久化状态存储
-`max-round` 已从协议实体中移除,只在 Pipeline adapter 中处理
## 1. 设计原则
### 1.1 Agent 拥有上下文策略
@@ -21,7 +35,7 @@
### 1.2 不再把 `max-round` 作为目标设计
`max-round` 是 Pipeline local-agent 时代的兼容配置。它可以在迁移期被读取并转换为某种默认 bootstrap policy但不应继续作为 AgentRunner 协议的核心概念。
Pipeline adapter 的 `max-round` 配置可以在运行时被读取并转换为某种默认 bootstrap policy但不应继续作为 AgentRunner 协议的核心概念。
新协议不应该问“LangBot 每轮裁几轮历史给 agent”而应该问
@@ -112,7 +126,7 @@ context:
- 自管 runtime`bootstrap: current_event`
- 简单 HTTP runner`bootstrap: recent_tail`
- 兼容旧 local-agent迁移期可以把旧 `max-round` 映射为 `recent_tail` 配置,但不再作为协议字段扩展。
- Pipeline adapter 的 `max-round` 映射为 `recent_tail` 配置,但不再作为协议字段扩展。
## 3. ContextAccess
@@ -296,13 +310,13 @@ LangBot core 不应内置官方 agent 的业务流程:
## 9. 当前实现需要调整
当前代码已有 `AgentContextPackager`,它按 legacy `max-round` 裁剪 `query.messages`。目标方向不是继续增强它,而是把它降级为兼容 adapter
**已完成(当前分支)**
- `max-round` 迁移为旧 binding 的 bootstrap 配置。
- 新 runner 默认不收到历史窗口
- `AgentRunContext` 增加 `context` / cursor / access capabilities
- `AgentRunAPIProxy` 增加 history / events / artifacts API
- Host 增加持久 EventLog / Transcript / ArtifactStore
- local-agent 插件再基于这些 API 决定是否拉历史、怎么压缩、怎么组 prompt。
- `max-round` 在 Pipeline adapter 中处理(不影响协议实体)
- 新 runner 默认不收到历史窗口
- `AgentRunContext` 增加 `context` / cursor / access capabilities
- `AgentRunAPIProxy` 增加 history / events / artifacts / state API
- Host 增加持久 EventLog / Transcript / ArtifactStore / PersistentStateStore
- `run_from_query()` 委托到 event-first `run(event, binding)`
这样 LangBot 既能服务依附 host 基础设施的官方 runner也能服务自带 memory/session/cache 的外部 agent runtime。

View File

@@ -1,5 +1,10 @@
# Event Based Agent 预留设计
> **注意**:本文档是 future design note不是当前分支实现范围。
>
> EventGateway、EventRouter、Event subscription/notification 由其他分支实现。
> 本分支只预留 event-first 入口和 envelope/binding models。
本文档描述未来 EBA 接入时,事件如何进入 LangBot、如何触发 AgentRunner以及如何复用插件化 agent 基础设施。
本阶段不实现完整 EventBus / EventRouter / Platform API。本阶段要做的是把协议边界设计对避免当前消息入口继续绑死 Pipeline 和用户文本消息。
@@ -188,22 +193,36 @@ EBA 事件进入 AgentRunner 时仍使用 [AGENT_CONTEXT_PROTOCOL.md](./AGENT_CO
## 10. 当前实现与目标差距
当前已有
**当前分支已落地Event-first 基础设施)**
- `AgentRunOrchestrator`
- `AgentRunContextBuilder`
- `AgentRunResult` 基础消息流
- `ctx.event` 的最小消息事件封装
- `AgentRunOrchestrator` — event-first `run(event, binding)` 入口
- `AgentRunContextBuilder` — event-first context 构建
- `AgentEventEnvelope` 模型
- `AgentBinding` 模型
-`AgentRunResult` 基础消息流
-`ctx.event` 的最小消息事件封装
-`PipelineAdapter` — Query → Event + Binding 转换
-`run_from_query()``run(event, binding)` 委托
- ✅ EventLog / Transcript / ArtifactStore
- ✅ History / Event / Artifact / State pull APIs
仍需要
**其他分支负责(非本分支范围)**
- `AgentEventEnvelope` 独立模型。
- EventLog 持久化。
- AgentBinding 持久模型。
- EventRouter。
- DeliveryContext。
- platform action permission model。
- `run_from_query()``run(event, binding)` 的迁移。
- EventGateway 实现
- EventRouter 实现
- Event subscription / notification
- EventLog 持久化管理 UI
- AgentBinding 持久化 UI
- 平台动作执行 (`action.requested` 执行器)
**未来 EBA 完整落地需要**
- EventGateway 完整实现
- EventRouter 与 BindingResolver 集成
- AgentBinding 持久模型和 UI
- DeliveryContext 完整实现
- platform action permission model 和执行器
- 真实平台事件接入
## 11. 落地顺序

View File

@@ -27,16 +27,17 @@ SDK 要提供稳定协议:
- 不要求官方 local-agent 的旧行为反向塑造 host 协议。
- 不在 host 中实现通用 agentic prompt assembler。
- 不强制 runner 使用 LangBot state / storageLangBot 只提供可选、受控的寄宿能力。
- **不实现 EventGateway**EventGateway 是 future integration point由外部 event branch 提供。本分支只定义 host-side envelope/binding models 和 `run(event, binding)` 入口。
## 3. 分层架构
目标结构:
```text
IM / WebUI / API / EventRouter
IM / WebUI / API / EventRouter (future)
|
v
Event Gateway
Event Gateway (future - external event branch)
|
v
AgentBindingResolver
@@ -47,7 +48,7 @@ AgentRunOrchestrator
|-- AgentResourceBuilder
|-- AgentContextBuilder
|-- AgentRunSessionRegistry
|-- AgentStateStore / Storage / EventLog / ArtifactStore
|-- PersistentStateStore / EventLogStore / TranscriptStore / ArtifactStore
v
Plugin Runtime / AgentRunner
|
@@ -58,13 +59,21 @@ AgentRunResult stream
Delivery / Renderer / Platform API
```
当前 Pipeline 只应接入在 `Event Gateway` 或兼容 adapter 位置。它可以继续产生 `message.received`,但不应继续拥有 runner 选择、上下文裁剪和业务 agent 执行的核心语义。
**当前状态**
- `PipelineAdapter` 作为当前 transition adapter将 Pipeline Query 转换为 `AgentEventEnvelope` + `AgentBinding`
- `run_from_query()` 内部委托到 `run(event, binding)`
- EventLog / Transcript / ArtifactStore / PersistentStateStore 已落地
- EventGateway 由外部 event branch 实现
当前 Pipeline 只应接入在 Pipeline adapter 位置。它可以继续产生 `message.received`,但不应继续拥有 runner 选择、上下文裁剪和业务 agent 执行的核心语义。
## 4. LangBot 侧能力
### 4.1 Event Gateway
### 4.1 Event GatewayFuture Integration Point
Event Gateway 负责把入口统一成 host event
> **注意**EventGateway 由外部 event branch 实现,不在本分支范围。本分支只预留 event-first 入口和 envelope/binding models。
Event Gateway 将负责把入口统一成 host event
- IM 平台消息。
- WebUI debug chat 消息。
@@ -90,11 +99,13 @@ class AgentEventEnvelope(BaseModel):
raw_ref: RawEventRef | None
```
**当前 transition source**`PipelineAdapter.query_to_event(query)` 从 Pipeline Query 生成 `AgentEventEnvelope`
原始平台 payload 可以存为 raw event 或 artifact ref不要把平台私有字段直接扩散到 AgentRunner 顶层协议。
### 4.2 Agent Binding
Agent binding 是什么事件调用哪个 runner、带什么绑定配置”的持久配置。它替代长期依赖 Pipeline runner config 的角色。
Agent binding 是什么事件调用哪个 runner、带什么绑定配置”的持久配置。它替代长期依赖 Pipeline runner config 的角色。
建议模型:
@@ -111,11 +122,13 @@ class AgentBinding(BaseModel):
enabled: bool
```
**当前 transition source**`PipelineAdapter.pipeline_config_to_binding(query, runner_id)` 从 Pipeline config 生成临时 `AgentBinding`
Pipeline 当前可以被迁移为一种 binding source
- Pipeline AI runner config -> `AgentBinding`
- Pipeline extension preference -> `resource_policy`
- Pipeline output settings -> `delivery_policy`
- Pipeline AI runner config -> `AgentBinding`
- Pipeline extension preference -> `resource_policy`
- Pipeline output settings -> `delivery_policy`
但新设计不应再把这些字段命名为 Pipeline 专属概念。
@@ -168,7 +181,7 @@ run(event, binding)
- state.updated 处理。
- delivery backpressure 和 telemetry。
`run_from_query()` 这类 API 可以保留为兼容 adapter但内部应转换成 event + binding 后走统一 `run()`
`run_from_query()` 这类 API 可以保留为 Pipeline adapter 入口,但内部应转换成 event + binding 后走统一 `run()`
### 4.5 Resource Authorization
@@ -335,30 +348,43 @@ Proxy 是 runner 访问 host 能力的唯一入口:
## 6. 当前实现与目标差距
已落地
**已落地(当前分支)**
- `AgentRunnerRegistry`
- `AgentRunOrchestrator`
- `AgentRunContextBuilder`
- `AgentResourceBuilder`
- `AgentRunSessionRegistry`
- `AgentRunAPIProxy` 基础模型 / 工具 / 知识库授权路径
- `AgentRunnerRegistry`
- `AgentRunOrchestrator` — event-first `run(event, binding)`
- `AgentRunContextBuilder` — event-first context
- `AgentResourceBuilder`
- `AgentRunSessionRegistry`
- `AgentRunAPIProxy` — model / tool / knowledge / history / event / artifact / state APIs
-`PipelineAdapter` — Query → Event + Binding
-`AgentBinding` 抽象
-`AgentEventEnvelope` 抽象
-`max-round` 从目标设计中移除,只在 Pipeline adapter 中处理
-`PersistentStateStore` — 持久化状态存储
-`EventLogStore` / `TranscriptStore` / `ArtifactStore`
- ✅ history / artifact / event 的受限拉取 API
需要调整
**其他分支负责(非本分支范围)**
- `pipeline_config` 语义抽象为 `AgentBinding`
- `Query` 输入抽象为 `AgentEventEnvelope`
- 把 legacy `max-round` 从目标设计中移除,只作为旧配置兼容处理。
- 把 state store 改为持久 host storage backend。
- 增加 EventLog / Transcript / ArtifactStore。
- 增加 history / artifact / event 的受限拉取 API。
- EventGateway 实现
- EventRouter 实现
- AgentBinding 持久化 UI
- platform API 动作执行
## 7. 落地顺序
1. 固化 README 路由和专题文档边界。
2. 在 Host 中抽象 `AgentBinding`,先由 Pipeline adapter 生成。
3. `AgentRunContextBuilder` 改为 event-first
4. 增加持久 transcript/event log 的最小存储模型
5. 扩展 `AgentRunAPIProxy` 的 history / artifact / state API
6. 将 Pipeline-only 字段逐步下沉到兼容 adapter
7. 再设计官方 local-agent 插件如何消费这些基础设施。
**已完成**
1. ✅ 固化 README 路由和专题文档边界
2. ✅ 在 Host 中抽象 `AgentBinding`,由 Pipeline adapter 生成
3. ✅ 将 `AgentRunContextBuilder` 改为 event-first
4. ✅ 增加持久 transcript/event log/artifact/state 存储模型
5. ✅ 扩展 `AgentRunAPIProxy` 的 history / artifact / state API。
6. ✅ 将 Pipeline-only 字段下沉到 Pipeline adapter。
7. ✅ 官方 runner 插件迁移完成7 个插件)。
**后续工作(其他分支)**
- EventGateway 实现
- EventRouter 与 BindingResolver 集成
- 平台动作执行器

View File

@@ -188,16 +188,16 @@ ctx.prompt + ctx.messages + [current_user_message_from_ctx.input]
现阶段不要优化裁剪算法,也不要把新的压缩或 token-budget 裁剪塞回 Pipeline stage。
插件化 AgentRunner 路径应跳过 Pipeline `msgtrun` 的破坏性截断,然后由
`AgentContextPackager` 在 AgentRunner 边界执行同一套 legacy max-round user-round 规则。
`AgentContextPackager` 在 AgentRunner 边界执行同一套 max-round user-round 规则。
当前 SDK v1 还没有顶层 context packaging 字段LangBot 先把本次 packaging
元数据放在 `ctx.runtime.metadata.context_packaging`。这是实际下发结果说明,不是 LangBot 侧的长期策略控制面。
后续 LiteLLM 接入后再把真实 context window、token 预算和摘要策略接到这个边界上。
### 3.4.1 Agentic context plan
本轮只落地 `AgentContextPackager``legacy_max_round` working window不改变旧裁剪算法
本轮只落地 `AgentContextPackager``max_round` working window不改变 user-round 选择规则
下面的 `ConversationStore` / `EventLog``ContextCompressor` 和 host history API 仍是设计预留。
目标是让 Pipeline 逐步退化为 legacy 入口,让 AgentRunner 层拥有上下文打包职责。
目标是让 Pipeline 逐步退化为入口 adapter,让 AgentRunner 层拥有上下文打包职责。
建议最终拆成四个 host-side 服务:
@@ -217,7 +217,7 @@ ContextCompressor
- 完整历史属于 LangBot host不属于插件实例。插件仍是 singleton/stateless。
- `ctx.messages` 是 working context window不是完整 conversation dump。
- 每轮不能全量复制/序列化完整历史给插件 runtime否则长会话会产生 O(n) 成本和跨进程 payload 膨胀。
- `max-round` user-round 规则可以先搬到 `AgentContextPackager`,作为 `legacy_max_round` 策略。
- `max-round` 的 user-round 规则可以先搬到 `AgentContextPackager`,作为 `max_round` adapter 策略。
- LiteLLM 接入后,`AgentContextPackager` 再读取模型 context window升级为 token budget 策略。
- `ContextCompressor` 生成的是派生 summary/checkpoint不能覆盖或删除 raw history。
- 重启恢复依赖持久化 store 和 summary checkpoint不依赖 `SessionManager` 里的进程内 conversation list。
@@ -231,7 +231,7 @@ context_packaging: ContextPackagingMetadata
建议语义:
- `context_request.mode`: AgentRunner manifest / binding config 请求的 `legacy_max_round``token_budget``summary_hybrid``external_session`
- `context_request.mode`: AgentRunner manifest / binding config 请求的 `max_round``token_budget``summary_hybrid``external_session`
- `context_request.budget`: 模型窗口、预留输出 token、工具/RAG 预算等偏好
- `context_packaging.policy`: Host 本次实际采用的打包策略
- `context_packaging.delivered_count`: 本次下发的历史消息数

View File

@@ -175,8 +175,7 @@ LangBot core 不应为了 local-agent 保留业务编排逻辑。local-agent 的
- `ctx.runtime.metadata.streaming_supported`:当前 adapter 是否能消费流式输出。
- 宿主代理 action模型、工具、知识库、rerank 调用必须通过 `run_id` 校验资源权限。
`max-round` 只能作为历史配置迁移输入。如果需要兼容旧 Pipeline 行为,可以把它转成
local-agent 插件自己的 bootstrap/history policy不要把它继续提升为 LangBot host 的目标协议。
`max-round` 可作为 Pipeline adapter 的历史配置输入。如需适配 Pipeline 行为,可以把 `max-round` 转成 local-agent 插件自己的 bootstrap/history policy不要把它提升为 LangBot host 的目标协议字段。
建议 local-agent manifest 使用 hybrid 或 self-managed context

View File

@@ -4,7 +4,7 @@
## 总体进度
**当前阶段**: Phase 3 已完成,Phase 4 预留/部分上下文字段已填充
**当前阶段**: Phase 3 已完成,Event-first 基础设施已完成
| Phase | 描述 | 状态 |
|-------|------|------|
@@ -12,7 +12,8 @@
| Phase 1 | 核心架构Registry、Orchestrator、上下文模型 | ✅ 完成 |
| Phase 2 | 权限、能力声明、资源注入 | ✅ 完成 |
| Phase 3 | 内置 runner 迁移到插件 | ✅ 完成7/7 |
| Phase 4 | EBA 事件支持 | 🔲 未开始已预留稳定事件名message event/actor/subject 上下文已预填充) |
| Phase 3.5 | Event-first 基础设施 | ✅ 完成 |
| Phase 4 | EBA 事件支持 | 🔲 未开始(已预留 event-first 入口EventGateway 由其他分支实现) |
---
@@ -31,21 +32,34 @@
| `LIST_AGENT_RUNNERS` action | ✅ | `runtime/io/handlers/control.py` |
| `RUN_AGENT` action | ✅ | `runtime/io/handlers/control.py` |
| `AgentRunAPIProxy` | ✅ | `api/proxies/agent_run_api.py` |
| Pull API handlers (State/History/Event/Artifact) | ✅ | `runtime/io/handlers/plugin.py` |
| `caller_plugin_identity` injection | ✅ | Pull API handlers inject caller identity |
### LangBot 侧
| 组件 | 状态 | 备注 |
|------|------|------|
| `AgentRunnerRegistry` | ✅ | `pkg/agent/runner/registry.py` |
| `AgentRunOrchestrator` | ✅ | `pkg/agent/runner/orchestrator.py` |
| `AgentRunOrchestrator` | ✅ | `pkg/agent/runner/orchestrator.py` - event-first `run(event, binding)` |
| `AgentRunnerDescriptor` | ✅ | `pkg/agent/runner/descriptor.py` |
| `AgentResourceBuilder` | ✅ | `pkg/agent/runner/resource_builder.py` |
| `AgentRunContextBuilder` | ✅ | `pkg/agent/runner/context_builder.py` |
| `AgentRunContextBuilder` | ✅ | `pkg/agent/runner/context_builder.py` - event-first context |
| `AgentResultNormalizer` | ✅ | `pkg/agent/runner/result_normalizer.py` |
| `ConfigMigration` | ✅ | `pkg/agent/runner/config_migration.py` |
| `PipelineAdapter` | ✅ | `pkg/agent/runner/pipeline_adapter.py` - Query → Event + Binding |
| `run_from_query()``run(event, binding)` | ✅ | Pipeline 路径委托到 event-first path |
| `ChatMessageHandler` 集成 | ✅ | 使用 orchestrator 替代 wrapper |
| `PipelineService` 集成 | ✅ | 从 registry 获取 runner metadata |
| Plugin connector | ✅ | `list_agent_runners()` / `run_agent()` |
| `EventLogStore` | ✅ | `pkg/agent/runner/event_log_store.py` |
| `TranscriptStore` | ✅ | `pkg/agent/runner/transcript_store.py` |
| `ArtifactStore` | ✅ | `pkg/agent/runner/artifact_store.py` |
| `PersistentStateStore` | ✅ | `pkg/agent/runner/persistent_state_store.py` |
| History / Event pull APIs | ✅ | Orchestrator + APIProxy |
| Artifact pull APIs | ✅ | Orchestrator + APIProxy |
| State pull APIs | ✅ | Orchestrator + APIProxy |
| `artifact.created` / `state.updated` handling | ✅ | Event-first handlers in orchestrator |
| Pipeline path host capability coverage | ✅ | EventLog/Transcript/ArtifactStore/PersistentStateStore |
### 官方插件
@@ -62,7 +76,31 @@
| `langflow-agent` | ✅ 已完成 | SSE 流式tweaks 配置支持 |
| `tbox-agent` | ✅ 已完成 | 蚂蚁百宝箱,多模态输入 |
**注意**: LangBot 内置的旧 runner`pkg/provider/runners/`)已标记为 legacy,文件顶部添加了 DEPRECATED 注释。
**注意**: LangBot 内置 runner`pkg/provider/runners/`)已停用,文件顶部添加了 DEPRECATED 注释。
---
## 未完成但仍属本分支收尾
以下项目属于本分支收尾工作:
- [ ] Smoke / manual validation
- [ ] Docs final QA
- [ ] 也许需要 minimal official runner adaptation如果当前分支需要
---
## 非本分支范围
以下能力由其他分支负责:
| 能力 | 负责分支 | 备注 |
|------|----------|------|
| EventGateway implementation | event branch | 完整事件网关、事件路由、持久化管理 |
| Event subscription / notification | event branch | 事件订阅、推送通知 |
| BindingResolver persistence UI | 其他模块 | 绑定配置的持久化 UI |
| Event router integration | event branch | 与 BindingResolver 集成 |
| Scheduler / background event source | 其他模块 | 定时任务、后台事件源 |
---
@@ -71,10 +109,14 @@
### 高优先级
- [x] 工具详情 API — SDK `GET_TOOL_DETAIL` action、`AgentRunAPIProxy.get_tool_detail()` 与 Host 侧授权校验已接通
- [x] Pipeline `run_from_query()``run(event, binding)` — 已完成
- [x] EventLog / Transcript / ArtifactStore / PersistentStateStore — 已完成
- [x] History / Event / Artifact / State pull APIs — 已完成
- [x] `caller_plugin_identity` 验证路径 — 已完成
### 低优先级 / 未来
- [ ] EBA 完整集成 — 稳定事件名与 message event/actor/subject 上下文已预留,完整事件路由与非消息事件仍待实现
- [ ] EBA 完整集成 — EventGateway、event subscription、event notification 由其他分支实现
- [ ] 平台 API 动作执行 — `action.requested` 结果类型存在但未执行
---
@@ -85,6 +127,7 @@
|------|------|
| 2026-05-10 | Phase 0 集成测试通过SDK v1 协议验证成功 |
| 2026-05-13 | Phase 3 完成:所有 7 个官方 runner 插件迁移完成 |
| 2026-05-23 | Phase 3.5 完成:`run_from_query()` 委托到 event-first `run(event, binding)`Pipeline path 获得 host capabilities |
---

View File

@@ -1,8 +1,20 @@
# LangBot AgentRunner Protocol v1
本文档定义 LangBot Host 与插件 SDK / Runtime / AgentRunner 之间的协议合同。它优先描述稳定接口应是什么”,不描述具体落地任务。
本文档定义 LangBot Host 与插件 SDK / Runtime / AgentRunner 之间的协议合同。它优先描述稳定接口应是什么”,不描述具体落地任务。
当前分支已有 Protocol v1 的早期实现,但仍带有 Query、Pipeline、`max-round` 等兼容语义。本文档定义的是目标 v1 合同,用于后续同步改造 LangBot 和 SDK。
## 当前状态
**Protocol v1 已在当前分支落地**
- ✅ SDK 定义 `AgentRunnerManifest``AgentRunContext``AgentRunResult``AgentRunAPIProxy`
- ✅ Runtime 支持 `LIST_AGENT_RUNNERS``RUN_AGENT`
- ✅ Host 支持 `run_id` session authorization
- ✅ Host 能从当前 Pipeline 入口生成 event-first context
-`messages` 降级为 optional bootstrap
-`max-round` 不出现在协议实体中(只在 Pipeline adapter 中处理)
- ✅ Proxy 覆盖 model、tool、knowledge、state/storage
- ✅ History / Event / Artifact / State API 已落地
- ✅ EventLog / Transcript / ArtifactStore / PersistentStateStore 已落地
## 1. 协议目标
@@ -168,7 +180,7 @@ class AgentRunContext(BaseModel):
runtime: AgentRuntimeContext
config: dict[str, Any] = {}
bootstrap: BootstrapContext | None = None
compatibility: CompatibilityContext | None = None
adapter: AdapterContext | None = None
metadata: dict[str, Any] = {}
```
@@ -177,7 +189,7 @@ class AgentRunContext(BaseModel):
- `event` 是必选字段Protocol v1 是 event-first。
- `input` 表示当前事件的主输入,不等于历史消息。
- `bootstrap` 是可选字段,不是完整 history。
- `compatibility` 只放旧 Query / Pipeline 迁移字段runner 不应依赖它做长期能力。
- `adapter` 只放 Pipeline adapter 字段runner 不应依赖它做长期能力。
- `config` 是 Host binding config不是插件实例状态。
### 4.3 AgentTrigger
@@ -185,7 +197,7 @@ class AgentRunContext(BaseModel):
```python
class AgentTrigger(BaseModel):
type: str
source: Literal["platform", "webui", "api", "scheduler", "system", "pipeline_compat"]
source: Literal["platform", "webui", "api", "scheduler", "system", "pipeline_adapter"]
timestamp: int | None = None
```
@@ -194,7 +206,7 @@ class AgentTrigger(BaseModel):
```json
{
"type": "message.received",
"source": "pipeline_compat"
"source": "pipeline_adapter"
}
```
@@ -327,8 +339,8 @@ class BootstrapContext(BaseModel):
- `bootstrap.messages` 是 host convenience不是协议核心。
- 自管 context runner 默认应收到空 bootstrap 或只收到当前 event。
- Host 不应为了帮 agent 更聪明”而自动拼接完整 transcript。
- `max-round` 只能影响兼容 adapter 如何生成 `bootstrap.messages`,不能成为 Protocol v1 字段。
- Host 不应为了帮 agent 更聪明”而自动拼接完整 transcript。
- Pipeline adapter 的 `max-round` 配置只影响 adapter 如何生成 `bootstrap.messages`,不能成为 Protocol v1 字段。
### 4.10 RuntimeContext
@@ -628,32 +640,41 @@ Host 不负责业务编排:
这些能力可以由官方或第三方 AgentRunner 插件实现,并通过公开 Host APIs 消费 LangBot 的状态、历史、存储、artifact、模型、工具和知识库能力。
## 11. Pipeline 兼容
## 11. Pipeline Adapter
Pipeline 是兼容入口,不是协议中心。
Pipeline 是当前入口 adapter,不是协议中心。
兼容 adapter 应负责
**当前分支已实现**
-`PipelineAdapter.query_to_event(query)` — 从 `Query` 构造 `AgentEventEnvelope`
-`PipelineAdapter.pipeline_config_to_binding(query, runner_id)` — 从 Pipeline config 构造临时 AgentBinding
-`run_from_query()` 委托到 `run(event, binding)`
-`max-round` 在 Pipeline adapter 中处理,不进入协议实体
- ✅ Query-only 字段放入 `adapter` context
Pipeline adapter 负责:
-`Query` 构造 `AgentEventContext`
- 从 Pipeline config 构造临时 AgentBinding。
- 从旧 runner config 构造 `ctx.config`
- `max-round` 转换为 `bootstrap` policy。
- Query-only 字段放入 `compatibility`
-`max-round` 转换为 `bootstrap` policy。
- 将 Query-only 字段放入 `adapter`
Runner 不应长期依赖 `compatibility`。新 runner 应只依赖 event-first context 和 Host APIs。
Runner 不应长期依赖 `adapter`。新 runner 应只依赖 event-first context 和 Host APIs。
## 12. 最小 v1 完成标准
Protocol v1 可认为稳定,至少需要
Protocol v1 已在当前分支完成
- SDK 定义 `AgentRunnerManifest``AgentRunContext``AgentRunResult``AgentRunAPIProxy`
- Runtime 支持 `LIST_AGENT_RUNNERS``RUN_AGENT`
- Host 支持 `run_id` session authorization
- Host 能从当前 Pipeline 入口生成 event-first context
- `messages` 降级为 optional bootstrap
- `max-round` 不出现在协议实体中
- Proxy 至少覆盖 model、tool、knowledge、state/storage
- History / event / artifact API 的方法签名确定,即使实现可以分阶段落地
- SDK 定义 `AgentRunnerManifest``AgentRunContext``AgentRunResult``AgentRunAPIProxy`
- Runtime 支持 `LIST_AGENT_RUNNERS``RUN_AGENT`
- Host 支持 `run_id` session authorization
- Host 能从当前 Pipeline 入口生成 event-first context
- `messages` 降级为 optional bootstrap
- `max-round` 不出现在协议实体中
- Proxy 至少覆盖 model、tool、knowledge、state/storage
- History / event / artifact API 落地
- ✅ EventLog / Transcript / ArtifactStore / PersistentStateStore 已落地
## 13. 开放问题

View File

@@ -2,24 +2,55 @@
本文档是 agent-runner 插件化工作的路由页。具体设计拆到独立文档中维护,避免把 LangBot 宿主架构、SDK 协议、上下文管理、EBA 预留和官方 runner 迁移混在同一份 README 里。
## 核心方向
## 本分支目标
LangBot 的目标不是把旧 Pipeline runner 机制简单搬进插件系统,而是逐步转为一个面向 Agent 的宿主层:
**本分支目标AgentRunner 外化 / 插件化基础设施**
- LangBot 负责 IM / WebUI / API / 未来事件入口、会话和身份解析、权限、存储、资源授权、运行生命周期、结果投递和审计。
- SDK 负责定义 AgentRunner 组件协议、上下文实体、返回事件流、运行期受限 API 和插件 runtime 协作方式。
- AgentRunner 负责具体 agent runtime 的上下文策略、prompt 组装、压缩、召回、模型调用策略和业务行为。
本分支只做 LangBot 作为 Agent Host 的基础能力建设:
后续会逐步弱化 Pipeline。当前 Pipeline 只能视为现有消息入口和兼容层,不应作为新架构设计的中心假设。
- LangBot 与 SDK 的稳定协议合同Protocol v1
- Host-side `AgentEventEnvelope` / `AgentBinding` 模型
- `run(event, binding)` event-first 入口
- `PipelineAdapter`Pipeline Query → AgentEventEnvelope + AgentBinding
- EventLog / Transcript / ArtifactStore / PersistentStateStore
- History / Event / Artifact / State pull APIs
- SDK runtime forwarding pull APIs + `caller_plugin_identity` 验证路径
## 本分支不实现
以下能力由其他分支负责,本分支只预留 integration point
- **EventGateway**:完整事件网关实现、事件路由、事件持久化管理
- **Event subscription / Event notification**:事件订阅、推送通知
- **BindingResolver persistence UI**:绑定配置的持久化 UI 和 event router 集成(如由其他模块负责)
- **Scheduler / Background event source**:定时任务、后台事件源
EventGateway 在本文档中描述为 **future integration point**,由外部 event branch 提供。本分支只定义 host-side envelope/binding models 和 `run(event, binding)` orchestrator 入口。
## 当前状态
**当前 Pipeline 是 transition adapter不再是 agent runner 设计核心。**
当前主入口仍可由 Pipeline 触发,但内部已转换成 event-first path
1. `run_from_query()` 使用 `PipelineAdapter.query_to_event(query)` 转换为 `AgentEventEnvelope`
2. `run_from_query()` 使用 `PipelineAdapter.pipeline_config_to_binding(query, runner_id)` 转换为 `AgentBinding`
3. `run_from_query()` 委托到 `run(event, binding, bound_plugins, adapter_context)`
Pipeline path 已获得 event-first host capabilities
- EventLog / Transcript 写入
- ArtifactStore 注册
- PersistentStateStore 状态持久化
- History / Event / Artifact / State pull APIs 可用
## 设计文档
| 文档 | 关注点 |
| --- | --- |
| [PROTOCOL_V1.md](./PROTOCOL_V1.md) | LangBot Host 与 SDK / Runtime / AgentRunner 的协议合同:discovery、run context、result stream、proxy actions、错误和兼容边界。 |
| [PROTOCOL_V1.md](./PROTOCOL_V1.md) | LangBot Host 与 SDK / Runtime / AgentRunner 的协议合同run context、result stream、proxy actions、错误和 adapter 边界。 |
| [HOST_SDK_INFRASTRUCTURE.md](./HOST_SDK_INFRASTRUCTURE.md) | LangBot 宿主能力、SDK 协议、runner 发现、绑定、权限、状态、存储、生命周期和调用链。 |
| [AGENT_CONTEXT_PROTOCOL.md](./AGENT_CONTEXT_PROTOCOL.md) | Agent-owned context 方向:事件到来时 LangBot 传什么agent 如何按需拉取更多历史 / artifact / state以及如何支持 KV cache 友好的上下文管理。 |
| [EVENT_BASED_AGENT.md](./EVENT_BASED_AGENT.md) | EBA 预留:事件模型、事件来源、触发绑定、非消息事件如何复用 AgentRunner 调度。 |
| [EVENT_BASED_AGENT.md](./EVENT_BASED_AGENT.md) | EBA 预留:事件模型、事件来源、触发绑定、非消息事件如何复用 AgentRunner 调度。**标注为 future design note**。 |
| [OFFICIAL_RUNNER_PLUGINS.md](./OFFICIAL_RUNNER_PLUGINS.md) | 官方 runner 插件迁移,包括 local-agent 和外部 runner。它是下游落地计划不是 LangBot 基础能力设计的前置约束。 |
| [PHASE1_QA_ACCEPTANCE_MATRIX.md](./PHASE1_QA_ACCEPTANCE_MATRIX.md) | 当前阶段的 QA 验收矩阵。它验证现有分支的兼容性,不代表最终架构边界。 |
@@ -45,15 +76,19 @@ LangBot 的目标不是把旧 Pipeline runner 机制简单搬进插件系统,
LangBot 不应成为最终 agentic context manager。它应提供事实源、默认上下文引用和按需读取 APIagent 或其背后的 runtime 负责历史剪裁、摘要、召回和 KV cache 策略。
当前代码中的 legacy `max-round` 只能视为旧 Pipeline 兼容行为,不应作为目标协议继续扩展。
当前代码中的 `max-round` Pipeline adapter 配置,不应作为目标协议继续扩展。
详见 [AGENT_CONTEXT_PROTOCOL.md](./AGENT_CONTEXT_PROTOCOL.md)。
### 3. Event Based Agent
### 3. Event Based AgentFuture
消息只是事件的一种。后续 `message.received``message.recalled``group.member_joined``friend.request_received` 等事件都应能通过统一事件 envelope 触发 AgentRunner。
EBA 设计要复用同一套 runner registry、resource authorization、session registry、state 更新、result normalization 和 delivery lifecycle不能另起一套调用协议。
**本分支不实现 EBA 完整能力,只预留:**
- event-first envelope (`AgentEventEnvelope`)
- AgentBinding model
- `run(event, binding)` 入口
- PipelineAdapter当前 AgentEventEnvelope / AgentBinding 的 Pipeline adapter source
详见 [EVENT_BASED_AGENT.md](./EVENT_BASED_AGENT.md)。
@@ -65,24 +100,6 @@ EBA 设计要复用同一套 runner registry、resource authorization、session
详见 [OFFICIAL_RUNNER_PLUGINS.md](./OFFICIAL_RUNNER_PLUGINS.md)。
## 当前实现状态
当前分支已经具备一部分基础设施:
- LangBot 已有 `AgentRunnerRegistry``AgentRunOrchestrator``AgentRunContextBuilder``AgentResourceBuilder``AgentResultNormalizer`
- `ChatMessageHandler` 主路径已经委托 orchestrator。
- Pipeline metadata 已经能从 registry 动态生成 runner 选项和配置 stage。
- SDK 已有 Protocol v1 的 `AgentRunContext``AgentRunResult`、capabilities、permissions、`AgentRunAPIProxy`
- 宿主侧已有 `run_id` session registry用于模型、工具、知识库、storage 等 runtime action 的授权校验。
仍需要从当前实现中继续剥离的部分:
- Pipeline 绑定仍是当前主要入口,后续需要抽象为通用 `AgentBinding`
- `AgentRunContext` 仍带有旧 Query / Pipeline 语义,需要迁移到 event-first envelope。
- context packaging 仍受 legacy `max-round` 影响,后续应改为 context reference + pull API。
- state store 当前是进程内实现,需要明确 host storage backend。
- artifact / transcript / event log 还没有成为完整宿主能力。
## 已确认决策
- 一个插件可以声明多个 `AgentRunner` 组件,每个组件独立暴露 manifest、配置 schema、能力和权限。
@@ -90,4 +107,5 @@ EBA 设计要复用同一套 runner registry、resource authorization、session
- 绑定只保存 runner id 和绑定配置,不代表插件实例状态。
- LangBot 可以提供 host-owned state / storage 能力,让 runner 把状态寄宿在 LangBot但这应该是授权能力不是强制要求。
- 官方 runner 插件是协议消费者,不是协议设计的优先约束。
- Pipeline 是当前兼容入口,不是未来架构中心。
- Pipeline 是当前入口 adapter,不是未来架构中心。
- EventGateway 是 future integration point由外部 event branch 提供。

View File

@@ -16,7 +16,6 @@ from .state_store import get_state_store
from .persistent_state_store import get_persistent_state_store
from . import events as runner_events
from .host_models import AgentEventEnvelope, AgentBinding
from .pipeline_compat_adapter import PipelineCompatAdapter
DEFAULT_RUNNER_TIMEOUT_SECONDS = 300
@@ -139,7 +138,7 @@ class AgentRunContextPayload(typing.TypedDict):
runtime: AgentRuntimeContext
config: dict[str, typing.Any] # Binding config from ai.runner_config[runner_id]
bootstrap: dict[str, typing.Any] | None # Optional bootstrap context
compatibility: dict[str, typing.Any] | None # Legacy compatibility context
adapter: dict[str, typing.Any] | None # Pipeline adapter context
metadata: dict[str, typing.Any] # Additional metadata
@@ -148,7 +147,7 @@ class AgentRunContextBuilder:
Two entry points:
- build_context_from_event(event, binding): Event-first Protocol v1
- build_context(query, descriptor, resources): Legacy Query-based (calls event-based internally)
- build_context(query, descriptor, resources): Pipeline adapter Query-based entry
Responsibilities:
- Generate new run_id (UUID, not query id)
@@ -212,14 +211,14 @@ class AgentRunContextBuilder:
conversation: ConversationContext | None = None
if event.conversation_id:
conversation = {
'session_id': None, # Legacy field
'session_id': None, # Pipeline adapter field
'conversation_id': event.conversation_id,
'thread_id': event.thread_id,
'launcher_type': None, # Will be filled from actor/subject if needed
'launcher_id': None,
'sender_id': event.actor.actor_id if event.actor else None,
'bot_uuid': event.bot_id,
'pipeline_uuid': binding.pipeline_uuid, # Legacy
'pipeline_uuid': binding.pipeline_uuid, # Pipeline adapter field
}
# Build event context (Protocol v1 event-first)
@@ -293,12 +292,12 @@ class AgentRunContextBuilder:
'platform_capabilities': event.delivery.platform_capabilities,
}
# Build compatibility context (empty for event-first)
compatibility_context = {
# Build adapter context (empty for event-first)
adapter_context = {
'query_id': None,
'pipeline_uuid': binding.pipeline_uuid,
'max_round': binding.max_round, # For reference only
'legacy_messages': [],
'adapter_messages': [],
'extra': {},
}
@@ -318,7 +317,7 @@ class AgentRunContextBuilder:
'runtime': runtime,
'config': binding.runner_config,
'bootstrap': None, # Optional - no messages inlined by default
'compatibility': compatibility_context,
'adapter': adapter_context,
'metadata': {}, # Additional metadata
}
@@ -332,12 +331,11 @@ class AgentRunContextBuilder:
) -> AgentRunContextPayload:
"""Build AgentRunContext envelope from Query.
This is a compatibility wrapper that converts Query to event + binding
This is a Pipeline adapter wrapper that converts Query to event + binding
and delegates to build_context_from_event().
For Protocol v1, messages are NOT inlined by default.
Legacy max-round only affects bootstrap (via compatibility adapter),
NOT Protocol v1 entities.
Pipeline max-round only affects bootstrap, NOT Protocol v1 entities.
Args:
query: Pipeline query
@@ -354,7 +352,7 @@ class AgentRunContextBuilder:
runner_id,
)
# Extract max_round for compatibility (NOT Protocol v1)
# Extract max_round for Pipeline adapter bootstrap (NOT Protocol v1)
# Note: config uses 'max-round' with hyphen, not 'max_round'
max_round = runner_config.get('max-round')
if max_round is None:
@@ -413,7 +411,7 @@ class AgentRunContextBuilder:
# Build delivery context from query adapter capabilities
delivery_context = {
'surface': 'pipeline', # Legacy pipeline surface
'surface': 'pipeline',
'reply_target': None,
'supports_streaming': streaming_supported,
'supports_edit': False,
@@ -422,8 +420,8 @@ class AgentRunContextBuilder:
'platform_capabilities': {},
}
# Build context access (for legacy, minimal API availability)
# Legacy Query-based mode does NOT have persistent state API
# Build context access for the direct Query adapter helper.
# The event-first run_from_query path uses build_context_from_event().
context_access = {
'conversation_id': conversation.get('conversation_id') if conversation else None,
'thread_id': None,
@@ -436,7 +434,7 @@ class AgentRunContextBuilder:
'delivered_count': 0,
'source_total_count': None,
'messages_complete': False,
'reason': 'legacy_pipeline',
'reason': 'pipeline_adapter',
},
'available_apis': {
'history_page': False,
@@ -445,40 +443,40 @@ class AgentRunContextBuilder:
'event_page': False,
'artifact_metadata': False,
'artifact_read': False,
'state': False, # Legacy Query mode does not have persistent state API
'state': False,
'storage': True,
},
}
# Build compatibility context (for legacy Query/Pipeline fields)
compatibility_context = {
# Build adapter context (for Pipeline adapter fields)
adapter_context = {
'query_id': query.query_id,
'pipeline_uuid': getattr(query, 'pipeline_uuid', None),
'max_round': max_round, # For reference only
'legacy_messages': [], # Will be filled if max_round is set
'adapter_messages': [], # Will be filled if max_round is set
'extra': {
'params': params, # Put params in compatibility.extra
'prompt': self._build_prompt(query), # Put prompt in compatibility.extra
'params': params, # Put params in adapter.extra
'prompt': self._build_prompt(query), # Put prompt in adapter.extra
},
}
# Build bootstrap context (optional, for legacy max-round)
# Build bootstrap context (optional, for Pipeline adapter max-round)
bootstrap_context = None
# For legacy compatibility: add bootstrap messages if max_round is set
# For Pipeline adapter: add bootstrap messages if max_round is set
# This goes into bootstrap.messages, NOT top-level messages
if max_round and max_round > 0:
packaged_context = self.context_packager.package_messages(query, runner_config)
legacy_messages = self._build_messages(packaged_context.messages)
adapter_messages = self._build_messages(packaged_context.messages)
# Put in bootstrap for Protocol v1
bootstrap_context = {
'messages': legacy_messages,
'messages': adapter_messages,
'summary': None,
'artifacts': [],
'metadata': {},
}
# Also update compatibility for legacy runners
compatibility_context['legacy_messages'] = legacy_messages
# Also update adapter for transition runners
adapter_context['adapter_messages'] = adapter_messages
# Update runtime metadata
runtime['metadata']['context_packaging'] = {
'policy': packaged_context.policy,
@@ -501,7 +499,7 @@ class AgentRunContextBuilder:
'runtime': runtime,
'config': runner_config,
'bootstrap': bootstrap_context, # Optional bootstrap
'compatibility': compatibility_context, # Legacy compatibility
'adapter': adapter_context, # Pipeline adapter context
'metadata': {}, # Additional metadata
}
@@ -902,7 +900,7 @@ class AgentRunContextBuilder:
artifact_read_enabled = 'read' in artifact_permissions
# Determine state API availability based on binding state_policy (event-first mode)
# For legacy Query-based mode, state is NOT available (no persistent state API)
# Direct Query context builder does not expose persistent state API.
state_enabled = False
if binding is not None:
state_policy = binding.state_policy

View File

@@ -7,7 +7,7 @@ import typing
from langbot_plugin.api.entities.builtin.pipeline import query as pipeline_query
DEFAULT_LEGACY_MAX_ROUND = 10
DEFAULT_MAX_ROUND = 10
@dataclasses.dataclass(frozen=True)
@@ -19,21 +19,16 @@ class ContextPackagingResult:
history: dict[str, typing.Any]
def get_legacy_max_round(runner_config: dict[str, typing.Any]) -> typing.Any:
"""Return the configured legacy max-round value.
Keep the existing config semantics intact: callers are expected to pass the
already-resolved runner binding config, and invalid values fail the same way
the old truncator failed when comparing them with an integer round count.
"""
return runner_config.get('max-round', DEFAULT_LEGACY_MAX_ROUND)
def get_max_round(runner_config: dict[str, typing.Any]) -> typing.Any:
"""Return the configured Pipeline adapter max-round value."""
return runner_config.get('max-round', DEFAULT_MAX_ROUND)
def select_legacy_max_round_messages(
def select_max_round_messages(
messages: list[typing.Any] | None,
max_round: typing.Any,
) -> list[typing.Any]:
"""Select the same message window as the legacy round truncator."""
"""Select a bounded recent message window by user-round count."""
if not messages:
return []
@@ -59,15 +54,15 @@ class AgentContextPackager:
query: pipeline_query.Query,
runner_config: dict[str, typing.Any],
) -> ContextPackagingResult:
"""Package query messages using the current legacy max-round policy."""
"""Package query messages using the Pipeline adapter max-round policy."""
source_messages = query.messages or []
max_round = get_legacy_max_round(runner_config)
packaged_messages = select_legacy_max_round_messages(source_messages, max_round)
max_round = get_max_round(runner_config)
packaged_messages = select_max_round_messages(source_messages, max_round)
return ContextPackagingResult(
messages=packaged_messages,
policy={
'mode': 'legacy_max_round',
'mode': 'max_round',
'max_round': max_round,
},
history={

View File

@@ -163,9 +163,9 @@ class AgentBinding(pydantic.BaseModel):
enabled: bool = True
"""Whether binding is enabled."""
# Legacy fields for compatibility adapter
# Fields for Pipeline adapter
pipeline_uuid: str | None = None
"""Legacy pipeline UUID (for compatibility)."""
"""Pipeline UUID (for Pipeline adapter)."""
max_round: int | None = None
"""Legacy max-round (for compatibility adapter, not Protocol v1)."""
"""max-round (for Pipeline adapter bootstrap, not Protocol v1)."""

View File

@@ -49,8 +49,7 @@ def parse_runner_id(runner_id: str) -> RunnerIdParts:
runner_name=runner_name,
)
else:
# For backward compatibility with old built-in runner names
# This should eventually be removed after migration
# Only plugin runner IDs are valid at the protocol boundary.
raise ValueError(
f'Invalid runner ID format: {runner_id}. '
f'Expected: plugin:author/plugin_name/runner_name'
@@ -89,4 +88,4 @@ def is_plugin_runner_id(runner_id: str) -> bool:
Returns:
True if runner ID starts with 'plugin:'
"""
return runner_id.startswith('plugin:')
return runner_id.startswith('plugin:')

View File

@@ -21,7 +21,7 @@ from .persistent_state_store import get_persistent_state_store, PersistentStateS
from .session_registry import get_session_registry, AgentRunSessionRegistry
from .config_migration import ConfigMigration
from .host_models import AgentEventEnvelope, AgentBinding
from .pipeline_compat_adapter import PipelineCompatAdapter
from .pipeline_adapter import PipelineAdapter
from .errors import (
RunnerNotFoundError,
RunnerExecutionError,
@@ -48,7 +48,7 @@ class AgentRunOrchestrator:
Entry points:
- run(event, binding): Main entry for event-first Protocol v1
- run_from_query(query): Compatibility wrapper for Pipeline
- run_from_query(query): Pipeline adapter wrapper
"""
ap: app.Application
@@ -86,7 +86,7 @@ class AgentRunOrchestrator:
event: AgentEventEnvelope,
binding: AgentBinding,
bound_plugins: list[str] | None = None,
compatibility_context: dict[str, typing.Any] | None = None,
adapter_context: dict[str, typing.Any] | None = None,
) -> typing.AsyncGenerator[provider_message.Message | provider_message.MessageChunk, None]:
"""Run agent runner from event-first envelope.
@@ -97,7 +97,7 @@ class AgentRunOrchestrator:
event: Event envelope from event gateway
binding: Agent binding configuration
bound_plugins: Optional list of bound plugin identities for authorization
compatibility_context: Optional compatibility context from Pipeline adapter
adapter_context: Optional adapter context from Pipeline adapter
Yields:
Message or MessageChunk for pipeline response
@@ -127,27 +127,27 @@ class AgentRunOrchestrator:
resources=resources,
)
# Merge compatibility context if provided (for Pipeline compatibility)
if compatibility_context:
# Merge params into compatibility.extra
if 'params' in compatibility_context:
context['compatibility']['extra']['params'] = compatibility_context['params']
# Merge prompt into compatibility.extra (for legacy runners)
if 'prompt' in compatibility_context:
context['compatibility']['extra']['prompt'] = compatibility_context['prompt']
# Merge adapter context if provided (for Pipeline adapter)
if adapter_context:
# Merge params into adapter.extra
if 'params' in adapter_context:
context['adapter']['extra']['params'] = adapter_context['params']
# Merge prompt into adapter.extra (for transition runners)
if 'prompt' in adapter_context:
context['adapter']['extra']['prompt'] = adapter_context['prompt']
# Merge bootstrap if provided
if compatibility_context.get('bootstrap'):
context['bootstrap'] = compatibility_context['bootstrap']
# Also set legacy_messages for legacy runners
bootstrap_messages = compatibility_context['bootstrap'].get('messages')
if adapter_context.get('bootstrap'):
context['bootstrap'] = adapter_context['bootstrap']
# Also set adapter_messages for transition runners
bootstrap_messages = adapter_context['bootstrap'].get('messages')
if bootstrap_messages:
context['compatibility']['legacy_messages'] = bootstrap_messages
context['adapter']['adapter_messages'] = bootstrap_messages
# Merge runtime metadata if provided
if compatibility_context.get('runtime_metadata'):
context['runtime']['metadata'].update(compatibility_context['runtime_metadata'])
if adapter_context.get('runtime_metadata'):
context['runtime']['metadata'].update(adapter_context['runtime_metadata'])
# Set query_id if provided
if compatibility_context.get('query_id'):
context['runtime']['query_id'] = compatibility_context['query_id']
if adapter_context.get('query_id'):
context['runtime']['query_id'] = adapter_context['query_id']
# Build state context for State API handlers
state_context = self._build_state_context(event, binding, descriptor)
@@ -243,7 +243,7 @@ class AgentRunOrchestrator:
) -> typing.AsyncGenerator[provider_message.Message | provider_message.MessageChunk, None]:
"""Run agent runner from pipeline query.
This is a compatibility wrapper for the legacy Query-based flow.
This is the Pipeline adapter wrapper for the Query-based flow.
It delegates to the event-first run(event, binding) method.
For the new event-first Protocol v1, use run(event, binding) instead.
@@ -265,34 +265,34 @@ class AgentRunOrchestrator:
raise RunnerNotFoundError('no runner configured')
# Convert Query to event-first envelope
event = PipelineCompatAdapter.query_to_event(query)
event = PipelineAdapter.query_to_event(query)
# Convert Pipeline config to binding
binding = PipelineCompatAdapter.pipeline_config_to_binding(query, runner_id)
binding = PipelineAdapter.pipeline_config_to_binding(query, runner_id)
# Extract bound plugins for authorization
bound_plugins = query.variables.get('_pipeline_bound_plugins')
# Build compatibility context for Pipeline-specific fields
compatibility_context = await self._build_compatibility_context(query, binding)
# Build adapter context for Pipeline-specific fields
adapter_context = await self._build_adapter_context(query, binding)
# Delegate to event-first run()
async for result in self.run(
event,
binding,
bound_plugins=bound_plugins,
compatibility_context=compatibility_context,
adapter_context=adapter_context,
):
yield result
async def _build_compatibility_context(
async def _build_adapter_context(
self,
query: pipeline_query.Query,
binding: AgentBinding,
) -> dict[str, typing.Any]:
"""Build compatibility context for Pipeline Query-based flow.
"""Build adapter context for Pipeline Query-based flow.
This extracts legacy fields from Query that aren't available in
This extracts adapter-specific fields from Query that aren't available in
the event-first flow:
- params (from query.variables)
- bootstrap messages (for max-round)
@@ -304,7 +304,7 @@ class AgentRunOrchestrator:
binding: Agent binding with max_round
Returns:
Compatibility context dict
Adapter context dict
"""
from .context_packager import AgentContextPackager
@@ -312,10 +312,10 @@ class AgentRunOrchestrator:
# (excludes internal vars, sensitive patterns, permission vars, non-JSON values)
params = self.context_builder._build_params(query)
# Build prompt from query.prompt.messages (for legacy compatibility)
# Build prompt from query.prompt.messages (for transition runners)
prompt = self.context_builder._build_prompt(query)
# Build bootstrap context for legacy max-round
# Build bootstrap context for max-round
bootstrap = None
runtime_metadata = {}
max_round = binding.max_round
@@ -327,12 +327,12 @@ class AgentRunOrchestrator:
packaged_context = context_packager.package_messages(query, runner_config)
# Build messages list
legacy_messages = []
adapter_messages = []
for msg in packaged_context.messages:
legacy_messages.append(msg.model_dump(mode='json'))
adapter_messages.append(msg.model_dump(mode='json'))
bootstrap = {
'messages': legacy_messages,
'messages': adapter_messages,
'summary': None,
'artifacts': [],
'metadata': {},
@@ -497,7 +497,7 @@ class AgentRunOrchestrator:
"""
data = result_dict.get('data', {})
# Extract scope (default to 'conversation' for backward compat)
# Extract scope (default to conversation when omitted by the runner)
scope = data.get('scope', 'conversation')
# Extract key and value

View File

@@ -1,7 +1,6 @@
"""Persistent state store for AgentRunner protocol state.
This module provides a database-backed state store for event-first Protocol v1,
while preserving in-memory state store for legacy Query-based flow.
This module provides a database-backed state store for event-first Protocol v1.
"""
from __future__ import annotations
@@ -25,8 +24,8 @@ from ...entity.persistence.agent_runner_state import AgentRunnerState
# Valid state scopes for agent runner state updates.
VALID_STATE_SCOPES = ('conversation', 'actor', 'subject', 'runner')
# Key mapping for backward compatibility
LEGACY_KEY_MAPPING = {
# External-facing key aliases accepted from runners.
STATE_KEY_ALIASES = {
'conversation_id': 'external.conversation_id',
}
@@ -276,9 +275,9 @@ class PersistentStateStore:
if not self._check_scope_enabled(scope, binding):
return False, f'Scope "{scope}" not enabled by binding policy'
# Map legacy key names
if key in LEGACY_KEY_MAPPING:
key = LEGACY_KEY_MAPPING[key]
# Map accepted key aliases
if key in STATE_KEY_ALIASES:
key = STATE_KEY_ALIASES[key]
# Get scope key
scope_key = self._get_scope_key(scope, event, binding, descriptor)

View File

@@ -1,7 +1,7 @@
"""Pipeline compatibility adapter for converting Query to event-first envelope.
"""Pipeline adapter for converting Query to event-first envelope.
This adapter bridges the legacy Query/Pipeline approach with the new
event-first Protocol v1 architecture. It is a compatibility layer only.
This adapter bridges the Query/Pipeline entry point with the event-first
Protocol v1 architecture.
"""
from __future__ import annotations
@@ -32,14 +32,14 @@ from .host_models import (
from . import events as runner_events
class PipelineCompatAdapter:
class PipelineAdapter:
"""Adapter for converting Pipeline Query to event-first envelope.
This adapter is responsible for:
- Converting Query to AgentEventEnvelope
- Converting Pipeline config to temporary AgentBinding
- Handling legacy max-round as bootstrap policy
- Putting Query-only fields into compatibility context
- Handling max-round as bootstrap policy
- Putting Query-only fields into adapter context
"""
@classmethod
@@ -80,7 +80,7 @@ class PipelineCompatAdapter:
event_id=event.event_id or str(query.query_id),
event_type=event.event_type or runner_events.MESSAGE_RECEIVED,
event_time=event.event_time,
source="pipeline_compat",
source="pipeline_adapter",
bot_id=query.bot_uuid,
workspace_id=None, # Not available in Query
conversation_id=conversation.conversation_id,
@@ -111,7 +111,7 @@ class PipelineCompatAdapter:
ai_config = pipeline_config.get('ai', {})
runner_config = ai_config.get('runner_config', {}).get(runner_id, {})
# Extract max_round for compatibility (used in bootstrap, not Protocol v1)
# Extract max_round for adapter (used in bootstrap, not Protocol v1)
# Note: config uses 'max-round' with hyphen, not 'max_round' with underscore
max_round = runner_config.get('max-round') or ai_config.get('max-round')
@@ -135,7 +135,6 @@ class PipelineCompatAdapter:
)
# Build delivery policy
output_config = pipeline_config.get('output', {})
delivery_policy = DeliveryPolicy(
enable_streaming=True,
enable_reply=True,
@@ -161,10 +160,10 @@ class PipelineCompatAdapter:
query: pipeline_query.Query,
binding: AgentBinding,
) -> dict[str, typing.Any]:
"""Build bootstrap context from binding for legacy max-round.
"""Build bootstrap context from binding for max-round.
This method handles the legacy max-round -> bootstrap conversion.
max-round is NOT part of Protocol v1, only used by compatibility adapter.
This method handles the max-round -> bootstrap conversion.
max-round is NOT part of Protocol v1, only used by Pipeline adapter.
Args:
query: Pipeline query
@@ -183,42 +182,42 @@ class PipelineCompatAdapter:
"artifacts": [],
"metadata": {
"policy": "self_managed",
"legacy_max_round": None,
"max_round": None,
},
}
# Legacy max-round packaging (will be handled by context_packager)
# max-round packaging (will be handled by context_packager)
return {
"messages": [], # Will be filled by context_packager
"summary": None,
"artifacts": [],
"metadata": {
"policy": "legacy_max_round",
"legacy_max_round": max_round,
"policy": "max_round",
"max_round": max_round,
},
}
@classmethod
def build_compatibility_context(
def build_adapter_context(
cls,
query: pipeline_query.Query,
) -> dict[str, typing.Any]:
"""Build compatibility context for legacy Query/Pipeline fields.
"""Build adapter context for Pipeline adapter fields.
These fields are for migration purposes only.
These fields are for transition purposes only.
Runners should NOT depend on them for long-term capabilities.
Args:
query: Pipeline query
Returns:
Compatibility context data
Adapter context data
"""
return {
"query_id": query.query_id,
"pipeline_uuid": query.pipeline_uuid,
"max_round": None, # Moved to binding, not here
"legacy_messages": [], # Will be filled by context_packager
"adapter_messages": [], # Will be filled by context_packager
"extra": {
"bot_uuid": query.bot_uuid,
"sender_id": str(query.sender_id) if query.sender_id else None,
@@ -266,7 +265,7 @@ class PipelineCompatAdapter:
event_id=str(message_id or query.query_id),
event_type=runner_events.MESSAGE_RECEIVED,
event_time=event_time,
source="pipeline_compat",
source="pipeline_adapter",
source_event_type=source_event_type,
data=event_data,
)

View File

@@ -32,7 +32,7 @@ class AgentResourceBuilder:
Entry points:
- build_resources_from_binding(event, binding, descriptor): Event-first Protocol v1
- build_resources(query, descriptor): Legacy Query-based
- build_resources(query, descriptor): Pipeline adapter Query-based
Note: This only builds the resource declaration. The actual proxy actions
in handler.py must still validate against ctx.resources at runtime.
@@ -216,7 +216,7 @@ class AgentResourceBuilder:
) -> AgentResources:
"""Build AgentResources from query and runner descriptor.
This is a compatibility wrapper for Query-based flow.
This is a Pipeline adapter wrapper for Query-based flow.
Args:
query: Pipeline query with pipeline_config and variables

View File

@@ -13,8 +13,8 @@ from .host_models import AgentEventEnvelope
# Valid state scopes for agent runner state updates.
VALID_STATE_SCOPES = ('conversation', 'actor', 'subject', 'runner')
# Key mapping for backward compatibility
LEGACY_KEY_MAPPING = {
# External-facing key aliases accepted from runners.
STATE_KEY_ALIASES = {
'conversation_id': 'external.conversation_id',
}
@@ -226,12 +226,12 @@ class RunnerScopedStateStore:
)
return False
# Map legacy key names
if key in LEGACY_KEY_MAPPING:
mapped_key = LEGACY_KEY_MAPPING[key]
# Map accepted key aliases
if key in STATE_KEY_ALIASES:
mapped_key = STATE_KEY_ALIASES[key]
if logger:
logger.debug(
f'Runner {descriptor.id} state.updated legacy key "{key}" mapped to "{mapped_key}"'
f'Runner {descriptor.id} state.updated key alias "{key}" mapped to "{mapped_key}"'
)
key = mapped_key
@@ -245,8 +245,7 @@ class RunnerScopedStateStore:
# Sync external.conversation_id to query.session.using_conversation.uuid
if scope == 'conversation' and key == 'external.conversation_id':
if query.session and query.session.using_conversation:
# Update conversation uuid for backward compatibility
# This ensures old conversation continuation behavior works
# Keep the active conversation UUID aligned with runner-owned state.
setattr(query.session.using_conversation, 'uuid', value)
if logger:
logger.debug(
@@ -564,12 +563,12 @@ class RunnerScopedStateStore:
)
return False
# Map legacy key names
if key in LEGACY_KEY_MAPPING:
mapped_key = LEGACY_KEY_MAPPING[key]
# Map accepted key aliases
if key in STATE_KEY_ALIASES:
mapped_key = STATE_KEY_ALIASES[key]
if logger:
logger.debug(
f'Runner {descriptor.id} state.updated legacy key "{key}" mapped to "{mapped_key}"'
f'Runner {descriptor.id} state.updated key alias "{key}" mapped to "{mapped_key}"'
)
key = mapped_key

View File

@@ -30,7 +30,7 @@ class EventLog(Base):
"""When the event occurred."""
source = sqlalchemy.Column(sqlalchemy.String(50), nullable=False)
"""Event source (platform, webui, api, scheduler, system, pipeline_compat)."""
"""Event source (platform, webui, api, scheduler, system, pipeline_adapter)."""
bot_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
"""Bot UUID that handled this event."""

View File

@@ -4,8 +4,8 @@ from .. import truncator
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
from ....agent.runner.config_migration import ConfigMigration
from ....agent.runner.context_packager import (
get_legacy_max_round,
select_legacy_max_round_messages,
get_max_round,
select_max_round_messages,
)
@@ -21,9 +21,9 @@ class RoundTruncator(truncator.Truncator):
else:
runner_config = query.pipeline_config.get('msg-truncate', {}).get('round', {})
query.messages = select_legacy_max_round_messages(
query.messages = select_max_round_messages(
query.messages,
get_legacy_max_round(runner_config),
get_max_round(runner_config),
)
return query

View File

@@ -182,8 +182,8 @@ class TestDefaultPipelineConfig:
assert config['ai']['runner_config'] == {}
class TestResolveRunnerIdBackwardCompat:
"""Tests for backward compatibility in resolve_runner_id."""
class TestResolveRunnerIdAliases:
"""Tests for runner id alias resolution."""
def test_resolve_new_format_id(self):
"""resolve_runner_id should work with new format."""

View File

@@ -420,12 +420,12 @@ class TestBuildParamsInContext:
context = await builder.build_context(query, descriptor, resources)
# Protocol v1: params is in compatibility.extra
assert 'compatibility' in context
assert 'extra' in context['compatibility']
assert 'params' in context['compatibility']['extra']
assert context['compatibility']['extra']['params']['public_param'] == 'value'
assert '_private' not in context['compatibility']['extra']['params']
# Protocol v1: params is in adapter.extra
assert 'adapter' in context
assert 'extra' in context['adapter']
assert 'params' in context['adapter']['extra']
assert context['adapter']['extra']['params']['public_param'] == 'value'
assert '_private' not in context['adapter']['extra']['params']
@pytest.mark.asyncio
async def test_params_and_state_both_present(self):
@@ -457,12 +457,12 @@ class TestBuildParamsInContext:
context = await builder.build_context(query, descriptor, resources)
# Protocol v1: params is in compatibility.extra
assert 'compatibility' in context
assert 'extra' in context['compatibility']
assert 'params' in context['compatibility']['extra']
assert context['compatibility']['extra']['params']['workflow_input'] == 'user_question'
assert context['compatibility']['extra']['params']['sender_name'] == 'John'
# Protocol v1: params is in adapter.extra
assert 'adapter' in context
assert 'extra' in context['adapter']
assert 'params' in context['adapter']['extra']
assert context['adapter']['extra']['params']['workflow_input'] == 'user_question'
assert context['adapter']['extra']['params']['sender_name'] == 'John'
# state should have seeded conversation_id
assert 'state' in context
@@ -495,10 +495,10 @@ class TestBuildParamsInContext:
context = await builder.build_context(query, descriptor, resources)
# Protocol v1: prompt is in compatibility.extra
assert 'compatibility' in context
assert 'extra' in context['compatibility']
assert 'prompt' in context['compatibility']['extra']
assert context['compatibility']['extra']['prompt'][0]['content'] == 'Effective prompt'
# Protocol v1: prompt is in adapter.extra
assert 'adapter' in context
assert 'extra' in context['adapter']
assert 'prompt' in context['adapter']['extra']
assert context['adapter']['extra']['prompt'][0]['content'] == 'Effective prompt'
assert context['runtime']['metadata']['streaming_supported'] is True
assert context['runtime']['metadata']['remove_think'] is True

View File

@@ -200,7 +200,7 @@ class TestContextValidation:
assert 'delivery' in context_dict, "delivery is REQUIRED in Protocol v1"
assert 'context' in context_dict, "context (ContextAccess) is REQUIRED in Protocol v1"
assert 'bootstrap' in context_dict, "bootstrap should exist (can be None)"
assert 'compatibility' in context_dict, "compatibility should exist"
assert 'adapter' in context_dict, "adapter should exist"
assert 'metadata' in context_dict, "metadata should exist"
@pytest.mark.asyncio

View File

@@ -1,10 +1,10 @@
"""Tests for event-first Protocol v1 entities and Pipeline compatibility adapter.
"""Tests for event-first Protocol v1 entities and Pipeline adapter.
Tests cover:
1. Pipeline Query -> AgentEventEnvelope conversion
2. Pipeline config -> AgentBinding conversion
3. AgentRunContext not inlining full history by default
4. Legacy max-round only affecting bootstrap/compat adapter
4. Pipeline max-round only affecting bootstrap/adapter context
5. Event-first run() entry point
"""
from __future__ import annotations
@@ -49,7 +49,7 @@ from langbot.pkg.agent.runner.host_models import (
StatePolicy,
DeliveryPolicy,
)
from langbot.pkg.agent.runner.pipeline_compat_adapter import PipelineCompatAdapter
from langbot.pkg.agent.runner.pipeline_adapter import PipelineAdapter
class TestPipelineQueryToEventEnvelope:
@@ -57,24 +57,24 @@ class TestPipelineQueryToEventEnvelope:
def test_query_to_event_basic_fields(self, mock_query):
"""Test basic field conversion from Query to Event envelope."""
event = PipelineCompatAdapter.query_to_event(mock_query)
event = PipelineAdapter.query_to_event(mock_query)
assert event.event_type == "message.received"
assert event.source == "pipeline_compat"
assert event.source == "pipeline_adapter"
assert event.bot_id == mock_query.bot_uuid
assert event.actor is not None
assert event.actor.actor_type == "user"
def test_query_to_event_input(self, mock_query):
"""Test input conversion from Query."""
event = PipelineCompatAdapter.query_to_event(mock_query)
event = PipelineAdapter.query_to_event(mock_query)
assert event.input is not None
assert event.input.text == "Hello world"
def test_query_to_event_conversation(self, mock_query):
"""Test conversation context extraction."""
event = PipelineCompatAdapter.query_to_event(mock_query)
event = PipelineAdapter.query_to_event(mock_query)
# Conversation may be None if no session
if event.conversation_id:
@@ -82,7 +82,7 @@ class TestPipelineQueryToEventEnvelope:
def test_query_to_event_delivery_context(self, mock_query):
"""Test delivery context extraction."""
event = PipelineCompatAdapter.query_to_event(mock_query)
event = PipelineAdapter.query_to_event(mock_query)
assert event.delivery is not None
assert event.delivery.surface == "platform"
@@ -94,7 +94,7 @@ class TestPipelineConfigToBinding:
def test_config_to_binding_runner_id(self, mock_query):
"""Test binding runner_id extraction."""
binding = PipelineCompatAdapter.pipeline_config_to_binding(
binding = PipelineAdapter.pipeline_config_to_binding(
mock_query, "plugin:author/plugin/runner"
)
@@ -102,7 +102,7 @@ class TestPipelineConfigToBinding:
def test_config_to_binding_scope(self, mock_query):
"""Test binding scope extraction."""
binding = PipelineCompatAdapter.pipeline_config_to_binding(
binding = PipelineAdapter.pipeline_config_to_binding(
mock_query, "plugin:test/plugin/runner"
)
@@ -110,8 +110,8 @@ class TestPipelineConfigToBinding:
assert binding.scope.scope_id == mock_query.pipeline_uuid
def test_config_to_binding_max_round(self, mock_query_with_max_round):
"""Test max_round extraction for compatibility adapter."""
binding = PipelineCompatAdapter.pipeline_config_to_binding(
"""Test max_round extraction for Pipeline adapter."""
binding = PipelineAdapter.pipeline_config_to_binding(
mock_query_with_max_round, "plugin:test/plugin/runner"
)
@@ -120,7 +120,7 @@ class TestPipelineConfigToBinding:
def test_config_to_binding_no_max_round(self, mock_query):
"""Test binding without max_round."""
binding = PipelineCompatAdapter.pipeline_config_to_binding(
binding = PipelineAdapter.pipeline_config_to_binding(
mock_query, "plugin:test/plugin/runner"
)
@@ -210,8 +210,8 @@ class TestAgentRunContextProtocolV1:
assert ctx.bootstrap is None or isinstance(ctx.bootstrap.messages, list)
class TestLegacyMaxRoundNotInProtocol:
"""Test that legacy max-round only affects compat adapter, not Protocol v1."""
class TestMaxRoundNotInProtocol:
"""Test that Pipeline max-round only affects adapter context, not Protocol v1."""
def test_max_round_not_in_sdk_context(self):
"""Test max-round is not a field in SDK AgentRunContext."""
@@ -221,8 +221,8 @@ class TestLegacyMaxRoundNotInProtocol:
assert "max_round" not in ctx_fields
assert "maxRound" not in ctx_fields
def test_max_round_in_compatibility_context(self):
"""Test max_round is in compatibility context, not main context."""
def test_max_round_in_adapter_context(self):
"""Test max_round is in adapter context, not main context."""
trigger = AgentTrigger(type="message.received")
event = AgentEventContext(
event_id="evt_1",
@@ -233,9 +233,9 @@ class TestLegacyMaxRoundNotInProtocol:
from langbot_plugin.api.entities.builtin.agent_runner.resources import AgentResources
from langbot_plugin.api.entities.builtin.agent_runner.runtime import AgentRuntimeContext
from langbot_plugin.api.entities.builtin.agent_runner.delivery import DeliveryContext
from langbot_plugin.api.entities.builtin.agent_runner.context import CompatibilityContext
from langbot_plugin.api.entities.builtin.agent_runner.context import AdapterContext
compat = CompatibilityContext(max_round=10)
adapter = AdapterContext(max_round=10)
ctx = AgentRunContext(
run_id="run_1",
@@ -245,20 +245,20 @@ class TestLegacyMaxRoundNotInProtocol:
delivery=DeliveryContext(surface="platform"),
resources=AgentResources(),
runtime=AgentRuntimeContext(),
compatibility=compat,
adapter=adapter,
)
# max_round is in compatibility context, not main context
assert ctx.compatibility is not None
assert ctx.compatibility.max_round == 10
# max_round is in adapter context, not main context
assert ctx.adapter is not None
assert ctx.adapter.max_round == 10
def test_binding_max_round_for_adapter_only(self, mock_query_with_max_round):
"""Test max_round in binding is for adapter use, not Protocol v1."""
binding = PipelineCompatAdapter.pipeline_config_to_binding(
binding = PipelineAdapter.pipeline_config_to_binding(
mock_query_with_max_round, "plugin:test/plugin/runner"
)
# max_round is in binding (Host-internal) for compat adapter
# max_round is in binding (Host-internal) for Pipeline adapter
assert binding.max_round == 10
# But SDK entities don't have it

View File

@@ -8,7 +8,7 @@ Tests focus on:
Authorization paths:
1. AgentRunner calls: has run_id, validates against session_registry
2. Regular plugin calls: no run_id, unrestricted (backward compatibility)
2. Regular plugin calls: no run_id, unscoped plugin action path
"""
from __future__ import annotations
@@ -220,7 +220,7 @@ class TestInvokeLLMAuthorization:
async def test_invoke_llm_no_run_id_unrestricted(self):
"""INVOKE_LLM: no run_id should be unrestricted (backward compat)."""
# When no run_id is provided, the authorization check is skipped
# This is the backward compatibility path for regular plugin calls
# This is the unscoped path for regular plugin calls
# Simulate: if not run_id, skip authorization
run_id = None
@@ -404,7 +404,7 @@ class TestRetrieveKnowledgeBaseAuthorization:
async def test_retrieve_knowledge_base_no_run_id_pipeline_check(self):
"""RETRIEVE_KNOWLEDGE_BASE: no run_id checks pipeline config."""
# When no run_id, the handler checks against pipeline's configured KBs
# This is the backward compatibility path for regular plugin calls
# This is the unscoped path for regular plugin calls
from langbot.pkg.agent.runner.config_migration import ConfigMigration
@@ -899,7 +899,7 @@ class TestSDKAgentRunAPIProxyFieldConsistency:
class TestNoRunIdBackwardCompatPath:
"""Tests for backward compatibility path when no run_id is provided.
"""Tests for unscoped plugin action path when no run_id is provided.
Regular plugins (non-AgentRunner) don't have run_id and should
have unrestricted access to certain APIs.
@@ -1965,7 +1965,7 @@ class TestCallerPluginIdentityValidation:
@pytest.mark.asyncio
async def test_no_caller_identity_allowed(self):
"""_validate_run_authorization allows when caller_plugin_identity not provided."""
# Backward compatibility: if caller_plugin_identity is None, skip identity check
# Unscoped plugin path: if caller_plugin_identity is None, skip identity check
from langbot.pkg.agent.runner.session_registry import get_session_registry
registry = get_session_registry()
resources = make_resources(models=[{'model_id': 'model_001'}])
@@ -2000,7 +2000,7 @@ class TestCallerPluginIdentityValidation:
class TestBackwardCompatStorageNoRunId:
"""Tests for backward compatibility: storage actions without run_id.
"""Tests for unscoped storage actions without run_id.
Regular plugins (non-AgentRunner) don't have run_id and should
have unrestricted access to storage APIs.

View File

@@ -317,8 +317,8 @@ async def test_orchestrator_runs_fake_plugin_with_authorized_context(clean_agent
context = plugin_connector.contexts[0]
assert context["config"]["timeout"] == 30
assert context["runtime"]["deadline_at"] is not None
# Protocol v1: params is in compatibility.extra
assert context["compatibility"]["extra"]["params"] == {"public_param": "visible"}
# Protocol v1: params is in adapter.extra
assert context["adapter"]["extra"]["params"] == {"public_param": "visible"}
assert context["event"]["event_type"] == "message.received"
# Note: source_event_type is in event.source_event_type, not event.data
# (event.data contains the raw event payload, not metadata)
@@ -341,8 +341,8 @@ async def test_orchestrator_runs_fake_plugin_with_authorized_context(clean_agent
@pytest.mark.asyncio
async def test_orchestrator_packages_legacy_max_round_without_mutating_query(clean_agent_state):
"""Test that legacy max-round is packaged without mutating original query."""
async def test_orchestrator_packages_max_round_without_mutating_query(clean_agent_state):
"""Test that max-round is packaged without mutating original query."""
db_engine = clean_agent_state
descriptor = make_descriptor()
plugin_connector = FakePluginConnector(
@@ -370,7 +370,7 @@ async def test_orchestrator_packages_legacy_max_round_without_mutating_query(cle
assert len(messages) == 1
context = plugin_connector.contexts[0]
# Protocol v1: legacy messages are in bootstrap.messages
# Protocol v1: messages are in bootstrap.messages
assert context["bootstrap"] is not None
assert [message["content"] for message in context["bootstrap"]["messages"]] == [
"message 2",
@@ -378,8 +378,8 @@ async def test_orchestrator_packages_legacy_max_round_without_mutating_query(cle
"message 3",
"response 3",
]
# Also in compatibility.legacy_messages for legacy runners
assert [message["content"] for message in context["compatibility"]["legacy_messages"]] == [
# Also in adapter.adapter_messages for transition runners
assert [message["content"] for message in context["adapter"]["adapter_messages"]] == [
"message 2",
"response 2",
"message 3",
@@ -395,7 +395,7 @@ async def test_orchestrator_packages_legacy_max_round_without_mutating_query(cle
]
assert context["runtime"]["metadata"]["context_packaging"] == {
"policy": {
"mode": "legacy_max_round",
"mode": "max_round",
"max_round": 2,
},
"history": {
@@ -592,12 +592,12 @@ class TestPipelineCompatibilityQueryIdInSession:
assert session_during_run["query_id"] is None
class TestPipelineCompatibilityPromptAndParams:
"""Tests for prompt and params handling in Pipeline compatibility."""
class TestPipelineAdapterPromptAndParams:
"""Tests for prompt and params handling in Pipeline adapter."""
@pytest.mark.asyncio
async def test_prompt_in_compatibility_extra(self, clean_agent_state):
"""Pipeline prompt is placed in compatibility.extra.prompt."""
async def test_prompt_in_adapter_extra(self, clean_agent_state):
"""Pipeline prompt is placed in adapter.extra.prompt."""
from langbot_plugin.api.entities.builtin.provider import prompt as provider_prompt
db_engine = clean_agent_state
@@ -625,10 +625,10 @@ class TestPipelineCompatibilityPromptAndParams:
messages = [message async for message in orchestrator.run_from_query(query)]
context = plugin_connector.contexts[0]
# Prompt should be in compatibility.extra
assert "prompt" in context["compatibility"]["extra"]
assert len(context["compatibility"]["extra"]["prompt"]) == 1
assert context["compatibility"]["extra"]["prompt"][0]["role"] == "system"
# Prompt should be in adapter.extra
assert "prompt" in context["adapter"]["extra"]
assert len(context["adapter"]["extra"]["prompt"]) == 1
assert context["adapter"]["extra"]["prompt"][0]["role"] == "system"
# Top-level should NOT have prompt
assert "prompt" not in context
@@ -656,7 +656,7 @@ class TestPipelineCompatibilityPromptAndParams:
messages = [message async for message in orchestrator.run_from_query(query)]
context = plugin_connector.contexts[0]
assert context["compatibility"]["extra"]["params"] == {
assert context["adapter"]["extra"]["params"] == {
"public_param": "visible",
"another_param": 123,
}
@@ -686,7 +686,7 @@ class TestPipelineCompatibilityPromptAndParams:
messages = [message async for message in orchestrator.run_from_query(query)]
context = plugin_connector.contexts[0]
params = context["compatibility"]["extra"]["params"]
params = context["adapter"]["extra"]["params"]
assert "public_param" in params
assert "_internal_var" not in params
assert "_pipeline_bound_plugins" not in params
@@ -718,7 +718,7 @@ class TestPipelineCompatibilityPromptAndParams:
messages = [message async for message in orchestrator.run_from_query(query)]
context = plugin_connector.contexts[0]
params = context["compatibility"]["extra"]["params"]
params = context["adapter"]["extra"]["params"]
assert "public_param" in params
assert "api_token" not in params
assert "secret_key" not in params
@@ -750,14 +750,14 @@ class TestPipelineCompatibilityPromptAndParams:
messages = [message async for message in orchestrator.run_from_query(query)]
context = plugin_connector.contexts[0]
params = context["compatibility"]["extra"]["params"]
params = context["adapter"]["extra"]["params"]
assert "public_param" in params
assert "a_set" not in params
assert "a_lambda" not in params
class TestPipelineCompatibilityHostCapabilities:
"""Tests for event-first host capabilities via Pipeline compatibility path."""
class TestPipelineAdapterHostCapabilities:
"""Tests for event-first host capabilities via Pipeline adapter path."""
@pytest.mark.asyncio
async def test_state_updated_writes_to_persistent_store(self, clean_agent_state):
@@ -795,9 +795,9 @@ class TestPipelineCompatibilityHostCapabilities:
persistent_store = get_persistent_state_store(db_engine)
# Build snapshot to check if state was written
# Note: We need to rebuild the event and binding to query the store
from langbot.pkg.agent.runner.pipeline_compat_adapter import PipelineCompatAdapter
event = PipelineCompatAdapter.query_to_event(query)
binding = PipelineCompatAdapter.pipeline_config_to_binding(query, RUNNER_ID)
from langbot.pkg.agent.runner.pipeline_adapter import PipelineAdapter
event = PipelineAdapter.query_to_event(query)
binding = PipelineAdapter.pipeline_config_to_binding(query, RUNNER_ID)
snapshot = await persistent_store.build_snapshot_from_event(event, binding, descriptor)
assert snapshot["conversation"]["external.test_key"] == "test_value"

View File

@@ -6,7 +6,7 @@ from langbot.pkg.agent.runner.state_store import (
get_state_store,
reset_state_store,
VALID_STATE_SCOPES,
LEGACY_KEY_MAPPING,
STATE_KEY_ALIASES,
)
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
from langbot.pkg.agent.runner.host_models import AgentBinding, BindingScope, StatePolicy
@@ -219,8 +219,8 @@ class TestStateStoreApplyUpdate:
assert len(logger.warnings) == 1
assert 'invalid scope' in logger.warnings[0]
def test_apply_update_legacy_key_mapping(self):
"""Legacy key conversation_id should be mapped to external.conversation_id."""
def test_apply_update_state_key_alias(self):
"""Alias key conversation_id should be mapped to external.conversation_id."""
store = RunnerScopedStateStore()
descriptor = make_descriptor()
query = FakeQuery()
@@ -481,9 +481,9 @@ class TestConstants:
"""VALID_STATE_SCOPES should have four scopes."""
assert VALID_STATE_SCOPES == ('conversation', 'actor', 'subject', 'runner')
def test_legacy_key_mapping(self):
"""LEGACY_KEY_MAPPING should map conversation_id."""
assert LEGACY_KEY_MAPPING == {'conversation_id': 'external.conversation_id'}
def test_state_key_aliases(self):
"""STATE_KEY_ALIASES should map conversation_id."""
assert STATE_KEY_ALIASES == {'conversation_id': 'external.conversation_id'}
# ========== Event-first Protocol v1 tests ==========
@@ -781,8 +781,8 @@ class TestStateStoreEventFirstApplyUpdate:
assert len(logger.warnings) == 1
assert 'missing identity' in logger.warnings[0]
def test_apply_update_legacy_key_mapping(self):
"""Legacy key conversation_id should be mapped to external.conversation_id."""
def test_apply_update_state_key_alias(self):
"""Alias key conversation_id should be mapped to external.conversation_id."""
store = RunnerScopedStateStore()
descriptor = make_descriptor()
event = FakeEventEnvelope(conversation_id='conv_001')
@@ -1371,4 +1371,4 @@ class TestPersistentStateStore:
# Delete non-existent should return False
deleted_again = await persistent_store.state_delete(scope_key, 'key')
assert deleted_again is False
assert deleted_again is False