chore: commit workspace changes

This commit is contained in:
huanghuoguoguo
2026-06-10 22:46:13 +08:00
parent d68e9b7c33
commit 14bcd90783
17 changed files with 483 additions and 231 deletions

View File

@@ -2,7 +2,7 @@
本文档是 LangBot Host 与插件 SDK / Runtime / AgentRunner 之间协议合同的**唯一规范来源single source of truth**。
- 本文件描述"稳定接口应是什么",是 normative spec不混入实现进度。实现状态见 [PROGRESS.md](./PROGRESS.md)。
- 本文件描述当前 Protocol v1 稳定合同,不混入验收流水。测试执行入口和 smoke 记录见 [AGENT_RUNNER_QA_GUIDE.md](./AGENT_RUNNER_QA_GUIDE.md),安全发布门槛见 [SECURITY_HARDENING.md](./SECURITY_HARDENING.md)。
- 本文件之外的任何文档**不得重新定义这里的数据结构**,只能引用,例如"见 PROTOCOL_V1 §4.2"。
- Host 内部模型(`AgentEventEnvelope``AgentBinding`、Descriptor、各 Store不属于 SDK 协议,定义在 [HOST_SDK_INFRASTRUCTURE.md](./HOST_SDK_INFRASTRUCTURE.md)。
@@ -57,27 +57,40 @@ Host 调用 Plugin Runtime 获取当前插件暴露的 runner 列表,请求无
```python
class ListAgentRunnersResponse(BaseModel):
runners: list[AgentRunnerManifest]
runners: list[AgentRunnerDiscovery]
class AgentRunnerDiscovery(BaseModel):
plugin_author: str
plugin_name: str
runner_name: str
runner_description: I18nObject | None = None
manifest: AgentRunnerManifest
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。
### 4.2 AgentRunnerManifest
这里的 manifest 指 Runtime 返回给 Host 的 typed runner manifest
```python
class AgentRunnerManifest(BaseModel):
id: str
name: str
label: I18nObject
description: I18nObject | None = None
capabilities: AgentRunnerCapabilities
permissions: AgentRunnerPermissions
context: AgentRunnerContextPolicy
capabilities: AgentRunnerCapabilities = AgentRunnerCapabilities()
permissions: AgentRunnerPermissions = AgentRunnerPermissions()
config_schema: list[DynamicFormItemSchema] = []
metadata: dict[str, Any] = {}
```
- `id` 必须稳定,格式 `plugin:author/name/runner`
- runner id 由 Host 生成,格式 `plugin:author/name/runner`
- `name` 是插件内 runner 名称,例如 `default`
- `config_schema` 只描述绑定配置表单,不代表插件实例状态。
- `capabilities` 是 Host 用于 UI 和资源投影的 typed bool model它不是权限授予。
- `permissions` 是 runner 申请的 LangBot 资源访问上限;实际授权仍必须与 binding policy 求交。
- `metadata` 只放展示、诊断、非稳定扩展信息。
### 4.3 Capabilities
@@ -89,29 +102,21 @@ class AgentRunnerCapabilities(BaseModel):
knowledge_retrieval: bool = False
multimodal_input: bool = False
skill_authoring: bool = False
event_context: bool = True
platform_api: bool = False
interrupt: bool = False
stateful_session: bool = False
self_managed_context: bool = True
```
语义:
model_config = ConfigDict(extra="forbid")
```
- `streaming`: runner 可以返回 `message.delta`
- `tool_calling`: runner 可能调用 Host tool API。
- `knowledge_retrieval`: runner 可能调用 Host knowledge API。
- `multimodal_input`: runner 可以处理非纯文本 input / artifact。
- `skill_authoring`: runner 需要 Host 提供 skill facts 以及 skill authoring tools例如 `activate` / `register_skill`
- `event_context`: runner 理解 event-first 输入。
- `platform_api`: runner 可能请求平台动作。
- `interrupt`: runner 支持取消或中断。
- `stateful_session`: runner 可能维护跨 run 会话状态。
- `self_managed_context`: runner 自己管理 working contextHost 不应默认 inline 历史。
> Capabilities 字段全部是 `bool`。runner 是否寄宿 host-owned state **不在 capabilities 表达**,而通过 `permissions.storage` 声明(见 §4.4),避免出现非 bool 取值
Capabilities 字段全部是 `bool`,未知 key 禁止进入 typed manifest。`event_context``stateful_session``self_managed_context` 等早期草案 key 不属于 Protocol v1 capabilities对应语义由 event-first context 和 runner-owned context 原则表达
### 4.4 Permissions
### 4.4 Permissions 与 Effective Access
```python
class AgentRunnerPermissions(BaseModel):
@@ -121,25 +126,30 @@ class AgentRunnerPermissions(BaseModel):
history: list[Literal["page", "search"]] = []
events: list[Literal["get", "page"]] = []
artifacts: list[Literal["metadata", "read"]] = []
storage: list[Literal["plugin", "workspace", "binding"]] = []
storage: list[Literal["plugin", "workspace"]] = []
files: list[Literal["config", "knowledge"]] = []
platform_api: list[str] = []
model_config = ConfigDict(extra="forbid")
```
Manifest permissions 是 runner 需要的**最大能力**。实际可用资源还要经过 Host binding policy 和当前 run scope 裁剪(三层裁剪见 HOST_SDK §4.5
`platform_api` 不属于当前 permissions。Platform action executor / EBA action 分支落地前runner 只能返回 `action.requested` telemetryHost 不执行平台动作
### 4.5 Context Policy
Runner 实际可用 LangBot 资源来自 Host 在 run 前冻结的授权快照:
```python
class AgentRunnerContextPolicy(BaseModel):
supports_history_pull: bool = True
supports_history_search: bool = False
supports_artifact_pull: bool = True
owns_compaction: bool = True
wants_static_context_refs: bool = True
```text
effective_access = manifest.permissions ∩ binding.resource_policy ∩ current scope/config
```
Host 不使用该声明给 runner inline 历史窗口。默认原则
具体落地
1. `AgentResourceBuilder` 先用 manifest permissions 与 binding resource policy / runner config 求交,生成 `ctx.resources`
2. `AgentContextBuilder` 用 manifest permissions 与 binding state/storage policy 求交,生成 `ctx.context.available_apis`
3. `AgentRunSessionRegistry` 冻结 run-scoped resources 与 available APIs。
4. Runtime handler / `AgentRunAPIProxy` 按 active `run_id`、runner identity、caller plugin identity、resource id、scope、payload size、rate limit 和 deadline 校验每次调用。
反承诺manifest permissions **只约束 LangBot 持有的资源访问**。它不承诺限制外部 harness 的 native shell、文件系统、CLI、MCP、网络或本机权限这些能力由 operator/runtime/sandbox 另行约束,见 HOST_SDK §4.8 与 SECURITY_HARDENING。
默认原则:
- Host 不得默认 inline 全量历史。
- Host 只 inline 当前 event / input 和 context handles。
@@ -263,12 +273,11 @@ class AgentInput(BaseModel):
text: str | None = None
contents: list[ContentElement] = []
attachments: list[ArtifactRef] = []
message_chain: dict[str, Any] | None = None
```
- 文本、多模态、附件都属于当前 event input。
- 大文件、图片、音频、工具大结果应以 artifact ref 传递。
- `message_chain` 是平台兼容字段,不应成为长期稳定依赖
- 平台原始消息链不属于 SDK `AgentInput`;需要诊断时放在 Host 内部 envelope 或 `ctx.adapter.extra` 的一次性兼容字段,不为长期 runner 合同
### 5.7 DeliveryContext
@@ -322,18 +331,12 @@ class ContextAPICapabilities(BaseModel):
```python
class AgentRuntimeContext(BaseModel):
host: str = "langbot"
langbot_version: str | None = None
trace_id: str
deadline_at: float | None = None
locale: str | None = None
timezone: str | None = None
static_refs: dict[str, StaticContextRef] = {}
metadata: dict[str, Any] = {}
```
`static_refs` 用于 KV cache 友好的静态上下文引用system policy、tool schema、resource manifest 的 hash/version。理由见 AGENT_CONTEXT_PROTOCOL §6。
### 5.10 AgentRunState
```python
@@ -366,7 +369,7 @@ class AgentResources(BaseModel):
`skills` 只包含本次 run 中 pipeline-visible 的 skill facts例如 `skill_name``display_name``description`。Host 不把这些 facts 追加到 system prompt也不把它们编排进工具描述runner 可以自行决定是否放入 model prompt、转换成 MCP surface或只在自己的策略层使用。
资源列表是本次 run 的授权结果。History / Event / Artifact 访问通过 permissions、`ctx.context.available_apis` 和 Host 侧 run session 校验控制,不作为可枚举 resource list 暴露。Runner 只能通过 `AgentRunAPIProxy` 访问这些能力。
资源列表是本次 run 的授权结果。History / Event / Artifact 访问通过 `ctx.context.available_apis` 和 Host 侧 run session 校验控制,不作为可枚举 resource list 暴露。Runner 只能通过 `AgentRunAPIProxy` 访问这些能力。
## 7. Result Stream
@@ -387,123 +390,37 @@ ResultType = Literal[
"run.failed",
]
class AgentRunResultBase(BaseModel):
class AgentRunResult(BaseModel):
run_id: str
type: AgentRunResultType
data: dict[str, Any] = {}
sequence: int | None = None
timestamp: int | None = None
metadata: dict[str, Any] = {}
```
`AgentRunResult` 是以下 typed result 的 discriminated union。Host 必须按 `type` 校验对应 `data` 结构;未知 `type` 按 §3 版本演进规则忽略并记录 warning
SDK 当前实现是单一 envelope`type` 枚举 + `data` dict。Payload 由 SDK typed model 构造并 dump但 wire 不改成 discriminated union这样新旧版本偏斜时 Host 仍可按 §3 忽略未知 `type`
Host 边界分级校验:
- `message.delta``message.completed``artifact.created``state.updated``action.requested``run.completed``run.failed` 属于会影响投递或 Host 副作用的严格 payload校验失败时丢弃该 result 并记录 warning。
- `tool.call.started``tool.call.completed` 当前只作为 telemetrypayload 宽松兼容。
- 未知 `type` 忽略并记录 warning。
### 7.2 稳定 result payloads
```python
class AssistantMessageChunk(BaseModel):
role: Literal["assistant"] = "assistant"
content: str | None = None
contents: list[ContentElement] = []
metadata: dict[str, Any] = {}
| type | `data` payload |
| --- | --- |
| `message.delta` | `{ "chunk": MessageChunk }` |
| `message.completed` | `{ "message": Message }` |
| `tool.call.started` | `{ "tool_call_id": str, "tool_name": str, "parameters": dict }` |
| `tool.call.completed` | `{ "tool_call_id": str, "tool_name": str, "result": dict \| None, "error": str \| None }` |
| `artifact.created` | `{ "artifact_type": str, "artifact_id"?: str, "mime_type"?: str, "name"?: str, "size_bytes"?: int, "sha256"?: str, "metadata"?: dict, "content_base64"?: str }` |
| `state.updated` | `{ "scope": "conversation" \| "actor" \| "subject" \| "runner", "key": str, "value": JSONValue }` |
| `action.requested` | `{ "action": str, "target": dict \| None, "payload": dict \| None }` |
| `run.completed` | `{ "finish_reason": str, "message"?: Message }` |
| `run.failed` | `{ "code": str, "error": str, "retryable": bool }` |
class AssistantMessage(BaseModel):
role: Literal["assistant"] = "assistant"
content: str | None = None
contents: list[ContentElement] = []
artifacts: list[ArtifactRef] = []
metadata: dict[str, Any] = {}
class MessageDeltaData(BaseModel):
chunk: AssistantMessageChunk
class MessageCompletedData(BaseModel):
message: AssistantMessage
class ToolCallStartedData(BaseModel):
tool_call_id: str
tool_name: str
parameters: dict[str, Any] = {}
class ToolCallCompletedData(BaseModel):
tool_call_id: str
tool_name: str
result_preview: dict[str, Any] | None = None
error_code: str | None = None
error_message: str | None = None
class ArtifactCreatedData(BaseModel):
artifact: ArtifactRef
class StateUpdatedData(BaseModel):
scope: Literal["conversation", "actor", "subject", "runner", "binding", "workspace"]
key: str
value: JSONValue
class ActionRequestedData(BaseModel):
action: str
target: dict[str, Any]
payload: dict[str, Any] = {}
idempotency_key: str | None = None
approval_hint: str | None = None
class RunCompletedData(BaseModel):
finish_reason: str = "stop"
message: AssistantMessage | None = None
usage: dict[str, Any] = {}
class RunFailedData(BaseModel):
code: str
message: str
retryable: bool = False
details: dict[str, Any] = {}
class MessageDeltaResult(AgentRunResultBase):
type: Literal["message.delta"]
data: MessageDeltaData
class MessageCompletedResult(AgentRunResultBase):
type: Literal["message.completed"]
data: MessageCompletedData
class ToolCallStartedResult(AgentRunResultBase):
type: Literal["tool.call.started"]
data: ToolCallStartedData
class ToolCallCompletedResult(AgentRunResultBase):
type: Literal["tool.call.completed"]
data: ToolCallCompletedData
class ArtifactCreatedResult(AgentRunResultBase):
type: Literal["artifact.created"]
data: ArtifactCreatedData
class StateUpdatedResult(AgentRunResultBase):
type: Literal["state.updated"]
data: StateUpdatedData
class ActionRequestedResult(AgentRunResultBase):
type: Literal["action.requested"]
data: ActionRequestedData
class RunCompletedResult(AgentRunResultBase):
type: Literal["run.completed"]
data: RunCompletedData
class RunFailedResult(AgentRunResultBase):
type: Literal["run.failed"]
data: RunFailedData
AgentRunResult = (
MessageDeltaResult
| MessageCompletedResult
| ToolCallStartedResult
| ToolCallCompletedResult
| ArtifactCreatedResult
| StateUpdatedResult
| ActionRequestedResult
| RunCompletedResult
| RunFailedResult
)
```
`artifact.created.content_base64` 是小 artifact 的 inline 通道Host 解码后写入 ArtifactStore当前 hard cap 是 1 MiB。大 artifact 应使用外部存储 / file key / 后续上传通道,不应塞入 result event。
### 7.3 稳定 result types
@@ -521,14 +438,14 @@ AgentRunResult = (
`action.requested` 是为 EBA 和 platform API 保留的协议表面:本分支 Host 收到后只记 telemetry**不执行**runner 作者不应在当前 Host 底座中依赖其副作用。真实执行器由外部 EBA / platform action 分支接入;执行模型见 EVENT_BASED_AGENT §6。
Host 必须校验 `state.updated` 的 scope、key、value 大小和 JSON 可序列化性。`action.requested` 如果请求未来会产生外部副作用runner 必须提供稳定 `idempotency_key`;本分支 Host 仍只记录 telemetry。
Host 必须校验 `state.updated` 的 scope、key、value 大小和 JSON 可序列化性。本分支 `action.requested` 仍只记录 telemetry。
### 7.4 Stream delivery semantics
- Host 按 Runtime stream 顺序消费 result。当前 v1 不定义跨连接 replay也不承诺 at-least-once从 Host 视角,收到的 result 最多应用一次。
- `sequence` 是单个 `run_id` 内的结果序号。in-process / stdio 这类天然有序的在线 stream 可以省略;任何会缓冲、重放、跨进程队列或 runtime-managed task 的 transport 必须提供从 1 开始严格递增的 `sequence`
- Host 看到已提供 `sequence` 的 result 时,应按 `(run_id, sequence)` 做重复检测,并在缺号或乱序时记录 warning除非 transport 明确声明 replay 语义Host 不应自行等待缺失序号重排用户可见输出。
- `run.failed.data.retryable` 只表示整次 run 理论上可由上层重试Protocol v1 不自动重试 run也不自动重试 proxy action。任何未来自动重试的 side-effecting action 必须依赖 `idempotency_key` 或等价 Host-owned 去重键。
- `run.failed.data.retryable` 只表示整次 run 理论上可由上层重试Protocol v1 不自动重试 run也不自动重试 proxy action。
- History / Event / Transcript cursor 是 opaque token。runner 不得解析 cursor也不得假设 cursor 在不同 API、conversation、thread 或 retention window 之间可比较;当前实现即使返回数字字符串,也只是实现细节。
### 7.5 示例
@@ -537,7 +454,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": "..."}, "idempotency_key": "run_1:edit:msg_1" } }
{ "type": "action.requested", "data": { "action": "message.edit", "target": {"message_id": "..."}, "payload": {"text": "..."} } }
```
## 8. AgentRunAPIProxy
@@ -569,18 +486,108 @@ await api.event_page(before_cursor=None, limit=50)
# Artifact必须支持大小限制、MIME 校验、过期时间和授权范围)
await api.artifact_metadata(artifact_id)
await api.artifact_read(artifact_id, offset=0, limit=None)
await api.artifact_read_range(artifact_id, offset=0, length=65536)
# State / Storage
await api.state_get(scope, key); await api.state_set(scope, key, value); await api.state_delete(scope, key)
await api.state_list(scope, prefix=None)
await api.get_plugin_storage(key); await api.set_plugin_storage(key, value); await api.delete_plugin_storage(key)
await api.get_plugin_storage_keys()
await api.get_workspace_storage(key); await api.set_workspace_storage(key, value); await api.delete_workspace_storage(key)
await api.get_workspace_storage_keys()
# Files / Host info
await api.get_file(file_key)
await api.get_langbot_version()
```
`state``storage` 的建议边界:`state` 放小型 JSONconversation / actor / runner / binding`storage` 放 blob 或较大数据插件私有数据、workspace 数据、checkpoint
`state``storage` 的建议边界:`state` 放小型 JSONconversation / actor / subject / runner`storage` 放 blob 或较大数据插件私有数据、workspace 数据、checkpoint
返回数据结构(如 `HistoryPage`、artifact metadata见 AGENT_CONTEXT_PROTOCOL §4。
Proxy 返回数据结构也属于本协议:
```python
class TranscriptItem(BaseModel):
transcript_id: str
event_id: str
conversation_id: str | None = None
thread_id: str | None = None
role: str
item_type: str = "message"
content: str | None = None
content_json: dict[str, Any] | None = None
artifact_refs: list[dict[str, Any]] = []
seq: int | None = None
cursor: str | None = None
created_at: int | None = None
metadata: dict[str, Any] = {}
class HistoryPage(BaseModel):
items: list[TranscriptItem] = []
next_cursor: str | None = None
prev_cursor: str | None = None
has_more: bool = False
total_count: int | None = None
class HistorySearchResult(BaseModel):
items: list[TranscriptItem] = []
total_count: int | None = None
query: str
class AgentEventRecord(BaseModel):
event_id: str
event_type: str
event_time: int | None = None
source: str
bot_id: str | None = None
workspace_id: str | None = None
conversation_id: str | None = None
thread_id: str | None = None
actor_type: str | None = None
actor_id: str | None = None
actor_name: str | None = None
subject_type: str | None = None
subject_id: str | None = None
input_summary: str | None = None
input_ref: str | None = None
raw_ref: str | None = None
seq: int | None = None
cursor: str | None = None
created_at: int | None = None
metadata: dict[str, Any] = {}
class EventPage(BaseModel):
items: list[AgentEventRecord] = []
next_cursor: str | None = None
prev_cursor: str | None = None
has_more: bool = False
total_count: int | None = None
class ArtifactMetadata(BaseModel):
artifact_id: str
artifact_type: str
mime_type: str | None = None
name: str | None = None
size_bytes: int | None = None
sha256: str | None = None
source: str
conversation_id: str | None = None
run_id: str | None = None
runner_id: str | None = None
created_at: int | None = None
expires_at: int | None = None
metadata: dict[str, Any] = {}
class ArtifactReadResult(BaseModel):
artifact_id: str
mime_type: str | None = None
size_bytes: int | None = None
offset: int = 0
length: int | None = None
content_base64: str | None = None
file_key: str | None = None
has_more: bool = False
```
## 9. 错误模型
@@ -605,7 +612,7 @@ class AgentAPIError(BaseModel):
Runner 失败使用 `run.failed`
```json
{ "type": "run.failed", "data": { "code": "runner.error", "message": "failed to call external agent", "retryable": false } }
{ "type": "run.failed", "data": { "code": "runner.error", "error": "failed to call external agent", "retryable": false } }
```
## 10. Timeout 与 Cancellation
@@ -624,7 +631,7 @@ Protocol v1 的安全边界在 Host
- SDK 本地校验只提升开发体验,不能替代 Host 校验。
- 所有 resource id 对 runner 来说都是 opaque。
- 默认只能访问当前 conversation / thread 的 history跨会话、workspace 级访问必须额外授权。
- 大 payload 必须 artifact 化。
- 大 payload 必须 artifact 化`artifact.created.content_base64` 只用于小 artifact当前 Host hard cap 是 1 MiB
- Host 必须记录 run_id、runner_id、action、resource、scope、result。
Host 不负责业务编排:不拼接全量历史、不替 runner 做 prompt assembly、不内置 agent memory / tool loop / 上下文压缩策略。这些由官方或第三方 AgentRunner 插件实现。
@@ -659,14 +666,12 @@ entry adapter 只是迁移桥。它负责:
- `AgentRunnerDescriptor.source` 只允许 `plugin`Host 内置 adapter 不能作为 runner source 绕过插件/runtime/proxy 权限链。
- `ctx.resources` 与 proxy action 校验必须来自同一个 run authorization snapshotruntime handler 不应重新执行资源裁剪。
- v1 不要求 Agent、AgentRunner 插件实例或 runner id 全局串行。多个 bot / channel 可复用同一个 Agent并发隔离依赖 `run_id`、binding、conversation / thread scope 和 Host authorization snapshot。
-`stateful_session` runner若外部 runtime 不支持同一 session 并发 turn串行化粒度应是稳定的 external session key例如 workspace / bot / binding / runner / conversation / thread / external session id不是 Agent 或插件实例全局锁。
- 外部 harness runner 当前是 MVP / dev path证明协议可接入不代表发布级安全边界或 Docker 生产可用性完成。
## 14. 开放问题
- `AgentBinding` 是否需要进入 SDK 文档作为只读诊断信息,还是完全 Host 内部。
- `TranscriptItem` 的最小字段集如何定义。
- ArtifactStore 是否复用现有 BinaryStorage backend还是引入独立实体。
- State 与 Storage 的边界是否需要更强类型。
- `platform_api` action 的审批模型如何表达。
- platform action 的审批模型如何表达。
- Host 侧 scoped MCP / skill / workspace projection 是否需要从 runner config 上移为一等 resource projection API。