Provider-neutral content-block vocabulary owned by dsh-llm
由 dsh-llm 拥有的提供方无关内容块词汇
The harness needs one internal language for messages that the loop, session log, and all plugins speak.
English
Problem
The harness needs one internal language for messages that the loop, session log, and all plugins speak.
Decision
Own the vocabulary: messages are arrays of typed content blocks (text, reasoning, tool-call, tool-result), with the union derived from the merge-extensible ContentBlockMap so plugins add block types via declaration merging. The same merge-extensible-map pattern types every "stringly" field (MessageSource, FinishReason, TurnTrigger, TurnEndReason). Streaming is a raw chunk protocol; BlockAssembler is the single shared assembly implementation. Adapters translate to provider wire formats — mapping cost lives in adapters, where it belongs.
In-session context injection (context/message) and mid-turn steering originally rendered as tagged user-role envelopes (the system-reminder pattern) rather than a new role, so adapters carry zero burden. Both now project as plain user content with no wrapper; see the injected-content-envelope Agent Note. Live-adapter validation confirms this rendering for current DeepSeek behavior; a future provider-specific mismatch belongs in that adapter rather than a new canonical role.
Alternatives considered
- Mirror the DeepSeek/OpenAI chat-completions shape — zero mapping cost for the first provider, but awkward for rich content (reasoning, tool results as structured blocks).
- Adopt Anthropic's Messages block structure verbatim — battle-tested, but the canonical types would mirror a third-party API the harness does not target first.
Consequences
- Reasoning has a core home without provider-specific shapes.
- Multimodal blocks return only with coordinated adapter, UI, and compaction support; see the drop-image Agent Note.
- Cache hints and assistant prefill remain absent until a shipping adapter can honor them; see the producer-less variants and inert request knobs Agent Notes.
- Every adapter pays a translation cost; the first real adapters have since validated the streaming protocol, and new adapters should continue proving their provider-specific mapping in adapter-local tests.
- IDs that cross package boundaries are branded (
CallId, the shared agent/sessionSessionId) — nominal typing at zero runtime cost.
中文
问题
harness 需要一套统一的内部消息语言,供 agent loop(智能体循环)、会话日志和所有插件共同使用。
决策
自主拥有词汇:消息是类型化内容块的数组(text、reasoning、tool-call、tool-result),其联合类型派生自可合并扩展的 ContentBlockMap,插件通过声明合并添加新的块类型。同一可合并扩展映射模式为所有「字符串化」字段提供类型(MessageSource、FinishReason、TurnTrigger、TurnEndReason)。流式输出采用原始分片协议;BlockAssembler 是唯一的共享组装实现。适配器负责转换为提供方的协议格式(wire format)——映射成本留在适配器中,正是它该在的地方。
会话内上下文注入(context/message)和轮次中途 steering(中途引导)最初渲染为带标签的 user-role 信封(system-reminder 模式),而非引入新角色,因此适配器无需承担额外负担。如今两者都投影为无包装的普通用户内容;见注入内容信封 Agent Note。实际适配器验证已确认此渲染方式符合当前 DeepSeek 的行为;如果未来某提供方出现不兼容,应在该适配器内处理,而非引入新的规范角色。
曾考虑的替代方案
- 镜像 DeepSeek/OpenAI chat-completions 结构:对第一个提供方零映射成本,但对富内容(推理、结构化块形式的工具结果)处理不便。
- 原样采用 Anthropic Messages 块结构:经过实战检验,但规范类型将镜像一个 harness 并非首要对接的第三方 API。
后果
- 推理(reasoning)在核心层有了归属,无需依赖提供方特有的结构。
- 多模态块只有在适配器、UI 和上下文压缩(context compaction)三方协同支持后才会回归;见 drop-image Agent Note。
- 缓存提示与 assistant prefill 在有实际适配器能兑现之前保持缺席;见无生产者的词汇变体与无端到端可用路径的请求旋钮 Agent Note。
- 每个适配器都需承担翻译成本;首批真实适配器已验证了流式输出协议,新适配器应继续在适配器本地测试中验证其提供方特有的映射。
- 跨包边界的 ID 使用品牌类型(
CallId、agent 与会话共享的SessionId)——零运行时开销的名义类型。