mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 18:54:58 +00:00
170 lines
4.6 KiB
TypeScript
170 lines
4.6 KiB
TypeScript
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
|
|
export const CHARS_PER_TOKEN_ESTIMATE = 4;
|
|
export const TOOL_RESULT_CHARS_PER_TOKEN_ESTIMATE = 2;
|
|
const IMAGE_CHAR_ESTIMATE = 8_000;
|
|
|
|
export type MessageCharEstimateCache = WeakMap<AgentMessage, number>;
|
|
|
|
function isTextBlock(block: unknown): block is { type: "text"; text: string } {
|
|
return !!block && typeof block === "object" && (block as { type?: unknown }).type === "text";
|
|
}
|
|
|
|
function isImageBlock(block: unknown): boolean {
|
|
return !!block && typeof block === "object" && (block as { type?: unknown }).type === "image";
|
|
}
|
|
|
|
function estimateUnknownChars(value: unknown): number {
|
|
if (typeof value === "string") {
|
|
return value.length;
|
|
}
|
|
if (value === undefined) {
|
|
return 0;
|
|
}
|
|
try {
|
|
const serialized = JSON.stringify(value);
|
|
return typeof serialized === "string" ? serialized.length : 0;
|
|
} catch {
|
|
return 256;
|
|
}
|
|
}
|
|
|
|
export function isToolResultMessage(msg: AgentMessage): boolean {
|
|
const role = (msg as { role?: unknown }).role;
|
|
const type = (msg as { type?: unknown }).type;
|
|
return role === "toolResult" || role === "tool" || type === "toolResult";
|
|
}
|
|
|
|
function getToolResultContent(msg: AgentMessage): unknown[] {
|
|
if (!isToolResultMessage(msg)) {
|
|
return [];
|
|
}
|
|
const content = (msg as { content?: unknown }).content;
|
|
if (typeof content === "string") {
|
|
return [{ type: "text", text: content }];
|
|
}
|
|
return Array.isArray(content) ? content : [];
|
|
}
|
|
|
|
export function getToolResultText(msg: AgentMessage): string {
|
|
const content = getToolResultContent(msg);
|
|
const chunks: string[] = [];
|
|
for (const block of content) {
|
|
if (isTextBlock(block)) {
|
|
chunks.push(block.text);
|
|
}
|
|
}
|
|
return chunks.join("\n");
|
|
}
|
|
|
|
function estimateMessageChars(msg: AgentMessage): number {
|
|
if (!msg || typeof msg !== "object") {
|
|
return 0;
|
|
}
|
|
|
|
if (msg.role === "user") {
|
|
const content = msg.content;
|
|
if (typeof content === "string") {
|
|
return content.length;
|
|
}
|
|
let chars = 0;
|
|
if (Array.isArray(content)) {
|
|
for (const block of content) {
|
|
if (isTextBlock(block)) {
|
|
chars += block.text.length;
|
|
} else if (isImageBlock(block)) {
|
|
chars += IMAGE_CHAR_ESTIMATE;
|
|
} else {
|
|
chars += estimateUnknownChars(block);
|
|
}
|
|
}
|
|
}
|
|
return chars;
|
|
}
|
|
|
|
if (msg.role === "assistant") {
|
|
let chars = 0;
|
|
const content = (msg as { content?: unknown }).content;
|
|
if (Array.isArray(content)) {
|
|
for (const block of content) {
|
|
if (!block || typeof block !== "object") {
|
|
continue;
|
|
}
|
|
const typed = block as {
|
|
type?: unknown;
|
|
text?: unknown;
|
|
thinking?: unknown;
|
|
arguments?: unknown;
|
|
};
|
|
if (typed.type === "text" && typeof typed.text === "string") {
|
|
chars += typed.text.length;
|
|
} else if (typed.type === "thinking" && typeof typed.thinking === "string") {
|
|
chars += typed.thinking.length;
|
|
} else if (typed.type === "toolCall") {
|
|
try {
|
|
chars += JSON.stringify(typed.arguments ?? {}).length;
|
|
} catch {
|
|
chars += 128;
|
|
}
|
|
} else {
|
|
chars += estimateUnknownChars(block);
|
|
}
|
|
}
|
|
}
|
|
return chars;
|
|
}
|
|
|
|
if (isToolResultMessage(msg)) {
|
|
let chars = 0;
|
|
const content = getToolResultContent(msg);
|
|
for (const block of content) {
|
|
if (isTextBlock(block)) {
|
|
chars += block.text.length;
|
|
} else if (isImageBlock(block)) {
|
|
chars += IMAGE_CHAR_ESTIMATE;
|
|
} else {
|
|
chars += estimateUnknownChars(block);
|
|
}
|
|
}
|
|
const details = (msg as { details?: unknown }).details;
|
|
chars += estimateUnknownChars(details);
|
|
const weightedChars = Math.ceil(
|
|
chars * (CHARS_PER_TOKEN_ESTIMATE / TOOL_RESULT_CHARS_PER_TOKEN_ESTIMATE),
|
|
);
|
|
return Math.max(chars, weightedChars);
|
|
}
|
|
|
|
return 256;
|
|
}
|
|
|
|
export function createMessageCharEstimateCache(): MessageCharEstimateCache {
|
|
return new WeakMap<AgentMessage, number>();
|
|
}
|
|
|
|
export function estimateMessageCharsCached(
|
|
msg: AgentMessage,
|
|
cache: MessageCharEstimateCache,
|
|
): number {
|
|
const hit = cache.get(msg);
|
|
if (hit !== undefined) {
|
|
return hit;
|
|
}
|
|
const estimated = estimateMessageChars(msg);
|
|
cache.set(msg, estimated);
|
|
return estimated;
|
|
}
|
|
|
|
export function estimateContextChars(
|
|
messages: AgentMessage[],
|
|
cache: MessageCharEstimateCache,
|
|
): number {
|
|
return messages.reduce((sum, msg) => sum + estimateMessageCharsCached(msg, cache), 0);
|
|
}
|
|
|
|
export function invalidateMessageCharsCacheEntry(
|
|
cache: MessageCharEstimateCache,
|
|
msg: AgentMessage,
|
|
): void {
|
|
cache.delete(msg);
|
|
}
|