diff --git a/.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.i18n.yaml new file mode 100644 index 0000000000..20d21565b5 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# 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 .agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.md +2026-08-06-web-markdown-incremental-ast-renderer.md: 3599bfcc78dc4eefe5e82f461a469bdba15f3aae +2026-08-06-web-markdown-incremental-ast-renderer.zh.md: 2e00977da58ef29a77c45abcecf0f3bb62737929 diff --git a/.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.md b/.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.md new file mode 100644 index 0000000000..3599bfcc78 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.md @@ -0,0 +1,33 @@ +# Agent Note: Incremental streaming markdown through a direct mdast renderer + +Status: implemented + +English | [中文](2026-08-06-web-markdown-incremental-ast-renderer.zh.md) + +## Problem + +`MarkdownText` re-parsed the whole accumulated reply on every streaming publish: react-markdown's string-only API builds a fresh unified processor per render and runs micromark → mdast → hast → React over the full text, so per-chunk main-thread work grew linearly with the reply and the stream's cumulative cost grew quadratically. The existing mitigations (frame batching, the isolated streaming tail, the plain fence arm) bounded how often and how widely that work ran, never how much text each run re-parsed. Fixing it needs AST-level input — freezing settled blocks and re-parsing only the source tail — which the string-only wrapper structurally cannot express. + +## Decision + +`MarkdownText` renders mdast directly and parses incrementally while streaming: + +- **Grammars** ([parse.ts](../../../../packages/client/ui-primitives/src/markdown/parse.ts)): `parseGfm` (streaming arm and `extractMarkdownPlainText`) and `parseGfmWithMath` (settled arm) call `mdast-util-from-markdown` with the same micromark extensions the replaced remark plugins wrapped, so block boundaries are identical everywhere. `mathCompatibility` (ex `remarkMathCompatibility`) now exports its micromark extension directly. +- **Incremental parsing** ([incremental.ts](../../../../packages/client/ui-primitives/src/markdown/incremental.ts)): CommonMark block parsing is line-based, so appended text reshapes only the parse frontier. `IncrementalMarkdownParser` keeps the trailing two blocks unstable (the last block is the frontier; the second-to-last is safety margin), freezes everything before them, and re-parses only the source tail from the last frozen block's `position.end.offset` — the parser's own offsets, no bespoke source scanning. Each source region parses O(1) times per stream instead of once per chunk; a single giant block (an unclosed fence) degrades to the old full-reparse cost and no worse. Non-append input resets the state under a bumped generation. +- **Rendering** ([render.tsx](../../../../packages/client/ui-primitives/src/markdown/render.tsx), [katex.tsx](../../../../packages/client/ui-primitives/src/markdown/katex.tsx)): one switch over mdast node types replaces remark-rehype + react-markdown, reproducing the replaced pipeline's DOM byte-for-byte — table alignment as `text-align` styles, tight-list paragraph unwrapping, task-list classes and checkbox spacing, the footnote section (whose in-page anchors the protocol allowlist already reduced to plain text), literal raw HTML, the separator newlines that surface next to literal HTML text, and rehype-katex's three-arm error chain with KaTeX HTML mapped to React through the browser's own `DOMParser` (no wrapper element, so first/last-child margin rules still reach `.katex-display`; React 18 puts the `.katex-mathml` subtree in the HTML namespace exactly as the replaced pipeline did — a pre-existing limitation outside this parity contract, invisible to the visual `.katex-html` arm). Frozen blocks cache their React elements and keep source-offset keys, so crossing the freeze boundary reconciles instead of remounting; `MarkdownText` is memoized. + +The DOM is pinned by `tests/fixtures/markdown-dom`: fixtures recorded from the react-markdown implementation before the swap, which the new renderer must reproduce under a whitespace-normalizing serializer. A fixture diff is a user-visible markdown style change to review, never to re-record for a refactor. `tests/markdown-incremental.spec.tsx` holds the equivalence property — at every appended prefix, chunked at 1/3/7/16 bytes, the live component's DOM equals a fresh mount's — plus freeze-boundary DOM-node identity and reset behavior. + +This reverses the [assistant-markdown note](../feature/2026-07-23-web-assistant-markdown.md)'s rejected alternative ("maintain a custom React walker"): the incremental requirement is new evidence, the walker's security-sensitive branches (URL allowlist, image policy, inert HTML) were already product-owned functions, and the dependency no longer deleted owned code — it blocked the architecture. That note's untrusted-output policy and renderer selection are unchanged. + +## Alternatives considered + +**Keep react-markdown and split the source into per-segment `` instances.** Zero renderer ownership, but each frame parses the tail twice (boundary detection + render), settled math still re-parses everything, hast construction and the per-render processor remain, and blocks remount when crossing the freeze boundary because element trees cannot be cached across instances. + +**Render cached mdast through `mdast-util-to-hast` + `hast-util-to-jsx-runtime`.** Keeps upstream's node mappings for free, but retains the hast intermediate per frame and two new direct dependencies for a pipeline whose mapping surface is small, closed, and now pinned by fixtures. + +**Parse KaTeX output with `hast-util-from-html-isomorphic` (as rehype-katex does).** Pulls a parse5-based HTML parser into the bundle to parse trusted, vocabulary-constrained KaTeX output the browser's `DOMParser` (with the spec's SVG/MathML attribute adjustments) already parses identically. + +## Consequences + +Streaming per-chunk work now tracks the unstable tail instead of the whole reply, and react-markdown, remark-gfm, remark-math, rehype-katex, unified, and the hast chain left the browser bundle (`mdast-util-math` and `micromark-util-sanitize-uri` became direct dependencies; both were already transitive). The package owns ~25 node mappings, their tests, and the KaTeX DOM conversion — priced against the fixture contract that freezes their output. Two behavioral deviations, both healed by the settled full parse at finalize: a reference-style link or footnote whose definition lands on the other side of a freeze boundary renders literally while streaming, and a footnote reference can flash back to literal text when its definition freezes while the referencing block is still unstable. This module and KaTeX conversion assume a browser DOM (`DOMParser`), which the client-only package already did. diff --git a/.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.zh.md b/.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.zh.md new file mode 100644 index 0000000000..2e00977da5 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.zh.md @@ -0,0 +1,33 @@ +# Agent Note: 经由直接 mdast 渲染器的增量流式 Markdown + +Status: implemented + +[English](2026-08-06-web-markdown-incremental-ast-renderer.md) | 中文 + +## Problem + +`MarkdownText` 在每次流式发布时都重新解析整个已累积的回复:react-markdown 的纯字符串 API 每次渲染都新建 unified processor,并对全文跑完 micromark → mdast → hast → React,因此每个 chunk 的主线程工作量随回复长度线性增长,整个流的累计成本随之二次增长。既有缓解手段(帧级合并、隔离的流式尾部、围栏 plain 臂)约束的是这份工作跑多频繁、波及多广,从未约束每次重新解析多少文本。修复它需要 AST 级输入——冻结已定型的块、只重新解析源文本尾部——这是纯字符串封装在结构上无法表达的。 + +## Decision + +`MarkdownText` 直接渲染 mdast,并在流式期间增量解析: + +- **语法**([parse.ts](../../../../packages/client/ui-primitives/src/markdown/parse.ts)):`parseGfm`(流式臂与 `extractMarkdownPlainText`)和 `parseGfmWithMath`(定稿臂)以被替换的 remark 插件所包装的同一组 micromark 扩展调用 `mdast-util-from-markdown`,因此各处块边界完全一致。`mathCompatibility`(原 `remarkMathCompatibility`)现在直接导出其 micromark 扩展。 +- **增量解析**([incremental.ts](../../../../packages/client/ui-primitives/src/markdown/incremental.ts)):CommonMark 块解析按行推进,追加文本只会重塑解析前沿。`IncrementalMarkdownParser` 保留末尾两个块不稳定(最后一块是前沿;倒数第二块是安全裕量),冻结其前的所有块,只从最后一个冻结块的 `position.end.offset` 起重新解析源尾部——用的是解析器自己的偏移量,没有任何自制源扫描。每个源区间在整个流中解析 O(1) 次而非每 chunk 一次;单个巨型块(未闭合围栏)退化为旧的全量重解析成本,不会更差。非追加输入在递增的 generation 下重置状态。 +- **渲染**([render.tsx](../../../../packages/client/ui-primitives/src/markdown/render.tsx)、[katex.tsx](../../../../packages/client/ui-primitives/src/markdown/katex.tsx)):一个对 mdast 节点类型的 switch 取代 remark-rehype + react-markdown,逐字节复刻被替换管线的 DOM——表格对齐渲染为 `text-align` 样式、紧凑列表段落解包、任务列表类名与复选框空格、脚注区(其页内锚点本就被协议白名单降为纯文本)、字面 raw HTML、会与字面 HTML 文本相邻显形的分隔换行,以及 rehype-katex 的三臂容错链,KaTeX HTML 经浏览器自带的 `DOMParser` 映射为 React(无包裹元素,首/末子元素的 margin 规则仍能作用于 `.katex-display`;React 18 会把 `.katex-mathml` 子树放进 HTML 命名空间,与被替换管线完全一致——既有限制,不在本对等性契约范围内,对承担视觉渲染的 `.katex-html` 臂不可见)。冻结块缓存其 React 元素并保持源偏移 key,跨过冻结边界时走 reconcile 而非重挂载;`MarkdownText` 已 memo 化。 + +DOM 由 `tests/fixtures/markdown-dom` 钉死:fixture 录制自替换前的 react-markdown 实现,新渲染器必须在空白规整序列化器下复现。fixture 差异即用户可见的 markdown 样式变更,必须按此评审,绝不能为重构而重录。`tests/markdown-incremental.spec.tsx` 承载等价性性质——以 1/3/7/16 字节分块,在每个追加前缀处,常驻组件的 DOM 都等于全新挂载——外加冻结边界的 DOM 节点同一性与重置行为。 + +这推翻了[助手 Markdown Note](../feature/2026-07-23-web-assistant-markdown.md) 中被否决的备选("维护一个自定义 React walker"):增量需求是当时不存在的新证据,walker 的安全敏感分支(URL 白名单、图片策略、惰性 HTML)本就是产品自有函数,而该依赖不再删减自有代码——它阻塞了架构。该 Note 的不可信输出策略与渲染器选型不变。 + +## Alternatives considered + +**保留 react-markdown,把源文本切成逐段 `` 实例。** 渲染器零自有成本,但每帧对尾部解析两次(边界检测 + 渲染),定稿数学仍要全量重解析,hast 构建与逐渲染 processor 依旧存在,且块跨过冻结边界时会重挂载——元素树无法跨实例缓存。 + +**用 `mdast-util-to-hast` + `hast-util-to-jsx-runtime` 渲染缓存的 mdast。** 白拿上游节点映射,但每帧保留 hast 中间层,并为一个映射面小、封闭、且已被 fixture 钉死的管线引入两个新直接依赖。 + +**用 `hast-util-from-html-isomorphic` 解析 KaTeX 输出(rehype-katex 的做法)。** 为解析可信、词汇受限的 KaTeX 输出把基于 parse5 的 HTML 解析器拉进 bundle,而浏览器自带的 `DOMParser`(带规范的 SVG/MathML 属性调整)解析结果完全相同。 + +## Consequences + +流式的每 chunk 工作量现在跟随不稳定尾部而非整个回复,react-markdown、remark-gfm、remark-math、rehype-katex、unified 及 hast 链退出浏览器 bundle(`mdast-util-math` 与 `micromark-util-sanitize-uri` 成为直接依赖;两者原本就是传递依赖)。包自有约 25 个节点映射、其测试以及 KaTeX DOM 转换——代价由冻结其输出的 fixture 契约对冲。两个行为偏差,均在定稿的全量解析处自愈:定义落在冻结边界另一侧的引用式链接或脚注在流式期间渲染为字面文本;当脚注定义先冻结而引用块仍不稳定时,脚注引用可能闪回字面文本。本模块与 KaTeX 转换假定浏览器 DOM(`DOMParser`),这个 client-only 包本就如此。 diff --git a/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.i18n.yaml b/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.i18n.yaml index 2a56348913..e78ab9abbd 100644 --- a/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.i18n.yaml @@ -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 .agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.md -2026-07-23-web-assistant-markdown.md: 8a8778351911bcb3448366c718aa124c4a89de58 -2026-07-23-web-assistant-markdown.zh.md: 2ac024e24ff95b4eb296112562c93f343b832187 +2026-07-23-web-assistant-markdown.md: ad86559e3b5294b6bd67d69ff5c6ce37a5172008 +2026-07-23-web-assistant-markdown.zh.md: b47375db843ea1c81b913e3f7ac8cd1bbc278830 diff --git a/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.md b/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.md index 8a87783519..ad86559e3b 100644 --- a/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.md +++ b/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.md @@ -12,9 +12,9 @@ The Web conversation preserves assistant Markdown source through session events, `@deepseek-ai/dsh-client-ui-primitives` exports `MarkdownText` as the untrusted assistant-text renderer, and `ui-conversation` selects it only for assistant `text` blocks. Finalized history, the streaming tail, and interrupted partials already share `AssistantMarkdown`, so they receive the same renderer without changing events or snapshots. User and steering messages keep `MessageText` and remain literal. -`MarkdownText` uses `react-markdown` with `remark-gfm` to build React elements from an AST. It covers CommonMark blocks plus GFM tables, task lists, strikethrough, and autolinks without raw-HTML parsing. Fenced code routes through the shared `CodeBlock`, which highlights registered grammars with the client's shiki singleton (`--shiki-*` tokens) and falls back to plain monospace otherwise. While a turn streams, fences stay on the plain arm so growing fences are not retokenized every chunk. +`MarkdownText` parses with `mdast-util-from-markdown` plus the GFM micromark extensions and renders the mdast tree through the package's own renderer, parsing incrementally while a turn streams (the [incremental AST renderer note](../architecture/2026-08-06-web-markdown-incremental-ast-renderer.md) owns that mechanism and its DOM-parity contract). It covers CommonMark blocks plus GFM tables, task lists, strikethrough, and autolinks without raw-HTML parsing. A micromark attention extension reuses the CommonMark resolver while letting runs of at least two asterisks close after Unicode punctuation when followed immediately by CJK text. This exception covers punctuation-terminated strong emphasis in whitespace-free CJK prose during streaming and after settlement; single-asterisk emphasis, non-CJK adjacency, escaped source, code, and math retain upstream parsing. Fenced code routes through the shared `CodeBlock`, which highlights registered grammars with the client's shiki singleton (`--shiki-*` tokens) and falls back to plain monospace otherwise. While a turn streams, fences stay on the plain arm so growing fences are not retokenized every chunk. -Visual spacing, tables, links, blockquotes, inline code, and code-block chrome follow deepsuite `@deepseek/md` (`markdown.css` / `code-block.css`) and the same `--dsw-alias-markdown-*`, `--dsw-font-markdown-*`, `--dsw-alias-border-l*`, and `--dsw-alias-label-*` tokens. Links use `--dsw-alias-state-business-primary` (deepsuite's sheet uses `--dsw-alias-brand-text`, which is blue only under newDesign; design-platform keeps brand-text near-black and is not retuned here). `CodeBlock` ships a language banner and a copy control (`复制` / `复制成功`). Finalized text renders KaTeX through `remark-math` and `rehype-katex`; `remarkMathCompatibility` maps `\(...\)`, `\[...\]`, and block-level same-line `$$...$$` to the same standard math AST nodes. This is a narrow parser compatibility layer, not a regex rewrite or malformed-model-output repair. Streaming stays literal until finalization so incomplete formulae do not flash errors. Citation pills, heading anchors, the thinking-small markdown variant, and custom □/☑ task markers remain out of scope; GFM task lists keep native checkboxes. +Visual spacing, tables, links, blockquotes, inline code, and code-block chrome follow deepsuite `@deepseek/md` (`markdown.css` / `code-block.css`) and the same `--dsw-alias-markdown-*`, `--dsw-font-markdown-*`, `--dsw-alias-border-l*`, and `--dsw-alias-label-*` tokens. Links use `--dsw-alias-state-business-primary` (deepsuite's sheet uses `--dsw-alias-brand-text`, which is blue only under newDesign; design-platform keeps brand-text near-black and is not retuned here). When one inline-code token consists entirely of an absolute HTTP(S) URL, its code chrome contains the same keyboard-focusable safe external anchor as an ordinary link; port, path, and query text remain unchanged, while commands, partial URLs, other schemes, and fenced code stay inert. `CodeBlock` ships a language banner and a copy control (`复制` / `复制成功`). Finalized text renders KaTeX through the settled grammar's math extensions; `mathCompatibility` maps `\(...\)`, `\[...\]`, and block-level same-line `$$...$$` to the same standard math AST nodes. This is a narrow parser compatibility layer, not a regex rewrite or malformed-model-output repair. Streaming stays literal until finalization so incomplete formulae do not flash errors. Citation pills, heading anchors, the thinking-small markdown variant, and custom □/☑ task markers remain out of scope; GFM task lists keep native checkboxes. The dependency is explicit in `ui-primitives`; because that pure library is seeded by the Web shell, the parser and highlighter are part of the initial browser bundle. @@ -26,7 +26,7 @@ Fenced code and GFM tables own horizontal overflow so long content cannot widen ## Alternatives considered -**Promote the existing mdast and micromark development dependencies and maintain a custom React walker.** This avoids a new parser family but makes the product own every node mapping, GFM extension, and security-sensitive rendering branch. The dedicated React renderer keeps that traversal upstream while preserving an AST-to-React path. +**Promote the existing mdast and micromark development dependencies and maintain a custom React walker.** This avoids a new parser family but makes the product own every node mapping, GFM extension, and security-sensitive rendering branch. The dedicated React renderer keeps that traversal upstream while preserving an AST-to-React path. *Later reversed on new evidence — incremental streaming parsing needs AST-level input the string-only wrapper cannot provide; the [incremental AST renderer note](../architecture/2026-08-06-web-markdown-incremental-ast-renderer.md) owns that decision.* **Replace `MessageText` with Markdown rendering.** This formats user prompts and steering as a side effect. Those authored surfaces remain literal until the product chooses that behavior explicitly. @@ -36,6 +36,10 @@ Fenced code and GFM tables own horizontal overflow so long content cannot widen **Port deepsuite Prism `highlight.css` and the mdast pipeline.** Appearance parity is owned by CSS Modules and shared `--dsw-*` tokens; highlighting stays on the existing shiki allowlist so the client does not take a second highlighter or Prism class contract. +**Preprocess Markdown source or repair text nodes after parsing for CJK punctuation boundaries.** A source rewrite must reproduce escape, code, math, and delimiter rules before the parser owns those distinctions, while a text-node repair has already lost some source intent and cannot compose with parsed inline nodes. Extending attention at the tokenizer boundary preserves the upstream resolver and limits the divergence to delimiter eligibility. + +**Require the model to emit standard links and leave URL-shaped inline code inert.** Output guidance cannot make persisted or third-party model replies uniform, and inline code is a common way to distinguish a literal endpoint. Recognizing only a complete absolute HTTP(S) value at the rendered inline-code boundary preserves code semantics while applying the existing untrusted-link policy. + ## Consequences -Assistant replies render semantic Markdown consistently during streaming and replay, while tool cards, reasoning rows, interactions, user bubbles, and the host protocol remain unchanged. Streaming reparses the current text after each accumulated update; incomplete Markdown can temporarily change structure, but the isolated tail bounds React invalidation and the final event does not switch renderers. Code fences share one chrome and copy path with tool and details surfaces. The initial Web shell includes the Markdown parser, GFM runtime, KaTeX, and shiki allowlist; citation, anchor, and thinking-small surfaces remain deferred. +Assistant replies render semantic Markdown consistently during streaming and replay, while tool cards, reasoning rows, interactions, user bubbles, and the host protocol remain unchanged. Streaming reparses only the unstable tail after each accumulated update; incomplete Markdown can temporarily change the tail's structure, but the isolated tail bounds React invalidation and the final event does not switch renderers. URL-shaped inline code becomes navigable without changing its visible literal, while unsafe schemes and mixed code remain non-interactive. Code fences share one chrome and copy path with tool and details surfaces. The initial Web shell includes the Markdown parser, GFM runtime, KaTeX, and shiki allowlist; citation, anchor, and thinking-small surfaces remain deferred. diff --git a/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.zh.md b/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.zh.md index 2ac024e24f..b47375db84 100644 --- a/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.zh.md +++ b/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.zh.md @@ -12,9 +12,9 @@ Web 对话通过会话事件、历史回放与流式累积保留 assistant Markd `@deepseek-ai/dsh-client-ui-primitives` 导出 `MarkdownText`,用作不受信任的 assistant 文本渲染器;`ui-conversation` 仅为 assistant `text` 块选择该渲染器。已完成的历史消息、流式输出尾部与被中断的部分输出已经共用 `AssistantMarkdown`,因此无需更改事件或快照,它们便会采用同一渲染器。用户消息与 steering 消息继续使用 `MessageText`,并保持按字面渲染。 -`MarkdownText` 使用 `react-markdown` 与 `remark-gfm`,从 AST 构建 React 元素。它覆盖 CommonMark 块,以及 GFM 表格、任务列表、删除线与自动链接,且不解析原始 HTML。围栏代码经共享的 `CodeBlock` 路由;该组件用客户端的 shiki 单例(`--shiki-*` token)高亮已注册语法,否则回退为纯等宽文本。轮次流式输出期间,围栏停留在纯文本分支,以免每收到一个分片就对增长中的围栏重新分词。 +`MarkdownText` 以 `mdast-util-from-markdown` 加 GFM micromark 扩展解析,并经包内自有渲染器渲染 mdast 树,轮次流式输出期间增量解析([增量 AST 渲染器 Note](../architecture/2026-08-06-web-markdown-incremental-ast-renderer.md) 拥有该机制及其 DOM 一致性契约)。它覆盖 CommonMark 块,以及 GFM 表格、任务列表、删除线与自动链接,且不解析原始 HTML。一个 micromark attention 扩展复用 CommonMark resolver,同时允许至少两个星号组成的连续序列在 Unicode 标点后闭合,前提是其后紧邻 CJK 文本。这一例外涵盖流式输出期间与完成后无空格 CJK 文本中以标点结尾的粗体;单星号强调、紧邻非 CJK 文本的情况、已转义源文本、代码与数学公式仍沿用上游解析行为。围栏代码经共享的 `CodeBlock` 路由;该组件用客户端的 shiki 单例(`--shiki-*` token)高亮已注册语法,否则回退为纯等宽文本。轮次流式输出期间,围栏停留在纯文本分支,以免每收到一个分片就对增长中的围栏重新分词。 -视觉间距、表格、链接、引用块、行内代码与代码块外框遵循 deepsuite `@deepseek/md`(`markdown.css` / `code-block.css`),并使用同一套 `--dsw-alias-markdown-*`、`--dsw-font-markdown-*`、`--dsw-alias-border-l*` 与 `--dsw-alias-label-*` token。链接使用 `--dsw-alias-state-business-primary`(deepsuite 的样式表使用 `--dsw-alias-brand-text`,仅在 newDesign 下为蓝色;design-platform 将 brand-text 保持为近黑色,此处不做重新调色)。`CodeBlock` 提供语言横幅与复制控件(`复制` / `复制成功`)。已完成的文本通过 `remark-math` 和 `rehype-katex` 渲染 KaTeX;`remarkMathCompatibility` 将 `\(...\)`、`\[...\]` 和块级同一行 `$$...$$` 映射为同一套标准数学 AST 节点。这是一层小范围的解析器兼容层,不是正则重写,也不修复格式错误的模型输出。流式输出在完成前保持按字面渲染,避免不完整公式闪现错误。引用胶囊、标题锚点、thinking-small markdown 变体,以及自定义 □/☑ 任务标记仍不在范围内;GFM 任务列表继续使用原生复选框。 +视觉间距、表格、链接、引用块、行内代码与代码块外框遵循 deepsuite `@deepseek/md`(`markdown.css` / `code-block.css`),并使用同一套 `--dsw-alias-markdown-*`、`--dsw-font-markdown-*`、`--dsw-alias-border-l*` 与 `--dsw-alias-label-*` token。链接使用 `--dsw-alias-state-business-primary`(deepsuite 的样式表使用 `--dsw-alias-brand-text`,仅在 newDesign 下为蓝色;design-platform 将 brand-text 保持为近黑色,此处不做重新调色)。当单个行内代码 token 完全由绝对 HTTP(S) URL 构成时,其代码外框会包含一个与普通链接相同、可通过键盘聚焦的安全外链锚点;端口、路径与查询文本保持不变,而命令、非完整 URL、其他 scheme 与围栏代码仍不会成为链接。`CodeBlock` 提供语言横幅与复制控件(`复制` / `复制成功`)。已完成的文本通过定稿语法的数学扩展渲染 KaTeX;`mathCompatibility` 将 `\(...\)`、`\[...\]` 和块级同一行 `$$...$$` 映射为同一套标准数学 AST 节点。这是一层小范围的解析器兼容层,不是正则重写,也不修复格式错误的模型输出。流式输出在完成前保持按字面渲染,避免不完整公式闪现错误。引用胶囊、标题锚点、thinking-small markdown 变体,以及自定义 □/☑ 任务标记仍不在范围内;GFM 任务列表继续使用原生复选框。 该依赖在 `ui-primitives` 中显式声明;由于这一纯库由 Web shell 预置,解析器与高亮器会成为初始浏览器 bundle 的一部分。 @@ -26,7 +26,7 @@ assistant 生成的链接目标地址仅限绝对 HTTP、HTTPS 与 mailto URL。 ## 考虑过的替代方案 -**将现有的 mdast 与 micromark 开发依赖提升为正式依赖,并维护自定义 React walker。**此方案避免引入新的解析器体系,但产品需要自行负责每种节点映射、GFM 扩展和安全敏感的渲染分支。专用 React 渲染器将这套遍历交由上游维护,同时保留 AST 到 React 的处理路径。 +**将现有的 mdast 与 micromark 开发依赖提升为正式依赖,并维护自定义 React walker。**此方案避免引入新的解析器体系,但产品需要自行负责每种节点映射、GFM 扩展和安全敏感的渲染分支。专用 React 渲染器将这套遍历交由上游维护,同时保留 AST 到 React 的处理路径。*后因新证据被推翻——增量流式解析需要纯字符串封装无法提供的 AST 级输入;该决策由[增量 AST 渲染器 Note](../architecture/2026-08-06-web-markdown-incremental-ast-renderer.md) 拥有。* **将 `MessageText` 替换为 Markdown 渲染。**这会产生格式化用户提示词与 steering 的副作用。在产品明确选择此行为之前,这两类输入内容仍按字面渲染。 @@ -36,6 +36,10 @@ assistant 生成的链接目标地址仅限绝对 HTTP、HTTPS 与 mailto URL。 **移植 deepsuite 的 Prism `highlight.css` 与 mdast 管线。**外观一致性由 CSS Modules 与共享的 `--dsw-*` token 负责;高亮仍走现有的 shiki 允许列表,使客户端不必引入第二套高亮器或 Prism class 契约。 +**为处理 CJK 标点边界而预处理 Markdown 源文本,或在解析后修复文本节点。**源文本重写必须在解析器掌握这些区别之前复现转义、代码、数学公式与定界符规则;文本节点修复则已经丢失部分源文本意图,也无法与已解析的行内节点组合。在分词器边界扩展 attention 可保留上游 resolver,并将差异限制在定界符的适用条件上。 + +**要求模型输出标准链接,并让 URL 形态的行内代码保持不可交互。**输出指引无法统一已持久化回复与第三方模型回复,而行内代码是将端点标记为字面值的常见方式。仅在行内代码的渲染边界识别完整的绝对 HTTP(S) 值,可在应用现有不受信任链接策略的同时保留代码语义。 + ## 后果 -assistant 回复在流式输出与回放期间都会一致地渲染为语义化 Markdown,而工具卡片、推理行、交互、用户气泡和宿主协议保持不变。每次累积更新后,流式输出都会重新解析当前文本;未完成的 Markdown 可能暂时改变结构,但独立的尾部会限定 React 失效范围,最终事件也不会切换渲染器。代码围栏与工具及详情表层共用同一外框与复制路径。初始 Web shell 包含 Markdown 解析器、GFM 运行时、KaTeX 与 shiki 允许列表;citation、anchor 和 thinking-small 表层仍暂缓。 +assistant 回复在流式输出与回放期间都会一致地渲染为语义化 Markdown,而工具卡片、推理行、交互、用户气泡和宿主协议保持不变。每次累积更新后,流式输出只重新解析不稳定的尾部;未完成的 Markdown 可能暂时改变尾部结构,但独立的尾部会限定 React 失效范围,最终事件也不会切换渲染器。URL 形态的行内代码会在不改变其可见字面文本的情况下变得可导航,而采用不安全 scheme 或混有其他内容的代码仍不可交互。代码围栏与工具及详情表层共用同一外框与复制路径。初始 Web shell 包含 Markdown 解析器、GFM 运行时、KaTeX 与 shiki 允许列表;citation、anchor 和 thinking-small 表层仍暂缓。 diff --git a/.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.i18n.yaml b/.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.i18n.yaml new file mode 100644 index 0000000000..f9c6976a0d --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# 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 .agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.md +2026-08-04-web-context-source-and-steer-marks.md: 9070ea6ed34fffecd9fd2b90275bd31155100c75 +2026-08-04-web-context-source-and-steer-marks.zh.md: 9d7c7c0a34587071e281ff8b2cb77e359a1580c1 diff --git a/.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.md b/.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.md new file mode 100644 index 0000000000..9070ea6ed3 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.md @@ -0,0 +1,49 @@ +# Agent Note: Web transcript marks context source, recall, and steering + +Status: implemented + +English | [中文](2026-08-04-web-context-source-and-steer-marks.zh.md) + +## Problem + +Everything a producer adds to the model-facing conversation reached the Web transcript as one of two anonymous shapes. Every logged non-user `user/message` — the skill catalog, the runtime snapshot, reconciled `AGENTS.md` instructions, a guard notice, a subagent report, a cross-session snapshot — collapsed into one identical `上下文注入` row, so a reader could not tell what had been added without expanding each row and reading raw JSON. Mid-turn steering was worse: it rendered in exactly the bubble a turn-opening prompt uses, leaving the transcript unable to say which message interrupted a running turn. + +The distinctions are already durable. `user/message.source` is the merge-extensible provenance every producer must supply, while `agent/inbox/spliced` records whether an identified message entered and left `next-turn` or `next-step`; only the presentation discarded them. The terminal transcript this Web UI replaced did name each card's producer, so the Web surface was a regression for the same log. + +## Decision + +The transcript names all three roles a non-prompt message can play — injected context, recalled session, and steering. + +`TranscriptAdapter` and the history fold attach a `provenance` view to every `ContextMessageNode`, computed by `contextProvenance()` from the durable source alone. It returns a `role` (`inject`, or `recall` for a cross-session snapshot) and a `label` naming the producer. `ContextInjectionRow` titles itself from the role and shows the label beside that title in `ToolRow`'s summary geometry, so the collapsed row already answers what was added and by whom; the 141px scrollport and truncation bound are unchanged from the [disclosure decision](2026-07-30-web-context-injection-disclosure.md). What renders inside that scrollport is chosen by the independent form axis added in the [context form decision](2026-08-05-context-form-vocabulary.md). + +**The label is read out of the log, never from a client-side table of producer names.** `workspace-instructions` is named by the distinct instruction paths it reconciled, `session-reference` by the titles of the sessions it read, a plugin source by its logged plugin id, and any other source by its own `kind` — the documented default arm for a merge-extensible union. A source carrying no readable kind degrades to an unnamed injection. A new or renamed producer is therefore identifiable without a client release, no label can go stale against the code, and a resumed, forked, or foreign log projects exactly like a live session. + +`recall` covers `session-reference` because that is the one shipped source that lifts another session's material into this one. No Web leaf mounts `dsh-session-reference` today — it had only a terminal host — so the arm exists for log portability rather than for a bundled producer, and it is exercised by unit coverage rather than an assembled Web scenario. + +`MessageItem` captions durable and pending steering bubbles with `插话`. The runtime replays durable `agent/inbox/spliced` events and projects a user-origin `user/message` as `SteeringMessageNode` when that same message identity was claimed from `next-step`; a queued-turn claim stays a `UserMessageNode`, and a non-user next-step message stays context. This reverses one clause of [no steer entry or interjection chrome](../simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md), which removed the badge because the composer could not steer and the label named a gesture users could not perform. The composer gained a Steer gesture afterwards without amending that note; this decision supplies the product decision its reintroduction clause required, and corrects the stale facts left in it. The caption is the only steering chrome here: composer modes, the Queue dock's strict-steer action, and pending-steering lifecycle stay with their own owners. + +## Alternatives considered + +**Localize producer names in the client.** A dictionary keyed by plugin id would read better than `@deepseek-ai/dsh-system-prompt`, but it drifts silently on every rename, needs a client change per new producer, and cannot name a producer from a foreign log at all. Provenance the log already carries is worth more than prose the client invents. + +**Register presentations per source kind.** The disclosure decision deferred a keyed context-view slot until source-owned presentations emerged. Naming a row is not a distinct presentation, and a registry keyed on mounted producers would fail exactly where it matters — a resumed log whose producer is no longer mounted still has to render. + +**Compute the role and label on the host.** The host would have to attach a view to each event copy, duplicating what the durable source already states and adding a wire field per context message. The projection derives it once per node instead, where the transcript's other derived facts live. + +**Give steering its own row instead of a captioned bubble.** Steering is a user message that arrived mid-turn; a separate row shape would break the right-aligned reading rhythm and duplicate the bubble's copy and branch actions for no new information. + +**Extend the trajectory table with the same names.** Out of scope: the table's context cell has its own text derivation, and the issue asks for the conversation surface. + +## Testing + +- `packages/client/runtime` unit coverage pins each provenance arm, the label fallbacks when a name field is missing, empty, or wrongly typed, the unnamed degradation for a source with no readable kind, and steering reconstruction on reset and live append paths. +- `packages/client/ui-conversation` jsdom coverage pins the role title, the producer label beside it, the label's survival while expanded, the roleless header, and the steering caption on both durable and pending bubbles. +- The keyless assembled-Web goldens carry the named header and the steering caption, so the assembled transcript — not only component tests — proves the marks. + +## Consequences + +- A reader can attribute every non-prompt message in the transcript at a glance, and the header stays honest for logs this client version has never seen a producer for. +- Producer names in the UI are package-shaped (`dsh-tool-skill`, `@deepseek-ai/dsh-system-prompt`) wherever the source carries only a plugin id. That is the cost of refusing a client-side name table; a producer that wants a better name records better provenance. +- `ContextMessageNode` gains a required field, so every constructed node — including test fixtures — must supply it. +- `SteeringMessageNode` remains a distinct presentation node even though the agent loop now records admitted steering as `user/message`; its identity comes from the durable inbox history rather than a separate message event. +- The `recall` arm has no producer in a shipped Web leaf until a host mounts `dsh-session-reference`; it is reachable only through logs written elsewhere. diff --git a/.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.zh.md b/.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.zh.md new file mode 100644 index 0000000000..9d7c7c0a34 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.zh.md @@ -0,0 +1,49 @@ +# Agent Note:Web transcript 标出上下文来源、召回与 steering + +Status: implemented + +[English](2026-08-04-web-context-source-and-steer-marks.md) | 中文 + +## Problem + +生产方向模型侧对话补充的一切内容,进入 Web transcript(文本记录)后只剩两种匿名形态。每一条已记录的非用户 `user/message`——skill 目录、运行时快照、经过对账的 `AGENTS.md` 指令、guard 提示、子 agent 汇报、跨会话快照——都塌缩成同一行 `上下文注入`,读者不逐行展开去读原始 JSON 就无从知道究竟注入了什么。steering(中途引导)的情况更糟:它渲染成与开轮提示完全相同的气泡,于是 transcript 无法说明哪一条消息打断了正在运行的轮次。 + +这些区分本来就是持久事实。`user/message.source` 是每个生产方都必须提供的可合并扩展来源,`agent/inbox/spliced` 则记录有身份的消息是从 `next-turn` 还是 `next-step` 进入和离开;把这些事实丢掉的只有呈现层。被这套 Web UI 取代的终端 transcript 本来会写出每张卡片的生产者,因此面对同一份日志,Web 侧是一次倒退。 + +## Decision + +transcript 为非提示消息可能承担的三种角色分别命名:注入上下文、召回会话、steering。 + +`TranscriptAdapter` 与历史折叠为每个 `ContextMessageNode` 附加一份 `provenance` 视图,由 `contextProvenance()` 仅依据持久来源计算得出。它返回 `role`(`inject`,跨会话快照则为 `recall`)与命名生产者的 `label`。`ContextInjectionRow` 以角色作为标题,并按 `ToolRow` 摘要的几何在标题旁展示该名称,因此折叠态就已经回答了「注入了什么、由谁注入」;141px 滚动视口与截断上限沿用[展开项决策](2026-07-30-web-context-injection-disclosure.md),未作改动。视口里渲染什么,则由[上下文形态决策](2026-08-05-context-form-vocabulary.md)引入的、相互独立的形态轴决定。 + +**名称从日志中读出,绝不来自客户端维护的生产者名称表。** `workspace-instructions` 以它对账过的去重指令文件路径命名,`session-reference` 以它读取的会话标题命名,插件来源以其记录的插件 id 命名,其余来源则以自身的 `kind` 命名——这正是可合并扩展联合类型有文档记载的默认分支。没有可读 kind 的来源降级为无名注入。于是新增或重命名的生产者无需客户端发版即可辨识,任何名称都不会相对代码变味,恢复、fork 或来自外部的日志与实时会话的投影结果完全一致。 + +`recall` 覆盖 `session-reference`,因为它是当前唯一会把另一个会话的材料搬进本会话的已发布来源。今天没有任何 Web 叶子挂载 `dsh-session-reference`——它此前只有终端宿主——因此该分支的存在是为了日志可移植性,而不是为了某个已打包的生产方,其覆盖来自单元测试而非组装后的 Web 场景。 + +`MessageItem` 为持久与待处理的 steering 气泡加上 `插话` 标注。runtime 会重放持久 `agent/inbox/spliced` 事件;如果一条用户来源的消息以相同身份从 `next-step` 被领取,后续 `user/message` 就投影为 `SteeringMessageNode`。从排队轮次领取的消息仍是 `UserMessageNode`,非用户来源的 next-step 消息仍是上下文。这推翻了[取消 steer 入口与插话装饰](../simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md)中的一条结论。当时移除徽章,是因为 composer 无法 steer,标签指向了用户做不到的动作。此后 composer 获得了 Steer 手势,却没有同步修订那份 note;本决策提供了它在「重新引入」条款中要求的产品决策,并订正了其中留下的过时事实。标注是这里唯一的 steering 装饰:composer 模式、Queue dock 的严格 steer 操作、待处理 steering 的生命周期仍归各自的所有者。 + +## Alternatives considered + +**在客户端本地化生产者名称。** 以插件 id 为键的字典读起来确实比 `@deepseek-ai/dsh-system-prompt` 好,但它会在每次重命名时悄悄失准,每新增一个生产者都要改客户端,而且对来自外部的日志根本无法命名。日志已经承载的来源,比客户端自己编出来的措辞更有价值。 + +**按来源 kind 注册呈现。** 展开项决策把键控的 context-view 槽位推迟到出现由来源自有的呈现需求为止。为一行命名并不构成独立呈现,而以「已挂载的生产者」为键的注册表恰恰会在最要紧的地方失效——生产者已不再挂载的恢复日志同样必须渲染出来。 + +**在 host 侧计算角色与名称。** 那需要为每份事件副本附加一个视图,重复陈述持久来源已经说明的事实,并为每条上下文消息增加一个 wire 字段。改由投影为每个节点计算一次,与 transcript 其他派生事实同处一地。 + +**给 steering 独立的行而非带标注的气泡。** steering 是一条在轮次中途抵达的用户消息;独立行形会打断右对齐的阅读节奏,并且要为零新增信息重复气泡上的复制与分支操作。 + +**把同一套名称扩展到 trajectory 表格。** 不在本次范围内:该表格的上下文单元格有自己的文本推导,而 issue 要求的是对话面。 + +## Testing + +- `packages/client/runtime` 单元覆盖钉住每个来源分支、名称字段缺失/为空/类型不符时的回退、来源没有可读 kind 时的无名降级,以及 reset 和实时 append 路径上的 steering 重建。 +- `packages/client/ui-conversation` 的 jsdom 覆盖钉住角色标题、标题旁的生产者名称、展开后该名称的留存、无名时的标题形态,以及持久与待处理气泡上的 steering 标注。 +- 无密钥的组装 Web 黄金基线携带带名称的标题栏与 steering 标注,因此证明这些标识的是组装后的 transcript,而不只是组件测试。 + +## Consequences + +- 读者一眼即可归因 transcript 中每一条非提示消息;即便面对本客户端版本从未见过其生产者的日志,标题栏依然如实。 +- 只要来源仅携带插件 id,UI 中的生产者名称就呈现为包名形态(`dsh-tool-skill`、`@deepseek-ai/dsh-system-prompt`)。这是拒绝客户端名称表的代价;想要更好名称的生产者应当记录更好的来源。 +- `ContextMessageNode` 增加了一个必填字段,因此每一处构造该节点的代码——包括测试 fixture——都必须提供它。 +- 即使 agent loop 现在把已经接纳的 steering 记录为 `user/message`,`SteeringMessageNode` 仍是独立的呈现节点;它的身份来自持久 inbox 历史,而不是独立消息事件。 +- 在某个宿主挂载 `dsh-session-reference` 之前,`recall` 分支在已发布的 Web 叶子中没有生产者,只能通过别处写入的日志抵达。 diff --git a/.agents/notes/implemented/feature/2026-08-05-context-form-vocabulary.i18n.yaml b/.agents/notes/implemented/feature/2026-08-05-context-form-vocabulary.i18n.yaml new file mode 100644 index 0000000000..65d14e9aa4 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-05-context-form-vocabulary.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# 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 .agents/notes/implemented/feature/2026-08-05-context-form-vocabulary.md +2026-08-05-context-form-vocabulary.md: 618c11b925208d09a62d10101656e3358e327ddb +2026-08-05-context-form-vocabulary.zh.md: 66278b261f23601461c96f08e87ea58766b2bfc2 diff --git a/.agents/notes/implemented/feature/2026-08-05-context-form-vocabulary.md b/.agents/notes/implemented/feature/2026-08-05-context-form-vocabulary.md new file mode 100644 index 0000000000..618c11b925 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-05-context-form-vocabulary.md @@ -0,0 +1,73 @@ +# Agent Note: Producer-declared context forms + +Status: implemented + +English | [中文](2026-08-05-context-form-vocabulary.zh.md) + +## Problem + +Every logged non-user `user/message` rendered through one body: the whole message serialized as inline JSON. A reader opening a row met `{ "content": [ { "type": "text", "text": "…\n\n…" } ], "source": { … } }`, where the escaping had collapsed the only thing worth reading — the model-facing prose — into a single line, and the provenance sat inside the same blob. + +Naming the producer in the header (the [source and steer marks decision](2026-08-04-web-context-source-and-steer-marks.md)) fixed *who added this*. It could not fix *what kind of thing was added*, because nothing in the log said so. Injected context is not one shape: a reconciled `AGENTS.md`, a catalog of available skills, a runtime policy snapshot, and a subagent's report are as different from each other as a terminal card is from a diff card, yet all four presented as the same wall of escaped JSON. + +The tool surface already solved this shape. `ToolCallView` has three cards, not one per tool, and a tool declares which card its call is. Context had no equivalent: no vocabulary of shapes, and no way for a producer to say which one it emits. + +## Decision + +`MessageSource` gains an optional producer-declared `form: ContextForm` — a small tagged vocabulary of information *shapes*, independent of `kind`: + +- `kind` answers **who produced this** and remains pure provenance. +- `form` answers **what shape of information it is**. Several producers may share one form, and one producer may emit more than one over a session. + +The vocabulary is semantic, never visual. A value states that the content is a file's instructions or a catalog of available items; colors, icons, ordering, and collapse defaults are the consumer's business and must not enter the union. It grows one value at a time, as producers gain the structured fields their form needs. This release declares two: + +**`instructions`** — instructions read out of workspace files. `workspace-context` declares it on both the startup baseline and later deltas; its existing `changes[]` already carried the paths, actions, and digests the presentation needs, so no field was added. The body lists the reconciled files above the text, and keeps the `` framing verbatim: the framing is part of what the model read, so hiding it would misreport the request. + +**`catalog`** — a catalog of items available this session, republished as it changes. `dsh-tool-skill` moves off the shared `plugin` kind to its own `skill-catalog` source carrying `entries` (the exact `name`/`description` pairs published) and `update` on a replacement, which the body renders as a replacement notice. The body lists those entries instead of re-parsing the `` block out of the prose. + +Entries record the published fact **unescaped**. The pseudo-XML escaping belongs to the `` frame, which exists for the model, so it is applied when rendering that frame and never stored; otherwise a consumer would have to know the frame's encoding to display a description containing `<`, and the same frame knowledge this decision removes would leak back in another shape. `escapeText` is deterministic and injective, so digesting the unescaped entries preserves republish semantics exactly, and the model-facing text stays byte-identical. + +That move also relocates catalog **identity**: the republish digest now covers the durable entries rather than the rendered text, so the model-facing framing can no longer decide whether a republish is needed, and the text-slicing that recovered entries from a logged message is gone. A resumed session whose newest catalog predates this change republishes once, which the pre-release stance permits. One case does not self-heal: if that old-format catalog is the only one and the current view has no skills, the plugin sees no published catalog and emits no tombstone, so the model keeps a stale catalog nothing replaces. The pre-release stance ("backends reject old on-disk formats") permits it; it is recorded here rather than left to the optimistic path. + +**`snapshot`** — current state that a later snapshot from the same producer supersedes. The runtime-context snapshot, `time-context`, and `tmux-context` declare it. `renderContextSections()` exposes the assembly's named contributions, which `renderContextSnapshot()` already joined for the model, so the body attributes each part to the subsystem that produced it without re-splitting joined prose. The two single-contribution producers record one section each. The cleared runtime-context marker has no contributions left and declares no form. + +**`notice`** — a one-off account of something that just happened. `tool-tasks`, `tool-goal` wrap-up, `plan-mode` switches, and `repeat-tool-guard` reminders declare it with a `summary`, which rides the **collapsed** row: a notice is meant to be read without expanding at all. The summary is bounded where its inputs are caller text (a task's label and status detail have no length of their own). Goal state changes remain domain-owned `goal/change` events rather than model context, so they declare no form. + +**`relay`** — a message another agent addressed to this one. Both subagent-addressed sources declare it; the sender is shown as the opaque session id the source already records, because this client cannot resolve it to a title. + +**`recall`** — material lifted out of another session's log. `session-reference` declares it and needed no new field: its references already record the label, retained and omitted counts, and truncation flag, which the body shows first, because recalled context is bounded on the way in and a card that hid the omitted count would overstate what the model received. + +Both readers are **all-or-nothing**: one unreadable entry disqualifies the record rather than being dropped, because a body that replaces the model-facing text must not present a confident but incomplete account of what the model read. The row's form marker reports what actually rendered, not what was declared. + +The producer side validates the same durable data with the same posture. `catalogHistory` reads `source.entries` out of `agent.session.events`, which on resume or fork is a JSONL/SQLite seed whose validation only guarantees a source object with a non-empty `kind` — no per-kind field is checked. An unreadable catalog is therefore skipped as "not this plugin's record", the posture the replaced content digest had; throwing there would fail every later step of that session at the latest, least diagnosable point. + +Everything else — including a form this UI version does not present, a form absent from the source, and a `catalog` whose entries are unusable — renders the **opaque** body: the model-facing text with its real line breaks, then the remaining provenance as fields. Opaque is the documented default, not a leftover bin. A resumed, forked, or foreign log must render whether or not its producer is mounted here, which is also why the classification lives in the durable source rather than in a client-side table keyed by producer. + +## Why not a presenter registry + +The tool seam pairs its vocabulary with `presentCall(args)`, a host-side pure function each tool implements. Context deliberately has no equivalent, because the input differs in ownership: a tool's `args` are generated by the **model** against a model-facing schema, so a translation step is unavoidable; a context `source` is constructed by the **producing plugin** itself, under no external constraint, and can simply record the facts a presentation needs. Adding a registry would have bought a translation nobody needs, at the cost of a host computation point, a wire field per context message, and a browser bundle for every producing package (the client purity gate forbids host packages from contributing components). + +## Alternatives considered + +**Map source kinds to renderers in the client.** Cheapest to write and requires no format change, but it puts producer knowledge back in the client: every new kind then needs a client release to render as anything but opaque, and a foreign log cannot be classified at all. It also reintroduces exactly the coupling the [source and steer marks decision](2026-08-04-web-context-source-and-steer-marks.md) removed for labels. + +**Reuse `kind` as the form.** One discriminant is simpler, and `workspace-instructions` is already 1:1 with its form. It breaks on the shared shapes: three producers emit runtime snapshots today, and folding them into one kind would erase their provenance. Two axes keep provenance exact while letting presentations be shared. + +**Let the client parse the model-facing prose.** The entries and file sections are visibly structured in the text. Parsing them couples the presentation to prompt wording, so every reword silently breaks a card — the same reason catalog identity moved off the text. + +**Render instructions as Markdown.** The body is a Markdown file and would read better rendered. The text also carries `` framing, which the Markdown renderer drops as raw HTML, so a Markdown body would silently hide part of what the model read. Deferred until the producer records per-file content structurally. + +## Testing + +- `packages/client/runtime` pins the form projection, including the unknown, empty, wrongly-typed, and absent values that must degrade to opaque. +- `packages/client/ui-conversation` pins each body: the opaque body's preserved line breaks and provenance fields, the instructions body's file list and verbatim framing, the catalog body's entry list, and a catalog with unusable entries falling back to opaque. +- `packages/skill/tool-skill` pins the new source on first publication and replacement, republish behavior driven by the durable entries, and a malformed durable catalog leaving step observation intact. +- The keyless assembled-Web seeded-history scenario expands a real `instructions` context in Chromium and asserts its file list, verbatim framing, and the unchanged disclosure geometry. `catalog` has no assembled coverage: the hermetic scaffold publishes no skills, so no catalog reaches a browser scenario. + +## Consequences + +- A reader can tell what was added without expanding, and reading it no longer means reading escaped JSON. +- The durable `MessageSource` now carries a semantic classification beside provenance. The boundary is load-bearing: facts and shape only, never presentation. A producer that wants a better card records better facts. +- Catalog identity no longer depends on the model-facing prose, deleting the text-slicing path that could mistake a reworded catalog for a changed one. +- Every shipped producer except the two hook bridges now declares a form. The bridges stay opaque by design: their content is whatever an external program printed, so no shape can be promised for it. Unknown kinds and unreadable records land there too. +- `ContextFormed` is discriminated by `form`, so a producer cannot declare a shape without the facts that shape is presented from — a `notice` without its summary, or a `snapshot` without its sections, fails to compile. diff --git a/.agents/notes/implemented/feature/2026-08-05-context-form-vocabulary.zh.md b/.agents/notes/implemented/feature/2026-08-05-context-form-vocabulary.zh.md new file mode 100644 index 0000000000..66278b261f --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-05-context-form-vocabulary.zh.md @@ -0,0 +1,73 @@ +# Agent Note:由生产方声明的上下文形态 + +Status: implemented + +[English](2026-08-05-context-form-vocabulary.md) | 中文 + +## Problem + +每一条已记录的非用户 `user/message` 都通过同一个内容区渲染:把整条消息序列化成内联 JSON。读者展开一行,看到的是 `{ "content": [ { "type": "text", "text": "…\n\n…" } ], "source": { … } }`——转义把唯一值得读的东西(面向模型的散文)压成了一行,而来源信息又和它挤在同一坨里。 + +在标题栏写出生产者([来源与 steer 标识决策](2026-08-04-web-context-source-and-steer-marks.md))解决了「这是谁加的」。它解决不了「加进来的是什么东西」,因为日志里根本没有这句话。注入上下文不是一种形状:对账后的 `AGENTS.md`、可用 skill 的目录、运行时策略快照、子 agent 的汇报,彼此之间的差别不亚于终端卡片与 diff 卡片,然而这四者呈现出来是同一堵转义 JSON 的墙。 + +工具面早就解决过同一个形状问题。`ToolCallView` 只有三种卡片,而不是每个工具一种,由工具自己声明本次调用属于哪一种。上下文没有对应物:既没有形状词汇表,生产方也无从声明自己发出的是哪一种。 + +## Decision + +`MessageSource` 新增一个可选、由生产方声明的 `form: ContextForm`——一份关于信息**形状**的小型 tagged 词汇表,与 `kind` 相互独立: + +- `kind` 回答**由谁产生**,保持纯粹的溯源语义。 +- `form` 回答**这是何种形态的信息**。多个生产方可以共用一种形态,一个生产方在一次会话中也可以发出多种。 + +该词汇表是语义的,绝不涉及视觉。取值只陈述「内容是某个文件的指令」或「是一份可用项目录」;颜色、图标、排序、默认折叠状态归消费方管,不得进入这个联合类型。它随生产方补齐各自形态所需的结构化字段而逐个增长。本次声明两个: + +**`instructions`**——从工作区文件中读出的指令。`workspace-context` 在启动基线与后续增量上都声明它;其既有的 `changes[]` 已经携带了呈现所需的路径、动作与 digest,因此没有新增字段。内容区在正文之上列出对账过的文件,并原样保留 `` 包装:那层包装本就是模型读到的一部分,隐藏它会歪曲这次请求。 + +**`catalog`**——本会话可用项的目录,随变化重新发布。`dsh-tool-skill` 从共享的 `plugin` kind 迁到自有的 `skill-catalog` 来源,携带 `entries`(本次发布的 `name`/`description` 对)与替换目录上的 `update`,后者由内容区渲染成替换提示。内容区直接列出这些条目,不再从散文里反解 `` 块。 + +条目记录的是**未转义**的发布事实。伪 XML 转义属于 `` 这层为模型而设的框架,因此只在渲染该框架时施加、从不存储;否则消费方要正确展示含 `<` 的描述就得知道框架的编码方式,本决策刚移除的框架知识会换一种形式泄漏回来。`escapeText` 确定且单射,故对未转义条目取 digest 与此前完全等价,重新发布语义不变,面向模型的文本逐字节不变。 + +这次迁移同时挪动了目录的**身份**:重新发布用的 digest 现在覆盖持久条目而非渲染文本,于是面向模型的包装再也无法左右是否需要重新发布,那段从已记录消息里切出条目的文本切分逻辑也随之删除。若恢复的会话中最新目录早于本次改动,会重新发布一次——发布前阶段的姿态允许这样做。有一种情形不会自愈:当那份旧格式目录是唯一的一份、且当前视图没有任何 skill 时,插件看不到已发布目录,也就不会发出 tombstone,模型手里会留着一份无人替换的过期目录。发布前阶段的姿态(「后端拒绝旧的磁盘格式」)允许这一点;此处如实记录,而不是只写乐观路径。 + +**`snapshot`**——会被同一生产方后续快照取代的当前状态。运行时快照、`time-context`、`tmux-context` 声明它。`renderContextSections()` 暴露出装配时的具名贡献——`renderContextSnapshot()` 本来就是把它们拼给模型的——因此内容区能把每一段归属到产生它的子系统,而不必去切分已经拼好的散文。两个单贡献生产方各记录一段。运行时快照的「已清空」标记没有任何贡献可归属,因此不声明形态。 + +**`notice`**——刚刚发生了什么的一次性说明。`tool-tasks`、`tool-goal` 收尾、`plan-mode` 切换与 `repeat-tool-guard` 提醒都带 `summary` 声明它,而该摘要出现在**折叠态**行上:notice 的全部意义就是不展开也能读完。摘要在其输入是调用方文本时自行封顶(任务的 label 与状态 detail 本身没有长度约束)。Goal 状态变更仍是由领域层持有的 `goal/change` 事件,而非模型上下文,因此不声明 form。 + +**`relay`**——另一个 agent 发给本 agent 的消息。两个子 agent 定向来源都声明它;发送方以来源已记录的不透明会话 id 呈现,因为本客户端无法把它解析成标题。 + +**`recall`**——从另一个会话日志搬来的材料。`session-reference` 声明它,且不需要新增字段:其 references 已经记录了标题、保留与省略条数、截断标记,内容区把这些放在最前面——召回上下文在进入时是有界的,隐藏省略条数的卡片会夸大模型实际收到的内容。 + +两个读取器都是**全有或全无**:一条不可读的条目即判定整条记录不可用,而不是把它丢掉——会替换掉面向模型文本的内容区,不得给出自信但残缺的「模型读到了什么」。行上的形态标记报告的是实际渲染出的形态,而非声明的形态。 + +生产方一侧对同一份持久数据采取同样的姿态。`catalogHistory` 从 `agent.session.events` 读 `source.entries`,而恢复或 fork 时它来自 JSONL/SQLite 种子,种子验证只保证来源是带非空 `kind` 的对象,不校验任何 kind 特有字段。因此不可读的目录被当作「不是本插件的记录」跳过——正是被替换掉的内容 digest 原有的姿态;在那里抛错会让该会话此后每一步都在最晚、最难定位的点失败。 + +其余一切——包括本 UI 版本不呈现的形态、来源未声明形态、以及条目不可用的 `catalog`——一律渲染 **opaque** 内容区:按真实换行展示面向模型的文本,其后把剩余来源信息列成字段。opaque 是有文档的默认,不是兜底垃圾桶。恢复的、fork 的、外部写入的日志,无论其生产方是否挂载在此处都必须渲染得出来——这同样是分类信息必须落在持久来源里、而不是落在客户端以生产方为键的表里的原因。 + +## 为什么不做 presenter 注册表 + +工具接缝把它的词汇表与 `presentCall(args)` 配对,那是每个工具在 host 侧实现的纯函数。上下文刻意不设对应物,因为输入的归属不同:工具的 `args` 由**模型**按面向模型的 schema 生成,翻译步骤无法回避;而上下文的 `source` 由**生产方插件**自己构造,不受任何外部约束,完全可以直接记录呈现所需的事实。加一层注册表买到的是一次没人需要的翻译,代价却是一个 host 计算点、每条上下文消息一个 wire 字段、以及每个生产方包都要出浏览器 bundle(客户端纯度门禁禁止 host 包贡献组件)。 + +## Alternatives considered + +**在客户端把来源 kind 映射到渲染器。** 写起来最省,也不用改格式,但它把生产方知识放回了客户端:此后每新增一个 kind 都要客户端发版才能渲染成 opaque 以外的东西,而外部日志根本无法分类。它还会重新引入[来源与 steer 标识决策](2026-08-04-web-context-source-and-steer-marks.md)刚为名称去掉的那种耦合。 + +**复用 `kind` 充当形态。** 单一判别式更简单,`workspace-instructions` 本来也与它的形态一一对应。但它在共享形状上就崩了:今天有三个生产方发出运行时快照,把它们并成一个 kind 会抹掉各自的溯源。两根轴既保住溯源的精确,又让呈现可以共享。 + +**让客户端解析面向模型的散文。** 条目与文件分节在文本里确实有可见结构。解析它们会把呈现耦合到 prompt 措辞上,于是每改一次文案就静默碎掉一张卡——这也正是目录身份从文本上迁走的原因。 + +**把 instructions 渲染成 Markdown。** 正文本来就是 Markdown 文件,渲染出来更好读。但文本同时携带 `` 包装,Markdown 渲染器会把它当原始 HTML 丢弃,于是 Markdown 内容区会悄悄隐藏模型读到的一部分。推迟到生产方按文件结构化记录内容之后再做。 + +## Testing + +- `packages/client/runtime` 钉住形态投影,包括必须降级为 opaque 的未知值、空值、类型不符与缺失。 +- `packages/client/ui-conversation` 逐个钉住内容区:opaque 的换行留存与来源字段、instructions 的文件列表与原样包装、catalog 的条目列表,以及条目不可用的 catalog 回落到 opaque。 +- `packages/skill/tool-skill` 钉住首次发布与替换时的新来源、由持久条目驱动的重新发布行为,以及畸形持久目录不打断步骤观察。 +- 无密钥的组装 Web seeded-history 场景在 Chromium 中展开一条真实的 `instructions` 上下文,断言其文件列表、原样包装与未改动的展开项几何。`catalog` 没有组装态覆盖:隔离脚手架不发布任何 skill,因此没有目录能进入浏览器场景。 + +## Consequences + +- 读者不展开就能知道加进来的是什么,展开之后读到的也不再是转义 JSON。 +- 持久 `MessageSource` 现在在溯源之外还承载一个语义分类。这条边界是承重的:只放事实与形状,绝不放呈现。想要更好卡片的生产方应当记录更好的事实。 +- 目录身份不再依赖面向模型的散文,删掉了那条可能把「改了措辞」误判为「改了内容」的文本切分路径。 +- 除两个 hook 桥接外,每个已发布的生产方现在都声明了形态。桥接按设计保持 opaque:其内容是外部程序打印出来的任意文本,无法承诺任何形状。未知 kind 与不可读记录同样落在这里。 +- `ContextFormed` 按 `form` 判别,因此生产方无法在缺少该形态所需事实的情况下声明它——没有 summary 的 `notice`、没有 sections 的 `snapshot`,都会编译失败。 diff --git a/.agents/notes/implemented/feature/2026-08-05-web-preview-product-badge.i18n.yaml b/.agents/notes/implemented/feature/2026-08-05-web-preview-product-badge.i18n.yaml new file mode 100644 index 0000000000..90d3d8e1f5 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-05-web-preview-product-badge.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# 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 .agents/notes/implemented/feature/2026-08-05-web-preview-product-badge.md +2026-08-05-web-preview-product-badge.md: c20dedf8caa497d17a577cf46261c6a24c09a1ce +2026-08-05-web-preview-product-badge.zh.md: c428dabf2a9a2ece2f90b0c3a527835a494c7cb7 diff --git a/.agents/notes/implemented/feature/2026-08-05-web-preview-product-badge.md b/.agents/notes/implemented/feature/2026-08-05-web-preview-product-badge.md new file mode 100644 index 0000000000..c20dedf8ca --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-05-web-preview-product-badge.md @@ -0,0 +1,33 @@ +# Agent Note: Web preview product badge + +Status: implemented + +English | [中文](2026-08-05-web-preview-product-badge.zh.md) + +## Problem + +The Web empty state does not identify the product as a preview. Users can enter the main session surface without seeing that the product is pre-release, while a deployment setting would misrepresent a product-wide lifecycle decision as an operator choice. + +## Decision + +The empty hero always renders a localized `Preview` / `预览版` badge beneath the headline. It has no configuration switch: preview status is one product identity shared by every deployment, not a deployment-varying tunable. + +The badge keeps the business-tertiary background so both themes retain the product-blue context, and uses the theme's primary label token for text. That pairing gives ordinary 12px text sufficient contrast in both light and dark themes; the business-primary foreground is reserved for larger or non-text accents because it does not reach the required contrast on this background. + +The badge leaves the product when the first tagged release removes the repository's pre-release stance, or when the owning product decision declares the preview phase complete. That change removes the badge and its locale key together rather than adding a runtime toggle. + +## Alternatives considered + +**Make preview status configurable.** Rejected because two deployments of the same pre-release product must not present different lifecycle identities, and a configuration field would turn product release state into an unsupported operator choice. + +**Use business-primary text on the business-tertiary background.** Rejected because the resulting light- and dark-theme contrast is below the 4.5:1 requirement for the badge's 12px text. + +**Hide the badge from the accessibility tree.** Rejected because preview status is product information rather than decoration; the accessible headline therefore includes the badge text. + +## Consequences + +Every new session exposes the same localized preview identity in visual and accessibility output. Removing preview status is an explicit product-release edit, and the badge favors readable neutral text over an all-blue treatment while retaining the business-tinted background. + +## Testing + +The conversation component test covers both localized badge values, and the Web lifecycle snapshots pin the English badge in the assembled empty hero. diff --git a/.agents/notes/implemented/feature/2026-08-05-web-preview-product-badge.zh.md b/.agents/notes/implemented/feature/2026-08-05-web-preview-product-badge.zh.md new file mode 100644 index 0000000000..c428dabf2a --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-05-web-preview-product-badge.zh.md @@ -0,0 +1,33 @@ +# Agent Note:Web 预览版产品徽标 + +状态:已实现 + +[English](2026-08-05-web-preview-product-badge.md) | 中文 + +## 问题 + +Web 空状态没有标明产品处于预览版阶段。用户可以在未看到产品尚未正式发布的情况下进入主会话界面;若改用部署设置,则会把面向整个产品的生命周期决策误表述为操作者的选择。 + +## 决策 + +空状态主视觉区始终在标题下方渲染本地化的 `Preview` / `预览版` 徽标。它没有配置开关:预览状态是所有部署共同的一项产品身份,而不是随部署变化的可调参数。 + +徽标沿用 business-tertiary 背景,使两套主题都保留产品蓝的视觉语境;文字则使用主题的 primary label token。这一组合让普通 12px 文字在浅色与暗色主题下都有足够的对比度。business-primary 前景色仅留给较大字号文本或非文本强调元素,因为它在该背景上达不到要求的对比度。 + +首个 tagged release 取消仓库的预发布立场时,或归属产品方明确决定预览阶段结束时,产品会移除该徽标。这一改动会同时移除徽标及其 locale key,而不是增加运行时开关。 + +## 曾考虑的替代方案 + +**让预览状态可配置。** 不予采纳:同一个预发布产品的两套部署不得展示不同的生命周期身份,配置字段还会把产品发布状态变成一项不受支持的操作者选择。 + +**在 business-tertiary 背景上使用 business-primary 文字。** 不予采纳:由此产生的浅色与暗色主题对比度低于徽标 12px 文字所要求的 4.5:1。 + +**在无障碍树中隐藏徽标。** 不予采纳:预览状态是产品信息而非装饰,因此无障碍标题会包含徽标文字。 + +## 后果 + +每个新会话都会在视觉与无障碍输出中呈现相同的本地化预览版身份。移除预览状态是一项显式的产品发布改动;徽标保留业务蓝色调背景,同时采用可读的中性色文字,而不是全蓝色处理方案。 + +## 测试 + +会话组件测试覆盖两个本地化徽标值,Web 生命周期快照则固定组装后空状态主视觉区中的英文徽标。 diff --git a/.agents/notes/implemented/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.i18n.yaml b/.agents/notes/implemented/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.i18n.yaml index 82e786ec60..00c51f7dff 100644 --- a/.agents/notes/implemented/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.i18n.yaml @@ -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 .agents/notes/implemented/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md -2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md: 25118af755d45211a6f3ff4339f7c952168d866c -2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.zh.md: bd2f8709814f5462afec48d30b3e4e48eb4e4848 +2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md: e2d821f3951af472ef1a13b7b6df88a3aa96a318 +2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.zh.md: b55d4a271e0c5f2729222f4652fb0cb43e5cc9f9 diff --git a/.agents/notes/implemented/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md b/.agents/notes/implemented/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md index 25118af755..e2d821f395 100644 --- a/.agents/notes/implemented/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md +++ b/.agents/notes/implemented/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md @@ -22,19 +22,20 @@ Keep host and runtime steering intact. Remove only the Web UI entry and chrome: **Delete host steering entirely.** Out of scope; the user asked only for Web UI display and entry. Agent-loop drain, session events, and the wire mode remain load-bearing for ACP/TUI/automation. -**Hide `steering/message` from the transcript.** Would lie on replay when an external client steers; rejected in favor of a plain bubble. +**Hide durable steer `user/message` content from the transcript.** Would lie on replay when an external client steers; rejected in favor of a plain bubble. **Keep the mode parameter but only ever pass `'queue'`.** Leaves dead API surface and tests that invent `'steer'` paths the composer cannot reach. ## Consequences -- Web users cannot steer from the composer or `ctx.conversation.send`; stop/cancel and Queue remain the only mid-turn controls. -- Host-wire and non-Web clients can still steer; the Web client shows those messages without labeling them as interjections. +- **Superseded in part.** Decision bullets 1 and 3 through 5 no longer describe master: composer steering shipped later, and the [context-source and steer marks decision](../feature/2026-08-04-web-context-source-and-steer-marks.md) owns its caption. The current facts follow. +- Host steering ownership is unchanged: agent-loop drain, session events, and the wire mode remain load-bearing for ACP, automation, and non-Web clients. +- `ConversationService.send(text)` still takes no mode and always queues; the composer's Steer gesture uses `session.prompt(mode: 'steer')` instead. +- Durable steer `user/message` content still folds into the transcript, so an externally submitted steer stays truthful on replay. It now carries the interjection caption instead of rendering as a bare bubble. - Non-user next-step items (`agent.inject` context: approval notices, task completion, attached snapshots) broadcast with the `context` placement and never render as pending steering bubbles; they stay invisible until claimed as durable `user/message` context cards. -- Reintroducing a dedicated steer UI would need a new product decision; do not revive the mode union or badge without one. ## Testing -- `packages/client/ui-conversation` unit/jsdom coverage: input machine enter/sink, ConversationService routing, MessageItem steering arm (no 「插话」), InputBar submit. -- `apps/web/tests/steering.e2e.ts` keyless replay plus updated `settled.expected.md` (steer text without badge). +- `packages/client/ui-conversation` unit/jsdom coverage: input machine enter/sink, ConversationService routing, the MessageItem steering arm, InputBar submit. +- `apps/web/tests/steering.e2e.ts` keyless replay plus its goldens, which pin the caption. - `packages/host/apiproxy` `session/queue` projection test asserts user-origin next-step items stay `steering` while plugin-origin items land as `context`. diff --git a/.agents/notes/implemented/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.zh.md b/.agents/notes/implemented/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.zh.md index bd2f870981..b55d4a271e 100644 --- a/.agents/notes/implemented/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.zh.md +++ b/.agents/notes/implemented/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.zh.md @@ -22,19 +22,20 @@ Status: implemented **整段删除 host steering。** 超出范围;用户只要求清 Web UI 展示与入口。agent-loop 排空、session 事件与线缆 mode 对 ACP/TUI/自动化仍是承重能力。 -**在 transcript 中隐藏 `steering/message`。** 外部客户端 steer 时回放会撒谎;改为普通气泡。 +**在 transcript 中隐藏持久 steer `user/message` 内容。** 外部客户端 steer 时回放会失真,因此改为普通气泡。 **保留 mode 参数但永远只传 `'queue'`。** 留下死 API 面与只会虚构 composer 到不了的 `'steer'` 路径的测试。 ## 后果 -- Web 用户无法从 composer 或 `ctx.conversation.send` steer;中途控制只剩停止/取消与 Queue。 -- Host 线缆与非 Web 客户端仍可 steer;Web 客户端展示这些消息时不再标成插话。 +- **部分被取代。** 决策中的第 1 条和第 3 至 5 条已经不再描述 master:composer steering 后来已经交付,[上下文来源与 steer 标识决策](../feature/2026-08-04-web-context-source-and-steer-marks.md)负责定义其标注。下面列出当前事实。 +- host 侧 steering 的归属未变:agent-loop 排空、session 事件与线缆 mode 对 ACP、自动化和非 Web 客户端仍然必要。 +- `ConversationService.send(text)` 仍然不接 mode,始终排队;composer 的 Steer 手势改走 `session.prompt(mode: 'steer')`。 +- 持久 steer `user/message` 内容仍然折叠进 transcript,因此外部提交的 steer 会如实出现在回放中。它现在带有插话标注,而不是无标识气泡。 - 非用户来源的 next-step 项(`agent.inject` 上下文:审批通知、任务完成、附加快照)以 `context` placement 广播,绝不渲染为待处理 steering 气泡;领取为持久 `user/message` context card 前保持不可见。 -- 若要重新引入专用 steer UI,需要新的产品决策;没有决策就不要复活 mode 联合类型或徽章。 ## 测试 -- `packages/client/ui-conversation` unit/jsdom 覆盖:input machine enter/sink、ConversationService 路由、MessageItem steering 分支(无「插话」)、InputBar submit。 -- `apps/web/tests/steering.e2e.ts` 无密钥回放,以及更新后的 `settled.expected.md`(有 steer 正文、无徽章)。 +- `packages/client/ui-conversation` unit/jsdom 覆盖:input machine enter/sink、ConversationService 路由、MessageItem steering 分支、InputBar submit。 +- `apps/web/tests/steering.e2e.ts` 无密钥回放及其黄金基线,后者会检查插话标注。 - `packages/host/apiproxy` 的 `session/queue` 投影测试断言用户来源的 next-step 项保持 `steering`,而插件来源的项落入 `context`。 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index e6907730fe..afe2b4b626 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -47,6 +47,7 @@ External packages that a workspace package resolves at runtime. `scripts/install | [`@shikijs/langs`](https://github.com/shikijs/shiki) | MIT | | [`@standard-schema/spec`](https://github.com/standard-schema/standard-schema) | MIT | | [`@tanstack/react-virtual`](https://github.com/TanStack/virtual) | MIT | +| [`@types/mdast`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@vscode/ripgrep`](https://github.com/microsoft/vscode-ripgrep) | MIT | | [`anser`](https://github.com/IonicaBizau/anser) | MIT | | [`chokidar`](https://github.com/paulmillr/chokidar) | MIT | @@ -63,10 +64,14 @@ External packages that a workspace package resolves at runtime. `scripts/install | [`koffi`](https://github.com/Koromix/koffi) | MIT | | [`mdast-util-from-markdown`](https://github.com/syntax-tree/mdast-util-from-markdown) | MIT | | [`mdast-util-gfm`](https://github.com/syntax-tree/mdast-util-gfm) | MIT | +| [`mdast-util-math`](https://github.com/syntax-tree/mdast-util-math) | MIT | +| [`micromark-core-commonmark`](https://github.com/micromark/micromark/tree/main/packages/micromark-core-commonmark) | MIT | | [`micromark-extension-gfm`](https://github.com/micromark/micromark-extension-gfm) | MIT | | [`micromark-extension-math`](https://github.com/micromark/micromark-extension-math) | MIT | | [`micromark-factory-space`](https://github.com/micromark/micromark/tree/main/packages/micromark-factory-space) | MIT | | [`micromark-util-character`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-character) | MIT | +| [`micromark-util-classify-character`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-classify-character) | MIT | +| [`micromark-util-sanitize-uri`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-sanitize-uri) | MIT | | [`micromark-util-symbol`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-symbol) | MIT | | [`micromark-util-types`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-types) | MIT | | [`node-addon-require-builtin`](https://www.npmjs.com/package/node-addon-require-builtin) | MIT | @@ -75,10 +80,6 @@ External packages that a workspace package resolves at runtime. `scripts/install | [`pnpm`](https://github.com/pnpm/pnpm) | MIT | | [`react`](https://github.com/facebook/react) | MIT | | [`react-dom`](https://github.com/facebook/react) | MIT | -| [`react-markdown`](https://github.com/remarkjs/react-markdown) | MIT | -| [`rehype-katex`](https://github.com/remarkjs/remark-math/tree/main/packages/rehype-katex) | MIT | -| [`remark-gfm`](https://github.com/remarkjs/remark-gfm) | MIT | -| [`remark-math`](https://github.com/remarkjs/remark-math/tree/main/packages/remark-math) | MIT | | [`shiki`](https://github.com/shikijs/shiki) | MIT | | [`supports-color`](https://github.com/chalk/supports-color) | MIT | | [`tsx`](https://github.com/privatenumber/tsx) | MIT | @@ -109,7 +110,6 @@ External packages **directly declared** only by repository tooling, test infrast | [`@types/babel__code-frame`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/js-yaml`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/jsdom`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | -| [`@types/mdast`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/node`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/picomatch`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/react`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | diff --git a/apps/web/tests/markdown-cjk-strong.e2e.ts b/apps/web/tests/markdown-cjk-strong.e2e.ts new file mode 100644 index 0000000000..0c57c513fe --- /dev/null +++ b/apps/web/tests/markdown-cjk-strong.e2e.ts @@ -0,0 +1,132 @@ +import { fileURLToPath } from 'node:url' +import type { Browser, Page } from 'playwright' +import { chromium } from 'playwright' +import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' +import { createMessage, createUserMessage } from '@deepseek-ai/dsh-llm' +import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session' +import type {} from '@deepseek-ai/dsh-session-title' +import { + assertFixtureInventory, + captureStableAria, + compareOrRefreshGolden, + launchWebScaffold, + seedSession, + watchConsole, + webSnapshotMode, + type WebScaffold, +} from './scaffold.ts' +import { newEnglishPage, saveFailureShot } from './support.ts' + +const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/markdown-cjk-strong', import.meta.url)) +const UI_EXPECTED = fileURLToPath(new URL('./snapshots/markdown-cjk-strong/ui.expected.md', import.meta.url)) +const MODE = webSnapshotMode() +const SEED_ID = 'markdown-cjk-strong-web-e2e' +const DONE = 'CJK_STRONG_DONE' +const CASES = [ + ['**注意:**内容', '注意:', '注意:内容'], + ['**Notice:**内容', 'Notice:', 'Notice:内容'], + ['**事件中间件(waterfall)**实现', '事件中间件(waterfall)', '事件中间件(waterfall)实现'], + ['**事件中间件(waterfall)**实现', '事件中间件(waterfall)', '事件中间件(waterfall)实现'], + ['**句号。**后续', '句号。', '句号。后续'], + ['**Period.**后续', 'Period.', 'Period.后续'], + ['**提醒!**继续', '提醒!', '提醒!继续'], + ['**Warning!**继续', 'Warning!', 'Warning!继续'], +] as const + +/** Build one settled assistant reply covering CJK-adjacent strong punctuation boundaries. */ +function markdownFixture(): string { + const session = Session.create(SessionId('markdown-cjk-strong-source')) + const eventTimeOrigin = new Date().setHours(12, 0, 0, 0) + session.append('turn/start', { turn: 1 }) + const user = session.append('user/message', createUserMessage({ + content: [{ type: 'text', text: 'Render adjacent CJK strong emphasis.' }], + source: { kind: 'user' }, + }), { surfaceOp: 'append' }) + session.append('session/title', { + title: 'CJK strong emphasis', + messageSeqs: [user.seq], + source: { kind: 'fallback' }, + }) + session.append('step/start', { turn: 1, step: 1 }) + session.append('assistant/message', { + turn: 1, + step: 1, + message: createMessage({ + role: 'assistant', + content: [{ + type: 'text', + text: [ + '## CJK strong emphasis', + '', + ...CASES.flatMap(([markdown]) => [markdown, '']), + DONE, + ].join('\n'), + }], + source: { kind: 'model', provider: 'fixture', model: 'fixture' }, + }), + }, { surfaceOp: 'append' }) + session.append('step/end', { turn: 1, step: 1 }) + session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + + return [ + JSON.stringify({ + type: 'session', + version: SESSION_FORMAT_VERSION, + id: '{{sessionId}}', + createdAt: 0, + cwd: '{{cwd}}', + }), + ...session.events.map(event => JSON.stringify({ + ...event, + time: eventTimeOrigin + event.seq * 1_000, + })), + '', + ].join('\n') +} + +describe('web e2e: CJK-adjacent Markdown strong emphasis', () => { + let scaffold: WebScaffold + let browser: Browser + let page: Page + let tripwire: ReturnType + + beforeAll(async () => { + scaffold = await launchWebScaffold({}) + await seedSession(scaffold, markdownFixture(), SEED_ID) + browser = await chromium.launch() + page = await newEnglishPage(browser) + tripwire = watchConsole(page) + await page.goto(scaffold.baseUrl, { waitUntil: 'load' }) + await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + }, 120_000) + + afterAll(async () => { + await browser?.close() + await scaffold?.close() + }) + + it.skipIf(MODE === 'record')('renders punctuation-terminated strong spans before adjacent CJK text', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-markdown-cjk-strong')) + const groupRow = page.locator('[role="treeitem"]').first() + await groupRow.waitFor({ timeout: 15_000 }) + await groupRow.click() + const sessionRow = page.locator('[role="treeitem"]').nth(1) + await sessionRow.waitFor({ timeout: 10_000 }) + await sessionRow.click() + await expect.poll(() => page.getByText(DONE, { exact: true }).count(), { timeout: 15_000 }).toBe(1) + + const strong = page.locator('[class*="markdown"] strong') + await expect.poll(() => strong.count(), { timeout: 10_000 }).toBe(CASES.length) + expect(await strong.allTextContents()).toEqual(CASES.map(([, expected]) => expected)) + for (const [, , paragraph] of CASES) { + expect(await page.getByText(paragraph, { exact: true }).count()).toBe(1) + } + + const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)) + .split(SEED_ID).join('{{seededId}}') + await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE) + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md']) + }, 60_000) +}) diff --git a/apps/web/tests/markdown-inline-code-links.e2e.ts b/apps/web/tests/markdown-inline-code-links.e2e.ts new file mode 100644 index 0000000000..7b5c466298 --- /dev/null +++ b/apps/web/tests/markdown-inline-code-links.e2e.ts @@ -0,0 +1,142 @@ +import { fileURLToPath } from 'node:url' +import type { Browser, Page } from 'playwright' +import { chromium } from 'playwright' +import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' +import { createMessage, createUserMessage } from '@deepseek-ai/dsh-llm' +import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session' +import type {} from '@deepseek-ai/dsh-session-title' +import { + assertFixtureInventory, + captureStableAria, + compareOrRefreshGolden, + launchWebScaffold, + seedSession, + watchConsole, + webSnapshotMode, + type WebScaffold, +} from './scaffold.ts' +import { newEnglishPage, saveFailureShot } from './support.ts' + +const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/markdown-inline-code-links', import.meta.url)) +const UI_EXPECTED = fileURLToPath(new URL('./snapshots/markdown-inline-code-links/ui.expected.md', import.meta.url)) +const MODE = webSnapshotMode() +const SEED_ID = 'markdown-inline-code-links-web-e2e' +const DONE = 'INLINE_CODE_LINK_DONE' + +/** Build a settled assistant reply with linkable URL code and inert code controls. */ +function markdownFixture(linkUrl: string): string { + const session = Session.create(SessionId('markdown-inline-code-links-source')) + const eventTimeOrigin = new Date().setHours(12, 0, 0, 0) + session.append('turn/start', { turn: 1 }) + const user = session.append('user/message', createUserMessage({ + content: [{ type: 'text', text: 'Show the local preview URL.' }], + source: { kind: 'user' }, + }), { surfaceOp: 'append' }) + session.append('session/title', { + title: 'Inline code links', + messageSeqs: [user.seq], + source: { kind: 'fallback' }, + }) + session.append('step/start', { turn: 1, step: 1 }) + session.append('assistant/message', { + turn: 1, + step: 1, + message: createMessage({ + role: 'assistant', + content: [{ + type: 'text', + text: [ + '## Inline code links', + '', + `Preview: \`${linkUrl}\``, + '', + `Standard: [Open preview](${linkUrl})`, + '', + `Command: \`curl ${linkUrl}\``, + '', + 'Unsafe: `javascript:alert(1)`', + '', + DONE, + ].join('\n'), + }], + source: { kind: 'model', provider: 'fixture', model: 'fixture' }, + }), + }, { surfaceOp: 'append' }) + session.append('step/end', { turn: 1, step: 1 }) + session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + + return [ + JSON.stringify({ + type: 'session', + version: SESSION_FORMAT_VERSION, + id: '{{sessionId}}', + createdAt: 0, + cwd: '{{cwd}}', + }), + ...session.events.map(event => JSON.stringify({ + ...event, + time: eventTimeOrigin + event.seq * 1_000, + })), + '', + ].join('\n') +} + +describe('web e2e: Markdown inline-code links', () => { + let scaffold: WebScaffold + let browser: Browser + let page: Page + let linkUrl: string + let tripwire: ReturnType + + beforeAll(async () => { + scaffold = await launchWebScaffold({}) + linkUrl = new URL('/?demo=1', scaffold.baseUrl).toString() + await seedSession(scaffold, markdownFixture(linkUrl), SEED_ID) + browser = await chromium.launch() + page = await newEnglishPage(browser) + tripwire = watchConsole(page) + await page.goto(scaffold.baseUrl, { waitUntil: 'load' }) + await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + }, 120_000) + + afterAll(async () => { + await browser?.close() + await scaffold?.close() + }) + + it.skipIf(MODE === 'record')('opens a complete HTTP URL from inline code and leaves other code inert', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-markdown-inline-code-links')) + const groupRow = page.locator('[role="treeitem"]').first() + await groupRow.waitFor({ timeout: 15_000 }) + await groupRow.click() + const sessionRow = page.locator('[role="treeitem"]').nth(1) + await sessionRow.waitFor({ timeout: 10_000 }) + await sessionRow.click() + await expect.poll(() => page.getByText(DONE, { exact: true }).count(), { timeout: 15_000 }).toBe(1) + + const inlineCodeLink = page.locator('[class*="markdown"] code a') + await expect.poll(() => inlineCodeLink.count(), { timeout: 10_000 }).toBe(1) + expect(await inlineCodeLink.getAttribute('href')).toBe(linkUrl) + expect(await inlineCodeLink.getAttribute('target')).toBe('_blank') + expect(await inlineCodeLink.getAttribute('rel')).toBe('noopener noreferrer') + await inlineCodeLink.focus() + expect(await inlineCodeLink.evaluate(element => document.activeElement === element)).toBe(true) + + const popupPromise = page.waitForEvent('popup') + await inlineCodeLink.click() + const popup = await popupPromise + await popup.waitForURL(linkUrl, { timeout: 15_000 }) + expect(popup.url()).toBe(linkUrl) + await popup.close() + + expect(await page.getByText(`curl ${linkUrl}`, { exact: true }).locator('a').count()).toBe(0) + expect(await page.getByText('javascript:alert(1)', { exact: true }).locator('a').count()).toBe(0) + const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)) + .split(SEED_ID).join('{{seededId}}') + .split(linkUrl).join('{{linkUrl}}') + await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE) + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md']) + }, 60_000) +}) diff --git a/apps/web/tests/seeded-history.e2e.ts b/apps/web/tests/seeded-history.e2e.ts index 813d77ff20..f9ef1055d1 100644 --- a/apps/web/tests/seeded-history.e2e.ts +++ b/apps/web/tests/seeded-history.e2e.ts @@ -262,6 +262,7 @@ describe('web e2e: seeded history renders through cold resume', () => { }], source: { kind: 'workspace-instructions', + form: 'instructions', baseline: true, changes: [{ action: 'set', @@ -271,7 +272,10 @@ describe('web e2e: seeded history renders through cold resume', () => { }], }, }), { surfaceOp: 'append' }) - await page.getByRole('button', { name: 'Context injection' }).waitFor({ timeout: 10_000 }) + // The header names the producer the durable source records, so the + // reconciled instruction file is readable without expanding the row. + await page.getByRole('button', { name: 'Context injection AGENTS.md', exact: true }) + .waitFor({ timeout: 10_000 }) }, 60_000) it.skipIf(MODE === 'record')('matches the historical conversation aria golden', async () => { @@ -288,7 +292,7 @@ describe('web e2e: seeded history renders through cold resume', () => { it.skipIf(MODE === 'record')('matches the Figma context disclosure geometry', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-context-injection')) - const disclosure = page.getByRole('button', { name: 'Context injection' }) + const disclosure = page.getByRole('button', { name: 'Context injection AGENTS.md', exact: true }) expect(await disclosure.getAttribute('aria-expanded')).toBe('false') const collapsedIcon = disclosure.locator('svg').first() const collapsedIconBox = await collapsedIcon.boundingBox() @@ -299,6 +303,10 @@ describe('web e2e: seeded history renders through cold resume', () => { await expect.poll(() => disclosure.getAttribute('aria-expanded')).toBe('true') const body = page.locator('[data-context-injection-body]') await body.waitFor({ timeout: 5_000 }) + // The instructions form names the file it reconciled above the text, and + // the text keeps the framing the model read rather than a cleaned excerpt. + expect(await body.locator('[data-context-files] li').allInnerTexts()).toEqual(['AGENTS.md\nloaded']) + expect(await body.locator('[data-context-text]').innerText()).toContain('') const headerBox = await disclosure.boundingBox() const bodyBox = await body.boundingBox() if (headerBox === null || bodyBox === null) throw new Error('context disclosure geometry is not measurable') @@ -399,13 +407,14 @@ describe('web e2e: seeded history renders through cold resume', () => { source: { kind: 'plugin', plugin: 'fixture' }, }), { surfaceOp: 'append' }) - const disclosures = page.getByRole('button', { name: 'Context injection' }) - await expect.poll(() => disclosures.count(), { timeout: 10_000 }).toBe(2) - const disclosure = disclosures.nth(1) + const disclosure = page.getByRole('button', { name: 'Context injection fixture', exact: true }) + await disclosure.waitFor({ timeout: 10_000 }) await disclosure.click() await expect.poll(() => disclosure.getAttribute('aria-expanded')).toBe('true') - const body = page.locator('[data-context-injection-body]') + // The instructions row above stays expanded from the geometry case; the + // opaque body is the one without a declared form. + const body = page.locator('[data-context-injection-body]:not([data-context-form])') const bodyBox = await body.boundingBox() if (bodyBox === null) throw new Error('short context disclosure geometry is not measurable') expect(bodyBox.height).toBeLessThan(141) diff --git a/apps/web/tests/snapshots/bash-abort-row/ui.expected.md b/apps/web/tests/snapshots/bash-abort-row/ui.expected.md index 7c975351d3..1b9e6aa339 100644 --- a/apps/web/tests/snapshots/bash-abort-row/ui.expected.md +++ b/apps/web/tests/snapshots/bash-abort-row/ui.expected.md @@ -10,10 +10,10 @@ - button "Branch into a new conversation" [disabled]: - img - text: Available only on the last message of a completed turn -- button "Context injection": +- button "Context injection @deepseek-ai/dsh-system-prompt": - img - img - - text: Context injection + - text: Context injection @deepseek-ai/dsh-system-prompt - 'button "Failed Bash Error: tool call aborted" [expanded]': - img - text: "Failed Bash Error: tool call aborted" diff --git a/apps/web/tests/snapshots/code-mode-round/ui.expected.md b/apps/web/tests/snapshots/code-mode-round/ui.expected.md index 6270a5429f..0c2cf8604c 100644 --- a/apps/web/tests/snapshots/code-mode-round/ui.expected.md +++ b/apps/web/tests/snapshots/code-mode-round/ui.expected.md @@ -10,10 +10,10 @@ - button "Branch into a new conversation" [disabled]: - img - text: Available only on the last message of a completed turn -- button "Context injection": +- button "Context injection @deepseek-ai/dsh-system-prompt": - img - img - - text: Context injection + - text: Context injection @deepseek-ai/dsh-system-prompt - 'button "Think The user wants me to write a single `run_code` program that:"': - img - img diff --git a/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md b/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md index 2442e9d927..33b1d6cd0f 100644 --- a/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md +++ b/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md @@ -10,10 +10,10 @@ - button "Branch into a new conversation" [disabled]: - img - text: Available only on the last message of a completed turn -- button "Context injection": +- button "Context injection @deepseek-ai/dsh-system-prompt": - img - img - - text: Context injection + - text: Context injection @deepseek-ai/dsh-system-prompt - button "Think The user wants me to:": - img - img diff --git a/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md b/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md index 37ae3ba520..aebc2a45b6 100644 --- a/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md +++ b/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md @@ -10,10 +10,10 @@ - button "Branch into a new conversation" [disabled]: - img - text: Available only on the last message of a completed turn -- button "Context injection": +- button "Context injection @deepseek-ai/dsh-system-prompt": - img - img - - text: Context injection + - text: Context injection @deepseek-ai/dsh-system-prompt - button "Think The user wants me to run a simple bash command and reply with \"DONE\".": - img - img diff --git a/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md b/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md index 497efc672f..728dc768f8 100644 --- a/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md +++ b/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md @@ -20,7 +20,7 @@ - button "Settings": - img - text: Settings -- text: Let's start building +- text: Let's start building Preview - button "Choose workspace": - img - text: workspace diff --git a/apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md b/apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md index 66d5b6f9ba..6b4d7633e5 100644 --- a/apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md +++ b/apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md @@ -20,7 +20,7 @@ - button "Settings": - img - text: Settings -- text: Let's start building +- text: Let's start building Preview - button "Choose workspace": - img - text: workspace diff --git a/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md b/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md index 5e554f289d..6b6671ec01 100644 --- a/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md +++ b/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md @@ -10,10 +10,10 @@ - button "Branch into a new conversation" [disabled]: - img - text: Available only on the last message of a completed turn -- button "Context injection": +- button "Context injection @deepseek-ai/dsh-system-prompt": - img - img - - text: Context injection + - text: Context injection @deepseek-ai/dsh-system-prompt - button "Think The user wants me to reply with a single word. Let me comply.": - img - img diff --git a/apps/web/tests/snapshots/live-interactions/cancel.expected.md b/apps/web/tests/snapshots/live-interactions/cancel.expected.md index 156b30f82a..9735b8acfe 100644 --- a/apps/web/tests/snapshots/live-interactions/cancel.expected.md +++ b/apps/web/tests/snapshots/live-interactions/cancel.expected.md @@ -10,10 +10,10 @@ - button "Branch into a new conversation" [disabled]: - img - text: Available only on the last message of a completed turn -- button "Context injection": +- button "Context injection @deepseek-ai/dsh-system-prompt": - img - img - - text: Context injection + - text: Context injection @deepseek-ai/dsh-system-prompt - paragraph: partial - text: Stopped - button "Copy": diff --git a/apps/web/tests/snapshots/live-interactions/error-auth.expected.md b/apps/web/tests/snapshots/live-interactions/error-auth.expected.md index 1a4aec678c..be1d936dd2 100644 --- a/apps/web/tests/snapshots/live-interactions/error-auth.expected.md +++ b/apps/web/tests/snapshots/live-interactions/error-auth.expected.md @@ -10,10 +10,10 @@ - button "Branch into a new conversation" [disabled]: - img - text: Available only on the last message of a completed turn -- button "Context injection": +- button "Context injection @deepseek-ai/dsh-system-prompt": - img - img - - text: Context injection + - text: Context injection @deepseek-ai/dsh-system-prompt - status: - text: This turn failedAPI key is invalid - code: AUTH diff --git a/apps/web/tests/snapshots/live-interactions/loading.expected.md b/apps/web/tests/snapshots/live-interactions/loading.expected.md index c50b440f86..6e81c87205 100644 --- a/apps/web/tests/snapshots/live-interactions/loading.expected.md +++ b/apps/web/tests/snapshots/live-interactions/loading.expected.md @@ -10,10 +10,10 @@ - button "Branch into a new conversation" [disabled]: - img - text: Available only on the last message of a completed turn -- button "Context injection": +- button "Context injection @deepseek-ai/dsh-system-prompt": - img - img - - text: Context injection + - text: Context injection @deepseek-ai/dsh-system-prompt - paragraph: partial - status: Deep diving... - textbox "Message the agent" diff --git a/apps/web/tests/snapshots/live-interactions/retry.expected.md b/apps/web/tests/snapshots/live-interactions/retry.expected.md index 91ddca897a..f127d3e8d1 100644 --- a/apps/web/tests/snapshots/live-interactions/retry.expected.md +++ b/apps/web/tests/snapshots/live-interactions/retry.expected.md @@ -10,10 +10,10 @@ - button "Branch into a new conversation" [disabled]: - img - text: Available only on the last message of a completed turn -- button "Context injection": +- button "Context injection @deepseek-ai/dsh-system-prompt": - img - img - - text: Context injection + - text: Context injection @deepseek-ai/dsh-system-prompt - group: - status: Retried model request (1/2) · {{duration}} - button "Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls.": diff --git a/apps/web/tests/snapshots/markdown-cjk-strong/ui.expected.md b/apps/web/tests/snapshots/markdown-cjk-strong/ui.expected.md new file mode 100644 index 0000000000..b28e30e4ef --- /dev/null +++ b/apps/web/tests/snapshots/markdown-cjk-strong/ui.expected.md @@ -0,0 +1,52 @@ +- banner: + - navigation "Session hierarchy": + - button "CJK strong emphasis" [disabled] + - tablist: + - tab "Chat" [selected] + - tab "Trajectory" +- text: Render adjacent CJK strong emphasis. {{clock}} +- button "Copy": + - img +- button "Branch into a new conversation" [disabled]: + - img +- text: Available only on the last message of a completed turn +- heading "CJK strong emphasis" [level=2] +- paragraph: + - strong: 注意: + - text: 内容 +- paragraph: + - strong: "Notice:" + - text: 内容 +- paragraph: + - strong: 事件中间件(waterfall) + - text: 实现 +- paragraph: + - strong: 事件中间件(waterfall) + - text: 实现 +- paragraph: + - strong: 句号。 + - text: 后续 +- paragraph: + - strong: Period. + - text: 后续 +- paragraph: + - strong: 提醒! + - text: 继续 +- paragraph: + - strong: Warning! + - text: 继续 +- paragraph: CJK_STRONG_DONE +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: {{clock}} Ran for {{duration}} +- textbox "Message the agent" +- button "Commands": + - img +- 'button "Access mode, current: Workspace Write"': Workspace Write +- button "Select model": + - text: Select model + - img +- button "Send message" [disabled] +- text: 1 turns · 1 steps LLM {{duration}} Input 0 tok · Output 0 tok diff --git a/apps/web/tests/snapshots/markdown-inline-code-links/ui.expected.md b/apps/web/tests/snapshots/markdown-inline-code-links/ui.expected.md new file mode 100644 index 0000000000..71851363d2 --- /dev/null +++ b/apps/web/tests/snapshots/markdown-inline-code-links/ui.expected.md @@ -0,0 +1,43 @@ +- banner: + - navigation "Session hierarchy": + - button "Inline code links" [disabled] + - tablist: + - tab "Chat" [selected] + - tab "Trajectory" +- text: Show the local preview URL. {{clock}} +- button "Copy": + - img +- button "Branch into a new conversation" [disabled]: + - img +- text: Available only on the last message of a completed turn +- heading "Inline code links" [level=2] +- paragraph: + - text: "Preview:" + - code: + - link "{{linkUrl}}": + - /url: {{linkUrl}} +- paragraph: + - text: "Standard:" + - link "Open preview": + - /url: {{linkUrl}} +- paragraph: + - text: "Command:" + - code: curl {{linkUrl}} +- paragraph: + - text: "Unsafe:" + - code: javascript:alert(1) +- paragraph: INLINE_CODE_LINK_DONE +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: {{clock}} Ran for {{duration}} +- textbox "Message the agent" +- button "Commands": + - img +- 'button "Access mode, current: Workspace Write"': Workspace Write +- button "Select model": + - text: Select model + - img +- button "Send message" [disabled] +- text: 1 turns · 1 steps LLM {{duration}} Input 0 tok · Output 0 tok diff --git a/apps/web/tests/snapshots/plan-review/approved.expected.md b/apps/web/tests/snapshots/plan-review/approved.expected.md index 0bcf2c5dca..f0c7d718e0 100644 --- a/apps/web/tests/snapshots/plan-review/approved.expected.md +++ b/apps/web/tests/snapshots/plan-review/approved.expected.md @@ -5,16 +5,16 @@ - tab "Chat" [selected] - tab "Trajectory" - img -- text: "plan Plan mode on. Use /plan off to leave. Plan a small change: add a --greeting flag to a CLI. Do not read or write any files. Call exit_plan_mode with a short plan of at most five bullet points. Once the plan is approved, reply with the single word DONE and stop. {{clock}}" +- text: "plan Plan mode on. Use /plan off to leave. Interjection Plan a small change: add a --greeting flag to a CLI. Do not read or write any files. Call exit_plan_mode with a short plan of at most five bullet points. Once the plan is approved, reply with the single word DONE and stop. {{clock}}" - button "Copy": - img - button "Branch into a new conversation" [disabled]: - img - text: Available only on the last message of a completed turn -- button "Context injection": +- button "Context injection @deepseek-ai/dsh-system-prompt": - img - img - - text: Context injection + - text: Context injection @deepseek-ai/dsh-system-prompt - 'button "Think The user wants me to plan a small change to add a `--greeting` flag to a CLI. They explicitly told me not to read or write any files, and to call exit_plan_mode with a short plan. Let me do that directly."': - img - img diff --git a/apps/web/tests/snapshots/question-composer/answered.expected.md b/apps/web/tests/snapshots/question-composer/answered.expected.md index 8176008f25..82e0b468c1 100644 --- a/apps/web/tests/snapshots/question-composer/answered.expected.md +++ b/apps/web/tests/snapshots/question-composer/answered.expected.md @@ -10,10 +10,10 @@ - button "Branch into a new conversation" [disabled]: - img - text: Available only on the last message of a completed turn -- button "Context injection": +- button "Context injection @deepseek-ai/dsh-system-prompt": - img - img - - text: Context injection + - text: Context injection @deepseek-ai/dsh-system-prompt - button "Think The user wants me to use the ask_user_question tool with specific parameters. Let me do exactly that.": - img - img diff --git a/apps/web/tests/snapshots/queue-actions/collapsed.expected.md b/apps/web/tests/snapshots/queue-actions/collapsed.expected.md index bd44e33ad0..cdde5d8790 100644 --- a/apps/web/tests/snapshots/queue-actions/collapsed.expected.md +++ b/apps/web/tests/snapshots/queue-actions/collapsed.expected.md @@ -10,10 +10,10 @@ - button "Branch into a new conversation" [disabled]: - img - text: Available only on the last message of a completed turn -- button "Context injection": +- button "Context injection @deepseek-ai/dsh-system-prompt": - img - img - - text: Context injection + - text: Context injection @deepseek-ai/dsh-system-prompt - paragraph: partial - status: Deep diving... - button "2 queued messages" diff --git a/apps/web/tests/snapshots/queue-actions/editing.expected.md b/apps/web/tests/snapshots/queue-actions/editing.expected.md index ff9ce89731..8bfd2f964d 100644 --- a/apps/web/tests/snapshots/queue-actions/editing.expected.md +++ b/apps/web/tests/snapshots/queue-actions/editing.expected.md @@ -10,10 +10,10 @@ - button "Branch into a new conversation" [disabled]: - img - text: Available only on the last message of a completed turn -- button "Context injection": +- button "Context injection @deepseek-ai/dsh-system-prompt": - img - img - - text: Context injection + - text: Context injection @deepseek-ai/dsh-system-prompt - paragraph: partial - status: Deep diving... - button "2 queued messages" [disabled] [expanded] diff --git a/apps/web/tests/snapshots/queue-actions/layout.expected.md b/apps/web/tests/snapshots/queue-actions/layout.expected.md index 6d51e81a2c..7370a15264 100644 --- a/apps/web/tests/snapshots/queue-actions/layout.expected.md +++ b/apps/web/tests/snapshots/queue-actions/layout.expected.md @@ -8,14 +8,14 @@ - img - img - text: "goal Goal created Status: active Objective: Keep the composer context panels aligned Rounds: 0/256 Activation: armed Commands: /goal edit , /goal pause, /goal clear" -- button "Context injection": +- button "Context injection goal": - img - img - - text: Context injection -- button "Context injection": + - text: Context injection goal +- button "Context injection @deepseek-ai/dsh-system-prompt": - img - img - - text: Context injection + - text: Context injection @deepseek-ai/dsh-system-prompt - paragraph: partial - status: Deep diving... - region "To-dos": diff --git a/apps/web/tests/snapshots/queue-actions/preserved.expected.md b/apps/web/tests/snapshots/queue-actions/preserved.expected.md index 1a6b568816..e8b65fdea1 100644 --- a/apps/web/tests/snapshots/queue-actions/preserved.expected.md +++ b/apps/web/tests/snapshots/queue-actions/preserved.expected.md @@ -10,10 +10,10 @@ - button "Branch into a new conversation" [disabled]: - img - text: Available only on the last message of a completed turn -- button "Context injection": +- button "Context injection @deepseek-ai/dsh-system-prompt": - img - img - - text: Context injection + - text: Context injection @deepseek-ai/dsh-system-prompt - paragraph: partial - text: Stopped - button "Copy": diff --git a/apps/web/tests/snapshots/queue-actions/ui.expected.md b/apps/web/tests/snapshots/queue-actions/ui.expected.md index 3cf1fc3743..48b714a88c 100644 --- a/apps/web/tests/snapshots/queue-actions/ui.expected.md +++ b/apps/web/tests/snapshots/queue-actions/ui.expected.md @@ -10,10 +10,10 @@ - button "Branch into a new conversation" [disabled]: - img - text: Available only on the last message of a completed turn -- button "Context injection": +- button "Context injection @deepseek-ai/dsh-system-prompt": - img - img - - text: Context injection + - text: Context injection @deepseek-ai/dsh-system-prompt - paragraph: partial - status: Deep diving... - list: diff --git a/apps/web/tests/snapshots/seeded-history/command-row.expected.md b/apps/web/tests/snapshots/seeded-history/command-row.expected.md index fb2f5e4cba..467a4364b8 100644 --- a/apps/web/tests/snapshots/seeded-history/command-row.expected.md +++ b/apps/web/tests/snapshots/seeded-history/command-row.expected.md @@ -37,10 +37,10 @@ - button "Context compacted View compaction summary": - img - text: Context compacted View compaction summary -- button "Context injection": +- button "Context injection AGENTS.md": - img - img - - text: Context injection + - text: Context injection AGENTS.md - img - text: permission preset read-only - textbox "Message the agent" diff --git a/apps/web/tests/snapshots/seeded-history/ui.expected.md b/apps/web/tests/snapshots/seeded-history/ui.expected.md index f804046ad2..55fcb89ec8 100644 --- a/apps/web/tests/snapshots/seeded-history/ui.expected.md +++ b/apps/web/tests/snapshots/seeded-history/ui.expected.md @@ -37,10 +37,10 @@ - button "Context compacted View compaction summary": - img - text: Context compacted View compaction summary -- button "Context injection": +- button "Context injection AGENTS.md": - img - img - - text: Context injection + - text: Context injection AGENTS.md - textbox "Message the agent" - button "Commands": - img diff --git a/apps/web/tests/snapshots/steering/mid-steer.expected.md b/apps/web/tests/snapshots/steering/mid-steer.expected.md index 5b316f4efb..c32cee0077 100644 --- a/apps/web/tests/snapshots/steering/mid-steer.expected.md +++ b/apps/web/tests/snapshots/steering/mid-steer.expected.md @@ -10,10 +10,10 @@ - button "Branch into a new conversation" [disabled]: - img - text: Available only on the last message of a completed turn -- button "Context injection": +- button "Context injection @deepseek-ai/dsh-system-prompt": - img - img - - text: Context injection + - text: Context injection @deepseek-ai/dsh-system-prompt - button "Think The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that.": - img - img @@ -24,7 +24,7 @@ - img - text: Ask question waiting - status: Deep diving... -- text: "Interjection: include the word BANANA in your final reply." +- text: "Interjection Interjection: include the word BANANA in your final reply." - button "Copy": - img - region "Ready to continue?": diff --git a/apps/web/tests/snapshots/steering/settled.expected.md b/apps/web/tests/snapshots/steering/settled.expected.md index c68451bc19..77385c6333 100644 --- a/apps/web/tests/snapshots/steering/settled.expected.md +++ b/apps/web/tests/snapshots/steering/settled.expected.md @@ -10,10 +10,10 @@ - button "Branch into a new conversation" [disabled]: - img - text: Available only on the last message of a completed turn -- button "Context injection": +- button "Context injection @deepseek-ai/dsh-system-prompt": - img - img - - text: Context injection + - text: Context injection @deepseek-ai/dsh-system-prompt - button "Think The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that.": - img - img @@ -22,7 +22,7 @@ - img - img - text: Ask question 1/1 answered -- text: "Interjection: include the word BANANA in your final reply. {{clock}}" +- text: "Interjection Interjection: include the word BANANA in your final reply. {{clock}}" - button "Copy": - img - button "Branch into a new conversation" [disabled]: diff --git a/apps/web/tests/snapshots/subagent-conversation/ui.expected.md b/apps/web/tests/snapshots/subagent-conversation/ui.expected.md index c3f1038650..a01eea56d8 100644 --- a/apps/web/tests/snapshots/subagent-conversation/ui.expected.md +++ b/apps/web/tests/snapshots/subagent-conversation/ui.expected.md @@ -15,10 +15,10 @@ - button "Branch into a new conversation" [disabled]: - img - text: Available only on the last message of a completed turn -- button "Context injection": +- button "Context injection @deepseek-ai/dsh-system-prompt": - img - img - - text: Context injection + - text: Context injection @deepseek-ai/dsh-system-prompt - button "Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls.": - img - img diff --git a/apps/web/tests/snapshots/web-search-round/ui.expected.md b/apps/web/tests/snapshots/web-search-round/ui.expected.md index 8fdd4d8585..1e2dcf9eca 100644 --- a/apps/web/tests/snapshots/web-search-round/ui.expected.md +++ b/apps/web/tests/snapshots/web-search-round/ui.expected.md @@ -10,10 +10,10 @@ - button "Branch into a new conversation" [disabled]: - img - text: Available only on the last message of a completed turn -- button "Context injection": +- button "Context injection @deepseek-ai/dsh-system-prompt": - img - img - - text: Context injection + - text: Context injection @deepseek-ai/dsh-system-prompt - button "Search DeepSeek Harness snapshot search": - img - img diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 714dfcb2d4..dd5fe879e7 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -51,6 +51,8 @@ "tests/message-actions.e2e.ts", "tests/markdown-images.e2e.ts", "tests/math-rendering.e2e.ts", + "tests/markdown-cjk-strong.e2e.ts", + "tests/markdown-inline-code-links.e2e.ts", "tests/queue-actions.e2e.ts", "tests/skill-invocation-policy.e2e.ts", "tests/permission-policy-context.e2e.ts", diff --git a/docs/config-catalog.md b/docs/config-catalog.md index abde0ea3b0..ffa3d5bdd4 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1925,7 +1925,7 @@ export interface Config { } ``` -Source: [`packages/skill/tool-skill/src/index.ts:30`](../packages/skill/tool-skill/src/index.ts) +Source: [`packages/skill/tool-skill/src/index.ts:58`](../packages/skill/tool-skill/src/index.ts) ## `@deepseek-ai/dsh-tool-str-replace-editor` diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index ed615ed377..b3d81d61b9 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -2195,7 +2195,7 @@ async assemble(context: AssembleContext = {}): Promise Types: [AssembleContext](../core-data-structures/system-prompt.md) · [PromptContext](../core-data-structures/system-prompt.md) · [PromptSection](../core-data-structures/system-prompt.md) · [ToolProviderResult](../core-data-structures/system-prompt.md) -Source: [`packages/core/system-prompt/src/index.ts:290`](../../packages/core/system-prompt/src/index.ts) +Source: [`packages/core/system-prompt/src/index.ts:314`](../../packages/core/system-prompt/src/index.ts) ## `ctx.tasks` — `TaskService` (abstract seam) diff --git a/docs/core-data-structures/core.i18n.yaml b/docs/core-data-structures/core.i18n.yaml index 6712d12b9f..6f16b8c573 100644 --- a/docs/core-data-structures/core.i18n.yaml +++ b/docs/core-data-structures/core.i18n.yaml @@ -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 docs/core-data-structures/core.md -core.md: 6886d9f15c37a6f3fd9cd825fb4c3f24d577db10 -core.zh.md: f89365dcdd620cd749c7ccca9ee2cc2da71118ac +core.md: 495651e1f3105afff15f68822568ff71c531da4f +core.zh.md: c895601f39d350811ab1169533287d8f59dba703 diff --git a/docs/core-data-structures/core.md b/docs/core-data-structures/core.md index 6886d9f15c..495651e1f3 100644 --- a/docs/core-data-structures/core.md +++ b/docs/core-data-structures/core.md @@ -160,12 +160,84 @@ Where a message came from is itself a merge-extensible sum type: */ interface MessageSourceMap { user: { kind: 'user' } - plugin: { kind: 'plugin'; plugin: string } + plugin: { kind: 'plugin'; plugin: string } & ContextFormed model: ModelMessageSource tool: ToolMessageSource } ``` +Provenance and shape are two independent axes. `kind` answers *who produced this*; the optional `form` a producer mixes in answers *what shape of information it is*, so several producers may share one presentation and one producer may emit more than one shape over a session. The vocabulary is semantic and grows one value at a time; an absent or unrecognized value is the documented default, presented as opaque content: + +```ts type-equiv +/** + * What SHAPE of information a producer-supplied context carries, declared by + * the producer beside its provenance. + * + * `MessageSource.kind` answers *who produced this*; `form` answers *what kind + * of thing it is*, and the two axes are deliberately independent — several + * producers share one form (three snapshot producers today), and one producer + * may emit more than one form over a session. + * + * The vocabulary is SEMANTIC, never visual: a value states that the content is + * a file's instructions or a catalog of available items, and a consumer decides + * what that looks like. Colors, icons, ordering, and collapse defaults are the + * consumer's business and must not enter this union. It grows one value at a + * time as producers gain the structured fields their form needs; an absent or + * unknown value is the documented default, presented as opaque content. + */ +type ContextForm = + /** Instructions read out of workspace files the model is expected to follow. */ + | 'instructions' + /** A catalog of items available in this session, republished as it changes. */ + | 'catalog' + /** Current state, where a later snapshot from the same producer supersedes an earlier one. */ + | 'snapshot' + /** A one-off account of something that just happened; it supersedes nothing. */ + | 'notice' + /** A message another agent addressed to this one. */ + | 'relay' + /** Material lifted out of another session's log, possibly reduced on the way in. */ + | 'recall' +``` + +```ts type-equiv +/** One named contribution to a `snapshot`-form context, in assembly order. */ +interface ContextSnapshotSection { + /** The contributing subsystem's name. */ + readonly name: string + /** That contribution's model-facing text, exactly as assembled. */ + readonly text: string +} +``` + +```ts type-equiv +/** + * Producer-declared {@link ContextForm} and the fields that form requires, + * mixed into the source shapes that carry one. + * + * Discriminated by `form` so a producer cannot declare a shape without the + * facts that shape is presented from: a `notice` must record its one-line + * account, a `snapshot` its sections. Omitting `form` stays valid — an + * undeclared context is the documented default. + */ +type ContextFormed = + | { readonly form?: never } + | { readonly form: 'instructions' } + | { readonly form: 'catalog' } + | { + readonly form: 'snapshot' + /** The named contributions this snapshot assembled, in order. */ + readonly sections: readonly ContextSnapshotSection[] + } + | { + readonly form: 'notice' + /** One-line account of what happened, shown without expanding the row. */ + readonly summary: string + } + | { readonly form: 'relay' } + | { readonly form: 'recall' } +``` + ## Streaming Adapters emit a raw **chunk** protocol; the loop logs the chunks (replay fidelity) while feeding the same chunks through a `BlockAssembler` to rebuild blocks and messages. `StreamChunk` is a closed discriminated union over `type` — `block-start`, `text-delta`, `reasoning-delta`, `tool-call-delta`, `block-end`, `usage`, `finish`. diff --git a/docs/core-data-structures/core.zh.md b/docs/core-data-structures/core.zh.md index f89365dcdd..c895601f39 100644 --- a/docs/core-data-structures/core.zh.md +++ b/docs/core-data-structures/core.zh.md @@ -166,12 +166,84 @@ interface Message { */ interface MessageSourceMap { user: { kind: 'user' } - plugin: { kind: 'plugin'; plugin: string } + plugin: { kind: 'plugin'; plugin: string } & ContextFormed model: ModelMessageSource tool: ToolMessageSource } ``` +溯源与形态是相互独立的两根轴。`kind` 回答「由谁产生」;生产方可选混入的 `form` 回答「这是何种形态的信息」,因此多个生产方可以共用一种呈现,一个生产方在一次会话中也可以发出多种形态。该词汇表是语义的,逐个取值增长;未声明或无法识别的取值是有文档的默认,按不透明内容呈现: + +```ts type-equiv +/** + * What SHAPE of information a producer-supplied context carries, declared by + * the producer beside its provenance. + * + * `MessageSource.kind` answers *who produced this*; `form` answers *what kind + * of thing it is*, and the two axes are deliberately independent — several + * producers share one form (three snapshot producers today), and one producer + * may emit more than one form over a session. + * + * The vocabulary is SEMANTIC, never visual: a value states that the content is + * a file's instructions or a catalog of available items, and a consumer decides + * what that looks like. Colors, icons, ordering, and collapse defaults are the + * consumer's business and must not enter this union. It grows one value at a + * time as producers gain the structured fields their form needs; an absent or + * unknown value is the documented default, presented as opaque content. + */ +type ContextForm = + /** Instructions read out of workspace files the model is expected to follow. */ + | 'instructions' + /** A catalog of items available in this session, republished as it changes. */ + | 'catalog' + /** Current state, where a later snapshot from the same producer supersedes an earlier one. */ + | 'snapshot' + /** A one-off account of something that just happened; it supersedes nothing. */ + | 'notice' + /** A message another agent addressed to this one. */ + | 'relay' + /** Material lifted out of another session's log, possibly reduced on the way in. */ + | 'recall' +``` + +```ts type-equiv +/** One named contribution to a `snapshot`-form context, in assembly order. */ +interface ContextSnapshotSection { + /** The contributing subsystem's name. */ + readonly name: string + /** That contribution's model-facing text, exactly as assembled. */ + readonly text: string +} +``` + +```ts type-equiv +/** + * Producer-declared {@link ContextForm} and the fields that form requires, + * mixed into the source shapes that carry one. + * + * Discriminated by `form` so a producer cannot declare a shape without the + * facts that shape is presented from: a `notice` must record its one-line + * account, a `snapshot` its sections. Omitting `form` stays valid — an + * undeclared context is the documented default. + */ +type ContextFormed = + | { readonly form?: never } + | { readonly form: 'instructions' } + | { readonly form: 'catalog' } + | { + readonly form: 'snapshot' + /** The named contributions this snapshot assembled, in order. */ + readonly sections: readonly ContextSnapshotSection[] + } + | { + readonly form: 'notice' + /** One-line account of what happened, shown without expanding the row. */ + readonly summary: string + } + | { readonly form: 'relay' } + | { readonly form: 'recall' } +``` + ## 流式输出 适配器发出原始**分片**协议;循环记录分片(回放保真度),同时将同一批分片送入 `BlockAssembler` 以重建块和消息。`StreamChunk` 是基于 `type` 的封闭判别联合——`block-start`、`text-delta`、`reasoning-delta`、`tool-call-delta`、`block-end`、`usage`、`finish`。 diff --git a/docs/core-data-structures/subagent.i18n.yaml b/docs/core-data-structures/subagent.i18n.yaml index d5de81fa45..a449388c60 100644 --- a/docs/core-data-structures/subagent.i18n.yaml +++ b/docs/core-data-structures/subagent.i18n.yaml @@ -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 docs/core-data-structures/subagent.md -subagent.md: c5fbf80ae71f99606dd86e38f06a4511b4ae4c73 -subagent.zh.md: 42c1fa7cb10863c1aa4ae975171b901207c08b85 +subagent.md: 956b47cfa85efe7826fbde47d4405d20a6abed6c +subagent.zh.md: 467fcd35bcde5fd01a2e18efbad862c72d7a4253 diff --git a/docs/core-data-structures/subagent.md b/docs/core-data-structures/subagent.md index c5fbf80ae7..956b47cfa8 100644 --- a/docs/core-data-structures/subagent.md +++ b/docs/core-data-structures/subagent.md @@ -149,6 +149,8 @@ Final settlement awaits `ctx.sessions.flush(session)` but ignores its participat /** Attribution for a model coordinator's follow-up to one of its children. */ interface CoordinatorMessageSource { readonly kind: 'coordinator' + /** A message another agent addressed to this one (`relay` context form). */ + readonly form: 'relay' /** Session id of the agent whose tool call produced the follow-up. */ readonly senderSessionId: SessionId } @@ -182,6 +184,8 @@ An optional continuable-child setup contribution can install scope-local capabil /** Durable attribution for a continuable child's explicit parent report. */ interface SubagentReportMessageSource { readonly kind: 'subagent-report' + /** A message another agent addressed to this one (`relay` context form). */ + readonly form: 'relay' /** Session id of the reporting child. */ readonly senderSessionId: SessionId } diff --git a/docs/core-data-structures/subagent.zh.md b/docs/core-data-structures/subagent.zh.md index 42c1fa7cb1..467fcd35bc 100644 --- a/docs/core-data-structures/subagent.zh.md +++ b/docs/core-data-structures/subagent.zh.md @@ -149,6 +149,8 @@ Agent 收件箱是唯一的队列。每条继续执行消息都会成为一个 ` /** Attribution for a model coordinator's follow-up to one of its children. */ interface CoordinatorMessageSource { readonly kind: 'coordinator' + /** A message another agent addressed to this one (`relay` context form). */ + readonly form: 'relay' /** Session id of the agent whose tool call produced the follow-up. */ readonly senderSessionId: SessionId } @@ -182,6 +184,8 @@ interface ContinuableStart { /** Durable attribution for a continuable child's explicit parent report. */ interface SubagentReportMessageSource { readonly kind: 'subagent-report' + /** A message another agent addressed to this one (`relay` context form). */ + readonly form: 'relay' /** Session id of the reporting child. */ readonly senderSessionId: SessionId } diff --git a/examples/acp-agent/tests/goal-snapshots/goal-session/session.expected.jsonl b/examples/acp-agent/tests/goal-snapshots/goal-session/session.expected.jsonl index 5f5b8971ce..f7b0cc84b2 100644 --- a/examples/acp-agent/tests/goal-snapshots/goal-session/session.expected.jsonl +++ b/examples/acp-agent/tests/goal-snapshots/goal-session/session.expected.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":0,"data":{"content":[{"type":"text","text":"Create a durable two-round goal for the ACP snapshot, inspect it, then report readiness."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":0,"data":{"title":"Create a durable two-round goal","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/goal-snapshots/goal-wrapup/session.expected.jsonl b/examples/acp-agent/tests/goal-snapshots/goal-wrapup/session.expected.jsonl index 60335e83b4..46ca2fe819 100644 --- a/examples/acp-agent/tests/goal-snapshots/goal-wrapup/session.expected.jsonl +++ b/examples/acp-agent/tests/goal-snapshots/goal-wrapup/session.expected.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":0,"data":{"content":[{"type":"text","text":"Create a durable goal for the wrap-up snapshot, then report readiness."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":0,"data":{"title":"Create a durable goal for","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} @@ -41,11 +41,11 @@ {"type":"tool/call","seq":39,"time":0,"data":{"turn":2,"step":1,"callId":"call_goal_complete","name":"update_goal","arguments":"{\"goal_id\":\"goal-{{sessionId}}\",\"revision\":1,\"action\":\"complete\"}"}} {"type":"goal/change","seq":40,"time":0,"data":{"kind":"goal/change","version":1,"operation":"complete","goal":{"id":"goal-{{sessionId}}","revision":2,"objective":"Finish the ACP goal wrap-up snapshot proof","phase":"complete","maxGoalRounds":2},"roundsStarted":1,"createdAt":0,"updatedAt":0}} {"type":"tool/result","seq":41,"time":0,"data":{"turn":2,"step":1,"message":{"source":{"kind":"tool","callId":"call_goal_complete"},"content":[{"type":"tool-result","toolCallId":"call_goal_complete","content":[{"type":"text","text":"{\"goal\":{\"id\":\"goal-{{sessionId}}\",\"revision\":2,\"objective\":\"Finish the ACP goal wrap-up snapshot proof\",\"phase\":\"complete\",\"roundsStarted\":1,\"maxGoalRounds\":2},\"activation\":\"disarmed\"}"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[39],"surfaceOp":"append"} -{"type":"agent/inbox/spliced","seq":42,"time":0,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"\nObjective: \"Finish the ACP goal wrap-up snapshot proof\"\nThe goal is marked complete and this autonomous run is ending. Write the closing message to the user now: state the outcome, summarize what was done and how it was verified, and point to the concrete results (files, commits, or other artifacts). Report only what earlier rounds and tool results in this session actually establish; when a detail is not in the session, say so instead of inventing it. Note anything the user should review or do next. Address the user directly. Do not call any more tools in this run; further work waits for the user's next instruction.\n"}],"source":{"kind":"plugin","plugin":"tool-goal"},"role":"user","id":"{{sessionId}}"}]}} +{"type":"agent/inbox/spliced","seq":42,"time":0,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"\nObjective: \"Finish the ACP goal wrap-up snapshot proof\"\nThe goal is marked complete and this autonomous run is ending. Write the closing message to the user now: state the outcome, summarize what was done and how it was verified, and point to the concrete results (files, commits, or other artifacts). Report only what earlier rounds and tool results in this session actually establish; when a detail is not in the session, say so instead of inventing it. Note anything the user should review or do next. Address the user directly. Do not call any more tools in this run; further work waits for the user's next instruction.\n"}],"source":{"kind":"plugin","plugin":"tool-goal","form":"notice","summary":"complete: Finish the ACP goal wrap-up snapshot proof"},"role":"user","id":"{{sessionId}}"}]}} {"type":"step/end","seq":43,"time":0,"data":{"turn":2,"step":1}} {"type":"agent/inbox/spliced","seq":44,"time":0,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":45,"time":0,"data":{"turn":2,"step":2}} -{"type":"user/message","seq":46,"time":0,"data":{"content":[{"type":"text","text":"\nObjective: \"Finish the ACP goal wrap-up snapshot proof\"\nThe goal is marked complete and this autonomous run is ending. Write the closing message to the user now: state the outcome, summarize what was done and how it was verified, and point to the concrete results (files, commits, or other artifacts). Report only what earlier rounds and tool results in this session actually establish; when a detail is not in the session, say so instead of inventing it. Note anything the user should review or do next. Address the user directly. Do not call any more tools in this run; further work waits for the user's next instruction.\n"}],"source":{"kind":"plugin","plugin":"tool-goal"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","seq":46,"time":0,"data":{"content":[{"type":"text","text":"\nObjective: \"Finish the ACP goal wrap-up snapshot proof\"\nThe goal is marked complete and this autonomous run is ending. Write the closing message to the user now: state the outcome, summarize what was done and how it was verified, and point to the concrete results (files, commits, or other artifacts). Report only what earlier rounds and tool results in this session actually establish; when a detail is not in the session, say so instead of inventing it. Note anything the user should review or do next. Address the user directly. Do not call any more tools in this run; further work waits for the user's next instruction.\n"}],"source":{"kind":"plugin","plugin":"tool-goal","form":"notice","summary":"complete: Finish the ACP goal wrap-up snapshot proof"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"assistant/chunk","seq":47,"time":0,"data":{"turn":2,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","seq":48,"time":0,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":0,"text":"GOAL WRAP-UP: the snapshot objective is achieved and this closing message reaches the user."}}} {"type":"assistant/chunk","seq":49,"time":0,"data":{"turn":2,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"GOAL WRAP-UP: the snapshot objective is achieved and this closing message reaches the user."}}}} diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl index 7e9f42d6fb..7908f0e71b 100644 --- a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl @@ -5,7 +5,7 @@ {"type":"subagent/descriptor","seq":3,"time":1785821418091,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Check direct child"}} {"type":"step/start","seq":4,"time":1785730458555,"data":{"turn":1,"step":1}} {"type":"user/message","seq":5,"time":1785730458555,"data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"ebe0cfa0-a909-47e0-8294-28ad84a8fe77"},"surfaceOp":"append"} -{"type":"user/message","seq":6,"time":1785730458555,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"f9a2d1b6-8f23-43a5-8702-d413fed40990"},"surfaceOp":"append"} +{"type":"user/message","seq":6,"time":1785730458555,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"f9a2d1b6-8f23-43a5-8702-d413fed40990"},"surfaceOp":"append"} {"type":"session/title","seq":7,"time":1785730458555,"data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}} {"type":"request/header","seq":8,"time":1785730458555,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":9,"time":1785730458555,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl index 38c5dfae47..adf877c24b 100644 --- a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl +++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl @@ -5,7 +5,7 @@ {"type":"subagent/descriptor","seq":3,"time":1785821418270,"data":{"version":2,"mode":"one-shot","provider":"spawn"}} {"type":"step/start","seq":4,"time":1785730458703,"data":{"turn":1,"step":1}} {"type":"user/message","seq":5,"time":1785730458703,"data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"2ac2cc54-9bce-4cfa-a569-a64f51bc30a7"},"surfaceOp":"append"} -{"type":"user/message","seq":6,"time":1785730458703,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"dfbcd587-db47-4c3d-bbe9-8c031b215fc3"},"surfaceOp":"append"} +{"type":"user/message","seq":6,"time":1785730458703,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"dfbcd587-db47-4c3d-bbe9-8c031b215fc3"},"surfaceOp":"append"} {"type":"session/title","seq":7,"time":1785730458703,"data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}} {"type":"request/header","seq":8,"time":1785730458703,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":9,"time":1785730458703,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl index ec5c5756f2..f4169abe78 100644 --- a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl +++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821417919,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783957884486,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498801761,"data":{"content":[{"type":"text","text":"Run this advanced flow exactly once: try a no-op temporary Cordis Plugin named snapshot-marker; use run_code to inspect the live temporary Plugins through tools.cordis_inspect; delegate once to a direct spawn child; run one workflow that delegates to another spawn child; stop dyn-1; then reply with exactly ADVANCED_ACP_OK."}],"source":{"kind":"user"},"role":"user","id":"6e45782a-31be-4ba7-8c4a-7411a2027e36"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730458430,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"9f38e2b8-1d4e-4c90-8896-00aa42307ea7"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730458430,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"9f38e2b8-1d4e-4c90-8896-00aa42307ea7"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730458430,"data":{"title":"Run this advanced flow exactly","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498801765,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730458431,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/bash-spill/session.jsonl b/examples/acp-agent/tests/snapshots/bash-spill/session.jsonl index 075b166442..449db5acb4 100644 --- a/examples/acp-agent/tests/snapshots/bash-spill/session.jsonl +++ b/examples/acp-agent/tests/snapshots/bash-spill/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821368742,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498767672,"data":{"content":[{"type":"text","text":"Use the bash tool to print a large deterministic output, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"4f33bd12-21b5-4ccc-bbd2-4edb0ab6b33b"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730421018,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"ac1209c1-ce77-4622-a7c4-b39225fda7ab"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730421018,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"ac1209c1-ce77-4622-a7c4-b39225fda7ab"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730421018,"data":{"title":"Use the bash tool to","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498767673,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730421019,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/bash-tool-turn/session.jsonl b/examples/acp-agent/tests/snapshots/bash-tool-turn/session.jsonl index 2d49114651..6e4de13d80 100644 --- a/examples/acp-agent/tests/snapshots/bash-tool-turn/session.jsonl +++ b/examples/acp-agent/tests/snapshots/bash-tool-turn/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821375023,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352050755,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498771360,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo TERMINAL_OK. Then reply with the single word DONE and stop."}],"source":{"kind":"user"},"role":"user","id":"38694db6-921d-41fd-b1fb-3b0c40caf67c"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730424635,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"80474489-442a-4e98-beef-df6cd1e85870"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730424635,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"80474489-442a-4e98-beef-df6cd1e85870"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730424635,"data":{"title":"Use the bash tool to","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498771361,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730424636,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl b/examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl index 9341d8f61a..a1b26a5af4 100644 --- a/examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl +++ b/examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821443048,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1785014504370,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498827112,"data":{"content":[{"type":"text","text":"Call the run_code tool (NOT the native bash tool directly) with a program that runs exactly `echo BOTH_OK` via tools.bash and returns its output. Then reply with that output only and stop."}],"source":{"kind":"user"},"role":"user","id":"922e078d-9ef7-4017-9c4e-96a34a721503"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730479344,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"d3891fd4-21eb-4869-8a66-498764450bf2"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730479344,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"d3891fd4-21eb-4869-8a66-498764450bf2"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730479344,"data":{"title":"Call the run_code tool (NOT","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498827116,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730479345,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/cancel-tool-calls/session.jsonl b/examples/acp-agent/tests/snapshots/cancel-tool-calls/session.jsonl index a6932c5165..a3676dbc3d 100644 --- a/examples/acp-agent/tests/snapshots/cancel-tool-calls/session.jsonl +++ b/examples/acp-agent/tests/snapshots/cancel-tool-calls/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821402705,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1784437195076,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498792518,"data":{"content":[{"type":"text","text":"Run two shell commands: wait for cancellation, then write skipped.txt."}],"source":{"kind":"user"},"role":"user","id":"6025dc7c-dc38-4a34-b7b1-688102631c75"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730445587,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"bf953438-d1c4-4e00-a06b-7f5e2da1df7a"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730445587,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"bf953438-d1c4-4e00-a06b-7f5e2da1df7a"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730445587,"data":{"title":"Run two shell commands: wait","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498792519,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730445588,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/cancel/session.jsonl b/examples/acp-agent/tests/snapshots/cancel/session.jsonl index f28bb4a224..c6396be679 100644 --- a/examples/acp-agent/tests/snapshots/cancel/session.jsonl +++ b/examples/acp-agent/tests/snapshots/cancel/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821401560,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498791446,"data":{"content":[{"type":"text","text":"Start a long task; this turn will be cancelled mid-stream."}],"source":{"kind":"user"},"role":"user","id":"f74653c2-8793-4004-ab0d-833a8dfd42bf"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730444531,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"2c4c8dc2-5141-4963-adbc-5928729d3bf6"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730444531,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"2c4c8dc2-5141-4963-adbc-5928729d3bf6"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730444531,"data":{"title":"Start a long task; this","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498791447,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730444532,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/code-mode-turn/session.jsonl b/examples/acp-agent/tests/snapshots/code-mode-turn/session.jsonl index 81422f7470..24e1525c79 100644 --- a/examples/acp-agent/tests/snapshots/code-mode-turn/session.jsonl +++ b/examples/acp-agent/tests/snapshots/code-mode-turn/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821440493,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1785014439593,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498824620,"data":{"content":[{"type":"text","text":"Using ONE run_code program: call the bash tool twice — exactly `echo CODE_ONE` then exactly `echo CODE_TWO`. Inside that same program, console.log exactly `captured output`, then return the two outputs joined with a plus sign. Reply with that joined string only and stop."}],"source":{"kind":"user"},"role":"user","id":"8e2d7086-925a-4734-ba89-418940b0ee58"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730477066,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"ea97a8e4-de78-4638-b80a-c24dfeaba555"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730477066,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"ea97a8e4-de78-4638-b80a-c24dfeaba555"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730477066,"data":{"title":"Using ONE run_code program: call","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498824624,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730477067,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl b/examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl index 2efe69428a..6107589cb2 100644 --- a/examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl +++ b/examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl @@ -4,8 +4,8 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785498825916,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1785901435161,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498825917,"data":{"content":[{"type":"text","text":"Using ONE run_code program, call tools.read on nested/task.txt. After the program finishes, answer the workspace handshake question using the newly discovered instructions: What is the Code Mode workspace handshake?"}],"source":{"kind":"user"},"role":"user","id":"3b04578e-7b22-4b44-b4cd-ef9d4d26fe8b"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785901435161,"data":{"content":[{"type":"text","text":"\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nWorkspace snapshot root instruction.\n\n"}],"source":{"kind":"workspace-instructions","baseline":true,"changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2119a7072358cc727f8d9c4cb7388e905b075fe6"}]},"role":"user","id":"ac92e76e-4861-47a6-87f8-4e9ca904eb24"},"surfaceOp":"append"} -{"type":"user/message","seq":6,"time":1785730478198,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"d6d78330-05c0-4ebd-9e29-595df6440250"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785901435161,"data":{"content":[{"type":"text","text":"\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nWorkspace snapshot root instruction.\n\n"}],"source":{"kind":"workspace-instructions","form":"instructions","baseline":true,"changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2119a7072358cc727f8d9c4cb7388e905b075fe6"}]},"role":"user","id":"ac92e76e-4861-47a6-87f8-4e9ca904eb24"},"surfaceOp":"append"} +{"type":"user/message","seq":6,"time":1785730478198,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"d6d78330-05c0-4ebd-9e29-595df6440250"},"surfaceOp":"append"} {"type":"session/title","seq":7,"time":1785730478198,"data":{"title":"Using ONE run_code program, call","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":8,"time":1785498825920,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":9,"time":1785730478199,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} @@ -20,10 +20,10 @@ {"type":"tool/code-dispatch","seq":18,"time":1785733131110,"data":{"parentCallId":"call_workspace_read","subCallId":"call_workspace_read:code:1","name":"read","arguments":{"file_path":"nested/task.txt"},"isError":false,"content":[{"type":"text","text":"{{cwd}}/nested/task.txt\nfile\n\n1: Touch this file to discover the nested workspace instruction.\n\n(End of file - total 1 lines)\n"}]}} {"type":"tool/result","seq":19,"time":1785733131112,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_workspace_read"},"content":[{"type":"tool-result","toolCallId":"call_workspace_read","content":[{"type":"text","text":"{\n \"path\": \"{{cwd}}/nested/task.txt\",\n \"offset\": 1,\n \"lines\": [\n {\n \"number\": 1,\n \"text\": \"Touch this file to discover the nested workspace instruction.\"\n }\n ],\n \"totalLines\": 1\n}"}],"isError":false}],"role":"user","id":"bde1c12e-44d1-44f7-ba7e-868349ed2b05"}},"sourceEventSeqs":[16],"surfaceOp":"append"} {"type":"step/end","seq":20,"time":1785733131112,"data":{"turn":1,"step":1}} -{"type":"agent/inbox/spliced","seq":21,"time":1785733131112,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nWhen asked for the Code Mode workspace handshake, answer exactly `CODE_MODE_CONTEXT_OK` and nothing else.\n\n"}],"source":{"kind":"workspace-instructions","changes":[{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"ae22936ed26dc76b7107005ed6d5e2482a88668a"}]},"role":"user","id":"29b0eb87-92d5-4915-ba64-7bd8133ed011"}]}} +{"type":"agent/inbox/spliced","seq":21,"time":1785733131112,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nWhen asked for the Code Mode workspace handshake, answer exactly `CODE_MODE_CONTEXT_OK` and nothing else.\n\n"}],"source":{"kind":"workspace-instructions","form":"instructions","changes":[{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"ae22936ed26dc76b7107005ed6d5e2482a88668a"}]},"role":"user","id":"29b0eb87-92d5-4915-ba64-7bd8133ed011"}]}} {"type":"agent/inbox/spliced","seq":22,"time":1785733131116,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[],"outcome":"canceled"}} {"type":"step/start","seq":23,"time":1785733131123,"data":{"turn":1,"step":2}} -{"type":"user/message","seq":24,"time":1785733131123,"data":{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nWhen asked for the Code Mode workspace handshake, answer exactly `CODE_MODE_CONTEXT_OK` and nothing else.\n\n"}],"source":{"kind":"workspace-instructions","changes":[{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"ae22936ed26dc76b7107005ed6d5e2482a88668a"}]},"role":"user","id":"29b0eb87-92d5-4915-ba64-7bd8133ed011"},"surfaceOp":"append"} +{"type":"user/message","seq":24,"time":1785733131123,"data":{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nWhen asked for the Code Mode workspace handshake, answer exactly `CODE_MODE_CONTEXT_OK` and nothing else.\n\n"}],"source":{"kind":"workspace-instructions","form":"instructions","changes":[{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"ae22936ed26dc76b7107005ed6d5e2482a88668a"}]},"role":"user","id":"29b0eb87-92d5-4915-ba64-7bd8133ed011"},"surfaceOp":"append"} {"type":"assistant/chunk","seq":25,"time":1785014475805,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","seq":26,"time":1785014475806,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"**Code Mode workspace handshake:** `CODE_MODE_CONTEXT_OK`"}}} {"type":"assistant/chunk","seq":27,"time":1785901435233,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"**Code Mode workspace handshake:** `CODE_MODE_CONTEXT_OK`"}}}} diff --git a/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl b/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl index f18e7f5954..12a88365a6 100644 --- a/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl +++ b/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821419616,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1784449176720,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498803419,"data":{"content":[{"type":"text","text":"Inspect the exact tools service API and tools/pre-execute event with cordis_inspect, then reply with exactly CORDIS_INSPECT_JSDOC_OK."}],"source":{"kind":"user"},"role":"user","id":"3a6e7222-9340-429e-bec7-c30fcd063c70"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730459873,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"f9a387d6-bd6f-4613-9c11-5768017feb5c"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730459873,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"f9a387d6-bd6f-4613-9c11-5768017feb5c"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730459873,"data":{"title":"Inspect the exact tools service","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498803423,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730459874,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} @@ -15,7 +15,7 @@ {"type":"assistant/chunk","seq":13,"time":1785730459883,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} {"type":"assistant/message","seq":14,"time":1785730459883,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"inspect-tools-api","name":"cordis_inspect","arguments":"{\"what\":\"api\",\"name\":\"tools\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"6b62bed7-113a-4d2e-a6aa-b935a1063ee2"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"} {"type":"tool/call","seq":15,"time":1785730459883,"data":{"turn":1,"step":1,"callId":"inspect-tools-api","name":"cordis_inspect","arguments":"{\"what\":\"api\",\"name\":\"tools\"}"}} -{"type":"tool/result","seq":16,"time":1785730459904,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"inspect-tools-api"},"content":[{"type":"tool-result","toolCallId":"inspect-tools-api","content":[{"type":"text","text":"## api\n- tools — Tool registry and execution pipeline.\n /**\n * Register globally or in the calling agent scope. Scoped tools shadow\n * globals; duplicates within one layer and the reserved `run_code` name fail.\n * @param definition - tool schema, execution, and optional finalization/presentation callbacks.\n * @returns the exact disposer that unregisters the tool.\n */\n register(definition: ToolDefinition): () => void\n /**\n * Restrict global tools for the calling agent scope. Empty filters, unknown\n * names, scope-local names, and reserved transport names fail. Restrictions\n * intersect; scoped registrations remain visible.\n * @param filter - global-surface mask: `allow` (keep only) and/or `deny` (remove).\n * @returns the exact disposer that lifts this restriction.\n */\n restrict(filter: ToolRestriction): () => void\n /**\n * Register a monotonic guard after the extensible `tools/pre-execute`\n * waterfall. A plain-context guard applies globally; one registered through\n * `agent.ctx` applies only to that agent. Any matching guard may deny by\n * returning a reason, while no guard can force-allow a call another guard\n * denied. The exact effect disposer is returned for ordered ownership and\n * HMR cleanup.\n * @param guard - synchronous check; a returned string denies the execution.\n * @returns the exact disposer that unregisters the guard.\n */\n guard(guard: ToolGuard): () => void\n /**\n * Look up a tool as one scope sees it (scoped\n * shadows global; a restricted-away global reads as absent). Presenters pass\n * the calling agent so the rendered card matches the definition that\n * actually executed.\n * @param name - the tool name as registered.\n * @param scope - the viewing scope (the agent); omitted = the global view.\n * @returns the definition the scope resolves, or undefined when none is visible.\n */\n get(name: string, scope?: ScopeKey): ToolDefinition | undefined\n /**\n * Project visible definitions onto the allowlisted model-facing schema fields,\n * excluding execution and presentation callbacks.\n * @param scope - the viewing scope (the agent); omitted = the global view.\n * @returns one deep-cloned schema per visible tool.\n */\n schemas(scope?: ScopeKey): ToolSchema[]\n /**\n * Classify a pending call through the caller's visible tool definition. Only\n * an exact `true` is parallel; unknown, hidden, undeclared, invalid, or\n * throwing classifiers are exclusive.\n * @param exec - call name, parsed arguments, and optional agent scope.\n * @returns the fail-closed scheduling mode.\n */\n executionMode(exec: ToolExecutionInput): ToolExecutionMode\n /**\n * Execute through pre-policy, guards, around-dispatch, post-policy,\n * definition-owned content finalization, and final notification. Tool and\n * listener failures resolve as materialized error results; an invisible tool\n * reports `UNKNOWN_TOOL`. The returned outcome is the same lossless, frozen\n * snapshot final observers receive. Cancellation\n * arriving after entry and before final result materialization skips a\n * not-yet-started body with `ABORTED_BEFORE_DISPATCH` or replaces a\n * successful started outcome with `ABORTED`; already-started work is still\n * drained and may retain a tool-owned structured error.\n * @param exec - the typed same-process call input. The registry assigns its\n * correlation token before policy begins.\n * @returns the materialized final result.\n */\n async execute(exec: ToolExecutionInput): Promise\ntype shapes (referenced by the signatures above — read these before assuming a field is a string):\n export interface Agent {\n readonly id: SessionId;\n readonly options: AgentOptions;\n readonly session: Session;\n readonly inbox: Inbox;\n readonly status: AgentStatus;\n readonly ctx: Context;\n cancel(cause: AgentCancelCause, options?: CancelOptions): void;\n whenIdle(): Promise;\n runMaintenance(task: (signal: AbortSignal) => Promise): Promise;\n send(message: UserMessage, target: InboxTarget, wakeup: boolean): void;\n followup(message: UserMessage): void;\n steer(message: UserMessage): void;\n inject(message: UserMessage): void;\n }\n export type AgentCancelCause = {\n readonly kind: 'user';\n } | {\n readonly kind: 'parent';\n } | {\n readonly kind: 'hook';\n readonly reason: string;\n } | {\n readonly kind: 'disposed';\n };\n export interface AgentOptions {\n provider?: string;\n model?: string;\n maxTokens?: number;\n }\n export type AgentStatus = 'idle' | 'running';\n export interface AssistantMessage extends Message {\n readonly role: 'assistant';\n readonly source: ModelMessageSource;\n }\n export interface AssistantProvenance {\n provider: string;\n model: string;\n replayState?: unknown;\n }\n export type Branded = string & {\n readonly [BRAND]: B;\n };\n export type CallId = Branded<'CallId'>;\n export interface CancelOptions {\n keepInbox?: boolean | undefined;\n }\n export interface ContentBlockMap {\n 'text': TextBlock;\n 'reasoning': ReasoningBlock;\n 'tool-call': ToolCallBlock;\n 'tool-result': ToolResultBlock;\n }\n export type ContentBlockType = keyof ContentBlockMap;\n export interface DiffCallView {\n card: 'diff';\n title: string;\n diffs: FileDiff[];\n locations?: FileLocation[];\n }\n export interface DiffResultView {\n card: 'diff';\n title?: string;\n diffs: FileDiff[];\n }\n export interface EpochHeader {\n config: LlmCallConfig;\n adapterDefaults?: LlmCallConfigAdapterDefaults;\n system?: string;\n tools?: ToolSchema[];\n }\n export interface FileDiff {\n path: string;\n oldText: string | null;\n newText: string;\n }\n export interface FileLocation {\n path: string;\n line?: number;\n }\n export type FinishReason = FinishReasonMap[keyof FinishReasonMap];\n export interface FinishReasonMap {\n 'stop': {\n kind: 'stop';\n };\n 'tool-calls': {\n kind: 'tool-calls';\n };\n 'max-tokens': {\n kind: 'max-tokens';\n };\n 'aborted': {\n kind: 'aborted';\n failure: LlmFailure;\n };\n 'error': {\n kind: 'error';\n failure: LlmFailure;\n };\n }\n export interface GenericCallView {\n card: 'generic';\n title: string;\n kind?: ToolCallKind;\n rawInput?: unknown;\n content?: ContentBlock[];\n locations?: FileLocation[];\n }\n export interface GenericResultView {\n card: 'generic';\n title?: string;\n content?: ContentBlock[];\n }\n export class Inbox {\n constructor(private readonly session: Session, private readonly notifications: InboxNotifications);\n get nextTurn(): readonly UserMessage[];\n get nextStep(): readonly UserMessage[];\n get hasPending(): boolean;\n clear(): void;\n claim(target: InboxTarget, turn: number): UserMessage[];\n append(target: InboxTarget, message: UserMessage): void;\n prepend(target: InboxTarget, message: UserMessage): void;\n replace(messageId: MessageId, newMessage: UserMessage): boolean;\n remove(messageId: MessageId): boolean;\n splice(target: InboxTarget, start: number, deleteCount: number, inserted: UserMessage[]): UserMessage[];\n }\n export interface InboxNotifications {\n inserted(message: UserMessage): void;\n discarded(message: UserMessage): void;\n claimed(message: UserMessage, turn: number): void;\n }\n export type InboxTarget = 'next-turn' | 'next-step';\n export interface JsonSchemaNode {\n type?: JsonSchemaType;\n oneOf?: JsonSchemaNode[];\n properties?: Record;\n required?: string[];\n additionalProperties?: boolean;\n items?: JsonSchemaNode;\n enum?: JsonSchemaScalar[];\n const?: JsonSchemaScalar;\n description?: string;\n title?: string;\n default?: JsonValue;\n examples?: JsonValue;\n }\n export type JsonSchemaScalar = string | number | boolean | null;\n export type JsonSchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null';\n export type JsonValue = null | boolean | number | string | JsonValue[] | {\n [key: string]: JsonValue;\n };\n export interface LlmCallConfig {\n provider: string;\n model: string;\n reasoningEffort?: ReasoningEffortId;\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n }\n export interface LlmCallConfigAdapterDefaults {\n reasoningEffort?: true;\n maxTokens?: true;\n }\n export interface LlmFailure {\n readonly message: string;\n readonly code: string;\n readonly status?: number;\n readonly providerRetryAfterMs?: number;\n readonly requestId?: ProviderRequestId;\n }\n export interface Message {\n readonly id: MessageId;\n readonly role: 'system' | 'user' | 'assistant';\n readonly content: ContentBlock[];\n readonly source: MessageSource;\n }\n export type MessageId = Branded<'MessageId'>;\n export type MessageSource = MessageSourceMap[keyof MessageSourceMap];\n export interface MessageSourceMap {\n user: {\n kind: 'user';\n };\n plugin: {\n kind: 'plugin';\n plugin: string;\n };\n model: ModelMessageSource;\n tool: ToolMessageSource;\n }\n export interface ModelMessageSource extends AssistantProvenance {\n kind: 'model';\n }\n export type ProviderRequestId = Branded<'ProviderRequestId'>;\n export interface ReadFileLine {\n number: number;\n text: string;\n }\n export interface ReadResultView {\n card: 'read';\n title?: string;\n path: string;\n offset: number;\n lines: ReadFileLine[];\n totalLines: number;\n lang?: string;\n content?: ContentBlock[];\n }\n export interface ReasoningBlock {\n type: 'reasoning';\n text: string;\n }\n export type ReasoningEffortId = Branded<'ReasoningEffortId'>;\n export interface RequestContext {\n provider: string;\n model: string;\n contextWindow?: number;\n }\n export type RequestHeaderReason = 'initial' | 'resume' | 'change';\n export type ScopeKey = object;\n export interface SearchFileMatches {\n path: string;\n matches: SearchLineMatch[];\n }\n export interface SearchLineMatch {\n lineNumber: number;\n line: string;\n }\n export interface SearchMatchesResultView {\n card: 'search';\n shape: 'matches';\n title?: string;\n files: SearchFileMatches[];\n truncated: boolean;\n total: number;\n }\n export interface SearchPathsResultView {\n card: 'search';\n shape: 'paths';\n title?: string;\n paths: string[];\n truncated: boolean;\n total: number;\n }\n export type SearchResultView = SearchMatchesResultView | SearchPathsResultView;\n export class Session {\n get surface(): SessionSurface;\n readonly header: SessionHeader;\n get id(): SessionId;\n readonly firstLiveSeq: number;\n static create(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader): Session;\n static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader): Session;\n get events(): readonly SessionEvent[];\n get seq(): number;\n append(type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [\n opts: SurfaceIntent\n ] : [\n ]): SessionEvent;\n requestHeader(): EpochHeader | undefined;\n requestContext(): RequestContext | undefined;\n deriveMessages(): Message[];\n deriveEventMessage(event: SessionEvent): Message | null;\n }\n export type SessionEvent = {\n [K in SessionEventType]: {\n type: K;\n seq: number;\n time: number;\n data: SessionEventMap[K];\n } & (K extends SurfaceEventType ? {\n sourceEventSeqs?: number[];\n surfaceOp?: SurfaceOp;\n } : object);\n }[T];\n export interface SessionEventMap {\n 'turn/start': {\n turn: number;\n };\n 'turn/end': {\n turn: number;\n reason: TurnEndReason;\n };\n 'step/start': {\n turn: number;\n step: number;\n };\n 'step/end': {\n turn: number;\n step: number;\n };\n 'user/message': UserMessage;\n 'assistant/chunk': {\n turn: number;\n step: number;\n chunk: StreamChunk;\n };\n 'assistant/message': {\n turn: number;\n step: number;\n message: AssistantMessage;\n usage?: TokenUsage;\n };\n 'tool/call': {\n turn: number;\n step: number;\n callId: CallId;\n name: string;\n arguments: string;\n };\n 'tool/result': {\n turn: number;\n step: number;\n message: ToolResultMessage;\n error?: {\n name: string;\n code: string;\n };\n meta?: JsonValue;\n };\n 'todo/write': {\n todos: TodoItem[];\n };\n 'request/header': {\n header: EpochHeader;\n reason: RequestHeaderReason;\n };\n 'request/context': RequestContext;\n 'session/end-seed': Record;\n }\n export type SessionEventType = keyof SessionEventMap;\n export interface SessionHeader {\n readonly version: number;\n readonly id: SessionId;\n readonly createdAt: number;\n readonly cwd?: string;\n readonly parentSession?: SessionId;\n readonly seedLength?: number;\n readonly origin?: 'subagent';\n readonly delegationDepth?: number;\n }\n export type SessionId = Branded<'SessionId'>;\n export interface SessionSurface {\n readonly nodes: readonly number[];\n readonly replaceGeneration: number;\n }\n export type StreamChunk = {\n type: 'block-start';\n index: number;\n blockType: ContentBlockType;\n } | {\n type: 'text-delta';\n index: number;\n text: string;\n } | {\n type: 'reasoning-delta';\n index: number;\n text: string;\n } | {\n type: 'tool-call-delta';\n index: number;\n id: CallId;\n name?: string;\n argumentsDelta: string;\n } | {\n type: 'block-end';\n index: number;\n block: ContentBlock;\n } | {\n type: 'usage';\n usage: TokenUsage;\n } | {\n type: 'finish';\n reason: FinishReason;\n replayState?: unknown;\n };\n export type SurfaceEventType = 'user/message' | 'assistant/message' | 'tool/result';\n export interface SurfaceIntent {\n surfaceOp: SurfaceOp;\n sourceEventSeqs?: number[];\n }\n export type SurfaceOp = 'append' | {\n op: 'replace';\n start: number;\n end: number;\n };\n export interface TerminalCallView {\n card: 'terminal';\n title: string;\n description?: string;\n cwd?: string;\n }\n export interface TerminalResultView {\n card: 'terminal';\n title?: string;\n output?: string;\n exitCode?: number;\n signal?: string;\n }\n export interface TodoItem {\n content: string;\n status: 'pending' | 'in_progress' | 'completed';\n }\n export interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n reasoningTokens?: number;\n }\n export interface ToolCallBlock {\n type: 'tool-call';\n id: CallId;\n name: string;\n arguments: string;\n }\n export type ToolCallKind = 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other';\n export type ToolCallView = GenericCallView | TerminalCallView | DiffCallView;\n export interface ToolDefinition extends ToolSchema {\n readonly output: ToolOutputDefinition;\n execute(args: unknown, exec: ToolRunContext): Promise;\n finalizeContent?(exec: Readonly, result: Readonly): ContentBlock[] | undefined;\n timeoutMs?: number;\n isConcurrencySafe?(args: unknown): boolean;\n presentCall?(args: unknown): ToolCallView | undefined;\n presentResult?(args: unknown, result: ToolResult): ToolResultView | undefined;\n }\n export interface ToolErrorInfo {\n name: string;\n code: string;\n }\n export interface ToolExecution extends ToolExecutionInput {\n readonly token: ToolExecutionToken;\n }\n export interface ToolExecutionFailure {\n readonly isError: true;\n readonly error: ToolFailure;\n readonly value?: never;\n readonly content: ContentBlock[];\n readonly meta?: JsonValue;\n readonly additionalContexts?: UserMessage[];\n readonly concludesTurn?: never;\n }\n export interface ToolExecutionInput {\n readonly callId: CallId;\n readonly name: string;\n readonly arguments: unknown;\n readonly agent?: Agent;\n readonly parent?: ToolExecutionToken;\n readonly signal: AbortSignal;\n }\n export type ToolExecutionMode = {\n kind: 'parallel';\n } | {\n kind: 'exclusive';\n };\n export type ToolExecutionResult = ToolExecutionSuccess | ToolExecutionFailure;\n export interface ToolExecutionSuccess {\n readonly isError: false;\n readonly value: JsonValue;\n readonly content: ContentBlock[];\n readonly error?: never;\n readonly meta?: JsonValue;\n readonly additionalContexts?: UserMessage[];\n readonly concludesTurn?: true;\n }\n export type ToolExecutionToken = symbol & {\n readonly [toolExecutionTokenBrand]: true;\n };\n export interface ToolFailure {\n message: string;\n info?: ToolErrorInfo;\n }\n export type ToolGuard = (execution: Readonly) => string | undefined;\n export interface ToolMessageSource {\n kind: 'tool';\n callId: CallId;\n }\n export interface ToolOutputDefinition {\n readonly schema: JsonSchemaNode;\n render(args: unknown, value: JsonValue): ContentBlock[];\n presentationMeta?(args: unknown, value: JsonValue): JsonValue;\n }\n export interface ToolRestriction {\n readonly allow?: readonly string[];\n readonly deny?: readonly string[];\n }\n export interface ToolResult {\n content: ContentBlock[];\n isError: boolean;\n meta?: JsonValue;\n }\n export interface ToolResultBlock {\n type: 'tool-result';\n toolCallId: CallId;\n content: ContentBlock[];\n isError?: boolean;\n }\n export interface ToolResultMessage extends Message {\n readonly role: 'user';\n readonly content: [\n ToolResultBlock\n ];\n readonly source: ToolMessageSource;\n }\n export type ToolResultView = GenericResultView | TerminalResultView | DiffResultView | SearchResultView | ReadResultView | WebResultView;\n export interface ToolRunContext extends ToolExecution {\n deferContext(context: UserMessage): void;\n concludeTurn(): void;\n }\n export interface ToolSchema {\n name: string;\n description: string;\n parameters: Record;\n }\n export type TurnEndCancelCause = AgentCancelCause | {\n readonly kind: 'legacy';\n };\n export type TurnEndReason = TurnEndReasonMap[keyof TurnEndReasonMap];\n export interface TurnEndReasonMap {\n completed: {\n kind: 'completed';\n };\n aborted: {\n kind: 'aborted';\n reason: TurnEndCancelCause;\n };\n blocked: {\n kind: 'blocked';\n };\n error: {\n kind: 'error';\n error: LlmFailure;\n };\n 'max-tokens': {\n kind: 'max-tokens';\n };\n interrupted: {\n kind: 'interrupted';\n };\n }\n export interface UserMessage extends Message {\n readonly role: 'user';\n }\n export interface WebFetchResultView {\n card: 'web';\n kind: 'fetch';\n title?: string;\n url: string;\n statusCode: number;\n truncated: boolean;\n }\n export type WebResultView = WebSearchResultView | WebFetchResultView;\n export interface WebSearchResultView {\n card: 'web';\n kind: 'search';\n title?: string;\n sources: WebSource[];\n answer?: string;\n truncated: boolean;\n }\n export interface WebSource {\n url: string;\n title?: string;\n snippet?: string;\n publishedAt?: string;\n }"}],"isError":false}],"role":"user","id":"847bf2e6-59da-4621-946d-06932a78f0ce"}},"sourceEventSeqs":[15],"surfaceOp":"append"} +{"type":"tool/result","seq":16,"time":1785730459904,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"inspect-tools-api"},"content":[{"type":"tool-result","toolCallId":"inspect-tools-api","content":[{"type":"text","text":"## api\n- tools — Tool registry and execution pipeline.\n /**\n * Register globally or in the calling agent scope. Scoped tools shadow\n * globals; duplicates within one layer and the reserved `run_code` name fail.\n * @param definition - tool schema, execution, and optional finalization/presentation callbacks.\n * @returns the exact disposer that unregisters the tool.\n */\n register(definition: ToolDefinition): () => void\n /**\n * Restrict global tools for the calling agent scope. Empty filters, unknown\n * names, scope-local names, and reserved transport names fail. Restrictions\n * intersect; scoped registrations remain visible.\n * @param filter - global-surface mask: `allow` (keep only) and/or `deny` (remove).\n * @returns the exact disposer that lifts this restriction.\n */\n restrict(filter: ToolRestriction): () => void\n /**\n * Register a monotonic guard after the extensible `tools/pre-execute`\n * waterfall. A plain-context guard applies globally; one registered through\n * `agent.ctx` applies only to that agent. Any matching guard may deny by\n * returning a reason, while no guard can force-allow a call another guard\n * denied. The exact effect disposer is returned for ordered ownership and\n * HMR cleanup.\n * @param guard - synchronous check; a returned string denies the execution.\n * @returns the exact disposer that unregisters the guard.\n */\n guard(guard: ToolGuard): () => void\n /**\n * Look up a tool as one scope sees it (scoped\n * shadows global; a restricted-away global reads as absent). Presenters pass\n * the calling agent so the rendered card matches the definition that\n * actually executed.\n * @param name - the tool name as registered.\n * @param scope - the viewing scope (the agent); omitted = the global view.\n * @returns the definition the scope resolves, or undefined when none is visible.\n */\n get(name: string, scope?: ScopeKey): ToolDefinition | undefined\n /**\n * Project visible definitions onto the allowlisted model-facing schema fields,\n * excluding execution and presentation callbacks.\n * @param scope - the viewing scope (the agent); omitted = the global view.\n * @returns one deep-cloned schema per visible tool.\n */\n schemas(scope?: ScopeKey): ToolSchema[]\n /**\n * Classify a pending call through the caller's visible tool definition. Only\n * an exact `true` is parallel; unknown, hidden, undeclared, invalid, or\n * throwing classifiers are exclusive.\n * @param exec - call name, parsed arguments, and optional agent scope.\n * @returns the fail-closed scheduling mode.\n */\n executionMode(exec: ToolExecutionInput): ToolExecutionMode\n /**\n * Execute through pre-policy, guards, around-dispatch, post-policy,\n * definition-owned content finalization, and final notification. Tool and\n * listener failures resolve as materialized error results; an invisible tool\n * reports `UNKNOWN_TOOL`. The returned outcome is the same lossless, frozen\n * snapshot final observers receive. Cancellation\n * arriving after entry and before final result materialization skips a\n * not-yet-started body with `ABORTED_BEFORE_DISPATCH` or replaces a\n * successful started outcome with `ABORTED`; already-started work is still\n * drained and may retain a tool-owned structured error.\n * @param exec - the typed same-process call input. The registry assigns its\n * correlation token before policy begins.\n * @returns the materialized final result.\n */\n async execute(exec: ToolExecutionInput): Promise\ntype shapes (referenced by the signatures above — read these before assuming a field is a string):\n export interface Agent {\n readonly id: SessionId;\n readonly options: AgentOptions;\n readonly session: Session;\n readonly inbox: Inbox;\n readonly status: AgentStatus;\n readonly ctx: Context;\n cancel(cause: AgentCancelCause, options?: CancelOptions): void;\n whenIdle(): Promise;\n runMaintenance(task: (signal: AbortSignal) => Promise): Promise;\n send(message: UserMessage, target: InboxTarget, wakeup: boolean): void;\n followup(message: UserMessage): void;\n steer(message: UserMessage): void;\n inject(message: UserMessage): void;\n }\n export type AgentCancelCause = {\n readonly kind: 'user';\n } | {\n readonly kind: 'parent';\n } | {\n readonly kind: 'hook';\n readonly reason: string;\n } | {\n readonly kind: 'disposed';\n };\n export interface AgentOptions {\n provider?: string;\n model?: string;\n maxTokens?: number;\n }\n export type AgentStatus = 'idle' | 'running';\n export interface AssistantMessage extends Message {\n readonly role: 'assistant';\n readonly source: ModelMessageSource;\n }\n export interface AssistantProvenance {\n provider: string;\n model: string;\n replayState?: unknown;\n }\n export type Branded = string & {\n readonly [BRAND]: B;\n };\n export type CallId = Branded<'CallId'>;\n export interface CancelOptions {\n keepInbox?: boolean | undefined;\n }\n export interface ContentBlockMap {\n 'text': TextBlock;\n 'reasoning': ReasoningBlock;\n 'tool-call': ToolCallBlock;\n 'tool-result': ToolResultBlock;\n }\n export type ContentBlockType = keyof ContentBlockMap;\n export type ContextFormed = {\n readonly form?: never;\n } | {\n readonly form: 'instructions';\n } | {\n readonly form: 'catalog';\n } | {\n readonly form: 'snapshot';\n readonly sections: readonly ContextSnapshotSection[];\n } | {\n readonly form: 'notice';\n readonly summary: string;\n } | {\n readonly form: 'relay';\n } | {\n readonly form: 'recall';\n };\n export interface ContextSnapshotSection {\n readonly name: string;\n readonly text: string;\n }\n export interface DiffCallView {\n card: 'diff';\n title: string;\n diffs: FileDiff[];\n locations?: FileLocation[];\n }\n export interface DiffResultView {\n card: 'diff';\n title?: string;\n diffs: FileDiff[];\n }\n export interface EpochHeader {\n config: LlmCallConfig;\n adapterDefaults?: LlmCallConfigAdapterDefaults;\n system?: string;\n tools?: ToolSchema[];\n }\n export interface FileDiff {\n path: string;\n oldText: string | null;\n newText: string;\n }\n export interface FileLocation {\n path: string;\n line?: number;\n }\n export type FinishReason = FinishReasonMap[keyof FinishReasonMap];\n export interface FinishReasonMap {\n 'stop': {\n kind: 'stop';\n };\n 'tool-calls': {\n kind: 'tool-calls';\n };\n 'max-tokens': {\n kind: 'max-tokens';\n };\n 'aborted': {\n kind: 'aborted';\n failure: LlmFailure;\n };\n 'error': {\n kind: 'error';\n failure: LlmFailure;\n };\n }\n export interface GenericCallView {\n card: 'generic';\n title: string;\n kind?: ToolCallKind;\n rawInput?: unknown;\n content?: ContentBlock[];\n locations?: FileLocation[];\n }\n export interface GenericResultView {\n card: 'generic';\n title?: string;\n content?: ContentBlock[];\n }\n export class Inbox {\n constructor(private readonly session: Session, private readonly notifications: InboxNotifications);\n get nextTurn(): readonly UserMessage[];\n get nextStep(): readonly UserMessage[];\n get hasPending(): boolean;\n clear(): void;\n claim(target: InboxTarget, turn: number): UserMessage[];\n append(target: InboxTarget, message: UserMessage): void;\n prepend(target: InboxTarget, message: UserMessage): void;\n replace(messageId: MessageId, newMessage: UserMessage): boolean;\n remove(messageId: MessageId): boolean;\n splice(target: InboxTarget, start: number, deleteCount: number, inserted: UserMessage[]): UserMessage[];\n }\n export interface InboxNotifications {\n inserted(message: UserMessage): void;\n discarded(message: UserMessage): void;\n claimed(message: UserMessage, turn: number): void;\n }\n export type InboxTarget = 'next-turn' | 'next-step';\n export interface JsonSchemaNode {\n type?: JsonSchemaType;\n oneOf?: JsonSchemaNode[];\n properties?: Record;\n required?: string[];\n additionalProperties?: boolean;\n items?: JsonSchemaNode;\n enum?: JsonSchemaScalar[];\n const?: JsonSchemaScalar;\n description?: string;\n title?: string;\n default?: JsonValue;\n examples?: JsonValue;\n }\n export type JsonSchemaScalar = string | number | boolean | null;\n export type JsonSchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null';\n export type JsonValue = null | boolean | number | string | JsonValue[] | {\n [key: string]: JsonValue;\n };\n export interface LlmCallConfig {\n provider: string;\n model: string;\n reasoningEffort?: ReasoningEffortId;\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n }\n export interface LlmCallConfigAdapterDefaults {\n reasoningEffort?: true;\n maxTokens?: true;\n }\n export interface LlmFailure {\n readonly message: string;\n readonly code: string;\n readonly status?: number;\n readonly providerRetryAfterMs?: number;\n readonly requestId?: ProviderRequestId;\n }\n export interface Message {\n readonly id: MessageId;\n readonly role: 'system' | 'user' | 'assistant';\n readonly content: ContentBlock[];\n readonly source: MessageSource;\n }\n export type MessageId = Branded<'MessageId'>;\n export type MessageSource = MessageSourceMap[keyof MessageSourceMap];\n export interface MessageSourceMap {\n user: {\n kind: 'user';\n };\n plugin: {\n kind: 'plugin';\n plugin: string;\n } & ContextFormed;\n model: ModelMessageSource;\n tool: ToolMessageSource;\n }\n export interface ModelMessageSource extends AssistantProvenance {\n kind: 'model';\n }\n export type ProviderRequestId = Branded<'ProviderRequestId'>;\n export interface ReadFileLine {\n number: number;\n text: string;\n }\n export interface ReadResultView {\n card: 'read';\n title?: string;\n path: string;\n offset: number;\n lines: ReadFileLine[];\n totalLines: number;\n lang?: string;\n content?: ContentBlock[];\n }\n export interface ReasoningBlock {\n type: 'reasoning';\n text: string;\n }\n export type ReasoningEffortId = Branded<'ReasoningEffortId'>;\n export interface RequestContext {\n provider: string;\n model: string;\n contextWindow?: number;\n }\n export type RequestHeaderReason = 'initial' | 'resume' | 'change';\n export type ScopeKey = object;\n export interface SearchFileMatches {\n path: string;\n matches: SearchLineMatch[];\n }\n export interface SearchLineMatch {\n lineNumber: number;\n line: string;\n }\n export interface SearchMatchesResultView {\n card: 'search';\n shape: 'matches';\n title?: string;\n files: SearchFileMatches[];\n truncated: boolean;\n total: number;\n }\n export interface SearchPathsResultView {\n card: 'search';\n shape: 'paths';\n title?: string;\n paths: string[];\n truncated: boolean;\n total: number;\n }\n export type SearchResultView = SearchMatchesResultView | SearchPathsResultView;\n export class Session {\n get surface(): SessionSurface;\n readonly header: SessionHeader;\n get id(): SessionId;\n readonly firstLiveSeq: number;\n static create(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader): Session;\n static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader): Session;\n get events(): readonly SessionEvent[];\n get seq(): number;\n append(type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [\n opts: SurfaceIntent\n ] : [\n ]): SessionEvent;\n requestHeader(): EpochHeader | undefined;\n requestContext(): RequestContext | undefined;\n deriveMessages(): Message[];\n deriveEventMessage(event: SessionEvent): Message | null;\n }\n export type SessionEvent = {\n [K in SessionEventType]: {\n type: K;\n seq: number;\n time: number;\n data: SessionEventMap[K];\n } & (K extends SurfaceEventType ? {\n sourceEventSeqs?: number[];\n surfaceOp?: SurfaceOp;\n } : object);\n }[T];\n export interface SessionEventMap {\n 'turn/start': {\n turn: number;\n };\n 'turn/end': {\n turn: number;\n reason: TurnEndReason;\n };\n 'step/start': {\n turn: number;\n step: number;\n };\n 'step/end': {\n turn: number;\n step: number;\n };\n 'user/message': UserMessage;\n 'assistant/chunk': {\n turn: number;\n step: number;\n chunk: StreamChunk;\n };\n 'assistant/message': {\n turn: number;\n step: number;\n message: AssistantMessage;\n usage?: TokenUsage;\n };\n 'tool/call': {\n turn: number;\n step: number;\n callId: CallId;\n name: string;\n arguments: string;\n };\n 'tool/result': {\n turn: number;\n step: number;\n message: ToolResultMessage;\n error?: {\n name: string;\n code: string;\n };\n meta?: JsonValue;\n };\n 'todo/write': {\n todos: TodoItem[];\n };\n 'request/header': {\n header: EpochHeader;\n reason: RequestHeaderReason;\n };\n 'request/context': RequestContext;\n 'session/end-seed': Record;\n }\n export type SessionEventType = keyof SessionEventMap;\n export interface SessionHeader {\n readonly version: number;\n readonly id: SessionId;\n readonly createdAt: number;\n readonly cwd?: string;\n readonly parentSession?: SessionId;\n readonly seedLength?: number;\n readonly origin?: 'subagent';\n readonly delegationDepth?: number;\n }\n export type SessionId = Branded<'SessionId'>;\n export interface SessionSurface {\n readonly nodes: readonly number[];\n readonly replaceGeneration: number;\n }\n export type StreamChunk = {\n type: 'block-start';\n index: number;\n blockType: ContentBlockType;\n } | {\n type: 'text-delta';\n index: number;\n text: string;\n } | {\n type: 'reasoning-delta';\n index: number;\n text: string;\n } | {\n type: 'tool-call-delta';\n index: number;\n id: CallId;\n name?: string;\n argumentsDelta: string;\n } | {\n type: 'block-end';\n index: number;\n block: ContentBlock;\n } | {\n type: 'usage';\n usage: TokenUsage;\n } | {\n type: 'finish';\n reason: FinishReason;\n replayState?: unknown;\n };\n export type SurfaceEventType = 'user/message' | 'assistant/message' | 'tool/result';\n export interface SurfaceIntent {\n surfaceOp: SurfaceOp;\n sourceEventSeqs?: number[];\n }\n export type SurfaceOp = 'append' | {\n op: 'replace';\n start: number;\n end: number;\n };\n export interface TerminalCallView {\n card: 'terminal';\n title: string;\n description?: string;\n cwd?: string;\n }\n export interface TerminalResultView {\n card: 'terminal';\n title?: string;\n output?: string;\n exitCode?: number;\n signal?: string;\n }\n export interface TodoItem {\n content: string;\n status: 'pending' | 'in_progress' | 'completed';\n }\n export interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n reasoningTokens?: number;\n }\n export interface ToolCallBlock {\n type: 'tool-call';\n id: CallId;\n name: string;\n arguments: string;\n }\n export type ToolCallKind = 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other';\n export type ToolCallView = GenericCallView | TerminalCallView | DiffCallView;\n export interface ToolDefinition extends ToolSchema {\n readonly output: ToolOutputDefinition;\n execute(args: unknown, exec: ToolRunContext): Promise;\n finalizeContent?(exec: Readonly, result: Readonly): ContentBlock[] | undefined;\n timeoutMs?: number;\n isConcurrencySafe?(args: unknown): boolean;\n presentCall?(args: unknown): ToolCallView | undefined;\n presentResult?(args: unknown, result: ToolResult): ToolResultView | undefined;\n }\n export interface ToolErrorInfo {\n name: string;\n code: string;\n }\n export interface ToolExecution extends ToolExecutionInput {\n readonly token: ToolExecutionToken;\n }\n export interface ToolExecutionFailure {\n readonly isError: true;\n readonly error: ToolFailure;\n readonly value?: never;\n readonly content: ContentBlock[];\n readonly meta?: JsonValue;\n readonly additionalContexts?: UserMessage[];\n readonly concludesTurn?: never;\n }\n export interface ToolExecutionInput {\n readonly callId: CallId;\n readonly name: string;\n readonly arguments: unknown;\n readonly agent?: Agent;\n readonly parent?: ToolExecutionToken;\n readonly signal: AbortSignal;\n }\n export type ToolExecutionMode = {\n kind: 'parallel';\n } | {\n kind: 'exclusive';\n };\n export type ToolExecutionResult = ToolExecutionSuccess | ToolExecutionFailure;\n export interface ToolExecutionSuccess {\n readonly isError: false;\n readonly value: JsonValue;\n readonly content: ContentBlock[];\n readonly error?: never;\n readonly meta?: JsonValue;\n readonly additionalContexts?: UserMessage[];\n readonly concludesTurn?: true;\n }\n export type ToolExecutionToken = symbol & {\n readonly [toolExecutionTokenBrand]: true;\n };\n export interface ToolFailure {\n message: string;\n info?: ToolErrorInfo;\n }\n export type ToolGuard = (execution: Readonly) => string | undefined;\n export interface ToolMessageSource {\n kind: 'tool';\n callId: CallId;\n }\n export interface ToolOutputDefinition {\n readonly schema: JsonSchemaNode;\n render(args: unknown, value: JsonValue): ContentBlock[];\n presentationMeta?(args: unknown, value: JsonValue): JsonValue;\n }\n export interface ToolRestriction {\n readonly allow?: readonly string[];\n readonly deny?: readonly string[];\n }\n export interface ToolResult {\n content: ContentBlock[];\n isError: boolean;\n meta?: JsonValue;\n }\n export interface ToolResultBlock {\n type: 'tool-result';\n toolCallId: CallId;\n content: ContentBlock[];\n isError?: boolean;\n }\n export interface ToolResultMessage extends Message {\n readonly role: 'user';\n readonly content: [\n ToolResultBlock\n ];\n readonly source: ToolMessageSource;\n }\n export type ToolResultView = GenericResultView | TerminalResultView | DiffResultView | SearchResultView | ReadResultView | WebResultView;\n export interface ToolRunContext extends ToolExecution {\n deferContext(context: UserMessage): void;\n concludeTurn(): void;\n }\n export interface ToolSchema {\n name: string;\n description: string;\n parameters: Record;\n }\n export type TurnEndCancelCause = AgentCancelCause | {\n readonly kind: 'legacy';\n };\n export type TurnEndReason = TurnEndReasonMap[keyof TurnEndReasonMap];\n export interface TurnEndReasonMap {\n completed: {\n kind: 'completed';\n };\n aborted: {\n kind: 'aborted';\n reason: TurnEndCancelCause;\n };\n blocked: {\n kind: 'blocked';\n };\n error: {\n kind: 'error';\n error: LlmFailure;\n };\n 'max-tokens': {\n kind: 'max-tokens';\n };\n interrupted: {\n kind: 'interrupted';\n };\n }\n export interface UserMessage extends Message {\n readonly role: 'user';\n }\n export interface WebFetchResultView {\n card: 'web';\n kind: 'fetch';\n title?: string;\n url: string;\n statusCode: number;\n truncated: boolean;\n }\n export type WebResultView = WebSearchResultView | WebFetchResultView;\n export interface WebSearchResultView {\n card: 'web';\n kind: 'search';\n title?: string;\n sources: WebSource[];\n answer?: string;\n truncated: boolean;\n }\n export interface WebSource {\n url: string;\n title?: string;\n snippet?: string;\n publishedAt?: string;\n }"}],"isError":false}],"role":"user","id":"847bf2e6-59da-4621-946d-06932a78f0ce"}},"sourceEventSeqs":[15],"surfaceOp":"append"} {"type":"step/end","seq":17,"time":1785730459904,"data":{"turn":1,"step":1}} {"type":"step/start","seq":18,"time":1785730459916,"data":{"turn":1,"step":2}} {"type":"assistant/chunk","seq":19,"time":1784449176734,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} diff --git a/examples/acp-agent/tests/snapshots/empty-response-retry/session.jsonl b/examples/acp-agent/tests/snapshots/empty-response-retry/session.jsonl index 741fb4d797..b619d352e5 100644 --- a/examples/acp-agent/tests/snapshots/empty-response-retry/session.jsonl +++ b/examples/acp-agent/tests/snapshots/empty-response-retry/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821397720,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498788095,"data":{"content":[{"type":"text","text":"This prompt first receives an empty completion, then a retried reply."}],"source":{"kind":"user"},"role":"user","id":"04a4b0d6-8873-4ec0-bed5-75de910b556f"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730441191,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"1bbd9bae-e790-4b83-8425-2f042dd37908"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730441191,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"1bbd9bae-e790-4b83-8425-2f042dd37908"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730441191,"data":{"title":"This prompt first receives an","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498788096,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730441192,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/error-finish/session.jsonl b/examples/acp-agent/tests/snapshots/error-finish/session.jsonl index 12afe1fa3e..fdcb57aabd 100644 --- a/examples/acp-agent/tests/snapshots/error-finish/session.jsonl +++ b/examples/acp-agent/tests/snapshots/error-finish/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821396359,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785499006415,"data":{"content":[{"type":"text","text":"This prompt triggers a recorded provider error."}],"source":{"kind":"user"},"role":"user","id":"87677683-56b7-458b-b512-6db73c570e08"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730686099,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"b3b9d048-3992-458f-aad5-b738e4a7d815"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730686099,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"b3b9d048-3992-458f-aad5-b738e4a7d815"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730686099,"data":{"title":"This prompt triggers a recorded","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785499006416,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730686100,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl b/examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl index 591cd7e4d6..77a4a39b8d 100644 --- a/examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl +++ b/examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821444447,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1784821261726,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498828313,"data":{"content":[{"type":"text","text":"The sandbox already denied writing /tmp/dsh-escalated.txt earlier (it is outside this workspace). Retry it now exactly once: one single bash call with the command printf 'escalated\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt, with sandbox_permissions set to danger-full-access and the justification 'the user asked to write a file outside the workspace'. Do not run it without sandbox_permissions first. I will approve the permission prompt. After the result, reply with the single word DONE and stop."}],"source":{"kind":"user"},"role":"user","id":"c8597dbb-3765-4c91-9315-2a5704ab60de"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730480503,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"b945fb82-1839-405c-9859-f2d4630a1801"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730480503,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"b945fb82-1839-405c-9859-f2d4630a1801"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730480503,"data":{"title":"The sandbox already denied writing","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498828315,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730480504,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl b/examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl index 4f05bbfbcf..96b51253d9 100644 --- a/examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl +++ b/examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821445663,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1784821263267,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498829488,"data":{"content":[{"type":"text","text":"The sandbox already denied writing /tmp/dsh-escalated.txt earlier (it is outside this workspace). Retry it now exactly once: one single bash call with the command printf 'escalated\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt, with sandbox_permissions set to danger-full-access and the justification 'the user asked to write a file outside the workspace'. Do not run it without sandbox_permissions first. I will reject the permission prompt; after the rejection, do not retry and do not work around it — explain in one short sentence and stop."}],"source":{"kind":"user"},"role":"user","id":"e1326897-4139-437b-959c-3b25e46e60ec"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730481594,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"016923c3-51c4-45ba-8a54-4d9d309c0d8e"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730481594,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"016923c3-51c4-45ba-8a54-4d9d309c0d8e"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730481594,"data":{"title":"The sandbox already denied writing","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498829489,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730481595,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/fs-edit/session.jsonl b/examples/acp-agent/tests/snapshots/fs-edit/session.jsonl index 18125bea36..cfcdfee68c 100644 --- a/examples/acp-agent/tests/snapshots/fs-edit/session.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-edit/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821389289,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352084742,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498781491,"data":{"content":[{"type":"text","text":"First use the read tool to read config.txt in the current directory. Then use the edit tool (NOT bash) to replace the literal text DEBUG with RELEASE in that file. Then reply with exactly the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"b900992d-cb68-45e3-bdf1-366e2529f6c0"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730434501,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"79d38e8e-c85a-434a-9638-490dea3c8ea8"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730434501,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"79d38e8e-c85a-434a-9638-490dea3c8ea8"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730434501,"data":{"title":"First use the read tool","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498781493,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730434502,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/fs-escalation-approved/session.jsonl b/examples/acp-agent/tests/snapshots/fs-escalation-approved/session.jsonl index b5feb459d3..6f69260bad 100644 --- a/examples/acp-agent/tests/snapshots/fs-escalation-approved/session.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-escalation-approved/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821446846,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1784821264855,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498830644,"data":{"content":[{"type":"text","text":"Use the write tool (NOT bash) to create escalated.md in the current directory containing exactly the single line: escalated. An equivalent write was denied earlier, so make this one single write call with sandbox_permissions set to danger-full-access and the justification 'the user asked to escalate this write'. Do not call write without sandbox_permissions first. I will approve the permission prompt. After the result, reply with exactly the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"c2a0f1a3-11ce-4d84-bff4-49213573cb37"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730482654,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"54411374-45a0-468c-b524-e5f4d0314e40"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730482654,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"54411374-45a0-468c-b524-e5f4d0314e40"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730482654,"data":{"title":"Use the write tool (NOT","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498830646,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730482655,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/fs-policy-reject/session.jsonl b/examples/acp-agent/tests/snapshots/fs-policy-reject/session.jsonl index 112a2b6cab..270cd287f4 100644 --- a/examples/acp-agent/tests/snapshots/fs-policy-reject/session.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-policy-reject/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821393932,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783611702550,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498784863,"data":{"content":[{"type":"text","text":"Do NOT use the read tool and do NOT use bash or shell commands. Immediately use the edit tool to replace the literal text blue with green in settings.txt in the current directory. Do not read the file first. After the tool result, reply with exactly the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"065530a1-5d85-4adb-9458-6511300b63bc"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730437873,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"35df0186-19a8-46d5-bdee-344a776db520"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730437873,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"35df0186-19a8-46d5-bdee-344a776db520"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730437873,"data":{"title":"Do NOT use the read","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498784864,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730437874,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/fs-read-window/session.jsonl b/examples/acp-agent/tests/snapshots/fs-read-window/session.jsonl index ea8e65620c..f9c19a18d3 100644 --- a/examples/acp-agent/tests/snapshots/fs-read-window/session.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-read-window/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821392334,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352099840,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498783725,"data":{"content":[{"type":"text","text":"Use the read tool (NOT bash) with offset 5 and limit 4 to read lines 5 through 8 of big.txt in the current directory. Then reply with exactly the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"a6db8c80-6239-490e-8ee4-1e2074d73a19"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730436765,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"d5453309-c7da-4071-b46f-5441ca4a828b"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730436765,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"d5453309-c7da-4071-b46f-5441ca4a828b"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730436765,"data":{"title":"Use the read tool (NOT","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498783727,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730436766,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/fs-read/session.jsonl b/examples/acp-agent/tests/snapshots/fs-read/session.jsonl index 0373748f2c..9bab6eb99e 100644 --- a/examples/acp-agent/tests/snapshots/fs-read/session.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-read/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821386137,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352072470,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498779296,"data":{"content":[{"type":"text","text":"Use the read tool (NOT bash) to read the file greeting.txt in the current directory, then reply with exactly the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"3b9f093c-8fed-49d1-8252-7e6560033ebd"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730432294,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"d2b5abf5-ff22-4268-bac3-b6338c6e2f02"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730432294,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"d2b5abf5-ff22-4268-bac3-b6338c6e2f02"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730432294,"data":{"title":"Use the read tool (NOT","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498779297,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730432295,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/fs-write-overwrite/session.jsonl b/examples/acp-agent/tests/snapshots/fs-write-overwrite/session.jsonl index 67c4a6c2be..52310b746b 100644 --- a/examples/acp-agent/tests/snapshots/fs-write-overwrite/session.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-write-overwrite/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821390865,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352092223,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498782618,"data":{"content":[{"type":"text","text":"First use the read tool to read data.txt in the current directory. Then use the write tool (NOT bash) to replace its entire contents with exactly the single line: replaced. Then reply with exactly the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"e1697ae3-3d38-4492-9dad-5115f056934a"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730435638,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"3d35609b-3d69-4790-8078-c79eff29bbd8"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730435638,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"3d35609b-3d69-4790-8078-c79eff29bbd8"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730435638,"data":{"title":"First use the read tool","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498782619,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730435639,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/fs-write/session.jsonl b/examples/acp-agent/tests/snapshots/fs-write/session.jsonl index 48a3c835c9..461d8999fe 100644 --- a/examples/acp-agent/tests/snapshots/fs-write/session.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-write/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821387697,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352078756,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498780381,"data":{"content":[{"type":"text","text":"Use the write tool (NOT bash) to create a file named notes.txt in the current directory containing exactly the single line: hello world. Then reply with exactly the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"8316fddb-e888-4ba9-b280-2d2bb8717633"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730433386,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"b54d8375-2277-4551-bd0b-06b40d1ad59a"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730433386,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"b54d8375-2277-4551-bd0b-06b40d1ad59a"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730433386,"data":{"title":"Use the write tool (NOT","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498780382,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730433387,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-invalid-matcher/session.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-invalid-matcher/session.jsonl index 11409c2a03..bc5f42573a 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-invalid-matcher/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-invalid-matcher/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821424016,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783600629542,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498807263,"data":{"content":[{"type":"text","text":"Reply with exactly the word: PONG. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"a56c3c26-071d-407c-8900-d84de1222c0c"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730463095,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"fe569552-1e83-41d2-a240-55df5da79bc9"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730463095,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"fe569552-1e83-41d2-a240-55df5da79bc9"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730463095,"data":{"title":"Reply with exactly the word:","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498807265,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730463096,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-posttool-block/session.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-posttool-block/session.jsonl index c8c3b91c7d..79bfe25837 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-posttool-block/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-posttool-block/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821430451,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783962504152,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498813609,"data":{"content":[{"type":"text","text":"Call the bash tool to run exactly: echo HELLO. If the first tool result is rejected, retry that command once. Quote the final tool result verbatim and stop."}],"source":{"kind":"user"},"role":"user","id":"ff685d2f-c629-45a2-a6b1-9aba6679e804"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730468551,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"30410f7f-af50-4d13-898a-6fc04927fd93"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730468551,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"30410f7f-af50-4d13-898a-6fc04927fd93"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730468551,"data":{"title":"Call the bash tool to","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498813611,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730468552,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-posttool-context/session.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-posttool-context/session.jsonl index 597b1eaa28..3de51d6886 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-posttool-context/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-posttool-context/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821431674,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352196664,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498815008,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo HELLO. Report the tool result you got back verbatim, then stop."}],"source":{"kind":"user"},"role":"user","id":"f13c12f8-c187-4bae-bab7-a63d04e66f38"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730469687,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"6bba4af4-6406-410c-b730-541278dcdbd7"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730469687,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"6bba4af4-6406-410c-b730-541278dcdbd7"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730469687,"data":{"title":"Use the bash tool to","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498815010,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730469688,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl index 1ee79964b0..e917d074d7 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821429300,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352171527,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498812202,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo HELLO. Report the tool result you got back verbatim, then stop."}],"source":{"kind":"user"},"role":"user","id":"8b8672e5-2bff-458d-b482-51b703f61dcb"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730467496,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"9d525efc-a44b-4217-a882-d29d8feb042f"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730467496,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"9d525efc-a44b-4217-a882-d29d8feb042f"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730467496,"data":{"title":"Use the bash tool to","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498812203,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730467497,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-pretool-deny/session.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-pretool-deny/session.jsonl index 880eb36f8c..b3aac9fb00 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-pretool-deny/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-pretool-deny/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821428131,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352165198,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498810766,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo HELLO. Report the tool result you got back verbatim, then stop."}],"source":{"kind":"user"},"role":"user","id":"8b449df9-9149-4e05-8464-5fccbbbf06ba"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730466373,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"37e3a9e3-c9f8-431f-8af2-aa16d270e534"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730466373,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"37e3a9e3-c9f8-431f-8af2-aa16d270e534"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730466373,"data":{"title":"Use the bash tool to","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498810768,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730466374,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-context/session.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-context/session.jsonl index e2a05b7d1d..b03b6b7b4e 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-context/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-context/session.jsonl @@ -6,7 +6,7 @@ {"type":"hook/result","seq":4,"time":1785821426920,"data":{"turn":1,"point":"UserPromptSubmit","handlerId":"claude:UserPromptSubmit:1","decision":"pass","exitCode":0,"durationMs":4.92145800000003}} {"type":"step/start","seq":5,"time":1785821426949,"data":{"turn":1,"step":1}} {"type":"user/message","seq":6,"time":1785730465275,"data":{"content":[{"type":"text","text":"What is my favorite color? Reply with just the color and stop. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"c403acd5-efa4-4c8c-948f-211f3b23c93f"},"surfaceOp":"append"} -{"type":"user/message","seq":7,"time":1785821426950,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"2514657a-056c-46a8-ac90-c0169b42f048"},"surfaceOp":"append"} +{"type":"user/message","seq":7,"time":1785821426950,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"2514657a-056c-46a8-ac90-c0169b42f048"},"surfaceOp":"append"} {"type":"user/message","seq":8,"time":1785821426950,"data":{"content":[{"type":"text","text":"The user has previously stated their favorite color is teal."}],"source":{"kind":"plugin","plugin":"hooks-claude"},"role":"user","id":"8006cbd3-a233-4d35-a61b-1a9e0c6b4545"},"surfaceOp":"append"} {"type":"session/title","seq":9,"time":1785821426950,"data":{"title":"What is my favorite color?","messageSeqs":[6],"source":{"kind":"fallback"}}} {"type":"request/header","seq":10,"time":1785821426951,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-stop-continue/session.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-stop-continue/session.jsonl index 96fe89d332..f2e54f6499 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-stop-continue/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-stop-continue/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821432845,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1784522140648,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498816483,"data":{"content":[{"type":"text","text":"Reply with the single word FIRST and stop."}],"source":{"kind":"user"},"role":"user","id":"4f322d30-9425-4c61-afbb-ee5432ba6552"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730470752,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"b3914542-4c81-4699-b07e-863d2ef3a818"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730470752,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"b3914542-4c81-4699-b07e-863d2ef3a818"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730470752,"data":{"title":"Reply with the single word","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498816486,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730470753,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/hook-codex-invalid-matcher/session.jsonl b/examples/acp-agent/tests/snapshots/hook-codex-invalid-matcher/session.jsonl index a161045ea5..374ae19eb3 100644 --- a/examples/acp-agent/tests/snapshots/hook-codex-invalid-matcher/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-codex-invalid-matcher/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821425396,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783600629542,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498808410,"data":{"content":[{"type":"text","text":"Reply with exactly the word: PONG. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"14d17f1b-63f3-478a-8859-2c0d8cbbf38d"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730464165,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"a4958955-419b-49bf-848b-d404c24e0061"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730464165,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"a4958955-419b-49bf-848b-d404c24e0061"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730464165,"data":{"title":"Reply with exactly the word:","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498808411,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730464166,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/hook-codex-posttool-block/session.jsonl b/examples/acp-agent/tests/snapshots/hook-codex-posttool-block/session.jsonl index 2903c7a247..cd72379fa4 100644 --- a/examples/acp-agent/tests/snapshots/hook-codex-posttool-block/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-codex-posttool-block/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821436674,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783986962240,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498820704,"data":{"content":[{"type":"text","text":"Call the bash tool exactly once to run: echo HELLO. Whatever tool result comes back, quote it verbatim and stop without calling another tool."}],"source":{"kind":"user"},"role":"user","id":"3c6acf4d-845a-44e9-9fde-0ff9611f1b89"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730473886,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"90fd41ec-8404-4c36-8c80-9eec3dda86a7"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730473886,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"90fd41ec-8404-4c36-8c80-9eec3dda86a7"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730473886,"data":{"title":"Call the bash tool exactly","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498820706,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730473887,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/hook-codex-posttool-context/session.jsonl b/examples/acp-agent/tests/snapshots/hook-codex-posttool-context/session.jsonl index 2813196245..085aca05da 100644 --- a/examples/acp-agent/tests/snapshots/hook-codex-posttool-context/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-codex-posttool-context/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821437930,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352228443,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498822136,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo HELLO. Report the tool result you got back verbatim, then stop."}],"source":{"kind":"user"},"role":"user","id":"428246ac-6aee-4609-9ff4-5c5f5755fb61"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730474943,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"442c4504-a8f1-4e47-9314-e3d2badd93df"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730474943,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"442c4504-a8f1-4e47-9314-e3d2badd93df"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730474943,"data":{"title":"Use the bash tool to","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498822138,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730474944,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/hook-codex-pretool-block/session.jsonl b/examples/acp-agent/tests/snapshots/hook-codex-pretool-block/session.jsonl index a329513cee..eb521c4df8 100644 --- a/examples/acp-agent/tests/snapshots/hook-codex-pretool-block/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-codex-pretool-block/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821435310,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352214607,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498819368,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo HELLO. Report the tool result you got back verbatim, then stop."}],"source":{"kind":"user"},"role":"user","id":"83299ced-cede-4e39-a425-4b58915f8c06"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730472832,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"a01d2417-d639-4920-ae79-bd3aa6b5c3bb"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730472832,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"a01d2417-d639-4920-ae79-bd3aa6b5c3bb"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730472832,"data":{"title":"Use the bash tool to","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498819371,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730472833,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-context/session.jsonl b/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-context/session.jsonl index b2a6d287da..e046950b21 100644 --- a/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-context/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-context/session.jsonl @@ -6,7 +6,7 @@ {"type":"hook/result","seq":4,"time":1785821434017,"data":{"turn":1,"point":"UserPromptSubmit","handlerId":"codex:UserPromptSubmit:1","decision":"pass","exitCode":0,"durationMs":3.7565839999999753}} {"type":"step/start","seq":5,"time":1785821434044,"data":{"turn":1,"step":1}} {"type":"user/message","seq":6,"time":1785730471801,"data":{"content":[{"type":"text","text":"What is my favorite color? Reply with just the color and stop. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"8d3df251-9583-4ddb-9ead-a50df35bbac6"},"surfaceOp":"append"} -{"type":"user/message","seq":7,"time":1785821434044,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"e5f01e9b-c7c7-4f33-b3aa-b949ad404d98"},"surfaceOp":"append"} +{"type":"user/message","seq":7,"time":1785821434044,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"e5f01e9b-c7c7-4f33-b3aa-b949ad404d98"},"surfaceOp":"append"} {"type":"user/message","seq":8,"time":1785821434044,"data":{"content":[{"type":"text","text":"The user has previously stated their favorite color is teal."}],"source":{"kind":"plugin","plugin":"hooks-codex"},"role":"user","id":"7c3bd47e-8613-4853-bf55-769ece5c609e"},"surfaceOp":"append"} {"type":"session/title","seq":9,"time":1785821434044,"data":{"title":"What is my favorite color?","messageSeqs":[6],"source":{"kind":"fallback"}}} {"type":"request/header","seq":10,"time":1785821434045,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} diff --git a/examples/acp-agent/tests/snapshots/hook-codex-stop-continue/session.jsonl b/examples/acp-agent/tests/snapshots/hook-codex-stop-continue/session.jsonl index b28ab2e081..11f329a53b 100644 --- a/examples/acp-agent/tests/snapshots/hook-codex-stop-continue/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-codex-stop-continue/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821439141,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1784522152399,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498823368,"data":{"content":[{"type":"text","text":"Reply with the single word FIRST and stop."}],"source":{"kind":"user"},"role":"user","id":"26dda5a7-298f-4809-96ba-e8be4381afa5"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730476001,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"af67bfc1-182f-4dc5-bbb4-093463938e34"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730476001,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"af67bfc1-182f-4dc5-bbb4-093463938e34"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730476001,"data":{"title":"Reply with the single word","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498823370,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730476002,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/lsp-definition/session.jsonl b/examples/acp-agent/tests/snapshots/lsp-definition/session.jsonl index 9f48213a1c..471b14d87e 100644 --- a/examples/acp-agent/tests/snapshots/lsp-definition/session.jsonl +++ b/examples/acp-agent/tests/snapshots/lsp-definition/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821380423,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498775018,"data":{"content":[{"type":"text","text":"Use the lsp tool exactly once to find the definition at subject.ts line 1 character 7, then reply with exactly DONE."}],"source":{"kind":"user"},"role":"user","id":"d50783a4-e1dd-4d27-8aaf-fa854ffa5560"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730428059,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"63d79744-f179-4840-8278-b1ec07d25158"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730428059,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"63d79744-f179-4840-8278-b1ec07d25158"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730428059,"data":{"title":"Use the lsp tool exactly","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498775021,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-pro"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730428060,"data":{"provider":"deepseek-official","model":"deepseek-v4-pro"}} diff --git a/examples/acp-agent/tests/snapshots/missing-sandbox-runner/session.jsonl b/examples/acp-agent/tests/snapshots/missing-sandbox-runner/session.jsonl index cf1475057f..f9e30bb24d 100644 --- a/examples/acp-agent/tests/snapshots/missing-sandbox-runner/session.jsonl +++ b/examples/acp-agent/tests/snapshots/missing-sandbox-runner/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785916902430,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1785916902458,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785916902458,"data":{"content":[{"type":"text","text":"Run true once with bash in the foreground. After that fails, run true with bash in the background, read task bash-1 with task_output and wait=true, then reply with exactly RUNNER_FAILURES_SURFACED and stop."}],"source":{"kind":"user"},"role":"user","id":"2d2f8e7a-f08a-464d-8e94-048d1d95717e"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785916902459,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"de3778e7-e47a-4d34-a004-ecf43da3c9db"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785916902459,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"de3778e7-e47a-4d34-a004-ecf43da3c9db"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785916902459,"data":{"title":"Run true once with bash","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785916902460,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785916902460,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} @@ -27,7 +27,7 @@ {"type":"tool/call","seq":25,"time":1785916902500,"data":{"turn":1,"step":2,"callId":"missing-runner-background","name":"bash","arguments":"{\"command\":\"true\",\"description\":\"Exercise missing sandbox runner in background\",\"run_in_background\":true}"}} {"type":"tool/result","seq":26,"time":1785916902508,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"missing-runner-background"},"content":[{"type":"tool-result","toolCallId":"missing-runner-background","content":[{"type":"text","text":"started background task bash-1"}],"isError":false}],"role":"user","id":"a40cf397-5842-4c09-a6b8-f831eb84827c"}},"sourceEventSeqs":[25],"surfaceOp":"append"} {"type":"step/end","seq":27,"time":1785916902508,"data":{"turn":1,"step":2}} -{"type":"agent/inbox/spliced","seq":28,"time":1785916902508,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"background task bash-1 (bash: true) finished [status: killed, killed before exit]. Read its output with task_output."}],"source":{"kind":"plugin","plugin":"tool-tasks"},"role":"user","id":"989e3c2b-5b21-4694-83d5-6cddac55ce0e"}]}} +{"type":"agent/inbox/spliced","seq":28,"time":1785916902508,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"background task bash-1 (bash: true) finished [status: killed, killed before exit]. Read its output with task_output."}],"source":{"kind":"plugin","plugin":"tool-tasks","form":"notice","summary":"bash true [status: killed, killed before exit]"},"role":"user","id":"989e3c2b-5b21-4694-83d5-6cddac55ce0e"}]}} {"type":"step/start","seq":29,"time":1785916902519,"data":{"turn":1,"step":3}} {"type":"assistant/chunk","seq":30,"time":1785825343607,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","seq":31,"time":1785825343607,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"missing-runner-output","name":"task_output","argumentsDelta":"{\"task_id\":\"bash-1\",\"wait\":true}"}}} @@ -40,7 +40,7 @@ {"type":"step/end","seq":38,"time":1785916902532,"data":{"turn":1,"step":3}} {"type":"agent/inbox/spliced","seq":39,"time":1785916902532,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":40,"time":1785916902542,"data":{"turn":1,"step":4}} -{"type":"user/message","seq":41,"time":1785916902542,"data":{"content":[{"type":"text","text":"background task bash-1 (bash: true) finished [status: killed, killed before exit]. Read its output with task_output."}],"source":{"kind":"plugin","plugin":"tool-tasks"},"role":"user","id":"989e3c2b-5b21-4694-83d5-6cddac55ce0e"},"surfaceOp":"append"} +{"type":"user/message","seq":41,"time":1785916902542,"data":{"content":[{"type":"text","text":"background task bash-1 (bash: true) finished [status: killed, killed before exit]. Read its output with task_output."}],"source":{"kind":"plugin","plugin":"tool-tasks","form":"notice","summary":"bash true [status: killed, killed before exit]"},"role":"user","id":"989e3c2b-5b21-4694-83d5-6cddac55ce0e"},"surfaceOp":"append"} {"type":"assistant/chunk","seq":42,"time":1785825343627,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","seq":43,"time":1785825343628,"data":{"turn":1,"step":4,"chunk":{"type":"text-delta","index":0,"text":"RUNNER_FAILURES_SURFACED"}}} {"type":"assistant/chunk","seq":44,"time":1785916902550,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"RUNNER_FAILURES_SURFACED"}}}} diff --git a/examples/acp-agent/tests/snapshots/multi-turn/session.jsonl b/examples/acp-agent/tests/snapshots/multi-turn/session.jsonl index 6392fbf640..46aac90caa 100644 --- a/examples/acp-agent/tests/snapshots/multi-turn/session.jsonl +++ b/examples/acp-agent/tests/snapshots/multi-turn/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821395167,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352113767,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498786007,"data":{"content":[{"type":"text","text":"Reply with exactly the word: ONE. No tools."}],"source":{"kind":"user"},"role":"user","id":"4d8893f0-f22d-4e43-ac31-f5e7afbda565"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730439011,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"92ebc873-c6cf-4d0f-a30c-7ae0739d1007"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730439011,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"92ebc873-c6cf-4d0f-a30c-7ae0739d1007"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730439011,"data":{"title":"Reply with exactly the word:","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498786009,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730439012,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/packed-chunks/session.jsonl b/examples/acp-agent/tests/snapshots/packed-chunks/session.jsonl index 4d951eebf9..d334ef777e 100644 --- a/examples/acp-agent/tests/snapshots/packed-chunks/session.jsonl +++ b/examples/acp-agent/tests/snapshots/packed-chunks/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821364567,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352165198,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498765364,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo HELLO. Report the tool result you got back verbatim, then stop."}],"source":{"kind":"user"},"role":"user","id":"a207bd9d-9312-46ed-baaf-7a07a6f08ae8"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730418683,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"1c954f81-4e70-4e28-bf11-5f8424f09391"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730418683,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"1c954f81-4e70-4e28-bf11-5f8424f09391"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730418683,"data":{"title":"Use the bash tool to","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498765365,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730418684,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/parallel-tool-calls/session.jsonl b/examples/acp-agent/tests/snapshots/parallel-tool-calls/session.jsonl index 251fcaf938..295fe91ed9 100644 --- a/examples/acp-agent/tests/snapshots/parallel-tool-calls/session.jsonl +++ b/examples/acp-agent/tests/snapshots/parallel-tool-calls/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821366930,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498766502,"data":{"content":[{"type":"text","text":"Use the read tool twice in the same assistant message: read a.txt and b.txt. Then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"e306a97e-4da2-4b50-bec4-90ede1237df4"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730419890,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"02b21476-4349-49c1-a1b8-91d80c27ef0d"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730419890,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"02b21476-4349-49c1-a1b8-91d80c27ef0d"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730419890,"data":{"title":"Use the read tool twice","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498766504,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730419891,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/partial-landlock-child-failure/session.jsonl b/examples/acp-agent/tests/snapshots/partial-landlock-child-failure/session.jsonl index 14ce6b3590..7b73001a01 100644 --- a/examples/acp-agent/tests/snapshots/partial-landlock-child-failure/session.jsonl +++ b/examples/acp-agent/tests/snapshots/partial-landlock-child-failure/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785916901383,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1785916901409,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785916901409,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: false. Then reply with exactly CHILD_EXIT_PRESERVED and stop."}],"source":{"kind":"user"},"role":"user","id":"8a81cb32-8acc-4929-bb63-ec02adea20df"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785916901409,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"b3b13d6d-dcef-47cb-bbb3-26229c44792c"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785916901409,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"b3b13d6d-dcef-47cb-bbb3-26229c44792c"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785916901409,"data":{"title":"Use the bash tool to","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785916901410,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785916901410,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/pty-tools/session.jsonl b/examples/acp-agent/tests/snapshots/pty-tools/session.jsonl index 8e5a8a2601..f9d4ed2242 100644 --- a/examples/acp-agent/tests/snapshots/pty-tools/session.jsonl +++ b/examples/acp-agent/tests/snapshots/pty-tools/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821373074,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498770152,"data":{"content":[{"type":"text","text":"Exercise the six PTY tools in order, including one missing-session signal error, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"96ac9845-3961-4010-8ee5-d9e5aff18b42"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730423409,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"f7ef1bc0-f4ec-4d3e-b198-399ee1cec46f"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730423409,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"f7ef1bc0-f4ec-4d3e-b198-399ee1cec46f"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730423409,"data":{"title":"Exercise the six PTY tools","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498770153,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-pro"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730423410,"data":{"provider":"deepseek-official","model":"deepseek-v4-pro"}} diff --git a/examples/acp-agent/tests/snapshots/repeat-tool-guard/session.jsonl b/examples/acp-agent/tests/snapshots/repeat-tool-guard/session.jsonl index 64bdcbd4fb..800e6f9b01 100644 --- a/examples/acp-agent/tests/snapshots/repeat-tool-guard/session.jsonl +++ b/examples/acp-agent/tests/snapshots/repeat-tool-guard/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821399027,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498789151,"data":{"content":[{"type":"text","text":"Write the todo list 'watch the kettle boil' five times in a row without changing it, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"f92afb51-ac61-47d2-b0fb-ee55cc744838"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730442276,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"f9ec98a9-17c2-418e-9982-b8b3e2f8a17d"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730442276,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"f9ec98a9-17c2-418e-9982-b8b3e2f8a17d"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730442276,"data":{"title":"Write the todo list 'watch","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498789152,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730442277,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} @@ -39,11 +39,11 @@ {"type":"tool/call","seq":37,"time":1785730442328,"data":{"turn":1,"step":3,"callId":"call_3","name":"todo_write","arguments":"{\"todos\": [{\"content\": \"watch the kettle boil\", \"status\": \"in_progress\"}]}"}} {"type":"todo/write","seq":38,"time":1785730442335,"data":{"todos":[{"content":"watch the kettle boil","status":"in_progress"}]}} {"type":"tool/result","seq":39,"time":1785730442335,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_3"},"content":[{"type":"tool-result","toolCallId":"call_3","content":[{"type":"text","text":"Updated todo list: 0 pending, 1 in progress, 0 completed."}],"isError":false}],"role":"user","id":"779c894c-9e9f-4c8e-a073-36d32b421b0f"}},"sourceEventSeqs":[37],"surfaceOp":"append"} -{"type":"agent/inbox/spliced","seq":40,"time":1785730442335,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"You are repeating the exact same tool call with identical arguments. Carefully analyze the previous result before calling again: if the task is not complete, try a different approach or different arguments instead of repeating the call."}],"source":{"kind":"plugin","plugin":"repeat-tool-guard"},"role":"user","id":"1dee8d17-2cdd-4f76-8330-709191cf8cbb"}]}} +{"type":"agent/inbox/spliced","seq":40,"time":1785730442335,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"You are repeating the exact same tool call with identical arguments. Carefully analyze the previous result before calling again: if the task is not complete, try a different approach or different arguments instead of repeating the call."}],"source":{"kind":"plugin","plugin":"repeat-tool-guard","form":"notice","summary":"todo_write × 3"},"role":"user","id":"1dee8d17-2cdd-4f76-8330-709191cf8cbb"}]}} {"type":"step/end","seq":41,"time":1785730442335,"data":{"turn":1,"step":3}} {"type":"agent/inbox/spliced","seq":42,"time":1785730442335,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":43,"time":1785730442344,"data":{"turn":1,"step":4}} -{"type":"user/message","seq":44,"time":1785730442344,"data":{"content":[{"type":"text","text":"You are repeating the exact same tool call with identical arguments. Carefully analyze the previous result before calling again: if the task is not complete, try a different approach or different arguments instead of repeating the call."}],"source":{"kind":"plugin","plugin":"repeat-tool-guard"},"role":"user","id":"1dee8d17-2cdd-4f76-8330-709191cf8cbb"},"surfaceOp":"append"} +{"type":"user/message","seq":44,"time":1785730442344,"data":{"content":[{"type":"text","text":"You are repeating the exact same tool call with identical arguments. Carefully analyze the previous result before calling again: if the task is not complete, try a different approach or different arguments instead of repeating the call."}],"source":{"kind":"plugin","plugin":"repeat-tool-guard","form":"notice","summary":"todo_write × 3"},"role":"user","id":"1dee8d17-2cdd-4f76-8330-709191cf8cbb"},"surfaceOp":"append"} {"type":"assistant/chunk","seq":45,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","seq":46,"time":1785498789219,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"call_4","name":"todo_write","argumentsDelta":"{\"todos\": [{\"content\": \"watch the kettle boil\", \"status\": \"in_progress\"}]}"}}} {"type":"assistant/chunk","seq":47,"time":1785498789219,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_4","name":"todo_write","arguments":"{\"todos\": [{\"content\": \"watch the kettle boil\", \"status\": \"in_progress\"}]}"}}}} @@ -64,11 +64,11 @@ {"type":"tool/call","seq":62,"time":1785730442368,"data":{"turn":1,"step":5,"callId":"call_5","name":"todo_write","arguments":"{\"todos\": [{\"content\": \"watch the kettle boil\", \"status\": \"in_progress\"}]}"}} {"type":"todo/write","seq":63,"time":1785730442376,"data":{"todos":[{"content":"watch the kettle boil","status":"in_progress"}]}} {"type":"tool/result","seq":64,"time":1785730442376,"data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"call_5"},"content":[{"type":"tool-result","toolCallId":"call_5","content":[{"type":"text","text":"Updated todo list: 0 pending, 1 in progress, 0 completed."}],"isError":false}],"role":"user","id":"fa3d2366-ffd8-4f75-833d-e4193c7c9749"}},"sourceEventSeqs":[62],"surfaceOp":"append"} -{"type":"agent/inbox/spliced","seq":65,"time":1785730442376,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Repeated tool call detected:\n- tool: todo_write\n- consecutive_calls: 5\n- arguments: {\"todos\":[{\"content\":\"watch the kettle boil\",\"status\":\"in_progress\"}]}\nThe repeated calls are not making progress. Do not call this tool with these exact arguments again. Inspect the latest result and choose a different action, different arguments, or finish the task if enough evidence has been gathered."}],"source":{"kind":"plugin","plugin":"repeat-tool-guard"},"role":"user","id":"4ca50ec2-4e31-43c2-bc10-f4bdaa678127"}]}} +{"type":"agent/inbox/spliced","seq":65,"time":1785730442376,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Repeated tool call detected:\n- tool: todo_write\n- consecutive_calls: 5\n- arguments: {\"todos\":[{\"content\":\"watch the kettle boil\",\"status\":\"in_progress\"}]}\nThe repeated calls are not making progress. Do not call this tool with these exact arguments again. Inspect the latest result and choose a different action, different arguments, or finish the task if enough evidence has been gathered."}],"source":{"kind":"plugin","plugin":"repeat-tool-guard","form":"notice","summary":"todo_write × 5"},"role":"user","id":"4ca50ec2-4e31-43c2-bc10-f4bdaa678127"}]}} {"type":"step/end","seq":66,"time":1785730442376,"data":{"turn":1,"step":5}} {"type":"agent/inbox/spliced","seq":67,"time":1785730442376,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":68,"time":1785730442384,"data":{"turn":1,"step":6}} -{"type":"user/message","seq":69,"time":1785730442384,"data":{"content":[{"type":"text","text":"Repeated tool call detected:\n- tool: todo_write\n- consecutive_calls: 5\n- arguments: {\"todos\":[{\"content\":\"watch the kettle boil\",\"status\":\"in_progress\"}]}\nThe repeated calls are not making progress. Do not call this tool with these exact arguments again. Inspect the latest result and choose a different action, different arguments, or finish the task if enough evidence has been gathered."}],"source":{"kind":"plugin","plugin":"repeat-tool-guard"},"role":"user","id":"4ca50ec2-4e31-43c2-bc10-f4bdaa678127"},"surfaceOp":"append"} +{"type":"user/message","seq":69,"time":1785730442384,"data":{"content":[{"type":"text","text":"Repeated tool call detected:\n- tool: todo_write\n- consecutive_calls: 5\n- arguments: {\"todos\":[{\"content\":\"watch the kettle boil\",\"status\":\"in_progress\"}]}\nThe repeated calls are not making progress. Do not call this tool with these exact arguments again. Inspect the latest result and choose a different action, different arguments, or finish the task if enough evidence has been gathered."}],"source":{"kind":"plugin","plugin":"repeat-tool-guard","form":"notice","summary":"todo_write × 5"},"role":"user","id":"4ca50ec2-4e31-43c2-bc10-f4bdaa678127"},"surfaceOp":"append"} {"type":"assistant/chunk","seq":70,"time":1785498789257,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","seq":71,"time":1785498789257,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":0,"text":"DONE."}}} {"type":"assistant/chunk","seq":72,"time":1785498789257,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE."}}}} diff --git a/examples/acp-agent/tests/snapshots/session-query-spill/session.jsonl b/examples/acp-agent/tests/snapshots/session-query-spill/session.jsonl index b5b7d42671..9ba17ca2ee 100644 --- a/examples/acp-agent/tests/snapshots/session-query-spill/session.jsonl +++ b/examples/acp-agent/tests/snapshots/session-query-spill/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821371103,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498768995,"data":{"content":[{"type":"text","text":"Read request event 5 with session_event_read, verify the complete spill was retained, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"05ed182c-4c88-4019-912e-518ed6e431ba"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730422266,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"82025f74-4ec2-4ac7-a90b-5eb18f184abb"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730422266,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"82025f74-4ec2-4ac7-a90b-5eb18f184abb"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730422266,"data":{"title":"Read request event 5 with","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498768997,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730422267,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} @@ -15,7 +15,7 @@ {"type":"assistant/chunk","seq":13,"time":1785730422276,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} {"type":"assistant/message","seq":14,"time":1785730422276,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_session_query_spill","name":"session_event_read","arguments":"{\"seq\":5}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"a4ee27a4-32b2-40d1-aeac-6a8bc8fcc2de"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"} {"type":"tool/call","seq":15,"time":1785730422276,"data":{"turn":1,"step":1,"callId":"call_session_query_spill","name":"session_event_read","arguments":"{\"seq\":5}"}} -{"type":"tool/result","seq":16,"time":1785730422286,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_session_query_spill"},"content":[{"type":"tool-result","toolCallId":"call_session_query_spill","content":[{"type":"text","text":"Session {{sessionId}} — Read request event 5 with\nTarget event seq 5:\n```json\n{\n \"type\": \"user/message\",\n \"seq\": 5,\n \"time\": 1785821371147,\n \"data\": {\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"Current runtime context. This snapshot supersedes ically — do not request sandbox escalation (do not set `sandbox_permissions`).\"\n }\n ],\n \"source\": {\n \"kind\": \"plugin\",\n \"plugin\": \"@deepseek-ai/dsh-system-prompt\"\n },\n \"role\": \"user\",\n \"id\": \"82025f74-4ec2-4ac7-a90b-5eb18f184abb\"\n },\n \"surfaceOp\": \"append\"\n}\n```\n\n(Omitted 266 bytes. Full formatted result stored at: /tmp/dsh-acp-snap-035d1d054/session-aa56455bb13a/dfff8c2b8a66-session_event_read.txt. Use read with offset/limit, or grep this path to search within it.)"}],"isError":false}],"role":"user","id":"8f96f03f-4fca-4c3a-ba34-ce891adde50f"}},"sourceEventSeqs":[15],"surfaceOp":"append"} +{"type":"tool/result","seq":16,"time":1785730422286,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_session_query_spill"},"content":[{"type":"tool-result","toolCallId":"call_session_query_spill","content":[{"type":"text","text":"Session {{sessionId}} — Read request event 5 with\nTarget event seq 5:\n```json\n{\n \"type\": \"user/message\",\n \"seq\": 5,\n \"time\": 1785987646184,\n \"data\": {\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"Current runtime context. This snapshot supersedes mpts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\"\n }\n ]\n },\n \"role\": \"user\",\n \"id\": \"985f57e7-e296-4210-af78-78a485f09894\"\n },\n \"surfaceOp\": \"append\"\n}\n```\n\n(Omitted 782 bytes. Full formatted result stored at: /tmp/dsh-acp-snap-035d1d054/session-aa56455bb13a/dfff8c2b8a66-session_event_read.txt. Use read with offset/limit, or grep this path to search within it.)"}],"isError":false}],"role":"user","id":"8f96f03f-4fca-4c3a-ba34-ce891adde50f"}},"sourceEventSeqs":[15],"surfaceOp":"append"} {"type":"step/end","seq":17,"time":1785730422286,"data":{"turn":1,"step":1}} {"type":"step/start","seq":18,"time":1785730422296,"data":{"turn":1,"step":2}} {"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} diff --git a/examples/acp-agent/tests/snapshots/session-sandbox-root/session.jsonl b/examples/acp-agent/tests/snapshots/session-sandbox-root/session.jsonl index 22b34e01e1..322d931e1a 100644 --- a/examples/acp-agent/tests/snapshots/session-sandbox-root/session.jsonl +++ b/examples/acp-agent/tests/snapshots/session-sandbox-root/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821448088,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1784821266397,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498831818,"data":{"content":[{"type":"text","text":"Use the write tool (NOT bash) to create session-root.txt in the current directory containing exactly: session root. Then reply with exactly the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"f7d05c95-98f0-44b5-9463-2449682817ff"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730483789,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"/Users/cty/acp-snap-cwd-MABAjO\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"7855df4a-1a61-4d6c-bb03-84b80edb0075"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730483789,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"/Users/cty/acp-snap-cwd-MABAjO\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"/Users/cty/acp-snap-cwd-MABAjO\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"7855df4a-1a61-4d6c-bb03-84b80edb0075"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730483789,"data":{"title":"Use the write tool (NOT","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498831819,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730483790,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/session-title-after-turn/session.jsonl b/examples/acp-agent/tests/snapshots/session-title-after-turn/session.jsonl index a2654b2fde..0993d85ded 100644 --- a/examples/acp-agent/tests/snapshots/session-title-after-turn/session.jsonl +++ b/examples/acp-agent/tests/snapshots/session-title-after-turn/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821360788,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1785222848199,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498762955,"data":{"content":[{"type":"text","text":"Reply with exactly TITLE_DONE. Do not use tools."}],"source":{"kind":"user"},"role":"user","id":"07495f06-71ba-4146-b27c-de2cf46a60fb"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730416395,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"d2f80db6-391b-4fe4-bfd8-744807253b12"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730416395,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"d2f80db6-391b-4fe4-bfd8-744807253b12"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730416395,"data":{"title":"Reply with exactly TITLE_DONE. Do","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498762958,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730416397,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/skill-load/session.jsonl b/examples/acp-agent/tests/snapshots/skill-load/session.jsonl index 5083f57ef9..f30dc715cf 100644 --- a/examples/acp-agent/tests/snapshots/skill-load/session.jsonl +++ b/examples/acp-agent/tests/snapshots/skill-load/session.jsonl @@ -4,8 +4,8 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821378605,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1785498773754,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498773754,"data":{"content":[{"type":"text","text":"Load the snapshot-skill skill with the skill tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"0ca31b92-27ac-451d-98d3-d1e5f605454b"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785498773755,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"3fc7e2f8-90fc-496c-b516-700cef1d86f1"},"surfaceOp":"append"} -{"type":"user/message","seq":6,"time":1785730426818,"data":{"content":[{"type":"text","text":"\nA skill is a reusable set of task-specific instructions. The following skills are available in this session:\n\n\n- `model-only-skill`: Prove user-disabled skills remain available to the model.\n- `snapshot-skill`: Exercise project skill discovery and loading in snapshot tests.\n\n\nIf the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.\n"}],"source":{"kind":"plugin","plugin":"dsh-tool-skill"},"role":"user","id":"60880315-9799-44c8-8a99-e6fe9ee5bdc5"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785498773755,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"3fc7e2f8-90fc-496c-b516-700cef1d86f1"},"surfaceOp":"append"} +{"type":"user/message","seq":6,"time":1785730426818,"data":{"content":[{"type":"text","text":"\nA skill is a reusable set of task-specific instructions. The following skills are available in this session:\n\n\n- `model-only-skill`: Prove user-disabled skills remain available to the model.\n- `snapshot-skill`: Exercise project skill discovery and loading in snapshot tests.\n\n\nIf the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.\n"}],"source":{"kind":"skill-catalog","form":"catalog","entries":[{"name":"model-only-skill","description":"Prove user-disabled skills remain available to the model."},{"name":"snapshot-skill","description":"Exercise project skill discovery and loading in snapshot tests."}]},"role":"user","id":"60880315-9799-44c8-8a99-e6fe9ee5bdc5"},"surfaceOp":"append"} {"type":"session/title","seq":7,"time":1785730426818,"data":{"title":"Load the snapshot-skill skill with","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":8,"time":1785498773756,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":9,"time":1785730426819,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-continuable/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-continuable/session.1.jsonl index ccaf248ca3..554de6f448 100644 --- a/examples/acp-agent/tests/snapshots/subagent-continuable/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-continuable/session.1.jsonl @@ -4,11 +4,11 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785730451347,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"c67a308f-d867-424e-b198-c9f464228703"}]}} {"type":"turn/start","seq":3,"time":1785821409024,"data":{"turn":1}} {"type":"agent/inbox/spliced","seq":4,"time":1785730917162,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"agent/inbox/spliced","seq":5,"time":1785730917192,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Now reply with exactly SECOND_OK."}],"source":{"kind":"coordinator","senderSessionId":"11111111-1111-4111-8111-111111111111"},"role":"user","id":"e7d15a94-203d-43ab-8279-4e22d5218feb"}]}} -{"type":"agent/inbox/spliced","seq":6,"time":1785821409076,"data":{"target":"next-turn","start":1,"inserted":[{"content":[{"type":"text","text":"Now reply with exactly THIRD_OK."}],"source":{"kind":"coordinator","senderSessionId":"11111111-1111-4111-8111-111111111111"},"role":"user","id":"755c76db-6ee8-432d-a2d0-f8a3b7914e08"}]}} +{"type":"agent/inbox/spliced","seq":5,"time":1785730917192,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Now reply with exactly SECOND_OK."}],"source":{"kind":"coordinator","form":"relay","senderSessionId":"11111111-1111-4111-8111-111111111111"},"role":"user","id":"e7d15a94-203d-43ab-8279-4e22d5218feb"}]}} +{"type":"agent/inbox/spliced","seq":6,"time":1785821409076,"data":{"target":"next-turn","start":1,"inserted":[{"content":[{"type":"text","text":"Now reply with exactly THIRD_OK."}],"source":{"kind":"coordinator","form":"relay","senderSessionId":"11111111-1111-4111-8111-111111111111"},"role":"user","id":"755c76db-6ee8-432d-a2d0-f8a3b7914e08"}]}} {"type":"step/start","seq":7,"time":1785730917198,"data":{"turn":1,"step":1}} {"type":"user/message","seq":8,"time":1785730917198,"data":{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"c67a308f-d867-424e-b198-c9f464228703"},"surfaceOp":"append"} -{"type":"user/message","seq":9,"time":1785730917198,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"49acbc16-4d58-460e-8cc0-62838472dce6"},"surfaceOp":"append"} +{"type":"user/message","seq":9,"time":1785730917198,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"49acbc16-4d58-460e-8cc0-62838472dce6"},"surfaceOp":"append"} {"type":"session/title","seq":10,"time":1785730917198,"data":{"title":"Reply with exactly the word","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"request/header","seq":11,"time":1785730917198,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":12,"time":1785730917199,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} @@ -23,7 +23,7 @@ {"type":"turn/start","seq":21,"time":1785821409092,"data":{"turn":2}} {"type":"agent/inbox/spliced","seq":22,"time":1785821409092,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":23,"time":1785730696682,"data":{"turn":2,"step":1}} -{"type":"user/message","seq":24,"time":1785730696682,"data":{"content":[{"type":"text","text":"Now reply with exactly SECOND_OK."}],"source":{"kind":"coordinator","senderSessionId":"11111111-1111-4111-8111-111111111111"},"role":"user","id":"e7d15a94-203d-43ab-8279-4e22d5218feb"},"surfaceOp":"append"} +{"type":"user/message","seq":24,"time":1785730696682,"data":{"content":[{"type":"text","text":"Now reply with exactly SECOND_OK."}],"source":{"kind":"coordinator","form":"relay","senderSessionId":"11111111-1111-4111-8111-111111111111"},"role":"user","id":"e7d15a94-203d-43ab-8279-4e22d5218feb"},"surfaceOp":"append"} {"type":"assistant/chunk","seq":25,"time":1785730696686,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","seq":26,"time":1785730696686,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"SECOND_OK"}}} {"type":"assistant/chunk","seq":27,"time":1789000000023,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SECOND_OK"}}}} diff --git a/examples/acp-agent/tests/snapshots/subagent-continuable/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-continuable/session.jsonl index 3c8efb1ddd..898d4b250c 100644 --- a/examples/acp-agent/tests/snapshots/subagent-continuable/session.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-continuable/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821408972,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1785730451327,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785730451327,"data":{"content":[{"type":"text","text":"Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. 2. Call send_message twice in a row, both with the subagent id from step 1: first with message 'Now reply with exactly SECOND_OK.', then with message 'Now reply with exactly THIRD_OK.'. 3. Call send_message with subagent_id exactly '22222222-2222-4222-8222-222222222222' (a subagent that does not exist) and message 'Please continue.', and observe that it fails. 4. Reply with the single word DONE. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"125665d3-8c03-4190-b4f9-c27d61d245f4"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730451328,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"e7521889-28d8-4434-84b2-21ff0e044fe7"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730451328,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"e7521889-28d8-4434-84b2-21ff0e044fe7"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730451328,"data":{"title":"Follow these steps exactly, then","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785730451329,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730451329,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.1.jsonl index e1362f5cdb..8b8b8cc0c2 100644 --- a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.1.jsonl @@ -5,7 +5,7 @@ {"type":"subagent/descriptor","seq":3,"time":1785821414185,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Start depth one"}} {"type":"step/start","seq":4,"time":1785730456013,"data":{"turn":1,"step":1}} {"type":"user/message","seq":5,"time":1785730456014,"data":{"content":[{"type":"text","text":"Call subagent once. Ask that child to attempt one further subagent call, then report the result."}],"source":{"kind":"user"},"role":"user","id":"a8129357-1bde-4cbd-90b4-6b8ad51d52e1"},"surfaceOp":"append"} -{"type":"user/message","seq":6,"time":1785730456014,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"3244b13c-f211-445f-acf5-fb8d1534537c"},"surfaceOp":"append"} +{"type":"user/message","seq":6,"time":1785730456014,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"3244b13c-f211-445f-acf5-fb8d1534537c"},"surfaceOp":"append"} {"type":"session/title","seq":7,"time":1785730456014,"data":{"title":"Call subagent once. Ask that","messageSeqs":[5],"source":{"kind":"fallback"}}} {"type":"request/header","seq":8,"time":1785730456014,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":9,"time":1785730456014,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.2.jsonl b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.2.jsonl index 8a957dd636..7f2ae89966 100644 --- a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.2.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.2.jsonl @@ -5,7 +5,7 @@ {"type":"subagent/descriptor","seq":3,"time":1785821414214,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Start depth two"}} {"type":"step/start","seq":4,"time":1785730456041,"data":{"turn":1,"step":1}} {"type":"user/message","seq":5,"time":1785730456041,"data":{"content":[{"type":"text","text":"Attempt one subagent call beyond the configured cap, then report the rejection."}],"source":{"kind":"user"},"role":"user","id":"d4dc5a16-e542-4dd9-8e82-e6b7829cfc4b"},"surfaceOp":"append"} -{"type":"user/message","seq":6,"time":1785730456041,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"4a252f7d-8523-433f-a3fc-33812be802ec"},"surfaceOp":"append"} +{"type":"user/message","seq":6,"time":1785730456041,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"4a252f7d-8523-433f-a3fc-33812be802ec"},"surfaceOp":"append"} {"type":"session/title","seq":7,"time":1785730456041,"data":{"title":"Attempt one subagent call beyond","messageSeqs":[5],"source":{"kind":"fallback"}}} {"type":"request/header","seq":8,"time":1785730456041,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":9,"time":1785730456042,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.jsonl index 37c7b008be..6288b6d516 100644 --- a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821414127,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1784540790308,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498798839,"data":{"content":[{"type":"text","text":"Delegate through two child generations. The depth-two child must attempt one more subagent call and report the rejection."}],"source":{"kind":"user"},"role":"user","id":"b2260a25-4667-49ed-9297-16b233f22332"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730455980,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"d3ba1b18-4d27-4c90-a95d-125e9ffc9f29"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730455980,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"d3ba1b18-4d27-4c90-a95d-125e9ffc9f29"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730455980,"data":{"title":"Delegate through two child generations.","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498798841,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730455981,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-fork/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-fork/session.1.jsonl index b26e6e1227..a56f7ccf60 100644 --- a/examples/acp-agent/tests/snapshots/subagent-fork/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-fork/session.1.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821406454,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352134840,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498796115,"data":{"content":[{"type":"text","text":"Remember this fact for later: the project codeword is MARMALADE. Reply with the single word OK and stop. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"8e65a90a-a69c-44f1-b55f-49fefdabb74c"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730448968,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"40eb2299-67e0-44db-8132-84564259fc8b"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730448968,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"40eb2299-67e0-44db-8132-84564259fc8b"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730448968,"data":{"title":"Remember this fact for later:","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498796118,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730448969,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-fork/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-fork/session.jsonl index 7daf489e18..c7eb17a0cb 100644 --- a/examples/acp-agent/tests/snapshots/subagent-fork/session.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-fork/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821406454,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352134840,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498796115,"data":{"content":[{"type":"text","text":"Remember this fact for later: the project codeword is MARMALADE. Reply with the single word OK and stop. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"8e65a90a-a69c-44f1-b55f-49fefdabb74c"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730448968,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"40eb2299-67e0-44db-8132-84564259fc8b"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730448968,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"40eb2299-67e0-44db-8132-84564259fc8b"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730448968,"data":{"title":"Remember this fact for later:","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498796118,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730448969,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-list-agents/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-list-agents/session.1.jsonl index 9a0ee79f04..d6e54e2096 100644 --- a/examples/acp-agent/tests/snapshots/subagent-list-agents/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-list-agents/session.1.jsonl @@ -6,7 +6,7 @@ {"type":"agent/inbox/spliced","seq":4,"time":1785821412774,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":5,"time":1785730454835,"data":{"turn":1,"step":1}} {"type":"user/message","seq":6,"time":1785730454835,"data":{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"2a46160e-89d3-433b-bf04-66fb0313abfa"},"surfaceOp":"append"} -{"type":"user/message","seq":7,"time":1785730454835,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"524be394-8639-4c12-a41d-799b9e0120a1"},"surfaceOp":"append"} +{"type":"user/message","seq":7,"time":1785730454835,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"524be394-8639-4c12-a41d-799b9e0120a1"},"surfaceOp":"append"} {"type":"session/title","seq":8,"time":1785730454835,"data":{"title":"Reply with exactly the word","messageSeqs":[6],"source":{"kind":"fallback"}}} {"type":"request/header","seq":9,"time":1785730454835,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":10,"time":1785730454835,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-list-agents/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-list-agents/session.jsonl index edf5ca3ea2..1a0113e440 100644 --- a/examples/acp-agent/tests/snapshots/subagent-list-agents/session.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-list-agents/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821412725,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1785730454783,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785730454783,"data":{"content":[{"type":"text","text":"Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. Then reply with the single word STARTED. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"c2febfff-792d-4457-a944-933ff0de0570"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730454783,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"cc8cb20d-5802-46a9-87b8-d3ee784f8e52"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730454783,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"cc8cb20d-5802-46a9-87b8-d3ee784f8e52"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730454783,"data":{"title":"Call the subagent tool once","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785730454784,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730454784,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-mixed/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-mixed/session.1.jsonl index fd2593bc1f..e275607cbf 100644 --- a/examples/acp-agent/tests/snapshots/subagent-mixed/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-mixed/session.1.jsonl @@ -5,7 +5,7 @@ {"type":"subagent/descriptor","seq":3,"time":1785821407767,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Reply ALPHA only"}} {"type":"step/start","seq":4,"time":1785730450187,"data":{"turn":1,"step":1}} {"type":"user/message","seq":5,"time":1785730450187,"data":{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"73ce401a-faaf-408a-879e-7485380d537d"},"surfaceOp":"append"} -{"type":"user/message","seq":6,"time":1785730450187,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"4a5a7c59-b6f8-47b0-8c09-d9a05607deac"},"surfaceOp":"append"} +{"type":"user/message","seq":6,"time":1785730450187,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"4a5a7c59-b6f8-47b0-8c09-d9a05607deac"},"surfaceOp":"append"} {"type":"session/title","seq":7,"time":1785730450187,"data":{"title":"Reply with exactly the word","messageSeqs":[5],"source":{"kind":"fallback"}}} {"type":"request/header","seq":8,"time":1785730450187,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":9,"time":1785730450188,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl b/examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl index d24cb8a038..fb6e5e0971 100644 --- a/examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821407687,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352142835,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498797379,"data":{"content":[{"type":"text","text":"Remember this fact for later: the project codeword is SAFFRON. Reply with the single word OK and stop. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"3d1ea7cb-c273-4c38-a765-5ff256eaaf51"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730450135,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"473ecf9e-52c4-4db2-be56-1c8f7fa7d932"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730450135,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"473ecf9e-52c4-4db2-be56-1c8f7fa7d932"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730450135,"data":{"title":"Remember this fact for later:","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498797380,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730450136,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-mixed/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-mixed/session.jsonl index dc704cc32a..fa390cf176 100644 --- a/examples/acp-agent/tests/snapshots/subagent-mixed/session.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-mixed/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821407687,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352142835,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498797379,"data":{"content":[{"type":"text","text":"Remember this fact for later: the project codeword is SAFFRON. Reply with the single word OK and stop. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"3d1ea7cb-c273-4c38-a765-5ff256eaaf51"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730450135,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"473ecf9e-52c4-4db2-be56-1c8f7fa7d932"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730450135,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"473ecf9e-52c4-4db2-be56-1c8f7fa7d932"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730450135,"data":{"title":"Remember this fact for later:","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498797380,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730450136,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl index d0b798bf1f..7948013736 100644 --- a/examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl @@ -5,7 +5,7 @@ {"type":"subagent/descriptor","seq":3,"time":1785821405245,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Return ALPHA only"}} {"type":"step/start","seq":4,"time":1785730447828,"data":{"turn":1,"step":1}} {"type":"user/message","seq":5,"time":1785730447828,"data":{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"a287f842-f6f2-4a17-ab4c-820e41f498d5"},"surfaceOp":"append"} -{"type":"user/message","seq":6,"time":1785730447828,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"bab5cdff-7925-478d-b55a-daa2ef524d7c"},"surfaceOp":"append"} +{"type":"user/message","seq":6,"time":1785730447828,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"bab5cdff-7925-478d-b55a-daa2ef524d7c"},"surfaceOp":"append"} {"type":"session/title","seq":7,"time":1785730447828,"data":{"title":"Reply with exactly the word","messageSeqs":[5],"source":{"kind":"fallback"}}} {"type":"request/header","seq":8,"time":1785730447828,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":9,"time":1785730447828,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl b/examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl index 51b8e8cca8..64e58b3741 100644 --- a/examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl @@ -5,7 +5,7 @@ {"type":"subagent/descriptor","seq":3,"time":1785821405299,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Return BETA only"}} {"type":"step/start","seq":4,"time":1785730447881,"data":{"turn":1,"step":1}} {"type":"user/message","seq":5,"time":1785730447881,"data":{"content":[{"type":"text","text":"Reply with exactly the word BETA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"53f6419d-8ddc-4eee-8803-5b68411336f9"},"surfaceOp":"append"} -{"type":"user/message","seq":6,"time":1785730447881,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"036067ef-a106-4955-841c-a0d2effe51ef"},"surfaceOp":"append"} +{"type":"user/message","seq":6,"time":1785730447881,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"036067ef-a106-4955-841c-a0d2effe51ef"},"surfaceOp":"append"} {"type":"session/title","seq":7,"time":1785730447881,"data":{"title":"Reply with exactly the word","messageSeqs":[5],"source":{"kind":"fallback"}}} {"type":"request/header","seq":8,"time":1785730447881,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":9,"time":1785730447881,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-multi/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-multi/session.jsonl index 66fedff41c..3a48c2d760 100644 --- a/examples/acp-agent/tests/snapshots/subagent-multi/session.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-multi/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821405184,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352126252,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498794765,"data":{"content":[{"type":"text","text":"Use the subagent tool TWICE, once at a time, to delegate two subtasks to child agents. First subtask: 'Reply with exactly the word ALPHA and nothing else.' Second subtask (after the first returns): 'Reply with exactly the word BETA and nothing else.' After both subagents return, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"07bf16df-0499-420d-9510-3204061f0122"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730447790,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"9b4b262d-cbd7-4cd8-b24b-70b2b401b0fe"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730447790,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"9b4b262d-cbd7-4cd8-b24b-70b2b401b0fe"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730447790,"data":{"title":"Use the subagent tool TWICE,","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498794766,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730447791,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-published-run-failure/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-published-run-failure/session.jsonl index ef64d948af..4578a14b09 100644 --- a/examples/acp-agent/tests/snapshots/subagent-published-run-failure/session.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-published-run-failure/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821410244,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1785730452505,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785730452505,"data":{"content":[{"type":"text","text":"Delegate one foreground subagent. Its published run will fail; report that failure as PARENT_OBSERVED_ERROR."}],"source":{"kind":"user"},"role":"user","id":"07e6bcfc-3d70-46ef-8bdd-17a45c2c346e"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730452505,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"902b2d5b-6b6a-471a-b765-5a5ca5d0ff53"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730452505,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"902b2d5b-6b6a-471a-b765-5a5ca5d0ff53"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730452505,"data":{"title":"Delegate one foreground subagent. Its","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785730452506,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730452506,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-report/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-report/session.1.jsonl index 27eb9b020f..b9992f1519 100644 --- a/examples/acp-agent/tests/snapshots/subagent-report/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-report/session.1.jsonl @@ -6,7 +6,7 @@ {"type":"agent/inbox/spliced","seq":4,"time":1785821411475,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":5,"time":1785730453639,"data":{"turn":1,"step":1}} {"type":"user/message","seq":6,"time":1785730453639,"data":{"content":[{"type":"text","text":"Call the report tool once with output exactly CHILD_REPORT_OK, then stop."}],"source":{"kind":"user"},"role":"user","id":"9045ac78-393a-4f24-b20d-8999286dd6ce"},"surfaceOp":"append"} -{"type":"user/message","seq":7,"time":1785730453639,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"67c76a21-6142-45a6-9a49-0485f51edc8d"},"surfaceOp":"append"} +{"type":"user/message","seq":7,"time":1785730453639,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"67c76a21-6142-45a6-9a49-0485f51edc8d"},"surfaceOp":"append"} {"type":"session/title","seq":8,"time":1785730453639,"data":{"title":"Call the report tool once","messageSeqs":[6],"source":{"kind":"fallback"}}} {"type":"request/header","seq":9,"time":1785730453639,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":10,"time":1785730453639,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-report/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-report/session.jsonl index 17f822c19e..db5cd4a77f 100644 --- a/examples/acp-agent/tests/snapshots/subagent-report/session.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-report/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821411429,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1785730453591,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785730453591,"data":{"content":[{"type":"text","text":"Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Report a finding', and prompt 'Call the report tool once with output exactly CHILD_REPORT_OK, then stop.'. 2. Reply with the single word STARTED. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"5cf78378-e004-4fd5-af4f-cef3b7e190ad"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730453592,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"77141070-eb99-4ec0-908d-646c387982f6"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730453592,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"77141070-eb99-4ec0-908d-646c387982f6"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730453592,"data":{"title":"Follow these steps exactly, then","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785730453592,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730453593,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} @@ -26,13 +26,13 @@ {"type":"assistant/message","seq":24,"time":1785730453628,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"571faad7-adbd-480c-922a-1499e1329ead"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"} {"type":"step/end","seq":25,"time":1785730453628,"data":{"turn":1,"step":2}} {"type":"turn/end","seq":26,"time":1785730453629,"data":{"turn":1,"reason":{"kind":"completed"}}} -{"type":"agent/inbox/spliced","seq":27,"time":1785730453654,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 reported:"},{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"subagent-report","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"824dc60a-f9d7-48ea-a0d4-6d56df83bd4f"}]}} +{"type":"agent/inbox/spliced","seq":27,"time":1785730453654,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 reported:"},{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"subagent-report","form":"relay","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"824dc60a-f9d7-48ea-a0d4-6d56df83bd4f"}]}} {"type":"agent/inbox/spliced","seq":28,"time":1785730453673,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Repeat back, verbatim, the exact output the background subagent reported to you. Reply with only that text. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"43f17984-22c3-48b9-911e-923a2f68dce0"}]}} {"type":"turn/start","seq":29,"time":1785821411548,"data":{"turn":2}} {"type":"agent/inbox/spliced","seq":30,"time":1785730453673,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}} {"type":"agent/inbox/spliced","seq":31,"time":1785821411548,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":32,"time":1785730453683,"data":{"turn":2,"step":1}} -{"type":"user/message","seq":33,"time":1785730453683,"data":{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 reported:"},{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"subagent-report","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"824dc60a-f9d7-48ea-a0d4-6d56df83bd4f"},"surfaceOp":"append"} +{"type":"user/message","seq":33,"time":1785730453683,"data":{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 reported:"},{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"subagent-report","form":"relay","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"824dc60a-f9d7-48ea-a0d4-6d56df83bd4f"},"surfaceOp":"append"} {"type":"user/message","seq":34,"time":1785730453683,"data":{"content":[{"type":"text","text":"Repeat back, verbatim, the exact output the background subagent reported to you. Reply with only that text. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"43f17984-22c3-48b9-911e-923a2f68dce0"},"surfaceOp":"append"} {"type":"assistant/chunk","seq":35,"time":1785730453687,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","seq":36,"time":1785730453687,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"CHILD_REPORT_OK"}}} diff --git a/examples/acp-agent/tests/snapshots/subagent-spawn/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-spawn/session.1.jsonl index aa6df16217..7fa229c2e3 100644 --- a/examples/acp-agent/tests/snapshots/subagent-spawn/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-spawn/session.1.jsonl @@ -5,7 +5,7 @@ {"type":"subagent/descriptor","seq":3,"time":1785821404020,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Reply with CHILD_OK"}} {"type":"step/start","seq":4,"time":1785730446720,"data":{"turn":1,"step":1}} {"type":"user/message","seq":5,"time":1785730446720,"data":{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"54ed23d6-e960-4f36-b192-cf06e1618ea6"},"surfaceOp":"append"} -{"type":"user/message","seq":6,"time":1785730446720,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"1b537017-6493-4f52-8504-01a7384e8cc6"},"surfaceOp":"append"} +{"type":"user/message","seq":6,"time":1785730446720,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"1b537017-6493-4f52-8504-01a7384e8cc6"},"surfaceOp":"append"} {"type":"session/title","seq":7,"time":1785730446720,"data":{"title":"Reply with exactly the word","messageSeqs":[5],"source":{"kind":"fallback"}}} {"type":"request/header","seq":8,"time":1785730446720,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":9,"time":1785730446721,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-spawn/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-spawn/session.jsonl index 9ae84809f1..edf8950dac 100644 --- a/examples/acp-agent/tests/snapshots/subagent-spawn/session.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-spawn/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821403947,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352119275,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498793625,"data":{"content":[{"type":"text","text":"Use the subagent tool exactly once to delegate this subtask to a child agent: 'Reply with exactly the word CHILD_OK and nothing else.' After the subagent returns, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"a9485ebd-2b4a-434a-bc35-afd757ce141b"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730446685,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"e40b1354-1856-48c3-a638-1be67af32920"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730446685,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"e40b1354-1856-48c3-a638-1be67af32920"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730446685,"data":{"title":"Use the subagent tool exactly","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498793626,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730446686,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/text-turn/session.jsonl b/examples/acp-agent/tests/snapshots/text-turn/session.jsonl index 5da6e46877..5016b38b30 100644 --- a/examples/acp-agent/tests/snapshots/text-turn/session.jsonl +++ b/examples/acp-agent/tests/snapshots/text-turn/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821359466,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783600629542,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498761313,"data":{"content":[{"type":"text","text":"Reply with exactly the word: PONG. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"3e25dc34-48e0-4738-8401-1a8d181d37e5"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730415287,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"4b8d9730-0b7b-4e14-8a30-3d852f808f0e"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730415287,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"4b8d9730-0b7b-4e14-8a30-3d852f808f0e"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730415287,"data":{"title":"Reply with exactly the word:","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498761318,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730415288,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/todo-write/session.jsonl b/examples/acp-agent/tests/snapshots/todo-write/session.jsonl index 32a6ab449c..46461efaa1 100644 --- a/examples/acp-agent/tests/snapshots/todo-write/session.jsonl +++ b/examples/acp-agent/tests/snapshots/todo-write/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821376741,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352057657,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498772510,"data":{"content":[{"type":"text","text":"Use the todo_write tool to record a plan with exactly three todos: \"read the code\" (in_progress), \"write the fix\" (pending), \"run the tests\" (pending). Send all three in one todo_write call. Then reply with the single word DONE and stop."}],"source":{"kind":"user"},"role":"user","id":"5ecf5e4b-6a18-447d-9341-48f38afdd12e"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730425725,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"8d4ac045-8016-4cec-8b12-91d9459231e1"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730425725,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"8d4ac045-8016-4cec-8b12-91d9459231e1"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730425725,"data":{"title":"Use the todo_write tool to","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498772511,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730425726,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/tool-call-turn/session.jsonl b/examples/acp-agent/tests/snapshots/tool-call-turn/session.jsonl index 15734149cf..c5f2f28f4d 100644 --- a/examples/acp-agent/tests/snapshots/tool-call-turn/session.jsonl +++ b/examples/acp-agent/tests/snapshots/tool-call-turn/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821362944,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352044773,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498764188,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo SNAPSHOT_OK. Then reply with the single word DONE and stop."}],"source":{"kind":"user"},"role":"user","id":"fe479aa0-1194-40fb-897b-bc7f99b54148"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730417556,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"11ca1551-2073-4990-bf8c-828c614d47a8"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730417556,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"11ca1551-2073-4990-bf8c-828c614d47a8"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730417556,"data":{"title":"Use the bash tool to","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498764190,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730417557,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/web-fetch/session.jsonl b/examples/acp-agent/tests/snapshots/web-fetch/session.jsonl index b15f1a8e00..87c02446d6 100644 --- a/examples/acp-agent/tests/snapshots/web-fetch/session.jsonl +++ b/examples/acp-agent/tests/snapshots/web-fetch/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821381783,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1785078727730,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498776258,"data":{"content":[{"type":"text","text":"Use the web_fetch tool exactly once to fetch http://127.0.0.1:43117/menu.html, then reply with exactly DONE. Do not describe the content."}],"source":{"kind":"user"},"role":"user","id":"7a222307-4336-4772-8a19-aa1b56558e31"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730429237,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"86a43ffd-fecc-482d-806b-54c13a88c9e5"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730429237,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"86a43ffd-fecc-482d-806b-54c13a88c9e5"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730429237,"data":{"title":"Use the web_fetch tool exactly","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498776259,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-pro"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730429239,"data":{"provider":"deepseek-official","model":"deepseek-v4-pro"}} diff --git a/examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl b/examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl index c6fb4f7983..7b77a09f5e 100644 --- a/examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl @@ -5,7 +5,7 @@ {"type":"subagent/descriptor","seq":3,"time":1785821416542,"data":{"version":2,"mode":"one-shot","provider":"spawn"}} {"type":"step/start","seq":4,"time":1785730457309,"data":{"turn":1,"step":1}} {"type":"user/message","seq":5,"time":1785730457309,"data":{"content":[{"type":"text","text":"Reply with exactly the word WF_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"f0f46771-663a-494a-8d40-6866a5bbe7c9"},"surfaceOp":"append"} -{"type":"user/message","seq":6,"time":1785730457309,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"e076edc0-a2bf-4fc6-aa58-d44bf1e8fd00"},"surfaceOp":"append"} +{"type":"user/message","seq":6,"time":1785730457309,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"e076edc0-a2bf-4fc6-aa58-d44bf1e8fd00"},"surfaceOp":"append"} {"type":"session/title","seq":7,"time":1785730457309,"data":{"title":"Reply with exactly the word","messageSeqs":[5],"source":{"kind":"fallback"}}} {"type":"request/header","seq":8,"time":1785730457310,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":9,"time":1785730457310,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/workflow-run/session.jsonl b/examples/acp-agent/tests/snapshots/workflow-run/session.jsonl index fc488e435f..6ee104dd0c 100644 --- a/examples/acp-agent/tests/snapshots/workflow-run/session.jsonl +++ b/examples/acp-agent/tests/snapshots/workflow-run/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821416248,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783600631839,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498800152,"data":{"content":[{"type":"text","text":"Use the workflow tool exactly once, with args omitted, meta set to { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }, and this EXACT script body (copy it verbatim):\nphase('Run')\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\nreturn { reply }\nAfter the workflow returns, reply with the single word WORKFLOW_DONE and stop. Do not use any other tool."}],"source":{"kind":"user"},"role":"user","id":"5188a9c7-d3ca-4679-b8df-1443e0a0a4df"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730457160,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"1c92c213-1d4f-45ad-be50-161f26a23e65"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730457160,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"1c92c213-1d4f-45ad-be50-161f26a23e65"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730457160,"data":{"title":"Use the workflow tool exactly","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498800153,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730457161,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/acp-agent/tests/snapshots/workspace-context/session.jsonl b/examples/acp-agent/tests/snapshots/workspace-context/session.jsonl index 9fffa3e640..d51a6c820a 100644 --- a/examples/acp-agent/tests/snapshots/workspace-context/session.jsonl +++ b/examples/acp-agent/tests/snapshots/workspace-context/session.jsonl @@ -4,8 +4,8 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785498790356,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1785901433981,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498790356,"data":{"content":[{"type":"text","text":"Read nested/task.txt, then read scope/task.txt with the read tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"22938d3b-c065-46c8-acb7-18f758285842"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785901433982,"data":{"content":[{"type":"text","text":"\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nRoot snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","baseline":true,"changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2e18766c26603608f321508caae00ea8f4434d59"}]},"role":"user","id":"ec039e95-6864-49ef-ad23-4f65b331dc29"},"surfaceOp":"append"} -{"type":"user/message","seq":6,"time":1785730689193,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"cbea9bd9-3e08-48bf-951f-fe3e5aa4b5d9"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785901433982,"data":{"content":[{"type":"text","text":"\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nRoot snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","form":"instructions","baseline":true,"changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2e18766c26603608f321508caae00ea8f4434d59"}]},"role":"user","id":"ec039e95-6864-49ef-ad23-4f65b331dc29"},"surfaceOp":"append"} +{"type":"user/message","seq":6,"time":1785730689193,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"cbea9bd9-3e08-48bf-951f-fe3e5aa4b5d9"},"surfaceOp":"append"} {"type":"session/title","seq":7,"time":1785730689193,"data":{"title":"Read nested/task.txt, then read scope{{cwd}}/nested/task.txt\nfile\n\n1: snapshot task\n\n(End of file - total 1 lines)\n"}],"isError":false}],"role":"user","id":"260bbd5d-4496-40cf-b987-d1d944d93cf1"},"meta":{"path":"{{cwd}}/nested/task.txt","offset":1,"lines":[{"number":1,"text":"snapshot task"}],"totalLines":1}},"sourceEventSeqs":[16],"surfaceOp":"append"} {"type":"step/end","seq":18,"time":1785498790369,"data":{"turn":1,"step":1}} -{"type":"agent/inbox/spliced","seq":19,"time":1785498790369,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nNested snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","changes":[{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"c446df9a85c7e73a3055f394a4822a19ac9ead5a"}]},"role":"user","id":"10cdaa4b-9654-420e-afca-3cfb07e26754"}]}} +{"type":"agent/inbox/spliced","seq":19,"time":1785498790369,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nNested snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","form":"instructions","changes":[{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"c446df9a85c7e73a3055f394a4822a19ac9ead5a"}]},"role":"user","id":"10cdaa4b-9654-420e-afca-3cfb07e26754"}]}} {"type":"agent/inbox/spliced","seq":20,"time":1785730689207,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[],"outcome":"canceled"}} {"type":"step/start","seq":21,"time":1785730689212,"data":{"turn":1,"step":2}} -{"type":"user/message","seq":22,"time":1785498790377,"data":{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nNested snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","changes":[{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"c446df9a85c7e73a3055f394a4822a19ac9ead5a"}]},"role":"user","id":"10cdaa4b-9654-420e-afca-3cfb07e26754"},"surfaceOp":"append"} +{"type":"user/message","seq":22,"time":1785498790377,"data":{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nNested snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","form":"instructions","changes":[{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"c446df9a85c7e73a3055f394a4822a19ac9ead5a"}]},"role":"user","id":"10cdaa4b-9654-420e-afca-3cfb07e26754"},"surfaceOp":"append"} {"type":"assistant/chunk","seq":23,"time":1784903339821,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","seq":24,"time":1785498790377,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"call_workspace_delimiter_read","name":"read","argumentsDelta":"{\"file_path\":\"scope/task.txt\"}"}}} {"type":"assistant/chunk","seq":25,"time":1785498790377,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_workspace_delimiter_read","name":"read","arguments":"{\"file_path\":\"scope/task.txt\"}"}}}} @@ -31,10 +31,10 @@ {"type":"tool/call","seq":29,"time":1785498790378,"data":{"turn":1,"step":2,"callId":"call_workspace_delimiter_read","name":"read","arguments":"{\"file_path\":\"scope/task.txt\"}"}} {"type":"tool/result","seq":30,"time":1785498790388,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_workspace_delimiter_read"},"content":[{"type":"tool-result","toolCallId":"call_workspace_delimiter_read","content":[{"type":"text","text":"{{cwd}}/scope/task.txt\nfile\n\n1: delimiter path snapshot task\n\n(End of file - total 1 lines)\n"}],"isError":false}],"role":"user","id":"3a62b23b-d165-4c6d-a028-e171c4b2d7fc"},"meta":{"path":"{{cwd}}/scope/task.txt","offset":1,"lines":[{"number":1,"text":"delimiter path snapshot task"}],"totalLines":1}},"sourceEventSeqs":[29],"surfaceOp":"append"} {"type":"step/end","seq":31,"time":1785730689220,"data":{"turn":1,"step":2}} -{"type":"agent/inbox/spliced","seq":32,"time":1785730689220,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"\nAdditional instructions from: scope<\\/system-reminder>/AGENTS.md\n\nThese instructions apply to work under `scope<\\/system-reminder>`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nDelimiter path snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","changes":[{"action":"set","scope":"scope\u0000AGENTS.md","path":"scope/AGENTS.md","digest":"38803cd13e2dff9105ba5fbbc703fe27e989e26e"}]},"role":"user","id":"01c3af61-4567-4be2-b776-21f04ddc9cba"}]}} +{"type":"agent/inbox/spliced","seq":32,"time":1785730689220,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"\nAdditional instructions from: scope<\\/system-reminder>/AGENTS.md\n\nThese instructions apply to work under `scope<\\/system-reminder>`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nDelimiter path snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","form":"instructions","changes":[{"action":"set","scope":"scope\u0000AGENTS.md","path":"scope/AGENTS.md","digest":"38803cd13e2dff9105ba5fbbc703fe27e989e26e"}]},"role":"user","id":"01c3af61-4567-4be2-b776-21f04ddc9cba"}]}} {"type":"agent/inbox/spliced","seq":33,"time":1785498790389,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[],"outcome":"canceled"}} {"type":"step/start","seq":34,"time":1785498790396,"data":{"turn":1,"step":3}} -{"type":"user/message","seq":35,"time":1785498790396,"data":{"content":[{"type":"text","text":"\nAdditional instructions from: scope<\\/system-reminder>/AGENTS.md\n\nThese instructions apply to work under `scope<\\/system-reminder>`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nDelimiter path snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","changes":[{"action":"set","scope":"scope\u0000AGENTS.md","path":"scope/AGENTS.md","digest":"38803cd13e2dff9105ba5fbbc703fe27e989e26e"}]},"role":"user","id":"01c3af61-4567-4be2-b776-21f04ddc9cba"},"surfaceOp":"append"} +{"type":"user/message","seq":35,"time":1785498790396,"data":{"content":[{"type":"text","text":"\nAdditional instructions from: scope<\\/system-reminder>/AGENTS.md\n\nThese instructions apply to work under `scope<\\/system-reminder>`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nDelimiter path snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","form":"instructions","changes":[{"action":"set","scope":"scope\u0000AGENTS.md","path":"scope/AGENTS.md","digest":"38803cd13e2dff9105ba5fbbc703fe27e989e26e"}]},"role":"user","id":"01c3af61-4567-4be2-b776-21f04ddc9cba"},"surfaceOp":"append"} {"type":"assistant/chunk","seq":36,"time":1785498790396,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","seq":37,"time":1785498790396,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":0,"text":"DONE"}}} {"type":"assistant/chunk","seq":38,"time":1785498790396,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}} diff --git a/examples/acp-agent/tests/snapshots/workspace-edit/session.jsonl b/examples/acp-agent/tests/snapshots/workspace-edit/session.jsonl index 3c60aa7c41..09f05a0659 100644 --- a/examples/acp-agent/tests/snapshots/workspace-edit/session.jsonl +++ b/examples/acp-agent/tests/snapshots/workspace-edit/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821383408,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783352264082,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498777358,"data":{"content":[{"type":"text","text":"A file named greeting.txt in the current directory contains one word. Use the bash tool to append a second line containing the word WORLD to it (so it has two lines), then read the file back with `cat greeting.txt` to confirm, and reply with the single word DONE. Use a single bash call per action."}],"source":{"kind":"user"},"role":"user","id":"96726dec-a718-4009-ba60-c2b856fe2e6f"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730430363,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"ff8d8fb0-6bd9-4484-9406-0548c71cca4f"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730430363,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"ff8d8fb0-6bd9-4484-9406-0548c71cca4f"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730430363,"data":{"title":"A file named greeting.txt in","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498777360,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730430364,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/headless-agent/tests/keyless-smoke.e2e.ts b/examples/headless-agent/tests/keyless-smoke.e2e.ts index 193558bcc2..2547d04ac1 100644 --- a/examples/headless-agent/tests/keyless-smoke.e2e.ts +++ b/examples/headless-agent/tests/keyless-smoke.e2e.ts @@ -39,8 +39,7 @@ describe('headless-agent keyless smoke', () => { expect(stderr).toBe('') expect(events.some(event => event.type === 'tool/call' && event.data.name === 'bash')).toBe(true) const catalogMessage = events.find(event => event.type === 'user/message' - && event.data.source.kind === 'plugin' - && event.data.source.plugin === 'dsh-tool-skill') + && event.data.source.kind === 'skill-catalog') const catalog = catalogMessage?.type === 'user/message' ? catalogMessage.data.content.filter(block => block.type === 'text').map(block => block.text).join('\n') : '' diff --git a/examples/headless-agent/tests/snapshots/pty-tools/session.jsonl b/examples/headless-agent/tests/snapshots/pty-tools/session.jsonl index 67dbe5883b..ad7c100179 100644 --- a/examples/headless-agent/tests/snapshots/pty-tools/session.jsonl +++ b/examples/headless-agent/tests/snapshots/pty-tools/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821457966,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498587436,"data":{"content":[{"type":"text","text":"Exercise the six PTY tools in order, including one missing-session signal error, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"39f39ecc-5772-4814-8feb-46433c71becd"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730504659,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"a5ae9c04-0652-436f-9b5a-437a3a6ed235"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730504659,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."}]},"role":"user","id":"a5ae9c04-0652-436f-9b5a-437a3a6ed235"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730504659,"data":{"title":"Exercise the six PTY tools","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498587438,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"You are an AI agent powered by the DeepSeek Harness SDK.\n\nYou are headless-agent, a coding assistant powered by the deepseek-v4-flash model.\n\nVerify your work by running the code or tests. Keep answers brief and\nfactual.\n\n\nUse the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.\n\nUse the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes.\n\nUse the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-policy requires it), unless you just created or edited it in this session.\n\nCheck the [exit code: N] marker on every bash result; investigate failures before moving on.\n\nUse a terminal session only when work needs persistent terminal state or interactive stdin; prefer bash/read/write/edit for bounded one-shot operations. Track every terminal session id and close sessions that no longer matter. An inferred_idle or timeout result does not prove the foreground command exited.\n\nTrack every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.\n\nUse the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.\n\nUse the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.","tools":[{"name":"bash","description":"Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`.","parameters":{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute."},"description":{"type":"string","description":"Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."},"timeoutMs":{"type":"number","description":"Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."},"workdir":{"type":"string","description":"Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."},"run_in_background":{"type":"boolean","description":"Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies."}},"required":["command","description"]}},{"name":"edit","description":"Edit an existing UTF-8 text file by replacing literal text.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to edit, resolved by the filesystem backend."},"old_string":{"type":"string","description":"Literal text to replace. Must match exactly."},"new_string":{"type":"string","description":"Literal replacement text. Use an empty string to delete the match."},"replace_all":{"type":"boolean","description":"Replace all matches. Defaults to false; when false, old_string must appear exactly once."}},"required":["file_path","old_string","new_string"]}},{"name":"ralph","description":"Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.","parameters":{"type":"object","properties":{"objective":{"type":"string","description":"The immutable completion objective for every fresh Ralph round."},"maxRounds":{"type":"number","description":"Optional positive safe-integer round cap, bounded by the deployment ceiling."}},"required":["objective"]}},{"name":"read","description":"Read a UTF-8 text file and return line-numbered content.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to read, resolved by the filesystem backend."},"offset":{"type":"number","description":"1-based first line to return. Defaults to 1."},"limit":{"type":"number","description":"Maximum number of lines to return. Defaults to 2000."}},"required":["file_path"]}},{"name":"send_message","description":"Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.","parameters":{"type":"object","properties":{"subagent_id":{"type":"string","description":"The subagent id returned when the background subagent was started."},"message":{"type":"string","description":"The message to deliver to the subagent."}},"required":["subagent_id","message"]}},{"name":"skill","description":"Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.","parameters":{"type":"object","properties":{"name":{"type":"string","description":"The exact skill name from the available skills list."}},"required":["name"]}},{"name":"subagent","description":"Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."},"run_in_background":{"type":"boolean","description":"Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."}},"required":["description","prompt"]}},{"name":"subagent_fork","description":"Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."},"run_in_background":{"type":"boolean","description":"Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."}},"required":["description","prompt"]}},{"name":"task_kill","description":"Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"reason":{"type":"string","description":"Optional short reason, recorded in the log and forwarded to the task."}},"required":["task_id"]}},{"name":"task_list","description":"List your background tasks (running and finished) with their ids, kinds, and statuses.","parameters":{"type":"object","properties":{}}},{"name":"task_output","description":"Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"wait":{"type":"boolean","description":"Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive."},"timeout_ms":{"type":"number","description":"Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."}},"required":["task_id"]}},{"name":"terminal_close","description":"Close one persistent terminal and wait until its captured owned process tree is gone.","parameters":{"type":"object","properties":{"sessionId":{"type":"string","description":"Terminal session id."}},"required":["sessionId"]}},{"name":"terminal_list","description":"List persistent terminal sessions owned by the current agent.","parameters":{"type":"object","properties":{}}},{"name":"terminal_open","description":"Create a persistent, owner-isolated terminal session from a registered backend type. Use this for shell or REPL state that must survive across tool calls.","parameters":{"type":"object","properties":{"type":{"type":"string","description":"Registered terminal backend type, usually \"shell\"."},"name":{"type":"string","description":"Optional owner-local display name such as \"main\" or \"gdb\"."},"cwd":{"type":"string","description":"Initial working directory. Defaults to the deployment workspace root."}},"required":["type"]}},{"name":"terminal_read","description":"Read a bounded page of retained output from a persistent terminal without sending input.","parameters":{"type":"object","properties":{"sessionId":{"type":"string","description":"Terminal session id."},"offset":{"type":"number","description":"Newest-relative line offset (default 0)."},"count":{"type":"number","description":"Requested line count (default 500; backend caps apply)."}},"required":["sessionId"]}},{"name":"terminal_send","description":"Send text to a persistent terminal. By default Enter is submitted and the call waits for a prompt, stdin wait, output silence, timeout, or session exit. Background mode returns a task id for task_output/task_kill.","parameters":{"type":"object","properties":{"sessionId":{"type":"string","description":"Terminal session id returned by terminal_open or terminal_list."},"text":{"type":"string","description":"UTF-8 text to write to the terminal."},"submit":{"type":"boolean","description":"Submit Enter after text (default true). Set false for control characters or incomplete REPL input."},"run_in_background":{"type":"boolean","description":"Return a task id immediately; collect with task_output or stop with task_kill."}},"required":["sessionId","text"]}},{"name":"terminal_signal","description":"Send an allowed signal to the current foreground process group of a persistent terminal.","parameters":{"type":"object","properties":{"sessionId":{"type":"string","description":"Terminal session id."},"signal":{"type":"string","description":"Signal to deliver. Shell-targeted SIGKILL is rejected; use terminal_close.","enum":["SIGINT","SIGTERM","SIGKILL","SIGTSTP","SIGHUP"]}},"required":["sessionId","signal"]}},{"name":"todo_write","description":"Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Keep AT MOST ONE todo `in_progress` at a time; while work remains, exactly one active task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).","parameters":{"type":"object","properties":{"todos":{"type":"array","description":"The COMPLETE task list, replacing any previous list.","items":{"type":"object","additionalProperties":false,"properties":{"content":{"type":"string","description":"What the task is — a short imperative line."},"status":{"type":"string","description":"pending (not started) | in_progress (now) | completed (done).","enum":["pending","in_progress","completed"]}},"required":["content","status"]}}},"required":["todos"]}},{"name":"workflow","description":"Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.","parameters":{"type":"object","properties":{"script":{"type":"string","description":"The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."},"meta":{"type":"object","description":"The workflow identity block (plain JSON — never code).","additionalProperties":true,"properties":{"name":{"type":"string","description":"Short kebab-case workflow name."},"description":{"type":"string","description":"One-line description of what the workflow does."},"whenToUse":{"type":"string","description":"Optional guidance on when this workflow applies."},"phases":{"type":"array","description":"Optional phase declarations matched by phase() calls.","items":{"type":"object","additionalProperties":true,"properties":{"title":{"type":"string","description":"The phase title phase() calls match by exact string."},"detail":{"type":"string","description":"Optional one-line description of the phase."},"provider":{"type":"string","description":"Optional provider override this phase is expected to use."},"model":{"type":"string","description":"Optional model override this phase is expected to use."}},"required":["title"]}}},"required":["name","description"]},"args":{"type":"object","description":"Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).","additionalProperties":true}},"required":["script","meta"]}},{"name":"write","description":"Create or fully replace a UTF-8 text file.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to write, resolved by the filesystem backend."},"content":{"type":"string","description":"Full UTF-8 text content to write."}},"required":["file_path","content"]}}]},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730504660,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/headless-agent/tests/snapshots/pty-tools/stream-json.expected.jsonl b/examples/headless-agent/tests/snapshots/pty-tools/stream-json.expected.jsonl index 5af477ac6b..2ef42323fb 100644 --- a/examples/headless-agent/tests/snapshots/pty-tools/stream-json.expected.jsonl +++ b/examples/headless-agent/tests/snapshots/pty-tools/stream-json.expected.jsonl @@ -3,7 +3,7 @@ {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":2,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":4,"time":0,"data":{"content":[{"type":"text","text":"Exercise the six PTY tools in order, including one missing-session signal error, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":5,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":5,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":6,"time":0,"data":{"title":"Exercise the six PTY tools","messageSeqs":[4],"source":{"kind":"fallback"}}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":7,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":8,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}} diff --git a/examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/child.expected.jsonl b/examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/child.expected.jsonl index d1d98988ce..dbcd59cc5a 100644 --- a/examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/child.expected.jsonl +++ b/examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/child.expected.jsonl @@ -6,7 +6,7 @@ {"type":"subagent/descriptor","seq":4,"time":0,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Delegated write probe"}} {"type":"step/start","seq":5,"time":0,"data":{"turn":1,"step":1}} {"type":"user/message","seq":6,"time":0,"data":{"content":[{"type":"text","text":"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","seq":7,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","seq":7,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"session/title","seq":8,"time":0,"data":{"title":"Use the write tool exactly","messageSeqs":[6],"source":{"kind":"fallback"}}} {"type":"request/header","seq":9,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":10,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/parent.expected.jsonl b/examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/parent.expected.jsonl index 748e69dc28..0bdcc94938 100644 --- a/examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/parent.expected.jsonl +++ b/examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/parent.expected.jsonl @@ -9,7 +9,7 @@ {"type":"agent/inbox/spliced","seq":7,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":8,"time":0,"data":{"turn":2,"step":1}} {"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Delegate the write probe to a subagent."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","seq":10,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","seq":10,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"session/title","seq":11,"time":0,"data":{"title":"Tighten this session to read-only.","messageSeqs":[1],"source":{"kind":"fallback"}}} {"type":"request/header","seq":12,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":13,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/examples/jsonrpc-agent/tests/snapshots/persistent-tools/notifications.expected.jsonl b/examples/jsonrpc-agent/tests/snapshots/persistent-tools/notifications.expected.jsonl index cbf8b34e75..ea8f220308 100644 --- a/examples/jsonrpc-agent/tests/snapshots/persistent-tools/notifications.expected.jsonl +++ b/examples/jsonrpc-agent/tests/snapshots/persistent-tools/notifications.expected.jsonl @@ -4,7 +4,7 @@ {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":2,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":4,"time":0,"data":{"content":[{"type":"text","text":"Prove that bash state persists. Then create {{cwd}}/note.txt with a tab-indented line, view it, replace that literal tab-indented line, and make the persistent shell exit with code 9."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":5,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":5,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session/title","seq":6,"time":0,"data":{"title":"Prove that bash state persists.","messageSeqs":[4],"source":{"kind":"fallback"}}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":7,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/context","seq":8,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}} diff --git a/examples/jsonrpc-agent/tests/snapshots/persistent-tools/session.jsonl b/examples/jsonrpc-agent/tests/snapshots/persistent-tools/session.jsonl index a436a9e5c2..be559eeb9a 100644 --- a/examples/jsonrpc-agent/tests/snapshots/persistent-tools/session.jsonl +++ b/examples/jsonrpc-agent/tests/snapshots/persistent-tools/session.jsonl @@ -4,7 +4,7 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821461907,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1785331618312,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498592368,"data":{"content":[{"type":"text","text":"Prove that bash state persists. Then create {{cwd}}/note.txt with a tab-indented line, view it, replace that literal tab-indented line, and make the persistent shell exit with code 9."}],"source":{"kind":"user"},"role":"user","id":"9a08e199-69d7-4b85-bfa4-27b41a92672a"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730508088,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"bb38bdc2-276e-46ec-87a1-089732acbc8d"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730508088,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."}]},"role":"user","id":"bb38bdc2-276e-46ec-87a1-089732acbc8d"},"surfaceOp":"append"} {"type":"session/title","seq":6,"time":1785730508088,"data":{"title":"Prove that bash state persists.","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","seq":7,"time":1785498592370,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","seq":8,"time":1785730508089,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/packages/bash/tool-bash/tests/integration.spec.ts b/packages/bash/tool-bash/tests/integration.spec.ts index 933df15509..788c086ba6 100644 --- a/packages/bash/tool-bash/tests/integration.spec.ts +++ b/packages/bash/tool-bash/tests/integration.spec.ts @@ -202,7 +202,12 @@ describe('bash tool through the agent loop', () => { expect(pendingNotice.content.some( block => block.type === 'text' && block.text.includes('background task bash-1 (bash: echo bg-ok) finished'), )).toBe(true) - expect(pendingNotice.source).toEqual({ kind: 'plugin', plugin: 'tool-tasks' }) + expect(pendingNotice.source).toEqual({ + kind: 'plugin', + plugin: 'tool-tasks', + form: 'notice', + summary: 'bash echo bg-ok [status: completed, exit code: 0]', + }) // The next turn first admits that notice as user/message, then collects // the output through the generic task tool. diff --git a/packages/client/runtime/README.i18n.yaml b/packages/client/runtime/README.i18n.yaml index 9c6ed3d557..23c867e4c0 100644 --- a/packages/client/runtime/README.i18n.yaml +++ b/packages/client/runtime/README.i18n.yaml @@ -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/client/runtime/README.md -README.md: f95e06162bca132a9aa83e0b84e875a81d2f8fc6 -README.zh.md: 4b1a9dda3bbe02ef9241a8a797a07e5285e6daae +README.md: 8ac29a4258bbd7456b20c61e547d48c570e84d27 +README.zh.md: 0e065e43ecc571e68d3976d2100eb43959cb2e3d diff --git a/packages/client/runtime/README.md b/packages/client/runtime/README.md index f95e06162b..8ac29a4258 100644 --- a/packages/client/runtime/README.md +++ b/packages/client/runtime/README.md @@ -34,7 +34,7 @@ SlotsService gives the renderer separate bare observables for `useSessions` and ## The human transcript -`ConversationSnapshot.nodes` is the human transcript, not the model surface. `TranscriptAdapter` projects the raw window in log order — every append-origin surface event (`isAppendSurfaceEvent`) at its own log position, plus one `CompactionSummaryNode` marker per landed compaction checkpoint — and never consults surface order. `ConversationSnapshot.turnEnds` maps each completed turn in that window to its `turn/end` seq, retaining turn completion independently from the transcript so presentation can require a real boundary before enabling an action. A landed compaction therefore keeps the conversation it shadowed on the model side: the marker reports where the model stopped seeing that history instead of erasing it. Model-only replacement copies stay out: a pruned `tool/result` and a regenerated `assistant/message` rewrite one node for the model and mark no boundary. A checkpoint is a `user/message` carrying the compaction seam's plugin source that **replaced** a surface range; an appending plugin-sourced `user/message` is injected context, not a compaction. The adapter's plugin literal is pinned to the seam's own declaration by a type-only import of the cordis-free [`dsh-compact/checkpoint`](../../compact/compact/README.md) leaf, so renaming it there fails `tsc` here; a **value** import of the package would fail the client purity gate, and the package **root** is unreachable even as a type (it reaches `dsh-session`'s root, whose `Context` merge collides the host `sessions` with this program's). +`ConversationSnapshot.nodes` is the human transcript, not the model surface. `TranscriptAdapter` projects the raw window in log order — every append-origin surface event (`isAppendSurfaceEvent`) at its own log position, plus one `CompactionSummaryNode` marker per landed compaction checkpoint — and never consults surface order. `SteeringHistory` replays the durable `agent/inbox/spliced` records in that window: a user-origin message claimed from `next-step` becomes a `SteeringMessageNode` when its matching `user/message` lands, a `next-turn` claim stays a user node, and non-user next-step input stays context. `ConversationSnapshot.turnEnds` maps each completed turn in that window to its `turn/end` seq, retaining turn completion independently from the transcript so presentation can require a real boundary before enabling an action. A landed compaction therefore keeps the conversation it shadowed on the model side: the marker reports where the model stopped seeing that history instead of erasing it. Model-only replacement copies stay out: a pruned `tool/result` and a regenerated `assistant/message` rewrite one node for the model and mark no boundary. A checkpoint is a `user/message` carrying the compaction seam's plugin source that **replaced** a surface range; an appending plugin-sourced `user/message` is injected context, not a compaction. Each context node also carries a `provenance` view: `contextProvenance()` reads the durable source alone to decide whether the row is an `inject` or a cross-session `recall`, and to name its producer from the instruction paths, referenced session titles, or plugin id that source already records. The client holds no table of plugin ids, so a renamed or newly mounted producer stays identifiable without a client release and a resumed or foreign log projects exactly like a live one; a source with no readable kind degrades to an unnamed injection. Beside it, `contextForm()` reads the producer-declared `ContextForm` — the second, independent axis: `kind` says who produced the context, `form` says what shape of information it is, so several producers may share one form. A form this UI version does not present projects as null and renders opaque. The adapter's plugin literal is pinned to the seam's own declaration by a type-only import of the cordis-free [`dsh-compact/checkpoint`](../../compact/compact/README.md) leaf, so renaming it there fails `tsc` here; a **value** import of the package would fail the client purity gate, and the package **root** is unreachable even as a type (it reaches `dsh-session`'s root, whose `Context` merge collides the host `sessions` with this program's). Because the projection is log-ordered, the node array is seq-monotonic by construction: log-only `command/run` / `command/done` nodes splice in by seq, `Session` merges interrupted frozen nodes by their fractional seqs, and a window whose checkpoint cites a shadowed range outside it renders the marker with nothing logged. The marker's summary text comes from the checkpoint's `compact/summary` provenance; a window cut that left the provenance outside makes the row non-expandable rather than empty, and a later page that supplies it resolves the text. Performance contract: one append materializes at most one node and copies the projection only when it adds that node; an event that changes no node keeps the previous array reference (a chunk storm costs nothing), and unchanged nodes keep their object identity. diff --git a/packages/client/runtime/README.zh.md b/packages/client/runtime/README.zh.md index 4b1a9dda3b..0e065e43ec 100644 --- a/packages/client/runtime/README.zh.md +++ b/packages/client/runtime/README.zh.md @@ -34,7 +34,7 @@ SlotsService 分别为 renderer 提供 `useSessions` 与 `useWorkspaces` 的裸 ## 面向人的 transcript(文本记录) -`ConversationSnapshot.nodes` 是面向人的 transcript,不是模型 surface。`TranscriptAdapter` 按日志顺序投影原始窗口——每个 append 来源的 surface 事件(`isAppendSurfaceEvent`)落在它自己的日志位置上,外加每次落地的压缩(compaction)检查点贡献一个 `CompactionSummaryNode` 标记——且从不查询 surface 顺序。`ConversationSnapshot.turnEnds` 把该窗口中的每个已完成轮次映射到其 `turn/end` seq;它独立于 transcript 保留轮次完成状态,使呈现层能够在启用操作前要求存在真实边界。于是一次落地的压缩会保留它在模型侧遮蔽掉的对话:标记报告模型从哪里开始看不见那段历史,而不是把它抹掉。仅模型可见的 replacement 副本不进入记录:被裁剪的 `tool/result` 和重新生成的 `assistant/message` 只为模型重写一个节点,不标记任何边界。检查点是携带压缩 seam 插件来源、且**替换**了一段 surface 范围的 `user/message`;一条 append 的插件来源 `user/message` 是注入上下文,不是压缩。适配器的插件字面量通过对无 cordis 的 [`dsh-compact/checkpoint`](../../compact/compact/README.md) 叶子做仅类型导入,钉在压缩 seam 自己的声明上:在那里改名会让此处 `tsc` 失败;而对该包(package)做**值**导入会被客户端纯度门禁拒绝,包的**根**即便作为类型也无法到达(它会到达 `dsh-session` 的根,其 `Context` 合并会让 host 的 `sessions` 与本程序的冲突)。 +`ConversationSnapshot.nodes` 是面向人的 transcript,不是模型 surface。`TranscriptAdapter` 按日志顺序投影原始窗口。每个 append 来源的 surface 事件(`isAppendSurfaceEvent`)落在它自己的日志位置上,每次落地的压缩(compaction)检查点还会贡献一个 `CompactionSummaryNode` 标记;适配器从不查询 surface 顺序。`SteeringHistory` 会重放该窗口中的持久 `agent/inbox/spliced` 记录:用户来源的消息从 `next-step` 被领取,并以相同身份落成 `user/message` 时,会投影为 `SteeringMessageNode`;从 `next-turn` 领取的消息仍是用户节点,非用户来源的 next-step 输入仍是上下文。`ConversationSnapshot.turnEnds` 把该窗口中的每个已完成轮次映射到其 `turn/end` seq;它独立于 transcript 保留轮次完成状态,使呈现层能够在启用操作前要求存在真实边界。于是一次落地的压缩会保留它在模型侧遮蔽掉的对话:标记报告模型从哪里开始看不见那段历史,而不是把它抹掉。仅模型可见的 replacement 副本不进入记录:被裁剪的 `tool/result` 和重新生成的 `assistant/message` 只为模型重写一个节点,不标记任何边界。检查点是携带压缩 seam 插件来源、且**替换**了一段 surface 范围的 `user/message`;一条 append 的插件来源 `user/message` 是注入上下文,不是压缩。每个上下文节点还携带一份 `provenance` 视图:`contextProvenance()` 只读取持久来源,据此判定该行是 `inject`(注入)还是跨会话的 `recall`(召回),并用该来源已经记录的指令文件路径、被引用会话标题或插件 id 命名其生产者。客户端不保存任何插件 id 表,因此重命名或新挂载的生产者无需客户端发版即可保持可辨识,恢复的会话日志与外部日志的投影结果和实时会话完全一致;没有可读 kind 的来源则降级为无名注入。与之并列的 `contextForm()` 读取生产方声明的 `ContextForm`,这是相互独立的第二根轴:`kind` 说明上下文由谁产生,`form` 说明它是何种形态的信息,因此多个生产方可以共用一种形态。本 UI 版本不呈现的形态投影为 null,按 opaque 渲染。适配器的插件字面量通过对无 cordis 的 [`dsh-compact/checkpoint`](../../compact/compact/README.md) 叶子做仅类型导入,钉在压缩 seam 自己的声明上:在那里改名会让此处 `tsc` 失败;而对该包(package)做**值**导入会被客户端纯度门禁拒绝,包的**根**即便作为类型也无法到达(它会到达 `dsh-session` 的根,其 `Context` 合并会让 host 的 `sessions` 与本程序的冲突)。 由于投影按日志顺序,节点数组天然按 seq 单调:仅日志的 `command/run` / `command/done` 节点按 seq 插入,`Session` 按分数 seq 归并被打断的冻结节点,而检查点所引范围落在窗口之外的窗口会渲染出标记且不打印任何日志。标记的摘要文本来自检查点的 `compact/summary` 溯源;窗口切分把溯源留在窗口外时该行不可展开而非空白,后续补上溯源的分页会解析出文本。性能契约:一次追加最多物化一个节点,并且仅在加入该节点时复制投影;不改变任何节点的事件保持上一次的数组引用(分片风暴零成本),未变化的节点保持其对象标识。 diff --git a/packages/client/runtime/src/client/index.ts b/packages/client/runtime/src/client/index.ts index ce1fffe6d7..06f88a9131 100644 --- a/packages/client/runtime/src/client/index.ts +++ b/packages/client/runtime/src/client/index.ts @@ -48,11 +48,14 @@ export type { AssistantTiming, CodeSubCall, CommandNode, CompactionSummaryNode, ComposerPhase, ContextMessageNode, ConversationNode, ConversationSnapshot, ModelRetryNode, QueuedMessage, RunningToolCall, - TodoItem, ToolResultNode, TurnErrorNode, UnknownSurfaceNode, UserMessageNode, + SteeringMessageNode, TodoItem, ToolResultNode, TurnErrorNode, UnknownSurfaceNode, UserMessageNode, } from './sessions/conversation.ts' export type { ConversationContext, ConversationContextOriginKind, } from './sessions/conversation-context.ts' +export type { + ContextProvenanceView, ContextRole, KnownContextForm, +} from './sessions/context-provenance.ts' export type { ConversationPromptSnapshot, RequestInspectionSnapshot, RequestPromptChange, RequestView, } from './sessions/request-inspection.ts' diff --git a/packages/client/runtime/src/client/session-history/history-fold.ts b/packages/client/runtime/src/client/session-history/history-fold.ts index 3130907aeb..d792fd2b76 100644 --- a/packages/client/runtime/src/client/session-history/history-fold.ts +++ b/packages/client/runtime/src/client/session-history/history-fold.ts @@ -11,6 +11,8 @@ import type { PartialAssistant, RunningToolCall, } from '../sessions/conversation.ts' import { toAssistantBlocks } from '../sessions/conversation.ts' +import { contextForm, contextProvenance } from '../sessions/context-provenance.ts' +import { SteeringHistory } from '../sessions/steering-history.ts' import type { ConversationContext, ConversationContextOriginKind, } from '../sessions/conversation-context.ts' @@ -126,6 +128,7 @@ function materializeNode( resultView: ToolResultView | null, assistantTiming: AssistantTiming | undefined, requestConfig: AssistantRequestConfig | undefined, + steering: boolean, ): ConversationNode { switch (event.type) { case 'user/message': @@ -133,6 +136,15 @@ function materializeNode( return { kind: 'context', seq: event.seq, time: event.time, content: event.data.content, source: event.data.source, + provenance: contextProvenance(event.data.source), + form: contextForm(event.data.source), + } + } + if (steering) { + return { + kind: 'steering', messageId: event.data.id, + seq: event.seq, time: event.time, + content: event.data.content, source: event.data.source, } } return { @@ -332,6 +344,11 @@ export function projectConversationHistory( entries: readonly HistoryEntry[], ): ConversationHistoryProjection { const events = entries.map(entry => entry.event) + const steeringHistory = new SteeringHistory() + const steeringSeqs = new Set() + for (const event of events) { + if (steeringHistory.apply(event)) steeringSeqs.add(event.seq) + } const baseSeq = events[0]?.seq ?? 0 const eventsBySeq = new Map(events.map(event => [event.seq, event])) const callIndex = new Map() @@ -392,6 +409,7 @@ export function projectConversationHistory( resultViews.get(seq) ?? null, assistantTimings.get(seq), assistantRequestConfigs.get(seq), + steeringSeqs.has(seq), ) nodeCache.set(seq, node) return node diff --git a/packages/client/runtime/src/client/sessions/context-provenance.ts b/packages/client/runtime/src/client/sessions/context-provenance.ts new file mode 100644 index 0000000000..5d231b6bd8 --- /dev/null +++ b/packages/client/runtime/src/client/sessions/context-provenance.ts @@ -0,0 +1,116 @@ +// Context provenance projection: the role and the human-facing producer name +// of one logged non-user `user/message`, read from its durable `source` alone. +// The client keeps no table of known plugin ids — a renamed or newly mounted +// producer must never need a client release to stay identifiable, and a resumed +// or foreign log must project the same way as a live one. + +/** + * Which model-facing role a logged non-user message plays. + * + * `recall` marks material lifted out of another session's log; `inject` marks + * every other producer-supplied context. Mid-turn steering is the third role + * the transcript distinguishes, but it has its own event and node kind + * (`steering/message` / `SteeringMessageNode`) and never reaches here. + */ +export type ContextRole = 'inject' | 'recall' + +/** Role and producer name presented for one logged non-user message. */ +export interface ContextProvenanceView { + /** The role this context plays in the model-facing conversation. */ + role: ContextRole + /** + * Producer name for the row header, taken from the durable source: the + * instruction paths, the referenced session titles, the plugin id, or the + * bare source kind for a producer this UI version does not know. Null only + * when the source carries no readable kind at all. + */ + label: string | null +} + +/** One durable source narrowed to the readable-record shape; null for anything else. */ +function asRecord(value: unknown): Record | null { + return typeof value === 'object' && value !== null && !Array.isArray(value) + ? value as Record + : null +} + +/** A record field read as a non-empty string, or null. */ +function readString(record: Record, key: string): string | null { + const value = record[key] + return typeof value === 'string' && value.length > 0 ? value : null +} + +/** Distinct non-empty `field` values of an array-valued source member, in first-seen order. */ +function collect(source: Record, member: string, field: string): string[] { + const list = source[member] + if (!Array.isArray(list)) return [] + const seen: string[] = [] + for (const entry of list) { + const record = asRecord(entry) + const value = record === null ? null : readString(record, field) + if (value !== null && !seen.includes(value)) seen.push(value) + } + return seen +} + +/** A collected name list rendered as one label; null when the list is empty. */ +function joined(names: string[]): string | null { + return names.length > 0 ? names.join(', ') : null +} + +/** + * Project one durable message source onto its transcript role and producer name. + * + * The source arrives over the wire as opaque JSON (`MessageSource` is + * merge-extensible, so no client-side union can be exhaustive), and a durable + * log may predate or postdate this UI; every unreadable shape therefore + * degrades to `inject` with whatever name the record still carries. + * @param source - the logged `user/message` source, exactly as recorded. + * @returns the role and producer name to present for this context. + */ +export function contextProvenance(source: unknown): ContextProvenanceView { + const record = asRecord(source) + const kind = record === null ? null : readString(record, 'kind') + if (record === null || kind === null) return { role: 'inject', label: null } + switch (kind) { + // Cross-session snapshots are the one durable source that carries another + // session's material; its references name the sessions they were read from. + case 'session-reference': + return { role: 'recall', label: joined(collect(record, 'references', 'label')) ?? kind } + // Workspace instructions name the files they were reconciled from, which + // identifies the producer far better than the plugin id would. + case 'workspace-instructions': + return { role: 'inject', label: joined(collect(record, 'changes', 'path')) ?? kind } + case 'plugin': + return { role: 'inject', label: readString(record, 'plugin') ?? kind } + // Documented default arm of the merge-extensible source map: an unknown + // producer still identifies itself by its own durable kind. + default: + return { role: 'inject', label: kind } + } +} + +/** + * Context forms this UI version renders with a dedicated presentation. The + * durable vocabulary (`ContextForm` in `dsh-llm`) may already be wider — an + * unrecognized or absent value degrades to the opaque presentation rather than + * dropping the row, so a log written by a newer or foreign producer still + * renders. + */ +const KNOWN_FORMS = ['instructions', 'catalog', 'snapshot', 'notice', 'relay', 'recall'] as const + +/** One durable context form this UI version knows how to present. */ +export type KnownContextForm = typeof KNOWN_FORMS[number] + +/** + * Read the producer-declared form off one durable message source. + * @param source - the logged `user/message` source, exactly as recorded. + * @returns the form when this UI version presents it, otherwise null (opaque). + */ +export function contextForm(source: unknown): KnownContextForm | null { + const record = asRecord(source) + const form = record === null ? null : readString(record, 'form') + return form !== null && (KNOWN_FORMS as readonly string[]).includes(form) + ? form as KnownContextForm + : null +} diff --git a/packages/client/runtime/src/client/sessions/conversation.ts b/packages/client/runtime/src/client/sessions/conversation.ts index 55cdebf4aa..d24b963d6b 100644 --- a/packages/client/runtime/src/client/sessions/conversation.ts +++ b/packages/client/runtime/src/client/sessions/conversation.ts @@ -12,6 +12,7 @@ import type { RpcError, SessionId, SubagentAddress, ToolCallView, ToolResultView, } from '@deepseek-ai/dsh-client-connection/client' import type { PendingInteraction } from './pending.ts' +import type { ContextProvenanceView, KnownContextForm } from './context-provenance.ts' export type { TodoItem } /** Request configuration recorded for one provider call. */ @@ -102,6 +103,18 @@ export interface AssistantMessageNode { interrupted?: true } +/** A human message admitted from the next-step inbox while a turn was running. */ +export interface SteeringMessageNode { + kind: 'steering' + /** Stable message identity shared with its pre-admission inbox occurrence. */ + messageId: MessageId + seq: number + /** Unix epoch ms from the source session event. */ + time: number + content: readonly ContentBlock[] + source: unknown +} + /** A context/system injection surfaced in the flow. */ export interface ContextMessageNode { kind: 'context' @@ -110,6 +123,10 @@ export interface ContextMessageNode { time: number content: readonly ContentBlock[] source: unknown + /** Role and producer name projected from `source` ({@link contextProvenance}). */ + provenance: ContextProvenanceView + /** Producer-declared information form ({@link contextForm}); null presents as opaque. */ + form: KnownContextForm | null } /** Durable notice that a closed failed step is waiting for a model-request retry. */ @@ -223,6 +240,7 @@ export interface CommandNode { export type ConversationNode = | UserMessageNode | AssistantMessageNode + | SteeringMessageNode | ContextMessageNode | ModelRetryNode | TurnErrorNode diff --git a/packages/client/runtime/src/client/sessions/steering-history.ts b/packages/client/runtime/src/client/sessions/steering-history.ts new file mode 100644 index 0000000000..0f66025e16 --- /dev/null +++ b/packages/client/runtime/src/client/sessions/steering-history.ts @@ -0,0 +1,65 @@ +/** Reconstruct durable steering identity from the event-sourced agent inbox. */ + +import type { SessionEvent } from '@deepseek-ai/dsh-session/types' + +type InboxTarget = 'next-turn' | 'next-step' + +/** Minimal pending identity retained while replaying durable inbox splices. */ +interface PendingIdentity { + readonly id: string +} + +/** Client-side structural view of the host-owned inbox event. */ +interface InboxSplice { + readonly target: InboxTarget + readonly start: number + readonly removedCount?: number + readonly inserted: readonly PendingIdentity[] + readonly outcome?: 'canceled' +} + +/** + * Incrementally identifies `user/message` events claimed from the next-step + * inbox. The agent loop records all admitted input as `user/message`; the + * preceding `agent/inbox/spliced` events preserve whether it came from the + * queued-turn list or the next-step list. + */ +export class SteeringHistory { + private readonly inbox: Record = { + 'next-turn': [], + 'next-step': [], + } + + private readonly claimedNextStep = new Set() + + /** Clear all replay state before rebuilding a history window. */ + reset(): void { + this.inbox['next-turn'] = [] + this.inbox['next-step'] = [] + this.claimedNextStep.clear() + } + + /** + * Apply one event and report whether it is a durable human steering message. + * @param event - next raw session event in sequence order. + * @returns true only for a user-origin message previously claimed from `next-step`. + */ + apply(event: SessionEvent): boolean { + if ((event.type as string) === 'agent/inbox/spliced') { + this.applySplice(event.data as unknown as InboxSplice) + return false + } + if (event.type !== 'user/message') return false + const id = event.data.id + if (!this.claimedNextStep.delete(id)) return false + return event.data.source.kind === 'user' + } + + /** Replay one host-validated inbox splice. */ + private applySplice({ target, start, removedCount = 0, inserted, outcome }: InboxSplice): void { + const removed = this.inbox[target].splice(start, removedCount, ...inserted) + for (const identity of inserted) this.claimedNextStep.delete(identity.id) + if (target !== 'next-step' || outcome === 'canceled') return + for (const identity of removed) this.claimedNextStep.add(identity.id) + } +} diff --git a/packages/client/runtime/src/client/sessions/transcript-adapter.ts b/packages/client/runtime/src/client/sessions/transcript-adapter.ts index d2973b63a1..306571b2bf 100644 --- a/packages/client/runtime/src/client/sessions/transcript-adapter.ts +++ b/packages/client/runtime/src/client/sessions/transcript-adapter.ts @@ -22,6 +22,8 @@ import type { COMPACT_CHECKPOINT_SOURCE } from '@deepseek-ai/dsh-compact/checkpo import type { ToolCallView, ToolEventView, ToolResultView } from '@deepseek-ai/dsh-client-connection/client' import type { CommandNode, CompactionSummaryNode, ConversationNode } from './conversation.ts' import { toAssistantBlocks } from './conversation.ts' +import { contextForm, contextProvenance } from './context-provenance.ts' +import { SteeringHistory } from './steering-history.ts' import type { AssistantStepMetadata } from './assistant-timing.ts' import { indexAssistantStepTiming, settledAssistantTiming } from './assistant-timing.ts' @@ -46,11 +48,12 @@ interface CallIndexEntry { callView: ToolCallView | null } -/** One event -> UI node (pure function; the eight-variant ConversationNode union). */ +/** One event -> UI node (pure function; the ten-variant ConversationNode union). */ function materializeNode( event: SessionEvent, callIndex: ReadonlyMap, resultView: ToolResultView | null, + steering: boolean, stepTimings: ReadonlyMap, ): ConversationNode { switch (event.type) { @@ -62,6 +65,15 @@ function materializeNode( return { kind: 'context', seq: event.seq, time: event.time, content: event.data.content, source: event.data.source, + provenance: contextProvenance(event.data.source), + form: contextForm(event.data.source), + } + } + if (steering) { + return { + kind: 'steering', messageId: event.data.id, + seq: event.seq, time: event.time, + content: event.data.content, source: event.data.source, } } return { @@ -178,6 +190,8 @@ export class TranscriptAdapter { private stepTimings = new Map() /** Wire result views keyed by the tool/result event's seq (views ride the envelope, not the event). */ private resultViews = new Map() + /** Durable inbox replay used to distinguish next-step human input from queued prompts. */ + private readonly steeringHistory = new SteeringHistory() /** * Command lifecycle nodes by commandId (insertion = run order). The * `command/run`/`command/done` pair is log-only, so it is not a surface @@ -206,6 +220,8 @@ export class TranscriptAdapter { this.callIdx = new Map() this.resultViews.clear() this.commandIdx = new Map() + this.steeringHistory.reset() + const steeringSeqs = new Set() this.stepTimings = new Map() for (let i = 0; i < events.length; i++) { const event = events[i] @@ -214,13 +230,14 @@ export class TranscriptAdapter { this.eventIndex.set(event.seq, event) this.indexCall(event, views?.[i]) this.indexCommand(event) + if (this.steeringHistory.apply(event)) steeringSeqs.add(event.seq) indexAssistantStepTiming(this.stepTimings, event) } // Indexes first, then project: a tool/result materializes against the // complete call index, and a checkpoint against the complete event index. const projected: ConversationNode[] = [] for (const event of events) { - if (isTranscriptEvent(event)) projected.push(this.materialize(event)) + if (isTranscriptEvent(event)) projected.push(this.materialize(event, steeringSeqs.has(event.seq))) } this.projected = projected } @@ -237,10 +254,11 @@ export class TranscriptAdapter { append(event: SessionEvent, view?: ToolEventView): void { this.eventIndex.set(event.seq, event) this.indexCall(event, view) + const steering = this.steeringHistory.apply(event) indexAssistantStepTiming(this.stepTimings, event) if (this.indexCommand(event)) this.rev++ if (!isTranscriptEvent(event)) return - this.projected = [...this.projected, this.materialize(event)] + this.projected = [...this.projected, this.materialize(event, steering)] this.rev++ } @@ -273,10 +291,16 @@ export class TranscriptAdapter { } /** Materialize one transcript event against the complete current indexes. */ - private materialize(event: SessionEvent): ConversationNode { + private materialize(event: SessionEvent, steering: boolean): ConversationNode { return isCompactCheckpoint(event) ? materializeCompaction(event, this.eventIndex) - : materializeNode(event, this.callIdx, this.resultViews.get(event.seq) ?? null, this.stepTimings) + : materializeNode( + event, + this.callIdx, + this.resultViews.get(event.seq) ?? null, + steering, + this.stepTimings, + ) } /** diff --git a/packages/client/runtime/tests/context-provenance.spec.ts b/packages/client/runtime/tests/context-provenance.spec.ts new file mode 100644 index 0000000000..11d8931439 Binary files /dev/null and b/packages/client/runtime/tests/context-provenance.spec.ts differ diff --git a/packages/client/runtime/tests/history-fold.spec.ts b/packages/client/runtime/tests/history-fold.spec.ts index 57f498b926..083bdc3566 100644 --- a/packages/client/runtime/tests/history-fold.spec.ts +++ b/packages/client/runtime/tests/history-fold.spec.ts @@ -1,4 +1,4 @@ -import { createMessage } from '@deepseek-ai/dsh-llm' +import { createMessage, createUserMessage } from '@deepseek-ai/dsh-llm' import type { SessionEvent } from '@deepseek-ai/dsh-session/types' import { describe, expect, it } from 'vitest' import { projectConversationHistory } from '../src/client/session-history/history-fold.ts' @@ -10,6 +10,49 @@ const at = (seq: number, event: Record): SessionEvent => ({ seq, time: 1_700_000_000_000 + seq, ...event }) as unknown as SessionEvent describe('projectConversationHistory', () => { + it('names an injected context node from its durable source, like the live adapter', () => { + // The fold declares its own node mapping (jscpd:ignore in the source), so + // the provenance projection is pinned on both sides independently. + const injected = at(0, { + type: 'user/message', + surfaceOp: 'append', + data: createUserMessage({ + content: [{ type: 'text', text: '' }], + // A plugin source, because the client program does not see the host + // packages that merge richer source kinds; those arms are pinned in + // context-provenance.spec.ts. + source: { kind: 'plugin', plugin: 'dsh-tool-skill', form: 'catalog' }, + }), + }) + const { contexts } = projectConversationHistory([{ event: injected }]) + expect(contexts[contexts.length - 1]?.nodes).toMatchObject([{ + kind: 'context', + seq: 0, + provenance: { role: 'inject', label: 'dsh-tool-skill' }, + form: 'catalog', + }]) + }) + + it('projects next-step human input as durable steering', () => { + const steering = createUserMessage({ + content: [{ type: 'text', text: 'change course' }], + source: { kind: 'user' }, + }) + const events = [ + at(0, { type: 'agent/inbox/spliced', data: { + target: 'next-step', start: 0, inserted: [steering], + } }), + at(1, { type: 'agent/inbox/spliced', data: { + target: 'next-step', start: 0, removedCount: 1, inserted: [], + } }), + at(2, { type: 'user/message', surfaceOp: 'append', data: steering }), + ] + const projection = projectConversationHistory(events.map(event => ({ event }))) + expect(projection.eventNodes).toMatchObject([{ + kind: 'steering', messageId: steering.id, seq: 2, + }]) + }) + it('projects a high-sequence history window without synthesizing its unloaded prefix', () => { const baseSeq = 400_000 const events = [ diff --git a/packages/client/runtime/tests/transcript-adapter.spec.ts b/packages/client/runtime/tests/transcript-adapter.spec.ts index 7666a3430a..031acf1780 100644 --- a/packages/client/runtime/tests/transcript-adapter.spec.ts +++ b/packages/client/runtime/tests/transcript-adapter.spec.ts @@ -85,22 +85,85 @@ describe('TranscriptAdapter', () => { it('materializes every append-origin variant with field mapping', () => { const adapter = new TranscriptAdapter() + const steering = createUserMessage({ + content: [{ type: 'text', text: '插话' }], + source: { kind: 'user' }, + }) adapter.reset([ ev.user(0, '用户'), ev.assistant(1, 0, '助手'), - at(2, { type: 'user/message', surfaceOp: 'append', data: createUserMessage({ + at(2, { type: 'agent/inbox/spliced', data: { + target: 'next-step', start: 0, inserted: [steering], + } }), + at(3, { type: 'agent/inbox/spliced', data: { + target: 'next-step', start: 0, removedCount: 1, inserted: [], + } }), + at(4, { type: 'user/message', surfaceOp: 'append', data: steering }), + at(5, { type: 'user/message', surfaceOp: 'append', data: createUserMessage({ content: [{ type: 'text', text: '上下文' }], source: { kind: 'plugin', plugin: 'p' }, }) }), - ev.toolCall(3, 0, 'c1', 'echo', '{"x":1}'), - ev.toolResult(4, 0, 'c1', '结果'), + ev.toolCall(6, 0, 'c1', 'echo', '{"x":1}'), + ev.toolResult(7, 0, 'c1', '结果'), ]) const nodes = adapter.nodes() - expect(nodes.map(n => n.kind)).toEqual(['user', 'assistant', 'context', 'tool-result']) + expect(nodes.map(n => n.kind)).toEqual(['user', 'assistant', 'steering', 'context', 'tool-result']) + expect(nodes.find(n => n.kind === 'steering')).toMatchObject({ messageId: steering.id }) expect(nodes.find(n => n.kind === 'tool-result')).toMatchObject({ callId: 'c1', call: { name: 'echo', argsRaw: '{"x":1}' }, isError: false, }) }) + it('identifies steering on the live append path', () => { + const adapter = new TranscriptAdapter() + const steering = createUserMessage({ + content: [{ type: 'text', text: 'live steer' }], + source: { kind: 'user' }, + }) + adapter.reset([]) + adapter.append(at(0, { type: 'agent/inbox/spliced', data: { + target: 'next-step', start: 0, inserted: [steering], + } })) + adapter.append(at(1, { type: 'agent/inbox/spliced', data: { + target: 'next-step', start: 0, removedCount: 1, inserted: [], + } })) + adapter.append(at(2, { type: 'user/message', surfaceOp: 'append', data: steering })) + expect(adapter.nodes()).toMatchObject([{ kind: 'steering', messageId: steering.id }]) + }) + + it('does not mark queued, canceled, or non-user next-step messages as steering', () => { + const adapter = new TranscriptAdapter() + const queued = createUserMessage({ content: [{ type: 'text', text: 'queued' }], source: { kind: 'user' } }) + const canceled = createUserMessage({ content: [{ type: 'text', text: 'canceled' }], source: { kind: 'user' } }) + const context = createUserMessage({ + content: [{ type: 'text', text: 'context' }], + source: { kind: 'plugin', plugin: 'test' }, + }) + adapter.reset([ + at(0, { type: 'agent/inbox/spliced', data: { + target: 'next-turn', start: 0, inserted: [queued], + } }), + at(1, { type: 'agent/inbox/spliced', data: { + target: 'next-turn', start: 0, removedCount: 1, inserted: [], + } }), + at(2, { type: 'user/message', surfaceOp: 'append', data: queued }), + at(3, { type: 'agent/inbox/spliced', data: { + target: 'next-step', start: 0, inserted: [canceled], + } }), + at(4, { type: 'agent/inbox/spliced', data: { + target: 'next-step', start: 0, removedCount: 1, inserted: [], outcome: 'canceled', + } }), + at(5, { type: 'user/message', surfaceOp: 'append', data: canceled }), + at(6, { type: 'agent/inbox/spliced', data: { + target: 'next-step', start: 0, inserted: [context], + } }), + at(7, { type: 'agent/inbox/spliced', data: { + target: 'next-step', start: 0, removedCount: 1, inserted: [], + } }), + at(8, { type: 'user/message', surfaceOp: 'append', data: context }), + ]) + expect(adapter.nodes().map(node => node.kind)).toEqual(['user', 'user', 'context']) + }) + it('skips events core does not call surface-eligible, marker or not', () => { // The transcript is the append-origin surface, so log-only events (a chunk, // a turn boundary, a compact/* provenance record) and a future type core @@ -198,10 +261,15 @@ describe('TranscriptAdapter', () => { adapter.reset([ at(0, { type: 'user/message', surfaceOp: 'append', data: createUserMessage({ content: [{ type: 'text', text: '注入的上下文' }], - source: { kind: 'plugin', plugin: 'compact' }, + source: { kind: 'plugin', plugin: 'compact', form: 'instructions' }, }) }), ]) - expect(adapter.nodes()).toMatchObject([{ kind: 'context', seq: 0 }]) + expect(adapter.nodes()).toMatchObject([{ + kind: 'context', + seq: 0, + provenance: { role: 'inject', label: 'compact' }, + form: 'instructions', + }]) }) it('ignores a foreign plugin s replacement user/message', () => { diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index dae7b44cf4..0df2b4b4df 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/README.i18n.yaml @@ -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/client/ui-conversation/README.md -README.md: b262c4f89ebcfb8148a0c9a579efe18c2bd4f7a9 -README.zh.md: 5a333e84000893040ec26f7c10dbb32cb3e064b7 +README.md: 7bd0d551fc41967326dd9860f5c31a99ea3c254a +README.zh.md: d339f6423d9a9f77c02d86ad0b8e57bd0baba52b diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index b262c4f89e..7bd0d551fc 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -14,7 +14,7 @@ Approvals take over the composer through the chain this package declares: `Appro The session header declares and renders the session-scoped `'conversation.session.header.actions'` list beside the title, allowing feature plugins to contribute controls without entering the skeleton. The composer chain currency includes the current conversation `session`; ui-subagent selects one-shot or parent-unavailable addressed sessions for reason-specific read-only copy, while the ordinary InputBar keeps every addressed child Send-only because the continuation service exposes no public per-Activation cancellation operation and `session.cancel` would bypass its ownership. -Logged non-user messages render as a default-collapsed `上下文注入` disclosure. It shares the Tool calls header geometry and interaction with `ToolRow` through the package-internal `DisclosureRow`, while retaining context semantics: the expanded body follows its content height up to a 141px scrolling cap, shows inline JSON for both `content` and `source`, and synthesizes no tool state, summary, or keyed toolview dispatch ([decision](../../../.agents/notes/implemented/feature/2026-07-30-web-context-injection-disclosure.md)). +Logged non-user messages render as a default-collapsed disclosure whose header names the role the runtime projected for the message — `上下文注入` for an injection, `跨会话召回` for a recalled session — followed by the producer name that projection read out of the durable source, so a reader distinguishes a skill catalog from a workspace instruction file or a recalled session without expanding. A source that names no producer shows the role alone. The header shares the Tool calls geometry and interaction with `ToolRow` through the package-internal `DisclosureRow`, while retaining context semantics: the expanded body follows its content height up to a 141px scrolling cap and synthesizes no tool state, summary, or keyed toolview dispatch ([disclosure decision](../../../.agents/notes/implemented/feature/2026-07-30-web-context-injection-disclosure.md), [provenance decision](../../../.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.md)). That body follows the form the producer declared on its durable source: `instructions` names the reconciled files above their text, `catalog` lists the entries the source recorded instead of the model-facing prose, and every other value — absent, unknown to this version, or carrying no usable fields — renders the opaque body, which shows the model-facing text with its real line breaks and the remaining provenance as fields. The opaque body is the documented default, not a leftover: a resumed, forked, or foreign log must render whether or not its producer is mounted here. A durable or pending steering bubble carries an `插话` / `Interjection` caption above it, the only thing distinguishing a mid-turn interjection from the turn-opening prompt that shares its bubble. A Think row stays collapsed by default and exposes live reasoning throughput without expanding the chain of thought: while its reasoning block is the streaming tail, the summary switches from the settled first line to the latest non-blank line and its one-line scrollport follows each delta to the inline end. Expanding the row removes the moving summary and leaves the full reasoning in ordinary page flow, so page reading never fights an internal follower; settlement restores the stable first-line summary at the left edge ([decision](../../../.agents/notes/implemented/feature/2026-08-02-web-thinking-tail-scroll.md)). diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index 5a333e8400..d339f6423d 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -12,7 +12,7 @@ 会话页头会在标题旁声明并渲染 Session scope 的 `'conversation.session.header.actions'` 列表,使功能插件无需进入骨架即可贡献控件。编辑器链的 currency 包含当前对话 `session`;ui-subagent 会选取 one-shot 或 parent 不可用的已寻址会话,并按原因显示只读文案,而普通 InputBar 会让所有已寻址 child 仅保留 Send,因为继续执行服务不公开逐 Activation 取消操作,`session.cancel` 也会绕过其所有权。 -已记录的非用户消息渲染为默认折叠的 `上下文注入` 展开项。它通过包内部的 `DisclosureRow` 与 `ToolRow` 共享 Tool calls 标题栏的几何与交互,同时保留上下文语义:展开内容区的高度会随内容自适应,最大为 141px,超出后滚动,并以内联 JSON 展示 `content` 和 `source`,且不会合成工具状态、摘要或键控 toolview 分发([决策](../../../.agents/notes/implemented/feature/2026-07-30-web-context-injection-disclosure.md))。 +已记录的非用户消息渲染为默认折叠的展开项,标题栏先给出运行时为该消息投影出的角色——注入为 `上下文注入`,召回为 `跨会话召回`——其后是该投影从持久来源读出的生产者名称,因此读者无需展开即可区分 skill(技能)目录、工作区指令文件与被召回的会话。来源未提供生产者名称时只显示角色。标题栏通过包内部的 `DisclosureRow` 与 `ToolRow` 共享 Tool calls 的几何与交互,同时保留上下文语义:展开内容区的高度会随内容自适应,最大为 141px,超出后滚动,且不会合成工具状态、摘要或键控 toolview 分发([展开项决策](../../../.agents/notes/implemented/feature/2026-07-30-web-context-injection-disclosure.md)、[来源决策](../../../.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.md))。该内容区按生产方在持久来源上声明的形态渲染:`instructions` 在正文之上列出它对账过的文件,`catalog` 列出来源记录的条目而非面向模型的散文,其余取值——未声明、本版本不认识、或字段不可用——一律渲染 opaque 内容区,即按真实换行展示面向模型的文本,并把剩余来源信息列成字段。opaque 不是兜底剩余物而是有文档的默认:恢复的、fork 的、外部写入的日志,无论其生产方是否挂载在此处,都必须渲染得出来。持久或待处理的 steering(中途引导)气泡上方带有 `插话` / `Interjection` 标注,这是把中途插话与共用同一气泡的开轮提示区分开的唯一标识。 Think 行默认保持折叠,并在不展开思维链的情况下暴露实时推理(reasoning)吞吐:当推理块是流式输出尾部时,摘要从结算后的首行切换到最新的非空行,其单行滚动区会随每个 delta 追到行内末端。展开该行会移除移动摘要,让完整推理进入普通页面流,因此页面阅读不会与内部跟随器争夺滚动;结算后恢复左对齐的稳定首行摘要([决策](../../../.agents/notes/implemented/feature/2026-08-02-web-thinking-tail-scroll.md))。 @@ -36,7 +36,7 @@ Think 行默认保持折叠,并在不展开思维链的情况下暴露实时 todo 两个面就是在该形状上的两个注册项,都使用 slot 声明注入,不依赖 `ConversationService`。`TodoRow` 占用 `'conversation.chat.toolview'` 的 `todo_write` key,摘要该次调用「试图写入」的内容(从其 args 解析出 `<已完成>/<总数> 已完成 · <进行中条目>`;模型 JSON 残缺或形状不对时回落到通用摘要;非 ok 执行状态保留通用状态点,使被取消的调用绝不读成一次已完成的更新)。`TodoDock` 以 `order: 0` 占用 `'conversation.input.dock'` 列表 slot(位于 Goal 与 Queue 之前),是计划条:它经 `useProjection` 读取 host 计算的 `todos` 投影(站立计划:其后没有更晚 `turn/start` 的最近一次 `todo/write`)并渲染 `TodoPanel`,后者接收纯列表,在列表为空时自我隐藏;列表非空时面板初始折叠,表头显示标题加 `"<已完成>/<总数> tasks · in progress"`(状态图标为 figma 的勾选/进行中/虚线未开始一组)。选取由 dock 适配器负责,因此面板保持为其 props 的纯函数;站立列表放在此处而非行内,行才能保持单行。输入区 composer 链隐藏的一切(例如 ui-question 对 `conversation.composer` 的接管)也会隐藏整个 dock,包括这条计划条。 -`QueueDock` 是 `order: 20` 的末端 input-dock 条目。队列为空时隐藏;只有一个待处理项时直接渲染该行;存在两个或更多待处理项时,默认收起为 `" 条排队消息"` 表头,其按钮可展开或收起完整列表。表头暴露 `aria-expanded` 和 `aria-controls`;展开后的列表以 180px 为高度上限,并可滚动。存在进行中的编辑或变更时,列表行会保持可见;队列清空后,下一次出现队列时会恢复默认收起状态。普通会话中的每条可见行仍是单行预览,并提供针对精确单次入队项的编辑、删除和严格 steering(中途引导)操作;已寻址 subagent 则保留只读行,因为其继续执行传输不提供 Queue 变更。如果严格 steering 输给已关闭的窗口,原单次入队项会留在 Queue 中正常投递;如果驱动器已经认领该项,正常投递就已开始。这两种已收敛的竞态都不显示失败,传输和未知错误仍会显示。 +`QueueDock` 是 `order: 20` 的末端 input-dock 条目。队列为空时隐藏;只有一个待处理项时直接渲染该行;存在两个或更多待处理项时,默认收起为 `" 条排队消息"` 表头,其按钮可展开或收起完整列表。表头暴露 `aria-expanded` 和 `aria-controls`;展开后的列表以 180px 为高度上限,并可滚动。存在进行中的编辑或变更时,列表行会保持可见;队列清空后,下一次出现队列时会恢复默认收起状态。普通会话中的每条可见行仍是单行预览,并提供针对精确单次入队项的编辑、删除和严格 steering 操作;已寻址 subagent 则保留只读行,因为其继续执行传输不提供 Queue 变更。如果严格 steering 输给已关闭的窗口,原单次入队项会留在 Queue 中正常投递;如果驱动器已经认领该项,正常投递就已开始。这两种已收敛的竞态都不显示失败,传输和未知错误仍会显示。 Host 带 placement 的 `session/queue` 快照也会携带待处理 steering。QueueDock 会将其过滤掉,ChatView 则把它投影为会话流末尾带复制操作的用户样式气泡;非用户来源的 next-step 项(注入上下文)改以 `context` placement 广播,领取前不在任何界面渲染。消息尚未进入持久轮次,因此不显示 fork。Host 会等携带该 steering 的持久 `user/message` 进入 mux 流之后再退役 steering。客户端运行时接纳该实时事件时,会在发布快照前退役第一个匹配的当前 steering 单次入队项;历史事件无法隐藏后来复用同一 `MessageId` 的单次入队项。气泡交接时因而不会产生空档或重复,会立即从持久节点恢复复制操作与分支控件,仅当该节点是已完成轮次的 transcript 尾部时才启用分支,并能在重连后从同一权威恢复。 diff --git a/packages/client/ui-conversation/src/client/chat/ContextBody.module.css b/packages/client/ui-conversation/src/client/chat/ContextBody.module.css new file mode 100644 index 0000000000..c1f6f5361b --- /dev/null +++ b/packages/client/ui-conversation/src/client/chat/ContextBody.module.css @@ -0,0 +1,161 @@ +/* Expanded context bodies: one code-block surface shared by every form, so the + disclosure keeps the Figma 10:2482 geometry whichever form renders inside. */ + +.text { + margin: 0; + color: var(--dsw-alias-label-secondary); + font: inherit; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +/* Provenance beneath the text: dimmer than the content it describes. */ +.fields { + display: flex; + flex-direction: column; + gap: 2px; + margin: 8px 0 0; + padding-top: 8px; + border-top: 1px solid var(--dsw-alias-line-secondary); +} + +.field { + display: flex; + gap: 8px; + min-width: 0; +} + +.fieldKey { + flex: none; + min-width: 96px; + color: var(--dsw-alias-label-caption); +} + +.fieldValue { + flex: 1 1 auto; + min-width: 0; + margin: 0; + color: var(--dsw-alias-label-tertiary); + overflow-wrap: anywhere; +} + +/* instructions: the reconciled files, above their text. */ +.files { + display: flex; + flex-wrap: wrap; + gap: 4px 12px; + margin: 0 0 8px; + padding: 0; + list-style: none; +} + +.file { + display: flex; + align-items: baseline; + gap: 6px; + min-width: 0; +} + +.filePath { + color: var(--dsw-alias-label-secondary); + overflow-wrap: anywhere; +} + +.fileAction { + color: var(--dsw-alias-label-caption); +} + +/* catalog: a replacement notice above one row per published entry. */ +.catalogNotice { + margin: 0 0 6px; + color: var(--dsw-alias-label-caption); +} + + +.entries { + display: flex; + flex-direction: column; + gap: 4px; + margin: 0; + padding: 0; + list-style: none; +} + +.entry { + display: flex; + gap: 8px; + min-width: 0; +} + +.entryName { + flex: none; + color: var(--dsw-alias-label-secondary); +} + +.entryDescription { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + color: var(--dsw-alias-label-tertiary); + text-overflow: ellipsis; + white-space: nowrap; +} + +/* snapshot: one titled block per contributing subsystem. */ +.sections { + display: flex; + flex-direction: column; + gap: 8px; + margin: 0; +} + +.section { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.sectionName { + color: var(--dsw-alias-label-caption); +} + +.sectionText { + margin: 0; + color: var(--dsw-alias-label-secondary); + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +/* relay: who sent this, above what they said. */ +.relaySender { + margin: 0 0 6px; + color: var(--dsw-alias-label-caption); + overflow-wrap: anywhere; +} + +/* recall: one row per source session, with how much of it survived. */ +.recalls { + display: flex; + flex-direction: column; + gap: 2px; + margin: 0 0 8px; + padding: 0; + list-style: none; +} + +.recall { + display: flex; + gap: 8px; + min-width: 0; +} + +.recallLabel { + color: var(--dsw-alias-label-secondary); + overflow-wrap: anywhere; +} + +.recallCounts { + flex: none; + color: var(--dsw-alias-label-caption); +} diff --git a/packages/client/ui-conversation/src/client/chat/ContextBody.tsx b/packages/client/ui-conversation/src/client/chat/ContextBody.tsx new file mode 100644 index 0000000000..6af65bcdb9 --- /dev/null +++ b/packages/client/ui-conversation/src/client/chat/ContextBody.tsx @@ -0,0 +1,591 @@ +// Expanded bodies for the context disclosure, one per durable context form. +// The producer declares the form; this module only chooses a presentation for +// it. Every form falls back to OpaqueBody, which is the documented default for +// an absent, unknown, or malformed form — a resumed or foreign log must render +// even when this UI version has never seen its producer. + +import type { ReactNode } from 'react' +import type { ContextMessageNode, KnownContextForm } from '@deepseek-ai/dsh-client-runtime/client' +import { JsonBlock } from '@deepseek-ai/dsh-client-ui-primitives' +import type { ChatViewSlotProps } from '../contract/slots.ts' +import css from './ContextBody.module.css' + +/** Model-facing text stays bounded at the disclosure, not at the producer. */ +const MAX_CHARS = 20_000 + +/** Rows a list body materializes before summarizing the remainder. */ +const MAX_ENTRIES = 200 + +type Translate = ChatViewSlotProps['t'] + +/** One durable source narrowed to the readable-record shape; null for anything else. */ +function asRecord(value: unknown): Record | null { + return typeof value === 'object' && value !== null && !Array.isArray(value) + ? value as Record + : null +} + +/** One run of the model-facing content: adjacent text, or one unknown block. */ +type ContentRun = { text: string } | { block: unknown } + +/** + * The content blocks as runs, IN THE ORDER the model received them. + * + * Adjacent text blocks join with no separator, matching how provider adapters + * flatten them — inserting a line break would show the reader a line the model + * never saw. An unknown block breaks the run and keeps its own fallback rather + * than being hoisted past the text around it or vanishing; the block union is + * merge-extensible, so a foreign log may interleave shapes this build does not + * know. + */ +function contentRuns(content: ContextMessageNode['content']): ContentRun[] { + const runs: ContentRun[] = [] + for (const block of content) { + if (block.type !== 'text') { + runs.push({ block }) + continue + } + const last = runs[runs.length - 1] + if (last !== undefined && 'text' in last) last.text += block.text + else runs.push({ text: block.text }) + } + return runs +} + +/** Only the blocks this UI version does not know, for bodies that replace the text. */ +function unknownBlocks(content: ContextMessageNode['content']): unknown[] { + return contentRuns(content).flatMap(run => 'block' in run ? [run.block] : []) +} + +/** The model-facing text, truncated to the display bound. */ +function boundedText(text: string, t: Translate): string { + return text.length > MAX_CHARS + ? `${text.slice(0, MAX_CHARS)}\n${t('json.truncated', { total: text.length })}` + : text +} + +/** + * One source field rendered as a value row; nested shapes stay compact JSON. + * Bounded on its own, because provenance is as unbounded as the text: an unknown + * producer may record an arbitrarily large string or array. + */ +function fieldValue(value: unknown, t: Translate): string { + const text = typeof value === 'string' + ? value + : typeof value === 'number' || typeof value === 'boolean' ? String(value) : JSON.stringify(value) + return boundedText(text, t) +} + +/** + * Provenance fields as a key/value list. `kind` is always omitted because the + * row header already names the producer. `form` is omitted only when a + * dedicated body rendered for it — then the presentation the reader is looking + * at IS that value. On the opaque fallback the declaration is kept, because + * that is the one place a form this version cannot present would otherwise + * disappear from the UI entirely. + */ +function SourceFields({ source, formRendered, t }: { + source: unknown + formRendered: boolean + t: Translate +}): ReactNode { + const record = asRecord(source) + if (record === null) return null + const hidden = formRendered ? ['kind', 'form'] : ['kind'] + const rows = Object.entries(record).filter(([key]) => !hidden.includes(key)) + if (rows.length === 0) return null + return ( +
+ {rows.map(([key, value]) => ( +
+
{key}
+
{fieldValue(value, t)}
+
+ ))} +
+ ) +} + +/** + * Content blocks this UI version does not know, kept visible rather than + * dropped: the block union is merge-extensible, so a newer or foreign log may + * carry a shape this build has no presentation for. + * @param props - The unrecognized blocks and the locale seat. + * @returns One generic JSON block per unknown entry. + */ +function UnknownBlocks({ blocks, t }: { blocks: readonly unknown[]; t: Translate }): ReactNode { + return ( + <> + {blocks.map((block, index) => ( + t('json.truncated', { total })} + /> + ))} + + ) +} + +/** + * The model-facing content of one context, shared by every form that shows it: + * the text with its real line breaks, then any block this UI version does not + * know, which keeps its own fallback rather than vanishing. + * @param props - Durable content and the locale seat. + * @returns The content blocks as the model received them. + */ +function ModelFacingContent({ content, t }: { + content: ContextMessageNode['content'] + t: Translate +}): ReactNode { + return ( + <> + {contentRuns(content).map((run, index) => ('text' in run + ? run.text !== '' && ( +
{boundedText(run.text, t)}
+ ) + : ( + t('json.truncated', { total })} + /> + )))} + + ) +} + +/** + * Default presentation: the model-facing text as text, with its real line + * breaks, and the remaining provenance beneath it. This is what every form + * this UI version does not recognize renders as. + * @param props - Durable content, its source, and the locale seat. + * @returns The opaque context body. + */ +export function OpaqueBody({ content, source, t }: { + content: ContextMessageNode['content'] + source: unknown + t: Translate +}): ReactNode { + return ( + <> + + + + ) +} + +/** One reconciled instruction file, as the durable source records it. */ +interface InstructionChange { + action: 'set' | 'replace' | 'remove' + path: string + digest?: string +} + +/** + * Instruction changes read off the source, or null when the record is not a + * usable instruction list. + * + * The read is all-or-nothing: silently dropping one unreadable entry would show + * a confident, incomplete file list for a log this version cannot fully read. + * Paths are deduplicated in first-seen order, matching how the header label is + * derived from the same array. + */ +function instructionChanges(source: unknown): InstructionChange[] | null { + const record = asRecord(source) + const list = record === null ? undefined : record['changes'] + if (!Array.isArray(list)) return null + const changes: InstructionChange[] = [] + const seen = new Set() + for (const entry of list as readonly unknown[]) { + const change = asRecord(entry) + if (change === null) return null + const path = change['path'] + if (typeof path !== 'string' || path === '') return null + const action = change['action'] + // The action decides which word the row shows, so an unrecognized one is + // not a readable change — it would be presented as loaded or updated. + if (action !== 'set' && action !== 'replace' && action !== 'remove') return null + const digest = change['digest'] + if (seen.has(path)) continue + seen.add(path) + changes.push({ action, path, ...typeof digest === 'string' ? { digest } : {} }) + } + return changes.length === 0 ? null : changes +} + +/** + * Locale key for one reconciled file. The baseline loads a file; a later delta + * distinguishes a newly reconciled path from a rewritten one, which `set` and + * `replace` already separate at the producer. + * @param action - the durable change action. + * @param baseline - whether this context is the startup/resume baseline. + * @returns the key naming what happened to that file. + */ +function instructionAction( + action: InstructionChange['action'], + baseline: boolean, +): 'message.context.instructions.removed' | 'message.context.instructions.loaded' + | 'message.context.instructions.added' | 'message.context.instructions.updated' { + if (action === 'remove') return 'message.context.instructions.removed' + if (baseline) return 'message.context.instructions.loaded' + return action === 'set' ? 'message.context.instructions.added' : 'message.context.instructions.updated' +} + +/** + * `instructions` form: the files this context reconciled, then their text. + * + * The text keeps its `` framing verbatim — the framing is part + * of what the model read, so hiding it would misreport the request. + * @param props - Durable content, its source, and the locale seat. + * @returns The instructions context body, or the opaque body when the change + * list is unreadable. + */ +export function InstructionsBody({ content, source, t }: { + content: ContextMessageNode['content'] + source: unknown + t: Translate +}): ReactNode { + const changes = instructionChanges(source) + if (changes === null) return + const baseline = asRecord(source)?.['baseline'] === true + return ( + <> +
    + {changes.map(change => ( +
  • + {change.path} + + {t(instructionAction(change.action, baseline))} + +
  • + ))} +
+ + + ) +} + +/** One catalog entry, as the durable source records it. */ +interface CatalogEntry { + name: string + description: string +} + +/** + * Catalog entries read off the source, or null when the record is not a usable + * catalog. All-or-nothing for the same reason as the instruction list: this body + * replaces the model-facing text, so a partial list would hide the only complete + * account of what the model read. + */ +function catalogEntries(source: unknown): CatalogEntry[] | null { + const record = asRecord(source) + const list = record === null ? undefined : record['entries'] + if (!Array.isArray(list)) return null + const entries: CatalogEntry[] = [] + for (const item of list as readonly unknown[]) { + const entry = asRecord(item) + if (entry === null) return null + const name = entry['name'] + const description = entry['description'] + if (typeof name !== 'string' || name === '' || typeof description !== 'string') return null + entries.push({ name, description }) + } + // An empty list is a real catalog: a replacement with no entries retires + // every earlier name. Only an unreadable shape falls back. + return entries +} + +/** + * `catalog` form: the published entries as a list, read from the source rather + * than re-parsed out of the model-facing prose. + * + * A catalog whose source carries no usable entries falls through to the opaque + * body, so an older or hand-edited log still shows its text. + * @param props - Durable content, its source, and the locale seat. + * @returns The catalog context body, or the opaque body when the entry list is + * unreadable. + */ +export function CatalogBody({ content, source, t }: { + content: ContextMessageNode['content'] + source: unknown + t: Translate +}): ReactNode { + const entries = catalogEntries(source) + if (entries === null) return + const update = asRecord(source)?.['update'] === true + // Entry count is unbounded (a provider may publish any number of skills), and + // the scrollport bounds height, not node count — so the list bounds itself. + const shown = entries.slice(0, MAX_ENTRIES) + const rest = unknownBlocks(content) + return ( + <> + {update &&

{t('message.context.catalog.replaced')}

} +
    + {shown.map((entry, index) => ( + // Index key: a hand-edited or foreign log may repeat a name, and a + // duplicate React key would drop a row the model did see. +
  • + {entry.name} + {entry.description} +
  • + ))} +
+ {shown.length < entries.length && ( +

+ {t('message.context.catalog.more', { count: entries.length - shown.length })} +

+ )} + {/* The block union is merge-extensible: a catalog message carrying an + unknown block still shows it rather than dropping model-visible content. */} + + + ) +} + +/** One named contribution to a runtime snapshot, as the durable source records it. */ +interface SnapshotSection { + name: string + text: string +} + +/** Snapshot sections read off the source, or null when the record is unusable. */ +function snapshotSections(source: unknown): SnapshotSection[] | null { + const record = asRecord(source) + const list = record === null ? undefined : record['sections'] + if (!Array.isArray(list)) return null + const sections: SnapshotSection[] = [] + for (const item of list as readonly unknown[]) { + const section = asRecord(item) + if (section === null) return null + const name = section['name'] + const text = section['text'] + if (typeof name !== 'string' || name === '' || typeof text !== 'string') return null + sections.push({ name, text }) + } + return sections.length === 0 ? null : sections +} + +/** + * `snapshot` form: the named contributions this snapshot assembled, in order. + * + * The sections are the same bytes the model read, split at the boundaries the + * producer assembled them on, so a reader sees which subsystem contributed + * which state instead of one undifferentiated wall. + * + * One sentence of the model-facing text is NOT in any section: the producer's + * framing line declaring that this snapshot supersedes earlier ones. Unlike the + * `` wrapper an instruction context carries — which wraps + * content and cannot be separated from it — that line states the form's own + * semantics, so the body states them as a caption instead of reprinting the + * joined prose beside the sections it was split from. + * @param props - Durable content, its source, and the locale seat. + * @returns The snapshot context body, or the opaque body when unreadable. + */ +export function SnapshotBody({ content, source, t }: { + content: ContextMessageNode['content'] + source: unknown + t: Translate +}): ReactNode { + const sections = snapshotSections(source) + /* v8 ignore next -- contextBody reads the sections before choosing this body. */ + if (sections === null) return + return ( + <> +

+ {t('message.context.snapshot.supersedes')} +

+
+ {sections.map((section, index) => ( +
+
{section.name}
+
{boundedText(section.text, t)}
+
+ ))} +
+ + ) +} + +/** + * `notice` form: what just happened, with the model-facing text beneath it. + * + * The one-line account also rides the collapsed row ({@link contextBody}), so a + * notice is usually readable without expanding at all. + * @param props - Durable content, its source, and the locale seat. + * @returns The notice context body. + */ +export function NoticeBody({ content, t }: { + content: ContextMessageNode['content'] + source: unknown + t: Translate +}): ReactNode { + return +} + +/** + * `relay` form: which agent sent this, then what it said. + * + * The sender is an opaque session id; it is shown as provenance rather than a + * label, because this client cannot resolve it to a title. + * @param props - Durable content, its source, and the locale seat. + * @returns The relay context body. + */ +export function RelayBody({ content, source, t }: { + content: ContextMessageNode['content'] + source: unknown + t: Translate +}): ReactNode { + const sender = relaySender(source) + /* v8 ignore next -- contextBody resolves the sender before choosing this body. */ + if (sender === null) return + return ( + <> +

+ {t('message.context.relay.from', { session: sender })} +

+ + + ) +} + +/** The sending agent's session id, or null when the record does not name one. */ +function relaySender(source: unknown): string | null { + const sender = asRecord(source)?.['senderSessionId'] + return typeof sender === 'string' && sender !== '' ? sender : null +} + +/** One recalled session, as the durable source records it. */ +interface RecalledSession { + label: string + retained: number + omitted: number + truncated: boolean +} + +/** Recalled sessions read off the source, or null when the record is unusable. */ +function recalledSessions(source: unknown): RecalledSession[] | null { + const record = asRecord(source) + const list = record === null ? undefined : record['references'] + if (!Array.isArray(list)) return null + const sessions: RecalledSession[] = [] + for (const item of list as readonly unknown[]) { + const reference = asRecord(item) + if (reference === null) return null + const label = reference['label'] + const retained = reference['retainedMessages'] + const omitted = reference['omittedMessages'] + const truncated = reference['truncated'] + // Completeness is the fact this card exists to report, so a reference that + // cannot state it is not a readable recall — showing the label alone would + // present a confident card over unknown loss. + if (typeof label !== 'string' || label === '' + || typeof retained !== 'number' || typeof omitted !== 'number' + || typeof truncated !== 'boolean') return null + sessions.push({ label, retained, omitted, truncated }) + } + return sessions.length === 0 ? null : sessions +} + +/** + * `recall` form: which sessions this material came from and how much of each + * survived the read, then the material itself. + * + * Completeness is the fact a reader needs first: recalled context is bounded on + * the way in, so a card that hid the omitted count would overstate what the + * model received. + * @param props - Durable content, its source, and the locale seat. + * @returns The recall context body, or the opaque body when unreadable. + */ +export function RecallBody({ content, source, t }: { + content: ContextMessageNode['content'] + source: unknown + t: Translate +}): ReactNode { + const sessions = recalledSessions(source) + if (sessions === null) return + return ( + <> +
    + {sessions.map((session, index) => ( +
  • + {session.label} + + {t('message.context.recall.counts', { + retained: session.retained, + omitted: session.omitted, + })} + + {session.truncated && ( + {t('message.context.recall.truncated')} + )} +
  • + ))} +
+ + + ) +} + +/** The one-line account a `notice` puts on its collapsed row, when it records one. */ +function noticeSummary(source: unknown): string | null { + const summary = asRecord(source)?.['summary'] + return typeof summary === 'string' && summary !== '' ? summary : null +} + +/** + * Choose the body for one context node. + * + * Returns the form the body actually rendered as, which is not always the + * declared one: a declared form whose fields are unreadable falls back to + * opaque, and the caller labels the row with what it really shows. + * `summary` is the collapsed row's one-line account, which only a `notice` + * records: its whole point is being readable without expanding. + * @param form - the producer-declared form projected onto the node. + * @param props - durable content, its source, and the locale seat. + * @returns the rendered form (null for opaque), its collapsed summary, and its body. + */ +export function contextBody( + form: ContextMessageNode['form'], + props: { content: ContextMessageNode['content']; source: unknown; t: Translate }, +): { rendered: KnownContextForm | null; summary: string | null; body: ReactNode } { + const opaque = { rendered: null, summary: null, body: } + switch (form) { + case 'instructions': + return instructionChanges(props.source) === null + ? opaque + : { rendered: 'instructions', summary: null, body: } + case 'catalog': + return catalogEntries(props.source) === null + ? opaque + : { rendered: 'catalog', summary: null, body: } + case 'snapshot': + return snapshotSections(props.source) === null + ? opaque + : { rendered: 'snapshot', summary: null, body: } + case 'notice': { + const summary = noticeSummary(props.source) + return summary === null + ? opaque + : { rendered: 'notice', summary, body: } + } + case 'relay': + return relaySender(props.source) === null + ? opaque + : { rendered: 'relay', summary: null, body: } + case 'recall': + return recalledSessions(props.source) === null + ? opaque + : { rendered: 'recall', summary: null, body: } + case null: + return opaque + /* v8 ignore next 4 -- closed-union backstop; the compiler rejects a new + KnownContextForm here rather than letting it degrade to opaque silently. */ + default: { + const unreachable: never = form + throw new Error(`unreachable context form: ${String(unreachable)}`) + } + } +} diff --git a/packages/client/ui-conversation/src/client/chat/ContextInjectionRow.module.css b/packages/client/ui-conversation/src/client/chat/ContextInjectionRow.module.css index e603931a27..e72bd594a2 100644 --- a/packages/client/ui-conversation/src/client/chat/ContextInjectionRow.module.css +++ b/packages/client/ui-conversation/src/client/chat/ContextInjectionRow.module.css @@ -12,6 +12,40 @@ color: var(--dsw-alias-label-secondary); } +/* Separator and producer name beside the role title: ToolRow's summary geometry, + so the two disclosure rows keep one 24px rhythm and one separator shape. */ +.sep { + flex: none; + width: 2px; + height: 2px; + margin: 0 8px; + border-radius: 1px; + background: var(--dsw-alias-label-caption); +} + +.source { + flex: none; + min-width: 0; + overflow: hidden; + color: var(--dsw-alias-label-tertiary); + font-size: 14px; + line-height: 24px; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* A notice's one-line account: the reason it rarely needs expanding. */ +.summary { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + color: var(--dsw-alias-label-tertiary); + font-size: 14px; + line-height: 24px; + text-overflow: ellipsis; + white-space: nowrap; +} + .body { box-sizing: border-box; width: calc(100% - 22px); @@ -23,7 +57,6 @@ border-radius: 8px; background: var(--dsw-alias-markdown-code-block); color: var(--dsw-alias-label-tertiary); + /* Figma 10:2482 code text: the form bodies inherit it from the scrollport. */ font: 400 11px/16px var(--ds-font-family-code); - white-space: pre-wrap; - overflow-wrap: anywhere; } diff --git a/packages/client/ui-conversation/src/client/chat/ContextInjectionRow.tsx b/packages/client/ui-conversation/src/client/chat/ContextInjectionRow.tsx index 4f7bbff348..6cfc1eebfb 100644 --- a/packages/client/ui-conversation/src/client/chat/ContextInjectionRow.tsx +++ b/packages/client/ui-conversation/src/client/chat/ContextInjectionRow.tsx @@ -1,84 +1,70 @@ -import { useMemo, useState } from 'react' +import { useState } from 'react' import type { ContextMessageNode } from '@deepseek-ai/dsh-client-runtime/client' import type { ChatViewSlotProps } from '../contract/slots.ts' import { IconBrowseOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' import { DisclosureRow } from './DisclosureRow.tsx' +import { contextBody } from './ContextBody.tsx' import css from './ContextInjectionRow.module.css' -const MAX_CHARS = 20_000 - -function inlineJson(payload: unknown): string { - const raw = JSON.stringify(payload) - let formatted = '' - let quoted = false - let escaped = false - - for (let index = 0; index < raw.length; index++) { - const char = raw.charAt(index) - if (quoted) { - formatted += char - if (escaped) escaped = false - else if (char === '\\') escaped = true - else if (char === '"') quoted = false - continue - } - if (char === '"') { - quoted = true - formatted += char - continue - } - if (char === '{' || char === '[') { - formatted += char - const close = char === '{' ? '}' : ']' - if (raw[index + 1] !== close) formatted += ' ' - continue - } - if (char === '}' || char === ']') { - const open = char === '}' ? '{' : '[' - if (raw[index - 1] !== open) formatted += ' ' - formatted += char - continue - } - formatted += char === ':' || char === ',' ? `${char} ` : char - } - return formatted -} - /** Props for the logged non-user message presentation. */ export interface ContextInjectionRowProps { content: ContextMessageNode['content'] source: ContextMessageNode['source'] + /** Role and producer name projected from the durable source. */ + provenance: ContextMessageNode['provenance'] + /** Producer-declared information form; null renders the opaque body. */ + form: ContextMessageNode['form'] /** The owning view's locale seat, passed down as a plain prop. */ t: ChatViewSlotProps['t'] } /** * Render logged context with the Tool calls disclosure chrome from Figma. - * @param props - Durable content and source provenance. - * @returns A collapsed context row with a bounded JSON body. + * + * The header names the role the context plays and, beside it, the producer the + * durable source identifies, so a reader can tell an injected skill catalog + * from a workspace instruction file or a recalled session without expanding. + * The expanded body follows the producer-declared form; an absent or unknown + * form renders the opaque body. + * @param props - Durable content, its projected provenance and form, and the locale seat. + * @returns A collapsed context row with a bounded, form-specific body. */ -export function ContextInjectionRow({ content, source, t }: ContextInjectionRowProps) { +export function ContextInjectionRow({ content, source, provenance, form, t }: ContextInjectionRowProps) { const [open, setOpen] = useState(false) - const body = useMemo(() => { - if (!open) return '' - const text = inlineJson({ content, source }) - return text.length > MAX_CHARS - ? `${text.slice(0, MAX_CHARS)}\n${t('json.truncated', { total: text.length })}` - : text - }, [content, open, source, t]) + // Resolved rather than declared: a form whose fields are unreadable renders + // the opaque body, and the marker must say what the row actually shows. + const { rendered, summary, body } = contextBody(form, { content, source, t }) return ( } chevronClassName={css.chevron} - title={t('message.contextInjection')} + title={t(provenance.role === 'recall' ? 'message.contextRecall' : 'message.contextInjection')} + collapsedContent={provenance.label === null ? undefined : ( + /* ToolRow's separator shape: an aria-hidden dot, so the accessible name + stays the two readable parts and the two disclosure rows expose one + name shape. A source that names no producer drops the dot with it. */ + <> + + {provenance.label} + {summary !== null && ( + <> + + {summary} + + )} + + )} + keepContentWhenOpen open={open} expandable expandOnRowClick onToggle={() => { setOpen(value => !value) }} > -
{body}
+
+ {body} +
) } diff --git a/packages/client/ui-conversation/src/client/chat/MessageItem.module.css b/packages/client/ui-conversation/src/client/chat/MessageItem.module.css index 323c5a5769..5c07ace71e 100644 --- a/packages/client/ui-conversation/src/client/chat/MessageItem.module.css +++ b/packages/client/ui-conversation/src/client/chat/MessageItem.module.css @@ -8,6 +8,15 @@ gap: 6px; } +/* Steering caption above the bubble: mid-turn interjections carry the same + bubble as a turn-opening prompt, so the transcript names which one this is. */ +.steeringMark { + padding-right: 4px; + color: var(--dsw-alias-label-tertiary); + font-size: 12px; + line-height: 16px; +} + .bubble { /* 525px cap inside the 736 column; percentage keeps narrow windows sane. */ max-width: min(525px, 82%); diff --git a/packages/client/ui-conversation/src/client/chat/MessageItem.tsx b/packages/client/ui-conversation/src/client/chat/MessageItem.tsx index fe68c3f70a..994cd9ca5c 100644 --- a/packages/client/ui-conversation/src/client/chat/MessageItem.tsx +++ b/packages/client/ui-conversation/src/client/chat/MessageItem.tsx @@ -1,12 +1,13 @@ -// MessageItem: simple chat nodes — user bubbles -// (right-aligned, with clock + copy / branch IconActions), pending steering -// (copy only), context injection, compaction marker, retry disclosure, and -// unknown-surface JSON rows. +// MessageItem: simple chat nodes — user and consumed-steering bubbles +// (right-aligned, with clock + copy / branch IconActions; steering adds the +// interjection caption that names it), pending steering (caption + copy only), +// context injection, compaction marker, retry disclosure, and unknown-surface +// JSON rows. import { memo, useEffect, useMemo, useState } from 'react' import type { ReactNode } from 'react' import type { - CompactionSummaryNode, ContextMessageNode, ModelRetryNode, + CompactionSummaryNode, ContextMessageNode, ModelRetryNode, SteeringMessageNode, TurnErrorNode, UnknownSurfaceNode, UserMessageNode, } from '@deepseek-ai/dsh-client-runtime/client' import { JsonBlock, MessageText, StateDot } from '@deepseek-ai/dsh-client-ui-primitives' @@ -19,6 +20,7 @@ import css from './MessageItem.module.css' export interface MessageItemProps { node: | UserMessageNode + | SteeringMessageNode | ContextMessageNode | CompactionSummaryNode | ModelRetryNode @@ -170,19 +172,22 @@ function projectUserText(text: string): ReactNode { /** Right-aligned bubble shared by user and steering rows. */ function UserStyleBubble({ - content, actions, pending = false, t, + content, actions, pending = false, steering = false, t, }: { content: readonly unknown[] /** Optional IconActions (or similar) below the bubble; receives the joined text. */ actions?: (text: string) => ReactNode /** Whether this is the Host-authoritative pre-admission steering projection. */ pending?: boolean + /** Marks the bubble as mid-turn steering rather than a turn-opening prompt. */ + steering?: boolean t: ChatViewSlotProps['t'] }): ReactNode { const { text, rest } = contentText(content) const truncated = (total: number): string => t('json.truncated', { total }) return (
+ {steering && {t('message.steering')}}
{projectUserText(text)} {rest.map((block, i) => )} @@ -206,6 +211,7 @@ export function PendingSteeringBubble({ content, t }: { ( t('json.truncated', { total }) switch (node.kind) { case 'user': + case 'steering': return ( ( + ) case 'compaction': return diff --git a/packages/client/ui-conversation/src/client/chat/chat-flow.ts b/packages/client/ui-conversation/src/client/chat/chat-flow.ts index 3fbd5925d6..57d2ac1bb0 100644 --- a/packages/client/ui-conversation/src/client/chat/chat-flow.ts +++ b/packages/client/ui-conversation/src/client/chat/chat-flow.ts @@ -86,7 +86,7 @@ export function messageBranchSeqs( tail = candidate nodeIndex++ } - if (tail?.kind === 'user' + if (tail?.kind === 'user' || tail?.kind === 'steering' || (tail?.kind === 'assistant' && tail.turn === turn && hasContentText(tail.blocks))) { result.add(tail.seq) } diff --git a/packages/client/ui-conversation/src/client/locales.ts b/packages/client/ui-conversation/src/client/locales.ts index 62a112cc42..9ba5ed3876 100644 --- a/packages/client/ui-conversation/src/client/locales.ts +++ b/packages/client/ui-conversation/src/client/locales.ts @@ -45,6 +45,7 @@ export const zh = { 'access.confirm.cancel': '取消', 'access.confirm.enable': '启用 Full access', 'hero.headline': '开始构建吧', + 'hero.preview': '预览版', 'hero.chooseWorkspace': '选择工作区', 'session.hierarchy': '会话层级', 'details.title': '详情', @@ -66,6 +67,18 @@ export const zh = { 'chat.toBottom': '回到底部', 'message.extraBlock': '附加内容块', 'message.contextInjection': '上下文注入', + 'message.contextRecall': '跨会话召回', + 'message.context.instructions.loaded': '已载入', + 'message.context.instructions.added': '已新增', + 'message.context.instructions.updated': '已更新', + 'message.context.instructions.removed': '已移除', + 'message.context.catalog.replaced': '替换目录', + 'message.context.catalog.more': '…还有 {count} 条', + 'message.context.snapshot.supersedes': '取代先前的快照', + 'message.context.relay.from': '来自会话 {session}', + 'message.context.recall.counts': '保留 {retained} 条 · 省略 {omitted} 条', + 'message.context.recall.truncated': '已截断', + 'message.steering': '插话', 'message.compaction': '上下文已压缩', 'message.compaction.expand': '点击查看压缩摘要', 'message.compaction.unavailable': '压缩摘要不可用', @@ -172,6 +185,7 @@ export const en = { 'access.confirm.cancel': 'Cancel', 'access.confirm.enable': 'Enable Full access', 'hero.headline': 'Let\'s start building', + 'hero.preview': 'Preview', 'hero.chooseWorkspace': 'Choose workspace', 'session.hierarchy': 'Session hierarchy', 'details.title': 'Details', @@ -193,6 +207,18 @@ export const en = { 'chat.toBottom': 'Back to bottom', 'message.extraBlock': 'Extra content block', 'message.contextInjection': 'Context injection', + 'message.contextRecall': 'Session recall', + 'message.context.instructions.loaded': 'loaded', + 'message.context.instructions.added': 'added', + 'message.context.instructions.updated': 'updated', + 'message.context.instructions.removed': 'removed', + 'message.context.catalog.replaced': 'Replacement catalog', + 'message.context.catalog.more': '… {count} more', + 'message.context.snapshot.supersedes': 'Supersedes earlier snapshots', + 'message.context.relay.from': 'From session {session}', + 'message.context.recall.counts': '{retained} kept · {omitted} omitted', + 'message.context.recall.truncated': 'truncated', + 'message.steering': 'Interjection', 'message.compaction': 'Context compacted', 'message.compaction.expand': 'View compaction summary', 'message.compaction.unavailable': 'Compaction summary unavailable', diff --git a/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx b/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx index 0c1b31bbb7..4b491e1f7f 100644 --- a/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx @@ -119,7 +119,8 @@ export function HeroShell({ t, children }: HeroShellProps) {
{/* figma 34:10412: fish 34×25 leading the headline, gap 10. */} - {t('hero.headline')} + {t('hero.headline')} + {t('hero.preview')}
{/* The resident composer (ConversationRoot wrapActiveBody seat; the diff --git a/packages/client/ui-conversation/src/client/skeleton/HeroShell.module.css b/packages/client/ui-conversation/src/client/skeleton/HeroShell.module.css index 523f640c0b..3166a81565 100644 --- a/packages/client/ui-conversation/src/client/skeleton/HeroShell.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/HeroShell.module.css @@ -23,21 +23,44 @@ overflow: visible; } -/* figma 34:10411: fish + title, gap 10, centered; 26/32 wt500. */ +/* figma 34:10411: fish + title, gap 10, centered; 26/32 wt500. The preview + badge is a product addition outside that source and aligns to the title. */ .headline { - display: flex; + display: grid; + grid-template-columns: 34px auto; + column-gap: 10px; + row-gap: 4px; align-items: center; justify-content: center; - gap: 10px; font-size: 26px; line-height: 32px; font-weight: 500; color: var(--dsw-alias-label-primary); } +.headlineText { + grid-row: 1; + grid-column: 2; +} + +.previewBadge { + grid-row: 2; + grid-column: 2; + justify-self: start; + padding: 0 4px; + border-radius: 4px; + background: var(--dsw-alias-state-business-tertiary); + color: var(--dsw-alias-label-primary); + font-size: 12px; + line-height: 18px; + font-weight: 500; + white-space: nowrap; +} + /* figma fish fill rides business blue. */ .fish { - flex: none; + grid-row: 1; + grid-column: 1; color: var(--dsw-alias-state-business-primary); } diff --git a/packages/client/ui-conversation/tests/chat-branch-tails.spec.tsx b/packages/client/ui-conversation/tests/chat-branch-tails.spec.tsx index 64081609de..53793f86c2 100644 --- a/packages/client/ui-conversation/tests/chat-branch-tails.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-branch-tails.spec.tsx @@ -212,47 +212,504 @@ describe('MessageItem arms', () => { expect(vi.getTimerCount()).toBe(0) }) - it('context uses the Tool calls disclosure chrome and keeps its JSON collapsed by default', () => { + it('consumed steering is captioned as an interjection and keeps copy and branch actions', () => { + const writeText = vi.fn().mockResolvedValue(undefined) + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: { writeText }, + }) + const fork = vi.fn() + const view = render( + , + ) + expect(view.getByText('插话')).toBeTruthy() + expect(view.getByText('steer!')).toBeTruthy() + expect(view.getByText(/附加内容块/)).toBeTruthy() + fireEvent.click(view.getByRole('button', { name: '复制' })) + expect(writeText).toHaveBeenCalledWith('steer!') + fireEvent.click(view.getByRole('button', { name: '在新对话中分支' })) + expect(fork).toHaveBeenCalledWith(2) + }) + + it('context uses the Tool calls disclosure chrome and keeps its body collapsed by default', () => { const ctxView = render( , ) - const disclosure = ctxView.getByRole('button', { name: '上下文注入' }) + const disclosure = ctxView.getByRole('button', { name: /^上下文注入\s*fixture$/ }) expect(disclosure.getAttribute('aria-expanded')).toBe('false') expect(ctxView.container.querySelector('[data-context-injection-body]')).toBeNull() expect(ctxView.container.querySelector('svg')).not.toBeNull() fireEvent.click(disclosure) expect(disclosure.getAttribute('aria-expanded')).toBe('true') - expect(ctxView.container.querySelector('[data-context-injection-body]')?.textContent).toBe( - '{ "content": [ { "type": "text", "text": "x\\n\\"y\\":,[{}]" } ], ' - + '"source": { "kind": "plugin", "plugin": "fixture", "empty": {}, "list": [] } }', - ) + // An unknown form renders the opaque body: the model-facing text keeps its + // real line breaks instead of being escaped into one JSON line, and the + // remaining provenance follows it as fields. + expect(ctxView.container.querySelector('[data-context-text]')?.textContent) + .toBe('line one\n\nline two') + const fields = [...ctxView.container.querySelectorAll('[data-context-fields] dt')].map(node => node.textContent) + expect(fields).toEqual(['plugin', 'empty', 'list']) fireEvent.keyDown(disclosure, { key: ' ' }) expect(disclosure.getAttribute('aria-expanded')).toBe('false') }) - it('context preserves the bounded JSON truncation contract', () => { + it('the instructions form names the files it reconciled above their text', () => { const view = render( \nInstructions from: AGENTS.md\n' }], + source: { + kind: 'workspace-instructions', + form: 'instructions', + baseline: true, + changes: [ + { action: 'set', scope: '.\u0000AGENTS.md', path: 'AGENTS.md', digest: 'abc' }, + { action: 'remove', scope: 'sub\u0000AGENTS.md', path: 'sub/AGENTS.md' }, + { action: 'replace', scope: '.\u0000AGENTS.md', path: 'AGENTS.md' }, + ], + }, + provenance: { role: 'inject', label: 'AGENTS.md, sub/AGENTS.md' }, + form: 'instructions', + } as never} + />, + ) + fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*AGENTS\.md, sub\/AGENTS\.md$/ })) + const files = [...view.container.querySelectorAll('[data-context-files] li')].map(node => node.textContent) + expect(files).toEqual(['AGENTS.md已载入', 'sub/AGENTS.md已移除']) + // The `` framing is part of what the model read, so the + // body keeps it verbatim rather than presenting a cleaned-up excerpt. + expect(view.container.querySelector('[data-context-text]')?.textContent) + .toContain('') + }) + + it('a delta distinguishes a newly reconciled file from a rewritten one', () => { + const view = render( + , + ) + fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*new\/AGENTS\.md, old\/AGENTS\.md$/ })) + const files = [...view.container.querySelectorAll('[data-context-files] li')].map(node => node.textContent) + expect(files).toEqual(['new/AGENTS.md已新增', 'old/AGENTS.md已更新']) + }) + + it('keeps an interleaved unknown block in the order the model received it', () => { + const view = render( + , ) fireEvent.click(view.getByRole('button', { name: '上下文注入' })) - expect(view.container.querySelector('[data-context-injection-body]')?.textContent) + const texts = [...view.container.querySelectorAll('[data-context-text]')].map(node => node.textContent) + expect(texts).toEqual(['before', 'after']) + expect(view.getByText(/未知内容块/)).toBeTruthy() + }) + + it('the catalog form lists its durable entries instead of the model-facing prose', () => { + const view = render( + \n\n- `a`: A\n' }], + source: { + kind: 'skill-catalog', + form: 'catalog', + entries: [{ name: 'a-skill', description: 'Does A' }, { name: 'b-skill', description: 'Does B' }], + }, + provenance: { role: 'inject', label: 'skill-catalog' }, + form: 'catalog', + } as never} + />, + ) + fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*skill-catalog$/ })) + const entries = [...view.container.querySelectorAll('[data-context-entries] li')].map(node => node.textContent) + expect(entries).toEqual(['a-skillDoes A', 'b-skillDoes B']) + expect(view.container.querySelector('[data-context-text]')).toBeNull() + expect(view.container.querySelector('[data-context-catalog-update]')).toBeNull() + }) + + it('a replacement catalog says so above its entries', () => { + const view = render( + , + ) + fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*skill-catalog$/ })) + expect(view.container.querySelector('[data-context-catalog-update]')?.textContent).toBe('替换目录') + }) + + it('a partially unreadable catalog falls back whole rather than showing a short list', () => { + // All-or-nothing: a body that replaces the model-facing text must not show + // a confident, incomplete account of what the model read. + const view = render( + , + ) + fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*skill-catalog$/ })) + expect(view.container.querySelector('[data-context-entries]')).toBeNull() + expect(view.container.querySelector('[data-context-text]')?.textContent).toBe('catalog prose') + // The marker reports what rendered, not what was declared. + expect(view.container.querySelector('[data-context-injection-body]')?.getAttribute('data-context-form')) + .toBeNull() + }) + + it('an unreadable instruction list falls back to the opaque body with its fields', () => { + const view = render( + , + ) + fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*workspace-instructions$/ })) + expect(view.container.querySelector('[data-context-files]')).toBeNull() + expect(view.container.querySelector('[data-context-text]')?.textContent).toBe('instruction prose') + expect(view.container.querySelector('[data-context-fields]')).not.toBeNull() + }) + + it('joins adjacent text blocks the way a provider adapter flattens them', () => { + // No invented separator: showing a line break the model never saw would + // misreport the request. + const view = render( + , + ) + fireEvent.click(view.getByRole('button', { name: '上下文注入' })) + expect(view.container.querySelector('[data-context-text]')?.textContent).toBe('firstsecond') + }) + + it('bounds an oversized provenance field, not only the model-facing text', () => { + const view = render( + , + ) + fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*plugin$/ })) + expect(view.container.querySelector('[data-context-fields] dd')?.textContent) .toMatch(/… 已截断,共 \d+ 字符$/) }) + it('an empty replacement catalog stays a catalog: it retires every earlier name', () => { + // `renderCatalogUpdate` legitimately publishes zero entries when the last + // skill disappears; falling back would hide that the catalog was cleared. + const view = render( + , + ) + fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*skill-catalog$/ })) + expect(view.container.querySelector('[data-context-catalog-update]')?.textContent).toBe('替换目录') + expect(view.container.querySelectorAll('[data-context-entries] li')).toHaveLength(0) + expect(view.container.querySelector('[data-context-injection-body]')?.getAttribute('data-context-form')) + .toBe('catalog') + }) + + it('a catalog whose entries are unreadable falls back to the opaque body', () => { + const view = render( + , + ) + fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*skill-catalog$/ })) + expect(view.container.querySelector('[data-context-entries]')).toBeNull() + expect(view.container.querySelector('[data-context-text]')?.textContent).toBe('catalog prose') + }) + + it('bounds a large catalog and says how many rows it withheld', () => { + const entries = Array.from({ length: 205 }, (_, index) => ({ name: `s-${index}`, description: 'd' })) + const view = render( + , + ) + fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*skill-catalog$/ })) + expect(view.container.querySelectorAll('[data-context-entries] li')).toHaveLength(200) + expect(view.container.querySelector('[data-context-entries-truncated]')?.textContent).toBe('…还有 5 条') + }) + + it('a catalog keeps a content block this version does not know', () => { + const view = render( + , + ) + fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*skill-catalog$/ })) + expect(view.getByText(/未知内容块/)).toBeTruthy() + }) + + it('an instruction change with an unrecognized action falls back whole', () => { + // The action decides the word the row shows, so an unknown one cannot be + // presented as loaded or updated. + const view = render( + , + ) + fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*workspace-instructions$/ })) + expect(view.container.querySelector('[data-context-files]')).toBeNull() + expect(view.container.querySelector('[data-context-text]')?.textContent).toBe('instruction prose') + }) + + it('the opaque fallback keeps a form declaration this version cannot present', () => { + // Otherwise a newer or foreign log's declared shape vanishes from the UI. + const view = render( + , + ) + fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*later$/ })) + const fields = [...view.container.querySelectorAll('[data-context-fields] dt')].map(node => node.textContent) + expect(fields).toEqual(['plugin', 'form']) + }) + + it('the snapshot form attributes each part to the subsystem that produced it', () => { + const view = render( + , + ) + fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*@deepseek-ai\/dsh-system-prompt$/ })) + const rows = [...view.container.querySelectorAll('[data-context-sections] div')].map(node => node.textContent) + expect(rows).toEqual(['sandbox:policyworkspace-write', 'workspace/repo']) + }) + + it('a notice puts its account on the collapsed row', () => { + // The whole point of the form: readable without expanding. + const view = render( + , + ) + expect(view.container.querySelector('[data-context-summary]')?.textContent) + .toBe('bash pnpm test [status: completed]') + expect(view.container.querySelector('[data-context-injection-body]')).toBeNull() + }) + + it('a notice without its account falls back to the opaque body', () => { + const view = render( + , + ) + expect(view.container.querySelector('[data-context-summary]')).toBeNull() + fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*tool-tasks$/ })) + expect(view.container.querySelector('[data-context-fields]')).not.toBeNull() + }) + + it('each form falls back to the opaque body when its required facts are unreadable', () => { + // The fallback chain is the load-bearing wall: every dedicated form must + // reach it, and the row marker must not claim a form that did not render. + const cases = [ + { form: 'snapshot', source: { kind: 'plugin', form: 'snapshot', sections: 'not-a-list' }, label: 'plugin' }, + { form: 'relay', source: { kind: 'subagent-report', form: 'relay' }, label: 'subagent-report' }, + { form: 'recall', source: { kind: 'session-reference', form: 'recall', references: [{ label: 'x' }] }, label: 'session-reference' }, + ] as const + for (const { form, source, label } of cases) { + cleanup() + const view = render( + , + ) + fireEvent.click(view.getByRole('button', { name: new RegExp(`^上下文注入\\s*${label}$`) })) + expect(view.container.querySelector('[data-context-text]')?.textContent).toBe(`${form} prose`) + expect(view.container.querySelector('[data-context-injection-body]')?.getAttribute('data-context-form')) + .toBeNull() + } + }) + + it('a snapshot states the supersession its framing line carries', () => { + const view = render( + , + ) + fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*plugin$/ })) + expect(view.container.querySelector('[data-context-snapshot-supersedes]')?.textContent) + .toBe('取代先前的快照') + }) + + it('a relay names the agent that sent it above what it said', () => { + const view = render( + , + ) + fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*subagent-report$/ })) + expect(view.container.querySelector('[data-context-relay-sender]')?.textContent).toBe('来自会话 child-7') + expect(view.container.querySelector('[data-context-text]')?.textContent).toBe('child report body') + }) + + it('a recall reports how much of each source session survived the read', () => { + // Recalled context is bounded on the way in, so hiding the omitted count + // would overstate what the model received. + const view = render( + , + ) + fireEvent.click(view.getByRole('button', { name: /^跨会话召回\s*重构 loader, 修 CI$/ })) + const rows = [...view.container.querySelectorAll('[data-context-recalls] li')].map(node => node.textContent) + expect(rows).toEqual(['重构 loader保留 18 条 · 省略 42 条已截断', '修 CI保留 3 条 · 省略 0 条']) + expect(view.container.querySelector('[data-context-text]')?.textContent).toBe('recalled material') + }) + it('unknown nodes retain the generic JSON row', () => { const unknownView = render( , diff --git a/packages/client/ui-conversation/tests/chat-view.spec.tsx b/packages/client/ui-conversation/tests/chat-view.spec.tsx index c4d37d9337..b7ca8dd149 100644 --- a/packages/client/ui-conversation/tests/chat-view.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-view.spec.tsx @@ -376,6 +376,9 @@ describe('ChatView', () => { expect(view.queryByText('later')).toBeNull() const pendingBubble = view.getByText('interrupt now').closest('[data-pending-steering]') expect(pendingBubble).not.toBeNull() + // Pending and durable steering carry the same interjection caption, so the + // hand-off does not change what the row says it is. + expect(within(pendingBubble as HTMLElement).getByText('插话')).toBeTruthy() fireEvent.click(within(pendingBubble as HTMLElement).getByRole('button', { name: '复制' })) expect(writeText).toHaveBeenCalledWith('interrupt now') expect(within(pendingBubble as HTMLElement).queryByRole('button', { name: '在新对话中分支' })).toBeNull() @@ -388,7 +391,8 @@ describe('ChatView', () => { nodes: [ assistant(1, 'working'), { - kind: 'user', seq: 2, time: 2_000, + kind: 'steering', messageId: pending.messageId, + seq: 2, time: 2_000, content: [{ type: 'text', text: 'interrupt now' }], source: null, }, ], @@ -396,6 +400,7 @@ describe('ChatView', () => { }) expect(view.getAllByText('interrupt now')).toHaveLength(1) expect(view.container.querySelector('[data-pending-steering]')).toBeNull() + expect(view.getAllByText('插话')).toHaveLength(1) expect(view.getAllByRole('button', { name: '复制' })).toHaveLength(2) const durableBubble = view.getByText('interrupt now').closest('[class*="userRow"]') as HTMLElement const unavailable = within(durableBubble).getByRole('button', { name: '在新对话中分支' }) @@ -441,6 +446,8 @@ describe('ChatView', () => { const nextRetry = { ...retry(3), turn: 2, retry: 2 } const context = { kind: 'context', seq: 4, time: 4_000, content: [], source: null, + provenance: { role: 'inject', label: null }, + form: null, } as const satisfies ConversationNode const h = makeHarness({ nodes: [user(1, 'try'), retryNode], running: true }) const view = render() diff --git a/packages/client/ui-conversation/tests/skeleton.spec.tsx b/packages/client/ui-conversation/tests/skeleton.spec.tsx index f9bd3fa3c1..b2828bcc80 100644 --- a/packages/client/ui-conversation/tests/skeleton.spec.tsx +++ b/packages/client/ui-conversation/tests/skeleton.spec.tsx @@ -12,12 +12,14 @@ import type { import type { ConversationRootProps } from '../src/client/skeleton/ConversationRoot.tsx' import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client' import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' +import { en as commonEn } from '@deepseek-ai/dsh-client-locale/src/locales/en.ts' import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts' import { createChatStore } from '../src/client/stores.ts' import { SessionInputShell } from '../src/client/input/facade.ts' -import { zh } from '../src/client/locales.ts' +import { en, zh } from '../src/client/locales.ts' import { ConversationRoot } from '../src/client/skeleton/ConversationRoot.tsx' import { ConversationSession } from '../src/client/skeleton/ConversationSession.tsx' +import { HeroShell } from '../src/client/skeleton/EmptyHero.tsx' import { InputBar } from '../src/client/skeleton/InputBar.tsx' import type { InputBarProps } from '../src/client/skeleton/InputBar.tsx' import type { @@ -213,6 +215,14 @@ function mount( } } +describe('Hero chrome', () => { + it('renders the English preview badge through the hero locale seat', () => { + const view = render() + expect(view.getByText('Let\'s start building')).toBeTruthy() + expect(view.getByText('Preview')).toBeTruthy() + }) +}) + describe('ConversationRoot resident composer', () => { it('keeps composer text in the machine, mirrors to the chat store, and submits through the sink', () => { const b = mount(conversationSnapshot()) @@ -273,6 +283,7 @@ describe('ConversationRoot resident composer', () => { expect(host).not.toBeNull() expect(header?.getAttribute('aria-hidden')).toBe('true') expect(b.view.getByText('开始构建吧')).toBeTruthy() + expect(b.view.getByText('预览版')).toBeTruthy() expect(b.view.queryByTestId('view-chat')).toBeNull() // The same machine-backed textarea is live in the hero, and the // persistence mirror stays bound (ConversationSession mounts chrome-hidden diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index 68f2f87258..7429447091 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -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/client/ui-primitives/README.md -README.md: 03e7e3649fd0913fb48579aa87634153f67f5baf -README.zh.md: 090ecc34e8d514e38853de8ed52e82d3bf019b43 +README.md: 385730c94831d2fd4af83f9eca0f55941551c796 +README.zh.md: b8a75dbffc6549f6294dfda5988c67d6569386c9 diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index 03e7e3649f..385730c948 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -10,7 +10,7 @@ Pure React atoms (zero cordis): StateDot, ic_ds_* icons, Button/Pill/Menu/Modal/ ## Markdown rendering -`MarkdownText` renders GFM and `$…$`, `$$…$$`, `\(…\)`, and `\[…\]` TeX math from untrusted assistant output through React elements, with math typeset by KaTeX and trusted commands disabled; block-level same-line `$$…$$` is display math, including `\tag{}`. It omits raw HTML, neutralizes relative and non-HTTP(S)/mailto links, opens HTTP(S) links with safe external-link attributes, and renders absolute HTTP(S) images without a referrer; relative paths, absolute local paths, `file:` URLs, and unsupported schemes retain their alt text. `MessageText` remains the literal-text primitive for user-authored content. `extractMarkdownPlainText` removes Markdown presentation markup for compact labels while preserving raw HTML as literal text. Element spacing, responsive images, tables, links, and inline code use the same `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` tokens as deepsuite `@deepseek/md`. Fenced blocks render through `CodeBlock` (language banner, copy control, shiki for the registered grammars). +`MarkdownText` renders GFM and `$…$`, `$$…$$`, `\(…\)`, and `\[…\]` TeX math from untrusted assistant output through React elements, with math typeset by KaTeX and trusted commands disabled; block-level same-line `$$…$$` is display math, including `\tag{}`. A narrow micromark extension lets asterisk strong emphasis ending in punctuation close before adjacent CJK text, where prose normally omits the whitespace CommonMark requires; single-asterisk emphasis, non-CJK adjacency, escapes, code, and math retain upstream parsing. It omits raw HTML, neutralizes relative and non-HTTP(S)/mailto links, opens HTTP(S) links with safe external-link attributes, and renders absolute HTTP(S) images without a referrer; relative paths, absolute local paths, `file:` URLs, and unsupported schemes retain their alt text. Inline code whose complete value is an absolute HTTP(S) URL keeps its code styling and gains the same safe external anchor; commands, partial URLs, other schemes, and fenced code remain inert. While a reply streams, `MarkdownText` parses incrementally: all but the trailing two blocks freeze as cached React elements and only the source tail behind them re-parses per chunk, so per-chunk work tracks the tail instead of the whole reply ([mechanism and DOM-parity contract](../../../.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.md)). `MessageText` remains the literal-text primitive for user-authored content. `extractMarkdownPlainText` removes Markdown presentation markup for compact labels while preserving raw HTML as literal text. Element spacing, responsive images, tables, links, and inline code use the same `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` tokens as deepsuite `@deepseek/md`. Fenced blocks render through `CodeBlock` (language banner, copy control, shiki for the registered grammars). ## Terminal output @@ -42,6 +42,7 @@ None; this package neither assembles nor sends a provider request. ## Known Limitations and Deferred Work +- **Streaming defers cross-boundary reference resolution** — a reference-style link or footnote whose definition sits on the other side of the incremental freeze boundary renders as literal text while the reply streams; the settled full parse at finalize resolves it. Inline links and references resolved within one parse are unaffected. - **Glyph-level icons are redrawn approximations** — the fish logo (and the sparkle held by ui-conversation) come from font glyphs whose vector geometry is not exportable from the local design data; hand-authored recreations stand in until an exact export path exists. - **Pill and Input have no design source** — both atoms are self-defined; the sidebar search field and view-tab strip that resemble them are consumer-owned compositions, not these atoms. - **No `Active` StateDot variant** — the supported states are done, warning, ongoing, and error. diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index 090ecc34e8..b8a75dbffc 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -10,7 +10,7 @@ ## Markdown 渲染 -`MarkdownText` 通过 React 元素渲染来自不受信任 assistant 输出的 GFM 与 `$…$`、`$$…$$`、`\(…\)` 和 `\[…\]` TeX 公式,公式由 KaTeX 排版并禁用受信任命令;块级同一行 `$$…$$` 是显示公式并支持 `\tag{}`。它会省略原始 HTML,使相对链接及非 HTTP(S)/mailto 链接失效,以安全的外部链接属性打开 HTTP(S) 链接,并在不发送 referrer 的情况下渲染采用绝对 HTTP(S) URL 的图片;相对路径、绝对本地路径、`file:` URL 与不受支持的 scheme 会保留其 alt 文本。`MessageText` 仍是用户创作内容使用的字面文本原语。`extractMarkdownPlainText` 会移除 Markdown 呈现标记以用于紧凑标签,同时将原始 HTML 保留为字面文本。元素间距、响应式图片、表格、链接与行内代码使用与 deepsuite `@deepseek/md` 相同的 `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` token。围栏代码块通过 `CodeBlock` 渲染(语言横幅、复制控件,以及对已注册语法使用 shiki)。 +`MarkdownText` 通过 React 元素渲染来自不受信任 assistant 输出的 GFM 与 `$…$`、`$$…$$`、`\(…\)` 和 `\[…\]` TeX 公式,公式由 KaTeX 排版并禁用受信任命令;块级同一行 `$$…$$` 是显示公式并支持 `\tag{}`。一个小范围的 micromark 扩展允许由星号标记、以标点结尾的粗体在紧邻的 CJK 文本前闭合,以适应 CJK 文本通常省略 CommonMark 所要求空格的写法;单星号强调、紧邻非 CJK 文本的情况、转义、代码与数学公式仍沿用上游解析行为。它会省略原始 HTML,使相对链接及非 HTTP(S)/mailto 链接失效,以安全的外部链接属性打开 HTTP(S) 链接,并在不发送 referrer 的情况下渲染采用绝对 HTTP(S) URL 的图片;相对路径、绝对本地路径、`file:` URL 与不受支持的 scheme 会保留其 alt 文本。完整内容为绝对 HTTP(S) URL 的行内代码会保留代码样式,并获得同样安全的外部链接;命令、非完整 URL、其他 scheme 与围栏代码仍不会成为链接。回复流式输出期间,`MarkdownText` 增量解析:除末尾两个块外全部冻结为缓存的 React 元素,每个分片只重新解析其后的源文本尾部,因此每分片的工作量跟随尾部而非整个回复([机制与 DOM 一致性契约](../../../.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.md))。`MessageText` 仍是用户创作内容使用的字面文本原语。`extractMarkdownPlainText` 会移除 Markdown 呈现标记以用于紧凑标签,同时将原始 HTML 保留为字面文本。元素间距、响应式图片、表格、链接与行内代码使用与 deepsuite `@deepseek/md` 相同的 `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` token。围栏代码块通过 `CodeBlock` 渲染(语言横幅、复制控件,以及对已注册语法使用 shiki)。 ## 终端输出 @@ -42,6 +42,7 @@ ## 已知限制与暂缓事项 +- **流式期间跨边界引用解析被推迟**:定义落在增量冻结边界另一侧的引用式链接或脚注,在回复流式输出期间渲染为字面文本;定稿时的全量解析会将其解析。内联链接以及在同一次解析内完成解析的引用不受影响。 - **字形级图标是重新绘制的近似版本**:鱼形标志(以及 ui-conversation 持有的闪光图标)来自字体字形,而本地设计数据无法导出其矢量几何;在获得精确导出路径前,使用手工重建版本代替。 - **Pill 与 Input 没有设计来源**:两个原子组件均自行定义;与其相似的侧边栏搜索字段和视图标签条由消费方组合,不是这些原子组件。 - **StateDot 没有 `Active` 变体**:支持的状态为 done、warning、ongoing 和 error。 diff --git a/packages/client/ui-primitives/package.json b/packages/client/ui-primitives/package.json index 683ca7a93b..bb788f7069 100644 --- a/packages/client/ui-primitives/package.json +++ b/packages/client/ui-primitives/package.json @@ -21,23 +21,24 @@ "license": "BSD-3-Clause", "dependencies": { "@shikijs/langs": "^4.3.1", + "@types/mdast": "^4.0.4", "anser": "^2.3.5", "clsx": "^2.0.0", "katex": "^0.16.47", "mdast-util-from-markdown": "^2.0.3", "mdast-util-gfm": "^3.1.0", + "mdast-util-math": "^3.0.0", + "micromark-core-commonmark": "^2.0.3", "micromark-extension-gfm": "^3.0.0", "micromark-extension-math": "^3.1.0", "micromark-factory-space": "^2.0.1", "micromark-util-character": "^2.1.1", + "micromark-util-classify-character": "^2.0.1", + "micromark-util-sanitize-uri": "^2.0.1", "micromark-util-symbol": "^2.0.1", "micromark-util-types": "^2.0.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-markdown": "^10.1.0", - "rehype-katex": "^7.0.1", - "remark-gfm": "^4.0.1", - "remark-math": "^6.0.0", "shiki": "^4.3.1" }, "devDependencies": { diff --git a/packages/client/ui-primitives/src/markdown/MarkdownText.tsx b/packages/client/ui-primitives/src/markdown/MarkdownText.tsx index 6450de55e2..bb4b62514c 100644 --- a/packages/client/ui-primitives/src/markdown/MarkdownText.tsx +++ b/packages/client/ui-primitives/src/markdown/MarkdownText.tsx @@ -1,155 +1,164 @@ -import { isValidElement, useMemo } from 'react' -import ReactMarkdown from 'react-markdown' -import type { Components, UrlTransform } from 'react-markdown' -import rehypeKatex from 'rehype-katex' -import remarkGfm from 'remark-gfm' -import remarkMath from 'remark-math' -import { CodeBlock } from './CodeBlock.tsx' -import { remarkMathCompatibility } from './remarkMathCompatibility.ts' +/** + * Untrusted assistant-Markdown renderer over the direct mdast pipeline: + * `parse.ts` grammars, the incremental streaming parser, and `render.tsx`. + * While a message streams, all but the trailing two blocks freeze as cached + * React elements and only the source tail behind them re-parses per chunk, + * so per-chunk work tracks the tail size instead of the whole reply. Frozen + * blocks keep their source-offset keys when they cross the freeze boundary, + * so React reconciles instead of remounting. Known deviation while + * streaming: a reference-style link or footnote whose definition sits on the + * other side of the freeze boundary renders literally until the settled + * full parse self-heals it. + */ + +import { memo, useMemo, useRef } from 'react' +import type { ReactNode } from 'react' +import { IncrementalMarkdownParser } from './incremental.ts' +import { parseGfm, parseGfmWithMath } from './parse.ts' +import { + collectReferenceTargets, createReferenceTargets, renderBlocks, renderFootnoteSection, + wrapBlockChildren, +} from './render.tsx' +import type { MarkdownCodeLabels, MarkdownRenderContext, ReferenceTargets } from './render.tsx' import 'katex/dist/katex.min.css' import css from './MarkdownText.module.css' -const streamingRemarkPlugins = [remarkGfm] -const settledRemarkPlugins = [ - remarkGfm, - remarkMathCompatibility, - remarkMath, -] -const settledRehypePlugins = [rehypeKatex] +export type { MarkdownCodeLabels } from './render.tsx' -function sanitizeUrl(url: string): string { - try { - switch (new URL(url).protocol) { - case 'http:': - case 'https:': - case 'mailto:': - return url - default: - return '' +/** One settled full render: parse with math, resolve references, append the footnote section. */ +function renderSettled(text: string, codeLabels: MarkdownCodeLabels | undefined): ReactNode[] { + const root = parseGfmWithMath(text) + const targets = createReferenceTargets() + collectReferenceTargets(root.children, targets) + const context: MarkdownRenderContext = { + streaming: false, + codeLabels, + targets, + footnoteOrder: [], + footnoteCounts: new Map(), + } + const blocks = wrapBlockChildren( + renderBlocks(root.children.map((node, index) => ({ node, key: index })), context), + false, + ) + const section = renderFootnoteSection(context) + return section === null ? blocks : [...blocks, '\n', section] +} + +/** + * Streaming render state for one growing message: the incremental parser, + * the frozen blocks' cached elements, and the reference/footnote state their + * rendering consumed (footnote numbering assigned to frozen references is + * final, so the tail continues from a copy of it each frame). + */ +class StreamingRenderer { + private readonly parser = new IncrementalMarkdownParser(parseGfm) + private generation = -1 + private frozenCount = 0 + private frozenElements: ReactNode[] = [] + private frozenTargets: ReferenceTargets = createReferenceTargets() + private frozenFootnoteOrder: string[] = [] + private frozenFootnoteCounts = new Map() + private lastText: string | null = null + private lastRendered: ReactNode[] = [] + + /** @param codeLabels - Fence copy labels baked into cached elements; the owner replaces the renderer when they change. */ + constructor(private readonly codeLabels: MarkdownCodeLabels | undefined) {} + + /** + * Render the current accumulated text. Idempotent per text value, so React + * may re-execute the calling render freely. + * @param text - The full accumulated markdown source. + * @returns Frozen elements, re-rendered tail, and the footnote section. + */ + render(text: string): ReactNode[] { + if (text === this.lastText) return this.lastRendered + const { frozen, tail, generation } = this.parser.update(text) + if (generation !== this.generation) { + this.generation = generation + this.frozenCount = 0 + this.frozenElements = [] + this.frozenTargets = createReferenceTargets() + this.frozenFootnoteOrder = [] + this.frozenFootnoteCounts = new Map() } - } catch { - return '' + const newlyFrozen = frozen.slice(this.frozenCount) + collectReferenceTargets(newlyFrozen.map(block => block.node), this.frozenTargets) + // Targets visible this frame: everything frozen so far plus the current + // tail parse — a newly frozen block's references resolved against the + // same parse tree its definitions came from. + const frameTargets: ReferenceTargets = { + definitions: new Map(this.frozenTargets.definitions), + footnotes: new Map(this.frozenTargets.footnotes), + } + collectReferenceTargets(tail.map(block => block.node), frameTargets) + if (newlyFrozen.length > 0) { + const frozenContext: MarkdownRenderContext = { + streaming: true, + codeLabels: this.codeLabels, + targets: frameTargets, + footnoteOrder: this.frozenFootnoteOrder, + footnoteCounts: this.frozenFootnoteCounts, + } + // Separator newlines are cached alongside the elements so the + // assembled children match the settled pipeline's block wrapping. + const batch = [...this.frozenElements] + for (const element of renderBlocks(newlyFrozen, frozenContext)) { + if (batch.length > 0) batch.push('\n') + batch.push(element) + } + this.frozenElements = batch + this.frozenCount = frozen.length + } + const tailContext: MarkdownRenderContext = { + streaming: true, + codeLabels: this.codeLabels, + targets: frameTargets, + footnoteOrder: [...this.frozenFootnoteOrder], + footnoteCounts: new Map(this.frozenFootnoteCounts), + } + const children = [...this.frozenElements] + for (const element of renderBlocks(tail, tailContext)) { + if (children.length > 0) children.push('\n') + children.push(element) + } + const section = renderFootnoteSection(tailContext) + if (section !== null) children.push('\n', section) + this.lastText = text + this.lastRendered = children + return this.lastRendered } } -const safeUrl: UrlTransform = url => sanitizeUrl(url) - -/** Copy-button labels forwarded to fence CodeBlocks (this package is cordis-free, so copy arrives via props). */ -export interface MarkdownCodeLabels { - /** Copy-button idle label. */ - copyLabel?: string | undefined - /** Copy-button label during the post-copy confirmation window. */ - copiedLabel?: string | undefined -} - -function remoteImageUrl(url: string): string | undefined { - try { - const protocol = new URL(url).protocol - return protocol === 'http:' || protocol === 'https:' ? url : undefined - } catch { - return undefined - } -} - -/** Build the component table; while `streaming`, fences render the plain arm (see CodeBlock). */ -function buildComponents(streaming: boolean, codeLabels?: MarkdownCodeLabels): Components { - return { - a: ({ href = '', children }) => { - const safeHref = sanitizeUrl(href) - if (safeHref === '') return <>{children} - const external = ['http:', 'https:'].includes(new URL(safeHref).protocol) - return ( - - {children} - - ) - }, - img: ({ alt = '', src = '' }) => { - const imageSrc = remoteImageUrl(src) - if (imageSrc === undefined) return {alt} - return ( - {alt} - ) - }, - table: ({ children }) => ( -
- {children}
-
- ), - // Fenced blocks route through the shared CodeBlock (shiki for registered - // grammars, identical-geometry plain fallback for unknown/absent - // languages); inline code keeps the default path (the :not(pre) - // rule styles it). While the message streams, the fence renders the - // plain arm — retokenizing a growing fence on every chunk is quadratic - // main-thread work; the finalize swap highlights it once. - pre: ({ children }) => { - // The markdown pipeline always hands `pre` its single `code` element; - // the undefined arm guards a react-markdown representation change. - /* v8 ignore next 2 */ - const child = isValidElement<{ className?: string; children?: unknown }>(children) ? children : undefined - const raw = child?.props.children - // A fence whose content isn't one plain string (e.g. an empty fence) - // keeps the stock
 rather than guessing.
-      if (typeof raw !== 'string') return 
{children}
- const lang = /language-([\w-]+)/.exec(child?.props.className ?? '')?.[1] - return ( - - ) - }, - } -} - -const staticComponents = buildComponents(false) -const streamingComponents = buildComponents(true) - /** * Render untrusted assistant-authored Markdown as semantic React elements. * @param props - Markdown source text preserved by the session projection; - * `streaming` renders fences and TeX plain (highlighting and KaTeX land on the finalize swap); - * `codeLabels` forwards localized copy-button labels to fence CodeBlocks — - * pass a reference-stable object (memoized per locale revision), because the - * component table memoizes on its identity and a fresh literal per render - * would rebuild it every streaming chunk. + * `streaming` renders fences and TeX plain (highlighting and KaTeX land on + * the finalize swap) and parses incrementally across chunks; `codeLabels` + * forwards localized copy-button labels to fence CodeBlocks — pass a + * reference-stable object (memoized per locale revision), because a new + * identity discards the streaming render cache mid-message. * @returns A GFM document with TeX math rendered through KaTeX; raw HTML, * relative links, and unsafe protocols are disabled, while absolute HTTP(S) * images render directly. */ -export function MarkdownText({ text, streaming = false, codeLabels }: { +export const MarkdownText = memo(function MarkdownText({ text, streaming = false, codeLabels }: { text: string streaming?: boolean codeLabels?: MarkdownCodeLabels | undefined }) { - // The label-free tables stay module-level singletons so the common case - // keeps referential stability across renders without a hook. - const components = useMemo(() => { - if (codeLabels === undefined) return streaming ? streamingComponents : staticComponents - return buildComponents(streaming, codeLabels) - }, [streaming, codeLabels]) - return ( -
- - {text} - -
- ) -} + const streamRef = useRef(null) + const streamLabelsRef = useRef(codeLabels) + const children = useMemo(() => { + if (!streaming) { + streamRef.current = null + return renderSettled(text, codeLabels) + } + if (streamRef.current === null || streamLabelsRef.current !== codeLabels) { + streamRef.current = new StreamingRenderer(codeLabels) + streamLabelsRef.current = codeLabels + } + return streamRef.current.render(text) + }, [text, streaming, codeLabels]) + return
{children}
+}) diff --git a/packages/client/ui-primitives/src/markdown/cjkFriendlyStrong.ts b/packages/client/ui-primitives/src/markdown/cjkFriendlyStrong.ts new file mode 100644 index 0000000000..aba740d058 --- /dev/null +++ b/packages/client/ui-primitives/src/markdown/cjkFriendlyStrong.ts @@ -0,0 +1,83 @@ +/** Let asterisk strong emphasis close after punctuation when CJK prose continues without whitespace. */ + +import { attention } from 'micromark-core-commonmark' +import { unicodePunctuation } from 'micromark-util-character' +import { classifyCharacter } from 'micromark-util-classify-character' +import { codes, constants } from 'micromark-util-symbol' +import type { Construct, Extension, State, Tokenizer } from 'micromark-util-types' + +const cjkCharacter = new RegExp([ + '\\p{Script_Extensions=Han}', + '\\p{Script_Extensions=Hiragana}', + '\\p{Script_Extensions=Katakana}', + '\\p{Script_Extensions=Hangul}', + '\\p{Script_Extensions=Bopomofo}', +].join('|'), 'u') + +function isCjkCharacter(code: number | null): boolean { + return code !== null && code >= 0 && cjkCharacter.test(String.fromCodePoint(code)) +} + +const tokenizeCjkFriendlyAttention: Tokenizer = function (effects, ok, nok) { + const configuredAttentionMarkers = this.parser.constructs.attentionMarkers.null + if (configuredAttentionMarkers === undefined) { + throw new Error('micromark CommonMark attention markers are unavailable') + } + const attentionMarkers = configuredAttentionMarkers + const previous = this.previous + const before = classifyCharacter(previous) + let marker: number | null = codes.eof + + return start + + function start(code: number | null): State | undefined { + /* v8 ignore next -- this text construct is dispatched only for an asterisk. */ + if (code !== codes.asterisk) return nok(code) + marker = code + effects.enter('attentionSequence') + return inside(code) + } + + function inside(code: number | null): State | undefined { + if (code === marker) { + effects.consume(code) + return inside + } + + const token = effects.exit('attentionSequence') + const after = classifyCharacter(code) + const open = !after || (after === constants.characterGroupPunctuation && Boolean(before)) + || attentionMarkers.includes(code) + const commonMarkClose = !before + || (before === constants.characterGroupPunctuation && Boolean(after)) + || attentionMarkers.includes(previous) + const markerCount = token.end.offset - token.start.offset + const cjkStrongClose = markerCount >= 2 + && unicodePunctuation(previous) + && isCjkCharacter(code) + const close = commonMarkClose || cjkStrongClose + + token._open = open + token._close = close + return ok(code) + } +} + +const cjkFriendlyAttention: Construct = { + name: 'cjkFriendlyAttention', + resolveAll: attention.resolveAll, + tokenize: tokenizeCjkFriendlyAttention, +} + +const cjkFriendlyStrongExtension: Extension = { + text: { [codes.asterisk]: cjkFriendlyAttention }, +} + +/** + * Extend CommonMark asterisk strong emphasis for punctuation-delimited CJK + * prose, as a micromark syntax extension for `fromMarkdown`. + * @returns The micromark syntax extension. + */ +export function cjkFriendlyStrong(): Extension { + return cjkFriendlyStrongExtension +} diff --git a/packages/client/ui-primitives/src/markdown/incremental.ts b/packages/client/ui-primitives/src/markdown/incremental.ts new file mode 100644 index 0000000000..3947d4801f --- /dev/null +++ b/packages/client/ui-primitives/src/markdown/incremental.ts @@ -0,0 +1,130 @@ +/** + * Incremental block-level markdown parsing for an append-only text stream. + * + * Re-parsing the whole accumulated document on every streaming chunk is + * quadratic in the final reply length. CommonMark block parsing is line-based + * and appended text can only reshape the parse frontier — the last top-level + * block (a paragraph becoming a setext heading or a table, a list continuing + * after a blank line, an unclosed fence swallowing lines) — so earlier blocks + * are final. This parser therefore freezes all but the trailing + * {@link UNSTABLE_TAIL_BLOCKS} blocks and re-parses only the source tail + * behind them: each source region is parsed O(1) times over the stream + * instead of once per chunk. + * + * The freeze boundary comes from the parser's own `position` offsets, never + * from custom source scanning. The cut sits at the *end offset* of the last + * frozen block (not the next block's start): a following block's start offset + * excludes up to three spaces of insignificant leading indentation, which is + * harmless to drop, but cutting at the previous end also keeps the + * inter-block blank lines in the tail so the sliced source stays verbatim. + * + * Known deviation, shared with any prefix-freeze scheme: micromark resolves + * reference-style links and footnotes document-wide at parse time, so a + * reference whose definition lands on the other side of the freeze boundary + * renders literally until the settled full parse self-heals it. + */ + +import type { Root, RootContent } from 'mdast' + +/** + * Trailing blocks kept unstable. Appended text reshapes at most the last + * block; the second-to-last is retained as safety margin so a freeze decision + * never has to reason about the parse frontier. + */ +const UNSTABLE_TAIL_BLOCKS = 2 + +/** A top-level mdast block plus a render key that is stable across chunks. */ +export interface PositionedBlock { + /** The parsed block. Positions inside it are relative to its parse slice. */ + readonly node: RootContent + /** + * The block's start offset in the full source text. Stable from the frame + * a block first appears through freezing, so React reconciles rather than + * remounts when a block crosses the freeze boundary. + */ + readonly key: number +} + +/** One {@link IncrementalMarkdownParser.update} result. */ +export interface IncrementalBlocks { + /** Blocks that can no longer change; grows monotonically per generation. */ + readonly frozen: readonly PositionedBlock[] + /** The re-parsed unstable tail (at most {@link UNSTABLE_TAIL_BLOCKS} blocks plus growth). */ + readonly tail: readonly PositionedBlock[] + /** Bumped whenever non-append input discards the frozen prefix; callers drop caches keyed on it. */ + readonly generation: number +} + +/** + * A block's render key: its absolute source start offset. A position-less + * node (a grammar is free to omit positions) falls back to a negative + * list-index key — unique within one update's tail, which is the only place + * the fallback can occur: freezing requires the cut block's position, so a + * position-less parse keeps every block in the tail (real grammars always + * stamp positions and never take this path). + */ +function blockKey(node: RootContent, base: number, index: number): number { + const offset = node.position?.start.offset + return offset === undefined ? -(index + 1) : base + offset +} + +/** + * Append-only incremental parser over a caller-supplied grammar. One instance + * accumulates one streaming document; non-append input resets it. + */ +export class IncrementalMarkdownParser { + private prevText = '' + private tailStart = 0 + private frozen: PositionedBlock[] = [] + private generation = 0 + private cached: IncrementalBlocks | null = null + + /** @param parse - Grammar shared with whatever renders the blocks, so boundaries agree. */ + constructor(private readonly parse: (text: string) => Root) {} + + /** + * Fold the current accumulated text and return the frozen/tail split. + * Idempotent for identical input (the previous result is returned as-is), + * so callers may invoke it from render paths that re-execute. + * @param text - The full accumulated markdown source. + * @returns Frozen and tail blocks with stream-stable render keys. + */ + update(text: string): IncrementalBlocks { + if (this.cached !== null && text === this.prevText) return this.cached + // Deliberate O(prefix) memcmp per update: sound divergence detection has + // to verify the whole retained prefix, and startsWith compares bytes two + // orders of magnitude faster than parsing them — the cost this class + // exists to remove. Passing append/reset deltas instead would push + // append bookkeeping across the session-projection seam for a check + // that stays sub-millisecond at realistic reply sizes. + if (!text.startsWith(this.prevText)) { + this.prevText = '' + this.tailStart = 0 + this.frozen = [] + this.generation += 1 + } + this.prevText = text + const base = this.tailStart + const blocks = this.parse(text.slice(base)).children + let firstUnstable = Math.max(0, blocks.length - UNSTABLE_TAIL_BLOCKS) + if (firstUnstable > 0) { + const cutEnd = blocks[firstUnstable - 1]?.position?.end.offset + if (cutEnd === undefined) { + // A grammar that omits positions leaves nothing to cut at; keep the + // whole parse in the tail rather than guessing a boundary. + firstUnstable = 0 + } else { + for (const node of blocks.slice(0, firstUnstable)) { + this.frozen.push({ node, key: blockKey(node, base, this.frozen.length) }) + } + this.tailStart = base + cutEnd + } + } + const tail = blocks.slice(firstUnstable).map((node, index) => ({ + node, + key: blockKey(node, base, index), + })) + this.cached = { frozen: [...this.frozen], tail, generation: this.generation } + return this.cached + } +} diff --git a/packages/client/ui-primitives/src/markdown/katex.tsx b/packages/client/ui-primitives/src/markdown/katex.tsx new file mode 100644 index 0000000000..45c1226b83 --- /dev/null +++ b/packages/client/ui-primitives/src/markdown/katex.tsx @@ -0,0 +1,90 @@ +/** + * TeX-to-React via KaTeX, replicating the rehype-katex pipeline this renderer + * replaced: the same three-arm error chain (strict render, `strict: 'ignore'` + * retry, error span) and a DOM-identical element tree, so settled math keeps + * its exact markup. KaTeX emits an HTML string; the browser's own HTML parser + * (`DOMParser`, applying the spec's SVG/MathML foreign-content attribute + * adjustments KaTeX output relies on) turns it into a tree this module maps + * onto React elements — KaTeX output is a static span/MathML/SVG vocabulary + * with no raw user HTML, the same trust shiki's tree gets in CodeBlock. + * + * React 18 has no MathML support, so the `.katex-mathml` subtree's elements + * land in the HTML namespace — exactly as they did under the replaced + * hast-util-to-jsx-runtime pipeline. The visual arm is the `.katex-html` + * span tree; the MathML arm serves assistive technology, which reads it by + * tag name regardless of namespace. + */ + +import { createElement } from 'react' +import type { CSSProperties, ReactNode } from 'react' +import katex from 'katex' + +/** + * Convert one inline `style` attribute string into React's style object. + * KaTeX emits only plain kebab-case declarations (no custom properties and no + * nameless declarations), so camel-casing the property is the whole mapping. + */ +function styleObject(css: string): CSSProperties { + const style: Record = {} + for (const declaration of css.split(';')) { + const colon = declaration.indexOf(':') + if (colon === -1) continue + const name = declaration.slice(0, colon).trim() + const key = name.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase()) + style[key] = declaration.slice(colon + 1).trim() + } + return style +} + +/** Map one parsed DOM node onto a React element (text nodes pass through). */ +function domToReact(node: ChildNode, key: number): ReactNode { + if (node.nodeType === Node.TEXT_NODE) return node.textContent + /* v8 ignore next 2 -- KaTeX output holds only elements and text; other + node kinds cannot appear in its serialized vocabulary. */ + if (node.nodeType !== Node.ELEMENT_NODE) return null + const element = node as Element + const props: Record = { key } + for (const attribute of element.attributes) { + if (attribute.name === 'class') props['className'] = attribute.value + else if (attribute.name === 'style') props['style'] = styleObject(attribute.value) + else props[attribute.name] = attribute.value + } + const children = [...element.childNodes].map(domToReact) + return children.length === 0 + ? createElement(element.localName, props) + : createElement(element.localName, props, ...children) +} + +/** + * Render TeX source to React elements through KaTeX. + * @param value - The TeX source (math node value; fenced `math` blocks append + * their trailing newline to match the replaced pipeline's text extraction). + * @param displayMode - Display (block) versus inline rendering. + * @returns KaTeX's element tree, or the error span when the source does not + * parse (colored with KaTeX's stock `errorColor`, matching rehype-katex). + */ +export function renderTexToReact(value: string, displayMode: boolean): ReactNode { + let html: string + try { + html = katex.renderToString(value, { displayMode, throwOnError: true }) + } catch (error) { + try { + html = katex.renderToString(value, { displayMode, strict: 'ignore', throwOnError: false }) + } catch { + // KaTeX renders ParseErrors itself under throwOnError: false; only its + // internal errors reach here, so mirror rehype-katex's manual span. + /* v8 ignore next 8 */ + return ( + + {value} + + ) + } + } + const parsed = new DOMParser().parseFromString(html, 'text/html') + return [...parsed.body.childNodes].map(domToReact) +} diff --git a/packages/client/ui-primitives/src/markdown/remarkMathCompatibility.ts b/packages/client/ui-primitives/src/markdown/mathCompatibility.ts similarity index 95% rename from packages/client/ui-primitives/src/markdown/remarkMathCompatibility.ts rename to packages/client/ui-primitives/src/markdown/mathCompatibility.ts index dcd8c32362..3edd9d1e63 100644 --- a/packages/client/ui-primitives/src/markdown/remarkMathCompatibility.ts +++ b/packages/client/ui-primitives/src/markdown/mathCompatibility.ts @@ -8,10 +8,6 @@ import type { Construct, Extension, Previous, State, Tokenizer } from 'micromark // oxlint-disable typescript/no-this-alias -- micromark binds tokenizer context only on the outer callback. -interface RemarkProcessor { - data(): { micromarkExtensions?: Extension[] } -} - const previousBackslash: Previous = function (code) { if (code !== codes.backslash) return true const tail = this.events.at(-1) @@ -342,12 +338,12 @@ const backslashMath: Extension = { } /** - * Add TeX backslash delimiters and same-line display-dollar blocks for remark-math. - * The same processor must register remark-math to compile the emitted math tokens. - * @returns Nothing. + * TeX backslash delimiters and same-line display-dollar blocks as a micromark + * syntax extension reusing `micromark-extension-math`'s token vocabulary; the + * caller must also register `math()` on the same parse so the emitted tokens + * compile to standard math nodes. + * @returns The micromark syntax extension. */ -export function remarkMathCompatibility(this: RemarkProcessor): undefined { - const data = this.data() - const extensions = data.micromarkExtensions ?? (data.micromarkExtensions = []) - extensions.push(backslashMath) +export function mathCompatibility(): Extension { + return backslashMath } diff --git a/packages/client/ui-primitives/src/markdown/parse.ts b/packages/client/ui-primitives/src/markdown/parse.ts new file mode 100644 index 0000000000..50f8f179b9 --- /dev/null +++ b/packages/client/ui-primitives/src/markdown/parse.ts @@ -0,0 +1,44 @@ +/** + * The markdown renderer's two mdast grammars, one per rendering arm. Each + * arm is internally consistent — the incremental tail parses, the one-shot + * parses, and the plain-text projection of a given grammar always agree on + * where blocks start and end — and the settled grammar is the streaming one + * plus the math extensions, so the arms differ only where TeX delimiters + * begin a math construct (a `$$` block is a paragraph while streaming and a + * math block once settled, by design). + */ + +import type { Root } from 'mdast' +import { fromMarkdown } from 'mdast-util-from-markdown' +import { gfmFromMarkdown } from 'mdast-util-gfm' +import { mathFromMarkdown } from 'mdast-util-math' +import { gfm } from 'micromark-extension-gfm' +import { math } from 'micromark-extension-math' +import { cjkFriendlyStrong } from './cjkFriendlyStrong.ts' +import { mathCompatibility } from './mathCompatibility.ts' + +/** + * Parse GFM markdown (the streaming arm's grammar: no math, so incomplete + * TeX never flashes KaTeX errors mid-stream). + * @param text - Markdown source. + * @returns The mdast root. + */ +export function parseGfm(text: string): Root { + return fromMarkdown(text, { + extensions: [gfm(), cjkFriendlyStrong()], + mdastExtensions: [gfmFromMarkdown()], + }) +} + +/** + * Parse GFM markdown plus TeX math with the compatibility delimiters + * (the settled arm's grammar). + * @param text - Markdown source. + * @returns The mdast root. + */ +export function parseGfmWithMath(text: string): Root { + return fromMarkdown(text, { + extensions: [gfm(), cjkFriendlyStrong(), mathCompatibility(), math()], + mdastExtensions: [gfmFromMarkdown(), mathFromMarkdown()], + }) +} diff --git a/packages/client/ui-primitives/src/markdown/plain-text.ts b/packages/client/ui-primitives/src/markdown/plain-text.ts index af449dc774..6797fdec6e 100644 --- a/packages/client/ui-primitives/src/markdown/plain-text.ts +++ b/packages/client/ui-primitives/src/markdown/plain-text.ts @@ -1,12 +1,12 @@ /** * Markdown-to-plain-text projection for compact summaries and labels. - * Parsing shares the renderer's GFM grammar; raw HTML stays literal, links - * keep their labels, images keep alt text, and code keeps its source text. + * Parsing shares the renderer's streaming GFM grammar ({@link parseGfm}), so + * the projection strips exactly the markup the renderer would draw; raw HTML + * stays literal, links keep their labels, images keep alt text, and code + * keeps its source text. */ -import { fromMarkdown } from 'mdast-util-from-markdown' -import { gfmFromMarkdown } from 'mdast-util-gfm' -import { gfm } from 'micromark-extension-gfm' +import { parseGfm } from './parse.ts' /** Amount of parsed Markdown content returned by the extractor. */ export type MarkdownPlainTextMode = 'all' | 'first-line' | 'first-paragraph' @@ -108,10 +108,7 @@ export function extractMarkdownPlainText( options: MarkdownPlainTextOptions = {}, ): string { const { mode = 'all' } = options - const root = fromMarkdown(markdown, { - extensions: [gfm()], - mdastExtensions: [gfmFromMarkdown()], - }) as MarkdownNode + const root = parseGfm(markdown) as MarkdownNode const all = fullText(root) switch (mode) { case 'all': diff --git a/packages/client/ui-primitives/src/markdown/render.tsx b/packages/client/ui-primitives/src/markdown/render.tsx new file mode 100644 index 0000000000..4ac8b4dbc1 --- /dev/null +++ b/packages/client/ui-primitives/src/markdown/render.tsx @@ -0,0 +1,544 @@ +/** + * Direct mdast→React markdown renderer. Replaces the react-markdown / + * remark-rehype pipeline with one switch over parsed nodes so streaming can + * cache frozen blocks as React elements; the rendered DOM is pinned + * byte-for-byte by `tests/fixtures/markdown-dom` and must not drift. + * + * Untrusted-output policy (unchanged from the replaced pipeline): link and + * image destinations pass a protocol allowlist, images additionally require + * absolute HTTP(S), raw HTML renders as literal text (no HTML enters the + * DOM), and KaTeX runs without trusted commands. Fragment-anchor URLs fail + * the allowlist, so footnote references and back-references render as plain + * text rather than in-page links. + * + * Merge-extensible node unions fall through the documented default (render + * nothing) rather than ending in assertNever: grammars registered elsewhere + * may add node types this renderer has no mapping for. + */ + +import { Fragment, createElement } from 'react' +import type { Key, ReactNode } from 'react' +import type * as Md from 'mdast' +import type {} from 'mdast-util-math' +import { normalizeUri } from 'micromark-util-sanitize-uri' +import { CodeBlock } from './CodeBlock.tsx' +import { renderTexToReact } from './katex.tsx' +import type { PositionedBlock } from './incremental.ts' +import css from './MarkdownText.module.css' + +/** Copy-button labels forwarded to fence CodeBlocks (this package is cordis-free, so copy arrives via props). */ +export interface MarkdownCodeLabels { + /** Copy-button idle label. */ + copyLabel?: string | undefined + /** Copy-button label during the post-copy confirmation window. */ + copiedLabel?: string | undefined +} + +function sanitizeUrl(url: string): string { + try { + switch (new URL(url).protocol) { + case 'http:': + case 'https:': + case 'mailto:': + return url + default: + return '' + } + } catch { + // Relative and otherwise unparsable destinations are disallowed alongside + // disallowed protocols; new URL() has no other failure mode for strings. + return '' + } +} + +function remoteImageUrl(url: string): string | undefined { + try { + const protocol = new URL(url).protocol + return protocol === 'http:' || protocol === 'https:' ? url : undefined + } catch { + // Same single failure mode as above: not an absolute URL. + return undefined + } +} + +/** Link/image reference targets collected from a document (first definition per identifier wins, as in CommonMark). */ +export interface ReferenceTargets { + /** Link/image definitions keyed by upper-cased identifier. */ + definitions: Map + /** Footnote definitions keyed by upper-cased identifier. */ + footnotes: Map +} + +/** + * Create an empty {@link ReferenceTargets}. + * @returns Fresh empty maps. + */ +export function createReferenceTargets(): ReferenceTargets { + return { definitions: new Map(), footnotes: new Map() } +} + +/** + * Record every definition and footnote definition under `nodes` into + * `targets`, depth-first, keeping the first definition per identifier. + * @param nodes - Subtrees to walk (top-level blocks or any nested children). + * @param targets - Accumulator, typically shared across incremental segments. + */ +export function collectReferenceTargets( + nodes: readonly Md.RootContent[], + targets: ReferenceTargets, +): void { + for (const node of nodes) { + if (node.type === 'definition') { + const id = node.identifier.toUpperCase() + if (!targets.definitions.has(id)) targets.definitions.set(id, node) + } else if (node.type === 'footnoteDefinition') { + const id = node.identifier.toUpperCase() + if (!targets.footnotes.has(id)) targets.footnotes.set(id, node) + } + if ('children' in node) collectReferenceTargets(node.children, targets) + } +} + +/** + * One render pass's state: immutable options and targets plus the footnote + * numbering accumulated in document order while references render. + */ +export interface MarkdownRenderContext { + /** Streaming arm: fences render plain and TeX stays literal. */ + readonly streaming: boolean + /** Localized fence copy-button labels. */ + readonly codeLabels: MarkdownCodeLabels | undefined + /** Reference targets visible to this pass. */ + readonly targets: ReferenceTargets + /** Footnote identifiers in first-reference order; a footnote's number is its 1-based index here. */ + readonly footnoteOrder: string[] + /** References rendered per identifier; drives the section's back-reference count. */ + readonly footnoteCounts: Map +} + +/** + * Render top-level blocks. Nodes that render nothing (definitions, unmapped + * types) are dropped rather than kept as null placeholders, matching the + * replaced pipeline's child lists so separator newlines land identically. + * @param blocks - Blocks with their stream-stable render keys. + * @param context - The pass state; footnote numbering mutates in document order. + * @returns One React node per rendered block. + */ +export function renderBlocks( + blocks: readonly PositionedBlock[], + context: MarkdownRenderContext, +): ReactNode[] { + return blocks + .map(block => renderNode(block.node, block.key, context)) + .filter(element => element !== null) +} + +/** + * Interleave the newline text nodes the replaced pipeline emitted between + * block-level children. They are invisible between elements but coalesce + * into adjacent literal raw-HTML text, where the DOM parity fixtures pin + * them. + * @param elements - Rendered block children with empty renders already dropped. + * @param edges - Also emit the leading and trailing newline (hast's loose wrap). + * @returns The interleaved children. + */ +export function wrapBlockChildren(elements: readonly ReactNode[], edges: boolean): ReactNode[] { + const wrapped: ReactNode[] = [] + for (const element of elements) { + if (edges || wrapped.length > 0) wrapped.push('\n') + wrapped.push(element) + } + if (edges && elements.length > 0) wrapped.push('\n') + return wrapped +} + +/** + * A block child rendered for a parent that must tell paragraphs apart from + * other blocks (list items unwrap them when tight; footnote bodies receive + * their back-references inside the trailing paragraph). + */ +type BlockEntry = { paragraph: ReactNode[] } | { element: ReactNode } + +/** Render container children into {@link BlockEntry} values, dropping empty renders. */ +function renderBlockEntries( + blocks: readonly Md.RootContent[], + context: MarkdownRenderContext, +): BlockEntry[] { + const entries: BlockEntry[] = [] + for (const [index, block] of blocks.entries()) { + if (block.type === 'paragraph') { + entries.push({ paragraph: renderChildren(block.children, context) }) + } else { + const element = renderNode(block, index, context) + if (element !== null) entries.push({ element }) + } + } + return entries +} + +function renderChildren( + nodes: readonly Md.RootContent[], + context: MarkdownRenderContext, +): ReactNode[] { + return nodes.map((node, index) => renderNode(node, index, context)) +} + +function renderNode(node: Md.RootContent, key: Key, context: MarkdownRenderContext): ReactNode { + switch (node.type) { + case 'text': + return node.value + case 'paragraph': + return

{renderChildren(node.children, context)}

+ case 'heading': + return createElement(`h${node.depth}`, { key }, ...renderChildren(node.children, context)) + case 'blockquote': + return ( +
+ {wrapBlockChildren(renderChildren(node.children, context).filter(child => child !== null), true)} +
+ ) + case 'thematicBreak': + return
+ case 'break': + // The replaced pipeline emitted a newline text node after each
. + return
{'\n'}
+ case 'strong': + return {renderChildren(node.children, context)} + case 'emphasis': + return {renderChildren(node.children, context)} + case 'delete': + return {renderChildren(node.children, context)} + case 'inlineCode': { + // Parity with mdast-util-to-hast: inline code renders line endings as spaces. + const value = node.value.replace(/\r?\n|\r/g, ' ') + // An inline-code token that is entirely an absolute HTTP(S) URL keeps + // its code chrome and gains the same safe external anchor as a link; + // commands, partial URLs, and other schemes stay inert. The value is + // authored text, not a parsed destination, so no normalizeUri: port, + // path, and query render unchanged. + const href = inlineCodeHttpUrl(value) + return {href === undefined ? value : renderSafeLink(href, [value], 'link')} + } + case 'html': + // No HTML parser enters the pipeline: raw HTML stays literal text. + return node.value + case 'code': + return renderCode(node, key, context) + case 'math': + return {renderTexToReact(node.value, true)} + case 'inlineMath': + return {renderTexToReact(node.value, false)} + case 'list': + return renderList(node, key, context) + case 'listItem': + // Reachable only in hand-built trees: the grammar emits items inside lists. + return renderListItem(node, listItemLoose(node), key, context) + case 'table': + return renderTable(node, key, context) + case 'link': + return renderAnchor(node.url, renderChildren(node.children, context), key) + case 'linkReference': + return renderLinkReference(node, key, context) + case 'image': + return renderImage(node.url, node.alt ?? '', key) + case 'imageReference': + return renderImageReference(node, key, context) + case 'footnoteReference': + return renderFootnoteReference(node, key, context) + case 'definition': + case 'footnoteDefinition': + // Targets render elsewhere: definitions resolve references in place; + // footnote bodies render in the trailing section. + return null + default: + // Documented default for the merge-extensible union: node types without + // a mapping (tableRow/tableCell outside a table, frontmatter, future + // grammar contributions) render nothing. + return null + } +} + +function renderCode(node: Md.Code, key: Key, context: MarkdownRenderContext): ReactNode { + const language = node.lang ?? undefined + if (node.value === '') { + // Parity: the replaced pipeline kept the stock
 for an empty fence.
+    return (
+      
+        
+      
+ ) + } + // The replaced pipeline recovered the grammar id from the hast class with + // /language-([\w-]+)/, which truncates at the first non-word character. + const lang = language === undefined ? undefined : /^[\w-]+/.exec(language)?.[0] + if (!context.streaming && lang === 'math') { + // ```math fences render as display TeX once settled (rehype-katex parity); + // its text extraction saw the code block's trailing newline. + return {renderTexToReact(`${node.value}\n`, true)} + } + return ( + + ) +} + +/** A list is loose when it or any of its items is spread; every item then keeps its paragraphs. */ +function listLoose(list: Md.List): boolean { + return (list.spread ?? false) || list.children.some(listItemLoose) +} + +function listItemLoose(item: Md.ListItem): boolean { + return item.spread ?? item.children.length > 1 +} + +function renderList(node: Md.List, key: Key, context: MarkdownRenderContext): ReactNode { + const loose = listLoose(node) + const properties: { start?: number; className?: string } = {} + if (typeof node.start === 'number' && node.start !== 1) properties.start = node.start + if (node.children.some(item => typeof item.checked === 'boolean')) { + properties.className = 'contains-task-list' + } + return createElement( + node.ordered === true ? 'ol' : 'ul', + { key, ...properties }, + ...node.children.map((item, index) => renderListItem(item, loose, index, context)), + ) +} + +function renderListItem( + item: Md.ListItem, + loose: boolean, + key: Key, + context: MarkdownRenderContext, +): ReactNode { + const entries = renderBlockEntries(item.children, context) + const task = typeof item.checked === 'boolean' + if (task) { + const checkbox = + const head = entries[0] + if (head !== undefined && 'paragraph' in head) { + head.paragraph = head.paragraph.length > 0 ? [checkbox, ' ', ...head.paragraph] : [checkbox] + } else { + entries.unshift({ paragraph: [checkbox] }) + } + } + // Newline placement and tight-paragraph unwrapping mirror + // mdast-util-to-hast's list-item handler: a newline before every child + // except a tight leading paragraph, and after a trailing non-paragraph + // (or any trailing child when loose). + const parts: ReactNode[] = [] + for (const [index, entry] of entries.entries()) { + const isParagraph = 'paragraph' in entry + if (loose || index !== 0 || !isParagraph) parts.push('\n') + if (!isParagraph) parts.push(entry.element) + else if (loose) parts.push(

{entry.paragraph}

) + else parts.push({entry.paragraph}) + } + const tail = entries[entries.length - 1] + if (tail !== undefined && (loose || !('paragraph' in tail))) parts.push('\n') + return ( +
  • + {parts} +
  • + ) +} + +function renderTable(node: Md.Table, key: Key, context: MarkdownRenderContext): ReactNode { + const align = node.align ?? null + const [headRow, ...bodyRows] = node.children + return ( +
    + + {headRow !== undefined && {renderTableRow(headRow, 'th', align, 0, context)}} + {bodyRows.length > 0 && ( + + {bodyRows.map((row, index) => renderTableRow(row, 'td', align, index + 1, context))} + + )} +
    +
    + ) +} + +function renderTableRow( + row: Md.TableRow, + cellTag: 'th' | 'td', + align: readonly Md.AlignType[] | null, + key: Key, + context: MarkdownRenderContext, +): ReactNode { + // With column alignment present, every row renders exactly one cell per + // column, padding or truncating the row (mdast-util-to-hast parity). + const length = align === null ? row.children.length : align.length + const cells: ReactNode[] = [] + for (let index = 0; index < length; index++) { + const cell = row.children[index] + const alignValue = align?.[index] + cells.push(createElement( + cellTag, + // hast-util-to-jsx-runtime's default tableCellAlignToStyle turned the + // deprecated align attribute into an inline style; keep that DOM. + { key: index, style: alignValue == null ? undefined : { textAlign: alignValue } }, + ...(cell === undefined ? [] : renderChildren(cell.children, context)), + )) + } + return {cells} +} + +/** Anchor over an already-authored href: allowlisted or unwrapped, external links get the safe attributes. */ +function renderSafeLink(href: string, children: ReactNode[], key: Key): ReactNode { + const safeHref = sanitizeUrl(href) + if (safeHref === '') return {children} + const external = ['http:', 'https:'].includes(new URL(safeHref).protocol) + return ( + + {children} + + ) +} + +/** Anchor over a parsed markdown destination, which hast normalized before the allowlist saw it. */ +function renderAnchor(url: string, children: ReactNode[], key: Key): ReactNode { + return renderSafeLink(normalizeUri(url), children, key) +} + +/** + * The complete inline-code value when it is exactly an absolute HTTP(S) URL + * (no surrounding whitespace); anything else stays inert code. + */ +function inlineCodeHttpUrl(value: string): string | undefined { + if (value.trim() !== value) return undefined + try { + const protocol = new URL(value).protocol + return protocol === 'http:' || protocol === 'https:' ? value : undefined + } catch { + // Not an absolute URL at all — the only way new URL() rejects a string. + return undefined + } +} + +function renderImage(url: string, alt: string, key: Key): ReactNode { + const imageSrc = remoteImageUrl(sanitizeUrl(normalizeUri(url))) + if (imageSrc === undefined) { + return {alt} + } + return ( + {alt} + ) +} + +/** The bracketed source text a reference reverts to when its definition is missing. */ +function referenceSuffix(node: Md.LinkReference | Md.ImageReference): string { + if (node.referenceType === 'collapsed') return '][]' + if (node.referenceType === 'full') return `][${node.label ?? node.identifier}]` + return ']' +} + +function renderLinkReference( + node: Md.LinkReference, + key: Key, + context: MarkdownRenderContext, +): ReactNode { + const definition = context.targets.definitions.get(node.identifier.toUpperCase()) + const children = renderChildren(node.children, context) + if (definition === undefined) { + // The grammar only emits references whose definitions exist somewhere in + // the same parse, but incremental segments and hand-built trees may still + // present unresolved ones: revert to the bracketed source text. + return {'['}{children}{referenceSuffix(node)} + } + return renderAnchor(definition.url, children, key) +} + +function renderImageReference( + node: Md.ImageReference, + key: Key, + context: MarkdownRenderContext, +): ReactNode { + const definition = context.targets.definitions.get(node.identifier.toUpperCase()) + if (definition === undefined) return `![${node.alt ?? ''}${referenceSuffix(node)}` + return renderImage(definition.url, node.alt ?? '', key) +} + +function renderFootnoteReference( + node: Md.FootnoteReference, + key: Key, + context: MarkdownRenderContext, +): ReactNode { + const id = node.identifier.toUpperCase() + const seen = context.footnoteCounts.get(id) + if (seen === undefined) context.footnoteOrder.push(id) + context.footnoteCounts.set(id, (seen ?? 0) + 1) + // The in-page anchor fails the protocol allowlist, so only the numbered + // superscript renders (matching the replaced pipeline's unwrapped link). + return {String(context.footnoteOrder.indexOf(id) + 1)} +} + +/** + * Render the trailing footnote section for every footnote referenced during + * the pass, in first-reference order, with one plain-text back-reference + * marker per rendered reference. + * @param context - The pass state after all blocks rendered. + * @returns The section, or null when no referenced footnote has a definition. + */ +export function renderFootnoteSection(context: MarkdownRenderContext): ReactNode | null { + const items: ReactNode[] = [] + for (const id of context.footnoteOrder) { + const definition = context.targets.footnotes.get(id) + if (definition === undefined) continue + const count = context.footnoteCounts.get(id) ?? 0 + const backrefs: ReactNode[] = [] + for (let reference = 1; reference <= count; reference++) { + if (backrefs.length > 0) backrefs.push(' ') + backrefs.push('↩') + if (reference > 1) backrefs.push({String(reference)}) + } + const entries = renderBlockEntries(definition.children, context) + const tail = entries[entries.length - 1] + const body: ReactNode[] = entries.map((entry, index) => ( + 'paragraph' in entry + ? ( +

    + {entry.paragraph} + {entry === tail && <>{' '}{backrefs}} +

    + ) + : entry.element + )) + // Without a trailing paragraph the back-references join the block list + // itself (and pick up the wrap newlines), as in the replaced pipeline. + if (tail === undefined || !('paragraph' in tail)) body.push(...backrefs) + items.push( +
  • + {wrapBlockChildren(body, true)} +
  • , + ) + } + if (items.length === 0) return null + return ( +
    +

    Footnotes

    +
      {items}
    +
    + ) +} diff --git a/packages/client/ui-primitives/tests/fixtures/markdown-dom/blockquote-nested.settled.txt b/packages/client/ui-primitives/tests/fixtures/markdown-dom/blockquote-nested.settled.txt new file mode 100644 index 0000000000..d213078f94 --- /dev/null +++ b/packages/client/ui-primitives/tests/fixtures/markdown-dom/blockquote-nested.settled.txt @@ -0,0 +1,12 @@ +
    +
    +

    + #text "level one\nstill one" +

    +

    + #text "nested" +