2026-01-14 09:11:16 +00:00
import type { NormalizedUsage } from "../../agents/usage.js" ;
2026-01-21 00:14:55 -08:00
import type { ChannelId , ChannelThreadingToolContext } from "../../channels/plugins/types.js" ;
2026-01-30 03:15:10 +01:00
import type { OpenClawConfig } from "../../config/config.js" ;
2026-01-14 09:11:16 +00:00
import type { TemplateContext } from "../templating.js" ;
import type { ReplyPayload } from "../types.js" ;
import type { FollowupRun } from "./queue.js" ;
2026-02-01 10:03:47 +09:00
import { getChannelDock } from "../../channels/dock.js" ;
import { normalizeAnyChannelId , normalizeChannelId } from "../../channels/registry.js" ;
import { isReasoningTagProvider } from "../../utils/provider-utils.js" ;
import { estimateUsageCost , formatTokenCount , formatUsd } from "../../utils/usage-format.js" ;
2026-01-14 09:11:16 +00:00
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 ;
2026-01-30 03:15:10 +01:00
config : OpenClawConfig | undefined ;
2026-01-14 09:11:16 +00:00
hasRepliedRef : { value : boolean } | undefined ;
} ) : ChannelThreadingToolContext {
const { sessionCtx , config , hasRepliedRef } = params ;
2026-01-31 16:19:20 +09:00
if ( ! config ) {
return { } ;
}
2026-01-21 00:14:55 -08:00
const rawProvider = sessionCtx . Provider ? . trim ( ) . toLowerCase ( ) ;
2026-01-31 16:19:20 +09:00
if ( ! rawProvider ) {
return { } ;
}
2026-01-22 03:27:26 +00:00
const provider = normalizeChannelId ( rawProvider ) ? ? normalizeAnyChannelId ( rawProvider ) ;
2026-01-21 00:14:55 -08:00
// Fallback for unrecognized/plugin channels (e.g., BlueBubbles before plugin registry init)
const dock = provider ? getChannelDock ( provider ) : undefined ;
if ( ! dock ? . threading ? . buildToolContext ) {
return {
2026-01-21 20:01:12 +00:00
currentChannelId : sessionCtx.To?.trim ( ) || undefined ,
2026-01-21 00:14:55 -08:00
currentChannelProvider : provider ? ? ( rawProvider as ChannelId ) ,
hasRepliedRef ,
} ;
}
2026-01-17 03:17:08 +00:00
const context =
2026-01-14 09:11:16 +00:00
dock . threading . buildToolContext ( {
cfg : config ,
accountId : sessionCtx.AccountId ,
context : {
Channel : sessionCtx.Provider ,
2026-01-21 20:01:12 +00:00
From : sessionCtx.From ,
To : sessionCtx.To ,
ChatType : sessionCtx.ChatType ,
2026-01-14 09:11:16 +00:00
ReplyToId : sessionCtx.ReplyToId ,
ThreadLabel : sessionCtx.ThreadLabel ,
2026-01-15 08:29:17 +00:00
MessageThreadId : sessionCtx.MessageThreadId ,
2026-01-14 09:11:16 +00:00
} ,
hasRepliedRef ,
2026-01-17 03:17:08 +00:00
} ) ? ? { } ;
return {
. . . context ,
2026-01-21 00:14:55 -08:00
currentChannelProvider : provider ! , // guaranteed non-null since dock exists
2026-01-17 03:17:08 +00:00
} ;
2026-01-14 09:11:16 +00:00
}
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 ;
2026-01-31 16:19:20 +09:00
if ( ! usage ) {
return null ;
}
2026-01-14 09:11:16 +00:00
const input = usage . input ;
const output = usage . output ;
2026-01-31 16:19:20 +09:00
if ( typeof input !== "number" && typeof output !== "number" ) {
return null ;
}
2026-01-14 09:11:16 +00:00
const inputLabel = typeof input === "number" ? formatTokenCount ( input ) : "?" ;
2026-01-14 14:31:43 +00:00
const outputLabel = typeof output === "number" ? formatTokenCount ( output ) : "?" ;
2026-01-14 09:11:16 +00:00
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 } ` ;
} ;
2026-01-14 14:31:43 +00:00
export const appendUsageLine = ( payloads : ReplyPayload [ ] , line : string ) : ReplyPayload [ ] = > {
2026-01-14 09:11:16 +00:00
let index = - 1 ;
for ( let i = payloads . length - 1 ; i >= 0 ; i -= 1 ) {
if ( payloads [ i ] ? . text ) {
index = i ;
break ;
}
}
2026-01-31 16:19:20 +09:00
if ( index === - 1 ) {
return [ . . . payloads , { text : line } ] ;
}
2026-01-14 09:11:16 +00:00
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 ;
} ;
2026-01-14 14:31:43 +00:00
export const resolveEnforceFinalTag = ( run : FollowupRun [ "run" ] , provider : string ) = >
Boolean ( run . enforceFinalTag || isReasoningTagProvider ( provider ) ) ;