feat(experimental): explain retries, attempts, context refreshes, and approvals as event rows
This commit is contained in:
1 parent
a5b5d3a865
commit
8a25bac0ab
12 files changed
+299
-15
No files matched your search
@@ -24,8 +24,8 @@
|
||||
en: e001601f2f9779b1
|
||||
zh: 077d3703b8c82774
|
||||
/deepseek-ai-dsh-experimental-client-ui-perf/understand-the-implementation:
|
||||
en: 0cbfb94712295d33
|
||||
zh: a2dff5b3ff9d7e20
|
||||
en: 4d645bef4c3a4274
|
||||
zh: 84a6579329a3451a
|
||||
/deepseek-ai-dsh-experimental-client-ui-perf/further-exploration:
|
||||
en: 285cfe1d7a99af34
|
||||
zh: fbcee76e0c2f20fb
|
||||
@@ -36,8 +36,8 @@
|
||||
en: 7605bb868f93b4a8
|
||||
zh: 184ca10ce6680499
|
||||
/deepseek-ai-dsh-experimental-client-ui-perf/known-limitations-and-deferred-work:
|
||||
en: 59c2ae9711e9a859
|
||||
zh: 330268dcb381ef56
|
||||
en: d312e0bd876fbda2
|
||||
zh: cad3af2d8f671158
|
||||
/deepseek-ai-dsh-experimental-client-ui-perf/known-limitations-and-deferred-work/dev-note:
|
||||
en: 3518dbaf30fb19a6
|
||||
zh: 73565f0bcc32a364
|
||||
@@ -61,7 +61,7 @@ The footer switches control the totals row, event rows, and the context basis, a
|
||||
|
||||
The footer switches control the totals row and the context basis. `Context basis` changes only how a turn row aggregates its steps: `Latest` shows the turn's final prompt size, while `Cumulative` sums that turn's step prompt sizes. The totals row is not affected by this switch. When the host exposes the `sessionStats` and `tokenUsage` projections, the totals row covers the complete Session: turns, steps, cumulative context and output, token-weighted cache, first-token/model/tool time, and output speed. Total time there is model time + tool time because no whole-session wall projection exists. Without those projections the row falls back to the loaded window and is labelled `Window total`. Token cells switch from `K` to `M` at ten million.
|
||||
|
||||
Non-step rows are hidden by default; the `Show event rows (incl. compaction)` footer switch enables them. Compaction is a log-only transaction, not a step. The table inserts a synthetic `Compaction` row between the steps it ran between; the row shows the summary call's context, output and cache-hit cells, the shadowed token count, the summary size, and its wall time. The same switch reveals request-header, system-prompt, tool-set, and model-free prune rows: a request header shows its reason, provider/model, series flag and tool count; a system prompt shows append/replace/clear and character size; a tool-set row lists added and removed tool names; a prune row shows the pruned token count. These rows are excluded from the turn and totals step sums, so the step columns keep their step-only meaning.
|
||||
Non-step rows are hidden by default; the `Show event rows (incl. compaction)` footer switch enables them. Compaction is a log-only transaction, not a step. The table inserts a synthetic `Compaction` row between the steps it ran between; the row shows the summary call's context, output and cache-hit cells, the shadowed token count, the summary size, and its wall time. The same switch reveals request-header, route-context, system-prompt, tool-set, model-retry, model-attempt, context-refresh, approval, and model-free prune rows: a request header shows its reason, provider/model, series flag and tool count; a route context shows provider/model and the prompt-update mode; a system prompt shows append/replace/clear and size; a tool-set row lists added and removed tool names; a retry row shows the attempt number, wait, and failure message; an attempt row marks a model attempt that committed no message; a context-refresh row shows runtime/time context refresh size; an approval row shows the tool and waiting/allowed/rejected state; a prune row shows the pruned token count. These rows are excluded from the turn and totals step sums, so the step columns keep their step-only meaning.
|
||||
|
||||
The turn cache-hit cell is token-weighted across the steps that reported usage (summed cache reads ÷ summed prompt tokens), so one transient cold step is not hidden by a warmer neighbour; the tooltip shows the summed read/input/write breakdown. A step row keeps its own measured share.
|
||||
|
||||
@@ -96,7 +96,8 @@ No effect. The package never constructs a model request and never mutates a prom
|
||||
- **Not mounted by the default product.** The Web bundle refuses experimental packages by rule; the optional Developer tools bundle (`@deepseek-ai/dsh-experimental-inspector-profile`) mounts this view, or a profile can add the row explicitly.
|
||||
- **Trimmed windows lose start times.** When the loaded window begins mid-step, first-token, model time, and total time cannot be derived; the row says `start unknown` instead of estimating.
|
||||
- **Code-Mode dispatches merge by root-call context.** Inner calls carry no turn or step, so each root call opens its own context keyed by the root call id, and the builder merges its dispatches into the step that owns the runner. A window that lost the dispatch start still merges the calls but cannot show their measured duration.
|
||||
- **Browser evidence is a test-only overlay.** Package tests cover folding, aggregation, formatting, and the jsdom table; `apps/web/tests/client-ui-perf-panel.e2e.ts` mounts the plugin through a test-only overlay over the shipped Web surface and compares a committed ARIA golden. No recorded-session scenario exists because no shipped profile mounts the package yet.
|
||||
- **Known limitation: no whole-session wall projection.** The totals row substitutes model time + tool time because the host projections expose no whole-session wall clock. The tool segment sums paired call durations, so parallel calls can make that split look longer than a non-overlapping wall interval. This is recorded here; the view does not change host logic to fill the gap.
|
||||
- **Browser evidence is a test-only overlay.** Package tests cover folding, aggregation, formatting, and the jsdom table; `apps/web/tests/client-ui-perf-panel.e2e.ts` mounts the plugin through a test-only overlay over the shipped Web surface and compares a committed ARIA golden. No recorded-session scenario exists.
|
||||
- **Late re-deliveries are not turns.** Compaction can restate a pruned tool result under its original turn and step after that turn left the window; a turn with no loaded start, prompt, or end marker is dropped instead of appearing as an empty ghost row.
|
||||
- **Timing is derived, not sampled.** Total time is the last step's end minus the first step's start; queueing and scheduler waits land in the "rest" segment. The total-time bar draws tool calls as the union of their paired intervals, so parallel calls count once; the tool column keeps the summed per-call time. A turn bar is scaled against the turns currently visible on screen, so a much larger turn that scrolled away does not flatten the rows in view.
|
||||
- **Prompt images load through the shared image cache.** The pill and its preview resolve durable attachments with `uiConversation.imageUrl`, so the cache owns and revokes the browser URLs per Session binding; the view creates none of its own. A failed read shows a localized failure state with retry. Touch clients do not open the hover preview; tapping the pill loads the first image directly.
|
||||
|
||||
@@ -61,7 +61,7 @@ kind: "package-reference"
|
||||
|
||||
页脚的开关控制合计行与上下文口径。「上下文口径」只改变轮次行对其各步的聚合方式:「最新」显示该轮最后一步的提示词大小,「累计」显示该轮各步提示词大小之和。合计行不受这个开关影响。宿主提供 `sessionStats` 与 `tokenUsage` projection 时,合计行覆盖整段会话:轮次、步数、累计上下文与输出、token 加权缓存、首 token/模型/工具用时和输出速度;总用时为模型用时 + 工具用时(暂无整段墙钟 projection)。projection 不可用时回退到已加载窗口,并标为「本窗口合计」。token 超过一千万时从 K 切换为 M。
|
||||
|
||||
非步骤行默认隐藏,由页脚的「显示事件行(含压缩)」开关打开。压缩是 log-only 事务,不是 step。表格会在压缩发生的相邻步骤之间插入一条合成的「压缩」行,显示摘要调用的上下文、输出与缓存命中、被压缩的 token 数、摘要大小和墙钟时间。同一个开关还会显示请求头、系统提示词、工具集变更和纯裁剪行:请求头显示 reason、provider/model、是否新序列和工具数;系统提示词显示追加/替换/清空和字符数;工具集行列出新增与移除;裁剪行显示被裁剪的 token 数。这些行不计入轮次与合计的步骤求和,步骤列仍只表示步骤。
|
||||
非步骤行默认隐藏,由页脚的「显示事件行(含压缩)」开关打开。压缩是 log-only 事务,不是 step。表格会在压缩发生的相邻步骤之间插入一条合成的「压缩」行,显示摘要调用的上下文、输出与缓存命中、被压缩的 token 数、摘要大小和墙钟时间。同一个开关还会显示请求头、路由能力、系统提示词、工具集变更、模型重试、模型尝试、上下文刷新、审批和纯裁剪行:请求头显示 reason、provider/model、是否新序列和工具数;路由能力显示 provider/model 和提示词更新模式;系统提示词显示追加/替换/清空和字符数;工具集行列出新增与移除;重试行显示第几次、等待时间和失败信息;尝试行标记未提交消息的模型尝试;上下文刷新行显示运行时/时间上下文的刷新大小;审批行显示工具和等待/允许/拒绝状态;裁剪行显示被裁剪的 token 数。这些行不计入轮次与合计的步骤求和,步骤列仍只表示步骤。
|
||||
|
||||
轮次的缓存命中率按报了 usage 的各步做 token 加权(缓存读之和 ÷ 提示词 token 之和),不会因为相邻步命中高就把某一次冷启动盖掉;tooltip 展示汇总后的读/输入/写明细。步骤行仍显示该步自己的实测比例。
|
||||
|
||||
@@ -96,6 +96,7 @@ kind: "package-reference"
|
||||
- **默认产品不挂载。** Web 包按规则拒绝引用 experimental 包;可选的「开发者工具」组合包(`@deepseek-ai/dsh-experimental-inspector-profile`)会挂载本视图,也可以由 profile 显式添加。
|
||||
- **窗口被截断时没有起始时刻。** 已加载窗口从半步中间开始时,首 token、模型用时和总用时都推导不出来;行上标「起止未知」,不估算。
|
||||
- **Code-Mode 内层调用按根调用上下文并回步骤。** 内层调用不带 turn/step,因此每个根调用各自开一个以 root call id 为键的上下文,由 builder 把它的派发并回持有 runner 的那一步。窗口里丢了派发起点时仍会合并调用,但无法给出它们各自的实测耗时。
|
||||
- **已知限制:没有整段墙钟 projection。** 合计行的总用时用模型用时 + 工具用时替代,因为宿主 projection 没有整段墙钟;工具段是逐调用求和,并行调用会让这段显得比非重叠墙钟更长。这里作为已知限制记录,插件不为此改动宿主逻辑。
|
||||
- **浏览器证据是仅测试用的 overlay。** 包级测试覆盖折叠、汇总、格式化与 jsdom 表格;`apps/web/tests/client-ui-perf-panel.e2e.ts` 用仅测试用的 overlay 把插件挂到真实 Web 面上,并比对提交的 ARIA golden。由于还没有任何 shipped profile 挂载本包,暂无录制会话场景。
|
||||
- **迟到重投不是轮次。** 压缩可能把被裁剪的工具结果按原来的轮次/步骤在之后重新投递;窗口里既没有加载到的起点、也没有问题或结束标记的轮次会被丢弃,不再显示成空的幽灵行。
|
||||
- **计时是推导值。** 总用时取最后一步结束减第一步开始;排队和调度等待落在「其余」段里。总用时条把工具调用画成配对区间之并,并行调用只算一次;工具列仍显示逐次求和。轮次时间条以当前屏幕内可见的轮次为刻度,滚出屏幕的超大轮次不会把本屏的行压扁。
|
||||
|
||||
@@ -67,7 +67,8 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"@deepseek-ai/dsh-compaction": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-stats": "workspace:*",
|
||||
"@deepseek-ai/dsh-token-meter": "workspace:*"
|
||||
"@deepseek-ai/dsh-token-meter": "workspace:*",
|
||||
"@deepseek-ai/dsh-user-approval": "workspace:*"
|
||||
},
|
||||
"files": [
|
||||
"locale/*.json",
|
||||
|
||||
@@ -1463,6 +1463,11 @@ function eventLabel(kind: PerfEventKind, t: PerfTranslate): string {
|
||||
case 'system-prompt': return t('event.system-prompt')
|
||||
case 'tool-set': return t('event.tool-set')
|
||||
case 'context-prune': return t('event.context-prune')
|
||||
case 'request-context': return t('event.request-context')
|
||||
case 'model-retry': return t('event.model-retry')
|
||||
case 'model-attempt': return t('event.model-attempt')
|
||||
case 'context-refresh': return t('event.context-refresh')
|
||||
case 'approval': return t('event.approval')
|
||||
/* v8 ignore next -- PerfEventKind is exhaustive. */
|
||||
default: return ''
|
||||
}
|
||||
@@ -1490,6 +1495,27 @@ function eventAction(action: PerfEventMetrics['surfaceOp'], t: PerfTranslate): s
|
||||
}
|
||||
}
|
||||
|
||||
/** Localized Context refresh source. */
|
||||
function eventSource(source: string | null, t: PerfTranslate): string {
|
||||
switch (source) {
|
||||
case 'runtime-context': return t('event.source.runtime')
|
||||
case 'time-context': return t('event.source.time')
|
||||
default: return source ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
/** Localized approval state or outcome. */
|
||||
function eventApproval(status: string | null, t: PerfTranslate): string {
|
||||
switch (status) {
|
||||
case 'asked': return t('event.approval.asked')
|
||||
case 'allowed-once': return t('event.approval.allowed')
|
||||
case 'rejected': return t('event.approval.rejected')
|
||||
case 'cancelled': return t('event.approval.cancelled')
|
||||
case 'unavailable': return t('event.approval.unavailable')
|
||||
default: return status ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
/** One-line explanation of a non-step event. */
|
||||
function eventDetail(event: PerfEventMetrics, t: PerfTranslate): string {
|
||||
switch (event.kind) {
|
||||
@@ -1513,6 +1539,29 @@ function eventDetail(event: PerfEventMetrics, t: PerfTranslate): string {
|
||||
}
|
||||
case 'context-prune':
|
||||
return t('event.prune.detail', { tokens: formatTokens(event.shadowedTokens) })
|
||||
case 'request-context':
|
||||
return t('event.context.detail', {
|
||||
provider: event.provider ?? '',
|
||||
model: event.model ?? '',
|
||||
mode: event.status ?? '',
|
||||
})
|
||||
case 'model-retry':
|
||||
return t('event.retry.detail', {
|
||||
retry: String(event.retry ?? 0),
|
||||
delay: formatMs(event.delayMs),
|
||||
})
|
||||
case 'model-attempt':
|
||||
return t('event.attempt.detail')
|
||||
case 'context-refresh':
|
||||
return t('event.refresh.detail', {
|
||||
source: eventSource(event.status, t),
|
||||
chars: formatChars(event.chars),
|
||||
})
|
||||
case 'approval':
|
||||
return t('event.approval.detail', {
|
||||
status: eventApproval(event.status, t),
|
||||
tool: event.label ?? '',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1534,9 +1583,14 @@ function eventRow(event: PerfEventMetrics, t: PerfTranslate): ReactElement {
|
||||
? `${event.provider} / ${event.model}`
|
||||
: event.kind === 'system-prompt'
|
||||
? eventAction(event.surfaceOp, t)
|
||||
: event.kind === 'tool-set' ? t('event.tool-set') : ''
|
||||
: event.kind === 'tool-set'
|
||||
? t('event.tool-set')
|
||||
: event.kind === 'context-refresh'
|
||||
? eventSource(event.status, t)
|
||||
: event.kind === 'approval' ? eventApproval(event.status, t) : ''
|
||||
const title = event.error === null ? t('event.title') : `${t('event.title')}\n${event.error}`
|
||||
return (
|
||||
<tr className={css.eventRow} title={t('event.title')}>
|
||||
<tr className={css.eventRow} title={title}>
|
||||
<td className={css.stepCell}>
|
||||
<span className={css.compactionTag}>{eventLabel(event.kind, t)}</span>
|
||||
<span className={css.stepClock}>{formatClock(event.time)}</span>
|
||||
|
||||
@@ -56,7 +56,24 @@ export const zh = {
|
||||
'event.system-prompt': '系统提示词',
|
||||
'event.tool-set': '工具集变更',
|
||||
'event.context-prune': '上下文裁剪',
|
||||
'event.title': '非步骤事件行:解释请求、提示词、工具集和裁剪变化,不计入轮次的步骤合计',
|
||||
'event.request-context': '路由能力',
|
||||
'event.model-retry': '模型重试',
|
||||
'event.model-attempt': '模型尝试',
|
||||
'event.context-refresh': '上下文刷新',
|
||||
'event.approval': '审批',
|
||||
'event.context.detail': '{provider} / {model} · 提示词模式 {mode}',
|
||||
'event.retry.detail': '第 {retry} 次重试 · 等待 {delay}',
|
||||
'event.attempt.detail': '未提交消息的模型尝试',
|
||||
'event.refresh.detail': '{source} · {chars} 字符',
|
||||
'event.approval.detail': '{tool} · {status}',
|
||||
'event.source.runtime': '运行时上下文',
|
||||
'event.source.time': '时间上下文',
|
||||
'event.approval.asked': '等待审批',
|
||||
'event.approval.allowed': '允许一次',
|
||||
'event.approval.rejected': '拒绝',
|
||||
'event.approval.cancelled': '取消',
|
||||
'event.approval.unavailable': '不可用',
|
||||
'event.title': '非步骤事件行:解释请求、提示词、工具集、重试、上下文刷新、审批和裁剪变化,不计入轮次的步骤合计',
|
||||
'event.reason.initial': '初始',
|
||||
'event.reason.resume': '恢复',
|
||||
'event.reason.change': '变更',
|
||||
@@ -232,7 +249,24 @@ export const en: Record<PerfKey, string> = {
|
||||
'event.system-prompt': 'System prompt',
|
||||
'event.tool-set': 'Tool set change',
|
||||
'event.context-prune': 'Context prune',
|
||||
'event.title': 'Non-step event rows: request, prompt, tool-set, and prune changes excluded from the turn step totals',
|
||||
'event.request-context': 'Request context',
|
||||
'event.model-retry': 'Model retry',
|
||||
'event.model-attempt': 'Model attempt',
|
||||
'event.context-refresh': 'Context refresh',
|
||||
'event.approval': 'Approval',
|
||||
'event.context.detail': '{provider} / {model} · prompt mode {mode}',
|
||||
'event.retry.detail': 'retry {retry} · wait {delay}',
|
||||
'event.attempt.detail': 'Model attempt without a committed message',
|
||||
'event.refresh.detail': '{source} · {chars} chars',
|
||||
'event.approval.detail': '{tool} · {status}',
|
||||
'event.source.runtime': 'runtime context',
|
||||
'event.source.time': 'time context',
|
||||
'event.approval.asked': 'waiting',
|
||||
'event.approval.allowed': 'allowed once',
|
||||
'event.approval.rejected': 'rejected',
|
||||
'event.approval.cancelled': 'cancelled',
|
||||
'event.approval.unavailable': 'unavailable',
|
||||
'event.title': 'Non-step event rows: request, prompt, tool-set, retry, context-refresh, approval, and prune changes excluded from the turn step totals',
|
||||
'event.reason.initial': 'initial',
|
||||
'event.reason.resume': 'resume',
|
||||
'event.reason.change': 'change',
|
||||
@@ -395,7 +429,24 @@ export const ja: Record<PerfKey, string> = {
|
||||
'event.system-prompt': 'システムプロンプト',
|
||||
'event.tool-set': 'ツールセット変更',
|
||||
'event.context-prune': 'コンテキスト削除',
|
||||
'event.title': '非ステップのイベント行:リクエスト・プロンプト・ツールセット・削除の変化を説明し、ターンのステップ合計には含めません',
|
||||
'event.request-context': 'ルーティング能力',
|
||||
'event.model-retry': 'モデル再試行',
|
||||
'event.model-attempt': 'モデル試行',
|
||||
'event.context-refresh': 'コンテキスト更新',
|
||||
'event.approval': '承認',
|
||||
'event.context.detail': '{provider} / {model} · プロンプトモード {mode}',
|
||||
'event.retry.detail': '再試行 {retry} · 待機 {delay}',
|
||||
'event.attempt.detail': 'メッセージを確定しなかったモデル試行',
|
||||
'event.refresh.detail': '{source} · {chars} 文字',
|
||||
'event.approval.detail': '{tool} · {status}',
|
||||
'event.source.runtime': 'ランタイムコンテキスト',
|
||||
'event.source.time': '時間コンテキスト',
|
||||
'event.approval.asked': '承認待ち',
|
||||
'event.approval.allowed': '1 回許可',
|
||||
'event.approval.rejected': '拒否',
|
||||
'event.approval.cancelled': 'キャンセル',
|
||||
'event.approval.unavailable': '利用不可',
|
||||
'event.title': '非ステップのイベント行:リクエスト・プロンプト・ツールセット・再試行・コンテキスト更新・承認・削除の変化を説明し、ターンのステップ合計には含めません',
|
||||
'event.reason.initial': '初期',
|
||||
'event.reason.resume': '再開',
|
||||
'event.reason.change': '変更',
|
||||
|
||||
@@ -179,7 +179,16 @@ export interface PerfTurnMetrics {
|
||||
}
|
||||
|
||||
/** Non-step event kinds the performance table can explain. */
|
||||
export type PerfEventKind = 'request-header' | 'system-prompt' | 'tool-set' | 'context-prune'
|
||||
export type PerfEventKind =
|
||||
| 'request-header'
|
||||
| 'system-prompt'
|
||||
| 'tool-set'
|
||||
| 'context-prune'
|
||||
| 'request-context'
|
||||
| 'model-retry'
|
||||
| 'model-attempt'
|
||||
| 'context-refresh'
|
||||
| 'approval'
|
||||
|
||||
/** One synthetic event row: request admission, prompt/tool changes, or a model-free prune. */
|
||||
export interface PerfEventMetrics {
|
||||
@@ -197,6 +206,11 @@ export interface PerfEventMetrics {
|
||||
readonly added: readonly string[]
|
||||
readonly removed: readonly string[]
|
||||
readonly shadowedTokens: number | null
|
||||
readonly status: string | null
|
||||
readonly retry: number | null
|
||||
readonly delayMs: number | null
|
||||
readonly error: string | null
|
||||
readonly label: string | null
|
||||
}
|
||||
|
||||
/** One completed or running compaction transaction with its model-call facts. */
|
||||
|
||||
@@ -5,11 +5,19 @@ import type {
|
||||
} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type {} from '@deepseek-ai/dsh-compaction/types'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import type { LlmRetryEventData } from '@deepseek-ai/dsh-llm-retry/types'
|
||||
import type {} from '@deepseek-ai/dsh-user-approval/types'
|
||||
import type { SessionEventMap } from '@deepseek-ai/dsh-session/types'
|
||||
import type { PerfEventMetrics } from './perf-contract.ts'
|
||||
|
||||
/** Event shape a Conversation Definition receives. */
|
||||
type MatchEvent = Parameters<ConversationNodeDefinition['match']>[0]
|
||||
|
||||
/** Payload of an event whose type is known to the caller. */
|
||||
function dataOf<T>(event: MatchEvent): T {
|
||||
return (event as MatchEvent & { data: T }).data
|
||||
}
|
||||
|
||||
/** Turn and step a Location resolves, or null when the window lost them. */
|
||||
function locationTurnStep(
|
||||
location: ConversationLocation | undefined,
|
||||
@@ -65,6 +73,11 @@ export function eventMetrics(event: MatchEvent, location: ConversationLocation |
|
||||
added: [] as string[],
|
||||
removed: [] as string[],
|
||||
shadowedTokens: null,
|
||||
status: null,
|
||||
retry: null,
|
||||
delayMs: null,
|
||||
error: null,
|
||||
label: null,
|
||||
} satisfies Omit<PerfEventMetrics, 'kind'>
|
||||
if (event.type === 'request/header') {
|
||||
const header = event.data.header
|
||||
@@ -94,5 +107,68 @@ export function eventMetrics(event: MatchEvent, location: ConversationLocation |
|
||||
if (event.type === 'compaction/prune') {
|
||||
return { ...base, kind: 'context-prune', shadowedTokens: event.data.shadowedTokenCount }
|
||||
}
|
||||
const eventType: string = event.type
|
||||
if (eventType === 'request/context') {
|
||||
const data = dataOf<SessionEventMap['request/context']>(event)
|
||||
return {
|
||||
...base,
|
||||
kind: 'request-context',
|
||||
provider: data.provider,
|
||||
model: data.model,
|
||||
status: data.systemPromptUpdate ?? null,
|
||||
chars: data.contextWindow ?? null,
|
||||
}
|
||||
}
|
||||
if (eventType === 'llm/retry') {
|
||||
const data = dataOf<LlmRetryEventData>(event)
|
||||
return {
|
||||
...base,
|
||||
kind: 'model-retry',
|
||||
turn: data.turn,
|
||||
step: data.step,
|
||||
provider: data.provider,
|
||||
status: data.mode,
|
||||
retry: data.retry,
|
||||
delayMs: data.delayMs,
|
||||
error: data.failure.message,
|
||||
}
|
||||
}
|
||||
if (eventType === 'assistant/attempt') {
|
||||
const data = dataOf<{ turn: number; step: number }>(event)
|
||||
return {
|
||||
...base,
|
||||
kind: 'model-attempt',
|
||||
turn: data.turn,
|
||||
step: data.step,
|
||||
status: 'attempt',
|
||||
}
|
||||
}
|
||||
if (eventType === 'user/message') {
|
||||
const data = dataOf<{
|
||||
source: { kind: string }
|
||||
content: readonly { type: string; text?: string }[]
|
||||
}>(event)
|
||||
if (data.source.kind !== 'runtime-context' && data.source.kind !== 'time-context') return null
|
||||
return {
|
||||
...base,
|
||||
kind: 'context-refresh',
|
||||
status: data.source.kind,
|
||||
chars: contentChars(data.content),
|
||||
}
|
||||
}
|
||||
if (eventType === 'approval/asked') {
|
||||
const data = dataOf<{ toolName: string; reason?: string }>(event)
|
||||
return {
|
||||
...base,
|
||||
kind: 'approval',
|
||||
status: 'asked',
|
||||
label: data.toolName,
|
||||
error: data.reason ?? null,
|
||||
}
|
||||
}
|
||||
if (eventType === 'approval/decided') {
|
||||
const data = dataOf<{ outcome: string }>(event)
|
||||
return { ...base, kind: 'approval', status: data.outcome }
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -823,6 +823,48 @@ describe('conversation Definitions', () => {
|
||||
}), undefined)).toBeNull()
|
||||
})
|
||||
|
||||
it('normalizes lifecycle events for the event rows', () => {
|
||||
const turnLocation = { kind: 'turn', turn: { turn: 9 } } as Parameters<typeof eventMetrics>[1]
|
||||
expect(eventMetrics(event('request/context', {
|
||||
provider: 'p', model: 'm', contextWindow: 128_000, systemPromptUpdate: 'in-history',
|
||||
}), turnLocation)).toMatchObject({
|
||||
kind: 'request-context', turn: 9, step: null, provider: 'p', model: 'm', status: 'in-history', chars: 128_000,
|
||||
})
|
||||
expect(eventMetrics(event('request/context', { provider: 'p', model: 'm' }), undefined)).toMatchObject({
|
||||
status: null, chars: null,
|
||||
})
|
||||
expect(eventMetrics(event('llm/retry', {
|
||||
retryId: 'r1', turn: 3, step: 2, provider: 'p', mode: 'normal', policyKey: 'k', retry: 2, maxRetries: 5,
|
||||
delayMs: 1_500, failure: { message: 'rate limited', code: 'RATE_LIMIT' },
|
||||
}), undefined)).toMatchObject({
|
||||
kind: 'model-retry', turn: 3, step: 2, provider: 'p', status: 'normal', retry: 2, delayMs: 1_500,
|
||||
error: 'rate limited',
|
||||
})
|
||||
expect(eventMetrics(event('assistant/attempt', { turn: 3, step: 2, stream: [] }), undefined)).toMatchObject({
|
||||
kind: 'model-attempt', turn: 3, step: 2, status: 'attempt',
|
||||
})
|
||||
expect(eventMetrics(surfaceEvent('user/message', {
|
||||
role: 'user', source: { kind: 'runtime-context' }, content: [{ type: 'text', text: 'ctx' }],
|
||||
}, 'append'), undefined)).toMatchObject({ kind: 'context-refresh', status: 'runtime-context', chars: 3 })
|
||||
expect(eventMetrics(surfaceEvent('user/message', {
|
||||
role: 'user', source: { kind: 'time-context' }, content: [],
|
||||
}, 'append'), undefined)).toMatchObject({ kind: 'context-refresh', status: 'time-context', chars: 0 })
|
||||
expect(eventMetrics(surfaceEvent('user/message', {
|
||||
role: 'user', source: { kind: 'user' }, content: [],
|
||||
}, 'append'), undefined)).toBeNull()
|
||||
expect(eventMetrics(event('approval/asked', {
|
||||
id: 'a1', toolName: 'bash', reason: 'needs approval',
|
||||
}), turnLocation)).toMatchObject({
|
||||
kind: 'approval', turn: 9, step: null, status: 'asked', label: 'bash', error: 'needs approval',
|
||||
})
|
||||
expect(eventMetrics(event('approval/asked', { id: 'a2', toolName: 'read' }), undefined)).toMatchObject({
|
||||
kind: 'approval', status: 'asked', label: 'read', error: null,
|
||||
})
|
||||
expect(eventMetrics(event('approval/decided', { id: 'a1', outcome: 'rejected' }), turnLocation)).toMatchObject({
|
||||
kind: 'approval', turn: 9, status: 'rejected',
|
||||
})
|
||||
})
|
||||
|
||||
it('attributes Code-Mode dispatch events through the root call table', async () => {
|
||||
const definitions = await registeredDefinitions()
|
||||
const snapshot = assemble(definitions, [
|
||||
|
||||
@@ -165,6 +165,11 @@ function perfEvent(overrides: Partial<PerfEventMetrics> = {}): PerfEventMetrics
|
||||
added: [],
|
||||
removed: [],
|
||||
shadowedTokens: null,
|
||||
status: null,
|
||||
retry: null,
|
||||
delayMs: null,
|
||||
error: null,
|
||||
label: null,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
@@ -956,7 +961,23 @@ describe('turn rows', () => {
|
||||
perfEvent({ time: BASE + 800, kind: 'tool-set', added: ['read'], removed: ['edit'] }),
|
||||
perfEvent({ time: BASE + 850, kind: 'tool-set', added: ['read'], removed: [] }),
|
||||
perfEvent({ time: BASE + 900, kind: 'tool-set', added: [], removed: ['edit'] }),
|
||||
perfEvent({ time: BASE + 1_000, kind: 'context-prune', shadowedTokens: 4_000 }),
|
||||
perfEvent({ time: BASE + 950, kind: 'request-context', provider: 'p', model: 'm', status: 'in-history', chars: 128_000 }),
|
||||
perfEvent({ time: BASE + 960, kind: 'request-context', provider: 'p', model: 'm', status: null, chars: null }),
|
||||
perfEvent({ time: BASE + 965, kind: 'request-context', provider: null, model: null, status: null, chars: null }),
|
||||
perfEvent({ time: BASE + 970, kind: 'model-retry', provider: 'p', retry: 2, delayMs: 1_500, error: 'rate limited' }),
|
||||
perfEvent({ time: BASE + 975, kind: 'model-retry', provider: null, retry: null, delayMs: null, error: null }),
|
||||
perfEvent({ time: BASE + 980, kind: 'model-attempt' }),
|
||||
perfEvent({ time: BASE + 990, kind: 'context-refresh', status: 'runtime-context', chars: 32 }),
|
||||
perfEvent({ time: BASE + 995, kind: 'context-refresh', status: 'time-context', chars: 16 }),
|
||||
perfEvent({ time: BASE + 997, kind: 'context-refresh', status: null, chars: null }),
|
||||
perfEvent({ time: BASE + 1_000, kind: 'approval', status: 'asked', label: 'bash', error: 'needs approval' }),
|
||||
perfEvent({ time: BASE + 1_010, kind: 'approval', status: 'allowed-once', label: 'bash' }),
|
||||
perfEvent({ time: BASE + 1_020, kind: 'approval', status: 'rejected', label: 'bash' }),
|
||||
perfEvent({ time: BASE + 1_030, kind: 'approval', status: 'cancelled', label: 'bash' }),
|
||||
perfEvent({ time: BASE + 1_040, kind: 'approval', status: 'unavailable', label: 'bash' }),
|
||||
perfEvent({ time: BASE + 1_050, kind: 'approval', status: null, label: null }),
|
||||
perfEvent({ time: BASE + 1_055, kind: 'approval', status: 'asked', label: null, error: null }),
|
||||
perfEvent({ time: BASE + 1_060, kind: 'context-prune', shadowedTokens: 4_000 }),
|
||||
],
|
||||
}))
|
||||
expect(screen.queryByText(say('event.request-header'))).toBeNull()
|
||||
@@ -973,6 +994,24 @@ describe('turn rows', () => {
|
||||
}))).toBeTruthy()
|
||||
expect(screen.getAllByText(/新增 read|added read/).length).toBeGreaterThan(0)
|
||||
expect(screen.getByText(say('event.prune.detail', { tokens: formatTokens(4_000) }))).toBeTruthy()
|
||||
expect(screen.getAllByText(say('event.request-context'))).toHaveLength(3)
|
||||
expect(screen.getByText(say('event.context.detail', { provider: 'p', model: 'm', mode: 'in-history' }))).toBeTruthy()
|
||||
expect(screen.getAllByText(say('event.model-retry'))).toHaveLength(2)
|
||||
expect(screen.getByText(say('event.retry.detail', {
|
||||
retry: '2', delay: formatMs(1_500),
|
||||
}))).toBeTruthy()
|
||||
expect(screen.getByText(say('event.attempt.detail'))).toBeTruthy()
|
||||
expect(screen.getAllByText(say('event.context-refresh'))).toHaveLength(3)
|
||||
expect(screen.getByText(say('event.refresh.detail', {
|
||||
source: say('event.source.runtime'), chars: formatChars(32),
|
||||
}))).toBeTruthy()
|
||||
expect(screen.getByText(say('event.refresh.detail', {
|
||||
source: say('event.source.time'), chars: formatChars(16),
|
||||
}))).toBeTruthy()
|
||||
expect(screen.getAllByText(say('event.approval'))).toHaveLength(7)
|
||||
for (const status of ['asked', 'allowed', 'rejected', 'cancelled', 'unavailable'] as const) {
|
||||
expect(screen.getAllByText(say(`event.approval.${status}`)).length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('shows context deltas only when the delta switch is on', () => {
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
{ "path": "../../core/session" },
|
||||
{ "path": "../../core/tools" },
|
||||
{ "path": "../../llm/llm" },
|
||||
{ "path": "../../llm/llm-retry" },
|
||||
{ "path": "../../interaction/user-approval" },
|
||||
{ "path": "../../llm/token-meter" },
|
||||
{ "path": "../../session/session-stats" }
|
||||
]
|
||||
|
||||
Generated
+3
@@ -9025,6 +9025,9 @@ importers:
|
||||
'@deepseek-ai/dsh-tools':
|
||||
specifier: workspace:*
|
||||
version: link:../../core/tools
|
||||
'@deepseek-ai/dsh-user-approval':
|
||||
specifier: workspace:*
|
||||
version: link:../../interaction/user-approval
|
||||
'@testing-library/react':
|
||||
specifier: ^16.1.0
|
||||
version: 16.3.3(@testing-library/[email protected])(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
|
||||
Reference in new issue
Block a user