Merge pull request #973 from deepseek-harness/feat/read-presenter

feat(fs): add a read render-intent card for the read tool result
This commit is contained in:
Chinesezjc
2026-07-31 14:30:31 +08:00
committed by GitHub
33 changed files with 537 additions and 62 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/core/tools/README.md
README.md: e7f395f8c1d6417db856e590f5267cf6887e4d12
README.zh.md: acb4c047bf86e36c828882ff751d4be1f627f99e
README.md: dcce455f9551318f3871e3df84c29789078fef7c
README.zh.md: 63eaa2e0b66797c74a1d4845c29ca970b63f5f99

View File

@@ -108,7 +108,7 @@ Optional `isConcurrencySafe(args)` receives typed, softly validated arguments. E
Tools optionally own pure `presentCall()` and `presentResult()` render intents, so UIs do not special-case tool names:
- Call views are `{ card: 'generic', title, kind?, rawInput?, content?, locations? }`, `{ card: 'terminal', title, description?, cwd? }`, or `{ card: 'diff', title, diffs, locations? }`.
- Result views are `{ card: 'generic', title?, content? }`, `{ card: 'terminal', title?, output?, exitCode?, signal? }`, `{ card: 'diff', title?, diffs }`, or `{ card: 'web', kind: 'search' | 'fetch', title?, … }` (a completed web retrieval; the `kind` arms carry the structured search sources or the fetch summary, and a UI without the `web` capability falls back to the raw result content).
- Result views are `{ card: 'generic', title?, content? }`, `{ card: 'terminal', title?, output?, exitCode?, signal? }`, `{ card: 'diff', title?, diffs }`, `{ card: 'read', title?, path, offset, lines, totalLines, lang?, content? }` (a completed file read → a line-numbered, optionally syntax-highlighted code view; `offset` is the 1-based first line the window requested, kept even when `lines` is empty; `lines` is `{ number, text }[]` keeping each file line number, and `content` is the envelope-stripped text a UI without read support falls back to), or `{ card: 'web', kind: 'search' | 'fetch', title?, … }` (a completed web retrieval; the `kind` arms carry the structured search sources or the fetch summary, and a UI without the `web` capability falls back to the raw result content).
Returning `undefined` selects generic fallback. Presenters depend only on their arguments and the durable result because UIs call them during live streaming and log replay. `output.presentationMeta(args, value)` derives JSON metadata for direct surface calls; that metadata persists with `tool/result` and returns to `presentResult`, while the canonical value itself remains execution-local and is never replayed. Nested Code dispatches do not compute metadata. `defineTool` soft-validates older logged arguments and falls back instead of crashing replay. `dsh-tool-bash` and `dsh-tool-fs` are the reference implementations; the [canonical-output Agent Note](../../../.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md) owns the value/presentation split and the [render-intent Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md) owns card vocabulary.

View File

@@ -108,7 +108,7 @@ ctx.tools.register(defineTool({
工具可以选择拥有纯 `presentCall()``presentResult()` 呈现意图,使 UI 无需特殊处理工具名称:
- 调用视图为 `{ card: 'generic', title, kind?, rawInput?, content?, locations? }``{ card: 'terminal', title, description?, cwd? }``{ card: 'diff', title, diffs, locations? }`
- 结果视图为 `{ card: 'generic', title?, content? }``{ card: 'terminal', title?, output?, exitCode?, signal? }``{ card: 'diff', title?, diffs }` `{ card: 'web', kind: 'search' | 'fetch', title?, … }`(已完成的 web 检索;`kind` 各分支携带结构化的搜索来源或抓取摘要,不具备 `web` 能力的 UI 回退到原始结果内容)。
- 结果视图为 `{ card: 'generic', title?, content? }``{ card: 'terminal', title?, output?, exitCode?, signal? }``{ card: 'diff', title?, diffs }``{ card: 'read', title?, path, offset, lines, totalLines, lang?, content? }`(已完成的文件读取→带行号、可选语法高亮的代码视图;`offset` 是窗口请求的 1-based 起始行,即使 `lines` 为空也保留;`lines``{ number, text }[]`,保留每一行的文件行号,`content` 是无读取能力的 UI 回退时使用的去信封文本)`{ card: 'web', kind: 'search' | 'fetch', title?, … }`(已完成的 web 检索;`kind` 各分支携带结构化的搜索来源或抓取摘要,不具备 `web` 能力的 UI 回退到原始结果内容)。
返回 `undefined` 会选择通用回退。呈现器只依赖其参数和持久结果,因为 UI 会在实时流式输出和日志回放期间调用它们。`output.presentationMeta(args, value)` 为直接接口调用派生 JSON 元数据;该元数据随 `tool/result` 持久化并传回 `presentResult`,而规范值本身仍只存在于执行局部,绝不会回放。嵌套 Code 分发不会计算元数据。`defineTool` 会软验证较旧的日志参数并回退,而不会使回放崩溃。`dsh-tool-bash``dsh-tool-fs` 是参考实现;[规范输出 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md) 规定值/呈现拆分,[呈现意图 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md) 规定卡片词汇。

View File

@@ -74,6 +74,7 @@ export type {
ToolCallKind,
FileLocation,
FileDiff,
ReadFileLine,
ToolCallView,
GenericCallView,
TerminalCallView,
@@ -82,6 +83,7 @@ export type {
GenericResultView,
TerminalResultView,
DiffResultView,
ReadResultView,
WebResultView,
WebSearchResultView,
WebFetchResultView,

View File

@@ -117,6 +117,18 @@ export interface DiffCallView {
locations?: FileLocation[]
}
/**
* One numbered line of a file, the unit a {@link ReadResultView} carries so a
* capable UI can render a syntax-highlighted, line-numbered code view. `number`
* is the 1-based line number in the file (a window past `offset` keeps the file's
* own numbering, not a 1-based re-count); `text` is the line without its trailing
* newline, already truncated to the read tool's per-line cap.
*/
export interface ReadFileLine {
number: number
text: string
}
/**
* How a tool wants the COMPLETED call shown — the *result* state, after `execute`
* returns. A `card`-tagged union mirroring {@link ToolCallView}: a UI switches on
@@ -125,7 +137,7 @@ export interface DiffCallView {
* `ToolDefinition.presentResult`; omitting the method keeps the pending
* title and renders the raw result content.
*/
export type ToolResultView = GenericResultView | TerminalResultView | DiffResultView | WebResultView
export type ToolResultView = GenericResultView | TerminalResultView | DiffResultView | ReadResultView | WebResultView
/**
* The default completed card: an optional replacement title and reformatted
@@ -177,6 +189,47 @@ export interface DiffResultView {
diffs: FileDiff[]
}
/**
* A completed file read rendered as a line-numbered, optionally syntax-highlighted
* code view by a capable UI. Set by a tool whose call reads file text (e.g.
* `read`); the pending state stays a {@link GenericCallView} (`kind: 'read'`)
* because a call carries no content until `execute` returns. The structured
* `lines`/`path`/`lang`/`totalLines` fields cannot be reconstructed from the
* model-facing result text alone, so the read tool projects them through its
* `output.presentationMeta` (persisted with the session log) and `presentResult`
* narrows that metadata back into this view on live and replay paths alike. A UI
* without the read capability falls back to `content` (the model-facing text with
* its envelope stripped), so this view degrades to the generic text card.
*/
export interface ReadResultView {
card: 'read'
/** Replacement title for the completed call. Omit to keep the pending-state title. */
title?: string
/** The read file's path (the model-facing path; the bridge relativizes it). */
path: string
/**
* The 1-based first line the window requested, preserved even when `lines` is
* empty (a byte cap below the first selected line yields an empty window) so a
* UI knows where the window starts and where a continuation resumes.
*/
offset: number
/** The returned window's lines, in file order, each keeping its file line number. */
lines: ReadFileLine[]
/** Exact total line count in the file, so a UI can show a "showing N of M" affordance. */
totalLines: number
/**
* A syntax-highlighting language hint derived from the file extension (e.g.
* `ts`, `py`), or omitted when the extension maps to no known language so a UI
* renders the lines as plain text.
*/
lang?: string
/**
* The model-facing result content with its envelope stripped, for a UI without
* the read capability. Omit to let such a UI render the raw result content.
*/
content?: ContentBlock[]
}
/**
* One citeable source in a completed {@link WebSearchResultView}, the faithful
* projection of one web-search source. The presentation projection of `dsh-web`'s