2026-01-14 01:08:15 +00:00
import fs from "node:fs/promises" ;
import type { ThinkLevel } from "../../auto-reply/thinking.js" ;
import { enqueueCommandInLane } from "../../process/command-queue.js" ;
import { resolveUserPath } from "../../utils.js" ;
2026-01-17 10:17:57 +00:00
import { isMarkdownCapableMessageChannel } from "../../utils/message-channel.js" ;
2026-01-14 01:08:15 +00:00
import { resolveClawdbotAgentDir } from "../agent-paths.js" ;
import {
markAuthProfileFailure ,
markAuthProfileGood ,
markAuthProfileUsed ,
} from "../auth-profiles.js" ;
import {
CONTEXT_WINDOW_HARD_MIN_TOKENS ,
CONTEXT_WINDOW_WARN_BELOW_TOKENS ,
evaluateContextWindowGuard ,
resolveContextWindowInfo ,
} from "../context-window-guard.js" ;
2026-01-14 14:31:43 +00:00
import { DEFAULT_CONTEXT_TOKENS , DEFAULT_MODEL , DEFAULT_PROVIDER } from "../defaults.js" ;
2026-01-14 01:08:15 +00:00
import { FailoverError , resolveFailoverStatus } from "../failover-error.js" ;
import {
ensureAuthProfileStore ,
getApiKeyForModel ,
resolveAuthProfileOrder ,
2026-01-20 07:53:25 +00:00
type ResolvedProviderAuth ,
2026-01-14 01:08:15 +00:00
} from "../model-auth.js" ;
2026-01-21 06:00:16 +00:00
import { normalizeProviderId } from "../model-selection.js" ;
2026-01-14 01:08:15 +00:00
import { ensureClawdbotModelsJson } from "../models-config.js" ;
import {
classifyFailoverReason ,
formatAssistantErrorText ,
isAuthAssistantError ,
isCompactionFailureError ,
isContextOverflowError ,
isFailoverAssistantError ,
isFailoverErrorMessage ,
2026-01-18 15:19:25 +00:00
parseImageDimensionError ,
2026-01-14 01:08:15 +00:00
isRateLimitAssistantError ,
isTimeoutErrorMessage ,
pickFallbackThinkingLevel ,
} from "../pi-embedded-helpers.js" ;
import { normalizeUsage , type UsageLike } from "../usage.js" ;
import { resolveGlobalLane , resolveSessionLane } from "./lanes.js" ;
import { log } from "./logger.js" ;
import { resolveModel } from "./model.js" ;
import { runEmbeddedAttempt } from "./run/attempt.js" ;
import type { RunEmbeddedPiAgentParams } from "./run/params.js" ;
import { buildEmbeddedRunPayloads } from "./run/payloads.js" ;
import type { EmbeddedPiAgentMeta , EmbeddedPiRunResult } from "./types.js" ;
import { describeUnknownError } from "./utils.js" ;
2026-01-20 07:53:25 +00:00
type ApiKeyInfo = ResolvedProviderAuth ;
2026-01-14 01:08:15 +00:00
2026-01-21 07:28:11 +00:00
// Avoid Anthropic's refusal test token poisoning session transcripts.
const ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL = "ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL" ;
const ANTHROPIC_MAGIC_STRING_REPLACEMENT = "ANTHROPIC MAGIC STRING TRIGGER REFUSAL (redacted)" ;
function scrubAnthropicRefusalMagic ( prompt : string ) : string {
if ( ! prompt . includes ( ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL ) ) return prompt ;
return prompt . replaceAll (
ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL ,
ANTHROPIC_MAGIC_STRING_REPLACEMENT ,
) ;
}
2026-01-14 01:08:15 +00:00
export async function runEmbeddedPiAgent (
params : RunEmbeddedPiAgentParams ,
) : Promise < EmbeddedPiRunResult > {
2026-01-14 14:31:43 +00:00
const sessionLane = resolveSessionLane ( params . sessionKey ? . trim ( ) || params . sessionId ) ;
2026-01-14 01:08:15 +00:00
const globalLane = resolveGlobalLane ( params . lane ) ;
const enqueueGlobal =
2026-01-14 14:31:43 +00:00
params . enqueue ? ? ( ( task , opts ) = > enqueueCommandInLane ( globalLane , task , opts ) ) ;
2026-01-17 10:17:57 +00:00
const channelHint = params . messageChannel ? ? params . messageProvider ;
const resolvedToolResultFormat =
params . toolResultFormat ? ?
( channelHint
? isMarkdownCapableMessageChannel ( channelHint )
? "markdown"
: "plain"
: "markdown" ) ;
2026-01-14 01:08:15 +00:00
return enqueueCommandInLane ( sessionLane , ( ) = >
enqueueGlobal ( async ( ) = > {
const started = Date . now ( ) ;
const resolvedWorkspace = resolveUserPath ( params . workspaceDir ) ;
const prevCwd = process . cwd ( ) ;
2026-01-14 14:31:43 +00:00
const provider = ( params . provider ? ? DEFAULT_PROVIDER ) . trim ( ) || DEFAULT_PROVIDER ;
2026-01-14 01:08:15 +00:00
const modelId = ( params . model ? ? DEFAULT_MODEL ) . trim ( ) || DEFAULT_MODEL ;
const agentDir = params . agentDir ? ? resolveClawdbotAgentDir ( ) ;
await ensureClawdbotModelsJson ( params . config , agentDir ) ;
const { model , error , authStorage , modelRegistry } = resolveModel (
provider ,
modelId ,
agentDir ,
params . config ,
) ;
if ( ! model ) {
throw new Error ( error ? ? ` Unknown model: ${ provider } / ${ modelId } ` ) ;
}
const ctxInfo = resolveContextWindowInfo ( {
cfg : params.config ,
provider ,
modelId ,
modelContextWindow : model.contextWindow ,
defaultTokens : DEFAULT_CONTEXT_TOKENS ,
} ) ;
const ctxGuard = evaluateContextWindowGuard ( {
info : ctxInfo ,
warnBelowTokens : CONTEXT_WINDOW_WARN_BELOW_TOKENS ,
hardMinTokens : CONTEXT_WINDOW_HARD_MIN_TOKENS ,
} ) ;
if ( ctxGuard . shouldWarn ) {
log . warn (
` low context window: ${ provider } / ${ modelId } ctx= ${ ctxGuard . tokens } (warn< ${ CONTEXT_WINDOW_WARN_BELOW_TOKENS } ) source= ${ ctxGuard . source } ` ,
) ;
}
if ( ctxGuard . shouldBlock ) {
log . error (
` blocked model (context window too small): ${ provider } / ${ modelId } ctx= ${ ctxGuard . tokens } (min= ${ CONTEXT_WINDOW_HARD_MIN_TOKENS } ) source= ${ ctxGuard . source } ` ,
) ;
throw new FailoverError (
` Model context window too small ( ${ ctxGuard . tokens } tokens). Minimum is ${ CONTEXT_WINDOW_HARD_MIN_TOKENS } . ` ,
{ reason : "unknown" , provider , model : modelId } ,
) ;
}
2026-01-18 04:18:58 +00:00
const authStore = ensureAuthProfileStore ( agentDir , { allowKeychainPrompt : false } ) ;
2026-01-18 08:22:50 +00:00
const preferredProfileId = params . authProfileId ? . trim ( ) ;
2026-01-21 06:00:16 +00:00
let lockedProfileId = params . authProfileIdSource === "user" ? preferredProfileId : undefined ;
if ( lockedProfileId ) {
const lockedProfile = authStore . profiles [ lockedProfileId ] ;
if (
! lockedProfile ||
normalizeProviderId ( lockedProfile . provider ) !== normalizeProviderId ( provider )
) {
lockedProfileId = undefined ;
}
}
2026-01-14 01:08:15 +00:00
const profileOrder = resolveAuthProfileOrder ( {
cfg : params.config ,
store : authStore ,
provider ,
2026-01-18 08:22:50 +00:00
preferredProfile : preferredProfileId ,
2026-01-14 01:08:15 +00:00
} ) ;
2026-01-18 08:22:50 +00:00
if ( lockedProfileId && ! profileOrder . includes ( lockedProfileId ) ) {
throw new Error ( ` Auth profile " ${ lockedProfileId } " is not configured for ${ provider } . ` ) ;
2026-01-14 01:08:15 +00:00
}
2026-01-14 14:31:43 +00:00
const profileCandidates = profileOrder . length > 0 ? profileOrder : [ undefined ] ;
2026-01-14 01:08:15 +00:00
let profileIndex = 0 ;
const initialThinkLevel = params . thinkLevel ? ? "off" ;
let thinkLevel = initialThinkLevel ;
const attemptedThinking = new Set < ThinkLevel > ( ) ;
let apiKeyInfo : ApiKeyInfo | null = null ;
let lastProfileId : string | undefined ;
const resolveApiKeyForCandidate = async ( candidate? : string ) = > {
return getApiKeyForModel ( {
model ,
cfg : params.config ,
profileId : candidate ,
store : authStore ,
2026-01-15 04:41:50 +00:00
agentDir ,
2026-01-14 01:08:15 +00:00
} ) ;
} ;
const applyApiKeyInfo = async ( candidate? : string ) : Promise < void > = > {
apiKeyInfo = await resolveApiKeyForCandidate ( candidate ) ;
2026-01-20 07:53:25 +00:00
if ( ! apiKeyInfo . apiKey ) {
if ( apiKeyInfo . mode !== "aws-sdk" ) {
throw new Error (
` No API key resolved for provider " ${ model . provider } " (auth mode: ${ apiKeyInfo . mode } ). ` ,
) ;
}
lastProfileId = apiKeyInfo . profileId ;
return ;
}
2026-01-23 02:10:17 +00:00
authStorage . setRuntimeApiKey ( model . provider , apiKeyInfo . apiKey ) ;
2026-01-14 01:08:15 +00:00
lastProfileId = apiKeyInfo . profileId ;
} ;
const advanceAuthProfile = async ( ) : Promise < boolean > = > {
2026-01-18 08:22:50 +00:00
if ( lockedProfileId ) return false ;
2026-01-14 01:08:15 +00:00
let nextIndex = profileIndex + 1 ;
while ( nextIndex < profileCandidates . length ) {
const candidate = profileCandidates [ nextIndex ] ;
try {
await applyApiKeyInfo ( candidate ) ;
profileIndex = nextIndex ;
thinkLevel = initialThinkLevel ;
attemptedThinking . clear ( ) ;
return true ;
} catch ( err ) {
2026-01-18 08:22:50 +00:00
if ( candidate && candidate === lockedProfileId ) throw err ;
2026-01-14 01:08:15 +00:00
nextIndex += 1 ;
}
}
return false ;
} ;
try {
await applyApiKeyInfo ( profileCandidates [ profileIndex ] ) ;
} catch ( err ) {
2026-01-18 08:22:50 +00:00
if ( profileCandidates [ profileIndex ] === lockedProfileId ) throw err ;
2026-01-14 01:08:15 +00:00
const advanced = await advanceAuthProfile ( ) ;
if ( ! advanced ) throw err ;
}
try {
while ( true ) {
attemptedThinking . add ( thinkLevel ) ;
await fs . mkdir ( resolvedWorkspace , { recursive : true } ) ;
2026-01-21 07:28:11 +00:00
const prompt =
provider === "anthropic" ? scrubAnthropicRefusalMagic ( params . prompt ) : params . prompt ;
2026-01-14 01:08:15 +00:00
const attempt = await runEmbeddedAttempt ( {
sessionId : params.sessionId ,
sessionKey : params.sessionKey ,
messageChannel : params.messageChannel ,
messageProvider : params.messageProvider ,
agentAccountId : params.agentAccountId ,
2026-01-20 17:22:07 +00:00
messageTo : params.messageTo ,
messageThreadId : params.messageThreadId ,
2026-01-14 01:08:15 +00:00
currentChannelId : params.currentChannelId ,
currentThreadTs : params.currentThreadTs ,
replyToMode : params.replyToMode ,
hasRepliedRef : params.hasRepliedRef ,
sessionFile : params.sessionFile ,
workspaceDir : params.workspaceDir ,
agentDir ,
config : params.config ,
skillsSnapshot : params.skillsSnapshot ,
2026-01-21 07:28:11 +00:00
prompt ,
2026-01-14 01:08:15 +00:00
images : params.images ,
provider ,
modelId ,
model ,
authStorage ,
modelRegistry ,
thinkLevel ,
verboseLevel : params.verboseLevel ,
reasoningLevel : params.reasoningLevel ,
2026-01-17 10:17:57 +00:00
toolResultFormat : resolvedToolResultFormat ,
2026-01-18 06:11:38 +00:00
execOverrides : params.execOverrides ,
2026-01-14 01:08:15 +00:00
bashElevated : params.bashElevated ,
timeoutMs : params.timeoutMs ,
runId : params.runId ,
abortSignal : params.abortSignal ,
shouldEmitToolResult : params.shouldEmitToolResult ,
2026-01-17 05:33:27 +00:00
shouldEmitToolOutput : params.shouldEmitToolOutput ,
2026-01-14 01:08:15 +00:00
onPartialReply : params.onPartialReply ,
onAssistantMessageStart : params.onAssistantMessageStart ,
onBlockReply : params.onBlockReply ,
onBlockReplyFlush : params.onBlockReplyFlush ,
blockReplyBreak : params.blockReplyBreak ,
blockReplyChunking : params.blockReplyChunking ,
onReasoningStream : params.onReasoningStream ,
onToolResult : params.onToolResult ,
onAgentEvent : params.onAgentEvent ,
extraSystemPrompt : params.extraSystemPrompt ,
2026-01-20 07:35:29 +00:00
streamParams : params.streamParams ,
2026-01-14 01:08:15 +00:00
ownerNumbers : params.ownerNumbers ,
enforceFinalTag : params.enforceFinalTag ,
} ) ;
2026-01-14 14:31:43 +00:00
const { aborted , promptError , timedOut , sessionIdUsed , lastAssistant } = attempt ;
2026-01-14 01:08:15 +00:00
if ( promptError && ! aborted ) {
const errorText = describeUnknownError ( promptError ) ;
if ( isContextOverflowError ( errorText ) ) {
const kind = isCompactionFailureError ( errorText )
? "compaction_failure"
: "context_overflow" ;
return {
payloads : [
{
text :
"Context overflow: prompt too large for the model. " +
"Try again with less input or a larger-context model." ,
isError : true ,
} ,
] ,
meta : {
durationMs : Date.now ( ) - started ,
agentMeta : {
sessionId : sessionIdUsed ,
provider ,
model : model.id ,
} ,
2026-01-15 01:06:19 +00:00
systemPromptReport : attempt.systemPromptReport ,
2026-01-14 01:08:15 +00:00
error : { kind , message : errorText } ,
} ,
} ;
}
2026-01-16 03:00:40 +00:00
// Handle role ordering errors with a user-friendly message
if ( /incorrect role information|roles must alternate/i . test ( errorText ) ) {
return {
payloads : [
{
text :
"Message ordering conflict - please try again. " +
"If this persists, use /new to start a fresh session." ,
isError : true ,
} ,
] ,
meta : {
durationMs : Date.now ( ) - started ,
agentMeta : {
sessionId : sessionIdUsed ,
provider ,
model : model.id ,
} ,
2026-01-16 09:03:54 +00:00
systemPromptReport : attempt.systemPromptReport ,
error : { kind : "role_ordering" , message : errorText } ,
2026-01-16 03:00:40 +00:00
} ,
} ;
}
2026-01-14 01:08:15 +00:00
const promptFailoverReason = classifyFailoverReason ( errorText ) ;
2026-01-14 14:31:43 +00:00
if ( promptFailoverReason && promptFailoverReason !== "timeout" && lastProfileId ) {
2026-01-14 01:08:15 +00:00
await markAuthProfileFailure ( {
store : authStore ,
profileId : lastProfileId ,
reason : promptFailoverReason ,
cfg : params.config ,
agentDir : params.agentDir ,
} ) ;
}
if (
isFailoverErrorMessage ( errorText ) &&
promptFailoverReason !== "timeout" &&
( await advanceAuthProfile ( ) )
) {
continue ;
}
const fallbackThinking = pickFallbackThinkingLevel ( {
message : errorText ,
attempted : attemptedThinking ,
} ) ;
if ( fallbackThinking ) {
log . warn (
` unsupported thinking level for ${ provider } / ${ modelId } ; retrying with ${ fallbackThinking } ` ,
) ;
thinkLevel = fallbackThinking ;
continue ;
}
2026-01-18 01:29:48 +00:00
// FIX: Throw FailoverError for prompt errors when fallbacks configured
// This enables model fallback for quota/rate limit errors during prompt submission
const promptFallbackConfigured =
( params . config ? . agents ? . defaults ? . model ? . fallbacks ? . length ? ? 0 ) > 0 ;
if ( promptFallbackConfigured && isFailoverErrorMessage ( errorText ) ) {
throw new FailoverError ( errorText , {
reason : promptFailoverReason ? ? "unknown" ,
provider ,
model : modelId ,
profileId : lastProfileId ,
status : resolveFailoverStatus ( promptFailoverReason ? ? "unknown" ) ,
} ) ;
}
2026-01-14 01:08:15 +00:00
throw promptError ;
}
const fallbackThinking = pickFallbackThinkingLevel ( {
message : lastAssistant?.errorMessage ,
attempted : attemptedThinking ,
} ) ;
if ( fallbackThinking && ! aborted ) {
log . warn (
` unsupported thinking level for ${ provider } / ${ modelId } ; retrying with ${ fallbackThinking } ` ,
) ;
thinkLevel = fallbackThinking ;
continue ;
}
const fallbackConfigured =
2026-01-14 14:31:43 +00:00
( params . config ? . agents ? . defaults ? . model ? . fallbacks ? . length ? ? 0 ) > 0 ;
2026-01-14 01:08:15 +00:00
const authFailure = isAuthAssistantError ( lastAssistant ) ;
const rateLimitFailure = isRateLimitAssistantError ( lastAssistant ) ;
const failoverFailure = isFailoverAssistantError ( lastAssistant ) ;
2026-01-14 14:31:43 +00:00
const assistantFailoverReason = classifyFailoverReason ( lastAssistant ? . errorMessage ? ? "" ) ;
2026-01-14 01:08:15 +00:00
const cloudCodeAssistFormatError = attempt . cloudCodeAssistFormatError ;
2026-01-18 15:19:25 +00:00
const imageDimensionError = parseImageDimensionError ( lastAssistant ? . errorMessage ? ? "" ) ;
if ( imageDimensionError && lastProfileId ) {
const details = [
imageDimensionError . messageIndex !== undefined
? ` message= ${ imageDimensionError . messageIndex } `
: null ,
imageDimensionError . contentIndex !== undefined
? ` content= ${ imageDimensionError . contentIndex } `
: null ,
imageDimensionError . maxDimensionPx !== undefined
? ` limit= ${ imageDimensionError . maxDimensionPx } px `
: null ,
]
. filter ( Boolean )
. join ( " " ) ;
log . warn (
` Profile ${ lastProfileId } rejected image payload ${ details ? ` ( ${ details } ) ` : "" } . ` ,
) ;
}
2026-01-14 01:08:15 +00:00
// Treat timeout as potential rate limit (Antigravity hangs on rate limit)
const shouldRotate = ( ! aborted && failoverFailure ) || timedOut ;
if ( shouldRotate ) {
if ( lastProfileId ) {
const reason =
timedOut || assistantFailoverReason === "timeout"
? "timeout"
: ( assistantFailoverReason ? ? "unknown" ) ;
await markAuthProfileFailure ( {
store : authStore ,
profileId : lastProfileId ,
reason ,
cfg : params.config ,
agentDir : params.agentDir ,
} ) ;
if ( timedOut ) {
log . warn (
` Profile ${ lastProfileId } timed out (possible rate limit). Trying next account... ` ,
) ;
}
if ( cloudCodeAssistFormatError ) {
log . warn (
` Profile ${ lastProfileId } hit Cloud Code Assist format error. Tool calls will be sanitized on retry. ` ,
) ;
}
}
const rotated = await advanceAuthProfile ( ) ;
if ( rotated ) continue ;
if ( fallbackConfigured ) {
2026-01-16 03:00:40 +00:00
// Prefer formatted error message (user-friendly) over raw errorMessage
2026-01-14 01:08:15 +00:00
const message =
( lastAssistant
? formatAssistantErrorText ( lastAssistant , {
cfg : params.config ,
sessionKey : params.sessionKey ? ? params . sessionId ,
} )
2026-01-16 03:00:40 +00:00
: undefined ) ||
lastAssistant ? . errorMessage ? . trim ( ) ||
2026-01-14 01:08:15 +00:00
( timedOut
? "LLM request timed out."
: rateLimitFailure
? "LLM request rate limited."
: authFailure
? "LLM request unauthorized."
: "LLM request failed." ) ;
const status =
resolveFailoverStatus ( assistantFailoverReason ? ? "unknown" ) ? ?
( isTimeoutErrorMessage ( message ) ? 408 : undefined ) ;
throw new FailoverError ( message , {
reason : assistantFailoverReason ? ? "unknown" ,
provider ,
model : modelId ,
profileId : lastProfileId ,
status ,
} ) ;
}
}
const usage = normalizeUsage ( lastAssistant ? . usage as UsageLike ) ;
const agentMeta : EmbeddedPiAgentMeta = {
sessionId : sessionIdUsed ,
provider : lastAssistant?.provider ? ? provider ,
model : lastAssistant?.model ? ? model . id ,
usage ,
} ;
const payloads = buildEmbeddedRunPayloads ( {
assistantTexts : attempt.assistantTexts ,
toolMetas : attempt.toolMetas ,
lastAssistant : attempt.lastAssistant ,
2026-01-18 18:35:03 +05:30
lastToolError : attempt.lastToolError ,
2026-01-14 01:08:15 +00:00
config : params.config ,
sessionKey : params.sessionKey ? ? params . sessionId ,
verboseLevel : params.verboseLevel ,
reasoningLevel : params.reasoningLevel ,
2026-01-17 10:17:57 +00:00
toolResultFormat : resolvedToolResultFormat ,
2026-01-14 14:31:43 +00:00
inlineToolResultsAllowed : ! params . onPartialReply && ! params . onToolResult ,
2026-01-14 01:08:15 +00:00
} ) ;
log . debug (
` embedded run done: runId= ${ params . runId } sessionId= ${ params . sessionId } durationMs= ${ Date . now ( ) - started } aborted= ${ aborted } ` ,
) ;
if ( lastProfileId ) {
await markAuthProfileGood ( {
store : authStore ,
provider ,
profileId : lastProfileId ,
} ) ;
await markAuthProfileUsed ( {
store : authStore ,
profileId : lastProfileId ,
} ) ;
}
return {
payloads : payloads.length ? payloads : undefined ,
meta : {
durationMs : Date.now ( ) - started ,
agentMeta ,
aborted ,
2026-01-15 01:06:19 +00:00
systemPromptReport : attempt.systemPromptReport ,
2026-01-19 12:43:00 +01:00
// Handle client tool calls (OpenResponses hosted tools)
stopReason : attempt.clientToolCall ? "tool_calls" : undefined ,
pendingToolCalls : attempt.clientToolCall
? [
{
id : ` call_ ${ Date . now ( ) } ` ,
name : attempt.clientToolCall.name ,
arguments : JSON.stringify ( attempt . clientToolCall . params ) ,
} ,
]
: undefined ,
2026-01-14 01:08:15 +00:00
} ,
didSendViaMessagingTool : attempt.didSendViaMessagingTool ,
messagingToolSentTexts : attempt.messagingToolSentTexts ,
messagingToolSentTargets : attempt.messagingToolSentTargets ,
} ;
}
} finally {
process . chdir ( prevCwd ) ;
}
} ) ,
) ;
}