mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge pull request #661 from deepseek-harness/code-mode-ui/dispatch-spill
feat(web): Code Mode UI completion — bounded sub-dispatch logs, shiki highlighting, trajectory/waterfall sub-calls
This commit is contained in:
@@ -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
|
||||
2026-07-26-code-dispatch-log-spill.md: eee8fb73b3f1ddba0a2da3ad5a9d2d4417d5951c
|
||||
2026-07-26-code-dispatch-log-spill.zh.md: 664a2aefcfef198d56809c289e10827a8084a06a
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: Spilling the durable copy of Code Mode sub-dispatch results
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-26-code-dispatch-log-spill.zh.md)
|
||||
|
||||
> Scope: the fourth PR of the Code Mode UI stack — bounding the `tool/code-dispatch` event's content with the existing spill machinery. The [host foundation note](2026-07-26-code-dispatch-ui-foundation.md) accepted the unbounded log deliberately and named this PR as the payoff point; the [live-parallel note](2026-07-26-code-mode-live-parallel-dispatch.md) settled the event pair this shaping hooks into.
|
||||
|
||||
## Problem
|
||||
|
||||
Since the full-content dispatch logging landed, a `run_code` program that reads a large file wrote the complete rendered text into the session log — uncapped and outside spill policy, while native results were bounded to `maxInlineBytes` before logging. The asymmetry was backwards: sub-calls (built for bulk data work) were precisely the calls most likely to carry huge results, and the JSONL grew by megabytes per such turn.
|
||||
|
||||
## Decision
|
||||
|
||||
**A log-shaping waterfall on the registry, and the spill policy as its first listener.**
|
||||
|
||||
- **Seam**: `tools/code-dispatch-log` — a scope-filtered waterfall the bridge runs (via the registry's PRIVATE `shapeDispatchLog` invoker, handed to the bridge as a capability closure in `RunCodeBridgeOptions` — the waterfall is the public seam, the invoker never widens the service surface; contained: a throwing listener falls back to the unshaped content, with total error formatting so a hostile thrown value cannot escape the containment) over each settled sub-dispatch before appending `tool/code-dispatch`. The payload (`CodeDispatchLog`) carries the outer execution, the hoisted `agent` routing key, the sub-call identity, and the default content — the RENDERED result projection a native `tool/result` would carry (the program itself received the structured `value`). Only the durable copy is shapeable; the model sees neither. Shaping runs OFF the program path as tracked side work, but bounded: past `maxParallelSubCalls` pending log tasks the ordered commit lane holds, so a slow spill backend backpressures the run instead of accumulating unbounded pending I/O; run settlement still drains every task inside the open turn.
|
||||
- **Policy**: `dsh-spill-policy` registers a second arm on the new seam sharing the exact replacement pipeline of its model-facing arm (same `maxInlineBytes` cap, same preview + locator + within-cap invariant, same best-effort fallbacks), with the artifact labeled `dispatch` under the sub-call id. UIs and replay read the full text through the spill artifact exactly as they do for spilled native results, so the native-parity rendering story survives bounding.
|
||||
- **One deliberate asymmetry**: the model-facing arm skips `read` (the `read → spill → read again` loop); the dispatch-log arm bounds `read` sub-calls too — a log copy is not model context, so the loop cannot happen, and `read` is precisely the tool that produces huge logs.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Bound inside the bridge with a plain cap (no spill).** Rejected: truncation without a locator loses data replay/UIs may need, and re-introduces the "truncated summary" degraded render path the stack removed.
|
||||
|
||||
**Spill inside the bridge directly (call `ctx.spillStore` from code-mode.ts).** Rejected: the registry would grow a hard dependency on the spill capability; the waterfall keeps the policy where every other spill decision lives, composable and disable-able (omitted `maxInlineBytes` still means a true no-op).
|
||||
|
||||
**Reuse `tools/post-execute` for nested calls instead of a new event.** Rejected: post-execute shapes the PROGRAM-facing result (nested calls deliberately skip it so programs get complete data); the durable copy needs its own decision point after the program has its value.
|
||||
|
||||
## Consequences
|
||||
|
||||
The session log is bounded again for Code Mode turns — the README's Known Limitations entry about uncapped dispatch logging is resolved and now points here. Old logs with oversized dispatch content still replay (the event shape is unchanged; only future appends shrink). The web UI renders spilled sub-call output as the preview + locator text through the identical native path, no special casing.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note:将 Code Mode 子分发结果的持久副本纳入 spill 机制
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-26-code-dispatch-log-spill.md) | 中文
|
||||
|
||||
> 范围:Code Mode UI 堆叠 PR(Pull Request)链的第四个 PR,即用既有的 spill 机制为 `tool/code-dispatch` 事件的内容施加边界。[宿主侧基础 Agent Note](2026-07-26-code-dispatch-ui-foundation.md)当初有意接受了不设上限的日志,并指明本 PR 就是兑现点;[实时并行 Agent Note](2026-07-26-code-mode-live-parallel-dispatch.md)敲定了本次整形所挂接的事件对。
|
||||
|
||||
## 问题
|
||||
|
||||
自携带完整内容的分发日志落地以来,读取大文件的 `run_code` 程序过去会把完整的渲染文本写进会话日志,不设上限、位于 spill 策略之外;而原生结果在记录之前就已被限制在 `maxInlineBytes` 以内。这种不对称的方向完全反了:子调用(本就为批量数据工作而设计)恰恰是最可能携带巨大结果的调用,而每个这样的轮次都会让 JSONL 增长数 MB。
|
||||
|
||||
## 决策
|
||||
|
||||
**在注册表上增设一个日志整形 waterfall(瀑布式事件),spill 策略作为其第一个监听器。**
|
||||
|
||||
- **Seam**:`tools/code-dispatch-log`,一个按作用域过滤的 waterfall,由桥接层在追加 `tool/code-dispatch` 之前对每个已结算的子分发运行(经由注册表的私有 `shapeDispatchLog` 调用器——作为能力闭包经 `RunCodeBridgeOptions` 交给桥接层;waterfall 才是公开 seam,调用器绝不加宽服务表面。故障被兜住:监听器抛出异常时回退到未整形的内容,并用全防御的错误格式化确保恶意抛出值无法逃出兜底)。载荷(`CodeDispatchLog`)携带外层执行、提升出来的 `agent` 路由键、子调用标识与默认内容——即原生 `tool/result` 所载的渲染后结果投影(程序本身收到的是结构化 `value`)。可整形的只有持久副本;模型两者都看不到。整形作为被跟踪的旁路工作在程序路径之外运行,但有界:待处理日志任务超过 `maxParallelSubCalls` 时有序提交车道会暂停,因此慢速 spill 后端会对整个 run 施加背压,而不是无限累积待完成 I/O;run 结算仍会在开放轮次内排空全部任务。
|
||||
- **策略**:`dsh-spill-policy` 在新 seam 上注册第二个分支,与其面向模型的分支共用一模一样的替换流水线(同样的 `maxInlineBytes` 上限、同样的预览 + 定位符 + 不超上限不变式、同样的尽力而为回退),产物以 `dispatch` 为标签,记在子调用 id 名下。UI 与回放通过 spill 产物读取全文,方式与读取被 spill 的原生结果完全相同,因此与原生同等保真的渲染在施加边界之后依然成立。
|
||||
- **一处有意的不对称**:面向模型的分支跳过 `read`(避免 `read → spill → read again` 循环);分发日志分支则连 `read` 子调用也施加边界:日志副本不是模型上下文,该循环因此不可能发生,而 `read` 恰恰是会产生巨大日志的那个工具。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**在桥接层内部用普通上限施加边界(不做 spill)。** 否决:没有定位符的截断会丢失回放与 UI 可能需要的数据,还会重新引入本堆叠 PR 链已经移除的「截断摘要」降级渲染路径。
|
||||
|
||||
**直接在桥接层内做 spill(从 code-mode.ts 调用 `ctx.spillStore`)。** 否决:注册表会因此对 spill 能力产生硬依赖;waterfall 则把策略留在所有其他 spill 决策所在的地方,既可组合也可禁用(省略 `maxInlineBytes` 依然意味着真正的 no-op)。
|
||||
|
||||
**让嵌套调用复用 `tools/post-execute`,而不是新增一个事件。** 否决:post-execute 整形的是面向程序的那份结果(嵌套调用有意跳过它,好让程序拿到完整数据);持久副本需要一个属于自己的决策点,位于程序取得其值之后。
|
||||
|
||||
## 后果
|
||||
|
||||
对 Code Mode 轮次而言,会话日志重新有了边界:README 中关于分发日志不设上限的 Known Limitations 条目已经解决,现在指向本篇。携带超大分发内容的旧日志仍可回放(事件形状未变;只有今后的追加才会变小)。web UI 经由与原生完全相同的路径,把被 spill 的子调用输出渲染为预览 + 定位符文本,没有任何特殊处理。
|
||||
@@ -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
|
||||
2026-07-26-code-mode-trajectory-waterfall-spans.md: fe4dcc25dbf211cf69e0d33937cf87a7482852e2
|
||||
2026-07-26-code-mode-trajectory-waterfall-spans.zh.md: aaae06b1fca1b5587d06aa7704adec421d2b2c27
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: Code Mode sub-calls in the trajectory and waterfall views
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-26-code-mode-trajectory-waterfall-spans.zh.md)
|
||||
|
||||
> Scope: the final PR of the Code Mode UI stack — sub-dispatch rendering in the two non-chat views. Chat nesting is owned by the [sub-call rows note](2026-07-26-code-mode-chat-subcall-rows.md); the timing this consumes is the [live-parallel note](2026-07-26-code-mode-live-parallel-dispatch.md)'s start/settle pair.
|
||||
|
||||
## Problem
|
||||
|
||||
Trajectory and waterfall still rendered a `run_code` turn as one opaque Tool cell / one node-count bar. The chat view got nested sub-rows in the earlier PRs, but the two analytical views — whose whole purpose is structure and timing — showed none of the sub-call structure and none of the per-sub-call wall time the dispatch pair now records. Waterfall sub-spans were deliberately deferred until that pair existed: a span without real timing would have been a lie.
|
||||
|
||||
## Decision
|
||||
|
||||
**Trajectory: `subtool` cells interleaved after their parent Tool cell. Waterfall: real-time sub-lanes under the owning turn row.**
|
||||
|
||||
- **Trajectory**: the layout fold takes the snapshot's `codeDispatches` index; after each Tool cell whose `callId` has dispatches (assistant-block calls, orphan results, and running calls alike), it interleaves one `subtool` cell per sub-dispatch in start order — indexes stay sequential across the interleave. A settled sub-call's duration is its start/settle pair (`durationSeconds(sub.time, sub.callTime)`); a running one shows the em dash, exactly the native in-flight convention. The new cell kind wears a `Sub` tag (business tint) and a 28px indent so nesting reads at a glance.
|
||||
- **Waterfall**: `deriveSubSpans` folds the dispatch index into per-turn lanes with REAL timing — each parent's dispatch window is first start → last settle, and every lane's offset/width is its fraction of that window, so parallel sub-calls (PR3) visibly overlap. Each lane carries a `timing` provenance tag: `measured` (pair observed), `running` (settle pending — extends to the window end at reduced opacity), or `unknown` (settle-only replay window, `callTime: null` — drawn hollow and titled "duration unknown", never a fabricated 0 ms). Lanes draw under the owning turn's bar row, scaled into a fixed lane budget.
|
||||
- Both views read `codeDispatches` through the standard snapshot hook — no new wire data, no new stores; replay renders identically to live by construction.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Fold sub-calls into the turn-span node counts (weight the existing bars).** Rejected: it hides exactly the structure this stack exists to show, and node-count weighting is already flagged as a stand-in (deviation ledger #3).
|
||||
|
||||
**A dedicated sub-call panel instead of in-view nesting.** Rejected: the stack's settled UX is nesting under the parent everywhere; a separate panel would diverge from chat and double the selection plumbing.
|
||||
|
||||
**Defer waterfall lanes until the P-III duration-lane redesign.** Rejected: the sub-lane timing is real today (the pair), and the fraction-of-window rendering is independent of whatever the turn-level lanes become; deferring would strand the stack's timing payoff.
|
||||
|
||||
## Consequences
|
||||
|
||||
The waterfall carries the first REAL wall-time rendering in the client (turn bars remain node-count stand-ins — the contrast is deliberate and labeled by hover titles). Trajectory cell indexes now count sub-calls, so `#N` totals grow on Code Mode turns. Specs pin the interleave order and durations, the running em-dash arm, window fractions (offsets/widths), the running-lane extension, the unknown-timing (settle-only) lane, and the rendered lane under the turn row; the built-client Code Mode fixture snapshot additionally pins both tabs' assembled rendering (sub-cells with real +0.8s durations, measured lanes).
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note:trajectory 与 waterfall 视图中的 Code Mode 子调用
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-26-code-mode-trajectory-waterfall-spans.md) | 中文
|
||||
|
||||
> 范围:Code Mode UI 堆叠 PR(Pull Request)链的最后一个 PR,涵盖两个非 chat 视图中的子分发渲染。chat 的嵌套归[子调用行 Agent Note](2026-07-26-code-mode-chat-subcall-rows.md)所有;本篇所消费的计时即[实时并行 Agent Note](2026-07-26-code-mode-live-parallel-dispatch.md)的 start/settle 事件对。
|
||||
|
||||
## 问题
|
||||
|
||||
trajectory 过去仍把一个 `run_code` 轮次渲染为单个不透明的 Tool 单元格,waterfall 则渲染为一根节点计数条。chat 视图在此前的几个 PR 中已获得嵌套子行,但这两个分析视图(其全部意义恰恰是结构与计时)过去既不显示任何子调用结构,也不显示分发事件对如今已记录的逐子调用墙钟时间。waterfall 的子调用 span 曾被刻意推迟到该事件对存在之后:没有真实计时的 span 就是在撒谎。
|
||||
|
||||
## 决策
|
||||
|
||||
**trajectory:`subtool` 单元格穿插在其父 Tool 单元格之后。waterfall:所属轮次行之下、带真实计时的子泳道(sub-lane)。**
|
||||
|
||||
- **trajectory**:布局 fold 接收快照的 `codeDispatches` 索引;凡某个 Tool 单元格的 `callId` 名下存在分发(assistant 块内的调用、孤儿结果与运行中的调用一视同仁),fold 就在该单元格之后按启动顺序为每个子分发穿插一个 `subtool` 单元格,索引在整个穿插序列中保持连续编号。已结算子调用的耗时来自其 start/settle 事件对(`durationSeconds(sub.time, sub.callTime)`);运行中的子调用则显示破折号,与原生的进行中约定完全一致。新增的单元格类型带有 `Sub` 标签(business 色调)与 28px 缩进,嵌套关系一眼可辨。
|
||||
- **waterfall**:`deriveSubSpans` 把分发索引折叠成带真实计时的逐轮次泳道:每个父调用的分发窗口为首个 start → 最后一个 settle,每条泳道的偏移/宽度即其在该窗口中的占比,因此并行的子调用(PR3)会肉眼可见地重叠。每条泳道带有 `timing` 来源标记:`measured`(观察到了成对事件)、`running`(settle 未到 — 以较低不透明度延伸至窗口末端)或 `unknown`(回放窗口只含 settle、`callTime: null` — 画成空心并以「duration unknown」为悬停标题,绝不伪造 0 ms)。泳道绘制在所属轮次的条形行之下,并缩放进固定的泳道预算。
|
||||
- 两个视图都经由标准的快照 hook 读取 `codeDispatches`:没有新的 wire 数据,也没有新的 store;回放的渲染由构造保证与实时完全一致。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**把子调用折入轮次 span 的节点计数(给既有的条加权)。** 否决:它隐藏的恰恰是本堆叠 PR 链存在就是为了展示的结构,而且节点计数加权本就已被标记为占位(偏差账本 #3)。
|
||||
|
||||
**用专用的子调用面板取代视图内嵌套。** 否决:本堆叠 PR 链已敲定的 UX 是处处嵌套在父级之下;独立面板会与 chat 发生偏差,还会让选中接线翻倍。
|
||||
|
||||
**把 waterfall 泳道推迟到 P-III 的时长泳道重新设计。** 否决:子泳道的计时如今已是真实的(即那对事件),而按窗口占比的渲染与轮次级泳道将来的形态无关;推迟只会让本堆叠 PR 链的计时收益搁浅。
|
||||
|
||||
## 后果
|
||||
|
||||
waterfall 承载了 client 中第一处真实的墙钟时间渲染(轮次条仍是节点计数的占位;这一反差是有意为之,并由悬停标题标注)。trajectory 的单元格索引现在会把子调用计入,因此 Code Mode 轮次上的 `#N` 总数会随之增大。spec 锁定穿插顺序与耗时、运行中的破折号分支、窗口占比(偏移/宽度)、运行中泳道的延伸、unknown 计时(仅 settle)泳道,以及轮次行之下实际渲染出的泳道;构建产物级的 Code Mode fixture 快照另行锁定两个标签页的组装后渲染(带真实 +0.8s 耗时的子单元格、measured 泳道)。
|
||||
@@ -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
|
||||
2026-07-26-web-syntax-highlighting-shiki.md: b329e35f1d0ce7b3de454758403a09f67056b5af
|
||||
2026-07-26-web-syntax-highlighting-shiki.zh.md: 8e9d1f0d0c38ce64bcb5da1262538da762f70b12
|
||||
@@ -0,0 +1,32 @@
|
||||
# Agent Note: Web client syntax highlighting — synchronous fine-grained shiki
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-26-web-syntax-highlighting-shiki.zh.md)
|
||||
|
||||
> Scope: the web client's one syntax-highlighting system — the dependency ruling, the singleton shape, the token-sheet contract, and the consuming surfaces. Fifth PR of the Code Mode UI stack; the [chat sub-call rows note](../feature/2026-07-26-code-mode-chat-subcall-rows.md) shipped the `run_code` program body this exists to make readable. Styling ground rules are owned by [the web styling ruling](2026-07-19-web-styling-system.md).
|
||||
|
||||
## Problem
|
||||
|
||||
The client rendered every code surface — markdown fences in assistant prose, the `run_code` program body, the details panel's args — as flat monospace text. The stack's primary payload is model-written TypeScript; unhighlighted programs are measurably harder to scan, and the repo already ships shiki-highlighted code on its VitePress site, so the web app was the one code-rendering surface without it.
|
||||
|
||||
## Decision
|
||||
|
||||
**Shiki in its synchronous fine-grained form, as one `ui-primitives` singleton, themed exclusively through CSS custom properties.**
|
||||
|
||||
- **Dependency**: `shiki/core` + `@shikijs/langs`, composed via `createHighlighterCoreSync` with `createJavaScriptRegexEngine({ forgiving: true })` — no oniguruma WASM, no async init, bundle-friendly. Grammar allowlist: `typescript` (embeds JS), `shellscript`, `json` — the languages the harness actually renders; everything else falls back to a geometry-identical plain block, never an error. Prior art: the VitePress site already renders all documentation code through shiki, and TextMate grammars materially beat regex highlighters on TypeScript — the payload that matters here.
|
||||
- **Singleton**: `ui-primitives/src/markdown/highlight.ts` creates one `HighlighterCore` per document and exposes `highlightToHtml(code, lang)` (undefined = render plain). Engine + grammar construction is a ~120-175ms long task, so the module pre-warms the singleton in a deferred task at plugin boot (the lazy path stays as the correctness fallback), keeping the cost off the render path where a stream's finalize swap would jank. The alias table is a `Map`, not an object: fence info strings are assistant-authored, so a label like `constructor` must miss instead of resolving an inherited property and crashing shiki. The shared `CodeBlock` component owns both arms; its shiki arm injects the generated span tree via `dangerouslySetInnerHTML` — sanctioned because shiki emits a static span tree computed from the code text (no user HTML passes through, no scripts/handlers), shiki's own documented consumption path.
|
||||
- **Theming**: shiki's `createCssVariablesTheme` routes every token color through `--shiki-*` custom properties; the VALUES live in a new `ui-theme/styles/shiki.css` token sheet (light on `:root`, dark on `body[data-ds-dark-theme]` — the same cascade as every other sheet), imported by the shell's `base.css` chain. Component CSS stays tokens-only; no literal color ever enters JS or component sheets. Background/foreground alias the existing markdown code-block tokens so highlighted and plain blocks agree.
|
||||
- **Surfaces**: markdown fences (`MarkdownText`'s `pre` component routes single-string fences through `CodeBlock`), the `run_code` expanded program body (ToolRow's code variant, `lang="typescript"`), and the details panel's Input args (`lang="json"`). Output stays plain deliberately — tool output is arbitrary text, and guessing a grammar would mis-highlight more than it helps.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**`rehype-highlight`/lowlight.** Runner-up: naturally sync and ~⅓ the bundle, but regex-grammar fidelity on TypeScript is visibly worse, and the repo would then run two highlighter systems (site: shiki, app: highlight.js) with two theming vocabularies.
|
||||
|
||||
**Full `shiki` bundle or the oniguruma WASM engine.** Rejected: the full bundle ships every grammar/theme; WASM needs async loading the sync client boot deliberately avoids. The fine-grained core with three grammars keeps the cost proportional to actual use.
|
||||
|
||||
**Highlight in a worker / async.** Rejected: the payloads are small (programs, fences, args); the synchronous JS engine tokenizes them in microseconds, and async introduces a flash-of-unhighlighted-code plus render-machinery churn for no measured need.
|
||||
|
||||
## Consequences
|
||||
|
||||
One code surface for every consumer — a future surface imports `CodeBlock` and inherits highlighting, theming, and the plain fallback. The bundle grows by the shiki core + three grammars (paid once in `ui-primitives`). Token colors are the first `--shiki-*` sheet; a theme package registering alias overrides extends them like any other token. jsdom specs pin the token-span structure, alias resolution, both fallback arms, and the fence route; the existing built-bundle snapshot and browser e2e cover the assembled path.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Agent Note:web client 的语法高亮——同步细粒度的 shiki
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-26-web-syntax-highlighting-shiki.md) | 中文
|
||||
|
||||
> 范围:web client 唯一的一套语法高亮体系——依赖裁决、单例形态、token 表契约与各消费表面。本篇是 Code Mode UI 堆叠 PR(Pull Request)链的第五个 PR;[chat 子调用行 Agent Note](../feature/2026-07-26-code-mode-chat-subcall-rows.md)交付了 `run_code` 程序正文,而本体系存在的意义正是让它可读。样式的基本规则归 [Web 样式体系裁决](2026-07-19-web-styling-system.md)所有。
|
||||
|
||||
## 问题
|
||||
|
||||
client 过去把每一处代码表面——assistant 正文里的 markdown 围栏代码块、`run_code` 程序正文、details 面板的参数——一律渲染成不带高亮的等宽纯文本。本堆叠 PR 链的主要载荷是模型撰写的 TypeScript;未经高亮的程序扫读起来明显更吃力,而仓库已经在自家 VitePress 站点上交付经 shiki 高亮的代码,于是 web 应用成了唯一不带语法高亮的代码渲染表面。
|
||||
|
||||
## 决策
|
||||
|
||||
**采用同步细粒度形态的 shiki,作为 `ui-primitives` 里的一个单例,主题化完全经由 CSS 自定义属性完成。**
|
||||
|
||||
- **依赖**:`shiki/core` + `@shikijs/langs`,经 `createHighlighterCoreSync` 搭配 `createJavaScriptRegexEngine({ forgiving: true })` 组装——不带 oniguruma WASM、没有异步初始化、对 bundle 友好。语法(grammar)白名单:`typescript`(内嵌 JS)、`shellscript`、`json`——即 harness 实际会渲染的那几种语言;其余一律回退到几何完全一致的纯文本块,绝不报错。先例:VitePress 站点已经通过 shiki 渲染全部文档代码;而在 TypeScript(正是此处要紧的载荷)上,TextMate 语法实质性优于正则高亮器。
|
||||
- **单例**:`ui-primitives/src/markdown/highlight.ts` 为每个 document 创建一个 `HighlighterCore`,并公开 `highlightToHtml(code, lang)`(undefined 即渲染为纯文本)。引擎加语法的构建是一次约 120-175ms 的长任务,因此模块在插件启动时用延迟任务预热单例(惰性路径保留为正确性兜底),把这笔开销挪出渲染路径——否则流式 finalize 交换的那一刻会卡顿。别名表用 `Map` 而非对象:fence 信息串由 assistant 撰写,诸如 `constructor` 这样的标签必须落空,而不是解析到继承属性并让 shiki 崩溃。共享的 `CodeBlock` 组件同时拥有两条分支;其 shiki 分支经 `dangerouslySetInnerHTML` 注入生成的 span 树——此用法获准,因为 shiki 输出的是从代码文本计算出的静态 span 树(不流经任何用户 HTML,没有脚本或事件处理器),这正是 shiki 自身文档载明的消费路径。
|
||||
- **主题化**:shiki 的 `createCssVariablesTheme` 让每一种 token 颜色都经由 `--shiki-*` 自定义属性路由;取值本身住在新增的 `ui-theme/styles/shiki.css` token 表里(亮色在 `:root`、暗色在 `body[data-ds-dark-theme]`——层叠方式与其余每张样式表相同),由壳的 `base.css` 导入链引入。组件 CSS 保持只用 token;任何字面颜色都不进入 JS 或组件样式表。背景/前景以别名指向既有的 markdown 代码块 token,使高亮块与纯文本块彼此一致。
|
||||
- **表面**:markdown 围栏代码块(`MarkdownText` 的 `pre` 组件把单字符串围栏路由到 `CodeBlock`)、`run_code` 展开后的程序正文(ToolRow 的 code 变体,`lang="typescript"`),以及 details 面板的 Input 参数(`lang="json"`)。输出有意保持纯文本——工具输出是任意文本,硬猜一种语法,带来的误高亮会多于帮助。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**`rehype-highlight`/lowlight。** 屈居次选:天然同步,bundle 体积约为三分之一,但基于正则的语法在 TypeScript 上的保真度肉眼可见地更差,而且仓库将从此同时运行两套高亮体系(站点用 shiki、应用用 highlight.js)、维护两套主题化词汇。
|
||||
|
||||
**完整的 `shiki` bundle,或 oniguruma WASM 引擎。** 否决:完整 bundle 会带上每一种语法和主题;WASM 需要异步加载,而这正是同步的 client 启动刻意规避的。细粒度 core 加三种语法,让成本与实际用量成正比。
|
||||
|
||||
**在 worker 中高亮/异步高亮。** 否决:载荷都很小(程序、围栏代码块、参数);同步 JS 引擎微秒级就能把它们 token 化,而异步会引入一段未高亮代码的闪现,外加渲染机制的扰动,却没有任何实测得出的需要。
|
||||
|
||||
## 后果
|
||||
|
||||
所有消费方共用同一个代码表面——未来的新表面导入 `CodeBlock` 即继承高亮、主题化与纯文本回退。bundle 的增量是 shiki core 加三种语法(在 `ui-primitives` 中一次性支付)。token 颜色是第一张 `--shiki-*` 表;注册别名覆写的主题包扩展它们的方式与扩展任何其他 token 无异。jsdom spec 锁定 token span 结构、别名解析、两条回退分支与围栏路由;既有的已构建 bundle 快照和浏览器 e2e 覆盖组装后的路径。
|
||||
@@ -30,7 +30,7 @@ description: Use when reviewing a pull request in the deepseek-harness repo —
|
||||
|
||||
- **Intent and seam contracts:** trace both sides of every changed interface. Confirm the implementation matches the PR and any Agent Note, including errors, cancellation, ownership, and disposal.
|
||||
- **Lifecycle and concurrency:** for async setup, callbacks, processes, or teardown, apply [defensive-patterns.md](../../../docs/defensive-patterns.md). Check races before publication, cancellation during awaits, independent error reporting, callback containment, ownership before reentry, complete detach cleanup, and quiescent disposal.
|
||||
- **Capability and consumer fit:** trace every current consumer, then flag consumer-specific behavior leaking into the interface under [the package contract](../../../packages/AGENTS.md).
|
||||
- **Capability and consumer fit:** trace every current consumer, then flag consumer-specific behavior leaking into the interface under [the package contract](../../../packages/AGENTS.md). Flag the inverse too: a new public method on a generic service (registry, session, agent) whose only caller is one internal consumer is an ad-hoc surface widening — require a private capability closure handed to that consumer at construction instead.
|
||||
- **Scope, ownership, and necessity:** map each abstraction, state machine, option, defensive copy, and compatibility path to its current contract, production consumer, and owning plugin or service. Challenge unrelated features and speculative generality, then test the PR's coherence against [the root contract](../../../AGENTS.md#conventions).
|
||||
- **Configuration and public choices:** ask what current-consumer evidence or prior art supports each default, public operation set, format, or imported external concept. Require an explicit choice or deferral when that evidence is absent.
|
||||
- **Model perspective:** inspect the exact prompts, tool schemas, results, and diagnostics the model receives across affected modes. Flag concepts outside the model's task, then verify stable text verbatim and dynamic behavior through snapshots or end-to-end coverage.
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
// the code-variant parent row titled by the model-authored description, its
|
||||
// three always-visible nested sub-rows (bash through the sample registration,
|
||||
// read through GenericToolCard, the failing read wearing the error state),
|
||||
// the expanded program body, and details-panel resolution of a sub-callId.
|
||||
// the expanded program body, details-panel resolution of a sub-callId, and
|
||||
// the trajectory/waterfall tabs' sub-call cells and timing lanes.
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { act, cleanup, fireEvent, screen, waitFor, within } from '@testing-library/react'
|
||||
@@ -155,13 +156,20 @@ it('expands the code row into the program body and resolves a sub-row through th
|
||||
boot()
|
||||
await openFixtureSession()
|
||||
|
||||
// Expand: the leading control reveals the program verbatim.
|
||||
// Expand: the leading control reveals the program (shiki-tokenized: the
|
||||
// text splits into styled spans inside one <pre class="shiki"> tree).
|
||||
const codeRoot = document.querySelector('[data-variant="code"]')
|
||||
if (codeRoot === null) throw new Error('code-variant row missing')
|
||||
const toggle = codeRoot.querySelector('button[aria-expanded]')
|
||||
if (toggle === null) throw new Error('code row expand control missing')
|
||||
fireEvent.click(toggle)
|
||||
await screen.findByText(/const listing = await tools\.bash/)
|
||||
await waitFor(() => {
|
||||
// Scope to THIS row: the markdown fixture turn also renders shiki pres.
|
||||
const pre = codeRoot.querySelector('pre.shiki')
|
||||
if (pre === null || !(pre.textContent ?? '').includes('const listing = await tools.bash')) {
|
||||
throw new Error('highlighted program body missing under the code row')
|
||||
}
|
||||
})
|
||||
|
||||
// Sub-row click → details panel resolves the sub-callId with FULL output.
|
||||
const nest = document.querySelector('[data-subcalls]')
|
||||
@@ -186,3 +194,64 @@ it('expands the code row into the program body and resolves a sub-row through th
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('trajectory and waterfall surface the run_code sub-calls with real timing', async () => {
|
||||
boot()
|
||||
await openFixtureSession()
|
||||
|
||||
// Switch to the trajectory tab (same slot ring the chat view registers in).
|
||||
fireEvent.click(await screen.findByRole('tab', { name: 'Trajectory' }))
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('[data-kind="subtool"]')).not.toBeNull()
|
||||
}, { timeout: 10_000 })
|
||||
const subCells = [...document.querySelectorAll('[data-kind="subtool"]')]
|
||||
expect({
|
||||
// Three Sub cells nested under the run_code Tool cell, in dispatch order,
|
||||
// each with a real +N.Ns own-duration off the start/settle pair (the
|
||||
// fixture spaces every event 800ms apart — never the em dash).
|
||||
subCells: subCells.map(cell => visibleText(cell)),
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"subCells": [
|
||||
"#53Subbash · {"command":"ls notes","description":"List notes"}+0.8s",
|
||||
"#54Subread · {"path":"notes/demo.txt"}+0.8s",
|
||||
"#55Subread · {"path":"notes/missing.txt"}+0.8s",
|
||||
],
|
||||
}
|
||||
`)
|
||||
|
||||
// Waterfall: each sub-call draws a measured lane scaled into the parent
|
||||
// turn's dispatch window.
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Waterfall' }))
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('[data-subspan]')).not.toBeNull()
|
||||
}, { timeout: 10_000 })
|
||||
const lanes = [...document.querySelectorAll('[data-subspan]')]
|
||||
expect({
|
||||
lanes: lanes.map(lane => ({
|
||||
label: visibleText(lane.querySelector('[class*="subTag"]') ?? lane),
|
||||
title: lane.querySelector('[data-timing]')?.getAttribute('title'),
|
||||
timing: lane.querySelector('[data-timing]')?.getAttribute('data-timing'),
|
||||
})),
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"lanes": [
|
||||
{
|
||||
"label": "bash",
|
||||
"timing": "measured",
|
||||
"title": "bash · 0.80s",
|
||||
},
|
||||
{
|
||||
"label": "read",
|
||||
"timing": "measured",
|
||||
"title": "read · 0.80s",
|
||||
},
|
||||
{
|
||||
"label": "read",
|
||||
"timing": "measured",
|
||||
"title": "read · 0.80s",
|
||||
},
|
||||
],
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
@@ -1202,7 +1202,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/spill/spill-policy/src/index.ts:51`](../packages/spill/spill-policy/src/index.ts)
|
||||
Source: [`packages/spill/spill-policy/src/index.ts:60`](../packages/spill/spill-policy/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-storage-domain`
|
||||
|
||||
@@ -1718,7 +1718,7 @@ export interface Config {
|
||||
export type ToolPresentationMode = 'native' | 'code' | 'both'
|
||||
```
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:529`](../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:566`](../packages/core/tools/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-tui`
|
||||
|
||||
|
||||
@@ -842,7 +842,31 @@ A tool was registered or unregistered, or a scoped restriction changed (the avai
|
||||
'tools/change'(): void
|
||||
```
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:143`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:156`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
### `tools/code-dispatch-log` — waterfall
|
||||
|
||||
Shape the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before the bridge appends its `tool/code-dispatch` event. `next()` keeps the content unchanged; a listener may return replacement blocks (e.g. the spill policy's preview + locator for an oversized text result). Only the logged copy is affected — the program already received the complete value, and the model sees neither. A throwing listener is contained: the bridge falls back to logging the unshaped content. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's dispatches.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Shape the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before
|
||||
* the bridge appends its `tool/code-dispatch` event. `next()` keeps the
|
||||
* content unchanged; a listener may return replacement blocks (e.g. the
|
||||
* spill policy's preview + locator for an oversized text result). Only the
|
||||
* logged copy is affected — the program already received the complete
|
||||
* value, and the model sees neither. A throwing listener is contained:
|
||||
* the bridge falls back to logging the unshaped content.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's dispatches.
|
||||
* @param dispatch - the parent execution, sub-call identity, and the settled content to log.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'tools/code-dispatch-log'(this: Scoped<ToolRegistry>, dispatch: CodeDispatchLog, next: () => Promise<ContentBlock[]>): Promise<ContentBlock[]>
|
||||
```
|
||||
|
||||
Types: [CodeDispatchLog](../core-data-structures/tools.md) · [ContentBlock](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [ToolRegistry](../core-data-structures/tools.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:138`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
### `tools/execute` — waterfall
|
||||
|
||||
@@ -927,7 +951,7 @@ Observe the frozen, lossless-JSON final outcome. Listener failures are contained
|
||||
|
||||
Types: [Scoped](../core-data-structures/scope.md) · [ToolExecution](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md) · [ToolRegistry](../core-data-structures/tools.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:133`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:146`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
## `workflow/*`
|
||||
|
||||
|
||||
@@ -1855,7 +1855,7 @@ async execute(exec: ToolExecutionInput): Promise<ToolExecutionResult>
|
||||
|
||||
Types: [ScopeKey](../core-data-structures/scope.md) · [ToolDefinition](../core-data-structures/tools.md) · [ToolExecutionInput](../core-data-structures/tools.md) · [ToolExecutionMode](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md) · [ToolGuard](../core-data-structures/tools.md) · [ToolRestriction](../core-data-structures/tools.md) · [ToolSchema](../core-data-structures/tools.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:651`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:688`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
## `ctx.tui` — `TuiExtensionService` (abstract seam)
|
||||
|
||||
|
||||
@@ -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
|
||||
tools.md: 875bea18ff0c34ca97f9c144f4320d3b3a6aaa4a
|
||||
tools.zh.md: 11f0b8d4a0f29304e6fdbde7c81be981bd940a2d
|
||||
tools.md: 250e869397f8ecb128d5b644ff7506376d0657c6
|
||||
tools.zh.md: 96fc9d3eeda0240e195beb11bea088d5606d4757
|
||||
|
||||
@@ -231,6 +231,34 @@ type ToolExecutionMode =
|
||||
| { kind: 'exclusive' }
|
||||
```
|
||||
|
||||
Code Mode's bridge additionally exposes each settled sub-dispatch to the `tools/code-dispatch-log` waterfall, which may reshape the durable event's copy of the content (the program's value and the model contract are untouched):
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One settled `run_code` sub-dispatch about to be logged, as seen by the
|
||||
* `tools/code-dispatch-log` waterfall: the parent execution (session owner,
|
||||
* outer call identity), the sub-call identity, and the outcome whose durable
|
||||
* copy a listener may reshape. `content` is the RENDERED result projection
|
||||
* (what a native `tool/result` would carry) — the program itself received
|
||||
* the structured `value` (or just the error message on failure); only the
|
||||
* `tool/code-dispatch` event's copy changes.
|
||||
*/
|
||||
interface CodeDispatchLog {
|
||||
/** The outer `run_code` execution. */
|
||||
readonly exec: ToolExecution
|
||||
/** The calling agent (the scope routing key and the spill owner), when the outer call has one. */
|
||||
readonly agent?: Agent
|
||||
/** Deterministic sub-call id (`<parent>:code:<n>`). */
|
||||
readonly subCallId: CallId
|
||||
/** The dispatched sub-tool name. */
|
||||
readonly name: string
|
||||
/** Whether the sub-call settled as an error. */
|
||||
readonly isError: boolean
|
||||
/** The sub-call's complete model-facing content (the settle event's default payload). */
|
||||
readonly content: ContentBlock[]
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One pending tool call inside the registry pipeline. Parsed arguments cross
|
||||
|
||||
@@ -231,6 +231,34 @@ type ToolExecutionMode =
|
||||
| { kind: 'exclusive' }
|
||||
```
|
||||
|
||||
Code Mode 的桥接层还会把每个已结算的子分派暴露给 `tools/code-dispatch-log` waterfall,该 waterfall 可以改写持久事件所存的内容副本(程序取得的值与模型契约均不受影响):
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One settled `run_code` sub-dispatch about to be logged, as seen by the
|
||||
* `tools/code-dispatch-log` waterfall: the parent execution (session owner,
|
||||
* outer call identity), the sub-call identity, and the outcome whose durable
|
||||
* copy a listener may reshape. `content` is the RENDERED result projection
|
||||
* (what a native `tool/result` would carry) — the program itself received
|
||||
* the structured `value` (or just the error message on failure); only the
|
||||
* `tool/code-dispatch` event's copy changes.
|
||||
*/
|
||||
interface CodeDispatchLog {
|
||||
/** The outer `run_code` execution. */
|
||||
readonly exec: ToolExecution
|
||||
/** The calling agent (the scope routing key and the spill owner), when the outer call has one. */
|
||||
readonly agent?: Agent
|
||||
/** Deterministic sub-call id (`<parent>:code:<n>`). */
|
||||
readonly subCallId: CallId
|
||||
/** The dispatched sub-tool name. */
|
||||
readonly name: string
|
||||
/** Whether the sub-call settled as an error. */
|
||||
readonly isError: boolean
|
||||
/** The sub-call's complete model-facing content (the settle event's default payload). */
|
||||
readonly content: ContentBlock[]
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One pending tool call inside the registry pipeline. Parsed arguments cross
|
||||
|
||||
@@ -44,11 +44,12 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:130`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`subagent`](../packages/subagent/subagent) |
|
||||
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:29`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:35`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
|
||||
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:143`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - |
|
||||
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:156`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - |
|
||||
| `tools/code-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:138`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
|
||||
| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:113`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`timeout-policy`](../packages/timeout/timeout-policy) |
|
||||
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:125`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search), [`workspace-context`](../packages/context/workspace-context) |
|
||||
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:102`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`tool-tasks`](../packages/tasks/tool-tasks) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:133`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`workspace-context`](../packages/context/workspace-context) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:146`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`workspace-context`](../packages/context/workspace-context) |
|
||||
| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:81`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`workflow`](../packages/workflow/workflow) |
|
||||
| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:70`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`workflow`](../packages/workflow/workflow) |
|
||||
| `workflow/end` | `emit` | [`packages/workflow/workflow/src/index.ts:91`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`workflow`](../packages/workflow/workflow) |
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
{"type":"session","version":0,"id":"main-session","createdAt":1785052797743,"cwd":"/tmp/dsh-tui-snapshot-code-mode-dispatch-spill-8cOdia"}
|
||||
{"type":"turn/start","seq":0,"time":1785052797817,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":1,"time":1785052797818,"data":{"content":[{"type":"text","text":"Using ONE run_code program: call the bash tool exactly once with the command `seq 1 200 | awk '{printf \"line %04d: the quick brown fox jumps over the lazy dog\\n\", $1}'`, then return ONLY the number of lines in its output. Reply with just that number and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1785052797825,"data":{"title":"Using ONE run_code program: call","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":1785052797826,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":1785052797827,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"assistant/chunk","seq":5,"time":1785052798220,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"assistant/chunk","seq":6,"time":1785052798221,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}}
|
||||
{"type":"assistant/chunk","seq":7,"time":1785052798391,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}}
|
||||
{"type":"assistant/chunk","seq":8,"time":1785052798421,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}}
|
||||
{"type":"assistant/chunk","seq":9,"time":1785052798421,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}}
|
||||
{"type":"assistant/chunk","seq":10,"time":1785052798421,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}}
|
||||
{"type":"assistant/chunk","seq":11,"time":1785052798421,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" write"}}}
|
||||
{"type":"assistant/chunk","seq":12,"time":1785052798451,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}}
|
||||
{"type":"assistant/chunk","seq":13,"time":1785052798452,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}}
|
||||
{"type":"assistant/chunk","seq":14,"time":1785052798452,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" run"}}}
|
||||
{"type":"assistant/chunk","seq":15,"time":1785052798452,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_code"}}}
|
||||
{"type":"assistant/chunk","seq":16,"time":1785052798480,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" program"}}}
|
||||
{"type":"assistant/chunk","seq":17,"time":1785052798480,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}}
|
||||
{"type":"assistant/chunk","seq":18,"time":1785052798480,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" calls"}}}
|
||||
{"type":"assistant/chunk","seq":19,"time":1785052798480,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" bash"}}}
|
||||
{"type":"assistant/chunk","seq":20,"time":1785052798509,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}}
|
||||
{"type":"assistant/chunk","seq":21,"time":1785052798539,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" once"}}}
|
||||
{"type":"assistant/chunk","seq":22,"time":1785052798539,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}}
|
||||
{"type":"assistant/chunk","seq":23,"time":1785052798569,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}}
|
||||
{"type":"assistant/chunk","seq":24,"time":1785052798569,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" specific"}}}
|
||||
{"type":"assistant/chunk","seq":25,"time":1785052798599,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" command"}}}
|
||||
{"type":"assistant/chunk","seq":26,"time":1785052798599,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}}
|
||||
{"type":"assistant/chunk","seq":27,"time":1785052798599,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" then"}}}
|
||||
{"type":"assistant/chunk","seq":28,"time":1785052798599,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" returns"}}}
|
||||
{"type":"assistant/chunk","seq":29,"time":1785052798629,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" only"}}}
|
||||
{"type":"assistant/chunk","seq":30,"time":1785052798629,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}}
|
||||
{"type":"assistant/chunk","seq":31,"time":1785052798629,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" number"}}}
|
||||
{"type":"assistant/chunk","seq":32,"time":1785052798629,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" of"}}}
|
||||
{"type":"assistant/chunk","seq":33,"time":1785052798629,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" lines"}}}
|
||||
{"type":"assistant/chunk","seq":34,"time":1785052798629,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" in"}}}
|
||||
{"type":"assistant/chunk","seq":35,"time":1785052798659,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" its"}}}
|
||||
{"type":"assistant/chunk","seq":36,"time":1785052798689,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" output"}}}
|
||||
{"type":"assistant/chunk","seq":37,"time":1785052798690,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}}
|
||||
{"type":"assistant/chunk","seq":38,"time":1785052798781,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":39,"time":1785052798781,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":""}}}
|
||||
{"type":"assistant/chunk","seq":40,"time":1785052798781,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"{"}}}
|
||||
{"type":"assistant/chunk","seq":41,"time":1785052798781,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\""}}}
|
||||
{"type":"assistant/chunk","seq":42,"time":1785052798809,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"description"}}}
|
||||
{"type":"assistant/chunk","seq":43,"time":1785052798809,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\""}}}
|
||||
{"type":"assistant/chunk","seq":44,"time":1785052798809,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":": "}}}
|
||||
{"type":"assistant/chunk","seq":45,"time":1785052798809,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\""}}}
|
||||
{"type":"assistant/chunk","seq":46,"time":1785052798839,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"Count"}}}
|
||||
{"type":"assistant/chunk","seq":47,"time":1785052798868,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" lines"}}}
|
||||
{"type":"assistant/chunk","seq":48,"time":1785052798869,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" in"}}}
|
||||
{"type":"assistant/chunk","seq":49,"time":1785052798869,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" seq"}}}
|
||||
{"type":"assistant/chunk","seq":50,"time":1785052798899,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"/"}}}
|
||||
{"type":"assistant/chunk","seq":51,"time":1785052798929,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"awk"}}}
|
||||
{"type":"assistant/chunk","seq":52,"time":1785052798930,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" output"}}}
|
||||
{"type":"assistant/chunk","seq":53,"time":1785052798930,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\""}}}
|
||||
{"type":"assistant/chunk","seq":54,"time":1785052798960,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":", "}}}
|
||||
{"type":"assistant/chunk","seq":55,"time":1785052798960,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\""}}}
|
||||
{"type":"assistant/chunk","seq":56,"time":1785052798960,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"code"}}}
|
||||
{"type":"assistant/chunk","seq":57,"time":1785052798988,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\""}}}
|
||||
{"type":"assistant/chunk","seq":58,"time":1785052798989,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":": "}}}
|
||||
{"type":"assistant/chunk","seq":59,"time":1785052798989,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\""}}}
|
||||
{"type":"assistant/chunk","seq":60,"time":1785052798989,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"const"}}}
|
||||
{"type":"assistant/chunk","seq":61,"time":1785052799019,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" result"}}}
|
||||
{"type":"assistant/chunk","seq":62,"time":1785052799019,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" ="}}}
|
||||
{"type":"assistant/chunk","seq":63,"time":1785052799019,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" await"}}}
|
||||
{"type":"assistant/chunk","seq":64,"time":1785052799019,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" tools"}}}
|
||||
{"type":"assistant/chunk","seq":65,"time":1785052799019,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":".b"}}}
|
||||
{"type":"assistant/chunk","seq":66,"time":1785052799020,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"ash"}}}
|
||||
{"type":"assistant/chunk","seq":67,"time":1785052799048,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"({\\n"}}}
|
||||
{"type":"assistant/chunk","seq":68,"time":1785052799049,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" "}}}
|
||||
{"type":"assistant/chunk","seq":69,"time":1785052799049,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" command"}}}
|
||||
{"type":"assistant/chunk","seq":70,"time":1785052799049,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":":"}}}
|
||||
{"type":"assistant/chunk","seq":71,"time":1785052799049,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" \\\""}}}
|
||||
{"type":"assistant/chunk","seq":72,"time":1785052799049,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"seq"}}}
|
||||
{"type":"assistant/chunk","seq":73,"time":1785052799107,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" "}}}
|
||||
{"type":"assistant/chunk","seq":74,"time":1785052799108,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"1"}}}
|
||||
{"type":"assistant/chunk","seq":75,"time":1785052799108,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" "}}}
|
||||
{"type":"assistant/chunk","seq":76,"time":1785052799123,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"200"}}}
|
||||
{"type":"assistant/chunk","seq":77,"time":1785052799123,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" |"}}}
|
||||
{"type":"assistant/chunk","seq":78,"time":1785052799123,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" awk"}}}
|
||||
{"type":"assistant/chunk","seq":79,"time":1785052799123,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" '{"}}}
|
||||
{"type":"assistant/chunk","seq":80,"time":1785052799123,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"printf"}}}
|
||||
{"type":"assistant/chunk","seq":81,"time":1785052799162,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" \\\\\\\""}}}
|
||||
{"type":"assistant/chunk","seq":82,"time":1785052799162,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"line"}}}
|
||||
{"type":"assistant/chunk","seq":83,"time":1785052799163,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" %"}}}
|
||||
{"type":"assistant/chunk","seq":84,"time":1785052799163,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"04"}}}
|
||||
{"type":"assistant/chunk","seq":85,"time":1785052799191,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"d"}}}
|
||||
{"type":"assistant/chunk","seq":86,"time":1785052799191,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":":"}}}
|
||||
{"type":"assistant/chunk","seq":87,"time":1785052799191,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" the"}}}
|
||||
{"type":"assistant/chunk","seq":88,"time":1785052799220,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" quick"}}}
|
||||
{"type":"assistant/chunk","seq":89,"time":1785052799220,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" brown"}}}
|
||||
{"type":"assistant/chunk","seq":90,"time":1785052799220,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" fox"}}}
|
||||
{"type":"assistant/chunk","seq":91,"time":1785052799220,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" jumps"}}}
|
||||
{"type":"assistant/chunk","seq":92,"time":1785052799220,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" over"}}}
|
||||
{"type":"assistant/chunk","seq":93,"time":1785052799220,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" the"}}}
|
||||
{"type":"assistant/chunk","seq":94,"time":1785052799250,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" lazy"}}}
|
||||
{"type":"assistant/chunk","seq":95,"time":1785052799250,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" dog"}}}
|
||||
{"type":"assistant/chunk","seq":96,"time":1785052799250,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\\\\\\\\"}}}
|
||||
{"type":"assistant/chunk","seq":97,"time":1785052799250,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"n"}}}
|
||||
{"type":"assistant/chunk","seq":98,"time":1785052799250,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\\\\\\\","}}}
|
||||
{"type":"assistant/chunk","seq":99,"time":1785052799251,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" $"}}}
|
||||
{"type":"assistant/chunk","seq":100,"time":1785052799280,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"1"}}}
|
||||
{"type":"assistant/chunk","seq":101,"time":1785052799281,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"}'"}}}
|
||||
{"type":"assistant/chunk","seq":102,"time":1785052799281,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\\\",\\n"}}}
|
||||
{"type":"assistant/chunk","seq":103,"time":1785052799281,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" "}}}
|
||||
{"type":"assistant/chunk","seq":104,"time":1785052799281,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" description"}}}
|
||||
{"type":"assistant/chunk","seq":105,"time":1785052799310,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":":"}}}
|
||||
{"type":"assistant/chunk","seq":106,"time":1785052799310,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" \\\""}}}
|
||||
{"type":"assistant/chunk","seq":107,"time":1785052799310,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"Generate"}}}
|
||||
{"type":"assistant/chunk","seq":108,"time":1785052799350,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" "}}}
|
||||
{"type":"assistant/chunk","seq":109,"time":1785052799350,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"200"}}}
|
||||
{"type":"assistant/chunk","seq":110,"time":1785052799350,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" lines"}}}
|
||||
{"type":"assistant/chunk","seq":111,"time":1785052799370,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" of"}}}
|
||||
{"type":"assistant/chunk","seq":112,"time":1785052799371,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" text"}}}
|
||||
{"type":"assistant/chunk","seq":113,"time":1785052799400,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\\\"\\n"}}}
|
||||
{"type":"assistant/chunk","seq":114,"time":1785052799400,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"});\\n\\n"}}}
|
||||
{"type":"assistant/chunk","seq":115,"time":1785052799430,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"//"}}}
|
||||
{"type":"assistant/chunk","seq":116,"time":1785052799430,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" Count"}}}
|
||||
{"type":"assistant/chunk","seq":117,"time":1785052799460,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" lines"}}}
|
||||
{"type":"assistant/chunk","seq":118,"time":1785052799460,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" in"}}}
|
||||
{"type":"assistant/chunk","seq":119,"time":1785052799460,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" stdout"}}}
|
||||
{"type":"assistant/chunk","seq":120,"time":1785052799491,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\\n"}}}
|
||||
{"type":"assistant/chunk","seq":121,"time":1785052799491,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"const"}}}
|
||||
{"type":"assistant/chunk","seq":122,"time":1785052799491,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" lines"}}}
|
||||
{"type":"assistant/chunk","seq":123,"time":1785052799521,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" ="}}}
|
||||
{"type":"assistant/chunk","seq":124,"time":1785052799521,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" result"}}}
|
||||
{"type":"assistant/chunk","seq":125,"time":1785052799521,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":".k"}}}
|
||||
{"type":"assistant/chunk","seq":126,"time":1785052799551,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"ind"}}}
|
||||
{"type":"assistant/chunk","seq":127,"time":1785052799551,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" ==="}}}
|
||||
{"type":"assistant/chunk","seq":128,"time":1785052799551,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" \\\""}}}
|
||||
{"type":"assistant/chunk","seq":129,"time":1785052799581,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"fore"}}}
|
||||
{"type":"assistant/chunk","seq":130,"time":1785052799581,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"ground"}}}
|
||||
{"type":"assistant/chunk","seq":131,"time":1785052799582,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\\\""}}}
|
||||
{"type":"assistant/chunk","seq":132,"time":1785052799582,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" ?"}}}
|
||||
{"type":"assistant/chunk","seq":133,"time":1785052799611,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" result"}}}
|
||||
{"type":"assistant/chunk","seq":134,"time":1785052799611,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":".stdout"}}}
|
||||
{"type":"assistant/chunk","seq":135,"time":1785052799611,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":".text"}}}
|
||||
{"type":"assistant/chunk","seq":136,"time":1785052799641,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":".trim"}}}
|
||||
{"type":"assistant/chunk","seq":137,"time":1785052799672,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"()."}}}
|
||||
{"type":"assistant/chunk","seq":138,"time":1785052799672,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"split"}}}
|
||||
{"type":"assistant/chunk","seq":139,"time":1785052799672,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"(\\\"\\\\"}}}
|
||||
{"type":"assistant/chunk","seq":140,"time":1785052799672,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"n"}}}
|
||||
{"type":"assistant/chunk","seq":141,"time":1785052799672,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\\\")."}}}
|
||||
{"type":"assistant/chunk","seq":142,"time":1785052799672,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"length"}}}
|
||||
{"type":"assistant/chunk","seq":143,"time":1785052799702,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" :"}}}
|
||||
{"type":"assistant/chunk","seq":144,"time":1785052799702,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" "}}}
|
||||
{"type":"assistant/chunk","seq":145,"time":1785052799702,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"0"}}}
|
||||
{"type":"assistant/chunk","seq":146,"time":1785052799702,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":";\\n"}}}
|
||||
{"type":"assistant/chunk","seq":147,"time":1785052799703,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"return"}}}
|
||||
{"type":"assistant/chunk","seq":148,"time":1785052799703,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" lines"}}}
|
||||
{"type":"assistant/chunk","seq":149,"time":1785052799731,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":";"}}}
|
||||
{"type":"assistant/chunk","seq":150,"time":1785052799731,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\""}}}
|
||||
{"type":"assistant/chunk","seq":151,"time":1785052799731,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"}"}}}
|
||||
{"type":"assistant/chunk","seq":152,"time":1785052799793,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to write a single run_code program that calls bash exactly once with a specific command, then returns only the number of lines in its output."}}}}
|
||||
{"type":"assistant/chunk","seq":153,"time":1785052799793,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","arguments":"{\"description\": \"Count lines in seq/awk output\", \"code\": \"const result = await tools.bash({\\n command: \\\"seq 1 200 | awk '{printf \\\\\\\"line %04d: the quick brown fox jumps over the lazy dog\\\\\\\\n\\\\\\\", $1}'\\\",\\n description: \\\"Generate 200 lines of text\\\"\\n});\\n\\n// Count lines in stdout\\nconst lines = result.kind === \\\"foreground\\\" ? result.stdout.text.trim().split(\\\"\\\\n\\\").length : 0;\\nreturn lines;\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":154,"time":1785052799794,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":90,"outputTokens":186,"cacheReadTokens":3968,"reasoningTokens":32}}}}
|
||||
{"type":"assistant/chunk","seq":155,"time":1785052799794,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":156,"time":1785052799798,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to write a single run_code program that calls bash exactly once with a specific command, then returns only the number of lines in its output."},{"type":"tool-call","id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","arguments":"{\"description\": \"Count lines in seq/awk output\", \"code\": \"const result = await tools.bash({\\n command: \\\"seq 1 200 | awk '{printf \\\\\\\"line %04d: the quick brown fox jumps over the lazy dog\\\\\\\\n\\\\\\\", $1}'\\\",\\n description: \\\"Generate 200 lines of text\\\"\\n});\\n\\n// Count lines in stdout\\nconst lines = result.kind === \\\"foreground\\\" ? result.stdout.text.trim().split(\\\"\\\\n\\\").length : 0;\\nreturn lines;\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":90,"outputTokens":186,"cacheReadTokens":3968,"reasoningTokens":32}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":157,"time":1785052799799,"data":{"turn":1,"step":1,"callId":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","arguments":"{\"description\": \"Count lines in seq/awk output\", \"code\": \"const result = await tools.bash({\\n command: \\\"seq 1 200 | awk '{printf \\\\\\\"line %04d: the quick brown fox jumps over the lazy dog\\\\\\\\n\\\\\\\", $1}'\\\",\\n description: \\\"Generate 200 lines of text\\\"\\n});\\n\\n// Count lines in stdout\\nconst lines = result.kind === \\\"foreground\\\" ? result.stdout.text.trim().split(\\\"\\\\n\\\").length : 0;\\nreturn lines;\"}"}}
|
||||
{"type":"tool/code-dispatch-start","seq":158,"time":1785052799893,"data":{"parentCallId":"call_00_R6g9Uzx4h0jeUv9g3fno7490","subCallId":"call_00_R6g9Uzx4h0jeUv9g3fno7490:code:1","name":"bash","arguments":{"command":"seq 1 200 | awk '{printf \"line %04d: the quick brown fox jumps over the lazy dog\\n\", $1}'","description":"Generate 200 lines of text"}}}
|
||||
{"type":"tool/code-dispatch","seq":159,"time":1785052799923,"data":{"parentCallId":"call_00_R6g9Uzx4h0jeUv9g3fno7490","subCallId":"call_00_R6g9Uzx4h0jeUv9g3fno7490:code:1","name":"bash","arguments":{"command":"seq 1 200 | awk '{printf \"line %04d: the quick brown fox jumps over the lazy dog\\n\", $1}'","description":"Generate 200 lines of text"},"isError":false,"content":[{"type":"text","text":"line 0001: the quick brown fox jumps over the lazy dog\nline 0002: the quick brown fox jumps over the lazy dog\nline 0003: the quick brown fox jumps over the lazy dog\nline 0004: the quick s over the lazy dog\nline 0198: the quick brown fox jumps over the lazy dog\nline 0199: the quick brown fox jumps over the lazy dog\nline 0200: the quick brown fox jumps over the lazy dog\n\n\n(Omitted 10629 bytes. Full formatted result stored at: /tmp/dsh-tui-snapshot-code-mode-dispatch-spill-8cOdia/.spill/session-2d2b9e84a250/825a63550249-bash.txt. Use read with offset/limit, or grep this path to search within it.)"}]}}
|
||||
{"type":"tool/result","seq":160,"time":1785052799925,"data":{"turn":1,"step":1,"callId":"call_00_R6g9Uzx4h0jeUv9g3fno7490","content":[{"type":"text","text":"200"}],"isError":false},"sourceEventSeqs":[157],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":161,"time":1785052799926,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":162,"time":1785052799928,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":163,"time":1785052800414,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"assistant/chunk","seq":164,"time":1785052800415,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}}
|
||||
{"type":"assistant/chunk","seq":165,"time":1785052800572,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" result"}}}
|
||||
{"type":"assistant/chunk","seq":166,"time":1785052800604,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" is"}}}
|
||||
{"type":"assistant/chunk","seq":167,"time":1785052800604,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}}
|
||||
{"type":"assistant/chunk","seq":168,"time":1785052800604,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"200"}}}
|
||||
{"type":"assistant/chunk","seq":169,"time":1785052800604,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" lines"}}}
|
||||
{"type":"assistant/chunk","seq":170,"time":1785052800605,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}}
|
||||
{"type":"assistant/chunk","seq":171,"time":1785052800635,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" The"}}}
|
||||
{"type":"assistant/chunk","seq":172,"time":1785052800636,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}}
|
||||
{"type":"assistant/chunk","seq":173,"time":1785052800666,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}}
|
||||
{"type":"assistant/chunk","seq":174,"time":1785052800666,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}}
|
||||
{"type":"assistant/chunk","seq":175,"time":1785052800666,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}}
|
||||
{"type":"assistant/chunk","seq":176,"time":1785052800666,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}}
|
||||
{"type":"assistant/chunk","seq":177,"time":1785052800699,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}}
|
||||
{"type":"assistant/chunk","seq":178,"time":1785052800700,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" just"}}}
|
||||
{"type":"assistant/chunk","seq":179,"time":1785052800700,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}}
|
||||
{"type":"assistant/chunk","seq":180,"time":1785052800700,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" number"}}}
|
||||
{"type":"assistant/chunk","seq":181,"time":1785052800700,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}}
|
||||
{"type":"assistant/chunk","seq":182,"time":1785052800731,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" stop"}}}
|
||||
{"type":"assistant/chunk","seq":183,"time":1785052800731,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}}
|
||||
{"type":"assistant/chunk","seq":184,"time":1785052800731,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":185,"time":1785052800731,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"200"}}}
|
||||
{"type":"assistant/chunk","seq":186,"time":1785052800731,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The result is 200 lines. The user wants me to reply with just that number and stop."}}}}
|
||||
{"type":"assistant/chunk","seq":187,"time":1785052800731,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"200"}}}}
|
||||
{"type":"assistant/chunk","seq":188,"time":1785052800732,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":33,"outputTokens":22,"cacheReadTokens":4224,"reasoningTokens":20}}}}
|
||||
{"type":"assistant/chunk","seq":189,"time":1785052800732,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":190,"time":1785052800733,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The result is 200 lines. The user wants me to reply with just that number and stop."},{"type":"text","text":"200"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":33,"outputTokens":22,"cacheReadTokens":4224,"reasoningTokens":20}},"sourceEventSeqs":[163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":191,"time":1785052800733,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":192,"time":1785052800733,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
@@ -0,0 +1,65 @@
|
||||
terminal 100x36 buffer=normal length=36 base=0 viewport=0
|
||||
lifecycle started=1 stopped=0 progress=inactive
|
||||
title "Using ONE run_code program: call — DSH TUI snapshot"
|
||||
cursor hidden column=1 viewportRow=26 bufferRow=26
|
||||
buffer
|
||||
0| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-blue bold
|
||||
style 10-16 bold
|
||||
1| " Using ONE run_code program: call"
|
||||
style 1-32 fg=bright-black
|
||||
2| " deepseek-v4-flash • main-session"
|
||||
style 1-34 dim
|
||||
3| <blank>
|
||||
4| "▌ "
|
||||
style 0-0 fg=bright-blue
|
||||
5| "▌ You "
|
||||
style 0-0 fg=bright-blue
|
||||
style 2-4 fg=bright-blue bold
|
||||
6| "▌ Using ONE run_code program: call the bash tool exactly once with the command seq 1 200 | awk "
|
||||
style 0-0 fg=bright-blue
|
||||
style 79-99 fg=cyan
|
||||
7| "▌ '{printf \"line %04d: the quick brown fox jumps over the lazy dog\\n\", $1}', then return ONLY the "
|
||||
style 0-0 fg=bright-blue
|
||||
style 2-74 fg=cyan
|
||||
8| "▌ number of lines in its output. Reply with just that number and stop. "
|
||||
style 0-0 fg=bright-blue
|
||||
9| "▌ "
|
||||
style 0-0 fg=bright-blue
|
||||
10| <blank>
|
||||
11| " Reasoning "
|
||||
style 1-9 fg=bright-black italic
|
||||
12| " The user wants me to write a single run_code program that calls bash exactly once with a specific "
|
||||
style 1-99 fg=bright-black italic
|
||||
13| " command, then returns only the number of lines in its output. "
|
||||
style 1-61 fg=bright-black italic
|
||||
14| <blank>
|
||||
15| "▌ "
|
||||
style 0-0 fg=green
|
||||
16| "▌ ✓ Count lines in seq/awk output "
|
||||
style 0-0 fg=green
|
||||
style 2-2 fg=green bold
|
||||
style 3-32 bold
|
||||
17| "▌ 200 "
|
||||
style 0-0 fg=green
|
||||
18| "▌ "
|
||||
style 0-0 fg=green
|
||||
19| <blank>
|
||||
20| " Reasoning "
|
||||
style 1-9 fg=bright-black italic
|
||||
21| " The result is 200 lines. The user wants me to reply with just that number and stop. "
|
||||
style 1-83 fg=bright-black italic
|
||||
22| <blank>
|
||||
23| " Assistant "
|
||||
style 1-9 fg=bright-magenta bold
|
||||
24| " 200 "
|
||||
25| "────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-99 dim
|
||||
26| " "
|
||||
style 1-1 inverse
|
||||
27| "────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-99 dim
|
||||
28| "deepseek-v4-flash /workspace/project ↑123 ↓208 cache 99% 3% c"
|
||||
style 0-93 dim
|
||||
style 96-99 dim
|
||||
29-35| <blank>
|
||||
@@ -27,6 +27,8 @@ import * as ToolTodo from '@deepseek-ai/dsh-tool-todo'
|
||||
import * as ToolRalph from '@deepseek-ai/dsh-tool-ralph'
|
||||
import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow'
|
||||
import { createTuiChat, FILE_REFERENCE_PROMPT } from '@deepseek-ai/dsh-tui'
|
||||
import LocalSpillStore from '@deepseek-ai/dsh-spill-local'
|
||||
import * as SpillPolicy from '@deepseek-ai/dsh-spill-policy'
|
||||
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
|
||||
import WorkerWorkflowEngine from '@deepseek-ai/dsh-workflow-workerthread'
|
||||
import { HeadlessTerminal } from '../../../packages/ui/tui/tests/headless-terminal.ts'
|
||||
@@ -56,6 +58,13 @@ interface Scenario {
|
||||
* mounts it; the rest cover the default, todo-free composition.
|
||||
*/
|
||||
enableTodo?: boolean
|
||||
/**
|
||||
* Mount the spill stack (local backend + policy) with this inline cap, as the
|
||||
* shipped configs do. The dispatch-spill scenario proves the durable
|
||||
* `tool/code-dispatch` copy of an oversized sub-result is bounded to a
|
||||
* preview + locator while the program value stays whole.
|
||||
*/
|
||||
spillMaxInlineBytes?: number
|
||||
}
|
||||
|
||||
const SCENARIOS: Scenario[] = [
|
||||
@@ -96,6 +105,14 @@ const SCENARIOS: Scenario[] = [
|
||||
expectedEventCounts: { 'tool/code-dispatch': 2 },
|
||||
recorded: true,
|
||||
},
|
||||
{
|
||||
name: 'code-mode-dispatch-spill',
|
||||
composition: 'code',
|
||||
expectedTools: ['run_code'],
|
||||
expectedEventCounts: { 'tool/code-dispatch-start': 1, 'tool/code-dispatch': 1 },
|
||||
recorded: true,
|
||||
spillMaxInlineBytes: 600,
|
||||
},
|
||||
{
|
||||
name: 'dynamic-workflow',
|
||||
composition: 'native',
|
||||
@@ -225,6 +242,10 @@ async function mountScenarioContext(
|
||||
if (scenario.composition === 'code' || scenario.composition === 'advanced') {
|
||||
await ctx.plugin(WorkerCodeRuntime, {})
|
||||
}
|
||||
if (scenario.spillMaxInlineBytes !== undefined) {
|
||||
await ctx.plugin(LocalSpillStore, { root: join(cwd, '.spill') })
|
||||
await ctx.plugin(SpillPolicy, { maxInlineBytes: scenario.spillMaxInlineBytes })
|
||||
}
|
||||
if (scenario.composition === 'advanced') await ctx.plugin(ToolCordis, { vmTimeoutMs: 5_000 })
|
||||
if (MODE === 'record' && scenario.recorded) {
|
||||
await ctx.plugin(LlmDeepSeek)
|
||||
@@ -344,6 +365,17 @@ async function runScenario(scenario: Scenario): Promise<ScenarioResult> {
|
||||
expect(events.filter(event => event.type === 'user/message' && event.data.source.kind === 'plugin').map(event => (event.data as { content: unknown }).content))
|
||||
.toContainEqual([{ type: 'text', text: 'The user switched this session back to the default mode.' }])
|
||||
}
|
||||
if (scenario.spillMaxInlineBytes !== undefined) {
|
||||
// The REAL pipeline ran (tools execute on replay too): the durable
|
||||
// dispatch copy is bounded to a preview + locator under the run cwd,
|
||||
// while the outer result still carries the program's whole value.
|
||||
const dispatch = events.find(event => (event.type as string) === 'tool/code-dispatch')
|
||||
const content = (dispatch?.data as { content: { type: string; text?: string }[] }).content
|
||||
const text = content.filter(block => block.type === 'text').map(block => block.text ?? '').join('')
|
||||
expect(Buffer.byteLength(text, 'utf8')).toBeLessThanOrEqual(scenario.spillMaxInlineBytes)
|
||||
expect(text).toContain('Full formatted result stored at:')
|
||||
expect(text).toContain('.spill')
|
||||
}
|
||||
expect(events.filter(event => event.type === 'tool/result').every(event => !event.data.isError)).toBe(true)
|
||||
expect(events.filter(event => event.type === 'turn/end').every(event => event.data.reason.kind !== 'error')).toBe(true)
|
||||
if (scenario.name === 'dynamic-workflow' || scenario.name === 'cordis-dynamic-toolchain') {
|
||||
|
||||
@@ -7,7 +7,7 @@ These package-specific rules supplement the repo-wide [conventions](../AGENTS.md
|
||||
- **Product-visible plugins require a non-unit REAL-composition test.** Hand-built `ctx.plugin(...)` suites are insufficient. Boot test-only `cordis.yml` through the Loader and app/process; mock only external/nondeterministic boundaries and assert model-visible, durable, or user-visible output. Keep opt-ins out of shipped defaults. [Policy](../docs/testing.md).
|
||||
- **Initiator-owned private chains derive, then capture.** Under `ctx.agents.withInitiator()`, recover the Agent at each orchestration entry, derive `agent.session`, and let operation-local helpers close over it. Keep `Agent` and `Session` explicit at lifecycle, session-log, service, authority, worker/process, persistence, and wire interfaces; do not widen a leaf helper from `Session` to `Context` merely to hide a parameter ([rationale](../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md)).
|
||||
- **Represent one asynchronous operation with one lifecycle controller or transaction.** Separate readiness, cancellation, disposal, reservation, or sentinel state requires an independent owner or settlement boundary; otherwise fold it while preserving rollback, callback containment, and quiescence.
|
||||
- **Shape capability interfaces around all current consumers.** Keep tool-schema, Loader, UI, transport, and backend-specific behavior in the consumer or adapter; do not let one consumer dictate the interface ([capability-seam rationale](../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)).
|
||||
- **Shape capability interfaces around all current consumers.** Keep tool-schema, Loader, UI, transport, and backend-specific behavior in the consumer or adapter; do not let one consumer dictate the interface ([capability-seam rationale](../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)). Inverse smell: a public service method with one internal caller — pass a private capability closure instead (`RunCodeBridgeOptions`).
|
||||
- **Require a current owner and need.** Tie each abstraction, state machine, option, defensive copy, and compatibility path to a current contract or production consumer, and keep behavior in its owning plugin or service.
|
||||
- **Require evidence for public choices.** Configurability does not justify an unsupported default, public operation set, format, or imported external concept. Use current-consumer evidence or relevant prior art; otherwise require an explicit value or defer the choice.
|
||||
- **Write model-facing contracts from the model's perspective.** Prompts, tool schemas, results, and diagnostics contain only task-relevant concepts, not UI, transport, or implementation vocabulary. Pin stable model-visible text verbatim and dynamic behavior through snapshots or end-to-end coverage.
|
||||
|
||||
@@ -44,7 +44,7 @@ export const AssistantMarkdown = memo(function AssistantMarkdown({ blocks, strea
|
||||
<div className={css.root} data-streaming={streaming || undefined}>
|
||||
{blocks.map((block, i) => {
|
||||
switch (block.kind) {
|
||||
case 'text': return <MarkdownText key={i} text={block.text} />
|
||||
case 'text': return <MarkdownText key={i} text={block.text} streaming={streaming} />
|
||||
case 'reasoning': return <ThinkRow key={i} text={block.text} running={streaming && i === last} />
|
||||
// Tool-call heads render as tool rows in the chat view's grouping pass.
|
||||
case 'tool-call': return null
|
||||
|
||||
@@ -87,14 +87,9 @@ button.leading {
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
/* The code variant's expanded body is the run_code program: monospace on the
|
||||
markdown code-block fill so the program reads as code, not prose. */
|
||||
.root[data-variant='code'] .body {
|
||||
font-family: var(--ds-font-family-code);
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
padding: 6px 8px;
|
||||
margin-left: 22px;
|
||||
border-radius: 6px;
|
||||
background: var(--dsw-alias-markdown-code-block);
|
||||
/* The code variant's expanded body is the run_code program, rendered through
|
||||
the shared CodeBlock (shiki-highlighted TypeScript); only indentation is
|
||||
this row's concern. */
|
||||
.codeBody {
|
||||
margin: 4px 0 4px 22px;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { useState, type KeyboardEvent, type MouseEvent, type ReactNode } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { CodeBlock, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { IconChevronDownOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { ToolRowState, ToolRowVariant } from '../contract/tool-call-model.ts'
|
||||
import css from './ToolRow.module.css'
|
||||
@@ -96,7 +96,9 @@ export function ToolRow({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{open && <div className={css.body}>{body}</div>}
|
||||
{open && (variant === 'code'
|
||||
? <CodeBlock code={body} lang="typescript" className={css.codeBody} />
|
||||
: <div className={css.body}>{body}</div>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// share the store seat exists for) and derives the call material from the
|
||||
// session snapshot — no data of its own.
|
||||
|
||||
import { CodeBlock } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { shallowEqual } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ConversationSnapshot, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { DetailsSlotProps } from '../contract/slots.ts'
|
||||
@@ -89,7 +90,7 @@ export function DetailsPanel({ useSession, useStore, closeDetails }: DetailsPane
|
||||
{material.argsRaw !== null && (
|
||||
<section className={css.section}>
|
||||
<div className={css.sectionLabel}>Input</div>
|
||||
<pre className={css.code}>{pretty(material.argsRaw)}</pre>
|
||||
<CodeBlock code={pretty(material.argsRaw)} lang="json" />
|
||||
</section>
|
||||
)}
|
||||
<section className={css.section}>
|
||||
|
||||
@@ -152,7 +152,7 @@ describe('run_code sub-calls through the real chat machinery', () => {
|
||||
expect(view.getByText('Tool call')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('expanding the code row reveals the program body verbatim', async () => {
|
||||
it('expanding the code row reveals the program body verbatim (shiki-tokenized)', async () => {
|
||||
const parent = 'call-64'
|
||||
const b = await bench(snapshotWith([codeResult(10, parent)], new Map()))
|
||||
const view = mountApp(b.slots)
|
||||
@@ -160,7 +160,12 @@ describe('run_code sub-calls through the real chat machinery', () => {
|
||||
const toggle = view.container.querySelector('[data-variant="code"] button[aria-expanded]')
|
||||
expect(toggle).not.toBeNull()
|
||||
fireEvent.click(toggle!)
|
||||
expect(view.getByText(/const listing = await tools\.bash/)).toBeTruthy()
|
||||
// Shiki splits the program into token spans inside one <pre class="shiki">:
|
||||
// assert the whole text and the highlighted tree rather than one node.
|
||||
const pre = view.container.querySelector('pre.shiki')
|
||||
expect(pre).not.toBeNull()
|
||||
expect(pre!.textContent).toContain('const listing = await tools.bash')
|
||||
expect(pre!.querySelectorAll('span[style]').length).toBeGreaterThan(3)
|
||||
})
|
||||
|
||||
it('an isError sub-call renders the error state dot exactly like a failed native row', async () => {
|
||||
|
||||
@@ -20,11 +20,13 @@
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@shikijs/langs": "^4.3.1",
|
||||
"clsx": "^2.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1"
|
||||
"remark-gfm": "^4.0.1",
|
||||
"shiki": "^4.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
|
||||
@@ -17,6 +17,7 @@ export { FishLogo } from './FishLogo.tsx'
|
||||
export { BrandWordmark } from './BrandWordmark.tsx'
|
||||
export { Tooltip } from './Tooltip.tsx'
|
||||
export type { TooltipSide } from './Tooltip.tsx'
|
||||
export { CodeBlock } from './markdown/CodeBlock.tsx'
|
||||
export { JsonBlock } from './markdown/JsonBlock.tsx'
|
||||
export { MarkdownText } from './markdown/MarkdownText.tsx'
|
||||
export { MessageText } from './markdown/MessageText.tsx'
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/* One code-block geometry for highlighted and plain arms: the shiki <pre>
|
||||
and the fallback <pre> draw identically except for token colors. */
|
||||
|
||||
.block :where(pre) {
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
background: var(--dsw-alias-markdown-code-block);
|
||||
font: var(--dsw-font-markdown-code-block);
|
||||
}
|
||||
|
||||
/* Shiki inlines its theme background var; route it to the repo token. */
|
||||
.block :where(pre.shiki) {
|
||||
background: var(--dsw-alias-markdown-code-block) !important;
|
||||
}
|
||||
|
||||
.block :where(pre) code {
|
||||
font: inherit;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.plain {
|
||||
color: var(--dsw-alias-label-primary);
|
||||
white-space: pre;
|
||||
}
|
||||
37
packages/client/ui-primitives/src/markdown/CodeBlock.tsx
Normal file
37
packages/client/ui-primitives/src/markdown/CodeBlock.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
// CodeBlock: one code surface for every consumer — markdown fences, the
|
||||
// run_code program body, and the details panel's raw args/output — with
|
||||
// shiki highlighting for the registered grammars and an identical-geometry
|
||||
// plain fallback for everything else. Shiki emits a single <pre class="shiki">
|
||||
// tree of nested spans whose colors are --shiki-* custom properties
|
||||
// (token sheets own the values); it produces no scripts or event handlers,
|
||||
// so injecting its output is safe by construction.
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { highlightToHtml } from './highlight.ts'
|
||||
import css from './CodeBlock.module.css'
|
||||
|
||||
export interface CodeBlockProps {
|
||||
/** The source text, rendered verbatim (trailing newline trimmed for display). */
|
||||
code: string
|
||||
/** Grammar hint (markdown fence info string or a fixed caller id); unknown = plain. */
|
||||
lang?: string | undefined
|
||||
/** Extra class merged onto the wrapper (callers position; this component draws). */
|
||||
className?: string | undefined
|
||||
}
|
||||
|
||||
export function CodeBlock({ code, lang, className }: CodeBlockProps) {
|
||||
const trimmed = code.endsWith('\n') ? code.slice(0, -1) : code
|
||||
const html = useMemo(() => highlightToHtml(trimmed, lang), [trimmed, lang])
|
||||
if (html === undefined) {
|
||||
return (
|
||||
<div className={clsx(css.block, className)}>
|
||||
<pre className={css.plain}><code>{trimmed}</code></pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// eslint-disable-next-line react/no-danger -- shiki's output is a static
|
||||
// span tree it generated from `code` (no user HTML passes through), the
|
||||
// sanctioned innerHTML consumption path per shiki's own docs.
|
||||
return <div className={clsx(css.block, className)} dangerouslySetInnerHTML={{ __html: html }} />
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { isValidElement } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import type { Components, UrlTransform } from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { CodeBlock } from './CodeBlock.tsx'
|
||||
import css from './MarkdownText.module.css'
|
||||
|
||||
const remarkPlugins = [remarkGfm]
|
||||
@@ -22,7 +24,9 @@ function sanitizeUrl(url: string): string {
|
||||
|
||||
const safeUrl: UrlTransform = url => sanitizeUrl(url)
|
||||
|
||||
const components: Components = {
|
||||
/** Build the component table; while `streaming`, fences render the plain arm (see CodeBlock). */
|
||||
function buildComponents(streaming: boolean): Components {
|
||||
return {
|
||||
a: ({ href = '', children }) => {
|
||||
const safeHref = sanitizeUrl(href)
|
||||
if (safeHref === '') return <>{children}</>
|
||||
@@ -42,19 +46,40 @@ const components: Components = {
|
||||
<table>{children}</table>
|
||||
</div>
|
||||
),
|
||||
// Fenced blocks route through the shared CodeBlock (shiki for registered
|
||||
// grammars, identical-geometry plain fallback for unknown/absent
|
||||
// languages); inline code keeps the default <code> 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 }) => {
|
||||
/* v8 ignore next 2 -- the markdown pipeline always hands `pre` its single `code` element; the undefined arm guards a react-markdown representation change. */
|
||||
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 <pre> rather than guessing.
|
||||
if (typeof raw !== 'string') return <pre>{children}</pre>
|
||||
const lang = /language-([\w-]+)/.exec(child?.props.className ?? '')?.[1]
|
||||
return <CodeBlock code={raw} lang={streaming ? undefined : lang} />
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
* @param props - Markdown source text preserved by the session projection;
|
||||
* `streaming` renders fences plain (highlighting lands on the finalize swap).
|
||||
* @returns A GFM document with raw HTML, relative links, unsafe protocols, and remote images disabled.
|
||||
*/
|
||||
export function MarkdownText({ text }: { text: string }) {
|
||||
export function MarkdownText({ text, streaming = false }: { text: string; streaming?: boolean }) {
|
||||
return (
|
||||
<div className={css.markdown}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={remarkPlugins}
|
||||
components={components}
|
||||
components={streaming ? streamingComponents : staticComponents}
|
||||
urlTransform={safeUrl}
|
||||
>
|
||||
{text}
|
||||
|
||||
82
packages/client/ui-primitives/src/markdown/highlight.ts
Normal file
82
packages/client/ui-primitives/src/markdown/highlight.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* The client's ONE syntax highlighter: a synchronous fine-grained shiki core
|
||||
* (JavaScript regex engine — no oniguruma WASM, bundle-friendly) with an
|
||||
* explicit grammar allowlist and a CSS-variables theme. Colors live in the
|
||||
* theme package's token sheets as `--shiki-*` custom properties (light and
|
||||
* dark blocks), never here — the repo's tokens-only styling rule.
|
||||
*
|
||||
* Grammars are the set the harness actually renders: TypeScript programs
|
||||
* (`run_code` bodies; TS pulls in JS via grammar embedding), shell commands,
|
||||
* and JSON payloads. An unknown or absent language falls back to plain text
|
||||
* (no highlighting, still monospace) — never an error.
|
||||
*/
|
||||
|
||||
import { createHighlighterCoreSync, createCssVariablesTheme } from 'shiki/core'
|
||||
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'
|
||||
import langTs from '@shikijs/langs/typescript'
|
||||
import langBash from '@shikijs/langs/shellscript'
|
||||
import langJson from '@shikijs/langs/json'
|
||||
import type { HighlighterCore } from 'shiki/core'
|
||||
|
||||
/**
|
||||
* Language ids (and aliases) the singleton registers; everything else renders
|
||||
* plain. A Map, not an object: fence info strings are assistant-authored, so
|
||||
* a label like `constructor` or `__proto__` must miss instead of resolving an
|
||||
* inherited property and crashing the renderer inside shiki.
|
||||
*/
|
||||
const LANG_ALIASES = new Map<string, string>([
|
||||
['typescript', 'typescript'],
|
||||
['ts', 'typescript'],
|
||||
['tsx', 'typescript'],
|
||||
['javascript', 'typescript'],
|
||||
['js', 'typescript'],
|
||||
['shellscript', 'shellscript'],
|
||||
['bash', 'shellscript'],
|
||||
['sh', 'shellscript'],
|
||||
['shell', 'shellscript'],
|
||||
['zsh', 'shellscript'],
|
||||
['json', 'json'],
|
||||
['jsonc', 'json'],
|
||||
])
|
||||
|
||||
/** All token colors resolve through `--shiki-*` custom properties (theme package sheets). */
|
||||
const cssVariablesTheme = createCssVariablesTheme({
|
||||
name: 'css-variables',
|
||||
variablePrefix: '--shiki-',
|
||||
fontStyle: true,
|
||||
})
|
||||
|
||||
let singleton: HighlighterCore | undefined
|
||||
|
||||
/** The synchronous highlighter (one instance per document); pre-warmed below, lazy as the fallback. */
|
||||
function highlighter(): HighlighterCore {
|
||||
singleton ??= createHighlighterCoreSync({
|
||||
themes: [cssVariablesTheme],
|
||||
langs: [langTs, langBash, langJson],
|
||||
engine: createJavaScriptRegexEngine({ forgiving: true }),
|
||||
})
|
||||
return singleton
|
||||
}
|
||||
|
||||
// Engine + grammar construction costs a long task (~120-175ms); building it
|
||||
// during the first finalized fence's render would jank exactly when a stream
|
||||
// completes. Warm the singleton in a deferred task at module load (= plugin
|
||||
// boot) instead; the lazy path above stays as the correctness fallback for a
|
||||
// fence that renders before the timer fires. `unref` (Node-only) keeps a
|
||||
// non-browser import from pinning the event loop.
|
||||
const warmupTimer = setTimeout(() => { highlighter() }, 0)
|
||||
;(warmupTimer as { unref?: () => void }).unref?.()
|
||||
|
||||
/**
|
||||
* Highlight `code` into shiki's HTML (a single `<pre class="shiki">` tree)
|
||||
* when `lang` maps to a registered grammar; `undefined` means the caller
|
||||
* renders its plain fallback.
|
||||
* @param code - the source text.
|
||||
* @param lang - the language hint (a markdown fence info string or a fixed caller id).
|
||||
* @returns the highlighted HTML, or `undefined` for unknown languages.
|
||||
*/
|
||||
export function highlightToHtml(code: string, lang: string | undefined): string | undefined {
|
||||
const resolved = lang === undefined ? undefined : LANG_ALIASES.get(lang.toLowerCase())
|
||||
if (resolved === undefined) return undefined
|
||||
return highlighter().codeToHtml(code, { lang: resolved, theme: 'css-variables' })
|
||||
}
|
||||
53
packages/client/ui-primitives/tests/code-block.spec.tsx
Normal file
53
packages/client/ui-primitives/tests/code-block.spec.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
// @vitest-environment jsdom
|
||||
// CodeBlock + the shiki singleton: registered grammars highlight into token
|
||||
// spans colored by --shiki-* custom properties; unknown/absent languages take
|
||||
// the identical-geometry plain arm; aliases resolve; the trailing newline is
|
||||
// display-trimmed. MarkdownText's fence route is pinned in markdown.spec.tsx
|
||||
// alongside the rest of the markdown family.
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { cleanup, render } from '@testing-library/react'
|
||||
import { afterEach } from 'vitest'
|
||||
import { CodeBlock } from '../src/markdown/CodeBlock.tsx'
|
||||
import { highlightToHtml } from '../src/markdown/highlight.ts'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('highlightToHtml', () => {
|
||||
it('highlights a registered grammar into css-variables token spans', () => {
|
||||
const html = highlightToHtml('const x: number = 1', 'typescript')
|
||||
expect(html).toContain('pre class="shiki css-variables"')
|
||||
expect(html).toContain('var(--shiki-')
|
||||
})
|
||||
|
||||
it.each([['ts'], ['js'], ['bash'], ['sh'], ['jsonc']])('resolves the %s alias', (alias) => {
|
||||
expect(highlightToHtml('x', alias)).toContain('shiki')
|
||||
})
|
||||
|
||||
it('returns undefined for unknown or absent languages', () => {
|
||||
expect(highlightToHtml('x', 'cobol')).toBeUndefined()
|
||||
expect(highlightToHtml('x', undefined)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('CodeBlock', () => {
|
||||
it('renders the highlighted tree for TypeScript', () => {
|
||||
const view = render(<CodeBlock code={'const a = 1\n'} lang="ts" />)
|
||||
const pre = view.container.querySelector('pre.shiki')
|
||||
expect(pre).not.toBeNull()
|
||||
expect(pre!.textContent).toBe('const a = 1')
|
||||
expect(pre!.querySelectorAll('span[style]').length).toBeGreaterThan(1)
|
||||
})
|
||||
|
||||
it('renders the plain arm for an unknown language with the text verbatim', () => {
|
||||
const view = render(<CodeBlock code={'IDENTIFICATION DIVISION.'} lang="cobol" />)
|
||||
expect(view.container.querySelector('pre.shiki')).toBeNull()
|
||||
expect(view.getByText('IDENTIFICATION DIVISION.')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders the plain arm when no language is given', () => {
|
||||
const view = render(<CodeBlock code="plain text" />)
|
||||
expect(view.container.querySelector('pre.shiki')).toBeNull()
|
||||
expect(view.getByText('plain text')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -57,11 +57,41 @@ describe('MarkdownText', () => {
|
||||
expect(container.querySelector('table')?.textContent).toContain('alphabeta')
|
||||
expect(container.querySelector('hr')).not.toBeNull()
|
||||
expect(container.querySelector('pre code')?.textContent).toContain('const answer = 42')
|
||||
// The ts fence routed through the shared CodeBlock: shiki token spans present.
|
||||
expect(container.querySelector('pre.shiki')).not.toBeNull()
|
||||
expect(container.querySelector('br')).not.toBeNull()
|
||||
expect(screen.getByRole('link', { name: 'safe' }).getAttribute('target')).toBe('_blank')
|
||||
expect(screen.getByRole('link', { name: 'https://deepseek.com' })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('a fence labeled with an inherited object key renders plain, never crashing shiki', () => {
|
||||
for (const label of ['constructor', '__proto__', 'toString', 'hasOwnProperty']) {
|
||||
const { container, unmount } = render(<MarkdownText text={'```' + label + '\ncode body\n```'} />)
|
||||
expect(container.querySelector('pre.shiki')).toBeNull()
|
||||
expect(container.querySelector('pre code')?.textContent).toContain('code body')
|
||||
unmount()
|
||||
}
|
||||
})
|
||||
|
||||
it('an empty fence keeps the stock pre; a language-less fence renders the plain CodeBlock arm', () => {
|
||||
const empty = render(<MarkdownText text={'```\n```'} />)
|
||||
expect(empty.container.querySelector('pre')?.outerHTML).toBe('<pre><code></code></pre>')
|
||||
|
||||
const plain = render(<MarkdownText text={'```\nno language here\n```'} />)
|
||||
expect(plain.container.querySelector('pre.shiki')).toBeNull()
|
||||
expect(plain.container.querySelector('pre code')?.textContent).toContain('no language here')
|
||||
})
|
||||
|
||||
it('streaming renders fences plain; the finalize swap highlights them', () => {
|
||||
const fence = '```ts\nconst answer = 42\n```'
|
||||
const live = render(<MarkdownText text={fence} streaming />)
|
||||
expect(live.container.querySelector('pre.shiki')).toBeNull()
|
||||
expect(live.container.querySelector('pre code')?.textContent).toContain('const answer = 42')
|
||||
live.unmount()
|
||||
const done = render(<MarkdownText text={fence} />)
|
||||
expect(done.container.querySelector('pre.shiki')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('neutralizes raw HTML, unsafe or relative links, and remote images', () => {
|
||||
const markdown = [
|
||||
'<script>globalThis.compromised = true</script>',
|
||||
|
||||
31
packages/client/ui-theme/src/styles/shiki.css
Normal file
31
packages/client/ui-theme/src/styles/shiki.css
Normal file
@@ -0,0 +1,31 @@
|
||||
/* Syntax-highlight token palette: the values behind shiki's css-variables
|
||||
theme (--shiki-* custom properties emitted by the ui-primitives CodeBlock).
|
||||
Light values on :root, dark overrides on the body attribute — the same
|
||||
cascade as every other token sheet. Background/foreground deliberately
|
||||
alias the markdown code-block tokens so highlighted and plain blocks agree. */
|
||||
|
||||
:root {
|
||||
--shiki-foreground: var(--dsw-alias-label-primary);
|
||||
--shiki-background: var(--dsw-alias-markdown-code-block);
|
||||
--shiki-token-constant: #1c7ed6;
|
||||
--shiki-token-string: #2f9e44;
|
||||
--shiki-token-comment: #868e96;
|
||||
--shiki-token-keyword: #d6336c;
|
||||
--shiki-token-parameter: #e8590c;
|
||||
--shiki-token-function: #6741d9;
|
||||
--shiki-token-string-expression: #2b8a3e;
|
||||
--shiki-token-punctuation: #495057;
|
||||
--shiki-token-link: #1971c2;
|
||||
}
|
||||
|
||||
body[data-ds-dark-theme] {
|
||||
--shiki-token-constant: #4dabf7;
|
||||
--shiki-token-string: #69db7c;
|
||||
--shiki-token-comment: #adb5bd;
|
||||
--shiki-token-keyword: #faa2c1;
|
||||
--shiki-token-parameter: #ffa94d;
|
||||
--shiki-token-function: #b197fc;
|
||||
--shiki-token-string-expression: #8ce99a;
|
||||
--shiki-token-punctuation: #ced4da;
|
||||
--shiki-token-link: #74c0fc;
|
||||
}
|
||||
@@ -61,6 +61,17 @@
|
||||
background: var(--dsw-alias-state-warn-tertiary);
|
||||
}
|
||||
|
||||
/* run_code sub-dispatch cells: the business tint plus an indent so the
|
||||
nesting under the parent Tool cell reads at a glance. */
|
||||
.tagSubtool {
|
||||
color: var(--dsw-alias-state-business-primary);
|
||||
background: var(--dsw-alias-state-business-tertiary);
|
||||
}
|
||||
|
||||
.root[data-kind='subtool'] {
|
||||
padding-left: 28px;
|
||||
}
|
||||
|
||||
.text {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
|
||||
@@ -4,20 +4,23 @@
|
||||
import type { HTMLAttributes } from 'react'
|
||||
import css from './TrajectoryCell.module.css'
|
||||
|
||||
/** Closed set of trajectory step kinds (call+result fold into Tool; no Think). */
|
||||
export type TrajectoryCellKind = 'user' | 'message' | 'tool'
|
||||
/** Closed set of trajectory step kinds (call+result fold into Tool; no Think;
|
||||
* subtool = one run_code sub-dispatch nested under its Tool cell). */
|
||||
export type TrajectoryCellKind = 'user' | 'message' | 'tool' | 'subtool'
|
||||
|
||||
/** Display label per kind (matches the design tags). */
|
||||
const KIND_LABEL: Record<TrajectoryCellKind, string> = {
|
||||
user: 'User',
|
||||
message: 'Message',
|
||||
tool: 'Tool',
|
||||
subtool: 'Sub',
|
||||
}
|
||||
|
||||
const TAG_CLASS: Record<TrajectoryCellKind, string> = {
|
||||
user: css.tagUser!,
|
||||
message: css.tagMessage!,
|
||||
tool: css.tagTool!,
|
||||
subtool: css.tagSubtool!,
|
||||
}
|
||||
|
||||
export interface TrajectoryCellProps extends HTMLAttributes<HTMLDivElement> {
|
||||
|
||||
@@ -12,9 +12,10 @@ export function TrajectoryView({ useSession }: ConvViewProps) {
|
||||
const nodes = useSession((s) => s.nodes)
|
||||
const partial = useSession((s) => s.partial)
|
||||
const runningCalls = useSession((s) => s.runningCalls)
|
||||
const codeDispatches = useSession((s) => s.codeDispatches)
|
||||
const turns = useMemo(
|
||||
() => deriveTrajectoryLayout({ nodes, partial, runningCalls }),
|
||||
[nodes, partial, runningCalls],
|
||||
() => deriveTrajectoryLayout({ nodes, partial, runningCalls, codeDispatches }),
|
||||
[nodes, partial, runningCalls, codeDispatches],
|
||||
)
|
||||
if (turns.length === 0) {
|
||||
return <div className={css.root}><p className={css.empty}>暂无轨迹数据</p></div>
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
// WaterfallView: P-I placeholder body for the waterfall tab — span stats
|
||||
// header over node-count bars per turn standing in for duration lanes (no
|
||||
// timing data yet; deviation ledger #3 defers real rendering to P-III).
|
||||
// WaterfallView: span stats header over per-turn node-count lanes (P-I
|
||||
// stand-in for duration lanes; deviation ledger #3). run_code turns
|
||||
// additionally draw TRUTHFUL sub-call lanes: the dispatch start/settle pair
|
||||
// carries per-sub-call wall time, so each sub-span's width is its real
|
||||
// duration against the parent turn's dispatch window.
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import type { ConvViewProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import { deriveSpans } from './spans.ts'
|
||||
import { deriveSpans, deriveSubSpans } from './spans.ts'
|
||||
import { TrajectoryStatsHeader } from './TrajectoryStatsHeader.tsx'
|
||||
import css from './views.module.css'
|
||||
|
||||
/** Bar width scale: px per node, clamped so tiny windows still show a bar. */
|
||||
const PX_PER_NODE = 14
|
||||
const MIN_BAR_PX = 8
|
||||
/** Sub-span lane width budget (the parent window scales into this). */
|
||||
const SUB_LANE_PX = 220
|
||||
|
||||
/** Optional density override (test/standalone knob; the register site passes nothing). */
|
||||
export interface WaterfallExtraProps {
|
||||
@@ -21,27 +25,48 @@ export interface WaterfallExtraProps {
|
||||
export function WaterfallView({ useSession, pxPerNode }: ConvViewProps & WaterfallExtraProps) {
|
||||
const scale = pxPerNode ?? PX_PER_NODE
|
||||
const nodes = useSession((s) => s.nodes)
|
||||
const codeDispatches = useSession((s) => s.codeDispatches)
|
||||
const spans = useMemo(() => deriveSpans(nodes), [nodes])
|
||||
const subSpans = useMemo(() => deriveSubSpans(nodes, codeDispatches), [nodes, codeDispatches])
|
||||
if (spans.length === 0) return <div className={css.root}><p className={css.empty}>暂无瀑布数据</p></div>
|
||||
return (
|
||||
<>
|
||||
<TrajectoryStatsHeader useSession={useSession} />
|
||||
<div className={css.root}>
|
||||
{spans.map((span, i) => (
|
||||
<div key={span.turn} className={css.row} style={{ paddingLeft: i * 12 }}>
|
||||
<span className={css.turnTag}>turn {span.turn}</span>
|
||||
<span
|
||||
className={css.bar}
|
||||
style={{ width: Math.max(span.nodes * scale, MIN_BAR_PX) }}
|
||||
title={`${span.nodes} nodes`}
|
||||
/>
|
||||
{span.calls > 0 && (
|
||||
<div key={span.turn}>
|
||||
<div className={css.row} style={{ paddingLeft: i * 12 }}>
|
||||
<span className={css.turnTag}>turn {span.turn}</span>
|
||||
<span
|
||||
className={`${css.bar} ${css.barCalls}`}
|
||||
style={{ width: Math.max(span.calls * scale, MIN_BAR_PX) }}
|
||||
title={`${span.calls} tool calls`}
|
||||
className={css.bar}
|
||||
style={{ width: Math.max(span.nodes * scale, MIN_BAR_PX) }}
|
||||
title={`${span.nodes} nodes`}
|
||||
/>
|
||||
)}
|
||||
{span.calls > 0 && (
|
||||
<span
|
||||
className={`${css.bar} ${css.barCalls}`}
|
||||
style={{ width: Math.max(span.calls * scale, MIN_BAR_PX) }}
|
||||
title={`${span.calls} tool calls`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{(subSpans.get(span.turn) ?? []).map((lane) => (
|
||||
<div key={lane.callId} className={css.subRow} data-subspan style={{ paddingLeft: i * 12 + 24 }}>
|
||||
<span className={css.subTag}>{lane.name}</span>
|
||||
<span
|
||||
className={`${css.bar} ${css.barSub}`}
|
||||
data-timing={lane.timing}
|
||||
style={{
|
||||
marginLeft: Math.round(lane.offsetFraction * SUB_LANE_PX),
|
||||
width: Math.max(Math.round(lane.widthFraction * SUB_LANE_PX), 4),
|
||||
}}
|
||||
title={lane.timing === 'measured'
|
||||
/* durationMs is non-null exactly when timing is measured. */
|
||||
? `${lane.name} · ${((lane.durationMs ?? 0) / 1000).toFixed(2)}s`
|
||||
: lane.timing === 'running' ? `${lane.name} · running` : `${lane.name} · duration unknown`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
import type {
|
||||
AssistantMessageNode,
|
||||
CodeSubCall,
|
||||
ConversationSnapshot,
|
||||
ToolResultNode,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
@@ -27,6 +28,8 @@ export interface TrajectoryLayoutInput {
|
||||
nodes: ConversationSnapshot['nodes']
|
||||
partial: ConversationSnapshot['partial']
|
||||
runningCalls: ConversationSnapshot['runningCalls']
|
||||
/** run_code sub-dispatches by parent callId (sub-cells nest under the parent Tool cell). */
|
||||
codeDispatches: ConversationSnapshot['codeDispatches']
|
||||
}
|
||||
|
||||
interface UsageLike {
|
||||
@@ -49,7 +52,7 @@ interface LaidCell {
|
||||
* @returns turns ordered by first appearance.
|
||||
*/
|
||||
export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly TrajectoryTurnModel[] {
|
||||
const { nodes, partial, runningCalls } = input
|
||||
const { nodes, partial, runningCalls, codeDispatches } = input
|
||||
const resultByCall = indexResults(nodes)
|
||||
const turns = new Map<number, { message: LaidCell[]; steps: Map<number, LaidCell[]> }>()
|
||||
let index = 0
|
||||
@@ -96,7 +99,7 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
|
||||
continue
|
||||
}
|
||||
if (node.kind === 'assistant') {
|
||||
const laidList = expandAssistant(node, index + 1, prevAbsTime, resultByCall)
|
||||
const laidList = withSubCalls(expandAssistant(node, index + 1, prevAbsTime, resultByCall), codeDispatches)
|
||||
for (const laid of laidList) {
|
||||
if (node.step > 0) pushStep(node.turn, node.step, laid)
|
||||
else pushMessage(node.turn, laid)
|
||||
@@ -128,6 +131,10 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
|
||||
timeSeconds: durationSeconds(node.time, node.callTime),
|
||||
},
|
||||
})
|
||||
for (const laid of expandSubCalls(codeDispatches.get(node.callId), index)) {
|
||||
pushStep(0, 1, laid)
|
||||
index = laid.cell.index
|
||||
}
|
||||
}
|
||||
prevAbsTime = finiteTime(node.time) ?? prevAbsTime
|
||||
}
|
||||
@@ -161,6 +168,10 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
|
||||
timeSeconds: null,
|
||||
},
|
||||
})
|
||||
for (const laid of expandSubCalls(codeDispatches.get(call.callId), index)) {
|
||||
pushStep(call.turn, call.step > 0 ? call.step : 1, laid)
|
||||
index = laid.cell.index
|
||||
}
|
||||
}
|
||||
|
||||
// Orphan turn-0 cells (orphaned tools / steering turn 0) fold into Turn 1.
|
||||
@@ -387,6 +398,53 @@ function collectCallIds(
|
||||
return ids
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Interleave each tool cell's run_code sub-dispatch cells right after it, reindexing followers. */
|
||||
function withSubCalls(laidList: LaidCell[], codeDispatches: ConversationSnapshot['codeDispatches']): LaidCell[] {
|
||||
if (codeDispatches.size === 0) return laidList
|
||||
const out: LaidCell[] = []
|
||||
let index = laidList[0] !== undefined ? laidList[0].cell.index - 1 : 0
|
||||
for (const laid of laidList) {
|
||||
out.push({ ...laid, cell: { ...laid.cell, index: ++index } })
|
||||
if (laid.callId === undefined) continue
|
||||
for (const sub of expandSubCalls(codeDispatches.get(laid.callId), index)) {
|
||||
out.push(sub)
|
||||
index = sub.cell.index
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** Sub-dispatch cells for one run_code parent, in start order (running = null duration). */
|
||||
function expandSubCalls(
|
||||
subs: readonly CodeSubCall[] | undefined,
|
||||
startIndex: number,
|
||||
): LaidCell[] {
|
||||
if (subs === undefined || subs.length === 0) return []
|
||||
const out: LaidCell[] = []
|
||||
let index = startIndex
|
||||
for (const sub of subs) {
|
||||
const settled = 'kind' in sub
|
||||
out.push({
|
||||
absTime: settled ? finiteTime(sub.callTime ?? sub.time) : finiteTime(sub.time),
|
||||
toolName: settled ? sub.call?.name ?? sub.callId : sub.name,
|
||||
callId: sub.callId,
|
||||
cell: {
|
||||
index: ++index,
|
||||
kind: 'subtool',
|
||||
text: settled
|
||||
? (sub.call !== null ? summarizeCall(sub.call.name, sub.call.argsRaw) : summarizeResult(sub))
|
||||
: summarizeCall(sub.name, sub.argsRaw),
|
||||
// PR3's start/settle pair carries per-sub-call wall time; a running
|
||||
// (unsettled) or pre-pair log entry shows the em dash.
|
||||
timeSeconds: settled ? durationSeconds(sub.time, sub.callTime) : null,
|
||||
},
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function summarizeCall(name: string, argsRaw: string): string {
|
||||
const args = argsRaw.replace(/\s+/g, ' ').trim()
|
||||
if (args === '') return name
|
||||
|
||||
@@ -5,6 +5,24 @@
|
||||
*/
|
||||
import type { ConversationNode, ConversationSnapshot } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
|
||||
/** One run_code sub-dispatch lane in the waterfall: real timing off the start/settle pair. */
|
||||
export interface SubSpanLane {
|
||||
callId: string
|
||||
name: string
|
||||
/** Wall duration in ms; null unless both endpoints were observed (`timing: 'measured'`). */
|
||||
durationMs: number | null
|
||||
/**
|
||||
* Timing provenance: `measured` = start/settle pair observed; `running` =
|
||||
* start seen, settle pending; `unknown` = settle-only replay window (the
|
||||
* start fell outside), so no duration claim is possible.
|
||||
*/
|
||||
timing: 'measured' | 'running' | 'unknown'
|
||||
/** Start offset as a fraction of the parent turn's dispatch window [0, 1). */
|
||||
offsetFraction: number
|
||||
/** Width as a fraction of the window (running lanes extend to the window end). */
|
||||
widthFraction: number
|
||||
}
|
||||
|
||||
/** One turn's worth of activity, folded from the snapshot node window. */
|
||||
export interface TurnSpan {
|
||||
turn: number
|
||||
@@ -69,3 +87,61 @@ export function deriveSpanStats(spans: readonly TurnSpan[]): SpanStats {
|
||||
function hasTurn(node: ConversationNode): node is ConversationNode & { turn: number } {
|
||||
return node.kind === 'assistant' || node.kind === 'steering'
|
||||
}
|
||||
|
||||
/**
|
||||
* Fold the dispatch index into per-turn sub-span lanes with REAL timing: each
|
||||
* lane's offset/width scale against its parent turn's dispatch window (first
|
||||
* start → last settle). Running (unsettled) lanes extend to the window end
|
||||
* with a null duration.
|
||||
* @param nodes - snapshot nodes (locates each parent run_code call's turn).
|
||||
* @param codeDispatches - the snapshot's dispatch index.
|
||||
* @returns lanes keyed by turn, in start order.
|
||||
*/
|
||||
export function deriveSubSpans(
|
||||
nodes: ConversationSnapshot['nodes'],
|
||||
codeDispatches: ConversationSnapshot['codeDispatches'],
|
||||
): ReadonlyMap<number, readonly SubSpanLane[]> {
|
||||
const out = new Map<number, SubSpanLane[]>()
|
||||
if (codeDispatches.size === 0) return out
|
||||
const turnByCall = new Map<string, number>()
|
||||
let currentTurn = 0
|
||||
for (const node of nodes) {
|
||||
if (node.kind === 'assistant' || node.kind === 'steering') currentTurn = node.turn
|
||||
if (node.kind === 'tool-result') turnByCall.set(node.callId, currentTurn)
|
||||
}
|
||||
for (const [parent, subs] of codeDispatches) {
|
||||
if (subs.length === 0) continue
|
||||
const turn = turnByCall.get(parent) ?? currentTurn
|
||||
// A settle-only entry (callTime null: its start fell outside the replay
|
||||
// window) anchors the window by its settle time — a real observation —
|
||||
// but must never masquerade as a measured zero-duration span.
|
||||
const starts: number[] = []
|
||||
const ends: number[] = []
|
||||
for (const sub of subs) {
|
||||
const settled = 'kind' in sub
|
||||
const start = settled ? sub.callTime ?? sub.time : sub.time
|
||||
starts.push(start)
|
||||
ends.push(settled ? sub.time : start)
|
||||
}
|
||||
const windowStart = Math.min(...starts)
|
||||
const windowEnd = Math.max(...ends, windowStart + 1)
|
||||
const windowSpan = windowEnd - windowStart
|
||||
const lanes: SubSpanLane[] = subs.map((sub, i) => {
|
||||
const settled = 'kind' in sub
|
||||
const timing = settled ? (sub.callTime === null ? 'unknown' as const : 'measured' as const) : 'running' as const
|
||||
const start = starts[i] ?? windowStart
|
||||
const end = settled ? sub.time : windowEnd
|
||||
return {
|
||||
callId: sub.callId,
|
||||
name: settled ? sub.call?.name ?? sub.callId : sub.name,
|
||||
durationMs: timing === 'measured' ? Math.max(0, end - start) : null,
|
||||
timing,
|
||||
offsetFraction: (start - windowStart) / windowSpan,
|
||||
widthFraction: Math.max((end - start) / windowSpan, 0.02),
|
||||
}
|
||||
})
|
||||
const existing = out.get(turn) ?? []
|
||||
out.set(turn, [...existing, ...lanes])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -45,3 +45,38 @@
|
||||
color: var(--dsw-alias-label-caption);
|
||||
font: var(--dsw-font-xs-13);
|
||||
}
|
||||
|
||||
/* run_code sub-span lanes: one row per sub-dispatch under its turn row,
|
||||
offset/width scaled to the dispatch window (real wall time). A running
|
||||
lane pulses via reduced opacity until its settle arrives. */
|
||||
.subRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.subTag {
|
||||
flex: none;
|
||||
width: 88px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
font: var(--dsw-font-xs-13);
|
||||
}
|
||||
|
||||
.barSub {
|
||||
height: 8px;
|
||||
background: var(--dsw-alias-state-business-primary);
|
||||
}
|
||||
|
||||
.barSub[data-timing='running'] {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
/* Settle-only replay entries: no measured span — hollow, not a solid bar. */
|
||||
.barSub[data-timing='unknown'] {
|
||||
background: transparent;
|
||||
border: 1px dashed var(--dsw-alias-state-business-primary);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('deriveTrajectoryLayout', () => {
|
||||
content: [{ type: 'text', text: 'a.txt' }], isError: false, callView: null, resultView: null,
|
||||
},
|
||||
] as unknown as ConversationSnapshot['nodes']
|
||||
const turns = deriveTrajectoryLayout({ nodes, partial: null, runningCalls: [] })
|
||||
const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
|
||||
expect(turns).toHaveLength(1)
|
||||
expect(turns[0]?.turn).toBe(1)
|
||||
const kinds = turns[0]?.groups.flatMap((g) => g.cells.map((c) => c.kind))
|
||||
@@ -86,6 +86,7 @@ describe('deriveTrajectoryLayout', () => {
|
||||
|
||||
it('adds runningCalls not already present and leaves their time blank', () => {
|
||||
const turns = deriveTrajectoryLayout({
|
||||
codeDispatches: new Map(),
|
||||
nodes: [] as unknown as ConversationSnapshot['nodes'],
|
||||
partial: null,
|
||||
runningCalls: [{
|
||||
@@ -111,7 +112,7 @@ describe('deriveTrajectoryLayout', () => {
|
||||
usage: { inputTokens: 1, outputTokens: 2, reasoningTokens: 3 },
|
||||
},
|
||||
] as unknown as ConversationSnapshot['nodes']
|
||||
const turns = deriveTrajectoryLayout({ nodes, partial: null, runningCalls: [] })
|
||||
const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
|
||||
const cells = turns[0]?.groups.flatMap((g) => g.cells) ?? []
|
||||
expect(cells.find((c) => c.kind === 'message')?.timeSeconds).toBeNull()
|
||||
expect(turns[0]?.groups.find((g) => g.title === 'Step 1')?.description).toBeUndefined()
|
||||
@@ -137,7 +138,7 @@ describe('deriveTrajectoryLayout', () => {
|
||||
content: [], isError: false, callView: null, resultView: null,
|
||||
},
|
||||
] as unknown as ConversationSnapshot['nodes']
|
||||
const turns = deriveTrajectoryLayout({ nodes, partial: null, runningCalls: [] })
|
||||
const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
|
||||
expect(turns[0]?.groups[0]?.description).toBe('2.9s bash×2')
|
||||
})
|
||||
|
||||
@@ -154,7 +155,7 @@ describe('deriveTrajectoryLayout', () => {
|
||||
blocks: [{ kind: 'text', text: 'ok2' }],
|
||||
},
|
||||
] as unknown as ConversationSnapshot['nodes']
|
||||
const turns = deriveTrajectoryLayout({ nodes, partial: null, runningCalls: [] })
|
||||
const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
|
||||
expect(turns.map((t) => t.turn)).toEqual([1, 2])
|
||||
expect(turns[0]?.groups.flatMap((g) => g.cells.map((c) => c.text))).toEqual(['first', 'ok1'])
|
||||
expect(turns[1]?.groups.flatMap((g) => g.cells.map((c) => c.text))).toEqual(['second', 'ok2'])
|
||||
@@ -168,7 +169,7 @@ describe('deriveTrajectoryLayout', () => {
|
||||
usage: { inputTokens: 11, outputTokens: 22, reasoningTokens: 3 },
|
||||
},
|
||||
] as unknown as ConversationSnapshot['nodes']
|
||||
const turns = deriveTrajectoryLayout({ nodes, partial: null, runningCalls: [] })
|
||||
const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
|
||||
const message = turns[0]?.groups.flatMap((g) => g.cells).find((c) => c.kind === 'message')
|
||||
expect(message).toMatchObject({
|
||||
text: '', input: 11, output: 22, think: 3,
|
||||
@@ -196,7 +197,7 @@ describe('deriveTrajectoryLayout', () => {
|
||||
blocks: [{ kind: 'text', text: 'done' }],
|
||||
},
|
||||
] as unknown as ConversationSnapshot['nodes']
|
||||
const turns = deriveTrajectoryLayout({ nodes, partial: null, runningCalls: [] })
|
||||
const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
|
||||
const message = turns[0]?.groups
|
||||
.flatMap((g) => g.cells)
|
||||
.find((c) => c.kind === 'message' && c.text === 'done')
|
||||
@@ -204,3 +205,51 @@ describe('deriveTrajectoryLayout', () => {
|
||||
expect(message?.timeSeconds).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('run_code sub-dispatch cells', () => {
|
||||
const runCodeNodes = [
|
||||
{
|
||||
kind: 'assistant', seq: 2, time: 6_000, turn: 1, step: 1,
|
||||
blocks: [
|
||||
{ kind: 'tool-call', callId: 'p1', name: 'run_code', argsRaw: '{"code":"…","description":"批量读取"}' },
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'tool-result', seq: 3, time: 9_000, callId: 'p1',
|
||||
call: { name: 'run_code', argsRaw: '{"code":"…","description":"批量读取"}' }, callTime: 6_200,
|
||||
content: [{ type: 'text', text: 'done' }], isError: false, callView: null, resultView: null,
|
||||
},
|
||||
] as unknown as ConversationSnapshot['nodes']
|
||||
|
||||
const settledSub = (n: number, name: string, start: number, end: number) => ({
|
||||
kind: 'tool-result' as const, seq: 100 + n, time: end,
|
||||
callId: `p1:code:${n}`,
|
||||
call: { name, argsRaw: '{"x":1}' }, callTime: start,
|
||||
content: [{ type: 'text' as const, text: 'ok' }], isError: false, callView: null, resultView: null,
|
||||
})
|
||||
|
||||
it('nests settled sub-cells after their parent Tool cell with real durations', () => {
|
||||
const codeDispatches = new Map([['p1', [
|
||||
settledSub(1, 'bash', 6_300, 7_300),
|
||||
settledSub(2, 'read', 7_300, 7_800),
|
||||
]]]) as unknown as ConversationSnapshot['codeDispatches']
|
||||
const turns = deriveTrajectoryLayout({ codeDispatches, nodes: runCodeNodes, partial: null, runningCalls: [] })
|
||||
const cells = turns[0]!.groups.flatMap((g) => g.cells)
|
||||
expect(cells.map((c) => c.kind)).toEqual(['tool', 'subtool', 'subtool'])
|
||||
// Sequential indexes across the interleave; durations from the pair times.
|
||||
expect(cells.map((c) => c.index)).toEqual([1, 2, 3])
|
||||
expect(cells[1]).toMatchObject({ text: 'bash · {"x":1}', timeSeconds: 1 })
|
||||
expect(cells[2]).toMatchObject({ timeSeconds: 0.5 })
|
||||
})
|
||||
|
||||
it('a running (unsettled) sub-call renders a subtool cell with blank time', () => {
|
||||
const running = {
|
||||
callId: 'p1:code:1', name: 'grep', argsRaw: '{"pattern":"x"}',
|
||||
turn: 0, step: 0, time: 6_400, callView: null,
|
||||
}
|
||||
const codeDispatches = new Map([['p1', [running]]]) as unknown as ConversationSnapshot['codeDispatches']
|
||||
const turns = deriveTrajectoryLayout({ codeDispatches, nodes: runCodeNodes, partial: null, runningCalls: [] })
|
||||
const sub = turns[0]!.groups.flatMap((g) => g.cells).find((c) => c.kind === 'subtool')
|
||||
expect(sub).toMatchObject({ text: 'grep · {"pattern":"x"}', timeSeconds: null })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,7 +21,7 @@ import type { ConvViewProps, ViewTab } from '@deepseek-ai/dsh-client-ui-conversa
|
||||
import { ConversationRoot, type ConversationRootProps } from '@deepseek-ai/dsh-client-ui-conversation/src/client/skeleton/ConversationRoot.tsx'
|
||||
import { createChatStore } from '@deepseek-ai/dsh-client-ui-conversation/src/client/stores.ts'
|
||||
import { apply, inject } from '@deepseek-ai/dsh-client-ui-trajectory/client'
|
||||
import { deriveSpans, deriveSpanStats } from '@deepseek-ai/dsh-client-ui-trajectory/src/client/spans.ts'
|
||||
import { deriveSpans, deriveSpanStats, deriveSubSpans } from '@deepseek-ai/dsh-client-ui-trajectory/src/client/spans.ts'
|
||||
import { TrajectoryStatsHeader } from '@deepseek-ai/dsh-client-ui-trajectory/src/client/TrajectoryStatsHeader.tsx'
|
||||
import { TrajectoryView } from '@deepseek-ai/dsh-client-ui-trajectory/src/client/TrajectoryView.tsx'
|
||||
import { WaterfallView } from '@deepseek-ai/dsh-client-ui-trajectory/src/client/WaterfallView.tsx'
|
||||
@@ -54,7 +54,7 @@ const NODES = [
|
||||
|
||||
function fakeSession(nodes: ConversationSnapshot['nodes']) {
|
||||
const store = createSnapshotStore({
|
||||
nodes, partial: null, runningCalls: [] as ConversationSnapshot['runningCalls'],
|
||||
nodes, partial: null, runningCalls: [] as ConversationSnapshot['runningCalls'], codeDispatches: new Map(),
|
||||
})
|
||||
return { store, useSession: bindSnapshotSelector(store) as unknown as UseSession<ConversationSnapshot> }
|
||||
}
|
||||
@@ -117,7 +117,7 @@ function tabsOf(slots: SlotsService): ViewTab[] {
|
||||
function mount(slots: SlotsService, nodes: ConversationSnapshot['nodes'] = NODES) {
|
||||
const sessionSnapshot = createSnapshotStore({
|
||||
running: false, removed: false, promptError: null, nodes,
|
||||
partial: null, runningCalls: [] as ConversationSnapshot['runningCalls'],
|
||||
partial: null, runningCalls: [] as ConversationSnapshot['runningCalls'], codeDispatches: new Map(),
|
||||
})
|
||||
const useSession = bindSnapshotSelector(sessionSnapshot) as unknown as UseSession<ConversationSnapshot>
|
||||
const chat = createChatStore().create()
|
||||
@@ -260,3 +260,115 @@ describe('node half', () => {
|
||||
expect(nodeApply()).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('deriveSubSpans (waterfall lanes)', () => {
|
||||
const dispatchNodes = [
|
||||
{ kind: 'assistant', seq: 2, time: 6_000, turn: 3, step: 1, blocks: [] },
|
||||
{
|
||||
kind: 'tool-result', seq: 3, time: 9_000, callId: 'p1',
|
||||
call: { name: 'run_code', argsRaw: '{}' }, callTime: 6_100,
|
||||
content: [], isError: false, callView: null, resultView: null,
|
||||
},
|
||||
] as unknown as ConversationSnapshot['nodes']
|
||||
|
||||
it('scales settled lanes into the dispatch window with real durations', () => {
|
||||
const codeDispatches = new Map([['p1', [
|
||||
{
|
||||
kind: 'tool-result', seq: 101, time: 7_000, callId: 'p1:code:1',
|
||||
call: { name: 'bash', argsRaw: '{}' }, callTime: 6_200,
|
||||
content: [], isError: false, callView: null, resultView: null,
|
||||
},
|
||||
{
|
||||
kind: 'tool-result', seq: 102, time: 8_200, callId: 'p1:code:2',
|
||||
call: { name: 'read', argsRaw: '{}' }, callTime: 7_000,
|
||||
content: [], isError: false, callView: null, resultView: null,
|
||||
},
|
||||
]]]) as unknown as ConversationSnapshot['codeDispatches']
|
||||
const lanes = deriveSubSpans(dispatchNodes, codeDispatches)
|
||||
const turn3 = lanes.get(3)
|
||||
expect(turn3).toHaveLength(2)
|
||||
// Window = 6200..8200 (2000ms). bash: 0..0.4; read: 0.4..1.0.
|
||||
expect(turn3?.[0]).toMatchObject({ name: 'bash', durationMs: 800, timing: 'measured', offsetFraction: 0 })
|
||||
expect(turn3?.[0]?.widthFraction).toBeCloseTo(0.4)
|
||||
expect(turn3?.[1]).toMatchObject({ name: 'read', durationMs: 1200 })
|
||||
expect(turn3?.[1]?.offsetFraction).toBeCloseTo(0.4)
|
||||
})
|
||||
|
||||
it('a running lane extends to the window end with a null duration', () => {
|
||||
const codeDispatches = new Map([['p1', [
|
||||
{
|
||||
kind: 'tool-result', seq: 101, time: 8_000, callId: 'p1:code:1',
|
||||
call: { name: 'bash', argsRaw: '{}' }, callTime: 6_200,
|
||||
content: [], isError: false, callView: null, resultView: null,
|
||||
},
|
||||
{ callId: 'p1:code:2', name: 'grep', argsRaw: '{}', turn: 0, step: 0, time: 7_000, callView: null },
|
||||
]]]) as unknown as ConversationSnapshot['codeDispatches']
|
||||
const lanes = deriveSubSpans(dispatchNodes, codeDispatches)
|
||||
const running = lanes.get(3)?.find((lane) => lane.name === 'grep')
|
||||
expect(running).toMatchObject({ durationMs: null, timing: 'running' })
|
||||
// Extends from its start to the window end.
|
||||
expect(running!.offsetFraction + running!.widthFraction).toBeCloseTo(1)
|
||||
})
|
||||
|
||||
it('a settle-only entry (null callTime) is unknown timing, never a measured 0 ms', () => {
|
||||
const codeDispatches = new Map([['p1', [
|
||||
{
|
||||
kind: 'tool-result', seq: 101, time: 8_000, callId: 'p1:code:1',
|
||||
call: { name: 'bash', argsRaw: '{}' }, callTime: null,
|
||||
content: [], isError: false, callView: null, resultView: null,
|
||||
},
|
||||
]]]) as unknown as ConversationSnapshot['codeDispatches']
|
||||
const lane = deriveSubSpans(dispatchNodes, codeDispatches).get(3)?.[0]
|
||||
expect(lane).toMatchObject({ durationMs: null, timing: 'unknown' })
|
||||
})
|
||||
|
||||
it('waterfall renders sub-span lanes under the owning turn row', () => {
|
||||
const codeDispatches = new Map([['p1', [
|
||||
{
|
||||
kind: 'tool-result', seq: 101, time: 8_000, callId: 'p1:code:1',
|
||||
call: { name: 'bash', argsRaw: '{}' }, callTime: 6_200,
|
||||
content: [], isError: false, callView: null, resultView: null,
|
||||
},
|
||||
]]]) as unknown as ConversationSnapshot['codeDispatches']
|
||||
const store = createSnapshotStore({
|
||||
nodes: dispatchNodes, partial: null,
|
||||
runningCalls: [] as ConversationSnapshot['runningCalls'], codeDispatches,
|
||||
})
|
||||
const props = {
|
||||
sessionId: SID,
|
||||
useSession: bindSnapshotSelector(store) as unknown as UseSession<ConversationSnapshot>,
|
||||
useSessions: emptySessions(),
|
||||
useWorkspaces: emptyWorkspaces(),
|
||||
} as unknown as ConvViewProps
|
||||
const view = render(createElement(WaterfallView as FC<ConvViewProps>, props))
|
||||
const lane = view.container.querySelector('[data-subspan]')
|
||||
expect(lane).not.toBeNull()
|
||||
expect(lane!.textContent).toContain('bash')
|
||||
expect(lane!.querySelector('[title*="1.80s"]')).not.toBeNull()
|
||||
expect(lane!.querySelector('[data-timing="measured"]')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('waterfall labels a settle-only lane as duration unknown', () => {
|
||||
const codeDispatches = new Map([['p1', [
|
||||
{
|
||||
kind: 'tool-result', seq: 101, time: 8_000, callId: 'p1:code:1',
|
||||
call: { name: 'read', argsRaw: '{}' }, callTime: null,
|
||||
content: [], isError: false, callView: null, resultView: null,
|
||||
},
|
||||
]]]) as unknown as ConversationSnapshot['codeDispatches']
|
||||
const store = createSnapshotStore({
|
||||
nodes: dispatchNodes, partial: null,
|
||||
runningCalls: [] as ConversationSnapshot['runningCalls'], codeDispatches,
|
||||
})
|
||||
const props = {
|
||||
sessionId: SID,
|
||||
useSession: bindSnapshotSelector(store) as unknown as UseSession<ConversationSnapshot>,
|
||||
useSessions: emptySessions(),
|
||||
useWorkspaces: emptyWorkspaces(),
|
||||
} as unknown as ConvViewProps
|
||||
const view = render(createElement(WaterfallView as FC<ConvViewProps>, props))
|
||||
const bar = view.container.querySelector('[data-timing="unknown"]')
|
||||
expect(bar).not.toBeNull()
|
||||
expect(bar!.getAttribute('title')).toContain('duration unknown')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/* Shell-owned global base: full-height mount plus the theme token sheets.
|
||||
* The three ui-theme sheets are the sole token source (--dsw-*); the shell
|
||||
* The four ui-theme sheets are the sole token source (--dsw-*); the shell
|
||||
* links them here so tokens exist before any plugin CSS lands. */
|
||||
@import '@deepseek-ai/dsh-client-ui-theme/styles/base.css';
|
||||
@import '@deepseek-ai/dsh-client-ui-theme/styles/design-platform.css';
|
||||
@import '@deepseek-ai/dsh-client-ui-theme/styles/gradient-shadow-text.css';
|
||||
@import '@deepseek-ai/dsh-client-ui-theme/styles/shiki.css';
|
||||
|
||||
html,
|
||||
body,
|
||||
|
||||
@@ -1218,6 +1218,13 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
||||
jsDoc: '/**\n * A tool was registered or unregistered, or a scoped restriction changed\n * (the available tool set changed — possibly for one scope only). An\n * UNFILTERED registry-subject notification, deliberately not scope-filtered\n * dispatch: a global change concerns every agent\'s next assembly, so a\n * scoped listener subscribing here sees every change, not just its own\n * scope\'s.\n * @mode emit\n */',
|
||||
summary: 'A tool was registered or unregistered, or a scoped restriction changed (the available tool set changed — possibly for one scope only).',
|
||||
},
|
||||
{
|
||||
name: 'tools/code-dispatch-log',
|
||||
mode: 'waterfall',
|
||||
signature: '\'tools/code-dispatch-log\'(this: Scoped<ToolRegistry>, dispatch: CodeDispatchLog, next: () => Promise<ContentBlock[]>): Promise<ContentBlock[]>',
|
||||
jsDoc: '/**\n * Shape the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before\n * the bridge appends its `tool/code-dispatch` event. `next()` keeps the\n * content unchanged; a listener may return replacement blocks (e.g. the\n * spill policy\'s preview + locator for an oversized text result). Only the\n * logged copy is affected — the program already received the complete\n * value, and the model sees neither. A throwing listener is contained:\n * the bridge falls back to logging the unshaped content.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent\'s dispatches.\n * @param dispatch - the parent execution, sub-call identity, and the settled content to log.\n * @mode waterfall\n */',
|
||||
summary: 'Shape the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before the bridge appends its `tool/code-dispatch` event.',
|
||||
},
|
||||
{
|
||||
name: 'tools/execute',
|
||||
mode: 'waterfall',
|
||||
|
||||
@@ -35,6 +35,7 @@ const scopedSubjectResolvers: Readonly<Record<string, ScopedSubjectResolver | nu
|
||||
'subagent/end': null,
|
||||
'subagent/start': null,
|
||||
'system-prompt/assemble': args => (args[1] as Record<string, unknown>)['scope'],
|
||||
'tools/code-dispatch-log': args => (args[0] as Record<string, unknown>)['agent'],
|
||||
'tools/execute': args => (args[0] as Record<string, unknown>)['agent'],
|
||||
'tools/post-execute': args => (args[0] as Record<string, unknown>)['agent'],
|
||||
'tools/pre-execute': args => (args[0] as Record<string, unknown>)['agent'],
|
||||
|
||||
@@ -63,6 +63,7 @@ describe('scoped-dispatch invariants', () => {
|
||||
['approval/request', [{ agent, toolName: 'echo' }, () => Promise.resolve('unavailable')]],
|
||||
['goal/changed', [agent, { operation: 'create', ref: { id: 'goal-a', revision: 1 } }]],
|
||||
['system-prompt/assemble', [[], { scope: agent }]],
|
||||
['tools/code-dispatch-log', [{ exec: { callId: 'c', name: 't', arguments: {} }, agent, subCallId: 'c:code:1', name: 't', isError: false, content: [] }, () => Promise.resolve([])]],
|
||||
['tools/execute', [{ callId: 'c', name: 't', arguments: {}, agent }, () => Promise.resolve({ content: [], isError: false })]],
|
||||
['tools/post-execute', [{ callId: 'c', name: 't', arguments: {}, agent }, { content: [], isError: false }, () => Promise.resolve({ kind: 'accept' })]],
|
||||
['tools/pre-execute', [{ callId: 'c', name: 't', arguments: {}, agent }, () => Promise.resolve({ kind: 'allow' })]],
|
||||
|
||||
@@ -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
|
||||
README.md: 893ba3afef71ea0fb6b4bca267d929b220e3d506
|
||||
README.zh.md: cefb35bbc3556a1aca97d9e2fc5f8e6e06391e2e
|
||||
README.md: 3a9082f99663586936ce1eafaf09264a91798e28
|
||||
README.zh.md: 763ae1195d2c7a2606806970a3ba6371114d9d1c
|
||||
|
||||
@@ -191,5 +191,5 @@ Append-only; newly visible content follows the reusable request prefix and does
|
||||
- **Caller-defined subagent and workflow structured outputs remain object-rooted** — this is a consumer-level guard; the shared schema vocabulary and tool outputs support every JSON root.
|
||||
- **`timeoutMs` on a definition is declarative only** — the registry never enforces deadlines; enforcement requires the `@deepseek-ai/dsh-timeout-policy` wrapper.
|
||||
- **Code Mode is TypeScript-only and the presentation mode is service-wide** — `mode: code`/`both` rejects prompt assembly unless `ctx.codeRuntime.language === 'typescript'`; scoped restrictions/shadows still choose each agent's visible bindings, but one tool cannot be native-only while another is code-only.
|
||||
- **Code Mode intermediate values are execution-local and unbounded by bytes** — the canonical typed values cannot be reconstructed from session replay and may exhaust process or worker memory; only the outer `run_code` output has the worker's configurable hard cap. The rendered `content` of every sub-call IS logged verbatim on `tool/code-dispatch`, uncapped and outside spill policy, so programs that read huge files grow the session log by the same bytes (spill integration for the logged copy is deferred work).
|
||||
- **Code Mode intermediate values are execution-local and unbounded by bytes** — the canonical typed values cannot be reconstructed from session replay and may exhaust process or worker memory; only the outer `run_code` output has the worker's configurable hard cap. The durable log copy of each sub-call IS bounded: the `tools/code-dispatch-log` waterfall lets the spill policy replace an oversized `tool/code-dispatch` content with a preview + locator ([rationale](../../../.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md)).
|
||||
- **`run_code` state is fresh per run** — a persistent REPL-style kernel is rejected for the MVP (cross-call state would be invisible to the log); see [the Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md).
|
||||
|
||||
@@ -191,5 +191,5 @@ The available tools:
|
||||
- **调用方定义的 subagent 与工作流结构化输出仍要求对象根**:这是消费方层面的守卫;共享 schema 词汇和工具输出支持任意 JSON 根。
|
||||
- **定义上的 `timeoutMs` 仅为声明**:注册表绝不会强制执行截止时间;要强制执行,必须使用 `@deepseek-ai/dsh-timeout-policy` 包装层。
|
||||
- **Code Mode 只支持 TypeScript,且呈现模式在服务内统一**:`mode: code`/`both` 会拒绝组装提示词,除非 `ctx.codeRuntime.language === 'typescript'`;作用域限制/遮蔽仍会选择每个 agent 的可见绑定,但不能让一个工具仅使用 Native,而另一个仅使用 Code。
|
||||
- **Code Mode 中间值只存在于执行局部,且没有字节上限**:这些规范的类型化值无法从会话回放重建,并可能耗尽进程或 worker 内存;只有外层 `run_code` 输出受 worker 可配置的硬上限约束。每个子调用渲染后的 `content` 确实会原样记录在 `tool/code-dispatch` 中,不受字节上限约束,也不在 spill 策略范围内。因此,读取超大文件的程序会使会话日志增加等量字节(日志中的副本尚未接入 spill,相关工作留待后续完成)。
|
||||
- **Code Mode 中间值只存在于执行局部,且没有字节上限**:这些规范的类型化值无法从会话回放重建,并可能耗尽进程或 worker 内存;只有外层 `run_code` 输出受 worker 可配置的硬上限约束。每个子调用的持久日志副本则**有**上限:`tools/code-dispatch-log` waterfall 允许 spill 策略把过大的 `tool/code-dispatch` 内容替换为预览加定位符([原理](../../../.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md))。
|
||||
- **每次运行都会获得全新的 `run_code` 状态**:MVP 不采用持久 REPL 风格内核(跨调用状态不会出现在日志中);参见 [Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md)。
|
||||
|
||||
@@ -13,7 +13,7 @@ import { snapshotJsonValue } from '@deepseek-ai/dsh-session'
|
||||
import type { JsonValue } from '@deepseek-ai/dsh-session'
|
||||
import { defineTool } from './schema.ts'
|
||||
import { TOOL_REGISTRY_SCHEDULER } from './index.ts'
|
||||
import type { ToolDefinition, ToolExecutionResult, ToolRegistry, ToolRunContext } from './index.ts'
|
||||
import type { CodeDispatchLog, ToolDefinition, ToolExecutionResult, ToolRegistry, ToolRunContext } from './index.ts'
|
||||
|
||||
declare module '@deepseek-ai/dsh-session' {
|
||||
interface SessionEventMap {
|
||||
@@ -186,6 +186,20 @@ function renderValue(value: JsonValue): string {
|
||||
/** Canonical value returned by the outer Code Mode transport. */
|
||||
type RunCodeOutput = { logs: string[]; result?: JsonValue }
|
||||
|
||||
/**
|
||||
* Registry-private capabilities the bridge receives at construction — the
|
||||
* `requireRuntime` idiom: operations only the owning registry can mint stay
|
||||
* off its public service surface and flow here as closures instead.
|
||||
*/
|
||||
export interface RunCodeBridgeOptions {
|
||||
/** Resolves `ctx.codeRuntime` or throws the loud misconfiguration error (shared with the registry's assembly-time checks). */
|
||||
requireRuntime: () => CodeRuntime
|
||||
/** The run's overlap cap for parallel-classified sub-calls (the registry passes its validated `maxParallelSubCalls`). */
|
||||
maxParallel: number
|
||||
/** Runs the contained `tools/code-dispatch-log` waterfall over one settled sub-dispatch (the registry's private invoker). */
|
||||
shapeDispatchLog: (dispatch: CodeDispatchLog) => Promise<ContentBlock[]>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the `run_code` {@link ToolDefinition}: required `code` and
|
||||
* `description` parameters, executed through the dispatch bridge described
|
||||
@@ -194,13 +208,11 @@ type RunCodeOutput = { logs: string[]; result?: JsonValue }
|
||||
* outside the filterable global/scoped capability layers.
|
||||
* @param registry - the owning registry (sub-calls go through its `execute`,
|
||||
* bindings cover its registered tools).
|
||||
* @param requireRuntime - resolves `ctx.codeRuntime` or throws the loud
|
||||
* misconfiguration error (shared with the registry's assembly-time checks).
|
||||
* @param maxParallel - the run's overlap cap for parallel-classified
|
||||
* sub-calls (the registry passes its validated `maxParallelSubCalls`).
|
||||
* @param options - the registry-private capabilities described above.
|
||||
* @returns the registry-ready definition.
|
||||
*/
|
||||
export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () => CodeRuntime, maxParallel: number): ToolDefinition {
|
||||
export function createRunCodeTool(registry: ToolRegistry, options: RunCodeBridgeOptions): ToolDefinition {
|
||||
const { requireRuntime, maxParallel, shapeDispatchLog } = options
|
||||
return defineTool({
|
||||
name: RUN_CODE_NAME,
|
||||
description:
|
||||
@@ -279,6 +291,8 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
|
||||
}
|
||||
const pendingQueue: PendingDispatch[] = []
|
||||
const inFlight = new Set<Promise<void>>()
|
||||
/** Tracked settle-event side work (log shaping + append), drained at run settlement. */
|
||||
const logWork = new Set<Promise<void>>()
|
||||
const commitQueue: PendingDispatch[] = []
|
||||
let exclusiveActive = false
|
||||
let driving = false
|
||||
@@ -357,6 +371,9 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
|
||||
// entries, awaits the live pool, and drains the ordered commit lane —
|
||||
// including a commit already in progress when the program returned.
|
||||
await drive()
|
||||
// Every settle's shaped append lands inside the open run_code turn
|
||||
// (tasks self-remove on settlement).
|
||||
while (logWork.size > 0) await Promise.allSettled([...logWork])
|
||||
}
|
||||
|
||||
// Read through a call, not a bare property: the abort state genuinely
|
||||
@@ -387,22 +404,41 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
|
||||
| { kind: 'post-result' | 'final-result'; exec: ToolRunContext; result: ToolExecutionResult }
|
||||
| undefined
|
||||
const settle = (result: ToolExecutionResult): void => {
|
||||
exec.agent?.session.append('tool/code-dispatch', {
|
||||
parentCallId: exec.callId,
|
||||
subCallId,
|
||||
name,
|
||||
// The SIBLING parse of the dispatched value: byte-identical JSON,
|
||||
// but a separate object — a tool mutating its args cannot desync
|
||||
// this record from what it actually received.
|
||||
arguments: normalized.logged,
|
||||
isError: result.isError,
|
||||
// The registry deep-froze this projection at result finalization;
|
||||
// append snapshots it again, so the log copy stays detached.
|
||||
content: result.content,
|
||||
})
|
||||
// The program gets its value NOW: log shaping (e.g. a spill
|
||||
// backend) must never delay the binding or occupy a dispatch
|
||||
// slot. The shaped append is tracked side work; the run's
|
||||
// settlement drains logWork so every settle event still lands
|
||||
// inside the open turn (shapeDispatchLog is contained, so this
|
||||
// chain cannot reject).
|
||||
resolve(result.isError
|
||||
? { isError: true, message: result.error.message }
|
||||
: { isError: false, value: result.value })
|
||||
const agent = exec.agent
|
||||
if (agent === undefined) return
|
||||
const task: Promise<void> = (async () => {
|
||||
// The durable copy may be reshaped (e.g. spilled to a preview +
|
||||
// locator) by the log-shaping waterfall; the program's value
|
||||
// and the model contract are untouched.
|
||||
const logged = await shapeDispatchLog({
|
||||
exec, agent, subCallId, name, isError: result.isError,
|
||||
// The registry deep-froze this projection at result
|
||||
// finalization; append snapshots the final copy again, so
|
||||
// the log stays detached.
|
||||
content: result.content,
|
||||
})
|
||||
agent.session.append('tool/code-dispatch', {
|
||||
parentCallId: exec.callId,
|
||||
subCallId,
|
||||
name,
|
||||
// The SIBLING parse of the dispatched value: byte-identical JSON,
|
||||
// but a separate object — a tool mutating its args cannot desync
|
||||
// this record from what it actually received.
|
||||
arguments: normalized.logged,
|
||||
isError: result.isError,
|
||||
content: logged,
|
||||
})
|
||||
})().finally(() => { logWork.delete(task) })
|
||||
logWork.add(task)
|
||||
}
|
||||
pendingQueue.push({
|
||||
flight: Promise.resolve(),
|
||||
@@ -444,6 +480,12 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
|
||||
exec.deferContext(context)
|
||||
}
|
||||
settle(result)
|
||||
// Backpressure on the shaped-append side channel: pending log
|
||||
// tasks (each retaining a full result while a slow backend
|
||||
// stores it) are bounded by the pool cap — beyond it the
|
||||
// ordered lane waits, so later sub-calls cannot start and
|
||||
// pending I/O/memory cannot grow without bound.
|
||||
while (logWork.size > maxParallel) await Promise.race(logWork)
|
||||
},
|
||||
})
|
||||
wakeup()
|
||||
|
||||
@@ -123,6 +123,19 @@ declare module 'cordis' {
|
||||
* @mode waterfall
|
||||
*/
|
||||
'tools/post-execute'(this: Scoped<ToolRegistry>, exec: ToolExecution, result: Readonly<ToolExecutionResult>, next: () => Promise<PostToolDecision>): Promise<PostToolDecision>
|
||||
/**
|
||||
* Shape the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before
|
||||
* the bridge appends its `tool/code-dispatch` event. `next()` keeps the
|
||||
* content unchanged; a listener may return replacement blocks (e.g. the
|
||||
* spill policy's preview + locator for an oversized text result). Only the
|
||||
* logged copy is affected — the program already received the complete
|
||||
* value, and the model sees neither. A throwing listener is contained:
|
||||
* the bridge falls back to logging the unshaped content.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's dispatches.
|
||||
* @param dispatch - the parent execution, sub-call identity, and the settled content to log.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'tools/code-dispatch-log'(this: Scoped<ToolRegistry>, dispatch: CodeDispatchLog, next: () => Promise<ContentBlock[]>): Promise<ContentBlock[]>
|
||||
/**
|
||||
* Observe the frozen, lossless-JSON final outcome. Listener failures are contained.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): keyed by `exec.agent`.
|
||||
@@ -272,6 +285,30 @@ export type ToolExecutionMode =
|
||||
| { kind: 'parallel' }
|
||||
| { kind: 'exclusive' }
|
||||
|
||||
/**
|
||||
* One settled `run_code` sub-dispatch about to be logged, as seen by the
|
||||
* `tools/code-dispatch-log` waterfall: the parent execution (session owner,
|
||||
* outer call identity), the sub-call identity, and the outcome whose durable
|
||||
* copy a listener may reshape. `content` is the RENDERED result projection
|
||||
* (what a native `tool/result` would carry) — the program itself received
|
||||
* the structured `value` (or just the error message on failure); only the
|
||||
* `tool/code-dispatch` event's copy changes.
|
||||
*/
|
||||
export interface CodeDispatchLog {
|
||||
/** The outer `run_code` execution. */
|
||||
readonly exec: ToolExecution
|
||||
/** The calling agent (the scope routing key and the spill owner), when the outer call has one. */
|
||||
readonly agent?: Agent
|
||||
/** Deterministic sub-call id (`<parent>:code:<n>`). */
|
||||
readonly subCallId: CallId
|
||||
/** The dispatched sub-tool name. */
|
||||
readonly name: string
|
||||
/** Whether the sub-call settled as an error. */
|
||||
readonly isError: boolean
|
||||
/** The sub-call's complete model-facing content (the settle event's default payload). */
|
||||
readonly content: ContentBlock[]
|
||||
}
|
||||
|
||||
/**
|
||||
* One pending tool call inside the registry pipeline. Parsed arguments cross
|
||||
* one lossless-JSON materialization boundary before policy and are deep-frozen;
|
||||
@@ -690,7 +727,11 @@ export class ToolRegistry extends Service {
|
||||
// the filterable global/scoped capability layers.
|
||||
this.codeTransport = this.mode === 'native'
|
||||
? undefined
|
||||
: createRunCodeTool(this, () => this.requireCodeRuntime(), resolveMaxParallelSubCalls(config.maxParallelSubCalls))
|
||||
: createRunCodeTool(this, {
|
||||
requireRuntime: () => this.requireCodeRuntime(),
|
||||
maxParallel: resolveMaxParallelSubCalls(config.maxParallelSubCalls),
|
||||
shapeDispatchLog: dispatch => this.shapeDispatchLog(dispatch),
|
||||
})
|
||||
ctx.systemPrompt.tools(context => this.wireSchemas(context.scope))
|
||||
if (this.mode !== 'native') {
|
||||
ctx.systemPrompt.section({
|
||||
@@ -941,6 +982,27 @@ export class ToolRegistry extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the `tools/code-dispatch-log` waterfall over one settled sub-dispatch
|
||||
* and return the content the bridge should log on `tool/code-dispatch`.
|
||||
* Contained: a throwing listener falls back to the unshaped content — log
|
||||
* shaping must never fail the dispatch or lose the settle event. Private:
|
||||
* the ONE consumer is the `run_code` bridge this registry constructs, which
|
||||
* receives it as a capability parameter (the `requireRuntime` idiom) — the
|
||||
* waterfall, not this invoker, is the public extension seam.
|
||||
*/
|
||||
private async shapeDispatchLog(dispatch: CodeDispatchLog): Promise<ContentBlock[]> {
|
||||
try {
|
||||
return await this.ctx.waterfall(
|
||||
scopeTarget(this, dispatch.agent), 'tools/code-dispatch-log', dispatch,
|
||||
() => Promise.resolve(dispatch.content),
|
||||
)
|
||||
} catch (error: unknown) {
|
||||
this.ctx.logger.warn(`tools: code-dispatch-log listener failed for ${dispatch.name}: ${errorMessage(error)}; logging the unshaped content`)
|
||||
return dispatch.content
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute through pre-policy, guards, around-dispatch, post-policy,
|
||||
* definition-owned content finalization, and final notification. Tool and
|
||||
|
||||
@@ -801,6 +801,21 @@ describe('the run_code dispatch bridge', () => {
|
||||
expect(result.content[0]).toEqual({ type: 'text', text: 'caught: deliberate failure' })
|
||||
})
|
||||
|
||||
it('a throwing tools/code-dispatch-log listener is contained: the unshaped content is logged', async () => {
|
||||
const { ctx, runtime } = await setup({ mode: 'code' })
|
||||
registerEcho(ctx)
|
||||
ctx.on('tools/code-dispatch-log', () => { throw new Error('shaper exploded') })
|
||||
const { agent, events } = fakeAgent()
|
||||
runtime.behavior = async (request) => {
|
||||
const value = await request.bindings[0]!.functions.echo!({ value: 'x' })
|
||||
return { logs: [], value: value as string }
|
||||
}
|
||||
const result = await runCode(ctx, 'program', { agent })
|
||||
expect(result.isError).toBe(false)
|
||||
const settle = events.find(event => event.type === 'tool/code-dispatch')
|
||||
expect(settle?.data).toMatchObject({ name: 'echo', isError: false, content: [{ type: 'text', text: 'echo:x' }] })
|
||||
})
|
||||
|
||||
it('a throwing tools/pre-execute listener settles the sub-call without post-execute', async () => {
|
||||
const { ctx, runtime } = await setup({ mode: 'code' })
|
||||
const calls = registerEcho(ctx)
|
||||
|
||||
@@ -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
|
||||
README.md: ed72e0d73ec83a6cb8620245e3793770ce622ab3
|
||||
README.zh.md: db0be6153465ee64bce7f9ab9a2a3c1dddf8b53d
|
||||
README.md: 7638f62c0426964d83d7a635ebf73d0f117abd78
|
||||
README.zh.md: bf6d52a8c6b2b2e56052075af4a2150fba357f84
|
||||
|
||||
@@ -15,7 +15,7 @@ This plugin registers **no service** and owns no storage or preview mechanics: p
|
||||
## Behavior
|
||||
|
||||
1. Let the tool run (delegates via `next()`, so it bounds whatever a downstream hook accepted).
|
||||
2. Skip nested executions (`exec.parent` is present), accepted value replacements (the registry must revalidate and rerender them), `read` (avoids a `read → spill → read again` loop), and any non-`accept` decision (a `block`'s corrective feedback passes through).
|
||||
2. Skip nested executions (`exec.parent` is present — their DURABLE copy is bounded by the dispatch-log arm below), accepted value replacements (the registry must revalidate and rerender them), `read` (avoids a `read → spill → read again` loop), and any non-`accept` decision (a `block`'s corrective feedback passes through).
|
||||
3. Flatten the accepted content only when it is **plain text** (all `text` blocks); a result with any non-text block is left untouched.
|
||||
4. If its UTF-8 size is `≤ maxInlineBytes`, leave it unchanged.
|
||||
5. Otherwise save the full text and replace the result with a preview + this notice, sized so the whole replacement (preview + blank line + notice) stays within `maxInlineBytes` — the notice's byte cost is reserved out of the budget, so the preview shrinks to fit and the model-facing result never exceeds the cap:
|
||||
@@ -30,6 +30,8 @@ This plugin registers **no service** and owns no storage or preview mechanics: p
|
||||
|
||||
**Best-effort:** no session owner, no `ctx.spillStore` backend, or a `saveText` rejection ⇒ the policy logs a warning and returns the original result. A spill failure never turns a successful call into an `isError` or hides the inline result. A successful replacement changes only `content`; the canonical programmatic value is preserved.
|
||||
|
||||
**The dispatch-log arm:** a second listener on `tools/code-dispatch-log` applies the same cap, replacement pipeline, and best-effort fallbacks to the DURABLE copy of each `run_code` sub-call result (artifact label `dispatch`, keyed by the sub-call id). The program's value is untouched — it already crossed the worker boundary whole — and `read` sub-calls are bounded too: a log copy is not model context, so the read-again loop cannot occur, and `read` is precisely the tool that produces huge logs ([rationale](../../../.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md)).
|
||||
|
||||
## Scope
|
||||
|
||||
The policy sees only the FINAL formatted surface result—not a tool's internal resource or canonical value. If a provider already truncated (e.g. `web-fetch-local.maxBodyChars`), the spill artifact holds the full formatted result the tool returned, not the full original source. Provider/resource caps stay mandatory and separate. `glob`/`grep` own item-level surface spill because their complete acquired values still exist before rendering; bash streams own acquisition-time spill. The generic policy prepends its waterfall listener, then delegates, so ordinary tool-owned asynchronous projections complete before generic byte bounding regardless of plugin load order. See the [tool output spill Agent Note](../../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md).
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
## 行为
|
||||
|
||||
1. 允许工具运行(通过 `next()` 委托,因此可以限制任何下游钩子接受的内容)。
|
||||
2. 跳过嵌套执行(存在 `exec.parent`)、已接受的值替换(注册表必须重新验证并渲染它们)、`read`(避免 `read → spill → read again` 循环)以及任何非 `accept` 决定(`block` 的纠正反馈会原样通过)。
|
||||
2. 跳过嵌套执行(存在 `exec.parent`——其持久副本由下方的 dispatch-log 分支设界)、已接受的值替换(注册表必须重新验证并渲染它们)、`read`(避免 `read → spill → read again` 循环)以及任何非 `accept` 决定(`block` 的纠正反馈会原样通过)。
|
||||
3. 仅在已接受的内容为**纯文本**(全部都是 `text` 块)时才将其展平;包含任何非文本块的结果都保持不变。
|
||||
4. 如果 UTF-8 大小为 `≤ maxInlineBytes`,则保持不变。
|
||||
5. 否则,保存完整文本,并将结果替换为预览和以下通知。系统会调整大小,使整个替换内容(预览、空行和通知)不超过 `maxInlineBytes`:先从预算中保留通知所需字节,再缩小预览以适配剩余空间,因此面向模型的结果绝不会超过上限:
|
||||
@@ -30,6 +30,8 @@
|
||||
|
||||
**尽力而为**:没有会话 owner、没有 `ctx.spillStore` 后端,或 `saveText` 拒绝 ⇒ 策略记录警告并返回原始结果。spill 失败绝不会将成功调用变为 `isError`,也不会隐藏内联结果。成功替换时只会更改 `content`;规范程序值保持不变。
|
||||
|
||||
**dispatch-log 分支:**注册在 `tools/code-dispatch-log` 上的第二个监听器,把同一套上限、替换流水线与尽力而为的回退应用到每个 `run_code` 子调用结果的持久副本上(工件标签为 `dispatch`,按子调用 id 归档)。程序取得的值不受影响——它早已完整跨过 worker 边界;`read` 子调用同样设界:日志副本不是模型上下文,因此不会发生 read-again 循环,而 `read` 恰恰是最容易产生巨型日志的工具([原理](../../../.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md))。
|
||||
|
||||
## 范围
|
||||
|
||||
该策略只能看到最终格式化接口结果,看不到工具的内部资源或规范值。如果提供方已经截断内容(例如 `web-fetch-local.maxBodyChars`),spill 产物保存的是工具返回的完整格式化结果,而非完整原始源。提供方/资源上限仍必须存在,并且与该策略分离。`glob`/`grep` 负责对项级接口结果执行 spill,因为渲染前仍然存在完整的已获取值;bash 流负责在获取时 spill。通用策略预先注册自己的 waterfall 监听器,然后再委托,因此无论插件加载顺序如何,普通工具拥有的异步投影都会在通用字节限制之前完成。详见[工具输出 spill Agent Note](../../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md)。
|
||||
|
||||
@@ -10,18 +10,26 @@
|
||||
* `@deepseek-ai/dsh-retention` (`TextRetainer`), storage is `ctx.spillStore`.
|
||||
* The policy only decides WHEN to spill and composes the notice.
|
||||
*
|
||||
* A second arm applies the SAME cap to the durable log: the
|
||||
* `tools/code-dispatch-log` waterfall bounds the `tool/code-dispatch` event's
|
||||
* copy of an oversized `run_code` sub-call result (the program's value is
|
||||
* untouched; UIs and replay read the full text through the spill artifact).
|
||||
*
|
||||
* ## Deliberately narrow
|
||||
*
|
||||
* - Omitted `maxInlineBytes` ⇒ the plugin registers nothing (a true no-op).
|
||||
* - Plain-text results only: a result carrying any non-text block is left
|
||||
* untouched (the policy knows only the final formatted text, not tool
|
||||
* internals).
|
||||
* - Nested composite calls are skipped; only their outer surface result may
|
||||
* become model-facing and spillable.
|
||||
* - Nested composite calls skip the MODEL-facing arm; their durable log copy
|
||||
* is bounded by the dispatch-log arm instead.
|
||||
* - Accepted value replacements pass through for registry revalidation and
|
||||
* rendering; this presentation policy cannot also replace content in the
|
||||
* same mutually exclusive decision.
|
||||
* - `read` is skipped to avoid a `read → spill → read again` loop.
|
||||
* - `read` is skipped by the model-facing arm to avoid a
|
||||
* `read → spill → read again` loop; the dispatch-log arm bounds `read`
|
||||
* sub-calls too (a log copy is not model context, and `read` is precisely
|
||||
* the tool that produces huge logs).
|
||||
* - Best-effort: no session owner, no `ctx.spillStore` backend, or a save
|
||||
* failure ⇒ log and return the original result. A spill failure must NEVER
|
||||
* turn a successful tool call into an `isError` or hide the inline result.
|
||||
@@ -42,6 +50,7 @@ import { TextRetainer, describeOmitted } from '@deepseek-ai/dsh-retention'
|
||||
import type { Omitted } from '@deepseek-ai/dsh-retention'
|
||||
import type { SaveTextSpill, SpillRef } from '@deepseek-ai/dsh-spill'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import type { PostToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'
|
||||
import type { SpillPolicyExec } from './types.ts'
|
||||
|
||||
@@ -108,6 +117,75 @@ export function apply(ctx: Context, config: Config): void {
|
||||
if (!Number.isInteger(maxInlineBytes) || maxInlineBytes < 0) {
|
||||
throw new Error(`spill-policy: maxInlineBytes must be a non-negative integer (got ${maxInlineBytes})`)
|
||||
}
|
||||
// Narrowed once for the nested arms (closure narrowing does not survive awaits).
|
||||
const cap: number = maxInlineBytes
|
||||
|
||||
/**
|
||||
* Spill `text` and build the bounded replacement (preview + notice), or
|
||||
* return `undefined` when the policy must keep the original (no session
|
||||
* owner, no backend, storage failure, or no within-cap replacement).
|
||||
* Shared verbatim by the model-facing post-execute arm and the durable
|
||||
* dispatch-log arm so both produce byte-identical projections.
|
||||
*/
|
||||
async function spillReplacement(
|
||||
text: string,
|
||||
totalBytes: number,
|
||||
sessionId: SessionId | undefined,
|
||||
toolName: string,
|
||||
callId: CallId,
|
||||
label: 'result' | 'dispatch',
|
||||
): Promise<string | undefined> {
|
||||
if (sessionId === undefined) {
|
||||
ctx.logger.warn(`spill-policy: no session owner for ${toolName} ${label}; keeping the inline content`)
|
||||
return undefined
|
||||
}
|
||||
const spillStore = ctx.get('spillStore')
|
||||
if (!spillStore) {
|
||||
ctx.logger.warn('spill-policy: no ctx.spillStore backend loaded; keeping the inline content')
|
||||
return undefined
|
||||
}
|
||||
const save: SaveTextSpill = {
|
||||
owner: { sessionId },
|
||||
source: { toolName, callId, label },
|
||||
suggestedName: `${toolName}.txt`,
|
||||
content: text,
|
||||
}
|
||||
let ref: SpillRef
|
||||
try {
|
||||
ref = await spillStore.saveText(save)
|
||||
} catch (error: unknown) {
|
||||
// Best-effort: a storage failure (permissions, ENOSPC, backend down) must
|
||||
// never fail the call or hide the content — keep the original inline.
|
||||
ctx.logger.warn(`spill-policy: saveText failed for ${toolName}: ${String(error)}; keeping the inline content`)
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Reserve the notice's byte cost INSIDE maxInlineBytes so the replacement
|
||||
// (preview + blank line + notice) never exceeds the documented cap — a naive
|
||||
// preview that spent the whole budget then appended the notice could be
|
||||
// larger than the cap, and for a marginally-over result even larger than the
|
||||
// original. The reservation uses a notice priced at the worst-case omission
|
||||
// count (the full byte total): its digit count bounds the real count's, so
|
||||
// the reserved size is a safe upper bound and the final notice is never
|
||||
// longer than what we reserved. `\n\n` is the 2-byte join.
|
||||
const reserve = Buffer.byteLength(spillNotice({ kind: 'exact', count: totalBytes }, ref), 'utf8') + 2
|
||||
const previewBudget = Math.max(0, cap - reserve)
|
||||
const { text: previewText, omitted } = preview(text, previewBudget)
|
||||
const notice = spillNotice(omitted, ref)
|
||||
const replacedText = previewText.length > 0 ? `${previewText}\n\n${notice}` : notice
|
||||
// Invariant: the policy NEVER emits a replacement larger than the cap. When
|
||||
// the notice alone exceeds maxInlineBytes (a tiny cap or a long spill root),
|
||||
// there is no within-cap replacement, so keep the inline content — spilling
|
||||
// would break the advertised cap. (A within-cap replacement is always
|
||||
// smaller than the original, which is > cap by the entry condition, so this
|
||||
// one check subsumes "not smaller than the original" too. The spill file
|
||||
// already written is a harmless orphan; cleanup is deferred.)
|
||||
if (Buffer.byteLength(replacedText, 'utf8') > cap) {
|
||||
ctx.logger.warn(`spill-policy: spill notice for ${toolName} exceeds maxInlineBytes; keeping the inline content`)
|
||||
return undefined
|
||||
}
|
||||
return replacedText
|
||||
}
|
||||
|
||||
ctx.on('tools/post-execute', async (exec, result, next): Promise<PostToolDecision> => {
|
||||
// Delegate first so a downstream listener (e.g. a hook) settles the result;
|
||||
@@ -124,58 +202,31 @@ export function apply(ctx: Context, config: Config): void {
|
||||
const totalBytes = Buffer.byteLength(text, 'utf8')
|
||||
if (totalBytes <= maxInlineBytes) return decision
|
||||
|
||||
const sessionId = ownerSessionId(exec)
|
||||
if (sessionId === undefined) {
|
||||
ctx.logger.warn(`spill-policy: no session owner for ${exec.name} result; keeping the inline result`)
|
||||
return decision
|
||||
}
|
||||
const spillStore = ctx.get('spillStore')
|
||||
if (!spillStore) {
|
||||
ctx.logger.warn('spill-policy: no ctx.spillStore backend loaded; keeping the inline result')
|
||||
return decision
|
||||
}
|
||||
|
||||
const save: SaveTextSpill = {
|
||||
owner: { sessionId },
|
||||
source: { toolName: exec.name, callId: exec.callId, label: 'result' },
|
||||
suggestedName: `${exec.name}.txt`,
|
||||
content: text,
|
||||
}
|
||||
let ref: SpillRef
|
||||
try {
|
||||
ref = await spillStore.saveText(save)
|
||||
} catch (error: unknown) {
|
||||
// Best-effort: a storage failure (permissions, ENOSPC, backend down) must
|
||||
// never fail the call or hide the result — keep the original inline.
|
||||
ctx.logger.warn(`spill-policy: saveText failed for ${exec.name}: ${String(error)}; keeping the inline result`)
|
||||
return decision
|
||||
}
|
||||
|
||||
// Reserve the notice's byte cost INSIDE maxInlineBytes so the replacement
|
||||
// (preview + blank line + notice) never exceeds the documented cap — a naive
|
||||
// preview that spent the whole budget then appended the notice could be
|
||||
// larger than the cap, and for a marginally-over result even larger than the
|
||||
// original. The reservation uses a notice priced at the worst-case omission
|
||||
// count (the full byte total): its digit count bounds the real count's, so
|
||||
// the reserved size is a safe upper bound and the final notice is never
|
||||
// longer than what we reserved. `\n\n` is the 2-byte join.
|
||||
const reserve = Buffer.byteLength(spillNotice({ kind: 'exact', count: totalBytes }, ref), 'utf8') + 2
|
||||
const previewBudget = Math.max(0, maxInlineBytes - reserve)
|
||||
const { text: previewText, omitted } = preview(text, previewBudget)
|
||||
const notice = spillNotice(omitted, ref)
|
||||
const replacedText = previewText.length > 0 ? `${previewText}\n\n${notice}` : notice
|
||||
// Invariant: the policy NEVER emits a replacement larger than the cap. When
|
||||
// the notice alone exceeds maxInlineBytes (a tiny cap or a long spill root),
|
||||
// there is no within-cap replacement, so keep the inline result — spilling
|
||||
// would break the advertised context cap. (A within-cap replacement is
|
||||
// always smaller than the original, which is > cap by the entry condition,
|
||||
// so this one check subsumes "not smaller than the original" too. The spill
|
||||
// file already written is a harmless orphan; cleanup is deferred.)
|
||||
if (Buffer.byteLength(replacedText, 'utf8') > maxInlineBytes) {
|
||||
ctx.logger.warn(`spill-policy: spill notice for ${exec.name} exceeds maxInlineBytes; keeping the inline result`)
|
||||
return decision
|
||||
}
|
||||
const replacedText = await spillReplacement(text, totalBytes, ownerSessionId(exec), exec.name, exec.callId, 'result')
|
||||
if (replacedText === undefined) return decision
|
||||
const replaced: ContentBlock[] = [{ type: 'text', text: replacedText }]
|
||||
return { kind: 'accept', content: replaced, ...decision.additionalContexts ? { additionalContexts: decision.additionalContexts } : {} }
|
||||
}, { prepend: true })
|
||||
|
||||
// The durable-log arm: bound the `tool/code-dispatch` event's copy of an
|
||||
// oversized sub-call result the same way the model-facing arm bounds an
|
||||
// outer result. The program's returned value is untouched (it already
|
||||
// crossed the worker boundary whole); only the session log's copy shrinks
|
||||
// to preview + locator, so replay and UIs read the full text through the
|
||||
// spill artifact exactly as they do for spilled native results.
|
||||
ctx.on('tools/code-dispatch-log', async (dispatch, next): Promise<ContentBlock[]> => {
|
||||
const content = await next()
|
||||
// `read` sub-calls spill too: the log copy is not model context, so the
|
||||
// read → spill → read-again loop the post-execute arm avoids cannot
|
||||
// happen here, and read is precisely the tool that produces huge logs.
|
||||
const text = flattenPlainText(content)
|
||||
if (text === undefined) return content
|
||||
const totalBytes = Buffer.byteLength(text, 'utf8')
|
||||
if (totalBytes <= maxInlineBytes) return content
|
||||
|
||||
const replacedText = await spillReplacement(
|
||||
text, totalBytes, ownerSessionId(dispatch.exec), dispatch.name, dispatch.subCallId, 'dispatch')
|
||||
if (replacedText === undefined) return content
|
||||
return [{ type: 'text', text: replacedText }]
|
||||
}, { prepend: true })
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
|
||||
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
|
||||
import type { PostToolDecision, ToolExecution, ToolExecutionToken } from '@deepseek-ai/dsh-tools'
|
||||
import { SpillLocator, SpillStore } from '@deepseek-ai/dsh-spill'
|
||||
import type { SaveTextSpill, SpillRef } from '@deepseek-ai/dsh-spill'
|
||||
@@ -28,9 +29,12 @@ const testToolSignal = new AbortController().signal
|
||||
class StubStore extends SpillStore {
|
||||
saves: SaveTextSpill[] = []
|
||||
fail = false
|
||||
/** Per-save hang hook: each call awaits the returned promise before completing. */
|
||||
gate: (() => Promise<void>) | undefined
|
||||
|
||||
async saveText(input: SaveTextSpill): Promise<SpillRef> {
|
||||
if (this.fail) throw new Error('disk full')
|
||||
await this.gate?.()
|
||||
this.saves.push(input)
|
||||
return {
|
||||
locator: SpillLocator(`/spill/${input.suggestedName}`),
|
||||
@@ -230,6 +234,230 @@ describe('read skip', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('the durable dispatch-log arm', () => {
|
||||
/** Boot code mode + the policy + the worker runtime; run one program via the real bridge. */
|
||||
async function runCodeWith(program: string, maxInlineBytes: number, extraTools: ToolDefinition[] = []) {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry, { mode: 'code' })
|
||||
await ctx.plugin(StubStore)
|
||||
await ctx.plugin(SpillPolicy, { maxInlineBytes })
|
||||
await ctx.plugin(WorkerCodeRuntime, {})
|
||||
const events: { type: string; data: unknown }[] = []
|
||||
const agent = {
|
||||
session: {
|
||||
header: { id: SessionId('dispatch-spill'), cwd: '/workspace' },
|
||||
append: (type: string, data: unknown) => { events.push({ type, data }) },
|
||||
},
|
||||
}
|
||||
ctx.tools.register(textTool('huge_read', 'H'.repeat(2_000)))
|
||||
ctx.tools.register(textTool('small_read', 'tiny'))
|
||||
for (const tool of extraTools) ctx.tools.register(tool)
|
||||
const result = await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('parent-1'),
|
||||
name: 'run_code',
|
||||
arguments: { code: program, description: 'Drive dispatch-log spilling' },
|
||||
agent: agent as never,
|
||||
})
|
||||
return { ctx, result, events, spill: ctx.spillStore as StubStore }
|
||||
}
|
||||
|
||||
it('bounds the tool/code-dispatch copy of an oversized sub-result while the program value stays whole', async () => {
|
||||
const { result, events, spill } = await runCodeWith(
|
||||
'const blocks = await tools.huge_read({});\nreturn blocks[0].text.length', 200)
|
||||
expect(result.isError).toBe(false)
|
||||
if (result.isError) throw new Error('expected success')
|
||||
// The program received the COMPLETE text (length 2000), untouched by spill.
|
||||
expect(result.value).toMatchObject({ result: 2_000 })
|
||||
// The durable settle event carries the bounded projection + locator.
|
||||
const settle = events.find(event => event.type === 'tool/code-dispatch')
|
||||
expect(settle).toBeDefined()
|
||||
const logged = (settle!.data as { content: { type: string; text: string }[] }).content
|
||||
expect(logged).toHaveLength(1)
|
||||
const loggedText = logged[0]!.text
|
||||
expect(Buffer.byteLength(loggedText, 'utf8')).toBeLessThanOrEqual(200)
|
||||
expect(loggedText).toContain('Full formatted result stored at: /spill/huge_read.txt')
|
||||
// The artifact holds the full text under the dispatch label and sub-call id.
|
||||
const save = spill.saves.find(entry => entry.source.label === 'dispatch')
|
||||
expect(save).toMatchObject({
|
||||
source: { toolName: 'huge_read', callId: 'parent-1:code:1', label: 'dispatch' },
|
||||
})
|
||||
expect(save?.content).toBe('H'.repeat(2_000))
|
||||
})
|
||||
|
||||
it('leaves a non-text sub-result log unchanged (flatten declines)', async () => {
|
||||
const { events, spill } = await runCodeWith(
|
||||
'return await tools.mixed_read({})', 5, [defineContentToolFixture({
|
||||
name: 'mixed_read',
|
||||
description: 'mixed_read',
|
||||
parameters: {},
|
||||
async execute(): Promise<ContentBlock[]> {
|
||||
return [{ type: 'text', text: 'x'.repeat(100) }, { type: 'reasoning', text: 'why' }]
|
||||
},
|
||||
})])
|
||||
const settle = events.find(event => event.type === 'tool/code-dispatch')
|
||||
expect((settle!.data as { content: unknown[] }).content).toHaveLength(2)
|
||||
expect(spill.saves.filter(entry => entry.source.label === 'dispatch')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('leaves a within-cap sub-result log untouched and saves nothing for it', async () => {
|
||||
const { events, spill } = await runCodeWith(
|
||||
'return await tools.small_read({})', 200)
|
||||
const settle = events.find(event => event.type === 'tool/code-dispatch')
|
||||
expect((settle!.data as { content: { type: string; text: string }[] }).content)
|
||||
.toEqual([{ type: 'text', text: 'tiny' }])
|
||||
expect(spill.saves.filter(entry => entry.source.label === 'dispatch')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('a slow spill backend never delays the program value or a later dispatch slot', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry, { mode: 'code' })
|
||||
await ctx.plugin(StubStore)
|
||||
await ctx.plugin(SpillPolicy, { maxInlineBytes: 100 })
|
||||
await ctx.plugin(WorkerCodeRuntime, {})
|
||||
// A spill backend that hangs until released.
|
||||
let releaseSave!: () => void
|
||||
const gate = new Promise<void>((resolve) => { releaseSave = resolve })
|
||||
const store = ctx.spillStore as StubStore
|
||||
const realSave = store.saveText.bind(store)
|
||||
store.saveText = async (input) => {
|
||||
await gate
|
||||
return realSave(input)
|
||||
}
|
||||
const events: { type: string; data: unknown }[] = []
|
||||
const agent = {
|
||||
session: {
|
||||
header: { id: SessionId('dispatch-slow-spill'), cwd: '/workspace' },
|
||||
append: (type: string, data: unknown) => { events.push({ type, data }) },
|
||||
},
|
||||
}
|
||||
ctx.tools.register(textTool('huge_read', 'H'.repeat(2_000)))
|
||||
ctx.tools.register(textTool('small_read', 'tiny'))
|
||||
let smallAfterHuge = false
|
||||
const runPromise = ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('parent-3'),
|
||||
name: 'run_code',
|
||||
arguments: {
|
||||
// The program takes BOTH values while the spill backend hangs: the
|
||||
// huge read's binding resolves immediately (its logged copy is side
|
||||
// work), so the small read proceeds without waiting.
|
||||
code: 'const big = await tools.huge_read({});\nconst small = await tools.small_read({});\nreturn big[0].text.length + small[0].text.length',
|
||||
description: 'Prove log shaping is off the program path',
|
||||
},
|
||||
agent: agent as never,
|
||||
}).then((result) => {
|
||||
return result
|
||||
})
|
||||
// The run cannot COMPLETE while the settle append is gated (drain waits
|
||||
// for logWork), but the program itself already ran both calls; release
|
||||
// the backend and observe the settle events land inside the turn.
|
||||
await vi.waitFor(() => {
|
||||
// The second dispatch STARTED while the first one's spill hung.
|
||||
smallAfterHuge = events.some(event => event.type === 'tool/code-dispatch-start'
|
||||
&& (event.data as { name: string }).name === 'small_read')
|
||||
if (!smallAfterHuge) throw new Error('small_read not started yet')
|
||||
})
|
||||
releaseSave()
|
||||
const result = await runPromise
|
||||
expect(result.isError).toBe(false)
|
||||
if (result.isError) throw new Error('expected success')
|
||||
expect(result.value).toMatchObject({ result: 2_004 })
|
||||
const settles = events.filter(event => event.type === 'tool/code-dispatch')
|
||||
expect(settles).toHaveLength(2)
|
||||
expect(smallAfterHuge).toBe(true)
|
||||
})
|
||||
|
||||
it('a sustained slow backend backpressures the run instead of accumulating unbounded log tasks', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
// Cap 1: once the hung shaped-append backlog exceeds the cap, the ordered
|
||||
// lane holds inside the second commit, so the THIRD dispatch cannot start
|
||||
// until a pending save drains — the bound is observable as its missing
|
||||
// start event.
|
||||
await ctx.plugin(ToolRegistry, { mode: 'code', maxParallelSubCalls: 1 })
|
||||
await ctx.plugin(StubStore)
|
||||
await ctx.plugin(SpillPolicy, { maxInlineBytes: 100 })
|
||||
await ctx.plugin(WorkerCodeRuntime, {})
|
||||
const store = ctx.spillStore as StubStore
|
||||
const releases: (() => void)[] = []
|
||||
store.gate = () => new Promise<void>((resolve) => { releases.push(resolve) })
|
||||
const events: { type: string; data: unknown }[] = []
|
||||
const agent = {
|
||||
session: {
|
||||
header: { id: SessionId('dispatch-spill-bound'), cwd: '/workspace' },
|
||||
append: (type: string, data: unknown) => { events.push({ type, data }) },
|
||||
},
|
||||
}
|
||||
ctx.tools.register(textTool('huge_read', 'H'.repeat(2_000)))
|
||||
const started = (n: number): boolean => events.some(event => event.type === 'tool/code-dispatch-start'
|
||||
&& (event.data as { subCallId: string }).subCallId.endsWith(`:code:${n}`))
|
||||
const runPromise = ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('parent-bound'),
|
||||
name: 'run_code',
|
||||
arguments: {
|
||||
code: 'await tools.huge_read({}); await tools.huge_read({}); await tools.huge_read({}); return "done"',
|
||||
description: 'Three oversized reads against a hung backend',
|
||||
},
|
||||
agent: agent as never,
|
||||
})
|
||||
// Two hung saves = backlog above the cap: the lane must hold before
|
||||
// starting dispatch 3.
|
||||
await vi.waitFor(() => {
|
||||
if (releases.length < 2) throw new Error('second hung save not reached yet')
|
||||
})
|
||||
expect(started(2)).toBe(true)
|
||||
expect(started(3)).toBe(false)
|
||||
releases.shift()!()
|
||||
// Draining one pending save releases the lane; dispatch 3 starts.
|
||||
await vi.waitFor(() => {
|
||||
if (!started(3)) throw new Error('third dispatch not started yet')
|
||||
})
|
||||
while (releases.length > 0) releases.shift()!()
|
||||
const result = await runPromise
|
||||
expect(result.isError).toBe(false)
|
||||
await vi.waitFor(() => {
|
||||
if (releases.length > 0) { while (releases.length > 0) releases.shift()!() }
|
||||
if (events.filter(event => event.type === 'tool/code-dispatch').length !== 3) {
|
||||
throw new Error('settle events still pending')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('a saveText failure keeps the complete content in the durable log (best-effort)', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry, { mode: 'code' })
|
||||
await ctx.plugin(StubStore)
|
||||
await ctx.plugin(SpillPolicy, { maxInlineBytes: 100 })
|
||||
await ctx.plugin(WorkerCodeRuntime, {})
|
||||
;(ctx.spillStore as StubStore).fail = true
|
||||
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {})
|
||||
const events: { type: string; data: unknown }[] = []
|
||||
const agent = {
|
||||
session: {
|
||||
header: { id: SessionId('dispatch-spill-fail'), cwd: '/workspace' },
|
||||
append: (type: string, data: unknown) => { events.push({ type, data }) },
|
||||
},
|
||||
}
|
||||
ctx.tools.register(textTool('huge_read', 'H'.repeat(2_000)))
|
||||
const result = await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('parent-2'),
|
||||
name: 'run_code',
|
||||
arguments: { code: 'return (await tools.huge_read({}))[0].text.length', description: 'Fail the spill backend' },
|
||||
agent: agent as never,
|
||||
})
|
||||
expect(result.isError).toBe(false)
|
||||
const settle = events.find(event => event.type === 'tool/code-dispatch')
|
||||
expect((settle!.data as { content: { text: string }[] }).content[0]!.text).toBe('H'.repeat(2_000))
|
||||
expect(warn).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('nested-call skip', () => {
|
||||
it('leaves nested composite results complete and spillable only through their outer call', async () => {
|
||||
const { ctx, spill } = await setup({ maxInlineBytes: 10 })
|
||||
|
||||
101
pnpm-lock.yaml
generated
101
pnpm-lock.yaml
generated
@@ -930,6 +930,9 @@ importers:
|
||||
|
||||
packages/client/ui-primitives:
|
||||
dependencies:
|
||||
'@shikijs/langs':
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1
|
||||
clsx:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.1
|
||||
@@ -945,6 +948,9 @@ importers:
|
||||
remark-gfm:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
shiki:
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1
|
||||
devDependencies:
|
||||
'@deepseek-ai/dsh-invariants':
|
||||
specifier: workspace:^
|
||||
@@ -6770,24 +6776,52 @@ packages:
|
||||
'@shikijs/core@2.5.0':
|
||||
resolution: {integrity: sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==}
|
||||
|
||||
'@shikijs/core@4.3.1':
|
||||
resolution: {integrity: sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@shikijs/engine-javascript@2.5.0':
|
||||
resolution: {integrity: sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==}
|
||||
|
||||
'@shikijs/engine-javascript@4.3.1':
|
||||
resolution: {integrity: sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@shikijs/engine-oniguruma@2.5.0':
|
||||
resolution: {integrity: sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==}
|
||||
|
||||
'@shikijs/engine-oniguruma@4.3.1':
|
||||
resolution: {integrity: sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@shikijs/langs@2.5.0':
|
||||
resolution: {integrity: sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==}
|
||||
|
||||
'@shikijs/langs@4.3.1':
|
||||
resolution: {integrity: sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@shikijs/primitive@4.3.1':
|
||||
resolution: {integrity: sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@shikijs/themes@2.5.0':
|
||||
resolution: {integrity: sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==}
|
||||
|
||||
'@shikijs/themes@4.3.1':
|
||||
resolution: {integrity: sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@shikijs/transformers@2.5.0':
|
||||
resolution: {integrity: sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==}
|
||||
|
||||
'@shikijs/types@2.5.0':
|
||||
resolution: {integrity: sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==}
|
||||
|
||||
'@shikijs/types@4.3.1':
|
||||
resolution: {integrity: sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@shikijs/vscode-textmate@10.0.2':
|
||||
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
|
||||
|
||||
@@ -8931,9 +8965,15 @@ packages:
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
oniguruma-parser@0.12.2:
|
||||
resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==}
|
||||
|
||||
oniguruma-to-es@3.1.1:
|
||||
resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==}
|
||||
|
||||
oniguruma-to-es@4.3.6:
|
||||
resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==}
|
||||
|
||||
openai@6.26.0:
|
||||
resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==}
|
||||
hasBin: true
|
||||
@@ -9290,6 +9330,10 @@ packages:
|
||||
shiki@2.5.0:
|
||||
resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==}
|
||||
|
||||
shiki@4.3.1:
|
||||
resolution: {integrity: sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
side-channel-list@1.0.1:
|
||||
resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -11407,25 +11451,58 @@ snapshots:
|
||||
'@types/hast': 3.0.5
|
||||
hast-util-to-html: 9.0.5
|
||||
|
||||
'@shikijs/core@4.3.1':
|
||||
dependencies:
|
||||
'@shikijs/primitive': 4.3.1
|
||||
'@shikijs/types': 4.3.1
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.5
|
||||
hast-util-to-html: 9.0.5
|
||||
|
||||
'@shikijs/engine-javascript@2.5.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 2.5.0
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
oniguruma-to-es: 3.1.1
|
||||
|
||||
'@shikijs/engine-javascript@4.3.1':
|
||||
dependencies:
|
||||
'@shikijs/types': 4.3.1
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
oniguruma-to-es: 4.3.6
|
||||
|
||||
'@shikijs/engine-oniguruma@2.5.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 2.5.0
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
|
||||
'@shikijs/engine-oniguruma@4.3.1':
|
||||
dependencies:
|
||||
'@shikijs/types': 4.3.1
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
|
||||
'@shikijs/langs@2.5.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 2.5.0
|
||||
|
||||
'@shikijs/langs@4.3.1':
|
||||
dependencies:
|
||||
'@shikijs/types': 4.3.1
|
||||
|
||||
'@shikijs/primitive@4.3.1':
|
||||
dependencies:
|
||||
'@shikijs/types': 4.3.1
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.5
|
||||
|
||||
'@shikijs/themes@2.5.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 2.5.0
|
||||
|
||||
'@shikijs/themes@4.3.1':
|
||||
dependencies:
|
||||
'@shikijs/types': 4.3.1
|
||||
|
||||
'@shikijs/transformers@2.5.0':
|
||||
dependencies:
|
||||
'@shikijs/core': 2.5.0
|
||||
@@ -11436,6 +11513,11 @@ snapshots:
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.5
|
||||
|
||||
'@shikijs/types@4.3.1':
|
||||
dependencies:
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.5
|
||||
|
||||
'@shikijs/vscode-textmate@10.0.2': {}
|
||||
|
||||
'@smithy/core@3.24.7':
|
||||
@@ -13998,12 +14080,20 @@ snapshots:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
oniguruma-parser@0.12.2: {}
|
||||
|
||||
oniguruma-to-es@3.1.1:
|
||||
dependencies:
|
||||
emoji-regex-xs: 1.0.0
|
||||
regex: 6.1.0
|
||||
regex-recursion: 6.0.2
|
||||
|
||||
oniguruma-to-es@4.3.6:
|
||||
dependencies:
|
||||
oniguruma-parser: 0.12.2
|
||||
regex: 6.1.0
|
||||
regex-recursion: 6.0.2
|
||||
|
||||
openai@6.26.0(ws@8.21.0)(zod@4.4.3):
|
||||
optionalDependencies:
|
||||
ws: 8.21.0
|
||||
@@ -14504,6 +14594,17 @@ snapshots:
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.5
|
||||
|
||||
shiki@4.3.1:
|
||||
dependencies:
|
||||
'@shikijs/core': 4.3.1
|
||||
'@shikijs/engine-javascript': 4.3.1
|
||||
'@shikijs/engine-oniguruma': 4.3.1
|
||||
'@shikijs/langs': 4.3.1
|
||||
'@shikijs/themes': 4.3.1
|
||||
'@shikijs/types': 4.3.1
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.5
|
||||
|
||||
side-channel-list@1.0.1:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"docs/defensive-patterns.md": 550,
|
||||
"docs/testing.md": 1100,
|
||||
"examples/AGENTS.md": 310,
|
||||
"packages/AGENTS.md": 660,
|
||||
"packages/AGENTS.md": 675,
|
||||
"packages/README.md": 835
|
||||
}
|
||||
|
||||
@@ -165,6 +165,7 @@ export const LINK_MAP: Record<string, string> = {
|
||||
TaskSnapshot: 'tasks.md',
|
||||
TaskStart: 'tasks.md',
|
||||
TokenMeasurement: 'token-meter.md',
|
||||
CodeDispatchLog: 'tools.md',
|
||||
PostToolDecision: 'tools.md',
|
||||
PreToolDecision: 'tools.md',
|
||||
ToolDefinition: 'tools.md',
|
||||
|
||||
@@ -609,6 +609,11 @@
|
||||
"symbol": "ToolExecutionMode",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/tools.md",
|
||||
"symbol": "CodeDispatchLog",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/tools.md",
|
||||
"symbol": "ToolRunContext",
|
||||
|
||||
Reference in New Issue
Block a user