feat(agent): integrate structured runner interactions

This commit is contained in:
fdc310
2026-07-22 11:12:26 +08:00
parent e3df35e7f7
commit 6b13c37c3b
58 changed files with 6153 additions and 319 deletions
+86 -8
View File
@@ -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` telemetryHost 不执行平台动作。
通用交互投递是当前唯一允许执行的 `action.requested` 白名单动作。Runner 必须同时声明
`capabilities.interactions=true``permissions.interactions=["request"]`Host 还必须将其与
当前 binding delivery policy、run authorization snapshot 和 adapter delivery capability 求交。
其它平台动作仍不属于当前 permissionsHost 收到后只记录 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/workspaceinput 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 targetRunner 不得通过 `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