2026-02-18 19:02:25 +00:00
import { resolveAgentModelFallbacksOverride } from "../../agents/agent-scope.js" ;
2026-01-14 09:11:16 +00:00
import type { NormalizedUsage } from "../../agents/usage.js" ;
2026-02-18 01:34:35 +00:00
import { getChannelDock } from "../../channels/dock.js" ;
2026-01-21 00:14:55 -08:00
import type { ChannelId , ChannelThreadingToolContext } from "../../channels/plugins/types.js" ;
2026-02-18 01:34:35 +00:00
import { normalizeAnyChannelId , normalizeChannelId } from "../../channels/registry.js" ;
2026-01-30 03:15:10 +01:00
import type { OpenClawConfig } from "../../config/config.js" ;
2026-02-18 19:02:25 +00:00
import { resolveAgentIdFromSessionKey } from "../../config/sessions.js" ;
2026-02-18 01:34:35 +00:00
import { isReasoningTagProvider } from "../../utils/provider-utils.js" ;
import { estimateUsageCost , formatTokenCount , formatUsd } from "../../utils/usage-format.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" ;
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-02-23 23:25:14 +08:00
const currentMessageId = sessionCtx . MessageSidFull ? ? sessionCtx . MessageSid ;
2026-01-31 16:19:20 +09:00
if ( ! config ) {
2026-02-23 23:25:14 +08:00
return {
currentMessageId ,
} ;
2026-01-31 16:19:20 +09:00
}
2026-01-21 00:14:55 -08:00
const rawProvider = sessionCtx . Provider ? . trim ( ) . toLowerCase ( ) ;
2026-01-31 16:19:20 +09:00
if ( ! rawProvider ) {
2026-02-23 23:25:14 +08:00
return {
currentMessageId ,
} ;
2026-01-31 16:19:20 +09:00
}
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 ) ,
2026-02-23 23:25:14 +08:00
currentMessageId ,
2026-01-21 00:14:55 -08:00
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-02-23 23:25:14 +08:00
CurrentMessageId : currentMessageId ,
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-02-23 23:25:14 +08:00
currentMessageId : context.currentMessageId ? ? currentMessageId ,
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 ) ) ;
2026-02-17 00:10:26 +00:00
2026-02-18 19:02:25 +00:00
export function resolveModelFallbackOptions ( run : FollowupRun [ "run" ] ) {
return {
cfg : run.config ,
provider : run.provider ,
model : run.model ,
agentDir : run.agentDir ,
fallbacksOverride : resolveAgentModelFallbacksOverride (
run . config ,
resolveAgentIdFromSessionKey ( run . sessionKey ) ,
) ,
} ;
}
export function buildEmbeddedRunBaseParams ( params : {
run : FollowupRun [ "run" ] ;
provider : string ;
model : string ;
runId : string ;
authProfile : ReturnType < typeof resolveProviderScopedAuthProfile > ;
} ) {
return {
sessionFile : params.run.sessionFile ,
workspaceDir : params.run.workspaceDir ,
agentDir : params.run.agentDir ,
config : params.run.config ,
skillsSnapshot : params.run.skillsSnapshot ,
ownerNumbers : params.run.ownerNumbers ,
2026-02-21 03:03:58 +00:00
senderIsOwner : params.run.senderIsOwner ,
2026-02-18 19:02:25 +00:00
enforceFinalTag : resolveEnforceFinalTag ( params . run , params . provider ) ,
provider : params.provider ,
model : params.model ,
. . . params . authProfile ,
thinkLevel : params.run.thinkLevel ,
verboseLevel : params.run.verboseLevel ,
reasoningLevel : params.run.reasoningLevel ,
execOverrides : params.run.execOverrides ,
bashElevated : params.run.bashElevated ,
timeoutMs : params.run.timeoutMs ,
runId : params.runId ,
} ;
}
2026-02-17 00:10:26 +00:00
export function buildEmbeddedContextFromTemplate ( params : {
run : FollowupRun [ "run" ] ;
sessionCtx : TemplateContext ;
hasRepliedRef : { value : boolean } | undefined ;
} ) {
return {
sessionId : params.run.sessionId ,
sessionKey : params.run.sessionKey ,
agentId : params.run.agentId ,
messageProvider : params.sessionCtx.Provider?.trim ( ) . toLowerCase ( ) || undefined ,
agentAccountId : params.sessionCtx.AccountId ,
messageTo : params.sessionCtx.OriginatingTo ? ? params . sessionCtx . To ,
messageThreadId : params.sessionCtx.MessageThreadId ? ? undefined ,
// Provider threading context for tool auto-injection
. . . buildThreadingToolContext ( {
sessionCtx : params.sessionCtx ,
config : params.run.config ,
hasRepliedRef : params.hasRepliedRef ,
} ) ,
} ;
}
export function buildTemplateSenderContext ( sessionCtx : TemplateContext ) {
return {
senderId : sessionCtx.SenderId?.trim ( ) || undefined ,
senderName : sessionCtx.SenderName?.trim ( ) || undefined ,
senderUsername : sessionCtx.SenderUsername?.trim ( ) || undefined ,
senderE164 : sessionCtx.SenderE164?.trim ( ) || undefined ,
} ;
}
export function resolveRunAuthProfile ( run : FollowupRun [ "run" ] , provider : string ) {
return resolveProviderScopedAuthProfile ( {
provider ,
primaryProvider : run.provider ,
authProfileId : run.authProfileId ,
authProfileIdSource : run.authProfileIdSource ,
} ) ;
}
2026-02-18 19:02:25 +00:00
export function buildEmbeddedRunContexts ( params : {
run : FollowupRun [ "run" ] ;
sessionCtx : TemplateContext ;
hasRepliedRef : { value : boolean } | undefined ;
provider : string ;
} ) {
return {
authProfile : resolveRunAuthProfile ( params . run , params . provider ) ,
embeddedContext : buildEmbeddedContextFromTemplate ( {
run : params.run ,
sessionCtx : params.sessionCtx ,
hasRepliedRef : params.hasRepliedRef ,
} ) ,
senderContext : buildTemplateSenderContext ( params . sessionCtx ) ,
} ;
}
2026-02-17 00:10:26 +00:00
export function resolveProviderScopedAuthProfile ( params : {
provider : string ;
primaryProvider : string ;
authProfileId? : string ;
authProfileIdSource ? : "auto" | "user" ;
} ) : { authProfileId? : string ; authProfileIdSource ? : "auto" | "user" } {
const authProfileId =
params . provider === params . primaryProvider ? params.authProfileId : undefined ;
return {
authProfileId ,
authProfileIdSource : authProfileId ? params.authProfileIdSource : undefined ,
} ;
}