Merge pull request #3071 from deepseek-harness/worktree/3070-minimal-bash-card-expand

fix(web): expand persistent Bash result cards
This commit is contained in:
Yichen Jiang
2026-08-25 21:22:18 +08:00
committed by GitHub
9 changed files with 173 additions and 33 deletions
+65 -10
View File
@@ -1,25 +1,41 @@
import { mkdir, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import type { AgentHandle } from '@deepseek-ai/dsh-agent'
import { CallId, createUserMessage } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-agent-presets'
import type {} from '@deepseek-ai/dsh-system-prompt'
import { assertFixtureInventory, launchWebScaffold, type WebScaffold } from './scaffold.ts'
import {
assertFixtureInventory,
captureStableAria,
compareOrRefreshGolden,
launchWebScaffold,
watchConsole,
webSnapshotMode,
type WebScaffold,
} from './scaffold.ts'
import { newEnglishPage, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/minimal-preset', import.meta.url))
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
const PROMPT = 'Reply exactly MINIMAL_PRESET_REQUEST_OK and stop.'
const UI_EXPECTED = join(SNAPSHOT_DIR, 'ui.expected.md')
const MODE = webSnapshotMode()
const PROMPT = "Use the bash tool to run exactly: printf 'MINIMAL_BASH_CARD_OK\\n'. Then reply exactly MINIMAL_PRESET_REQUEST_OK and stop."
describe('minimal agent preset', () => {
let scaffold: WebScaffold
let agentHandle: AgentHandle
let disposeInjectedPrompt: () => void
let browser: Browser | undefined
let page: Page | undefined
let tripwire: ReturnType<typeof watchConsole> | undefined
beforeAll(async () => {
scaffold = await launchWebScaffold({ replayFixture: FIXTURE, compareReplaySession: true })
scaffold = await launchWebScaffold({ replayFixture: FIXTURE, compareReplaySession: true, paceMs: 10 })
disposeInjectedPrompt = scaffold.ctx.systemPrompt.section({
name: 'test:injected-prompt',
order: 999,
@@ -31,10 +47,17 @@ describe('minimal agent preset', () => {
agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
setup: agentCtx => scaffold.ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
})
agentHandle.agent.followup(createUserMessage({
content: [{ type: 'text', text: PROMPT }],
source: { kind: 'user' },
}))
await agentHandle.agent.whenIdle()
})
afterAll(async () => {
const failures: unknown[] = []
await page?.close().catch((error: unknown) => failures.push(error))
await browser?.close().catch((error: unknown) => failures.push(error))
await agentHandle?.dispose().catch((error: unknown) => failures.push(error))
try {
disposeInjectedPrompt?.()
@@ -47,12 +70,6 @@ describe('minimal agent preset', () => {
})
it('sends the exact RL prompt and schemas, then executes the persistent shell and editor', async () => {
agentHandle.agent.followup(createUserMessage({
content: [{ type: 'text', text: PROMPT }],
source: { kind: 'user' },
}))
await agentHandle.agent.whenIdle()
const requestHeader = agentHandle.agent.session.requestHeader()
if (requestHeader === undefined) throw new Error('the minimal agent issued no model request')
expect(agentHandle.agent.session.events.some(event => event.type === 'user/message'
@@ -119,10 +136,48 @@ describe('minimal agent preset', () => {
`)
expect(requestHeader.tools?.toSorted((left, right) => left.name.localeCompare(right.name)))
.toEqual(scaffold.ctx.tools.schemas(agentHandle.agent).toSorted((left, right) => left.name.localeCompare(right.name)))
})
it.skipIf(MODE === 'record')('expands the completed persistent Bash call in the Web conversation', async () => {
onTestFailed(() => { if (page !== undefined) void saveFailureShot(page, 'web-minimal-persistent-bash-card') })
browser = await chromium.launch()
page = await newEnglishPage(browser)
tripwire = watchConsole(page)
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
const groupRow = page.locator('[role="treeitem"]').first()
await groupRow.waitFor({ timeout: 15_000 })
await groupRow.click()
const sessionRow = page.locator('[role="treeitem"]').nth(1)
await sessionRow.waitFor({ timeout: 10_000 })
await sessionRow.click()
await page.getByText('MINIMAL_PRESET_REQUEST_OK', { exact: true }).waitFor({ timeout: 15_000 })
const row = page.locator('[data-sample="bash"]').first()
await row.waitFor({ timeout: 15_000 })
await expect.poll(() => row.getAttribute('aria-expanded')).toBe('false')
await row.click()
await expect.poll(() => row.getAttribute('aria-expanded')).toBe('true')
const call = row.locator('xpath=..')
await call.getByText('IN', { exact: true }).waitFor()
await call.getByText('OUT', { exact: true }).waitFor()
await call.getByText('MINIMAL_BASH_CARD_OK', { exact: true }).waitFor()
await call.getByText(/"command": "printf 'MINIMAL_BASH_CARD_OK/).waitFor()
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 60_000)
it('keeps its snapshot inventory closed', async () => {
await assertFixtureInventory(SNAPSHOT_DIR, [
'session.jsonl',
'system-prompt.expected.md',
'tool-schemas.expected.json',
'ui.expected.md',
])
})
})
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-tool/README.md
README.md: 2db7d716dc80fbf40a953b217810fb8674e2e98f
README.zh.md: 79ed5befe751b329984c1320144921339fdf3d3f
README.md: 8e3b4290d2402cf0bcb907f9887ddace35b349a6
README.zh.md: f15899aaedcd52fe7f181c163ab7ab87e9bd20b8
+1 -1
View File
@@ -30,7 +30,7 @@ ctx.slots.inject('tool.call.toolview', () =>
The owner payload is `ToolCallOwnerProps`: `callId`, `toolName`, the frozen `block`, optional `cwd` and `home`, and plain `openFile`/`inspect` callbacks. A Code Dispatch block retains its event's `parentCallId`; the field is absent on a root Session call, so row and Details card models preserve the generic flattened form for descendants without another placement flag. Path summaries relativize to the Session cwd first, then replace a leftover POSIX Host home with `~`; `filePath` and Host open keep the authored filesystem path. The registration receives the normal Session slot runtime share but no React node or Runtime service.
This package currently owns the generic fallback and the built-in shell/pwsh, read, write/edit, running `str_replace_editor` `create`/`str_replace`, grep/glob, web, todo, question, and Code Dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall`/`presentResult` values never enter the Client. `ui-skill` demonstrates a business-owned registration for `skill`.
This package currently owns the generic fallback and the built-in shell/pwsh, read, write/edit, running `str_replace_editor` `create`/`str_replace`, grep/glob, web, todo, question, and Code Dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall`/`presentResult` values never enter the Client. Foreground one-shot shell results use terminal cards. Settled persistent-shell results use the expandable generic input/output card because reset and partial-output diagnostics do not always describe one process exit status; background acknowledgements remain collapsed. `ui-skill` demonstrates a business-owned registration for `skill`.
Card-specific limits and fallback rules remain in the owning [terminal](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.md), [diff](../../../.agents/notes/implemented/feature/2026-07-30-web-diff-card.md), [read](../../../.agents/notes/implemented/feature/2026-07-30-web-read-card-frontend.md), [search](../../../.agents/notes/implemented/feature/2026-07-30-web-search-card.md), and [web](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card-frontend.md) notes.
+1 -1
View File
@@ -30,7 +30,7 @@ ctx.slots.inject('tool.call.toolview', () =>
owner 载荷为 `ToolCallOwnerProps``callId`、`toolName`、冻结的 `block`、可选 `cwd` 与 `home`,以及普通的 `openFile`、`inspect` 回调。Code Dispatch block 保留其事件已有的 `parentCallId`root Session call 没有该字段,因此 row 与 Details card model 无需另一项 placement 标志即可让 descendant 保持 generic 压平形态。路径摘要先相对 Session cwd 缩短,再把剩余的 POSIX Host home 写成 `~``filePath` 与 Host 打开仍使用作者给出的文件系统路径。注册项会收到常规 Session slot runtime share,但不会收到 React node 或 runtime service。
本包当前拥有 generic fallback,以及 shell/pwsh、read、write/edit、running `str_replace_editor` `create``str_replace`、grep/glob、web、todo、question 和 Code Dispatch 的内置展示。结构化卡片直接从第一方原始 event 字段派生;Host `presentCall``presentResult` 值不会进入 Client。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。
本包当前拥有 generic fallback,以及 shell/pwsh、read、write/edit、running `str_replace_editor` `create``str_replace`、grep/glob、web、todo、question 和 Code Dispatch 的内置展示。结构化卡片直接从第一方原始 event 字段派生;Host `presentCall``presentResult` 值不会进入 Client。前台一次性 shell 结果使用 terminal 卡片。已完成的持久 shell 结果使用可展开的 generic 输入/输出卡片,因为 reset 与部分输出诊断不一定描述单个进程的退出状态;后台启动回执保持折叠。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。
各类卡片的上限与 fallback 规则仍由对应的 [terminal](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.zh.md)、[diff](../../../.agents/notes/implemented/feature/2026-07-30-web-diff-card.zh.md)、[read](../../../.agents/notes/implemented/feature/2026-07-30-web-read-card-frontend.zh.md)、[search](../../../.agents/notes/implemented/feature/2026-07-30-web-search-card.zh.md) 和 [web](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card-frontend.zh.md) Agent Note 负责。
@@ -205,6 +205,20 @@ function shellCall(name: string, args: Record<string, unknown>): ShellCall | nul
}
}
/**
* Identify a settled root call from the persistent Bash or PowerShell tool.
* Its result stays on the generic input/output path because the persistent
* shell can report resets and partial output without one process exit status.
* @param block - running or settled Tool block.
* @returns whether the block is a settled persistent-shell call.
*/
export function isSettledPersistentShellCall(block: ToolCallBlock): boolean {
if (!('kind' in block) || block.parentCallId !== undefined) return false
const parsed = parsedToolCall(block)
if (parsed === null) return false
return shellCall(parsed.name, parsed.args)?.persistent === true
}
interface TerminalSendCall {
kind: 'terminal-send'
text: string
@@ -244,7 +258,8 @@ function parseExitStatus(text: string): { output: string; exitCode?: number; sig
* Derive terminal props for supported root shell and terminal-send calls.
* Standard shell results parse their final status marker; persistent shell
* results, background calls, errors, malformed input, or child dispatches use
* the generic path.
* the generic path. {@link isSettledPersistentShellCall} lets that generic
* persistent result remain expandable without inventing one process status.
* @param block - running or settled Tool block.
* @param sessionCwd - session workspace root used to resolve workdir.
* @returns locale-neutral terminal-card data, or null for the generic path.
@@ -7,7 +7,11 @@ import {
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
import type { ToolCallViewProps } from '../../contract/slots.ts'
import {
localizeTerminalCardModel, terminalBlockLabels, terminalCardModel, terminalFailed,
isSettledPersistentShellCall,
localizeTerminalCardModel,
terminalBlockLabels,
terminalCardModel,
terminalFailed,
} from '../models/terminal-card-model.ts'
import { toolRowModel, type ToolRowState } from '../models/tool-call-model.ts'
import { CONVERSATION_NS as NS } from '../../locale.ts'
@@ -49,13 +53,13 @@ export function BashRow({ toolName, block, sessionId, useSessions, inspect, t }:
: model.state
const status = stateStatus(state, t)
const [expanded, setExpanded] = useState(false)
// Execution failures (for example cancellation before the process reports a
// terminal result) use the generic body. Keep their recorded args and
// full error reachable instead of collapsing the row to the first line.
const genericError = terminal === null
&& model.state === 'error'
// Execution failures and persistent-shell results have no terminal card.
// Keep their recorded args and complete output reachable through the generic
// body; background acknowledgements and malformed calls remain collapsed.
const genericBody = terminal === null
&& (model.state === 'error' || isSettledPersistentShellCall(block))
&& (model.body !== null || model.output !== null)
const expandable = terminal !== null || genericError
const expandable = terminal !== null || genericBody
const open = expanded && expandable
const failureLine = model.state === 'error' ? model.errorSummary : null
const toggleExpand = () => {
@@ -123,7 +127,7 @@ export function BashRow({ toolName, block, sessionId, useSessions, inspect, t }:
{model.output !== null && (
<div className={css.ioSection}>
<span className={css.ioLabel}>{t('row.output')}</span>
<span className={css.ioText} data-error>
<span className={css.ioText} data-error={state === 'error' || undefined}>
{model.output}
</span>
</div>
@@ -442,11 +442,24 @@ describe('BashRow terminal card', () => {
expect(view.queryByText('List files')).toBeNull()
})
it('keeps the command summary for a persistent shell with no description', () => {
it('expands a settled persistent shell through the generic input/output card', () => {
const view = render(<BashRow {...rowProps(settled({
call: { name: 'bash', argsRaw: JSON.stringify({ command: 'ls -la' }) },
}))} />)
const row = view.container.querySelector('[data-sample="bash"]')!
expect(view.getByText('ls -la')).toBeTruthy()
expect(row.getAttribute('role')).toBe('button')
expect(row.getAttribute('aria-expanded')).toBe('false')
fireEvent.click(row)
expect(row.getAttribute('aria-expanded')).toBe('true')
expect(view.getByText('输入')).toBeTruthy()
expect(view.getByText('输出')).toBeTruthy()
expect(view.getByText(/"command": "ls -la"/)).toBeTruthy()
expect(view.container.querySelector('[class*="_ioText_"][data-error]')).toBeNull()
expect(view.container.querySelectorAll('[class*="_ioText_"]')[1]?.textContent)
.toBe('a.ts b.ts\nc.ts d.ts\n')
})
it('a non-terminal bash call (background start) renders the summary row alone', () => {
+19 -9
View File
@@ -1,20 +1,30 @@
{"type":"session","version":0,"id":"{{session:1}}","createdAt":1787520042622,"cwd":"{{cwd}}","agentPreset":"minimal"}
{"type":"session","version":0,"id":"{{session:1}}","createdAt":1787660564247,"cwd":"{{cwd}}","agentPreset":"minimal"}
{"type":"permission/preset","data":{"preset":"workspace-write"}}
{"type":"sandbox/mode","data":{"mode":"workspace-write"}}
{"type":"approval/policy","data":{"policy":"ask"}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply exactly MINIMAL_PRESET_REQUEST_OK and stop."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use the bash tool to run exactly: printf 'MINIMAL_BASH_CARD_OK\\n'. Then reply exactly MINIMAL_PRESET_REQUEST_OK and stop."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}}
{"type":"turn/start","data":{"turn":1}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply exactly MINIMAL_PRESET_REQUEST_OK and stop."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"}
{"type":"session/title","data":{"title":"Reply exactly MINIMAL_PRESET_REQUEST_OK","messageSeqs":[7],"source":{"kind":"fallback"}}}
{"type":"user/message","data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: printf 'MINIMAL_BASH_CARD_OK\\n'. Then reply exactly MINIMAL_PRESET_REQUEST_OK and stop."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"}
{"type":"session/title","data":{"title":"Use the bash tool to","messageSeqs":[7],"source":{"kind":"fallback"}}}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":128000}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"MINIMAL_PRESET_REQUEST_OK"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"MINIMAL_PRESET_REQUEST_OK"}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"minimal-bash-card","name":"bash","argumentsDelta":"{\"command\":\"printf 'MINIMAL_BASH_CARD_OK\\\\n'\"}"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"minimal-bash-card","name":"bash","arguments":"{\"command\":\"printf 'MINIMAL_BASH_CARD_OK\\\\n'\"}"}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"MINIMAL_PRESET_REQUEST_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:2}}"},"usage":{"inputTokens":10,"outputTokens":4}},"sourceEventSeqs":[11,12,13,14,15],"surfaceOp":"append"}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"minimal-bash-card","name":"bash","arguments":"{\"command\":\"printf 'MINIMAL_BASH_CARD_OK\\\\n'\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:2}}"},"usage":{"inputTokens":10,"outputTokens":4}},"sourceEventSeqs":[11,12,13,14,15],"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"minimal-bash-card","name":"bash","arguments":"{\"command\":\"printf 'MINIMAL_BASH_CARD_OK\\\\n'\"}"}}
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"minimal-bash-card"},"content":[{"type":"tool-result","toolCallId":"minimal-bash-card","content":[{"type":"text","text":"MINIMAL_BASH_CARD_OK"}],"isError":false}],"role":"user","id":"{{message:3}}"}},"sourceEventSeqs":[17],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":1}}
{"type":"step/start","data":{"turn":1,"step":2}}
{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"MINIMAL_PRESET_REQUEST_OK"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"MINIMAL_PRESET_REQUEST_OK"}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"MINIMAL_PRESET_REQUEST_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":4}},"sourceEventSeqs":[21,22,23,24,25],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":2}}
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
@@ -0,0 +1,43 @@
- banner:
- navigation "Session hierarchy":
- button "Use the bash tool to" [disabled]
- img
- text: Minimal mode
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
- button "System prompt":
- img
- img
- text: System prompt
- text: "Use the bash tool to run exactly: printf 'MINIMAL_BASH_CARD_OK\\n'. Then reply exactly MINIMAL_PRESET_REQUEST_OK and stop. {{clock}}"
- button "Copy":
- img
- button "Bash printf 'MINIMAL_BASH_CARD_OK\\n'" [expanded]:
- img
- text: Bash printf 'MINIMAL_BASH_CARD_OK\n'
- text: "IN { \"command\": \"printf 'MINIMAL_BASH_CARD_OK\\\\n'\" } OUT MINIMAL_BASH_CARD_OK"
- button "Inspect"
- paragraph: MINIMAL_PRESET_REQUEST_OK
- button "Copy":
- img
- button "Good response":
- img
- button "Bad response":
- img
- button "Branch into a new conversation":
- img
- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img
- button "0% of context used"
- button "Send message" [disabled]
- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 0% Input 20 tok · Output 8 tok