Files
deerflow-code/frontend-web/src/core/skill-display/runtime.ts
T
2026-06-28 00:23:01 +08:00

1255 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Message } from "@langchain/langgraph-sdk";
import { normalizeSearchResults } from "@/core/messages/search-results";
import type { Skill, SkillDisplayConfig } from "@/core/skills/type";
import {
normalizeDisplaySources,
type DisplaySourceItem,
} from "./display-adapter";
export type RuntimeDisplaySource = DisplaySourceItem & {
index: number;
skillName: string;
mode: SkillDisplayConfig["mode"];
tableColumns?: SkillDisplayConfig["tableColumns"];
};
type RuntimeDisplaySourceDraft = Omit<RuntimeDisplaySource, "index">;
export type SkillDisplayMap = Map<string, SkillDisplayConfig>;
type RuntimeToolCall = {
id?: string;
name: string;
args?: Record<string, unknown>;
};
export function buildSkillDisplayMap(skills: Skill[]): SkillDisplayMap {
const map = new Map<string, SkillDisplayConfig>();
for (const skill of skills) {
const display = skill.display;
if (!display?.enabled || display.mode === "none") {
continue;
}
map.set(skill.name, display);
}
return map;
}
export function findDisplayConfigForTool(
toolName: string,
displays: SkillDisplayMap,
toolArgs?: Record<string, unknown>,
): { skillName: string; display: SkillDisplayConfig } | null {
const exact = displays.get(toolName);
if (exact) {
return { skillName: toolName, display: exact };
}
const normalizedTool = normalizeName(toolName);
for (const [skillName, display] of displays) {
const normalizedSkill = normalizeName(skillName);
if (
normalizedTool === normalizedSkill ||
normalizedTool.endsWith(normalizedSkill) ||
normalizedTool.includes(normalizedSkill)
) {
return { skillName, display };
}
}
// bash/python skill invocations don't carry the skill name in the tool name —
// the path lives inside the command/args (e.g. `python3 skills/custom/demo/scripts/run.py`).
// Scan stringy args for the `skills/{public,custom}/<name>/` shape so display
// configs still resolve. Mirrors the backend `_SKILL_PATH_RE` in
// tool_metrics_middleware.py.
if (toolArgs && canInferSkillFromToolArgs(toolName)) {
const skillFromArgs = extractSkillNameFromArgs(toolArgs);
if (skillFromArgs) {
const direct = displays.get(skillFromArgs);
if (direct) {
return { skillName: skillFromArgs, display: direct };
}
}
}
return null;
}
const SKILL_PATH_RE = /[\\/]skills[\\/](?:public|custom)[\\/]([^\\/]+)(?:[\\/]|$)/;
const TOOL_ARG_SKILL_MATCH_EXCLUDES = new Set([
"read_file",
"readfile",
"ls",
"write_file",
"writefile",
"str_replace",
"strreplace",
"web_fetch",
"webfetch",
]);
function canInferSkillFromToolArgs(toolName: string): boolean {
const normalized = normalizeName(toolName);
if (
TOOL_ARG_SKILL_MATCH_EXCLUDES.has(toolName) ||
TOOL_ARG_SKILL_MATCH_EXCLUDES.has(normalized)
) {
return false;
}
return (
normalized.includes("bash") ||
normalized.includes("shell") ||
normalized.includes("python") ||
normalized.includes("execute") ||
normalized.includes("terminal")
);
}
function extractSkillNameFromArgs(args: Record<string, unknown>): string | undefined {
for (const value of Object.values(args)) {
if (typeof value !== "string") continue;
const match = SKILL_PATH_RE.exec(value);
if (match) {
return match[1];
}
}
return undefined;
}
export function collectRuntimeDisplaySources(
messages: Message[],
displays: SkillDisplayMap,
options: {
afterMessageId?: string;
beforeMessageId?: string;
includeBeforeMessage?: boolean;
includeModes?: SkillDisplayConfig["mode"][];
} = {},
): RuntimeDisplaySource[] {
const start =
options.afterMessageId === undefined
? 0
: Math.max(
messages.findIndex((message) => message.id === options.afterMessageId) + 1,
0,
);
const limit =
options.beforeMessageId === undefined
? messages.length
: Math.max(
messages.findIndex((message) => message.id === options.beforeMessageId) +
(options.includeBeforeMessage ? 1 : 0),
0,
);
const allowedModes = options.includeModes
? new Set(options.includeModes)
: null;
const sources: RuntimeDisplaySource[] = [];
let citationIndex = 0;
const seenCitationKeys = new Map<string, RuntimeDisplaySource>();
const consumedToolMessageIds = new Set<string>();
const consumedToolMessages = new Set<Message>();
for (let index = start; index < limit; index++) {
const message = messages[index];
if (!message || message.type !== "ai") {
continue;
}
for (const toolCall of extractRuntimeToolCalls(message)) {
const toolMatch = findDisplayConfigForTool(
toolCall.name,
displays,
toolCall.args,
);
const toolMessage = findToolResultMessage(
toolCall,
messages,
index,
consumedToolMessageIds,
consumedToolMessages,
);
if (!toolMessage) {
continue;
}
const result = parseToolMessagePayload(toolMessage);
if (result === undefined) {
continue;
}
if (toolMessage.id) {
consumedToolMessageIds.add(toolMessage.id);
}
consumedToolMessages.add(toolMessage);
if (isDisplaySamplePayload(result)) {
continue;
}
const match = toolMatch;
const effectiveMode = match?.display.mode ?? "citation";
if (allowedModes && !allowedModes.has(effectiveMode)) {
continue;
}
const normalized = match
? normalizeDisplaySources(result, match.display)
: shouldUseDefaultCitationMode(
toolCall.name,
result,
toolCall.args,
)
? normalizeDefaultDisplaySources(result)
: [];
appendDisplaySources(
sources,
normalized.map((item) => ({
...item,
skillName: match?.skillName ?? toolCall.name,
mode: item.displayMode ?? effectiveMode,
tableColumns: item.tableColumns ?? match?.display.tableColumns,
})),
seenCitationKeys,
() => ++citationIndex,
);
}
}
for (let index = start; index < limit; index++) {
const message = messages[index];
if (
!message ||
message.type !== "tool" ||
consumedToolMessages.has(message) ||
consumedToolMessageIds.has(message.id ?? "")
) {
continue;
}
const result = parseToolMessagePayload(message);
if (result === undefined || isDisplaySamplePayload(result)) {
continue;
}
const toolName = extractToolMessageName(message);
const match = findDisplayConfigForPayload(toolName, result, displays);
const effectiveMode = match?.display.mode ?? "citation";
if (allowedModes && !allowedModes.has(effectiveMode)) {
continue;
}
const normalized = match
? normalizeDisplaySources(result, match.display)
: shouldUseDefaultCitationMode(toolName, result) ||
normalizeDefaultDisplaySources(result).length > 0
? normalizeDefaultDisplaySources(result)
: [];
appendDisplaySources(
sources,
normalized.map((item) => ({
...item,
skillName: match?.skillName ?? toolName,
mode: item.displayMode ?? effectiveMode,
tableColumns: item.tableColumns ?? match?.display.tableColumns,
})),
seenCitationKeys,
() => ++citationIndex,
);
}
return sources;
}
function appendDisplaySources(
target: RuntimeDisplaySource[],
drafts: RuntimeDisplaySourceDraft[],
seenCitationKeys: Map<string, RuntimeDisplaySource>,
nextCitationIndex: () => number,
) {
for (const draft of drafts) {
if (draft.mode === "citation") {
const source = {
...draft,
index: nextCitationIndex(),
};
const key = citationSourceKey(draft);
if (key) {
seenCitationKeys.set(`${key}:${source.index}`, source);
}
target.push(source);
continue;
}
target.push({
...draft,
index: nextCitationIndex(),
});
}
}
export function reindexRuntimeDisplaySources(
sources: RuntimeDisplaySource[],
globalSources: RuntimeDisplaySource[],
): RuntimeDisplaySource[] {
if (sources.length === 0 || globalSources.length === 0) {
return sources;
}
const globalByKey = new Map(
globalSources.map((source) => [citationSourceKey(source), source]),
);
return sources.map((source) => {
const global = globalByKey.get(citationSourceKey(source));
return global
? {
...source,
index: global.index,
}
: source;
});
}
function citationSourceKey(source: RuntimeDisplaySourceDraft): string {
const raw = source.raw ?? {};
const id =
source.id ||
source.url ||
stringifyMaybe(raw.recUuid) ||
stringifyMaybe(raw.uuid) ||
stringifyMaybe(raw.docId) ||
stringifyMaybe(raw.id);
if (id) {
return normalizeCitationKey(String(id));
}
const fallback = [
source.skillName,
source.title,
source.source,
source.time,
source.content,
source.snippet,
]
.filter(Boolean)
.join("|");
return normalizeCitationKey(fallback) || normalizeCitationKey(JSON.stringify(raw).slice(0, 300));
}
function extractRuntimeToolCalls(message: Message): RuntimeToolCall[] {
const calls: RuntimeToolCall[] = [];
const direct = (message as { tool_calls?: unknown }).tool_calls;
if (Array.isArray(direct)) {
for (const raw of direct) {
const call = normalizeRuntimeToolCall(raw);
if (call) {
calls.push(call);
}
}
}
const kwargs = (message as { additional_kwargs?: Record<string, unknown> })
.additional_kwargs;
const extra = kwargs?.tool_calls;
if (Array.isArray(extra)) {
for (const raw of extra) {
const call = normalizeRuntimeToolCall(raw);
if (call && !calls.some((item) => item.id && item.id === call.id)) {
calls.push(call);
}
}
}
const chunks = (message as { tool_call_chunks?: unknown }).tool_call_chunks;
if (Array.isArray(chunks)) {
for (const raw of chunks) {
const call = normalizeRuntimeToolCall(raw);
if (call && !calls.some((item) => item.id && item.id === call.id)) {
calls.push(call);
}
}
}
return calls;
}
function normalizeRuntimeToolCall(raw: unknown): RuntimeToolCall | null {
if (!isRecord(raw)) {
return null;
}
const fn = isRecord(raw.function) ? raw.function : null;
const name =
stringifyMaybe(raw.name) ??
stringifyMaybe(fn?.name) ??
stringifyMaybe(raw.tool_name);
if (!name) {
return null;
}
return {
id:
stringifyMaybe(raw.id) ??
stringifyMaybe(raw.tool_call_id) ??
stringifyMaybe(raw.call_id),
name,
args: normalizeToolArgs(raw.args ?? raw.arguments ?? fn?.arguments),
};
}
function normalizeToolArgs(value: unknown): Record<string, unknown> | undefined {
if (isRecord(value)) {
return value;
}
if (typeof value === "string") {
try {
const parsed = JSON.parse(value);
return isRecord(parsed) ? parsed : undefined;
} catch {
return { command: value };
}
}
return undefined;
}
export function findDisplayConfigForPayload(
toolName: string,
_payload: unknown,
displays: SkillDisplayMap,
): { skillName: string; display: SkillDisplayConfig } | null {
return findDisplayConfigForTool(toolName, displays);
}
export function isUnconfiguredSkillInvocation(
toolName: string,
displays: SkillDisplayMap | undefined,
toolArgs?: Record<string, unknown>,
): boolean {
if (!toolArgs || !canInferSkillFromToolArgs(toolName)) {
return false;
}
const skillName = extractSkillNameFromArgs(toolArgs);
return !!skillName && !displays?.has(skillName);
}
export function isDisplaySamplePayload(payload: unknown): boolean {
if (!isRecord(payload)) {
return false;
}
if (payload.__displaySample === true || payload.displaySample === true) {
return true;
}
if (payload.query === "sample query") {
return true;
}
const items =
getArrayByPath(payload, "results") ??
getArrayByPath(payload, "data") ??
getArrayByPath(payload, "items");
const first = items?.find((item): item is Record<string, unknown> => isRecord(item));
if (!first) {
return false;
}
const text = Object.values(first)
.filter((value) => typeof value === "string")
.join("\n")
.toLowerCase();
return (
/^sample-/i.test(String(first.id ?? "")) ||
text.includes("mock result") ||
text.includes("knowledge result one")
);
}
function getArrayByPath(payload: unknown, path: string): unknown[] | null {
const value = path.split(".").reduce<unknown>((acc, key) => {
if (acc && typeof acc === "object") {
return (acc as Record<string, unknown>)[key];
}
return undefined;
}, payload);
return Array.isArray(value) ? value : null;
}
export function shouldUseDefaultCitationMode(
toolName: string,
payload: unknown,
toolArgs?: Record<string, unknown>,
): boolean {
const normalizedName = normalizeName(toolName);
const isSearchLikeTool =
normalizedName.includes("search") ||
normalizedName.includes("retriev") ||
normalizedName.includes("knowledge") ||
normalizedName.includes("query");
const isSkillInvocation = !!(
toolArgs &&
canInferSkillFromToolArgs(toolName) &&
extractSkillNameFromArgs(toolArgs)
);
if (!isSearchLikeTool && !isSkillInvocation) {
return false;
}
return normalizeDefaultDisplaySources(payload).length > 0;
}
export function normalizeDefaultDisplaySources(payload: unknown): DisplaySourceItem[] {
const normalized = normalizeSearchResults(
typeof payload === "string" || isRecord(payload)
? (payload as string | Record<string, unknown>)
: undefined,
);
return normalized.map((item, index) => ({
id:
item.recUuid ??
item.uuid ??
item.docId ??
item.id ??
item.url,
title: item.title ?? item.source ?? `结果 ${index + 1}`,
type: "result",
snippet: item.snippet ?? item.content ?? "",
content: item.content ?? item.snippet,
source: item.source,
author: item.author,
time: item.published_date ?? item.publishedDate ?? item.date,
url: item.url,
raw: item as unknown as Record<string, unknown>,
}));
}
export function applyInlineCitations(
content: string,
sources: RuntimeDisplaySource[],
): string {
if (!content || sources.length === 0) {
return content;
}
const referenceSources = sources.filter((source) => source.mode !== "none");
if (referenceSources.length === 0) {
return content;
}
const byIndex = new Map(referenceSources.map((source) => [source.index, source]));
const byUrl = new Map<string, RuntimeDisplaySource>();
const byTitle = new Map<string, RuntimeDisplaySource>();
for (const source of referenceSources) {
if (source.url) {
byUrl.set(source.url, source);
}
if (source.title) {
byTitle.set(source.title.trim(), source);
}
}
const normalized = normalizeExistingCitationLinks(
content,
byIndex,
byUrl,
byTitle,
referenceSources,
);
const markdownLinkPattern = /!?\[(?:[^\[\]]|\[[^\]]*\])*\]\([^)]+\)/g;
let cursor = 0;
let next = "";
let replaced = false;
for (const match of normalized.matchAll(markdownLinkPattern)) {
const start = match.index ?? 0;
const replacedSlice = replaceCitationTokens(
normalized.slice(cursor, start),
byIndex,
referenceSources,
);
if (replacedSlice !== normalized.slice(cursor, start)) {
replaced = true;
}
next += replacedSlice;
next += match[0];
cursor = start + match[0].length;
}
const tail = replaceCitationTokens(normalized.slice(cursor), byIndex, referenceSources);
if (tail !== normalized.slice(cursor)) {
replaced = true;
}
next += tail;
if (replaced || hasCitationMarkdown(next)) {
return next;
}
return appendCitationSummary(next, referenceSources);
}
function replaceCitationTokens(
content: string,
byIndex: Map<number, RuntimeDisplaySource>,
orderedSources: RuntimeDisplaySource[],
): string {
const normalized = normalizeLooseCitationTokens(content, byIndex, orderedSources);
const withDoubleBracketCitations = normalized
.replace(/\[\[(\d+)\]\]/g, (match, rawIndex: string) =>
citationMarkdown(match, Number(rawIndex), byIndex, orderedSources),
);
return replaceBracketCitationTokens(
withDoubleBracketCitations,
byIndex,
orderedSources,
);
}
function replaceBracketCitationTokens(
content: string,
byIndex: Map<number, RuntimeDisplaySource>,
orderedSources: RuntimeDisplaySource[],
): string {
const tokenPattern = /\\?\[\s*([0-9\uFF10-\uFF19]{1,3})\s*\\?\](?!\()/g;
let cursor = 0;
let next = "";
for (const match of content.matchAll(tokenPattern)) {
const start = match.index ?? 0;
const previous = start > 0 ? content[start - 1] : "";
if (previous === ":" || previous === "[" || previous === "!") {
continue;
}
next += content.slice(cursor, start);
next += citationMarkdown(
match[0],
Number(normalizeAsciiDigits(match[1] ?? "")),
byIndex,
orderedSources,
);
cursor = start + match[0].length;
}
next += content.slice(cursor);
return next;
}
function normalizeAsciiDigits(value: string): string {
return value.replace(/[\uFF10-\uFF19]/g, (digit) =>
String.fromCharCode(digit.charCodeAt(0) - 0xfee0),
);
}
function normalizeLooseCitationTokens(
content: string,
byIndex: Map<number, RuntimeDisplaySource>,
orderedSources: RuntimeDisplaySource[],
): string {
const isValidIndex = (index: number) =>
Number.isInteger(index) &&
index > 0 &&
(byIndex.has(index) || !!orderedSources[index - 1]);
const toCitationRun = (raw: string) => {
const parts = raw.match(/\\?\[\d{1,3}\\?\]|\d{1,3}/g) ?? [];
const indexes = parts.map((part) => Number(part.replace(/\D/g, "")));
if (indexes.length === 0 || indexes.some((index) => !isValidIndex(index))) {
return raw;
}
return indexes.map((index) => `[${index}]`).join("");
};
// 全角数字转半角:123 → 123(仅用于括号内的编号识别)
const toAsciiDigits = (value: string) =>
value.replace(/[0-9]/g, (digit) =>
String.fromCharCode(digit.charCodeAt(0) - 0xfee0),
);
// 全角括号写法(含全角数字):
// 【1】/【1】 → [1]
// 【1,2】/【1,2】/【1、2】 → [1][2]
// 【1-3】/【2~5】 → [1][2][3]
// 三种形态互斥(单编号内不含分隔符),且都要求编号能对应到参考来源,
// 避免把正文里的【…】误判成引用。转成半角后交给下面的半角逻辑统一处理。
// 全角单编号:【1】/【1】
let next = content
.replace(/[/g, "[")
.replace(/]/g, "]")
.replace(/[,、]/g, ",")
.replace(
/【\s*([0-90-9]{1,3})\s*】/g,
(match, rawIndex: string) => {
const index = Number(normalizeAsciiDigits(rawIndex));
return isValidIndex(index) ? `[${index}]` : match;
},
);
// 全角合并编号:【1,2】/【1,2】/【1、2】
next = next.replace(
/【\s*([0-90-9]{1,3}(?:\s*[,,、]\s*[0-90-9]{1,3})+)\s*】/g,
(match, rawList: string) => {
const indexes = rawList
.split(/[,,、]/)
.map((part) => Number(normalizeAsciiDigits(part.trim())));
if (indexes.some((index) => !isValidIndex(index))) {
return match;
}
return indexes.map((index) => `[${index}]`).join("");
},
);
// 全角区间编号:【1-3】/【2~5】
next = next.replace(
/【\s*([0-90-9]{1,3})\s*[-–~~]\s*([0-90-9]{1,3})\s*】/g,
(match, rawStart: string, rawEnd: string) => {
const start = Number(normalizeAsciiDigits(rawStart));
const end = Number(normalizeAsciiDigits(rawEnd));
if (
end <= start ||
end - start > 20 ||
!isValidIndex(start) ||
!isValidIndex(end)
) {
return match;
}
const tokens: string[] = [];
for (let index = start; index <= end; index++) {
tokens.push(`[${index}]`);
}
return tokens.join("");
},
);
// 合并写法:[1,2,3] / [1,2] / [1、2] → [1][2][3]
next = next.replace(
/\\?\[(\d{1,3}(?:\s*[,,、]\s*\d{1,3})+)\\?\](?!\()/g,
(match, rawList: string) => {
const indexes = rawList.split(/[,,、]/).map((part) => Number(part.trim()));
if (indexes.some((index) => !isValidIndex(index))) {
return match;
}
return indexes.map((index) => `[${index}]`).join("");
},
);
// 区间写法:[1-3] / [2~5] → [1][2][3]
next = next.replace(
/\\?\[(\d{1,3})\s*[-–~~]\s*(\d{1,3})\\?\](?!\()/g,
(match, rawStart: string, rawEnd: string) => {
const start = Number(rawStart);
const end = Number(rawEnd);
if (
end <= start ||
end - start > 20 ||
!isValidIndex(start) ||
!isValidIndex(end)
) {
return match;
}
const tokens: string[] = [];
for (let index = start; index <= end; index++) {
tokens.push(`[${index}]`);
}
return tokens.join("");
},
);
next = next.replace(
/(^|[^\d\[\]\\])((?:\d{1,3}|\\?\[\d{1,3}\\?\])(?:\\?\[\d{1,3}\\?\]|\d{1,3})+)/g,
(match, prefix: string, rawRun: string) => {
if (!rawRun.includes("[")) {
return match;
}
return `${prefix}${toCitationRun(rawRun)}`;
},
);
next = next.replace(
/(^|[\s\uFF0C\u3002\uFF1B\uFF1A\u3001,!?!?)])(\d{1,3})(?=([\s\uFF0C\u3002\uFF1B\uFF1A\u3001,!?!?)]|$))/g,
(match, prefix: string, rawIndex: string) => {
const index = Number(rawIndex);
if (!isValidIndex(index)) {
return match;
}
return `${prefix}[${index}]`;
},
);
return next;
}
function normalizeExistingCitationLinks(
content: string,
byIndex: Map<number, RuntimeDisplaySource>,
byUrl: Map<string, RuntimeDisplaySource>,
byTitle: Map<string, RuntimeDisplaySource>,
orderedSources: RuntimeDisplaySource[],
): string {
// Resolve every `[citation:LABEL](URL)` link — whether LABEL is a number
// ("[1]", "1") or a free-form title — to a numbered reference link that
// CitationLink renders as a badge and scrolls to the right-side panel.
return content.replace(
/\[citation:([^\]]+)\]\(([^)]+)\)(?:\([^)]+\))*/g,
(match, rawLabel: string, rawUrl: string) => {
const label = rawLabel.trim();
const url = rawUrl.trim();
const numberMatch = label.match(/^\[?(\d+)\]?$/);
if (numberMatch) {
const index = Number(numberMatch[1]);
const source = byIndex.get(index) ?? orderedSources[index - 1];
if (source) {
return citationLinkMarkdown(source);
}
}
const sourceByUrl = byUrl.get(url);
if (sourceByUrl) {
return citationLinkMarkdown(sourceByUrl);
}
const sourceByTitle = byTitle.get(label);
if (sourceByTitle) {
return citationLinkMarkdown(sourceByTitle);
}
return match;
},
);
}
function citationMarkdown(
fallback: string,
index: number,
byIndex: Map<number, RuntimeDisplaySource>,
orderedSources: RuntimeDisplaySource[],
): string {
const source = byIndex.get(index) ?? orderedSources[index - 1];
if (!source) {
return fallback;
}
return citationLinkMarkdown(source);
}
function hasCitationMarkdown(content: string): boolean {
return /\[citation:(?:\[[^\]]+\]|\d+)\]\(#reference-[^)]+\)/.test(content);
}
function appendCitationSummary(
content: string,
citationSources: RuntimeDisplaySource[],
): string {
const citations = citationSources
.slice(0, 5)
.map((source) => citationLinkMarkdown(source))
.join(" ");
if (!citations) {
return content;
}
const trimmed = content.trimEnd();
return `${trimmed}${trimmed ? "\n\n" : ""}参考来源:${citations}`;
}
function citationLinkMarkdown(source: RuntimeDisplaySource): string {
return `[citation:${source.index}](#reference-${source.index})`;
}
function normalizeName(name: string): string {
return name.toLowerCase().replace(/[^a-z0-9]+/g, "");
}
function normalizeCitationKey(value: string): string {
return value
.toLowerCase()
.normalize("NFKC")
.replace(/\s+/g, " ")
.trim();
}
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value);
}
function stringifyMaybe(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value : undefined;
}
function findToolResultMessage(
toolCall: RuntimeToolCall,
messages: Message[],
toolCallMessageIndex = -1,
consumedToolMessageIds?: Set<string>,
consumedToolMessages?: Set<Message>,
): Message | undefined {
const byId = findToolMessageById(toolCall.id, messages);
if (
byId &&
!consumedToolMessages?.has(byId) &&
(!byId.id || !consumedToolMessageIds?.has(byId.id))
) {
return byId;
}
return findToolMessageByName(
toolCall,
messages,
toolCallMessageIndex,
consumedToolMessageIds,
consumedToolMessages,
);
}
function parseToolMessagePayload(toolMessage: Message): unknown {
const structured = extractStructuredToolPayload(toolMessage.content);
if (structured !== undefined) {
return structured;
}
const content = extractToolText(toolMessage.content);
if (!content) {
return undefined;
}
const parsed = parseJSONLikeText(content);
if (parsed !== undefined) {
return parsed;
}
return content;
}
function extractToolMessageName(message: Message): string {
return (
stringifyMaybe((message as { name?: unknown }).name) ??
stringifyMaybe(
(message as { additional_kwargs?: Record<string, unknown> }).additional_kwargs
?.name,
) ??
"tool"
);
}
function findToolMessageById(
toolCallId: string | undefined,
messages: Message[],
): Message | undefined {
if (!toolCallId) {
return undefined;
}
return messages.find((message) => {
if (message.type !== "tool") {
return false;
}
const candidates = [
(message as { tool_call_id?: unknown }).tool_call_id,
(message as { id?: unknown }).id,
(message as { additional_kwargs?: Record<string, unknown> }).additional_kwargs
?.tool_call_id,
(message as { additional_kwargs?: Record<string, unknown> }).additional_kwargs
?.id,
];
return candidates.some((candidate) => candidate === toolCallId);
});
}
function findToolMessageByName(
toolCall: RuntimeToolCall,
messages: Message[],
toolCallMessageIndex: number,
consumedToolMessageIds?: Set<string>,
consumedToolMessages?: Set<Message>,
): Message | undefined {
const start = Math.max(toolCallMessageIndex + 1, 0);
for (let index = start; index < messages.length; index++) {
const message = messages[index];
if (!message) {
continue;
}
if (index > start && (message.type === "human" || message.type === "ai")) {
break;
}
if (message.type !== "tool") {
continue;
}
if (message.id && consumedToolMessageIds?.has(message.id)) {
continue;
}
if (consumedToolMessages?.has(message)) {
continue;
}
const name =
stringifyMaybe((message as { name?: unknown }).name) ??
stringifyMaybe(
(message as { additional_kwargs?: Record<string, unknown> }).additional_kwargs
?.name,
);
const messageToolCallId = stringifyMaybe(
(message as { tool_call_id?: unknown }).tool_call_id,
);
if (messageToolCallId && toolCall.id && messageToolCallId !== toolCall.id) {
continue;
}
if (name && normalizeName(name) !== normalizeName(toolCall.name)) {
continue;
}
const payload = extractStructuredToolPayload(message.content);
if (payload !== undefined) {
return message;
}
const text = extractToolText(message.content);
if (text && parseJSONLikeText(text) !== undefined) {
return message;
}
}
return undefined;
}
function extractStructuredToolPayload(content: unknown): unknown {
if (isRecord(content)) {
return unwrapToolPayloadObject(content);
}
if (Array.isArray(content)) {
for (const part of content) {
if (!isRecord(part)) {
continue;
}
if (isRecord(part.json)) {
return unwrapToolPayloadObject(part.json);
}
const unwrapped = unwrapToolPayloadObject(part);
if (unwrapped !== undefined) {
return unwrapped;
}
}
}
return undefined;
}
function unwrapToolPayloadObject(value: Record<string, unknown>): unknown {
const directCandidate = findDisplayPayloadCandidate(value, 0);
if (directCandidate !== undefined) {
return directCandidate;
}
for (const nested of Object.values(value)) {
if (typeof nested === "string") {
const parsed = parseJSONLikeText(nested);
if (parsed !== undefined) {
if (isRecord(parsed)) {
const structured = findDisplayPayloadCandidate(parsed, 0);
if (structured !== undefined) {
return structured;
}
}
return parsed;
}
continue;
}
if (isRecord(nested)) {
const unwrapped = unwrapToolPayloadObject(nested);
if (unwrapped !== undefined) {
return unwrapped;
}
}
}
return undefined;
}
function parseJSONLikeText(text: string): unknown {
const trimmed = text.trim();
if (!trimmed) {
return undefined;
}
const fenced = extractFencedJSON(trimmed);
if (fenced) {
try {
return JSON.parse(fenced) as unknown;
} catch {
// Fall through to best-effort parsing.
}
}
try {
return JSON.parse(trimmed) as unknown;
} catch {
return extractJSONFromText(trimmed) ?? extractSearchPayloadFromText(trimmed);
}
}
function extractJSONFromText(text: string): unknown {
for (const candidate of iterateBalancedJsonCandidates(text)) {
try {
return JSON.parse(candidate);
} catch {
// try the next candidate
}
}
return undefined;
}
function *iterateBalancedJsonCandidates(text: string): Generator<string> {
for (let index = 0; index < text.length; index++) {
const char = text[index];
if (char !== "{" && char !== "[") {
continue;
}
const candidate = extractBalancedJson(text, index);
if (candidate) {
yield candidate;
}
}
}
function extractBalancedJson(text: string, start: number): string | null {
const opening = text[start];
if (opening !== "{" && opening !== "[") {
return null;
}
const closing = opening === "{" ? "}" : "]";
let depth = 0;
let inString = false;
let escaped = false;
for (let index = start; index < text.length; index++) {
const char = text[index];
if (inString) {
if (escaped) {
escaped = false;
} else if (char === "\\") {
escaped = true;
} else if (char === "\"") {
inString = false;
}
continue;
}
if (char === "\"") {
inString = true;
continue;
}
if (char === opening) {
depth += 1;
continue;
}
if (char === closing) {
depth -= 1;
if (depth === 0) {
return text.slice(start, index + 1);
}
}
}
return null;
}
function extractFencedJSON(text: string): string | null {
const match = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
return match?.[1]?.trim() ?? null;
}
function findDisplayPayloadCandidate(value: unknown, depth: number): unknown {
if (depth > 8) {
return undefined;
}
if (Array.isArray(value)) {
const objectItems = value.filter(
(item): item is Record<string, unknown> =>
isRecord(item),
);
if (value.length === 0 || objectItems.length === value.length) {
return { results: objectItems };
}
for (const item of value) {
const nested = findDisplayPayloadCandidate(item, depth + 1);
if (nested !== undefined) {
return nested;
}
}
return undefined;
}
if (!isRecord(value)) {
return undefined;
}
for (const key of ["results", "data", "items", "docs", "records", "rows", "list", "hits", "sources"]) {
const nested = value[key];
if (!Array.isArray(nested)) {
continue;
}
const objectItems = nested.filter(
(item): item is Record<string, unknown> =>
isRecord(item),
);
if (nested.length === 0 || objectItems.length === nested.length) {
return { ...value, [key]: objectItems };
}
}
for (const nested of Object.values(value)) {
const candidate = findDisplayPayloadCandidate(nested, depth + 1);
if (candidate !== undefined) {
return candidate;
}
}
return undefined;
}
function extractSearchPayloadFromText(text: string): unknown {
if (!text.includes('"results"') || !text.includes('"recUuid"')) {
return undefined;
}
const results: Record<string, unknown>[] = [];
const objectPattern = /\{\s*"index"\s*:\s*\d+[\s\S]*?\n\s*\}/g;
for (const match of text.matchAll(objectPattern)) {
try {
const item = JSON.parse(match[0]) as unknown;
if (isRecord(item) && item.recUuid) {
results.push(item);
}
} catch {
const item = extractSearchItemFromText(match[0]);
if (item) {
results.push(item);
}
}
}
if (results.length === 0) {
return undefined;
}
return {
success: true,
query: extractStringField(text, "query") ?? "",
count: results.length,
source: extractStringField(text, "source") ?? "tool-output",
results,
};
}
function extractSearchItemFromText(text: string): Record<string, unknown> | null {
const recUuid = extractStringField(text, "recUuid");
if (!recUuid) {
return null;
}
const score = extractNumberField(text, "relevance_score");
return {
index: extractNumberField(text, "index") ?? undefined,
recUuid,
relevance_score: score,
title: extractStringField(text, "title") ?? recUuid,
source_category: extractStringField(text, "source_category") ?? "",
source: extractStringField(text, "source") ?? "",
publish_time: extractStringField(text, "publish_time") ?? "",
content_preview: extractStringField(text, "content_preview") ?? "",
content_length: extractNumberField(text, "content_length") ?? undefined,
};
}
function extractStringField(text: string, field: string): string | undefined {
const pattern = new RegExp(`"${escapeRegExp(field)}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`);
const match = pattern.exec(text);
if (!match) {
return undefined;
}
try {
return JSON.parse(`"${match[1]}"`) as string;
} catch {
return match[1];
}
}
function extractNumberField(text: string, field: string): number | undefined {
const pattern = new RegExp(`"${escapeRegExp(field)}"\\s*:\\s*(-?\\d+(?:\\.\\d+)?)`);
const match = pattern.exec(text);
if (!match) {
return undefined;
}
const value = Number(match[1]);
return Number.isFinite(value) ? value : undefined;
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function extractToolText(content: unknown): string {
if (typeof content === "string") {
return content.trim();
}
if (Array.isArray(content)) {
return content
.map((part) => {
if (typeof part === "string") {
return part;
}
if (part && typeof part === "object" && "text" in part) {
const text = (part as { text?: unknown }).text;
return typeof text === "string" ? text : "";
}
return "";
})
.join("\n")
.trim();
}
return "";
}