@@ -132,6 +132,7 @@ import { AutoApprovalHandler, checkAutoApproval } from "../auto-approval"
132132import { MessageManager } from "../message-manager"
133133import { validateAndFixToolResultIds } from "./validateToolResultIds"
134134import { mergeConsecutiveApiMessages } from "./mergeConsecutiveApiMessages"
135+ import { prepareApiConversationMessage } from "./apiConversationHistory"
135136
136137const MAX_EXPONENTIAL_BACKOFF_SECONDS = 600 // 10 minutes
137138const DEFAULT_USAGE_COLLECTION_TIMEOUT_MS = 5000 // 5 seconds
@@ -861,162 +862,15 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
861862 }
862863
863864 private async addToApiConversationHistory ( message : Anthropic . MessageParam , reasoning ?: string ) {
864- // Capture the encrypted_content / thought signatures from the provider (e.g., OpenAI Responses API, Google GenAI) if present.
865- // We only persist data reported by the current response body.
866- const handler = this . api as ApiHandler & {
867- getResponseId ?: ( ) => string | undefined
868- getEncryptedContent ?: ( ) => { encrypted_content : string ; id ?: string } | undefined
869- getThoughtSignature ?: ( ) => string | undefined
870- getSummary ?: ( ) => any [ ] | undefined
871- getReasoningDetails ?: ( ) => any [ ] | undefined
872- }
873-
874- if ( message . role === "assistant" ) {
875- const responseId = handler . getResponseId ?.( )
876- const reasoningData = handler . getEncryptedContent ?.( )
877- const thoughtSignature = handler . getThoughtSignature ?.( )
878- const reasoningSummary = handler . getSummary ?.( )
879- const reasoningDetails = handler . getReasoningDetails ?.( )
880-
881- // Only Anthropic's API expects/validates the special `thinking` content block signature.
882- // Other providers (notably Gemini 3) use different signature semantics (e.g. `thoughtSignature`)
883- // and require round-tripping the signature in their own format.
884- const modelId = getModelId ( this . apiConfiguration )
885- const apiProvider = this . apiConfiguration . apiProvider
886- const apiProtocol = getApiProtocol (
887- apiProvider && ! isRetiredProvider ( apiProvider ) ? apiProvider : undefined ,
888- modelId ,
889- )
890- const isAnthropicProtocol = apiProtocol === "anthropic"
891-
892- // Start from the original assistant message
893- const messageWithTs : any = {
894- ...message ,
895- ...( responseId ? { id : responseId } : { } ) ,
896- ts : Date . now ( ) ,
897- }
898-
899- // Store reasoning_details array if present (for models like Gemini 3)
900- if ( reasoningDetails ) {
901- messageWithTs . reasoning_details = reasoningDetails
902- }
903-
904- // Store reasoning: Anthropic thinking (with signature), plain text (most providers), or encrypted (OpenAI Native)
905- // Skip if reasoning_details already contains the reasoning (to avoid duplication)
906- if ( isAnthropicProtocol && reasoning && thoughtSignature && ! reasoningDetails ) {
907- // Anthropic provider with extended thinking: Store as proper `thinking` block
908- // This format passes through anthropic-filter.ts and is properly round-tripped
909- // for interleaved thinking with tool use (required by Anthropic API)
910- const thinkingBlock = {
911- type : "thinking" ,
912- thinking : reasoning ,
913- signature : thoughtSignature ,
914- }
915-
916- if ( typeof messageWithTs . content === "string" ) {
917- messageWithTs . content = [
918- thinkingBlock ,
919- { type : "text" , text : messageWithTs . content } satisfies Anthropic . Messages . TextBlockParam ,
920- ]
921- } else if ( Array . isArray ( messageWithTs . content ) ) {
922- messageWithTs . content = [ thinkingBlock , ...messageWithTs . content ]
923- } else if ( ! messageWithTs . content ) {
924- messageWithTs . content = [ thinkingBlock ]
925- }
926- } else if ( reasoning && ! reasoningDetails ) {
927- // Other providers (non-Anthropic): Store as generic reasoning block
928- const reasoningBlock = {
929- type : "reasoning" ,
930- text : reasoning ,
931- summary : reasoningSummary ?? ( [ ] as any [ ] ) ,
932- }
933-
934- if ( typeof messageWithTs . content === "string" ) {
935- messageWithTs . content = [
936- reasoningBlock ,
937- { type : "text" , text : messageWithTs . content } satisfies Anthropic . Messages . TextBlockParam ,
938- ]
939- } else if ( Array . isArray ( messageWithTs . content ) ) {
940- messageWithTs . content = [ reasoningBlock , ...messageWithTs . content ]
941- } else if ( ! messageWithTs . content ) {
942- messageWithTs . content = [ reasoningBlock ]
943- }
944- } else if ( reasoningData ?. encrypted_content ) {
945- // OpenAI Native encrypted reasoning
946- const reasoningBlock = {
947- type : "reasoning" ,
948- summary : [ ] as any [ ] ,
949- encrypted_content : reasoningData . encrypted_content ,
950- ...( reasoningData . id ? { id : reasoningData . id } : { } ) ,
951- }
952-
953- if ( typeof messageWithTs . content === "string" ) {
954- messageWithTs . content = [
955- reasoningBlock ,
956- { type : "text" , text : messageWithTs . content } satisfies Anthropic . Messages . TextBlockParam ,
957- ]
958- } else if ( Array . isArray ( messageWithTs . content ) ) {
959- messageWithTs . content = [ reasoningBlock , ...messageWithTs . content ]
960- } else if ( ! messageWithTs . content ) {
961- messageWithTs . content = [ reasoningBlock ]
962- }
963- }
964-
965- // For non-Anthropic providers (e.g., Gemini 3), persist the thought signature as its own
966- // content block so converters can attach it back to the correct provider-specific fields.
967- // Note: For Anthropic extended thinking, the signature is already included in the thinking block above.
968- if ( thoughtSignature && ! isAnthropicProtocol ) {
969- const thoughtSignatureBlock = {
970- type : "thoughtSignature" ,
971- thoughtSignature,
972- }
973-
974- if ( typeof messageWithTs . content === "string" ) {
975- messageWithTs . content = [
976- { type : "text" , text : messageWithTs . content } satisfies Anthropic . Messages . TextBlockParam ,
977- thoughtSignatureBlock ,
978- ]
979- } else if ( Array . isArray ( messageWithTs . content ) ) {
980- messageWithTs . content = [ ...messageWithTs . content , thoughtSignatureBlock ]
981- } else if ( ! messageWithTs . content ) {
982- messageWithTs . content = [ thoughtSignatureBlock ]
983- }
984- }
985-
986- this . apiConversationHistory . push ( messageWithTs )
987- } else {
988- // For user messages, validate tool_result IDs ONLY when the immediately previous *effective* message
989- // is an assistant message.
990- //
991- // If the previous effective message is also a user message (e.g., summary + a new user message),
992- // validating against any earlier assistant message can incorrectly inject placeholder tool_results.
993- const effectiveHistoryForValidation = getEffectiveApiHistory ( this . apiConversationHistory )
994- const lastEffective = effectiveHistoryForValidation [ effectiveHistoryForValidation . length - 1 ]
995- const historyForValidation = lastEffective ?. role === "assistant" ? effectiveHistoryForValidation : [ ]
996-
997- // If the previous effective message is NOT an assistant, convert tool_result blocks to text blocks.
998- // This prevents orphaned tool_results from being filtered out by getEffectiveApiHistory.
999- // This can happen when condensing occurs after the assistant sends tool_uses but before
1000- // the user responds - the tool_use blocks get condensed away, leaving orphaned tool_results.
1001- let messageToAdd = message
1002- if ( lastEffective ?. role !== "assistant" && Array . isArray ( message . content ) ) {
1003- messageToAdd = {
1004- ...message ,
1005- content : message . content . map ( ( block ) =>
1006- block . type === "tool_result"
1007- ? {
1008- type : "text" as const ,
1009- text : `Tool result:\n${ typeof block . content === "string" ? block . content : JSON . stringify ( block . content ) } ` ,
1010- }
1011- : block ,
1012- ) ,
1013- }
1014- }
1015-
1016- const validatedMessage = validateAndFixToolResultIds ( messageToAdd , historyForValidation )
1017- const messageWithTs = { ...validatedMessage , ts : Date . now ( ) }
1018- this . apiConversationHistory . push ( messageWithTs )
1019- }
865+ this . apiConversationHistory . push (
866+ prepareApiConversationMessage ( {
867+ message,
868+ reasoning,
869+ api : this . api ,
870+ apiConfiguration : this . apiConfiguration ,
871+ apiConversationHistory : this . apiConversationHistory ,
872+ } ) ,
873+ )
1020874
1021875 await this . saveApiConversationHistory ( )
1022876 }
0 commit comments