mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-11 15:56:08 +00:00
109 lines
3.0 KiB
TypeScript
109 lines
3.0 KiB
TypeScript
/**
|
|
* sessions_yield transcript detectors.
|
|
*
|
|
* Accepts provider-specific tool-call and tool-result shapes used by transcript repair and announce capture.
|
|
*/
|
|
import { safeParseJson } from "@openclaw/normalization-core";
|
|
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
|
|
import { readTrimmedStringAlias } from "../utils/string-readers.js";
|
|
|
|
function readToolName(value: unknown): string | undefined {
|
|
const record = asOptionalRecord(value);
|
|
if (!record) {
|
|
return undefined;
|
|
}
|
|
return readTrimmedStringAlias(record, [
|
|
"name",
|
|
"toolName",
|
|
"tool_name",
|
|
"functionName",
|
|
"function_name",
|
|
]);
|
|
}
|
|
|
|
function isToolCallBlock(value: unknown): boolean {
|
|
const record = asOptionalRecord(value);
|
|
if (!record) {
|
|
return false;
|
|
}
|
|
return (
|
|
record.type === "toolCall" ||
|
|
record.type === "tool_use" ||
|
|
record.type === "toolUse" ||
|
|
record.type === "functionCall" ||
|
|
record.type === "function_call"
|
|
);
|
|
}
|
|
|
|
/** Returns true when an assistant message requested the sessions_yield tool. */
|
|
export function assistantCallsSessionsYield(message: unknown): boolean {
|
|
const record = asOptionalRecord(message);
|
|
if (!record || record.role !== "assistant" || !Array.isArray(record.content)) {
|
|
return false;
|
|
}
|
|
return record.content.some(
|
|
(block) => isToolCallBlock(block) && readToolName(block) === "sessions_yield",
|
|
);
|
|
}
|
|
|
|
function parseJsonObject(text: string): Record<string, unknown> | undefined {
|
|
const trimmed = text.trim();
|
|
if (!trimmed.startsWith("{")) {
|
|
return undefined;
|
|
}
|
|
return asOptionalRecord(safeParseJson(trimmed));
|
|
}
|
|
|
|
function readStructuredToolPayload(content: unknown): Record<string, unknown> | undefined {
|
|
const record = asOptionalRecord(content);
|
|
if (record) {
|
|
return record;
|
|
}
|
|
if (typeof content === "string") {
|
|
return parseJsonObject(content);
|
|
}
|
|
if (!Array.isArray(content)) {
|
|
return undefined;
|
|
}
|
|
for (const block of content) {
|
|
const blockRecord = asOptionalRecord(block);
|
|
if (!blockRecord) {
|
|
continue;
|
|
}
|
|
const text = blockRecord.text;
|
|
if (typeof text !== "string") {
|
|
continue;
|
|
}
|
|
const parsed = parseJsonObject(text);
|
|
if (parsed) {
|
|
return parsed;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/** Returns true when a tool result represents a completed sessions_yield handoff. */
|
|
export function isSessionsYieldToolResult(
|
|
message: unknown,
|
|
previousAssistantCalledYield: boolean,
|
|
): boolean {
|
|
const record = asOptionalRecord(message);
|
|
if (!record || (record.role !== "toolResult" && record.role !== "tool")) {
|
|
return false;
|
|
}
|
|
const toolName = readToolName(record);
|
|
if (toolName === "sessions_yield") {
|
|
return true;
|
|
}
|
|
if (!previousAssistantCalledYield) {
|
|
return false;
|
|
}
|
|
// Some providers omit the tool name on results; use adjacency plus yielded status as fallback.
|
|
const details = asOptionalRecord(record.details);
|
|
if (details?.status === "yielded") {
|
|
return true;
|
|
}
|
|
const payload = readStructuredToolPayload(record.content);
|
|
return payload?.status === "yielded";
|
|
}
|