mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
perf: bound whitespace normalization for tool previews (#20332)
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import fs from 'node:fs'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import assert from 'node:assert/strict'
|
||||
import { build } from 'esbuild'
|
||||
const file = 'src/shared/native-chat-tool-summary.ts'
|
||||
const baseline = process.argv[2] ?? '20ab9950654'
|
||||
const beforeSource = execFileSync('git', ['show', `${baseline}:${file}`], {
|
||||
encoding: 'utf8',
|
||||
windowsHide: true
|
||||
})
|
||||
const afterSource = fs.readFileSync(file, 'utf8')
|
||||
async function load(contents) {
|
||||
const { outputFiles } = await build({
|
||||
stdin: { contents, loader: 'ts', resolveDir: dirname(resolve(file)) },
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false
|
||||
})
|
||||
return import(
|
||||
`data:text/javascript;base64,${Buffer.from(outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
}
|
||||
const before = await load(beforeSource),
|
||||
after = await load(afterSource)
|
||||
const display = (m, input) => {
|
||||
const d = m.createToolInputDisplay(input)
|
||||
return { ...d, formatDetail: d.formatDetail() }
|
||||
}
|
||||
let seed = 8121
|
||||
const random = (max) => {
|
||||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
||||
return Math.floor((seed / 4294967296) * max)
|
||||
}
|
||||
const pieces = [
|
||||
'a',
|
||||
'b',
|
||||
'…',
|
||||
'😀',
|
||||
'\ud800',
|
||||
'\udc00',
|
||||
'\0',
|
||||
'\t',
|
||||
'\r',
|
||||
'\n',
|
||||
' ',
|
||||
'\v',
|
||||
'\f',
|
||||
'\u00a0',
|
||||
'\u1680',
|
||||
'\u2000',
|
||||
'\u200a',
|
||||
'\u2028',
|
||||
'\u2029',
|
||||
'\u202f',
|
||||
'\u205f',
|
||||
'\u3000',
|
||||
'\ufeff',
|
||||
'\u0085',
|
||||
'\u200b'
|
||||
]
|
||||
for (let i = 0; i < 3000; i++) {
|
||||
const input = Array.from({ length: random(500) }, () => pieces[random(pieces.length)]).join('')
|
||||
assert.equal(after.summarizeToolInput(input), before.summarizeToolInput(input))
|
||||
assert.deepEqual(display(after, input), display(before, input))
|
||||
}
|
||||
for (const input of [
|
||||
`${'a'.repeat(79)}…`,
|
||||
'a'.repeat(80) + ' '.repeat(500),
|
||||
' '.repeat(100000),
|
||||
`${'\n'.repeat(100000)}x`,
|
||||
{ command: 'a '.repeat(50000) },
|
||||
{ file_path: 'a '.repeat(500) },
|
||||
{ x: 'a '.repeat(500) },
|
||||
JSON.stringify({ command: 'a '.repeat(50000) })
|
||||
]) {
|
||||
assert.deepEqual(display(after, input), display(before, input))
|
||||
}
|
||||
for (const [shape, input] of [
|
||||
['tiny', 'ls -la'],
|
||||
['100KB', 'a b\n\t'.repeat(15000)],
|
||||
['1MB', 'a b\n\t'.repeat(150000)],
|
||||
['all-space', ' '.repeat(1000000)],
|
||||
['leading', `${' '.repeat(1000000)}x`],
|
||||
['trailing', `x${' '.repeat(1000000)}`],
|
||||
['long-word', 'x'.repeat(1000000)]
|
||||
]) {
|
||||
const samples = { before: [], after: [] }
|
||||
for (let i = 0; i < 20; i++) {
|
||||
before.createToolInputDisplay(input)
|
||||
after.createToolInputDisplay(input)
|
||||
}
|
||||
for (let r = 0; r < 8; r++) {
|
||||
for (const [label, m] of r % 2
|
||||
? [
|
||||
['after', after],
|
||||
['before', before]
|
||||
]
|
||||
: [
|
||||
['before', before],
|
||||
['after', after]
|
||||
]) {
|
||||
global.gc()
|
||||
const start = process.cpuUsage()
|
||||
for (let i = 0; i < 20; i++) {
|
||||
m.createToolInputDisplay(input)
|
||||
}
|
||||
const cpu = process.cpuUsage(start)
|
||||
samples[label].push((cpu.user + cpu.system) / 20000)
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify({ shape, samples }))
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export const MAX_TOOL_PREVIEW_LENGTH = 80
|
||||
const SHORT_INPUT_LENGTH = 160
|
||||
|
||||
// One extra normalized code unit proves truncation and inequality with an 80-unit label.
|
||||
export function collapsedToolInputPrefix(input: string): string {
|
||||
if (input.length <= SHORT_INPUT_LENGTH) {
|
||||
return input.replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
let collapsed = ''
|
||||
let pendingSpace = false
|
||||
const whitespace = /\s+/y
|
||||
for (let index = 0; index < input.length;) {
|
||||
whitespace.lastIndex = index
|
||||
if (whitespace.test(input)) {
|
||||
index = whitespace.lastIndex
|
||||
pendingSpace = collapsed.length > 0
|
||||
continue
|
||||
}
|
||||
if (pendingSpace) {
|
||||
collapsed += ' '
|
||||
pendingSpace = false
|
||||
}
|
||||
collapsed += input[index++]
|
||||
if (collapsed.length > MAX_TOOL_PREVIEW_LENGTH) {
|
||||
return collapsed
|
||||
}
|
||||
}
|
||||
return collapsed
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createToolInputDisplay, summarizeToolInput } from './native-chat-tool-summary'
|
||||
|
||||
const originalSummary = (input: string): string => {
|
||||
const collapsed = input.replace(/\s+/g, ' ').trim()
|
||||
return collapsed.length <= 80 ? collapsed : `${collapsed.slice(0, 79)}…`
|
||||
}
|
||||
|
||||
describe('bounded tool preview whitespace normalization', () => {
|
||||
it('avoids whole-input replacement for long prose previews', () => {
|
||||
const input = 'a b\n\t'.repeat(20_000)
|
||||
const spy = vi.spyOn(String.prototype, 'replace')
|
||||
let fullReplacements: number
|
||||
let display: ReturnType<typeof createToolInputDisplay>
|
||||
try {
|
||||
display = createToolInputDisplay(input)
|
||||
fullReplacements = spy.mock.instances.filter((receiver) => String(receiver) === input).length
|
||||
} finally {
|
||||
spy.mockRestore()
|
||||
}
|
||||
expect(display.label).toBe(originalSummary(input))
|
||||
expect(display.hasDetail).toBe(true)
|
||||
expect(fullReplacements).toBe(0)
|
||||
})
|
||||
|
||||
it('preserves exact labels and detail flags across whitespace and UTF-16 boundaries', () => {
|
||||
const whitespace = '\t\n\v\f\r \u00a0\u1680\u2000\u200a\u2028\u2029\u202f\u205f\u3000\ufeff'
|
||||
const inputs = [
|
||||
'',
|
||||
whitespace.repeat(50),
|
||||
`${'x'.repeat(79)}…`,
|
||||
'😀'.repeat(41),
|
||||
'\ud800'.repeat(82),
|
||||
'\u0085\u200b'.repeat(50)
|
||||
]
|
||||
for (const length of [0, 1, 78, 79, 80, 81, 159, 160, 161]) {
|
||||
inputs.push(`${whitespace}${'x'.repeat(length)}${whitespace.repeat(30)}`)
|
||||
inputs.push(`${'x'.repeat(length)}${whitespace}tail`)
|
||||
}
|
||||
for (const input of inputs) {
|
||||
const expected = originalSummary(input)
|
||||
const display = createToolInputDisplay(input)
|
||||
expect(summarizeToolInput(input)).toBe(expected)
|
||||
expect(display.label).toBe(expected)
|
||||
expect(display.hasDetail).toBe(input.replace(/\s+/g, ' ').trim() !== expected)
|
||||
expect(display.formatDetail()).toBe(input.length > 4000 ? `${input.slice(0, 4000)}…` : input)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,10 @@
|
||||
import {
|
||||
collapsedToolInputPrefix,
|
||||
MAX_TOOL_PREVIEW_LENGTH
|
||||
} from './native-chat-tool-preview-prefix'
|
||||
import type { NativeChatMcpIdentity } from './native-chat-tool-identity'
|
||||
import { isToolCallBlock, type NativeChatBlock } from './native-chat-types'
|
||||
|
||||
const MAX_PREVIEW_LENGTH = 80
|
||||
const MAX_PREVIEW_STRING_INPUT = 160
|
||||
const MAX_PREVIEW_COLLECTION_ITEMS = 8
|
||||
const MAX_PREVIEW_DEPTH = 2
|
||||
@@ -34,10 +37,10 @@ export type ToolInputDisplay = {
|
||||
}
|
||||
|
||||
export function summarizeToolInput(input: unknown): string {
|
||||
const collapsed = toRawPreview(input).replace(/\s+/g, ' ').trim()
|
||||
return collapsed.length <= MAX_PREVIEW_LENGTH
|
||||
const collapsed = collapsedToolInputPrefix(toRawPreview(input))
|
||||
return collapsed.length <= MAX_TOOL_PREVIEW_LENGTH
|
||||
? collapsed
|
||||
: `${collapsed.slice(0, MAX_PREVIEW_LENGTH - 1)}…`
|
||||
: `${collapsed.slice(0, MAX_TOOL_PREVIEW_LENGTH - 1)}…`
|
||||
}
|
||||
|
||||
/** Build the renderer-independent row model from one normalization pass. Detail
|
||||
@@ -124,7 +127,7 @@ function normalizedToolInputHasDetail(input: unknown, label: string): boolean {
|
||||
if (isStructuredNormalizedToolInput(input)) {
|
||||
return true
|
||||
}
|
||||
return typeof input === 'string' && input.replace(/\s+/g, ' ').trim() !== label
|
||||
return typeof input === 'string' && collapsedToolInputPrefix(input) !== label
|
||||
}
|
||||
|
||||
export function toolFilePath(input: unknown): string | null {
|
||||
@@ -241,10 +244,10 @@ function firstPrimaryToolArg(
|
||||
* absolute path drops the filename, the one part that tells two rows apart. */
|
||||
function summarizeToolPath(path: string): string {
|
||||
const collapsed = path.replace(/\s+/g, ' ').trim()
|
||||
if (collapsed.length <= MAX_PREVIEW_LENGTH) {
|
||||
if (collapsed.length <= MAX_TOOL_PREVIEW_LENGTH) {
|
||||
return collapsed
|
||||
}
|
||||
const tail = collapsed.slice(collapsed.length - (MAX_PREVIEW_LENGTH - 1))
|
||||
const tail = collapsed.slice(collapsed.length - (MAX_TOOL_PREVIEW_LENGTH - 1))
|
||||
// Start at a segment boundary so the label doesn't open mid-name.
|
||||
const boundary = tail.search(/[\\/]/)
|
||||
return `…${boundary > 0 ? tail.slice(boundary) : tail}`
|
||||
|
||||
Reference in New Issue
Block a user