ui : trim whole-blob scans in tool block headers

Tool block headers parsed their entire blobs at mount, even collapsed,
and most tool results and args are large plain text or embedded file
content: skip JSON.parse unless the blob starts with a JSON container,
prefilter search-result extraction with a Title:/URL: substring check,
and match the end-anchored exit-code marker against only the tail of exec
outputs.

Assisted-by: pi:zai-org/GLM-5.3
This commit is contained in:
Aleksander Grygier
2026-09-06 01:47:54 +02:00
parent 27091debfc
commit fb308031a2
7 changed files with 65 additions and 18 deletions
@@ -13,7 +13,12 @@
import ChatMessageToolCallBlockWriteFile from './ChatMessageToolCallBlockWriteFile.svelte';
import { BuiltInTool } from '$lib/enums';
import type { AgenticSection, DatabaseMessageExtra } from '$lib/types';
import { extractSearchQuery, extractSearchResults, isWebSearchToolName } from '$lib/utils';
import {
extractSearchQuery,
extractSearchResults,
isWebSearchToolName,
looksLikeSearchResult
} from '$lib/utils';
interface Props {
section: AgenticSection;
@@ -26,11 +31,16 @@
let { attachments, isExecuting, isStreaming, onToggle, open, section }: Props = $props();
const searchResults = $derived(extractSearchResults(section.toolResult));
const searchQuery = $derived(extractSearchQuery(section.toolArgs));
const isSearchCall = $derived(
searchResults.length > 0 || (searchQuery.length > 0 && isWebSearchToolName(section.toolName))
);
// Runs for every tool block on mount, before the body renders: the cheap
// content prefilter and the tool-name allow-list come first so blobs from
// exec/file tools are never line-split or JSON-parsed here
const isSearchCall = $derived.by(() => {
if (looksLikeSearchResult(section.toolResult)) {
return extractSearchResults(section.toolResult).length > 0;
}
return isWebSearchToolName(section.toolName) && extractSearchQuery(section.toolArgs).length > 0;
});
</script>
{#if isSearchCall}
@@ -38,14 +38,21 @@ export function parseRunJavascriptMeta(section: AgenticSection): RunJavascriptMe
// do we scan raw lines for the `Error:` prefix.
let parsedObject: Record<string, unknown> | null = null;
try {
const parsed: unknown = JSON.parse(toolResultString);
// Successful sandbox output is a JSON array, errors are objects; plain
// text (huge console logs) fails the parse below anyway, so only try
// when the blob starts with a JSON container
const trimmedResult = toolResultString.trimStart();
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
parsedObject = parsed as Record<string, unknown>;
if (trimmedResult[0] === '{' || trimmedResult[0] === '[') {
try {
const parsed: unknown = JSON.parse(trimmedResult);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
parsedObject = parsed as Record<string, unknown>;
}
} catch {
parsedObject = null;
}
} catch {
parsedObject = null;
}
if (typeof parsedObject?.error === 'string') {
+2 -1
View File
@@ -285,7 +285,8 @@ export {
extractSearchResults,
extractSearchQuery,
faviconForUrl,
isWebSearchToolName
isWebSearchToolName,
looksLikeSearchResult
} from './search-results';
// Cache utilities
@@ -3,8 +3,14 @@ export function parseExecShellCommandError(
): string | undefined {
if (!toolResultString) return undefined;
// Exec results are usually large plain-text stdout; only a JSON object
// root can carry an error field, so skip the parse otherwise
const trimmed = toolResultString.trimStart();
if (trimmed[0] !== '{') return undefined;
try {
const parsed: unknown = JSON.parse(toolResultString);
const parsed: unknown = JSON.parse(trimmed);
if (
parsed &&
@@ -15,15 +15,18 @@ export interface ExecShellExitStatus {
}
// Anchor to the absolute end so intermediate "[exit code: N]" string content
// (e.g. a shell echo) doesn't false-positive.
// (e.g. a shell echo) doesn't false-positive. The marker is at most ~50 chars
// with the timed-out suffix, so matching a tail slice keeps the cost constant
// for megabyte exec outputs instead of scanning the whole blob.
const EXIT_CODE_TAIL_REGEX = /\[exit code: (-?\d+)\](?: \[exit due to timed out\])?\s*$/;
const EXIT_CODE_TAIL_SCAN = 128;
export function parseExecShellCommandExitStatus(
toolResultString: string | undefined
): ExecShellExitStatus | undefined {
if (!toolResultString) return undefined;
const match = toolResultString.match(EXIT_CODE_TAIL_REGEX);
const match = toolResultString.slice(-EXIT_CODE_TAIL_SCAN).match(EXIT_CODE_TAIL_REGEX);
if (!match) return undefined;
+15 -1
View File
@@ -156,6 +156,20 @@ function parseChunk(chunk: string): SearchResult | null {
return result;
}
const EMPTY_SEARCH_RESULTS: SearchResult[] = [];
/**
* Cheap prefilter for the wire format: a parseable result needs both a
* `Title:` and a `URL:` field line, so a blob missing either substring can
* never yield a result. Two substring scans cost far less than the
* line-split parse for the megabyte tool results exec and file tools emit.
*/
export function looksLikeSearchResult(text: string | undefined | null): boolean {
if (!text) return false;
return text.includes('Title:') && text.includes('URL:');
}
/** Bounded cache for extractSearchResults results. */
const SEARCH_RESULTS_CACHE_MAX_SIZE = 32;
const searchResultsCache = new Map<string, SearchResult[]>();
@@ -168,7 +182,7 @@ const searchResultsCache = new Map<string, SearchResult[]>();
* tool result strings.
*/
export function extractSearchResults(text: string | undefined | null): SearchResult[] {
if (!text) return [];
if (!text || !looksLikeSearchResult(text)) return EMPTY_SEARCH_RESULTS;
const cached = searchResultsCache.get(text);
+7 -1
View File
@@ -16,8 +16,14 @@ export function tryParseToolResultObject(
): Record<string, unknown> | null {
if (!toolResultString) return null;
// Tool results are usually large plain text (file contents, stdout); only
// a JSON object root can carry fields, so skip the parse otherwise
const trimmed = toolResultString.trimStart();
if (trimmed[0] !== '{') return null;
try {
const parsed: unknown = JSON.parse(toolResultString);
const parsed: unknown = JSON.parse(trimmed);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return parsed as Record<string, unknown>;