127 lines
4.5 KiB
TypeScript
127 lines
4.5 KiB
TypeScript
import type { NormalizedUsage } from "../../agents/usage.js";
|
|
import { getChannelDock } from "../../channels/dock.js";
|
|
import type { ChannelId, ChannelThreadingToolContext } from "../../channels/plugins/types.js";
|
|
import { normalizeAnyChannelId, normalizeChannelId } from "../../channels/registry.js";
|
|
import type { ClawdbotConfig } from "../../config/config.js";
|
|
import { isReasoningTagProvider } from "../../utils/provider-utils.js";
|
|
import { estimateUsageCost, formatTokenCount, formatUsd } from "../../utils/usage-format.js";
|
|
import type { TemplateContext } from "../templating.js";
|
|
import type { ReplyPayload } from "../types.js";
|
|
import type { FollowupRun } from "./queue.js";
|
|
|
|
const BUN_FETCH_SOCKET_ERROR_RE = /socket connection was closed unexpectedly/i;
|
|
|
|
/**
|
|
* Build provider-specific threading context for tool auto-injection.
|
|
*/
|
|
export function buildThreadingToolContext(params: {
|
|
sessionCtx: TemplateContext;
|
|
config: ClawdbotConfig | undefined;
|
|
hasRepliedRef: { value: boolean } | undefined;
|
|
}): ChannelThreadingToolContext {
|
|
const { sessionCtx, config, hasRepliedRef } = params;
|
|
if (!config) return {};
|
|
const rawProvider = sessionCtx.Provider?.trim().toLowerCase();
|
|
if (!rawProvider) return {};
|
|
const provider = normalizeChannelId(rawProvider) ?? normalizeAnyChannelId(rawProvider);
|
|
// Fallback for unrecognized/plugin channels (e.g., BlueBubbles before plugin registry init)
|
|
const dock = provider ? getChannelDock(provider) : undefined;
|
|
if (!dock?.threading?.buildToolContext) {
|
|
return {
|
|
currentChannelId: sessionCtx.To?.trim() || undefined,
|
|
currentChannelProvider: provider ?? (rawProvider as ChannelId),
|
|
hasRepliedRef,
|
|
};
|
|
}
|
|
const context =
|
|
dock.threading.buildToolContext({
|
|
cfg: config,
|
|
accountId: sessionCtx.AccountId,
|
|
context: {
|
|
Channel: sessionCtx.Provider,
|
|
From: sessionCtx.From,
|
|
To: sessionCtx.To,
|
|
ChatType: sessionCtx.ChatType,
|
|
ReplyToId: sessionCtx.ReplyToId,
|
|
ThreadLabel: sessionCtx.ThreadLabel,
|
|
MessageThreadId: sessionCtx.MessageThreadId,
|
|
},
|
|
hasRepliedRef,
|
|
}) ?? {};
|
|
return {
|
|
...context,
|
|
currentChannelProvider: provider!, // guaranteed non-null since dock exists
|
|
};
|
|
}
|
|
|
|
export const isBunFetchSocketError = (message?: string) =>
|
|
Boolean(message && BUN_FETCH_SOCKET_ERROR_RE.test(message));
|
|
|
|
export const formatBunFetchSocketError = (message: string) => {
|
|
const trimmed = message.trim();
|
|
return [
|
|
"⚠️ LLM connection failed. This could be due to server issues, network problems, or context length exceeded (e.g., with local LLMs like LM Studio). Original error:",
|
|
"```",
|
|
trimmed || "Unknown error",
|
|
"```",
|
|
].join("\n");
|
|
};
|
|
|
|
export const formatResponseUsageLine = (params: {
|
|
usage?: NormalizedUsage;
|
|
showCost: boolean;
|
|
costConfig?: {
|
|
input: number;
|
|
output: number;
|
|
cacheRead: number;
|
|
cacheWrite: number;
|
|
};
|
|
}): string | null => {
|
|
const usage = params.usage;
|
|
if (!usage) return null;
|
|
const input = usage.input;
|
|
const output = usage.output;
|
|
if (typeof input !== "number" && typeof output !== "number") return null;
|
|
const inputLabel = typeof input === "number" ? formatTokenCount(input) : "?";
|
|
const outputLabel = typeof output === "number" ? formatTokenCount(output) : "?";
|
|
const cost =
|
|
params.showCost && typeof input === "number" && typeof output === "number"
|
|
? estimateUsageCost({
|
|
usage: {
|
|
input,
|
|
output,
|
|
cacheRead: usage.cacheRead,
|
|
cacheWrite: usage.cacheWrite,
|
|
},
|
|
cost: params.costConfig,
|
|
})
|
|
: undefined;
|
|
const costLabel = params.showCost ? formatUsd(cost) : undefined;
|
|
const suffix = costLabel ? ` · est ${costLabel}` : "";
|
|
return `Usage: ${inputLabel} in / ${outputLabel} out${suffix}`;
|
|
};
|
|
|
|
export const appendUsageLine = (payloads: ReplyPayload[], line: string): ReplyPayload[] => {
|
|
let index = -1;
|
|
for (let i = payloads.length - 1; i >= 0; i -= 1) {
|
|
if (payloads[i]?.text) {
|
|
index = i;
|
|
break;
|
|
}
|
|
}
|
|
if (index === -1) return [...payloads, { text: line }];
|
|
const existing = payloads[index];
|
|
const existingText = existing.text ?? "";
|
|
const separator = existingText.endsWith("\n") ? "" : "\n";
|
|
const next = {
|
|
...existing,
|
|
text: `${existingText}${separator}${line}`,
|
|
};
|
|
const updated = payloads.slice();
|
|
updated[index] = next;
|
|
return updated;
|
|
};
|
|
|
|
export const resolveEnforceFinalTag = (run: FollowupRun["run"], provider: string) =>
|
|
Boolean(run.enforceFinalTag || isReasoningTagProvider(provider));
|