DSH / Atlas
2026-06-20implementedsimplificationarchived 2026-07-26

Drop unconsumed assembled LLM convenience surfaces

移除未被消费的 LLM 组装便捷接口

`LlmService` ([packages/llm/llm/src/index.ts](../../../../packages/llm/llm/src/index.ts)) exposes three call surfaces over a model: - `stream()` — raw `StreamChunk`s, dispatched through the `llm/stream` waterfall. - `streamBlocks()` — a "convenience view" that runs the chunks through a `BlockAssembler` and yields completed `ContentBlock`s in stream order ([index.ts:137-144](../../../../packages/llm/llm/src/index.ts))

English

Problem

LlmService (packages/llm/llm/src/index.ts) exposes three call surfaces over a model:

  • stream() — raw StreamChunks, dispatched through the llm/stream waterfall.
  • streamBlocks() — a "convenience view" that runs the chunks through a BlockAssembler and yields completed ContentBlocks in stream order (index.ts:137-144).
  • generate() — one fully-assembled GenerateResult, dispatched through a second llm/generate waterfall (index.ts:151-157).

The only production consumer of the LLM service is the agent loop, and it uses stream() exclusively — feeding raw chunks through its own BlockAssembler so it can log chunks for replay fidelity while assembling in parallel (packages/core/agent-loop/src/loop.ts, the ctx.llm.stream(req) step). Grepping streamBlocks and ctx.llm.generate across packages/*/src and examples/*/src finds no production callers. The references are the service methods, docs, and tests; adapter tests use generate() as a convenient driver, but they can hand-drain stream() through the same assembler helper without preserving a public production API.

This is the drop-mutable-session-summary pattern: assembled-view APIs with tested contracts, consumed by tests rather than production. They were built speculatively for consumers that do not care about token-level deltas, but the one real consumer cares about deltas precisely so it can persist high-fidelity replay data.

streamBlocks() drags a dedicated slice of BlockAssembler behind it: flushReady() and flushRemaining() (packages/llm/llm/src/assembler.ts:138-168) plus the flushed cursor field exist only to support incremental in-order yield. generate() drags GenerateResult, BlockAssembler.result(), and the llm/generate waterfall as a second interception surface over the same underlying stream. The loop's assembler usage is push() / message() / usage / finish — not streaming flush or one-shot service assembly.

Decision

stream() is the sole public LLM call surface. Remove streamBlocks, generate, its event/result types, and assembler helpers used only by that path. Adapter tests assemble the public stream through a local helper, while BlockAssembler retains only the operations with production consumers.

Alternatives considered

Keep generate() as a test-only convenience — rejected: adapter tests hand-draining stream() through the shared assembler exercise the same streaming path production uses, and a public method whose only callers are tests is exactly the dead-surface shape the drop-mutable-summary precedent retired. A future consumer that wants assembled blocks without deltas reintroduces a focused helper with that consumer.

Verification

streamBlocks, generate, llm/generate, and the assembler helpers they alone required are gone with no new dead exports; both real adapters are exercised through stream() and the shared assembler; the loop behaves identically (ACP snapshot expected outputs unchanged); and the README, architecture doc, and module docs carry no mention of the removed surfaces.

Consequences

  • It removes public methods from a core vocabulary package. A future plugin that wants assembled blocks without deltas would need to call stream() and use BlockAssembler directly or reintroduce a focused helper with a real consumer. Given the pre-release "foundation over speculative future" stance (AGENTS.md), this is the right time to cut test-only public shape.
  • Adapter tests get a little more explicit. They lose the ergonomic generate() wrapper, but that is useful pressure: tests exercise the same streaming path production uses.
  • Waterfall users lose llm/generate. No production listener exists. Any future caching/retry/logging plugin should wrap llm/stream, which remains the single provider call path.

The size is modest, but it is a clean removal of speculative surface area from the LLM package, leaving one model-call contract for both production and tests.

中文

问题

LlmServicepackages/llm/llm/src/index.ts)在模型之上暴露了三个调用接口:

  • stream():原始 StreamChunk,通过 llm/stream waterfall(瀑布式事件)分发。
  • streamBlocks():一个「便捷视图」,将分片送入 BlockAssembler 并按流顺序产出已组装的 ContentBlockindex.ts:137-144)。
  • generate():一个完整组装的 GenerateResult,通过第二条 llm/generate waterfall 分发(index.ts:151-157)。

LLM(大语言模型)服务唯一的生产消费方是 agent loop(智能体循环),它只使用 stream():将原始分片送入自己的 BlockAssembler,以便在并行组装的同时记录分片,保证回放保真度(packages/core/agent-loop/src/loop.tsctx.llm.stream(req) 步骤)。在 packages/*/srcexamples/*/src 中 grep streamBlocksctx.llm.generate,找不到任何生产调用方。仅有的引用来自服务方法定义、文档和测试;适配器测试用 generate() 作为便捷驱动,但它们完全可以通过同一个 assembler 辅助函数手动消费 stream(),无需为此保留一个公开的生产 API。

这属于删除可变会话 summary 的同类模式:带有受测契约的组装视图 API,由测试而非生产代码消费。它们是为不关心 token 级增量的消费方推测性构建的,但唯一的真实消费方恰恰关心增量,以便持久化高保真重放数据。

streamBlocks() 拖带了 BlockAssembler 的一块专用逻辑:flushReady()flushRemaining()packages/llm/llm/src/assembler.ts:138-168)以及 flushed 游标字段,仅为支持按序增量产出而存在。generate() 拖带了 GenerateResultBlockAssembler.result() 以及 llm/generate waterfall——在同一底层流之上的第二个拦截面。agent loop 对 assembler 的使用仅限于 push() / message() / usage / finish,不涉及流式 flush 或一次性服务组装。

决策

stream() 是唯一的公开 LLM 调用接口。移除 streamBlocksgenerate、其事件/结果类型,以及仅被该路径使用的 assembler 辅助方法。适配器测试通过本地辅助函数对公开流进行组装;BlockAssembler 仅保留有生产消费方的操作。

曾考虑的替代方案

保留 generate() 作为仅供测试的便捷方法:否决。适配器测试通过共享 assembler 手动消费 stream(),走的是与生产完全相同的流式路径;一个唯一调用方只有测试的公开方法,正是 drop-mutable-summary 先例所淘汰的死接口形态。未来如果有消费方需要不带增量的组装块,届时再为该消费方引入一个聚焦的辅助方法。

验证

streamBlocksgeneratellm/generate 及仅供它们使用的 assembler 辅助函数均已移除,且未产生新的无用导出;两个真实适配器都通过 stream() 和共享 assembler 接受测试;循环行为保持一致(ACP(Agent Client Protocol)快照预期输出未变);README、架构文档和模块文档也不再提及已删除表面。

后果

  • 从一个核心词汇包中移除了公开方法。 未来如果有插件需要不带增量的组装块,它需要直接调用 stream() 并使用 BlockAssembler,或在有真实消费方时重新引入一个聚焦的辅助方法。鉴于预发布阶段「基础优先于预设未来」的立场(AGENTS.md),现在正是裁剪仅供测试的公开接口的合适时机。
  • 适配器测试变得更显式。 它们失去了便捷的 generate() 包装层,但这是有益的压力:测试走的是与生产相同的流式路径。
  • waterfall 使用者失去 llm/generate 不存在生产监听者。未来的缓存/重试/日志插件应包装 llm/stream,它仍然是唯一的提供方调用路径。

改动规模不大,但它从 LLM 包中干净地移除了预设的接口面积,为生产和测试留下唯一一份模型调用契约。