dsh-xray 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -66,6 +66,7 @@ The cost view answers the question no other tool asks: **who put this in my cont
66
66
  - **By-plugin rollup** — each plugin's per-request context tax: sections + schemas + tokens + share, ranked.
67
67
  - **Entry inspection** — `/xray/api/entry` returns any entry's live text with a chars/tokens ruler. Computed per request, never persisted.
68
68
  - **Skill cost** — a dedicated view prices every skill twice: its catalog line (resident on every request once any model-invocable skill exists) and its body (billed per load). Pricing only — toggling belongs to the ecosystem's skill managers.
69
+ - **Per-request ledger** — one classified bill per LLM call: system / tool schemas / history / tool results (aggregated per tool), a **Δprev** growth column, and prefix-stability markers (⚡ system+tools byte-identical to the previous request — KV-cache-friendly; ✂ prefix broken). Compaction and title calls are tagged apart from chat. Only counts, names, and hashes are kept — never message text.
69
70
  - **Explained UI** — every view opens with a one-line "what am I looking at"; terms carry plain-language tooltips; the whole tab is localized (English / 中文) through the host locale service.
70
71
 
71
72
  The same data flows through three surfaces: the **X-Ray tab** (native GUI), the standalone **`/xray` page** (works even when the client-module pipeline it diagnoses is broken), and the **CLI**.
@@ -210,6 +211,7 @@ Diagnostic imaging for a running composition — complementary to [dsh-doctor](h
210
211
  | --- | --- |
211
212
  | Context-tax attribution & entry inspection | 💰 Optimization |
212
213
  | Skill cost (catalog line + body pricing) | 💰 Optimization |
214
+ | Per-request ledger (Δprev, prefix stability) | 💰 Optimization |
213
215
  | Layer attribution | 🔍 Inspection |
214
216
  | Declared vs. actual diff | 🔍 Inspection |
215
217
  | Conflict detection | 🔍 Inspection |
package/README.zh.md CHANGED
@@ -65,6 +65,7 @@ cost 视图回答一个别的工具都不问的问题:**这段上下文是谁放
65
65
  - **按插件汇总** —— 每个插件的每请求上下文税:sections + schemas + tokens + 占比,排序呈现。
66
66
  - **条目原文查看** —— `/xray/api/entry` 返回任意条目的实时文本,附字符/token 标尺。按请求现算,绝不落盘。
67
67
  - **skill 成本** —— 专属视图为每个 skill 计两笔价:catalog 行(只要存在模型可调用的 skill 就随每次请求常驻)与正文(每次加载计费)。只计价——启停归生态里的 skill 管理器。
68
+ - **逐请求账单** —— 每次 LLM 调用一张分类账单:system / 工具 schema / 历史 / 工具结果(按工具聚合),**Δprev** 增量列,前缀稳定性标记(⚡ system+工具与上一请求逐字节一致——KV 缓存友好;✂ 前缀击穿)。compaction 与标题生成单独标注。只存计数、名字和 hash——绝不存消息正文。
68
69
  - **界面自解释** —— 每个视图开头一句"你在看什么";术语带白话提示;整个标签页通过宿主 locale 服务双语呈现(English / 中文)。
69
70
 
70
71
  同一份数据流经三个界面:**X 光标签页**(原生 GUI)、独立 **`/xray` 页面**(连它所诊断的 client-module 加载链路挂了都能用)、以及 **CLI**。
@@ -209,6 +210,7 @@ npx dsh-xray health # 读取运行时快照
209
210
  | --- | --- |
210
211
  | 上下文税归因 & 条目原文查看 | 💰 优化 |
211
212
  | skill 成本(catalog 行 + 正文计价) | 💰 优化 |
213
+ | 逐请求账单(Δprev、前缀稳定性) | 💰 优化 |
212
214
  | 层归因 | 🔍 检视 |
213
215
  | 声明 vs 实际 diff | 🔍 检视 |
214
216
  | 冲突检测 | 🔍 检视 |
package/lib/client.js CHANGED
@@ -189,6 +189,10 @@ window.__ModuleLoader__.load({
189
189
  'requests.none': 'no requests observed yet — send one agent message first',
190
190
  'requests.session': 'session {id} ({n} request(s))',
191
191
  'requests.chat': 'chat',
192
+ 'requests.copyId': 'copy call id',
193
+ 'requests.copied': 'copied',
194
+ 'tip.copyId':
195
+ "Opaque id of this tool's largest result in this request — find the call in the Trajectory tab",
192
196
  'entry.loading': 'loading entry…',
193
197
  'entry.stats': '{chars} chars · ~{tokens} tokens ({estimator})',
194
198
  'entry.close': 'close',
@@ -304,6 +308,9 @@ window.__ModuleLoader__.load({
304
308
  'requests.none': '尚未观测到请求——先发送一条 agent 消息',
305
309
  'requests.session': '会话 {id}({n} 个请求)',
306
310
  'requests.chat': 'chat',
311
+ 'requests.copyId': '复制 call id',
312
+ 'requests.copied': '已复制',
313
+ 'tip.copyId': '该工具在本请求中最大结果的不透明 id——在 Trajectory 标签页中定位那次调用',
307
314
  'entry.loading': '正在加载条目…',
308
315
  'entry.stats': '{chars} 字符 · 约 {tokens} tokens({estimator})',
309
316
  'entry.close': '关闭',
@@ -433,6 +440,56 @@ window.__ModuleLoader__.load({
433
440
  return h(Table, { t, headers: ['plugin', 'sections', 'tools', 'tokens', 'share', ''], rows });
434
441
  }
435
442
 
443
+ /** Expandable tool-results cell: unfolds top tools with the opaque call
444
+ * id of each tool's largest result — the manual bridge to Trajectory
445
+ * until the host exposes a requestInspect write-side. */
446
+ function ResultsCell({ tokens, rows, t }) {
447
+ const [open, setOpen] = react.useState(false);
448
+ const [copied, setCopied] = react.useState(null);
449
+ if (!rows?.length) return h('span', { className: 'xray-num' }, String(tokens));
450
+ return h(
451
+ react.Fragment,
452
+ null,
453
+ h(
454
+ 'button',
455
+ { className: 'xray-entry-link xray-num', onClick: () => setOpen(!open) },
456
+ `${open ? '▾' : '▸'} ${tokens}`,
457
+ ),
458
+ open
459
+ ? h(
460
+ 'div',
461
+ { style: { marginTop: 4 } },
462
+ rows.map((row) =>
463
+ h(
464
+ 'div',
465
+ { key: row.name, style: { whiteSpace: 'nowrap' } },
466
+ h('span', { className: 'xray-muted' }, `${row.name} ~${row.tokens} `),
467
+ row.topCallId
468
+ ? h(
469
+ 'button',
470
+ {
471
+ className: 'xray-entry-link',
472
+ title: t('tip.copyId'),
473
+ onClick: () => {
474
+ try {
475
+ navigator.clipboard.writeText(String(row.topCallId));
476
+ setCopied(row.name);
477
+ setTimeout(() => setCopied(null), 1500);
478
+ } catch {
479
+ /* clipboard unavailable: the id is still visible */
480
+ }
481
+ },
482
+ },
483
+ copied === row.name ? t('requests.copied') : t('requests.copyId'),
484
+ )
485
+ : null,
486
+ ),
487
+ ),
488
+ )
489
+ : null,
490
+ );
491
+ }
492
+
436
493
  /** Raw-text inspector: fetches one entry's live text on open. Fetched per
437
494
  * view, never cached — the text IS the audit artifact. */
438
495
  function EntryModal({ target, onClose, t }) {
@@ -775,10 +832,6 @@ window.__ModuleLoader__.load({
775
832
  'delta',
776
833
  ],
777
834
  rows: session.requests.map((r) => {
778
- const top = (r.toolResultRows ?? [])
779
- .slice(0, 3)
780
- .map((row) => `${row.name} ~${row.tokens}`)
781
- .join(', ');
782
835
  return Row(`${session.sessionId}/${r.seq}`, [
783
836
  { cls: 'xray-num', text: String(r.seq) },
784
837
  { cls: 'xray-muted', text: new Date(r.at).toTimeString().slice(0, 8) },
@@ -789,7 +842,7 @@ window.__ModuleLoader__.load({
789
842
  { cls: 'xray-num', text: String(r.system) },
790
843
  { cls: 'xray-num', text: String(r.toolSchemas) },
791
844
  { cls: 'xray-num', text: String(r.history) },
792
- h('span', { className: 'xray-num', title: top }, String(r.toolResults)),
845
+ h(ResultsCell, { tokens: r.toolResults, rows: r.toolResultRows, t }),
793
846
  r.prefixStable === null ? '—' : r.prefixStable ? '⚡' : '✂',
794
847
  {
795
848
  cls: 'xray-num',
@@ -57,7 +57,7 @@ function classify(options) {
57
57
  let historyMessages = 0;
58
58
  let userMessages = 0;
59
59
  let assistantMessages = 0;
60
- const toolResults = new Map(); // tool name -> {count, chars}
60
+ const toolResults = new Map(); // tool name -> {count, chars, topCallId, topChars}
61
61
  for (const message of options.messages ?? []) {
62
62
  const content = message.content ?? [];
63
63
  for (const block of content) {
@@ -68,15 +68,21 @@ function classify(options) {
68
68
  const chars = content.reduce((sum, block) => sum + blockChars(block), 0);
69
69
  if (isToolResult) {
70
70
  let name = 'unknown';
71
+ let callId = null;
71
72
  for (const block of content) {
72
73
  if (block?.type === 'tool-result') {
74
+ callId = block.toolCallId ?? null;
73
75
  name = callNames.get(block.toolCallId) ?? 'unknown';
74
76
  break;
75
77
  }
76
78
  }
77
- const row = toolResults.get(name) ?? { count: 0, chars: 0 };
79
+ const row = toolResults.get(name) ?? { count: 0, chars: 0, topCallId: null, topChars: -1 };
78
80
  row.count += 1;
79
81
  row.chars += chars;
82
+ if (chars > row.topChars) {
83
+ row.topChars = chars;
84
+ row.topCallId = callId;
85
+ }
80
86
  toolResults.set(name, row);
81
87
  } else {
82
88
  historyChars += chars;
@@ -90,6 +96,9 @@ function classify(options) {
90
96
  name,
91
97
  count: row.count,
92
98
  tokens: estimateTokens(' '.repeat(row.chars)),
99
+ // Opaque provider call id of this tool's LARGEST result — paste it
100
+ // into Trajectory to find the culprit call. No text is retained.
101
+ topCallId: row.topCallId,
93
102
  }))
94
103
  .sort((a, b) => b.tokens - a.tokens);
95
104
  const toolResultTokens = toolResultRows.reduce((sum, r) => sum + r.tokens, 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-xray",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "X-ray for your DeepSeek Harness — see what's actually loaded, why, and what it costs you.",
5
5
  "repository": {
6
6
  "type": "git",