mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-23 21:06:10 +00:00
feat(agent): integrate structured runner interactions
This commit is contained in:
@@ -103,6 +103,7 @@ class AgentRunnerCapabilities(BaseModel):
|
||||
skill_authoring: bool = False
|
||||
interrupt: bool = False
|
||||
steering: bool = False
|
||||
interactions: bool = False
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
```
|
||||
@@ -114,6 +115,7 @@ class AgentRunnerCapabilities(BaseModel):
|
||||
- `skill_authoring`:(降级为便捷开关,非访问硬前提)声明该 runner 期望使用 LangBot skill 工具链。skill 本身通过**统一 tool 授权**获得——发现走 `list_skills` / `langbot_list_assets`,激活/注册走 `activate` / `register_skill`,操作走 native exec/read/write,全部计入 `resource_policy.allowed_tool_names`。该 capability 仅作为「一键授权这组 skill tool + sandbox」的便捷开关,不再单独决定 skill 是否可用。
|
||||
- `interrupt`: runner 支持取消或中断。
|
||||
- `steering`: runner 支持在 turn 边界通过 Host pull API 消费同 conversation 在途追加消息。
|
||||
- `interactions`: runner 可以请求 Host 展示结构化交互,并处理后续 `interaction.submitted` 事件。
|
||||
|
||||
Capabilities 字段全部是 `bool`,未知 key 禁止进入 typed manifest。早期草案里的上下文/会话类 capability 已删除;对应语义由 event-first context 和 runner-owned context 原则表达。
|
||||
|
||||
@@ -128,11 +130,15 @@ class AgentRunnerPermissions(BaseModel):
|
||||
events: list[Literal["get", "page"]] = []
|
||||
storage: list[Literal["plugin", "workspace"]] = []
|
||||
files: list[Literal["config", "knowledge"]] = []
|
||||
interactions: list[Literal["request"]] = []
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
```
|
||||
|
||||
平台动作执行不属于当前 permissions。Platform action executor / EBA action 分支落地前,runner 只能返回 `action.requested` telemetry,Host 不执行平台动作。
|
||||
通用交互投递是当前唯一允许执行的 `action.requested` 白名单动作。Runner 必须同时声明
|
||||
`capabilities.interactions=true` 和 `permissions.interactions=["request"]`,Host 还必须将其与
|
||||
当前 binding delivery policy、run authorization snapshot 和 adapter delivery capability 求交。
|
||||
其它平台动作仍不属于当前 permissions,Host 收到后只记录 telemetry,不得执行。
|
||||
|
||||
Runner 实际可用 LangBot 资源来自 Host 在 run 前冻结的授权快照:
|
||||
|
||||
@@ -275,11 +281,13 @@ class AgentInput(BaseModel):
|
||||
text: str | None = None
|
||||
contents: list[ContentElement] = []
|
||||
attachments: list[InputAttachment] = []
|
||||
interaction: InteractionSubmission | None = None
|
||||
```
|
||||
|
||||
- 文本、多模态、附件都属于当前 event input。
|
||||
- 大文件、图片、音频、工具大结果应进入授权 sandbox/workspace,input attachment 只携带轻量 metadata/path/url/content。
|
||||
- 平台原始消息链不属于 SDK `AgentInput`;需要诊断时放在 Host 内部 envelope 或 `ctx.adapter.extra` 的一次性兼容字段中,不作为长期 runner 合同。
|
||||
- `interaction` 只在 `event.event_type == "interaction.submitted"` 时出现,承载经过 Host 校验和归一化的用户提交;平台原始 callback payload 不进入该字段。
|
||||
|
||||
### 5.7 DeliveryContext
|
||||
|
||||
@@ -291,17 +299,80 @@ class DeliveryContext(BaseModel):
|
||||
supports_edit: bool = False
|
||||
supports_reaction: bool = False
|
||||
max_message_size: int | None = None
|
||||
interactions: InteractionDeliveryCapabilities | None = None
|
||||
platform_capabilities: dict[str, Any] = {}
|
||||
```
|
||||
|
||||
Runner 可参考 delivery 能力决定返回 `message.delta`、`message.completed` 或 `action.requested`。
|
||||
Runner 可参考 delivery 能力决定返回 `message.delta`、`message.completed` 或交互请求。
|
||||
`interactions is not None` 表示当前 surface 可以投递结构化交互;其中的 field/action 能力
|
||||
决定 Host 可原生渲染的范围。Runner 仍必须提供 `fallback_text`,供不支持富交互或投递失败时降级。
|
||||
平台事件进入独立 Agent 时,Host 会从当前 adapter 的 `get_supported_apis()` 投影
|
||||
`supports_edit`、`supports_reaction`,并把去重后的 API 名称写入
|
||||
`platform_capabilities.supported_apis`。这些字段只描述当前投递表面的能力,不授予
|
||||
平台动作权限;`action.requested` 仍受 §7.3 的 reserved 约束。合成路由测试使用的
|
||||
任意平台动作权限;交互请求按 §5.8 和 §7.3 的白名单约束执行。合成路由测试使用的
|
||||
adapter 会过滤所有已知副作用 API,因此测试事件不会向 runner 宣告真实出站能力。
|
||||
|
||||
### 5.8 ContextAccess
|
||||
### 5.8 Structured Interaction
|
||||
|
||||
```python
|
||||
InteractionFieldType = Literal[
|
||||
"text", "textarea", "select", "multiselect", "number", "boolean", "file"
|
||||
]
|
||||
|
||||
class InteractionOption(BaseModel):
|
||||
value: str
|
||||
label: str
|
||||
description: str | None = None
|
||||
|
||||
class InteractionField(BaseModel):
|
||||
id: str
|
||||
label: str
|
||||
type: InteractionFieldType
|
||||
required: bool = False
|
||||
options: list[InteractionOption] = []
|
||||
placeholder: str | None = None
|
||||
default: JSONValue | None = None
|
||||
|
||||
class InteractionAction(BaseModel):
|
||||
id: str
|
||||
label: str
|
||||
style: Literal["default", "primary", "danger"] = "default"
|
||||
|
||||
class InteractionRequest(BaseModel):
|
||||
interaction_id: str
|
||||
kind: Literal["form", "confirmation", "choice"] = "form"
|
||||
title: str
|
||||
description: str | None = None
|
||||
fields: list[InteractionField] = []
|
||||
actions: list[InteractionAction] = []
|
||||
expires_at: int | None = None
|
||||
fallback_text: str
|
||||
|
||||
class InteractionSubmission(BaseModel):
|
||||
interaction_id: str
|
||||
action_id: str | None = None
|
||||
values: dict[str, JSONValue] = {}
|
||||
submitted_at: int | None = None
|
||||
|
||||
class InteractionDeliveryCapabilities(BaseModel):
|
||||
field_types: list[InteractionFieldType] = []
|
||||
action_styles: list[Literal["default", "primary", "danger"]] = []
|
||||
supports_updates: bool = False
|
||||
max_fields: int | None = None
|
||||
```
|
||||
|
||||
Runner 使用 `AgentRunResult.interaction_requested()` 生成
|
||||
`action.requested(action="interaction.requested")`。Host 只能把请求投递到当前 run 冻结的
|
||||
delivery target,Runner 不得通过 `target` 改写 bot、conversation 或用户。Host 为请求保存
|
||||
`interaction_id -> processor/binding/conversation/expiry` 关联;平台 callback 必须先经过签名、
|
||||
目标、过期和幂等校验,再生成 `interaction.submitted` 事件。
|
||||
|
||||
Provider 私有 continuation token、workflow id、凭据或原始 callback payload 不属于该协议。
|
||||
Runner 应把这些值保存到受授权的 Host state 或 plugin storage,并只用 `interaction_id` 关联提交。
|
||||
Host 必须使用服务器接收时间判断 callback 是否过期,并覆盖 `InteractionSubmission.submitted_at`;
|
||||
平台事件时间只可作为原始事件诊断信息,不能控制 TTL 或进入 Runner 的可信提交字段。
|
||||
|
||||
### 5.9 ContextAccess
|
||||
|
||||
```python
|
||||
class ContextAccess(BaseModel):
|
||||
@@ -463,13 +534,20 @@ Runner 生成的大文件、工具输出和临时产物不通过 result event
|
||||
| `tool.call.started` | 工具调用开始的可观测事件。 | telemetry |
|
||||
| `tool.call.completed` | 工具调用完成的可观测事件。 | telemetry |
|
||||
| `state.updated` | runner 请求更新 host-owned state。 | ✅ |
|
||||
| `action.requested` | runner 请求 Host 执行平台动作。 | **reserved / 仅 telemetry,不执行** |
|
||||
| `action.requested` | runner 请求 Host 执行受控动作。 | `interaction.requested` 可执行;其它动作仅 telemetry |
|
||||
| `run.completed` | run 正常结束。 | ✅ |
|
||||
| `run.failed` | run 失败。 | ✅ |
|
||||
|
||||
`action.requested` 是为 EBA 和 platform API 保留的协议表面:本分支 Host 收到后只记 telemetry,**不执行**,runner 作者不应在当前 Host 底座中依赖其副作用。真实执行器由外部 EBA / platform action 分支接入;执行模型见 EVENT_BASED_AGENT §6。
|
||||
`action.requested` 仍是严格白名单协议表面。当前 Host 只执行
|
||||
`action="interaction.requested"`,并要求 payload 可校验为 `InteractionRequest`、Runner 声明
|
||||
interaction capability/permission、当前 binding 允许交互且 delivery surface 支持交互。
|
||||
Host 忽略 Runner 提供的外部 target,只使用 run authorization snapshot 中冻结的 delivery target。
|
||||
其它 action 只记 telemetry,不执行;通用 platform action executor 仍不属于本协议。
|
||||
|
||||
Host 必须校验 `state.updated` 的 scope、key、value 大小和 JSON 可序列化性。本分支 `action.requested` 仍只记录 telemetry。
|
||||
Host 必须校验 `state.updated` 的 scope、key、value 大小和 JSON 可序列化性。Host 还必须限制
|
||||
交互字段/动作数量、字符串长度、payload 总大小、过期时间和 interaction id 唯一性,并对 callback
|
||||
应用默认 30 分钟、最长 24 小时的有效期;request 与 submission payload 均不得超过 256 KiB。
|
||||
执行目标绑定、签名、幂等和过期校验。
|
||||
|
||||
除 runner 经 `state.updated` 写之外,Host 自身也可直接写部分 host-owned state。例如 `activate` tool 在 Host 侧执行时,直接把已激活 skill 写入 conversation scope 的 `host.activated_skills` 快照。当 host 直接写与 runner `state.updated` 写到同一 key 时,按 **last-write-wins** 合并——runner 可以覆盖 host 写的快照。
|
||||
|
||||
@@ -487,7 +565,7 @@ Host 必须校验 `state.updated` 的 scope、key、value 大小和 JSON 可序
|
||||
{ "type": "message.delta", "data": { "chunk": { "role": "assistant", "content": "hel" } } }
|
||||
{ "type": "message.completed", "data": { "message": { "role": "assistant", "content": "hello" } } }
|
||||
{ "type": "state.updated", "data": { "scope": "conversation", "key": "external.session_id", "value": "abc" } }
|
||||
{ "type": "action.requested", "data": { "action": "message.edit", "target": {"message_id": "..."}, "payload": {"text": "..."} } }
|
||||
{ "type": "action.requested", "data": { "action": "interaction.requested", "payload": { "interaction_id": "form_1", "kind": "choice", "title": "Approve?", "actions": [{"id": "approve", "label": "Approve", "style": "primary"}], "fallback_text": "Reply approve or reject." } } }
|
||||
```
|
||||
|
||||
## 8. AgentRunAPIProxy
|
||||
|
||||
@@ -21,11 +21,12 @@
|
||||
| Steering control path | Done | claim 异常不再逃逸 consumer loop;queue 有上限;未 pull 的 claimed 输入在 run 结束时写 `steering.dropped` 审计终态。 |
|
||||
| SDK v1 contract closure | Done | SDK 提供 `AgentAPIError` / `AgentAPIException`、typed `SteeringPullResult`、未知 result type 宽容解析、result `sequence` 注入与取消传播。 |
|
||||
| EBA processor routing | Done; release gate 5/5 pass | Bot `event_bindings`、Pipeline / Agent 平级路由、WebUI dry-run / 合成测试 / 状态、OneBot 非消息事件到 Agent 及平台回复已闭环;隔离空白实例已验证从 Space 安装并注册 LocalAgent。 |
|
||||
| Structured interactions | Cross-repo unit-pass; provider E2E pending | Host 已完成 `interaction.requested` 白名单、持久化 callback correlation、TTL/作用域/幂等校验和 Pipeline/Agent 原处理器恢复;六个平台已接入按钮/单选投递,Lark 和 DingTalk 进一步支持原生单字段 `text` / `textarea` / `number` / `select` 控件。SDK typed contract、通用 Runner 脚手架和 DifyAgent `workflow_paused` plugin-storage continuation 已落入对应仓库。 |
|
||||
|
||||
## Spec 与实现已知差距
|
||||
|
||||
- `action.requested` 仍只作为 telemetry / reserved surface;platform action executor 不在本分支执行。
|
||||
- `action.requested` 权限模型完成前,DeliveryContext 的 adapter capability 投影只用于输出决策,不提供平台动作执行权限。
|
||||
- `action.requested` 是严格白名单协议面:当前只执行 `interaction.requested`;其它 action 仍只记录 telemetry,不提供通用 platform action executor。
|
||||
- 结构化交互 SDK typed contract 与 DifyAgent continuation 已实现;SDK 正式发布、真实 Dify 凭据 E2E,以及需要长驻双向进程的 Claude Code 权限确认仍是后续验收项。Host 不持有 provider 私有 token。
|
||||
- State 与 storage 的长期类型边界仍可继续收窄;当前合同只要求 JSON-safe state 与受控 storage API。
|
||||
- `ToolResource.parameters` 已作为 best-effort full schema 由 Host 在构造 `ctx.resources` 时一次塞齐;无 schema 时 runner 仍需兼容 `parameters=None` 或按需调用 detail API。
|
||||
- EventLog / Transcript 已提供显式 cleanup primitive;长期 retention 默认值、TTL 调度接入和 sandbox/workspace 文件清理仍是运维收尾项,应在 Runtime Control Plane 产品化前补齐。
|
||||
@@ -40,7 +41,8 @@
|
||||
| `plugin:langbot-team/LocalAgent/default` | Unit-pass; Marketplace UI pass; Debug Chat E2E pass | 2026-07-12 隔离 first-run 实例从真实 AgentRunner catalog 安装 `langbot-team/LocalAgent` 0.1.0,Host 注册 `plugin:langbot-team/LocalAgent/default`,Wizard 自动选中并解锁后续操作。2026-07-15 `2026-07-15-08-44-10-770-08-00-sandbox-skill-authoring-edit-existing-e2e` 使用真实 `gpt-5.5` 完成 Skill 创建、注册、同 Query 激活、已激活包编辑与脚本执行;三阶段 UI、浏览器诊断和结构化文件系统检查全部通过,每阶段恰好新增一个 Bot 气泡,p95 14.6 秒、错误率 0。 |
|
||||
| `plugin:langbot-team/ACPAgentRunner/default` | Unit-pass; Debug Chat E2E pass | 2026-07-15 从本地 0.1.4 发布包安装并注册 PascalCase runner,remote-ssh Claude ACP 通过反向隧道调用 run-scoped `langbot_get_current_event`,97.8 秒返回可见结果;Host 将增量 delta 和 `message.completed` 聚合为一个完整 Bot 气泡。 |
|
||||
| `plugin:langbot-team/ClaudeCodeAgent/default` / `plugin:langbot-team/CodexAgent/default` | Unit-pass; E2E pending | 通过 runner 仓库单测覆盖 session、run_id 注入和 LangBot MCP gateway;真实 harness E2E 取决于对应运行环境、CLI/daemon 可用性和 provider 登录态。 |
|
||||
| Dify / n8n / Coze / DashScope / Langflow / Tbox / DeerFlow / WeKnora | Unit-pass; credential smoke optional | 2026-06-13 plugin layout / parser tests 通过;真实服务凭据 smoke 非每轮必跑。 |
|
||||
| Dify | Human-input unit-pass; credential E2E pending | `langbot-agent-runner/dify-agent` 已实现 `workflow_paused`、原子字段/确认交互、plugin-storage continuation、Dify submit/events 恢复与再次暂停;真实 Dify 凭据 E2E 待执行。 |
|
||||
| n8n / Coze / DashScope / Langflow / Tbox / DeerFlow / WeKnora | Unit-pass; credential smoke optional | 2026-06-13 plugin layout / parser tests 通过;真实服务凭据 smoke 非每轮必跑。 |
|
||||
|
||||
## Host / SDK 验收状态
|
||||
|
||||
|
||||
Reference in New Issue
Block a user