Fix agent runner steering and lifecycle hardening

This commit is contained in:
huanghuoguoguo
2026-06-12 11:58:09 +08:00
parent 8da5fecfbf
commit 9f95c6bd0d
17 changed files with 547 additions and 28 deletions

View File

@@ -145,6 +145,7 @@ bin/lbs case list
| LA-06 | 多模态 | 发送图片输入。 | `ctx.input.contents` 保留图片;支持视觉模型时正常处理,不支持时受控失败。 |
| LA-07 | fallback / 错误 | 模拟 primary 模型失败或 runner 抛错。 | fallback 或 `run.failed` 行为受控;后续请求不受影响。 |
| LA-08 | 无输出保护 | 测试 runner 完成但不产出消息。 | 不产生空白成功回复;按受控失败或明确缺陷处理。 |
| LA-09 | steering / 运行中追加消息 | 使用支持 steering 的 runner第一条消息触发长 runrun 未结束时在同 conversation 追加第二条消息。 | 第二条消息被 active run claim不启动并发 runrunner 通过 `steering_pull` 看到追加输入EventLog 有 `queued` -> `steering.injected`,若未消费则有 `steering.dropped` 终态;后续普通消息仍可处理。 |
Rerank、remove-think、文件输入等场景只在本次改动直接涉及时补测不作为每轮必跑项。
@@ -195,7 +196,10 @@ Dify、n8n、Coze、DashScope、Langflow、Tbox 等外部服务 runner 不作为
| run_id 结束后复用被拒绝 | session registry 注销测试。 |
| 插件身份不匹配被拒绝 | `caller_plugin_identity` mismatch 测试。 |
| 绑定插件身份的 run_id 省略 caller identity 被拒绝 | `_validate_run_authorization(..., caller_plugin_identity=None)` 返回错误。 |
| 未注册 Runtime 连接伪造插件身份被剥离 | SDK runtime forwarding 测试:请求自带 `caller_plugin_identity` 时,未注册连接转发前必须 `pop`,已注册连接必须覆盖为真实插件身份。 |
| storage/state scope 越权被拒绝 | state/storage proxy 单测。 |
| steering claim 异常不杀 consumer loop | controller 单测:无效 runner / registry 异常只让当前消息回到普通 session 槽位路径,消息消费循环继续。 |
| steering queue 未消费有终态 | session registry / orchestrator 单测队列有上限run unregister 时未 pull 项写 `steering.dropped` 审计。 |
如果这些单测失败,不能用 WebUI 正常回复替代。

View File

@@ -47,7 +47,8 @@ Agent / binding 的持久化形态。
- 新增可选字段保持向后兼容。
- 删除字段或改变既有字段语义,需要在 SDK 发布前完成;发布后应走新的显式兼容方案。
- 结果流演进Host **必须忽略未知 result type 并记录 warning**(除非该 type 明确要求强校验)。新增 result type 不提升大版本。
- 结果流演进Host **必须忽略未知 result type 并记录 warning**(除非该 type 明确要求强校验)。SDK envelope 接收入站未知 `type` 字符串runner 侧可按原字符串转发或忽略;新增 result type 不提升大版本。
- SDK 入站 context 类实体偏宽松,用于兼容 Host 附加的非核心字段manifest、result payload、page/result 返回与错误模型偏严格,未知字段默认禁止。安全边界仍在 HostSDK 校验只提升开发体验。
## 4. Discovery 协议
@@ -65,11 +66,15 @@ class AgentRunnerDiscovery(BaseModel):
runner_name: str
runner_description: I18nObject | None = None
manifest: AgentRunnerManifest
capabilities: AgentRunnerCapabilities # compatibility alias of manifest.capabilities
permissions: AgentRunnerPermissions # compatibility alias of manifest.permissions
config: list[DynamicFormItemSchema] = []
```
`manifest` 是 SDK typed `AgentRunnerManifest`,由 Runtime 从插件组件 manifest 解析并校验后返回。`plugin_author` / `plugin_name` / `runner_name` 保留为 transport 寻址字段Host 以它们生成稳定 runner id并把 `manifest.id` 校验为 `plugin:author/name/runner`。单个 runner manifest 解析失败时 Runtime/Host 记录 warning 并跳过该 runner不影响同一插件或其它插件的 runner discovery。
`capabilities` / `permissions` 顶层字段是兼容旧 discovery 消费方的冗余别名;新代码必须以 `manifest.capabilities` / `manifest.permissions` 为准。
### 4.2 AgentRunnerManifest
这里的 manifest 指 Runtime 返回给 Host 的 typed runner manifest
@@ -247,8 +252,10 @@ class ConversationContext(BaseModel):
thread_id: str | None = None
launcher_type: str | None = None
launcher_id: str | None = None
sender_id: str | None = None
bot_id: str | None = None
workspace_id: str | None = None
session_id: str | None = None
class ActorContext(BaseModel):
actor_type: str
@@ -335,7 +342,7 @@ class ContextAPICapabilities(BaseModel):
```python
class AgentRuntimeContext(BaseModel):
langbot_version: str | None = None
trace_id: str
trace_id: str | None = None
deadline_at: float | None = None
metadata: dict[str, Any] = {}
```
@@ -395,7 +402,7 @@ ResultType = Literal[
class AgentRunResult(BaseModel):
run_id: str
type: AgentRunResultType
type: AgentRunResultType | str
data: dict[str, Any] = {}
sequence: int | None = None
timestamp: int | None = None
@@ -508,7 +515,7 @@ await api.get_langbot_version()
`steering_pull(mode="all")` 是推荐默认Host 按 claim 顺序返回全部 pending steering 输入并清空对应队列。`mode="one-at-a-time"` 仅用于 runner 主动节流每次返回一条。Host 不合并多条用户消息runner 负责在 turn 边界决定模型侧格式。
Steering 审计使用 EventLog 而不是 Transcript schema 扩展:被 active run 吸收的原始 `message.received` 事件保留原事件类型,并在 `metadata.steering` 标记 `status="queued"``trigger_behavior="absorbed_into_active_run"``claimed_by_run_id``claimed_runner_id``claimed_at`。Runner 成功 pull 后Host 追加 `steering.injected` EventLog 记录,`metadata.steering.status="injected"` 并引用 `source_event_id`。Transcript 继续只表示会话事实,不承担 dispatch 行为标记。
Steering 审计使用 EventLog 而不是 Transcript schema 扩展:被 active run 吸收的原始 `message.received` 事件保留原事件类型,并在 `metadata.steering` 标记 `status="queued"``trigger_behavior="absorbed_into_active_run"``claimed_by_run_id``claimed_runner_id``claimed_at`。Runner 成功 pull 后Host 追加 `steering.injected` EventLog 记录,`metadata.steering.status="injected"` 并引用 `source_event_id`若 run 结束时仍有已 claim 但未 pull 的 steering 输入Host 追加 `steering.dropped` EventLog 记录,`metadata.steering.status="dropped"` 并引用 `source_event_id`;这不是用户消息事实的删除,只是 dispatch 终态。Transcript 继续只表示会话事实,不承担 dispatch 行为标记。
`state``storage` 的建议边界:`state` 放小型 JSONconversation / actor / subject / runner`storage` 放 blob 或较大数据插件私有数据、workspace 数据、checkpoint
@@ -650,6 +657,8 @@ class AgentAPIError(BaseModel):
| `invalid_argument` | 参数错误。 |
| `runtime_error` | Host 或下游能力错误。 |
SDK runner-facing proxy 在 Host 返回结构化错误或畸形响应时抛出 `AgentAPIException`,其中 `error` 字段为 `AgentAPIError`。Legacy transport 只返回字符串错误时SDK 使用 `host.action_error` 包装,避免 runner 继续依赖裸 `KeyError` 或字符串匹配。
Runner 失败使用 `run.failed`
```json

View File

@@ -82,7 +82,7 @@ EventGateway / EventRouter 在本文档中描述为 **external EBA branch integr
| [EVENT_BASED_AGENT.md](./EVENT_BASED_AGENT.md) | EBA 接入边界:事件模型、事件来源、触发绑定、非消息事件如何复用 AgentRunner 调度;完整 EventGateway / EventRouter 由外部 EBA 分支联调。 |
| [RUNTIME_CONTROL_PLANE_V2.md](./RUNTIME_CONTROL_PLANE_V2.md) | Agent Platform v2 / runtime 管控面决策:第一阶段优先把 `AgentRun` / `AgentRunEvent` / run control 做成 Host 事实源;完整 runtime registry / daemon 管控是后续可选阶段。**标注为 future design note**。 |
| [OFFICIAL_RUNNER_PLUGINS.md](./OFFICIAL_RUNNER_PLUGINS.md) | 官方 runner 插件迁移,包括 local-agent 和外部 runner。它是下游落地计划不是 LangBot 基础能力设计的前置约束。 |
| [RUN_STEERING_AND_CHECKPOINT.md](./RUN_STEERING_AND_CHECKPOINT.md) | 运行中消息注入steering / follow-up与压缩摘要持久化compaction checkpoint Host 能力缺口设计:来自 local-agent 对照 Pi agent harness 的差距分析。**标注为 future design note**。 |
| [RUN_STEERING_AND_CHECKPOINT.md](./RUN_STEERING_AND_CHECKPOINT.md) | 运行中消息注入steering / follow-up与压缩摘要持久化compaction checkpoint设计与落地状态记录schema 仍以 PROTOCOL_V1 为准。 |
| [STATUS.md](./STATUS.md) | 当前实现状态、spec 与实现已知差距、runner 验收状态和历史高价值记录。 |
| [AGENT_RUNNER_QA_GUIDE.md](./AGENT_RUNNER_QA_GUIDE.md) | Agent Runner QA 指南:保留最高价值测试路径,指导 agent 开展下一轮 WebUI / runner smoke 验证。 |
| [SECURITY_HARDENING.md](./SECURITY_HARDENING.md) | 安全发布级 hardening 的后续发布门槛路径隔离、权限边界、secret、资源配额、MCP / skill 投影和审计。 |

View File

@@ -52,6 +52,8 @@ pi-agent-core 区分两个队列,注入时机都在 turn 边界,不打断进
- **回执**:被 steering 消费的事件通过 EventLog 审计。原始 `message.received`
记录在 `metadata.steering` 标记 queued/absorbed 与 `claimed_by_run_id`
runner 成功 pull 后Host 追加 `steering.injected` 记录并引用源事件。
run 结束时仍未被 pull 的已 claim 输入Host 追加 `steering.dropped` 记录作为
dispatch 终态;原始 Transcript 事实不删除。
Transcript 继续只表示会话事实,不扩展 dispatch 行为字段。
已落地的协议面(最终定义归 PROTOCOL_V1
@@ -61,6 +63,8 @@ pi-agent-core 区分两个队列,注入时机都在 turn 边界,不打断进
pending 输入;`one-at-a-time` 仅作为 runner 主动节流选项。
3. dispatch 层的"认领"规则:`message.received` 可被同 conversation 的 active run
吸收,原事件写 EventLog / Transcriptdispatch 行为写入 EventLog metadata。
4. Host 对单 run steering queue 设置内存上限,队列满时不再 claim 新消息,消息回到
正常 dispatch 路径,避免 active run 无限吞入同会话输入。
### 1.4 边界
@@ -131,7 +135,7 @@ pi-agent-core 把 compaction 条目持久化进 session tree摘要带
| 项 | 归属 | 依赖 |
| --- | --- | --- |
| steering queue、事件认领、基础审计 | LangBot Hostdispatch / binding 层) | 已落地 |
| steering queue、事件认领、基础审计 | LangBot Hostdispatch / binding 层) | 已落地,含队列上限与未消费 dropped 终态 |
| steering pull API + capability 位 | PROTOCOL_V1 + SDK proxy | 已落地 |
| turn 边界拉取与注入 | langbot-local-agent | 已落地 |
| local-agent 对 state API 的 checkpoint 读写 | langbot-local-agent | 已落地 |

View File

@@ -2,7 +2,7 @@
本文档是 `docs/agent-runner-pluginization/` 的状态事实源。协议 schema 仍以 [PROTOCOL_V1.md](./PROTOCOL_V1.md) 为准;测试步骤以 [AGENT_RUNNER_QA_GUIDE.md](./AGENT_RUNNER_QA_GUIDE.md) 为准;安全发布门槛以 [SECURITY_HARDENING.md](./SECURITY_HARDENING.md) 为准。
状态快照日期2026-06-10
状态快照日期2026-06-12
## 实现状态
@@ -17,12 +17,15 @@
| Official runner manifests | Done | `local-agent`、LiteLLM Agent Platform、外部服务 runner 已重新声明真实生效的 LangBot resource permissions。 |
| Runtime Control Plane v2 | Future | 第一阶段设计为 Host-owned Run Ledgerruntime registry / heartbeat / daemon claim 是后续可选阶段。 |
| Full release security gate | Future | self-host / container opt-in 可继续managed/default external harness 需完成 SECURITY_HARDENING full gate。 |
| Steering control path | Done | claim 异常不再逃逸 consumer loopqueue 有上限;未 pull 的 claimed 输入在 run 结束时写 `steering.dropped` 审计终态。 |
| SDK v1 contract closure | Done | SDK 提供 `AgentAPIError` / `AgentAPIException`、typed `SteeringPullResult`、未知 result type 宽容解析、result `sequence` 注入与取消传播。 |
## Spec 与实现已知差距
- `action.requested` 仍只作为 telemetry / reserved surfaceplatform action executor 不在本分支执行。
- EventGateway / EventRouter 完整实现由外部 EBA 分支联调;本分支只提供 event-first host envelope / binding / run 入口。
- State 与 storage 的长期类型边界仍可继续收窄;当前合同只要求 JSON-safe state 与受控 storage API。
- Artifact 读取路径已检查 `expires_at`EventLog / Transcript / Artifact 已提供显式 cleanup primitive长期 retention 默认值、TTL 调度接入和大 payload 去重仍是运维收尾项,应在 Runtime Control Plane Phase 1 前补齐。
- External harness 的 native shell / filesystem / CLI / MCP 权限不受 manifest permissions 约束manifest permissions 只约束 LangBot 持有的资源访问。
- Managed/cloud/default external harness 的 OS/process/network quota、workspace GC、完整 audit/admin control 仍是发布门槛,不是 Protocol v1 已完成能力。