mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-17 20:31:47 +02:00
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:
+16
-6
@@ -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}
|
||||
|
||||
+13
-6
@@ -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') {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user