mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
feat(agent-runner): align protocol adapter terminology
This commit is contained in:
@@ -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。
|
||||
|
||||
@@ -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. 落地顺序
|
||||
|
||||
|
||||
@@ -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 集成
|
||||
- 平台动作执行器
|
||||
|
||||
@@ -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`: 本次下发的历史消息数
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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. 开放问题
|
||||
|
||||
|
||||
@@ -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 提供。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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)."""
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user