diff --git a/docs/agent-runner-pluginization/AGENT_CONTEXT_PROTOCOL.md b/docs/agent-runner-pluginization/AGENT_CONTEXT_PROTOCOL.md index 13e47cc9..0d64caf7 100644 --- a/docs/agent-runner-pluginization/AGENT_CONTEXT_PROTOCOL.md +++ b/docs/agent-runner-pluginization/AGENT_CONTEXT_PROTOCOL.md @@ -2,6 +2,20 @@ 本文档描述插件化 AgentRunner 场景下的上下文边界。结论先行:LangBot 不应成为最终 agentic context manager;LangBot 应提供 context substrate,AgentRunner 或其背后的 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。 diff --git a/docs/agent-runner-pluginization/EVENT_BASED_AGENT.md b/docs/agent-runner-pluginization/EVENT_BASED_AGENT.md index 3250d096..53f225b1 100644 --- a/docs/agent-runner-pluginization/EVENT_BASED_AGENT.md +++ b/docs/agent-runner-pluginization/EVENT_BASED_AGENT.md @@ -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. 落地顺序 diff --git a/docs/agent-runner-pluginization/HOST_SDK_INFRASTRUCTURE.md b/docs/agent-runner-pluginization/HOST_SDK_INFRASTRUCTURE.md index 2490d0e0..6d3418be 100644 --- a/docs/agent-runner-pluginization/HOST_SDK_INFRASTRUCTURE.md +++ b/docs/agent-runner-pluginization/HOST_SDK_INFRASTRUCTURE.md @@ -27,16 +27,17 @@ SDK 要提供稳定协议: - 不要求官方 local-agent 的旧行为反向塑造 host 协议。 - 不在 host 中实现通用 agentic prompt assembler。 - 不强制 runner 使用 LangBot state / storage;LangBot 只提供可选、受控的寄宿能力。 +- **不实现 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 Gateway(Future 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 集成 +- 平台动作执行器 diff --git a/docs/agent-runner-pluginization/IMPLEMENTATION_PLAN.md b/docs/agent-runner-pluginization/IMPLEMENTATION_PLAN.md index df62d60b..51aaa92f 100644 --- a/docs/agent-runner-pluginization/IMPLEMENTATION_PLAN.md +++ b/docs/agent-runner-pluginization/IMPLEMENTATION_PLAN.md @@ -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`: 本次下发的历史消息数 diff --git a/docs/agent-runner-pluginization/OFFICIAL_RUNNER_PLUGINS.md b/docs/agent-runner-pluginization/OFFICIAL_RUNNER_PLUGINS.md index dd6700a1..75d28f76 100644 --- a/docs/agent-runner-pluginization/OFFICIAL_RUNNER_PLUGINS.md +++ b/docs/agent-runner-pluginization/OFFICIAL_RUNNER_PLUGINS.md @@ -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: diff --git a/docs/agent-runner-pluginization/PROGRESS.md b/docs/agent-runner-pluginization/PROGRESS.md index babacd29..f70d2d66 100644 --- a/docs/agent-runner-pluginization/PROGRESS.md +++ b/docs/agent-runner-pluginization/PROGRESS.md @@ -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 | --- diff --git a/docs/agent-runner-pluginization/PROTOCOL_V1.md b/docs/agent-runner-pluginization/PROTOCOL_V1.md index 757fc0ca..0b6f5341 100644 --- a/docs/agent-runner-pluginization/PROTOCOL_V1.md +++ b/docs/agent-runner-pluginization/PROTOCOL_V1.md @@ -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. 开放问题 diff --git a/docs/agent-runner-pluginization/README.md b/docs/agent-runner-pluginization/README.md index 56050085..b0797fcc 100644 --- a/docs/agent-runner-pluginization/README.md +++ b/docs/agent-runner-pluginization/README.md @@ -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。它应提供事实源、默认上下文引用和按需读取 API;agent 或其背后的 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 Agent(Future) 消息只是事件的一种。后续 `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 提供。 diff --git a/src/langbot/pkg/agent/runner/context_builder.py b/src/langbot/pkg/agent/runner/context_builder.py index 551d207e..b023ebc9 100644 --- a/src/langbot/pkg/agent/runner/context_builder.py +++ b/src/langbot/pkg/agent/runner/context_builder.py @@ -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 diff --git a/src/langbot/pkg/agent/runner/context_packager.py b/src/langbot/pkg/agent/runner/context_packager.py index 5f0d2c60..3de8a558 100644 --- a/src/langbot/pkg/agent/runner/context_packager.py +++ b/src/langbot/pkg/agent/runner/context_packager.py @@ -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={ diff --git a/src/langbot/pkg/agent/runner/host_models.py b/src/langbot/pkg/agent/runner/host_models.py index 8cc2484f..92e8756c 100644 --- a/src/langbot/pkg/agent/runner/host_models.py +++ b/src/langbot/pkg/agent/runner/host_models.py @@ -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).""" diff --git a/src/langbot/pkg/agent/runner/id.py b/src/langbot/pkg/agent/runner/id.py index a3d6d2ab..e0109904 100644 --- a/src/langbot/pkg/agent/runner/id.py +++ b/src/langbot/pkg/agent/runner/id.py @@ -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:') \ No newline at end of file + return runner_id.startswith('plugin:') diff --git a/src/langbot/pkg/agent/runner/orchestrator.py b/src/langbot/pkg/agent/runner/orchestrator.py index 9a337de2..05659a0f 100644 --- a/src/langbot/pkg/agent/runner/orchestrator.py +++ b/src/langbot/pkg/agent/runner/orchestrator.py @@ -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 diff --git a/src/langbot/pkg/agent/runner/persistent_state_store.py b/src/langbot/pkg/agent/runner/persistent_state_store.py index 6bf8d850..2f90d939 100644 --- a/src/langbot/pkg/agent/runner/persistent_state_store.py +++ b/src/langbot/pkg/agent/runner/persistent_state_store.py @@ -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) diff --git a/src/langbot/pkg/agent/runner/pipeline_compat_adapter.py b/src/langbot/pkg/agent/runner/pipeline_adapter.py similarity index 93% rename from src/langbot/pkg/agent/runner/pipeline_compat_adapter.py rename to src/langbot/pkg/agent/runner/pipeline_adapter.py index 95505fbc..8aaf3ec3 100644 --- a/src/langbot/pkg/agent/runner/pipeline_compat_adapter.py +++ b/src/langbot/pkg/agent/runner/pipeline_adapter.py @@ -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, ) diff --git a/src/langbot/pkg/agent/runner/resource_builder.py b/src/langbot/pkg/agent/runner/resource_builder.py index ce54c246..1fcde97b 100644 --- a/src/langbot/pkg/agent/runner/resource_builder.py +++ b/src/langbot/pkg/agent/runner/resource_builder.py @@ -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 diff --git a/src/langbot/pkg/agent/runner/state_store.py b/src/langbot/pkg/agent/runner/state_store.py index 3eb13d11..53e570e2 100644 --- a/src/langbot/pkg/agent/runner/state_store.py +++ b/src/langbot/pkg/agent/runner/state_store.py @@ -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 diff --git a/src/langbot/pkg/entity/persistence/event_log.py b/src/langbot/pkg/entity/persistence/event_log.py index 1d1dd86a..d29510e2 100644 --- a/src/langbot/pkg/entity/persistence/event_log.py +++ b/src/langbot/pkg/entity/persistence/event_log.py @@ -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.""" diff --git a/src/langbot/pkg/pipeline/msgtrun/truncators/round.py b/src/langbot/pkg/pipeline/msgtrun/truncators/round.py index 57c58a24..e44a4b29 100644 --- a/src/langbot/pkg/pipeline/msgtrun/truncators/round.py +++ b/src/langbot/pkg/pipeline/msgtrun/truncators/round.py @@ -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 diff --git a/tests/unit_tests/agent/test_config_migration_full.py b/tests/unit_tests/agent/test_config_migration_full.py index 39c4a52e..15bdef93 100644 --- a/tests/unit_tests/agent/test_config_migration_full.py +++ b/tests/unit_tests/agent/test_config_migration_full.py @@ -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.""" diff --git a/tests/unit_tests/agent/test_context_builder_params_state.py b/tests/unit_tests/agent/test_context_builder_params_state.py index 45ffa200..4f46bb3f 100644 --- a/tests/unit_tests/agent/test_context_builder_params_state.py +++ b/tests/unit_tests/agent/test_context_builder_params_state.py @@ -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 diff --git a/tests/unit_tests/agent/test_context_validation.py b/tests/unit_tests/agent/test_context_validation.py index 84fc354e..fdc442cd 100644 --- a/tests/unit_tests/agent/test_context_validation.py +++ b/tests/unit_tests/agent/test_context_validation.py @@ -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 diff --git a/tests/unit_tests/agent/test_event_first_protocol.py b/tests/unit_tests/agent/test_event_first_protocol.py index 132df809..ee77007c 100644 --- a/tests/unit_tests/agent/test_event_first_protocol.py +++ b/tests/unit_tests/agent/test_event_first_protocol.py @@ -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 diff --git a/tests/unit_tests/agent/test_handler_auth.py b/tests/unit_tests/agent/test_handler_auth.py index 7397ac89..26c560f7 100644 --- a/tests/unit_tests/agent/test_handler_auth.py +++ b/tests/unit_tests/agent/test_handler_auth.py @@ -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. diff --git a/tests/unit_tests/agent/test_orchestrator_integration.py b/tests/unit_tests/agent/test_orchestrator_integration.py index 30bb52dd..1ed456ed 100644 --- a/tests/unit_tests/agent/test_orchestrator_integration.py +++ b/tests/unit_tests/agent/test_orchestrator_integration.py @@ -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" diff --git a/tests/unit_tests/agent/test_state_store.py b/tests/unit_tests/agent/test_state_store.py index bb9a1020..41dbd958 100644 --- a/tests/unit_tests/agent/test_state_store.py +++ b/tests/unit_tests/agent/test_state_store.py @@ -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 \ No newline at end of file + assert deleted_again is False