refactor(agent-runner): align config with agent semantics

This commit is contained in:
huanghuoguoguo
2026-06-02 17:43:44 +08:00
parent d0383e146e
commit f2153f736c
23 changed files with 94 additions and 126 deletions

View File

@@ -14,7 +14,7 @@
-`AgentRunAPIProxy.state` — get/set/delete API
- ✅ EventLog / Transcript / ArtifactStore — host 事实源
- ✅ PersistentStateStore — 持久化状态存储
-`max-round` / host-side history window 已从 LangBot Host/Pipeline 语义中移除;如某 runner 仍需要类似参数,应由该 runner 自己解释配置
-Host-side history window 已从 LangBot Host 语义中移除runner 自己管理 working context
- ✅ 外部 harness context projection 已用 Claude Code runner 做 MVP 验证context 文件、skill 投影、MCP 配置和 host-owned resume state
## 1. 设计原则
@@ -35,11 +35,10 @@
- 可投影给外部 harness 的 scoped context、MCP、skill 和 resource refs。
- payload hard cap 和权限 guardrail。
### 1.2 不再把 `max-round` 作为目标设计
### 1.2 Host 不定义通用历史窗口
`max-round` 这类历史窗口参数不应继续作为 AgentRunner 协议或 Pipeline adapter 的核心概念。
如果某个 runner 仍需要“最多读取多少轮历史”这样的策略参数,应由该 runner 在自己的 manifest/config schema 中声明,并作为 binding config 存到 `ctx.config` / `runner_config`。Host 只提供 history pull API、cursor、hard cap 和权限边界runner 自己决定是否读取、读取多少、如何截断和压缩。
历史窗口策略不应继续作为 AgentRunner 协议或 Pipeline adapter 的核心概念。
Host 只提供 history pull API、cursor、hard cap 和权限边界runner 自己决定是否读取、读取多少、如何截断和压缩。
当前 official local-agent 方向是通过 Host history API 拉取 transcript并由 runner 自己管理模型上下文。它不依赖 Pipeline adapter 下发历史窗口。
@@ -100,7 +99,7 @@ class AgentRunContext(BaseModel):
- delivery 能力,例如是否支持 streaming、reply target、平台限制。
- 已授权资源列表。
- context cursors 和可用 API 能力。
- runner binding config。
- Agent/runner config。
这些是 agent 决定下一步需要的最低信息。
@@ -324,7 +323,7 @@ LangBot core 不应内置官方 agent 的业务流程:
**已完成(当前分支)**
-`max-round` 不再是协议字段,也不再是 Host / Pipeline 通用语义
-Host 不再定义通用历史窗口字段或策略
- ✅ 新 runner 默认不收到历史窗口
-`AgentRunContext` 增加 `context` / cursor / access capabilities
-`AgentRunAPIProxy` 增加 history / events / artifacts / state API

View File

@@ -393,7 +393,7 @@ Proxy 是 runner 访问 host 能力的唯一入口:
-`PipelineAdapter` — Query → Event + Binding
-`AgentBinding` 抽象
-`AgentEventEnvelope` 抽象
-`max-round` 从目标协议中移除;类似历史窗口参数若仍需要,应由具体 runner 的 manifest/config schema 暴露为 binding config
-Host 不定义通用历史窗口字段或策略runner 自己管理 working context
-`PersistentStateStore` — 持久化状态存储
-`EventLogStore` / `TranscriptStore` / `ArtifactStore`
- ✅ history / artifact / event 的受限拉取 API

View File

@@ -75,7 +75,7 @@ SDK Runtime RUN_AGENT -> plugin AgentRunner.run()
- `PipelineService.get_pipeline_metadata()` 不直接访问插件 runtime而是读取 registry。
-`RequestRunner` 只作为迁移参考,不作为最终运行路径。
- `AgentRunOrchestrator` 是 LangBot 侧运行编排层:负责 runner 绑定解析、资源授权、context envelope provisioning、run scope 注册、插件调用和结果归一化;不负责决定 Agent 的最终 prompt/window/压缩策略。
- 插件是无状态执行单元:多个 Pipeline 可以绑定同一个 runner id并分别保存自己的 `ai.runner_config[id]`;运行时 LangBot 只把当前绑定配置放入 `ctx.config` 转发给同一个插件 runner。
- 插件是无状态执行单元:多个 Agent 可以绑定同一个 runner id并分别保存自己的 `ai.runner_config[id]`;运行时 LangBot 只把当前 Agent/runner config 放入 `ctx.config` 转发给同一个插件 runner。
- 禁止按 Pipeline 或 runner config 创建多个插件实例。需要跨请求持久化的状态必须走明确授权的 plugin storage / workspace storage / 外部服务,不能隐式保存在 per-pipeline 插件对象里。
- EBA 只做字段预留,不在本轮实现 EventBus、EventRouter、平台动作执行。
@@ -191,7 +191,7 @@ Pipeline adapter 的 `prompt` 和公开业务变量不进入顶层协议字段
- filtered params -> `ctx.adapter.extra["params"]`
- legacy/effective prompt 可以暂存到 `ctx.adapter.extra["prompt"]`,但 official
runner 不应把它当作行为契约
- LangBot Host 不生成 `bootstrap.messages``adapter_messages` 或 context packaging 元数据
- LangBot Host 不生成 bootstrap history payload 或 context packaging 元数据
现阶段不要把新的压缩或 token-budget 裁剪塞回 Pipeline stage。Pipeline 只负责入口适配;完整历史和长期上下文由 EventLog / Transcript / pull APIs / future ContextCompressor 支撑。
@@ -216,7 +216,7 @@ ContextCompressor
- 完整历史属于 LangBot host不属于插件实例。插件仍是 singleton/stateless。
- `ctx.bootstrap.messages` 不是 Host 默认下发的 working context。
- 每轮不能全量复制/序列化完整历史给插件 runtime否则长会话会产生 O(n) 成本和跨进程 payload 膨胀。
- `max-round` 或类似窗口规则不属于 LangBot Host / Pipeline 语义。
- 通用历史窗口规则不属于 LangBot Host 语义。
- LiteLLM 接入后,模型窗口元信息应作为 resource/runtime metadata 暴露给 runner由 runner 决定预算和压缩策略。
- `ContextCompressor` 生成的是派生 summary/checkpoint不能覆盖或删除 raw history。
- 重启恢复依赖持久化 store 和 summary checkpoint不依赖 `SessionManager` 里的进程内 conversation list。
@@ -470,7 +470,7 @@ async def run_from_query(query: pipeline_query.Query) -> AsyncGenerator[Message
- SDK `AgentRunContext` 保持 event-first`event/input/delivery/resources/context/state/runtime/config/bootstrap/adapter`
- LangBot context builder 只从 `AgentEventEnvelope + AgentBinding` 写入稳定协议字段。
- Pipeline adapter 可以把公开业务变量写入 `ctx.adapter.extra["params"]`legacy/effective prompt 若保留在 `ctx.adapter.extra["prompt"]`,也只属于 adapter metadata。
- 保持 `ctx.config` 只表达静态绑定配置
- 保持 `ctx.config` 只表达静态 Agent/runner config
### Step 2增强宿主 AgentRun proxy action
@@ -489,7 +489,7 @@ async def run_from_query(query: pipeline_query.Query) -> AsyncGenerator[Message
### Step 4local-agent parity
- 使用静态绑定配置 `ctx.config["prompt"]`,不读取 `ctx.adapter.extra["prompt"]`
- 使用静态 Agent/runner config `ctx.config["prompt"]`,不读取 `ctx.adapter.extra["prompt"]`
- 通过 Host history API 拉取 transcript不读取 `ctx.bootstrap.messages` 或 adapter window 字段。
- 当前 user message 从 `ctx.input.contents` 构造,保留多模态内容。
- RAG 只替换/插入文本部分,不丢图片/文件。
@@ -544,7 +544,7 @@ async def run_from_query(query: pipeline_query.Query) -> AsyncGenerator[Message
- `ChatMessageHandler` 不包含插件 runner 解析和 wrapper。
- `PipelineService` 不直接拼插件 runner metadata。
- 所有 runner 配置使用 `ai.runner.id` + `ai.runner_config`
- 插件 runtime 不为每个 Pipeline 或 runner 配置创建插件实例;`runner_config` 只作为绑定配置`ctx.config` 传入。
- 插件 runtime 不为每个 Agent 或 runner 配置创建插件实例;`runner_config` 只作为 Agent/runner config `ctx.config` 传入。
- 主聊天路径不再通过旧内置 runner 执行业务 runner。迁移期间旧文件可以保留。
- 插件只能访问 `ctx.resources` 授权的模型、工具、知识库和文件。
- 宿主 action 能为 AgentRunner 调用恢复必要 Query 语义,插件不需要拿裸 Query。

View File

@@ -265,7 +265,7 @@ Claude Code / Codex 这类外部 harness 不能直接持有 Python 进程内的
- 默认输出:`message.completed` + `run.completed`
- 默认权限:`permission-mode=plan``max-turns=1``disallowedTools=AskUserQuestion`
- 默认状态:如果 Claude Code 返回 `session_id`runner 通过 `state.updated` 写回 `external.session_id`
- 工作目录:优先使用 binding config 的 `working-directory`,其次使用 Host state 中的 `external.working_directory`
- 工作目录:优先使用 Agent/runner config 的 `working-directory`,其次使用 Host state 中的 `external.working_directory`
### 8.2 Context / skill / MCP 投影
@@ -274,8 +274,8 @@ Claude Code runner 当前把 LangBot event-first context 投影给外部 harness
- 写入 `agent-context.json`schema 为 `langbot.agent_runner.external_harness_context.v1`
- 写入 `LANGBOT_CONTEXT.md`,作为人类可读摘要
- 将 prompt prefix 指向 context 文件路径
- 可把 binding 提供的 `skills-json` 写入 Claude Code 原生 `.claude/skills/<name>/SKILL.md`
- 可把 binding 提供的 `mcp-config-json` 写成每次 run 的 MCP config并通过 `--mcp-config` / `--strict-mcp-config` 传给 Claude Code
- 可把 Agent/runner config 提供的 `skills-json` 写入 Claude Code 原生 `.claude/skills/<name>/SKILL.md`
- 可把 Agent/runner config 提供的 `mcp-config-json` 写成每次 run 的 MCP config并通过 `--mcp-config` / `--strict-mcp-config` 传给 Claude Code
- 可通过 `enable-langbot-mcp=true` 启用 SDK-owned per-run LangBot MCP bridge使 Claude Code 通过 MCP 调用受限的 `AgentRunAPIProxy` 能力
这些投影目前由 runner adapter 完成;长期更理想的形态是 LangBot Host 负责生成 scoped resource projectionrunner 只负责适配 Claude Code 的原生目录和 CLI 参数。
@@ -287,12 +287,12 @@ Claude Code runner 当前把 LangBot event-first context 投影给外部 harness
- WebUI Debug Chat 能通过 Pipeline adapter 调用 `claude-code-agent`
- Claude Code 能读取 LangBot context 文件并按指令输出 sentinel
- Skill 文件可以投影到 `.claude/skills/`
- MCP config 可以通过 binding config 投影为 Claude Code CLI 参数
- MCP config 可以通过 Agent/runner config 投影为 Claude Code CLI 参数
- SDK-owned per-run LangBot MCP bridge 可以被真实 Claude Code CLI 调用,并通过 `langbot_get_current_event` 读取当前 run_id
- `external.session_id``external.working_directory` 可以写入 host-owned state用于后续 resume
- `codex-agent` 可通过 WebUI Debug Chat 调用本机 Codex CLI读取 LangBot event context并把 Codex `thread_id` 写入 host-owned state
- SDK-owned per-run LangBot MCP bridge 可以被真实 Codex CLI 调用,并通过 `langbot_get_current_event` 读取当前 run_id
- 对需要代理的本地运行环境,`codex-agent` 可通过 binding config 的 `environment-json` 显式传递非 secret 环境变量
- 对需要代理的本地运行环境,`codex-agent` 可通过 Agent/runner config 的 `environment-json` 显式传递非 secret 环境变量
下一轮测试入口见 [PHASE1_QA_ACCEPTANCE_MATRIX.md](./PHASE1_QA_ACCEPTANCE_MATRIX.md)。

View File

@@ -111,7 +111,7 @@ bin/lbs case list
- runner 选项来自插件 registry。
- 保存后配置仍为 `ai.runner.id` + `ai.runner_config[id]`
- `runner_config` 表示 binding config不表示插件实例状态。
- `runner_config` 表示 Agent/runner config不表示插件实例状态。
- 插件没有循环重启或 metadata 加载失败。
### 5.2 主聊天路径
@@ -175,7 +175,7 @@ Rerank、remove-think、文件输入等场景只在本次改动直接涉及时
1. 确认 `codex` CLI 在 LangBot runtime host 上可执行。
2. 绑定 `plugin:langbot/codex-agent/default`
3. 如需要代理,使用 binding config 的 `environment-json` 显式传入。
3. 如需要代理,使用 Agent/runner config 的 `environment-json` 显式传入。
4. 在 Debug Chat 执行一次真实 smoke。
5. 检查 JSONL 事件、last message、host-owned state。

View File

@@ -11,7 +11,7 @@
- ✅ Host 支持 `run_id` session authorization
- ✅ Host 能从当前 Pipeline 入口生成 event-first context
-`messages` 降级为 optional bootstrap
-`max-round` 不出现在协议实体中,也不属于 Host / Pipeline 语义;类似参数若存在,由 runner 自己解释 `ctx.config`
-Host 不定义通用历史窗口字段或策略runner 自己管理 working context
- ✅ Proxy 覆盖 model、tool、knowledge、state/storage
- ✅ History / Event / Artifact / State API 已落地
- ✅ EventLog / Transcript / ArtifactStore / PersistentStateStore 已落地
@@ -148,7 +148,7 @@ Host 不使用该声明给 runner inline 历史窗口。默认原则:
- Host 只 inline 当前 event / input 和 context handles。
- Runner 拥有 working context assembly。
- Runner 可在授权后通过 Host history / event / artifact / state APIs 拉取更多上下文。
- `max-round` 或类似窗口参数不属于 Protocol v1 字段,也不属于 Pipeline / Host 通用语义;如果某个 runner 需要,应由 runner 自己解释 `ctx.config`
- 历史窗口策略不属于 Protocol v1 字段,也不属于 Host 通用语义
## 4. Run 协议
@@ -194,8 +194,8 @@ class AgentRunContext(BaseModel):
- `event` 是必选字段Protocol v1 是 event-first。
- `input` 表示当前事件的主输入,不等于历史消息。
- `bootstrap` 是可选字段LangBot Host 默认不填历史窗口。
- `adapter` 只放 Pipeline adapter 字段runner 不应依赖它做长期能力。
- `config`Host binding config不是插件实例状态。
- `adapter` 只放入口 adapter 的非核心元数据runner 不应依赖它做长期能力。
- `config`Agent/runner config不是插件实例状态。
### 4.3 AgentTrigger
@@ -345,7 +345,7 @@ class BootstrapContext(BaseModel):
- `bootstrap.messages` 不是 LangBot Host 的默认行为。
- 自管 context runner 默认应收到空 bootstrap。
- Host 不应为了”帮 agent 更聪明”而自动拼接完整 transcript。
- 类似历史窗口策略应由具体 runner 自己解释 binding config,并通过 Host history API 拉取历史new/official runners 不应依赖 Pipeline adapter 下发历史窗口。
- 历史窗口策略 runner 自己管理,并通过 Host history API 按需拉取历史new/official runners 不应依赖入口 adapter 下发历史窗口。
### 4.10 RuntimeContext
@@ -661,14 +661,14 @@ Pipeline 是当前入口 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)`
-runner-specific config 从 Pipeline 当前绑定配置透传到 `AgentBinding.runner_config` / `ctx.config`
-Agent/runner config 从当前配置容器透传到 `AgentBinding.runner_config` / `ctx.config`
- ✅ Query-only 字段放入 `adapter` context
Pipeline adapter 负责:
-`Query` 构造 `AgentEventContext`
- Pipeline config 构造临时 AgentBinding。
- 从当前 runner binding config 构造 `ctx.config`
-当前配置容器构造临时 AgentBinding。
- 从当前 Agent/runner config 构造 `ctx.config`
- 保留必要的 legacy adapter metadata但不定义历史窗口、prompt 组装或 agentic context 策略。
- 后续若需要传递 preprocessing / hook 后的有效指令,应通过 Host prompt/instruction
package pull API 暴露能力位和引用,而不是继续把 prompt 推入 `ctx.adapter.extra`
@@ -685,7 +685,7 @@ Protocol v1 已在当前分支完成:
- ✅ Host 支持 `run_id` session authorization
- ✅ Host 能从当前 Pipeline 入口生成 event-first context
-`messages` 降级为 optional bootstrap
-`max-round` 不出现在协议实体中,也不属于 Host / Pipeline 语义
-Host 不定义通用历史窗口字段或策略
- ✅ Proxy 至少覆盖 model、tool、knowledge、state/storage
- ✅ History / event / artifact API 已落地
- ✅ EventLog / Transcript / ArtifactStore / PersistentStateStore 已落地

View File

@@ -80,7 +80,7 @@ Pipeline path 已获得 event-first host capabilities
LangBot 不应成为最终 agentic context manager。它应提供事实源、默认上下文引用和按需读取 APIagent 或其背后的 runtime 负责历史剪裁、摘要、召回和 KV cache 策略。
`max-round` 这类历史窗口参数不应作为目标协议继续扩展;如果某个 runner 仍需要类似策略,应由该 runner 的 manifest/config schema 暴露为 binding config
Host 不定义通用历史窗口字段或策略runner 通过 Host pull API 按需拉取历史并自行管理 working context
详见 [AGENT_CONTEXT_PROTOCOL.md](./AGENT_CONTEXT_PROTOCOL.md)。

View File

@@ -38,7 +38,7 @@ v2 只是在 Host 上新增一层可选能力。需要管控面的 runner 或管
- EventLog / Transcript / ArtifactStore / PersistentStateStore
- History / Event / Artifact / State / Storage pull APIs
- AgentRunner result stream 和受控错误回流
- binding config 与 host-owned state
- Agent/runner config 与 host-owned state
这些能力足够支持一次 `runner.run(ctx)` 内的安全执行,但不足以承担完整 runtime 管控面。

View File

@@ -23,7 +23,7 @@
### Runner Plugin 负责
- 遵守 LangBot 下发的 binding config、授权资源和运行约束。
- 遵守 LangBot 下发的 Agent/runner config、授权资源和运行约束。
- 将 LangBot 资源投影成目标 runner 可消费的形式,例如 context 文件、MCP 配置、环境变量或 CLI 参数。
- 不把长期状态保存在插件实例内;需要跨轮次保存的外部 session id / working directory 等状态应写入 host-owned state。
- 对外部进程做最小必要封装,包括命令参数构造、超时、取消、输出解析和错误映射。
@@ -70,4 +70,3 @@ Claude Code、Codex、Kimi Code 等外部 harness 可以继续使用自身的权
- Codex / Kimi runner 全量接入。
- EBA 分支完整迁移和联调。
- 发布级安全 hardening 的完整实现。

View File

@@ -25,7 +25,7 @@ class ConfigMigration:
Responsibilities:
- Resolve runner ID from new ai.runner.id or old ai.runner.runner
- Map old built-in runner names to official plugin runner IDs
- Extract runtime runner config from ai.runner_config
- Extract current Agent/runner config from ai.runner_config
- Migrate old ai.<runner-name> blocks into ai.runner_config
"""
@@ -74,7 +74,7 @@ class ConfigMigration:
pipeline_config: dict[str, typing.Any],
runner_id: str,
) -> dict[str, typing.Any]:
"""Resolve runner binding configuration from pipeline configuration.
"""Resolve Agent/runner configuration from pipeline configuration.
Runtime code should only read the migrated format. Legacy
ai.<runner-name> blocks are handled by migration helpers, not by the
@@ -114,9 +114,6 @@ class ConfigMigration:
if old_runner_name:
old_config = ai_config.get(old_runner_name, {})
if old_config:
old_config = dict(old_config)
if runner_id == OLD_RUNNER_TO_PLUGIN_RUNNER_ID['local-agent']:
old_config.pop('max-round', None)
return ConfigMigration.normalize_runner_config_for_migration(runner_id, old_config)
return {}
@@ -126,7 +123,7 @@ class ConfigMigration:
runner_id: str,
runner_config: dict[str, typing.Any],
) -> dict[str, typing.Any]:
"""Normalize released legacy runner config before storing binding config.
"""Normalize released legacy runner config before storing Agent/runner config.
Runtime code should not carry aliases. This helper is intentionally used
only by config migration so AgentRunner implementations can consume the

View File

@@ -22,7 +22,7 @@ class AgentTrigger(typing.TypedDict):
"""Agent trigger information."""
type: str
source: str # 'pipeline' or 'event_router'
source: str
timestamp: int | None
@@ -37,7 +37,6 @@ class ConversationContext(typing.TypedDict):
bot_id: str | None
workspace_id: str | None
session_id: str | None
pipeline_uuid: str | None
class AgentInput(typing.TypedDict):
@@ -128,8 +127,9 @@ class AgentRunContextPayload(typing.TypedDict):
Protocol v1 structure - matches SDK AgentRunContext.
Note: The 'config' field contains the binding config from ai.runner_config[runner_id],
which is Pipeline's configuration for this specific runner binding (not plugin instance config).
Note: The 'config' field contains the current Agent/runner config
from ai.runner_config[runner_id] while Pipeline remains the temporary
configuration container. It is not plugin instance config.
"""
run_id: str
@@ -144,9 +144,9 @@ class AgentRunContextPayload(typing.TypedDict):
context: dict[str, typing.Any] # ContextAccess - REQUIRED for Protocol v1
state: AgentRunState
runtime: AgentRuntimeContext
config: dict[str, typing.Any] # Binding config from ai.runner_config[runner_id]
config: dict[str, typing.Any] # Agent/runner config from ai.runner_config[runner_id]
bootstrap: dict[str, typing.Any] | None # Optional bootstrap context
adapter: dict[str, typing.Any] | None # Pipeline adapter context
adapter: dict[str, typing.Any] | None # Entry adapter context
metadata: dict[str, typing.Any] # Additional metadata
@@ -160,7 +160,7 @@ class AgentRunContextBuilder:
- Build input from event
- Build state snapshot from PersistentStateStore
- Build runtime context with host info, trace_id, deadline
- Set config from runner binding configuration.
- Set config from current Agent/runner configuration.
Pipeline Query adaptation belongs to PipelineAdapter, not this builder.
"""
@@ -184,7 +184,7 @@ class AgentRunContextBuilder:
Args:
event: Event envelope
binding: Agent binding configuration
binding: Agent binding
descriptor: Runner descriptor
resources: Built resources
@@ -205,7 +205,7 @@ class AgentRunContextBuilder:
conversation: ConversationContext | None = None
if event.conversation_id:
conversation = {
'session_id': None, # Pipeline adapter field
'session_id': None,
'conversation_id': event.conversation_id,
'thread_id': event.thread_id,
'launcher_type': None, # Will be filled from actor/subject if needed
@@ -213,7 +213,6 @@ class AgentRunContextBuilder:
'sender_id': event.actor.actor_id if event.actor else None,
'bot_id': event.bot_id,
'workspace_id': event.workspace_id,
'pipeline_uuid': binding.pipeline_uuid, # Pipeline adapter field
}
# Build event context (Protocol v1 event-first)
@@ -277,7 +276,7 @@ class AgentRunContextBuilder:
'model_context_window_tokens': None,
# TODO(model-info): populate model_context_window_tokens after
# LiteLLM/model metadata lands. Runners fall back to their
# binding config until Host can provide the real window.
# ctx.config until Host can provide the real window.
},
}
@@ -295,7 +294,6 @@ class AgentRunContextBuilder:
# Build adapter context (empty for event-first)
adapter_context = {
'query_id': None,
'pipeline_uuid': binding.pipeline_uuid,
'extra': {},
}

View File

@@ -15,7 +15,7 @@ class RunnerNotFoundError(AgentRunnerError):
class RunnerNotAuthorizedError(AgentRunnerError):
"""Runner not authorized for this pipeline."""
"""Runner not authorized for this binding."""
def __init__(self, runner_id: str, bound_plugins: list[str] | None):
self.runner_id = runner_id
self.bound_plugins = bound_plugins
@@ -34,4 +34,4 @@ class RunnerExecutionError(AgentRunnerError):
def __init__(self, runner_id: str, message: str, retryable: bool = False):
self.runner_id = runner_id
self.retryable = retryable
super().__init__(f'Agent runner {runner_id} execution failed: {message}')
super().__init__(f'Agent runner {runner_id} execution failed: {message}')

View File

@@ -73,11 +73,11 @@ class AgentEventEnvelope(pydantic.BaseModel):
class BindingScope(pydantic.BaseModel):
"""Scope for agent binding."""
scope_type: typing.Literal["bot", "pipeline", "workspace", "global"] = "pipeline"
scope_type: typing.Literal["agent", "bot", "workspace", "global"] = "agent"
"""Scope type."""
scope_id: str | None = None
"""Scope identifier (bot_uuid, pipeline_uuid, etc.)."""
"""Scope identifier (agent_id, bot_uuid, etc.)."""
class ResourcePolicy(pydantic.BaseModel):
@@ -153,7 +153,7 @@ class AgentBinding(pydantic.BaseModel):
"""Runner ID to invoke."""
runner_config: dict[str, typing.Any] = pydantic.Field(default_factory=dict)
"""Runner binding configuration."""
"""Current Agent/runner configuration."""
resource_policy: ResourcePolicy = pydantic.Field(default_factory=ResourcePolicy)
"""Resource policy."""
@@ -167,6 +167,5 @@ class AgentBinding(pydantic.BaseModel):
enabled: bool = True
"""Whether binding is enabled."""
# Fields for Pipeline adapter
pipeline_uuid: str | None = None
"""Pipeline UUID (for Pipeline adapter)."""
agent_id: str | None = None
"""Host-side Agent/config identifier for this binding."""

View File

@@ -93,9 +93,9 @@ class AgentRunOrchestrator:
Args:
event: Event envelope from event gateway
binding: Agent binding configuration
binding: Agent binding
bound_plugins: Optional list of bound plugin identities for authorization
adapter_context: Optional adapter context from Pipeline adapter
adapter_context: Optional context from an entry adapter
Yields:
Message or MessageChunk for pipeline response
@@ -125,12 +125,12 @@ class AgentRunOrchestrator:
resources=resources,
)
# Merge adapter context if provided (for Pipeline adapter)
# Merge adapter context if provided
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 Pipeline adapter consumers.
# Merge prompt into adapter.extra for transitional adapter consumers.
if 'prompt' in adapter_context:
context['adapter']['extra']['prompt'] = adapter_context['prompt']
# Set query_id if provided
@@ -420,7 +420,7 @@ class AgentRunOrchestrator:
Args:
result_dict: Raw result dict with type='state.updated'
event: Event envelope
binding: Agent binding configuration
binding: Agent binding
descriptor: Runner descriptor
"""
data = result_dict.get('data', {})

View File

@@ -114,12 +114,11 @@ class PipelineAdapter:
pipeline_config = query.pipeline_config or {}
ai_config = pipeline_config.get('ai', {})
runner_config = ai_config.get('runner_config', {}).get(runner_id, {})
pipeline_uuid = getattr(query, 'pipeline_uuid', None)
agent_id = getattr(query, 'pipeline_uuid', None)
# Build scope
scope = BindingScope(
scope_type="pipeline",
scope_id=pipeline_uuid,
scope_type="agent",
scope_id=agent_id,
)
# Build resource policy from pipeline config
@@ -142,7 +141,7 @@ class PipelineAdapter:
)
return AgentBinding(
binding_id=f"pipeline_{pipeline_uuid or 'default'}_{runner_id}",
binding_id=f"agent_{agent_id or 'default'}_{runner_id}",
scope=scope,
event_types=[runner_events.MESSAGE_RECEIVED],
runner_id=runner_id,
@@ -151,7 +150,7 @@ class PipelineAdapter:
state_policy=state_policy,
delivery_policy=delivery_policy,
enabled=True,
pipeline_uuid=pipeline_uuid,
agent_id=agent_id,
)
@classmethod
@@ -335,9 +334,6 @@ class PipelineAdapter:
# Handle bot_uuid
bot_uuid = getattr(query, 'bot_uuid', None)
# Handle pipeline_uuid
pipeline_uuid = getattr(query, 'pipeline_uuid', None)
return ConversationContext(
conversation_id=str(conversation_id) if conversation_id is not None else None,
thread_id=None,
@@ -347,7 +343,6 @@ class PipelineAdapter:
bot_id=bot_uuid,
workspace_id=None,
session_id=session_id,
pipeline_uuid=pipeline_uuid,
)
@classmethod
@@ -398,7 +393,6 @@ class PipelineAdapter:
"launcher_id": getattr(query, 'launcher_id', None),
"sender_id": str(getattr(query, 'sender_id', '')) if getattr(query, 'sender_id', None) else None,
"bot_uuid": getattr(query, 'bot_uuid', None),
"pipeline_uuid": getattr(query, 'pipeline_uuid', None),
},
)

View File

@@ -23,7 +23,7 @@ class AgentResourceBuilder:
- Apply 3-layer permission filtering:
1. Runner manifest declared permissions
2. Pipeline extensions_preference (bound plugins/MCP servers)
3. Runner binding config selected resources
3. Agent/runner config selected resources
- Build models list from authorized models
- Build tools list from bound plugins/MCP servers
- Build knowledge_bases list from config
@@ -68,7 +68,7 @@ class AgentResourceBuilder:
# Layer 2: Binding resource policy
resource_policy = binding.resource_policy
# Layer 3: Runner binding config
# Layer 3: Agent/runner config
runner_config = binding.runner_config
# Build each resource category
@@ -112,7 +112,7 @@ class AgentResourceBuilder:
# Get additional model UUID grants from resource policy.
allowed_uuids = resource_policy.allowed_model_uuids
# Add model resources from binding config schema
# Add model resources from Agent/runner config schema
await self._append_config_declared_model_resources(
models=models,
seen_model_ids=seen_model_ids,

View File

@@ -36,7 +36,6 @@ class TestMigratePipelineConfig:
assert 'plugin:langbot/local-agent/default' in migrated['ai']['runner_config']
assert migrated['ai']['runner_config']['plugin:langbot/local-agent/default']['knowledge-bases'] == ['kb-uuid']
assert 'knowledge-base' not in migrated['ai']['runner_config']['plugin:langbot/local-agent/default']
assert 'max-round' not in migrated['ai']['runner_config']['plugin:langbot/local-agent/default']
# Expire-time preserved
assert migrated['ai']['runner']['expire-time'] == 0
@@ -270,7 +269,7 @@ class TestResolveRunnerConfig:
"""resolve_runner_config should not read old ai.local-agent at runtime."""
config = {
'ai': {
'local-agent': {'max-round': 15, 'custom-option': 20},
'local-agent': {'custom-option': 20},
},
}
runner_config = ConfigMigration.resolve_runner_config(config, 'plugin:langbot/local-agent/default')
@@ -280,7 +279,7 @@ class TestResolveRunnerConfig:
"""resolve_legacy_runner_config should read old ai.local-agent for migration."""
config = {
'ai': {
'local-agent': {'max-round': 15, 'custom-option': 20},
'local-agent': {'custom-option': 20},
},
}
runner_config = ConfigMigration.resolve_legacy_runner_config(config, 'plugin:langbot/local-agent/default')
@@ -293,7 +292,7 @@ class TestResolveRunnerConfig:
'runner_config': {
'plugin:langbot/local-agent/default': {'custom-option': 25},
},
'local-agent': {'max-round': 10, 'custom-option': 10}, # Old, should be ignored
'local-agent': {'custom-option': 10}, # Old, should be ignored
},
}
runner_config = ConfigMigration.resolve_runner_config(config, 'plugin:langbot/local-agent/default')

View File

@@ -8,7 +8,7 @@ Tests focus on:
from __future__ import annotations
import pytest
from unittest.mock import MagicMock, AsyncMock, patch
from unittest.mock import MagicMock
from langbot.pkg.agent.runner.context_builder import AgentRunContextBuilder
from langbot.pkg.agent.runner.host_models import AgentEventEnvelope, AgentBinding, BindingScope, StatePolicy
@@ -67,7 +67,7 @@ class TestContextAccessStateDetermination:
binding = AgentBinding(
binding_id='binding_001',
runner_id='plugin:test/runner/default',
scope=BindingScope(scope_type='pipeline', scope_id='conv_001'),
scope=BindingScope(scope_type='agent', scope_id='conv_001'),
state_policy=StatePolicy(
enable_state=True,
state_scopes=['conversation', 'actor'],
@@ -88,7 +88,7 @@ class TestContextAccessStateDetermination:
binding = AgentBinding(
binding_id='binding_001',
runner_id='plugin:test/runner/default',
scope=BindingScope(scope_type='pipeline', scope_id='conv_001'),
scope=BindingScope(scope_type='agent', scope_id='conv_001'),
state_policy=StatePolicy(
enable_state=False,
state_scopes=[],
@@ -109,7 +109,7 @@ class TestContextAccessStateDetermination:
binding = AgentBinding(
binding_id='binding_001',
runner_id='plugin:test/runner/default',
scope=BindingScope(scope_type='pipeline', scope_id='conv_001'),
scope=BindingScope(scope_type='agent', scope_id='conv_001'),
state_policy=StatePolicy(
enable_state=True,
state_scopes=[], # Empty scopes - state not available
@@ -177,7 +177,7 @@ class TestContextAccessStateDetermination:
binding = AgentBinding(
binding_id='binding_003',
runner_id='plugin:test/runner/default',
scope=BindingScope(scope_type='pipeline', scope_id='conv_001'),
scope=BindingScope(scope_type='agent', scope_id='conv_001'),
state_policy=StatePolicy(
enable_state=True,
state_scopes=['conversation', 'actor', 'subject', 'runner'],
@@ -226,7 +226,7 @@ class TestBindingWithStatePolicy:
binding = AgentBinding(
binding_id='binding_001',
runner_id='plugin:test/runner/default',
scope=BindingScope(scope_type='pipeline', scope_id='conv_001'),
scope=BindingScope(scope_type='agent', scope_id='conv_001'),
state_policy=StatePolicy(
enable_state=True,
state_scopes=['conversation'],
@@ -260,7 +260,7 @@ class TestContextAccessOtherAPIs:
binding = AgentBinding(
binding_id='binding_001',
runner_id='plugin:test/runner/default',
scope=BindingScope(scope_type='pipeline', scope_id='conv_001'),
scope=BindingScope(scope_type='agent', scope_id='conv_001'),
state_policy=StatePolicy(enable_state=False, state_scopes=[]),
)
@@ -288,7 +288,7 @@ class TestContextAccessOtherAPIs:
binding = AgentBinding(
binding_id='binding_001',
runner_id='plugin:test/runner/default',
scope=BindingScope(scope_type='pipeline', scope_id='conv_001'),
scope=BindingScope(scope_type='agent', scope_id='conv_001'),
state_policy=StatePolicy(enable_state=False, state_scopes=[]),
)
@@ -316,7 +316,7 @@ class TestContextAccessOtherAPIs:
binding = AgentBinding(
binding_id='binding_001',
runner_id='plugin:test/runner/default',
scope=BindingScope(scope_type='pipeline', scope_id='conv_001'),
scope=BindingScope(scope_type='agent', scope_id='conv_001'),
state_policy=StatePolicy(enable_state=False, state_scopes=[]),
)
@@ -342,7 +342,7 @@ class TestContextAccessOtherAPIs:
binding = AgentBinding(
binding_id='binding_001',
runner_id='plugin:test/runner/default',
scope=BindingScope(scope_type='pipeline', scope_id='conv_001'),
scope=BindingScope(scope_type='agent', scope_id='conv_001'),
state_policy=StatePolicy(enable_state=False, state_scopes=[]),
)

View File

@@ -66,11 +66,11 @@ class TestContextValidation:
"""Create a test binding."""
return AgentBinding(
binding_id="binding_1",
scope=BindingScope(scope_type="pipeline", scope_id="pipeline_1"),
scope=BindingScope(scope_type="agent", scope_id="pipeline_1"),
event_types=["message.received"],
runner_id="plugin:test/plugin/runner",
runner_config={"timeout": 300},
pipeline_uuid="pipeline_1",
agent_id="pipeline_1",
enabled=True,
)

View File

@@ -144,17 +144,9 @@ class TestPipelineConfigToBinding:
mock_query, "plugin:test/plugin/runner"
)
assert binding.scope.scope_type == "pipeline"
assert binding.scope.scope_type == "agent"
assert binding.scope.scope_id == mock_query.pipeline_uuid
def test_config_to_binding_does_not_add_host_context_window(self, mock_query):
"""Pipeline binding should not define Host-side context window controls."""
binding = PipelineAdapter.pipeline_config_to_binding(
mock_query, "plugin:test/plugin/runner"
)
assert not hasattr(binding, "max_round")
assert binding.agent_id == mock_query.pipeline_uuid
class TestAgentRunContextProtocolV1:
"""Test AgentRunContext Protocol v1 behavior."""
@@ -238,23 +230,14 @@ class TestAgentRunContextProtocolV1:
assert ctx.bootstrap is None or isinstance(ctx.bootstrap.messages, list)
class TestHostContextWindowNotInProtocol:
"""Test that Host-side context window controls are not in Protocol v1."""
class TestHostManagedHistoryNotInProtocol:
"""Test that Host-managed history payloads are not in Protocol v1."""
def test_context_window_not_in_sdk_context(self):
"""AgentRunContext should not expose Host-side window controls."""
def test_messages_not_in_sdk_context_top_level(self):
"""AgentRunContext should not expose top-level history messages."""
ctx_fields = AgentRunContext.model_fields.keys()
assert "max_round" not in ctx_fields
assert "maxRound" not in ctx_fields
def test_binding_has_no_context_window_field(self, mock_query):
"""Pipeline adapter should not attach context window policy to binding."""
binding = PipelineAdapter.pipeline_config_to_binding(
mock_query, "plugin:test/plugin/runner"
)
assert not hasattr(binding, "max_round")
assert "messages" not in ctx_fields
class TestSDKCapabilitiesProtocolV1:

View File

@@ -56,7 +56,7 @@ def make_binding(runner_id: str = "plugin:test/plugin/runner") -> AgentBinding:
"""Create a test binding."""
return AgentBinding(
binding_id="binding_1",
scope=BindingScope(scope_type="pipeline", scope_id="pipeline_1"),
scope=BindingScope(scope_type="agent", scope_id="pipeline_1"),
event_types=["message.received"],
runner_id=runner_id,
runner_config={},

View File

@@ -347,7 +347,7 @@ async def test_orchestrator_does_not_package_query_messages_into_context(clean_a
ap = FakeApplication(plugin_connector, db_engine)
orchestrator = AgentRunOrchestrator(ap, FakeRegistry(descriptor))
query = make_query()
query.pipeline_config["ai"]["runner_config"][RUNNER_ID]["agent-window"] = 2
query.pipeline_config["ai"]["runner_config"][RUNNER_ID]["custom-option"] = 2
query.messages = [
provider_message.Message(role="user", content="message 1"),
provider_message.Message(role="assistant", content="response 1"),
@@ -361,9 +361,9 @@ async def test_orchestrator_does_not_package_query_messages_into_context(clean_a
assert len(messages) == 1
context = plugin_connector.contexts[0]
assert context["config"]["agent-window"] == 2
assert context["config"]["custom-option"] == 2
assert context["bootstrap"] is None
assert "adapter_messages" not in context["adapter"]
assert set(context["adapter"]) == {"query_id", "extra"}
assert "context_packaging" not in context["runtime"]["metadata"]
assert [message.content for message in query.messages] == [
"message 1",
@@ -538,7 +538,7 @@ class TestPipelineCompatibilityQueryIdInSession:
)
binding = AgentBinding(
binding_id="binding_001",
scope=BindingScope(scope_type="pipeline", scope_id="pipeline_001"),
scope=BindingScope(scope_type="agent", scope_id="pipeline_001"),
event_types=["message.received"],
runner_id=RUNNER_ID,
runner_config={},

View File

@@ -82,8 +82,8 @@ class FakeBinding:
self,
binding_id: str = 'binding_001',
state_policy: StatePolicy | None = None,
scope_type: str = 'pipeline',
scope_id: str = 'pipeline_001',
scope_type: str = 'agent',
scope_id: str = 'agent_001',
):
self.binding_id = binding_id
self.scope = BindingScope(scope_type=scope_type, scope_id=scope_id)