@code-yeongyu/senpi 2026.5.15-3 → 2026.5.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"speculative.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/compaction/speculative.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentMessage } from \"@earendil-works/pi-agent-core\";\nimport {\n\ttype AssistantMessage,\n\tisContextOverflow,\n\ttype Message,\n\ttype Model,\n\tstream,\n\ttype TextContent,\n} from \"@earendil-works/pi-ai\";\nimport {\n\ttype CompactionPreparation,\n\ttype CompactionResult,\n\tDEFAULT_COMPACTION_SETTINGS,\n\testimateContextTokens,\n\testimateTokens,\n\tprepareCompaction,\n\tserializeConversation,\n} from \"../../../compaction/index.js\";\nimport { convertToLlm } from \"../../../messages.js\";\nimport type { ModelRegistry } from \"../../../model-registry.js\";\nimport type { ReadonlySessionManager } from \"../../../session-manager.js\";\nimport type { ApplyCompactionResult, ContextUsage } from \"../../types.js\";\nimport { computeEffectiveKeepRecentTokens, computeEffectiveThreshold } from \"./policy.js\";\nimport { buildPrompt, type MergedCompactionPromptVariant } from \"./prompts.js\";\nimport * as truncation from \"./tool-truncation.js\";\n\nconst DEFAULT_CONTEXT_WINDOW = 200_000;\nconst COMPACTION_BUDGET_RATIO = 0.6;\nconst COMPACTION_RETRY_BUDGET_RATIO = 0.4;\nconst EMERGENCY_CONTEXT_TARGET_RATIO = 0.95;\nconst MAX_SUMMARY_TOKENS = 8192;\nconst SUMMARY_SCHEMA = \"senpi.compaction.summary.v1\";\ntype CompactionProgressCallback = (delta: string) => void;\n\nexport interface SpeculativeCompactionContext {\n\tmodel: Model<any> | undefined;\n\tsessionManager: ReadonlySessionManager;\n\tmodelRegistry?: ModelRegistry;\n\tgetContextUsage(): ContextUsage | undefined;\n\tgetCompactionSettings?(): CompactionPreparation[\"settings\"];\n\tgetMessageRevision(): number;\n\tapplyCompaction(\n\t\tprecomputed: CompactionResult,\n\t\toptions: { reason: \"extension\"; expectedRevision: number },\n\t): Promise<ApplyCompactionResult>;\n}\n\nexport interface SpeculativeCompactionSnapshot {\n\tgeneration: number;\n\texpectedRevision: number;\n\tmodel: Model<any>;\n\tcontextWindow: number;\n\tpreparation: CompactionPreparation;\n\tpromptVariant: MergedCompactionPromptVariant;\n\tcustomInstructions?: string;\n}\n\nexport type SpeculativeCompactionResult = ApplyCompactionResult | { applied: false; reason: \"unavailable\" };\n\nexport type ExtensionCompactionRequest = {\n\tcustomInstructions?: string;\n\tgeneration: number;\n\tsignal?: AbortSignal;\n};\n\nfunction approxTokens(text: string): number {\n\treturn Math.ceil(text.length / 4);\n}\n\nfunction getSummaryText(message: Message): string {\n\tconst content = Array.isArray(message.content)\n\t\t? message.content\n\t\t: [{ type: \"text\" as const, text: message.content }];\n\treturn content\n\t\t.filter((content): content is TextContent => content.type === \"text\")\n\t\t.map((content) => content.text)\n\t\t.join(\"\\n\")\n\t\t.trim();\n}\n\nfunction isAssistantMessage(message: Message): message is AssistantMessage {\n\treturn message.role === \"assistant\" && \"stopReason\" in message;\n}\n\nasync function generateSummaryMessage(options: {\n\tmessages: AgentMessage[];\n\tonProgress?: CompactionProgressCallback;\n\tprompt: ReturnType<typeof buildPrompt>;\n\tsignal?: AbortSignal;\n\tsnapshot: SpeculativeCompactionSnapshot;\n\tauth: {\n\t\tapiKey: string;\n\t\theaders?: Record<string, string>;\n\t\textraBody?: Record<string, unknown>;\n\t};\n}): Promise<Message | undefined> {\n\tconst conversationText = serializeConversation(convertToLlm(options.messages));\n\tconst responseStream = stream(\n\t\toptions.snapshot.model,\n\t\t{\n\t\t\tsystemPrompt: options.prompt.system,\n\t\t\tmessages: [\n\t\t\t\t{\n\t\t\t\t\trole: \"user\",\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `${options.prompt.user}\\n\\n<conversation>\\n${conversationText}\\n</conversation>`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\t{\n\t\t\tapiKey: options.auth.apiKey,\n\t\t\theaders: options.auth.headers,\n\t\t\textraBody: options.auth.extraBody,\n\t\t\tmaxTokens: MAX_SUMMARY_TOKENS,\n\t\t\tsignal: options.signal,\n\t\t},\n\t);\n\tfor await (const event of responseStream) {\n\t\tif (event.type === \"text_delta\" && event.delta) {\n\t\t\toptions.onProgress?.(event.delta);\n\t\t}\n\t}\n\treturn await responseStream.result();\n}\n\nfunction pruneToolResults(messages: AgentMessage[], contextWindow: number): AgentMessage[] {\n\tconst toolResults = messages\n\t\t.filter((message) => message.role === \"toolResult\")\n\t\t.map((message) => ({ content: message.content, details: undefined }));\n\tif (toolResults.length === 0) return messages;\n\n\tconst prunedResults = truncation.prePruneToolOutputsToBudget(toolResults, contextWindow * COMPACTION_BUDGET_RATIO);\n\tlet resultIndex = 0;\n\treturn messages.map((message) => {\n\t\tif (message.role !== \"toolResult\") return message;\n\t\tconst pruned = prunedResults[resultIndex];\n\t\tresultIndex++;\n\t\treturn pruned ? { ...message, content: pruned.content } : message;\n\t});\n}\n\nexport function truncateContextMessages(messages: AgentMessage[]): AgentMessage[] {\n\tconst toolResults = messages\n\t\t.filter((message) => message.role === \"toolResult\")\n\t\t.map((message) => ({ content: message.content, details: undefined }));\n\tif (toolResults.length === 0) return messages;\n\n\tconst truncatedResults = truncation.truncateOversizedToolResults(toolResults);\n\tlet resultIndex = 0;\n\treturn messages.map((message) => {\n\t\tif (message.role !== \"toolResult\") return message;\n\t\tconst truncated = truncatedResults[resultIndex];\n\t\tresultIndex++;\n\t\treturn truncated ? { ...message, content: truncated.content } : message;\n\t});\n}\n\nfunction getToolCallIds(message: AgentMessage): Set<string> {\n\tconst ids = new Set<string>();\n\tif (message.role !== \"assistant\") return ids;\n\tfor (const block of message.content) {\n\t\tif (block.type === \"toolCall\") ids.add(block.id);\n\t}\n\treturn ids;\n}\n\nfunction findLastUserLikeIndex(messages: AgentMessage[]): number {\n\tfor (let index = messages.length - 1; index >= 0; index--) {\n\t\tconst role = messages[index]?.role;\n\t\tif (role === \"user\" || role === \"bashExecution\") return index;\n\t}\n\treturn messages.length;\n}\n\nfunction removeAssistantToolPair(messages: AgentMessage[], assistantIndex: number): AgentMessage[] {\n\tconst ids = getToolCallIds(messages[assistantIndex]);\n\treturn messages.filter((message, index) => {\n\t\tif (index === assistantIndex) return false;\n\t\treturn message.role !== \"toolResult\" || !ids.has(message.toolCallId);\n\t});\n}\n\nfunction removeFirstOldToolPair(messages: AgentMessage[], boundaryIndex: number): AgentMessage[] | undefined {\n\tfor (let index = 0; index < boundaryIndex; index++) {\n\t\tconst message = messages[index];\n\t\tif (!message) continue;\n\t\tif (message.role === \"assistant\" && getToolCallIds(message).size > 0)\n\t\t\treturn removeAssistantToolPair(messages, index);\n\t\tif (message.role === \"toolResult\") return messages.filter((_message, candidateIndex) => candidateIndex !== index);\n\t}\n\treturn undefined;\n}\n\nfunction removeFirstOldMessage(messages: AgentMessage[], boundaryIndex: number): AgentMessage[] | undefined {\n\tfor (let index = 0; index < boundaryIndex; index++) {\n\t\tconst message = messages[index];\n\t\tif (!message || message.role === \"toolResult\") continue;\n\t\tif (message.role === \"assistant\" && getToolCallIds(message).size > 0)\n\t\t\treturn removeAssistantToolPair(messages, index);\n\t\treturn messages.filter((_candidate, candidateIndex) => candidateIndex !== index);\n\t}\n\treturn undefined;\n}\n\nfunction pruneOldMessagesToBudget(messages: AgentMessage[], targetTokens: number): AgentMessage[] {\n\tlet pruned = messages;\n\twhile (estimateTotalTokens(pruned) > targetTokens) {\n\t\tconst boundaryIndex = findLastUserLikeIndex(pruned);\n\t\tconst next = removeFirstOldToolPair(pruned, boundaryIndex) ?? removeFirstOldMessage(pruned, boundaryIndex);\n\t\tif (!next || next.length === pruned.length) break;\n\t\tpruned = next;\n\t}\n\treturn pruned;\n}\n\nfunction pruneMessagesForOverflowRetry(messages: AgentMessage[], targetTokens: number): AgentMessage[] | undefined {\n\tconst budgetPruned = pruneOldMessagesToBudget(messages, targetTokens);\n\tif (budgetPruned.length < messages.length) return budgetPruned;\n\tconst boundaryIndex = findLastUserLikeIndex(messages);\n\treturn (\n\t\tremoveFirstOldToolPair(messages, boundaryIndex) ??\n\t\tremoveFirstOldMessage(messages, boundaryIndex) ??\n\t\t(messages.length > 1 ? messages.slice(1) : undefined)\n\t);\n}\n\nfunction estimateTotalTokens(messages: AgentMessage[]): number {\n\tlet total = 0;\n\tfor (const message of messages) total += estimateTokens(message);\n\treturn total;\n}\n\nexport function hardLimitEmergencyPrune(\n\tmessages: AgentMessage[],\n\tcontextWindow: number,\n): {\n\tmessages: AgentMessage[];\n\tneedsAggressiveCompaction: boolean;\n} {\n\tconst targetTokens = Math.floor(contextWindow * EMERGENCY_CONTEXT_TARGET_RATIO);\n\tconst noLlmPruned = truncateContextMessages(pruneToolResults(messages, contextWindow));\n\tif (estimateTotalTokens(noLlmPruned) <= targetTokens) {\n\t\treturn { messages: noLlmPruned, needsAggressiveCompaction: false };\n\t}\n\treturn {\n\t\tmessages: pruneOldMessagesToBudget(noLlmPruned, targetTokens),\n\t\tneedsAggressiveCompaction: true,\n\t};\n}\n\nexport function getPromptVariant(options: {\n\treason: string;\n\tpreparation: { previousSummary?: string; isSplitTurn: boolean };\n}): MergedCompactionPromptVariant {\n\tif (options.reason === \"branch\") return \"branch\";\n\tif (options.preparation.previousSummary) return \"update\";\n\tif (options.preparation.isSplitTurn) return \"turn_prefix\";\n\treturn \"default\";\n}\n\nexport function createSpeculativeCompactionSnapshot(\n\tcontext: SpeculativeCompactionContext,\n\toptions: { customInstructions?: string; generation: number },\n): SpeculativeCompactionSnapshot | undefined {\n\tconst model = context.model;\n\tif (!model) return undefined;\n\n\tconst expectedRevision = context.getMessageRevision();\n\tconst branchEntries = context.sessionManager.getBranch();\n\tconst contextWindow = context.getContextUsage()?.contextWindow ?? model.contextWindow ?? DEFAULT_CONTEXT_WINDOW;\n\tconst settings = context.getCompactionSettings?.() ?? DEFAULT_COMPACTION_SETTINGS;\n\tconst thresholdRatio = computeEffectiveThreshold(contextWindow);\n\tconst preparation = prepareCompaction(branchEntries, {\n\t\t...settings,\n\t\tkeepRecentTokens: computeEffectiveKeepRecentTokens(settings.keepRecentTokens, contextWindow, thresholdRatio),\n\t});\n\tif (!preparation) return undefined;\n\n\treturn {\n\t\tgeneration: options.generation,\n\t\texpectedRevision,\n\t\tmodel,\n\t\tcontextWindow,\n\t\tpreparation,\n\t\tpromptVariant: getPromptVariant({ reason: \"extension\", preparation }),\n\t\tcustomInstructions: options.customInstructions,\n\t};\n}\n\nexport async function runExtensionCompaction(\n\tcontext: SpeculativeCompactionContext,\n\tsnapshot: SpeculativeCompactionSnapshot,\n\tsignal?: AbortSignal,\n\tonProgress?: CompactionProgressCallback,\n): Promise<CompactionResult | undefined> {\n\tconst auth = await context.modelRegistry?.getApiKeyAndHeaders(snapshot.model);\n\tif (!auth?.ok || !auth.apiKey) return undefined;\n\n\tlet messages = pruneToolResults(\n\t\t[...snapshot.preparation.messagesToSummarize, ...snapshot.preparation.turnPrefixMessages],\n\t\tsnapshot.contextWindow,\n\t);\n\tconst prompt = buildPrompt({\n\t\tvariant: snapshot.promptVariant,\n\t\tpreviousSummary: snapshot.preparation.previousSummary,\n\t\tcustomInstructions: snapshot.customInstructions,\n\t});\n\n\tfor (let attempt = 0; attempt < 2; attempt++) {\n\t\tconst response = await generateSummaryMessage({\n\t\t\tmessages,\n\t\t\tonProgress,\n\t\t\tprompt,\n\t\t\tsignal,\n\t\t\tsnapshot,\n\t\t\tauth: {\n\t\t\t\tapiKey: auth.apiKey,\n\t\t\t\theaders: auth.headers,\n\t\t\t\textraBody: auth.extraBody,\n\t\t\t},\n\t\t});\n\t\tif (!response) return undefined;\n\n\t\tif (isAssistantMessage(response) && isContextOverflow(response, snapshot.contextWindow)) {\n\t\t\tconst retryMessages = pruneMessagesForOverflowRetry(\n\t\t\t\tmessages,\n\t\t\t\tMath.floor(snapshot.contextWindow * COMPACTION_RETRY_BUDGET_RATIO),\n\t\t\t);\n\t\t\tif (!retryMessages || retryMessages.length === messages.length) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tmessages = retryMessages;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst summary = getSummaryText(response);\n\t\tif (!summary) return undefined;\n\n\t\tconst tokenEstimate = estimateContextTokens(convertToLlm(messages)).tokens + approxTokens(summary);\n\t\tif (tokenEstimate > snapshot.contextWindow * COMPACTION_BUDGET_RATIO) return undefined;\n\n\t\treturn {\n\t\t\tsummary,\n\t\t\tfirstKeptEntryId: snapshot.preparation.firstKeptEntryId,\n\t\t\ttokensBefore: snapshot.preparation.tokensBefore,\n\t\t\tdetails: { schema: SUMMARY_SCHEMA, promptVariant: snapshot.promptVariant, tokenEstimate },\n\t\t};\n\t}\n\n\tthrow new Error(\"Compaction summary request exceeded the context window after retrying with a smaller input\");\n}\n\nexport async function applyGeneratedCompaction(\n\tcontext: SpeculativeCompactionContext,\n\tsnapshot: SpeculativeCompactionSnapshot | undefined,\n\tgetCurrentGeneration: () => number,\n\tcompaction: CompactionResult | undefined,\n): Promise<SpeculativeCompactionResult> {\n\tif (!snapshot || !compaction) return { applied: false, reason: \"unavailable\" };\n\n\tif (snapshot.generation !== getCurrentGeneration() || snapshot.expectedRevision !== context.getMessageRevision()) {\n\t\treturn { applied: false, reason: \"stale\" };\n\t}\n\n\treturn await context.applyCompaction(compaction, {\n\t\treason: \"extension\",\n\t\texpectedRevision: snapshot.expectedRevision,\n\t});\n}\n\nexport function snapshotExtensionCompaction(\n\tcontext: SpeculativeCompactionContext,\n\trequest: ExtensionCompactionRequest,\n): SpeculativeCompactionSnapshot | undefined {\n\treturn createSpeculativeCompactionSnapshot(context, request);\n}\n\nexport async function applySpeculativeCompaction(\n\tcontext: SpeculativeCompactionContext,\n\tsnapshot: SpeculativeCompactionSnapshot | undefined,\n\tgetCurrentGeneration: () => number,\n\tgenerate: () => Promise<CompactionResult | undefined>,\n): Promise<SpeculativeCompactionResult> {\n\tif (!snapshot) return { applied: false, reason: \"unavailable\" };\n\n\tconst compaction = await generate();\n\treturn await applyGeneratedCompaction(context, snapshot, getCurrentGeneration, compaction);\n}\n"]}
1
+ {"version":3,"file":"speculative.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/compaction/speculative.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentMessage } from \"@earendil-works/pi-agent-core\";\nimport {\n\ttype AssistantMessage,\n\tisContextOverflow,\n\ttype Message,\n\ttype Model,\n\tstream,\n\ttype TextContent,\n} from \"@earendil-works/pi-ai\";\nimport {\n\ttype CompactionPreparation,\n\ttype CompactionResult,\n\tDEFAULT_COMPACTION_SETTINGS,\n\testimateContextTokens,\n\testimateTokens,\n\tprepareCompaction,\n\tserializeConversation,\n} from \"../../../compaction/index.js\";\nimport { convertToLlm } from \"../../../messages.js\";\nimport type { ModelRegistry } from \"../../../model-registry.js\";\nimport type { ReadonlySessionManager } from \"../../../session-manager.js\";\nimport type { ApplyCompactionResult, ContextUsage } from \"../../types.js\";\nimport { computeEffectiveKeepRecentTokens, computeEffectiveThreshold } from \"./policy.js\";\nimport { buildPrompt, type MergedCompactionPromptVariant } from \"./prompts.js\";\nimport * as truncation from \"./tool-truncation.js\";\n\nconst DEFAULT_CONTEXT_WINDOW = 200_000;\nconst COMPACTION_BUDGET_RATIO = 0.6;\nconst EMERGENCY_CONTEXT_TARGET_RATIO = 0.95;\nconst MAX_SUMMARY_TOKENS = 8192;\nconst SUMMARY_SCHEMA = \"senpi.compaction.summary.v1\";\ntype CompactionProgressCallback = (delta: string) => void;\n\nexport interface SpeculativeCompactionContext {\n\tmodel: Model<any> | undefined;\n\tsessionManager: ReadonlySessionManager;\n\tmodelRegistry?: ModelRegistry;\n\tgetContextUsage(): ContextUsage | undefined;\n\tgetCompactionSettings?(): CompactionPreparation[\"settings\"];\n\tgetMessageRevision(): number;\n\tapplyCompaction(\n\t\tprecomputed: CompactionResult,\n\t\toptions: { reason: \"extension\"; expectedRevision: number },\n\t): Promise<ApplyCompactionResult>;\n}\n\nexport interface SpeculativeCompactionSnapshot {\n\tgeneration: number;\n\texpectedRevision: number;\n\tmodel: Model<any>;\n\tcontextWindow: number;\n\tpreparation: CompactionPreparation;\n\tpromptVariant: MergedCompactionPromptVariant;\n\tcustomInstructions?: string;\n}\n\nexport type SpeculativeCompactionResult = ApplyCompactionResult | { applied: false; reason: \"unavailable\" };\n\nexport type ExtensionCompactionRequest = {\n\tcustomInstructions?: string;\n\tgeneration: number;\n\tsignal?: AbortSignal;\n};\n\nfunction approxTokens(text: string): number {\n\treturn Math.ceil(text.length / 4);\n}\n\nfunction getSummaryText(message: Message): string {\n\tconst content = Array.isArray(message.content)\n\t\t? message.content\n\t\t: [{ type: \"text\" as const, text: message.content }];\n\treturn content\n\t\t.filter((content): content is TextContent => content.type === \"text\")\n\t\t.map((content) => content.text)\n\t\t.join(\"\\n\")\n\t\t.trim();\n}\n\nfunction isAssistantMessage(message: Message): message is AssistantMessage {\n\treturn message.role === \"assistant\" && \"stopReason\" in message;\n}\n\nasync function generateSummaryMessage(options: {\n\tmessages: AgentMessage[];\n\tonProgress?: CompactionProgressCallback;\n\tprompt: ReturnType<typeof buildPrompt>;\n\tsignal?: AbortSignal;\n\tsnapshot: SpeculativeCompactionSnapshot;\n\tauth: {\n\t\tapiKey: string;\n\t\theaders?: Record<string, string>;\n\t\textraBody?: Record<string, unknown>;\n\t};\n}): Promise<Message | undefined> {\n\tconst conversationText = serializeConversation(convertToLlm(options.messages));\n\tconst responseStream = stream(\n\t\toptions.snapshot.model,\n\t\t{\n\t\t\tsystemPrompt: options.prompt.system,\n\t\t\tmessages: [\n\t\t\t\t{\n\t\t\t\t\trole: \"user\",\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `${options.prompt.user}\\n\\n<conversation>\\n${conversationText}\\n</conversation>`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\t{\n\t\t\tapiKey: options.auth.apiKey,\n\t\t\theaders: options.auth.headers,\n\t\t\textraBody: options.auth.extraBody,\n\t\t\tmaxTokens: MAX_SUMMARY_TOKENS,\n\t\t\tsignal: options.signal,\n\t\t},\n\t);\n\tfor await (const event of responseStream) {\n\t\tif (event.type === \"text_delta\" && event.delta) {\n\t\t\toptions.onProgress?.(event.delta);\n\t\t}\n\t}\n\treturn await responseStream.result();\n}\n\nfunction pruneToolResults(messages: AgentMessage[], contextWindow: number): AgentMessage[] {\n\tconst toolResults = messages\n\t\t.filter((message) => message.role === \"toolResult\")\n\t\t.map((message) => ({ content: message.content, details: undefined }));\n\tif (toolResults.length === 0) return messages;\n\n\tconst prunedResults = truncation.prePruneToolOutputsToBudget(toolResults, contextWindow * COMPACTION_BUDGET_RATIO);\n\tlet resultIndex = 0;\n\treturn messages.map((message) => {\n\t\tif (message.role !== \"toolResult\") return message;\n\t\tconst pruned = prunedResults[resultIndex];\n\t\tresultIndex++;\n\t\treturn pruned ? { ...message, content: pruned.content } : message;\n\t});\n}\n\nexport function truncateContextMessages(messages: AgentMessage[]): AgentMessage[] {\n\tconst toolResults = messages\n\t\t.filter((message) => message.role === \"toolResult\")\n\t\t.map((message) => ({ content: message.content, details: undefined }));\n\tif (toolResults.length === 0) return messages;\n\n\tconst truncatedResults = truncation.truncateOversizedToolResults(toolResults);\n\tlet resultIndex = 0;\n\treturn messages.map((message) => {\n\t\tif (message.role !== \"toolResult\") return message;\n\t\tconst truncated = truncatedResults[resultIndex];\n\t\tresultIndex++;\n\t\treturn truncated ? { ...message, content: truncated.content } : message;\n\t});\n}\n\nfunction getToolCallIds(message: AgentMessage): Set<string> {\n\tconst ids = new Set<string>();\n\tif (message.role !== \"assistant\") return ids;\n\tfor (const block of message.content) {\n\t\tif (block.type === \"toolCall\") ids.add(block.id);\n\t}\n\treturn ids;\n}\n\nfunction findLastUserLikeIndex(messages: AgentMessage[]): number {\n\tfor (let index = messages.length - 1; index >= 0; index--) {\n\t\tconst role = messages[index]?.role;\n\t\tif (role === \"user\" || role === \"bashExecution\") return index;\n\t}\n\treturn messages.length;\n}\n\nfunction removeAssistantToolPair(messages: AgentMessage[], assistantIndex: number): AgentMessage[] {\n\tconst ids = getToolCallIds(messages[assistantIndex]);\n\treturn messages.filter((message, index) => {\n\t\tif (index === assistantIndex) return false;\n\t\treturn message.role !== \"toolResult\" || !ids.has(message.toolCallId);\n\t});\n}\n\nfunction removeFirstOldToolPair(messages: AgentMessage[], boundaryIndex: number): AgentMessage[] | undefined {\n\tfor (let index = 0; index < boundaryIndex; index++) {\n\t\tconst message = messages[index];\n\t\tif (!message) continue;\n\t\tif (message.role === \"assistant\" && getToolCallIds(message).size > 0)\n\t\t\treturn removeAssistantToolPair(messages, index);\n\t\tif (message.role === \"toolResult\") return messages.filter((_message, candidateIndex) => candidateIndex !== index);\n\t}\n\treturn undefined;\n}\n\nfunction removeFirstOldMessage(messages: AgentMessage[], boundaryIndex: number): AgentMessage[] | undefined {\n\tfor (let index = 0; index < boundaryIndex; index++) {\n\t\tconst message = messages[index];\n\t\tif (!message || message.role === \"toolResult\") continue;\n\t\tif (message.role === \"assistant\" && getToolCallIds(message).size > 0)\n\t\t\treturn removeAssistantToolPair(messages, index);\n\t\treturn messages.filter((_candidate, candidateIndex) => candidateIndex !== index);\n\t}\n\treturn undefined;\n}\n\nfunction pruneOldMessagesToBudget(messages: AgentMessage[], targetTokens: number): AgentMessage[] {\n\tlet pruned = messages;\n\twhile (estimateTotalTokens(pruned) > targetTokens) {\n\t\tconst boundaryIndex = findLastUserLikeIndex(pruned);\n\t\tconst next = removeFirstOldToolPair(pruned, boundaryIndex) ?? removeFirstOldMessage(pruned, boundaryIndex);\n\t\tif (!next || next.length === pruned.length) break;\n\t\tpruned = next;\n\t}\n\treturn pruned;\n}\n\nfunction removeOldestHistoryItemForOverflowRetry(messages: AgentMessage[]): AgentMessage[] | undefined {\n\tif (messages.length <= 1) return undefined;\n\tconst boundaryIndex = findLastUserLikeIndex(messages);\n\treturn (\n\t\tremoveFirstOldToolPair(messages, boundaryIndex) ??\n\t\tremoveFirstOldMessage(messages, boundaryIndex) ??\n\t\t(messages.length > 1 ? messages.slice(1) : undefined)\n\t);\n}\n\nfunction estimateTotalTokens(messages: AgentMessage[]): number {\n\tlet total = 0;\n\tfor (const message of messages) total += estimateTokens(message);\n\treturn total;\n}\n\nexport function hardLimitEmergencyPrune(\n\tmessages: AgentMessage[],\n\tcontextWindow: number,\n): {\n\tmessages: AgentMessage[];\n\tneedsAggressiveCompaction: boolean;\n} {\n\tconst targetTokens = Math.floor(contextWindow * EMERGENCY_CONTEXT_TARGET_RATIO);\n\tconst noLlmPruned = truncateContextMessages(pruneToolResults(messages, contextWindow));\n\tif (estimateTotalTokens(noLlmPruned) <= targetTokens) {\n\t\treturn { messages: noLlmPruned, needsAggressiveCompaction: false };\n\t}\n\treturn {\n\t\tmessages: pruneOldMessagesToBudget(noLlmPruned, targetTokens),\n\t\tneedsAggressiveCompaction: true,\n\t};\n}\n\nexport function getPromptVariant(options: {\n\treason: string;\n\tpreparation: { previousSummary?: string; isSplitTurn: boolean };\n}): MergedCompactionPromptVariant {\n\tif (options.reason === \"branch\") return \"branch\";\n\tif (options.preparation.previousSummary) return \"update\";\n\tif (options.preparation.isSplitTurn) return \"turn_prefix\";\n\treturn \"default\";\n}\n\nexport function createSpeculativeCompactionSnapshot(\n\tcontext: SpeculativeCompactionContext,\n\toptions: { customInstructions?: string; generation: number },\n): SpeculativeCompactionSnapshot | undefined {\n\tconst model = context.model;\n\tif (!model) return undefined;\n\n\tconst expectedRevision = context.getMessageRevision();\n\tconst branchEntries = context.sessionManager.getBranch();\n\tconst contextWindow = context.getContextUsage()?.contextWindow ?? model.contextWindow ?? DEFAULT_CONTEXT_WINDOW;\n\tconst settings = context.getCompactionSettings?.() ?? DEFAULT_COMPACTION_SETTINGS;\n\tconst thresholdRatio = computeEffectiveThreshold(contextWindow);\n\tconst preparation = prepareCompaction(branchEntries, {\n\t\t...settings,\n\t\tkeepRecentTokens: computeEffectiveKeepRecentTokens(settings.keepRecentTokens, contextWindow, thresholdRatio),\n\t});\n\tif (!preparation) return undefined;\n\n\treturn {\n\t\tgeneration: options.generation,\n\t\texpectedRevision,\n\t\tmodel,\n\t\tcontextWindow,\n\t\tpreparation,\n\t\tpromptVariant: getPromptVariant({ reason: \"extension\", preparation }),\n\t\tcustomInstructions: options.customInstructions,\n\t};\n}\n\nexport async function runExtensionCompaction(\n\tcontext: SpeculativeCompactionContext,\n\tsnapshot: SpeculativeCompactionSnapshot,\n\tsignal?: AbortSignal,\n\tonProgress?: CompactionProgressCallback,\n): Promise<CompactionResult | undefined> {\n\tconst auth = await context.modelRegistry?.getApiKeyAndHeaders(snapshot.model);\n\tif (!auth?.ok || !auth.apiKey) return undefined;\n\n\tlet messages = pruneToolResults(\n\t\t[...snapshot.preparation.messagesToSummarize, ...snapshot.preparation.turnPrefixMessages],\n\t\tsnapshot.contextWindow,\n\t);\n\tconst prompt = buildPrompt({\n\t\tvariant: snapshot.promptVariant,\n\t\tpreviousSummary: snapshot.preparation.previousSummary,\n\t\tcustomInstructions: snapshot.customInstructions,\n\t});\n\n\twhile (true) {\n\t\tconst response = await generateSummaryMessage({\n\t\t\tmessages,\n\t\t\tonProgress,\n\t\t\tprompt,\n\t\t\tsignal,\n\t\t\tsnapshot,\n\t\t\tauth: {\n\t\t\t\tapiKey: auth.apiKey,\n\t\t\t\theaders: auth.headers,\n\t\t\t\textraBody: auth.extraBody,\n\t\t\t},\n\t\t});\n\t\tif (!response) return undefined;\n\n\t\tif (isAssistantMessage(response) && isContextOverflow(response, snapshot.contextWindow)) {\n\t\t\tconst retryMessages = removeOldestHistoryItemForOverflowRetry(messages);\n\t\t\tif (!retryMessages || retryMessages.length === messages.length) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tmessages = retryMessages;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst summary = getSummaryText(response);\n\t\tif (!summary) return undefined;\n\n\t\tconst tokenEstimate = estimateContextTokens(convertToLlm(messages)).tokens + approxTokens(summary);\n\t\tif (tokenEstimate > snapshot.contextWindow * COMPACTION_BUDGET_RATIO) return undefined;\n\n\t\treturn {\n\t\t\tsummary,\n\t\t\tfirstKeptEntryId: snapshot.preparation.firstKeptEntryId,\n\t\t\ttokensBefore: snapshot.preparation.tokensBefore,\n\t\t\tdetails: { schema: SUMMARY_SCHEMA, promptVariant: snapshot.promptVariant, tokenEstimate },\n\t\t};\n\t}\n\n\tthrow new Error(\"Compaction summary request exceeded the context window after retrying with a smaller input\");\n}\n\nexport async function applyGeneratedCompaction(\n\tcontext: SpeculativeCompactionContext,\n\tsnapshot: SpeculativeCompactionSnapshot | undefined,\n\tgetCurrentGeneration: () => number,\n\tcompaction: CompactionResult | undefined,\n): Promise<SpeculativeCompactionResult> {\n\tif (!snapshot || !compaction) return { applied: false, reason: \"unavailable\" };\n\n\tif (snapshot.generation !== getCurrentGeneration() || snapshot.expectedRevision !== context.getMessageRevision()) {\n\t\treturn { applied: false, reason: \"stale\" };\n\t}\n\n\treturn await context.applyCompaction(compaction, {\n\t\treason: \"extension\",\n\t\texpectedRevision: snapshot.expectedRevision,\n\t});\n}\n\nexport function snapshotExtensionCompaction(\n\tcontext: SpeculativeCompactionContext,\n\trequest: ExtensionCompactionRequest,\n): SpeculativeCompactionSnapshot | undefined {\n\treturn createSpeculativeCompactionSnapshot(context, request);\n}\n\nexport async function applySpeculativeCompaction(\n\tcontext: SpeculativeCompactionContext,\n\tsnapshot: SpeculativeCompactionSnapshot | undefined,\n\tgetCurrentGeneration: () => number,\n\tgenerate: () => Promise<CompactionResult | undefined>,\n): Promise<SpeculativeCompactionResult> {\n\tif (!snapshot) return { applied: false, reason: \"unavailable\" };\n\n\tconst compaction = await generate();\n\treturn await applyGeneratedCompaction(context, snapshot, getCurrentGeneration, compaction);\n}\n"]}
@@ -6,7 +6,6 @@ import { buildPrompt } from "./prompts.js";
6
6
  import * as truncation from "./tool-truncation.js";
7
7
  const DEFAULT_CONTEXT_WINDOW = 200_000;
8
8
  const COMPACTION_BUDGET_RATIO = 0.6;
9
- const COMPACTION_RETRY_BUDGET_RATIO = 0.4;
10
9
  const EMERGENCY_CONTEXT_TARGET_RATIO = 0.95;
11
10
  const MAX_SUMMARY_TOKENS = 8192;
12
11
  const SUMMARY_SCHEMA = "senpi.compaction.summary.v1";
@@ -148,10 +147,9 @@ function pruneOldMessagesToBudget(messages, targetTokens) {
148
147
  }
149
148
  return pruned;
150
149
  }
151
- function pruneMessagesForOverflowRetry(messages, targetTokens) {
152
- const budgetPruned = pruneOldMessagesToBudget(messages, targetTokens);
153
- if (budgetPruned.length < messages.length)
154
- return budgetPruned;
150
+ function removeOldestHistoryItemForOverflowRetry(messages) {
151
+ if (messages.length <= 1)
152
+ return undefined;
155
153
  const boundaryIndex = findLastUserLikeIndex(messages);
156
154
  return (removeFirstOldToolPair(messages, boundaryIndex) ??
157
155
  removeFirstOldMessage(messages, boundaryIndex) ??
@@ -218,7 +216,7 @@ export async function runExtensionCompaction(context, snapshot, signal, onProgre
218
216
  previousSummary: snapshot.preparation.previousSummary,
219
217
  customInstructions: snapshot.customInstructions,
220
218
  });
221
- for (let attempt = 0; attempt < 2; attempt++) {
219
+ while (true) {
222
220
  const response = await generateSummaryMessage({
223
221
  messages,
224
222
  onProgress,
@@ -234,7 +232,7 @@ export async function runExtensionCompaction(context, snapshot, signal, onProgre
234
232
  if (!response)
235
233
  return undefined;
236
234
  if (isAssistantMessage(response) && isContextOverflow(response, snapshot.contextWindow)) {
237
- const retryMessages = pruneMessagesForOverflowRetry(messages, Math.floor(snapshot.contextWindow * COMPACTION_RETRY_BUDGET_RATIO));
235
+ const retryMessages = removeOldestHistoryItemForOverflowRetry(messages);
238
236
  if (!retryMessages || retryMessages.length === messages.length) {
239
237
  break;
240
238
  }
@@ -1 +1 @@
1
- {"version":3,"file":"speculative.js","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/compaction/speculative.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentMessage } from \"@earendil-works/pi-agent-core\";\nimport {\n\ttype AssistantMessage,\n\tisContextOverflow,\n\ttype Message,\n\ttype Model,\n\tstream,\n\ttype TextContent,\n} from \"@earendil-works/pi-ai\";\nimport {\n\ttype CompactionPreparation,\n\ttype CompactionResult,\n\tDEFAULT_COMPACTION_SETTINGS,\n\testimateContextTokens,\n\testimateTokens,\n\tprepareCompaction,\n\tserializeConversation,\n} from \"../../../compaction/index.js\";\nimport { convertToLlm } from \"../../../messages.js\";\nimport type { ModelRegistry } from \"../../../model-registry.js\";\nimport type { ReadonlySessionManager } from \"../../../session-manager.js\";\nimport type { ApplyCompactionResult, ContextUsage } from \"../../types.js\";\nimport { computeEffectiveKeepRecentTokens, computeEffectiveThreshold } from \"./policy.js\";\nimport { buildPrompt, type MergedCompactionPromptVariant } from \"./prompts.js\";\nimport * as truncation from \"./tool-truncation.js\";\n\nconst DEFAULT_CONTEXT_WINDOW = 200_000;\nconst COMPACTION_BUDGET_RATIO = 0.6;\nconst COMPACTION_RETRY_BUDGET_RATIO = 0.4;\nconst EMERGENCY_CONTEXT_TARGET_RATIO = 0.95;\nconst MAX_SUMMARY_TOKENS = 8192;\nconst SUMMARY_SCHEMA = \"senpi.compaction.summary.v1\";\ntype CompactionProgressCallback = (delta: string) => void;\n\nexport interface SpeculativeCompactionContext {\n\tmodel: Model<any> | undefined;\n\tsessionManager: ReadonlySessionManager;\n\tmodelRegistry?: ModelRegistry;\n\tgetContextUsage(): ContextUsage | undefined;\n\tgetCompactionSettings?(): CompactionPreparation[\"settings\"];\n\tgetMessageRevision(): number;\n\tapplyCompaction(\n\t\tprecomputed: CompactionResult,\n\t\toptions: { reason: \"extension\"; expectedRevision: number },\n\t): Promise<ApplyCompactionResult>;\n}\n\nexport interface SpeculativeCompactionSnapshot {\n\tgeneration: number;\n\texpectedRevision: number;\n\tmodel: Model<any>;\n\tcontextWindow: number;\n\tpreparation: CompactionPreparation;\n\tpromptVariant: MergedCompactionPromptVariant;\n\tcustomInstructions?: string;\n}\n\nexport type SpeculativeCompactionResult = ApplyCompactionResult | { applied: false; reason: \"unavailable\" };\n\nexport type ExtensionCompactionRequest = {\n\tcustomInstructions?: string;\n\tgeneration: number;\n\tsignal?: AbortSignal;\n};\n\nfunction approxTokens(text: string): number {\n\treturn Math.ceil(text.length / 4);\n}\n\nfunction getSummaryText(message: Message): string {\n\tconst content = Array.isArray(message.content)\n\t\t? message.content\n\t\t: [{ type: \"text\" as const, text: message.content }];\n\treturn content\n\t\t.filter((content): content is TextContent => content.type === \"text\")\n\t\t.map((content) => content.text)\n\t\t.join(\"\\n\")\n\t\t.trim();\n}\n\nfunction isAssistantMessage(message: Message): message is AssistantMessage {\n\treturn message.role === \"assistant\" && \"stopReason\" in message;\n}\n\nasync function generateSummaryMessage(options: {\n\tmessages: AgentMessage[];\n\tonProgress?: CompactionProgressCallback;\n\tprompt: ReturnType<typeof buildPrompt>;\n\tsignal?: AbortSignal;\n\tsnapshot: SpeculativeCompactionSnapshot;\n\tauth: {\n\t\tapiKey: string;\n\t\theaders?: Record<string, string>;\n\t\textraBody?: Record<string, unknown>;\n\t};\n}): Promise<Message | undefined> {\n\tconst conversationText = serializeConversation(convertToLlm(options.messages));\n\tconst responseStream = stream(\n\t\toptions.snapshot.model,\n\t\t{\n\t\t\tsystemPrompt: options.prompt.system,\n\t\t\tmessages: [\n\t\t\t\t{\n\t\t\t\t\trole: \"user\",\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `${options.prompt.user}\\n\\n<conversation>\\n${conversationText}\\n</conversation>`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\t{\n\t\t\tapiKey: options.auth.apiKey,\n\t\t\theaders: options.auth.headers,\n\t\t\textraBody: options.auth.extraBody,\n\t\t\tmaxTokens: MAX_SUMMARY_TOKENS,\n\t\t\tsignal: options.signal,\n\t\t},\n\t);\n\tfor await (const event of responseStream) {\n\t\tif (event.type === \"text_delta\" && event.delta) {\n\t\t\toptions.onProgress?.(event.delta);\n\t\t}\n\t}\n\treturn await responseStream.result();\n}\n\nfunction pruneToolResults(messages: AgentMessage[], contextWindow: number): AgentMessage[] {\n\tconst toolResults = messages\n\t\t.filter((message) => message.role === \"toolResult\")\n\t\t.map((message) => ({ content: message.content, details: undefined }));\n\tif (toolResults.length === 0) return messages;\n\n\tconst prunedResults = truncation.prePruneToolOutputsToBudget(toolResults, contextWindow * COMPACTION_BUDGET_RATIO);\n\tlet resultIndex = 0;\n\treturn messages.map((message) => {\n\t\tif (message.role !== \"toolResult\") return message;\n\t\tconst pruned = prunedResults[resultIndex];\n\t\tresultIndex++;\n\t\treturn pruned ? { ...message, content: pruned.content } : message;\n\t});\n}\n\nexport function truncateContextMessages(messages: AgentMessage[]): AgentMessage[] {\n\tconst toolResults = messages\n\t\t.filter((message) => message.role === \"toolResult\")\n\t\t.map((message) => ({ content: message.content, details: undefined }));\n\tif (toolResults.length === 0) return messages;\n\n\tconst truncatedResults = truncation.truncateOversizedToolResults(toolResults);\n\tlet resultIndex = 0;\n\treturn messages.map((message) => {\n\t\tif (message.role !== \"toolResult\") return message;\n\t\tconst truncated = truncatedResults[resultIndex];\n\t\tresultIndex++;\n\t\treturn truncated ? { ...message, content: truncated.content } : message;\n\t});\n}\n\nfunction getToolCallIds(message: AgentMessage): Set<string> {\n\tconst ids = new Set<string>();\n\tif (message.role !== \"assistant\") return ids;\n\tfor (const block of message.content) {\n\t\tif (block.type === \"toolCall\") ids.add(block.id);\n\t}\n\treturn ids;\n}\n\nfunction findLastUserLikeIndex(messages: AgentMessage[]): number {\n\tfor (let index = messages.length - 1; index >= 0; index--) {\n\t\tconst role = messages[index]?.role;\n\t\tif (role === \"user\" || role === \"bashExecution\") return index;\n\t}\n\treturn messages.length;\n}\n\nfunction removeAssistantToolPair(messages: AgentMessage[], assistantIndex: number): AgentMessage[] {\n\tconst ids = getToolCallIds(messages[assistantIndex]);\n\treturn messages.filter((message, index) => {\n\t\tif (index === assistantIndex) return false;\n\t\treturn message.role !== \"toolResult\" || !ids.has(message.toolCallId);\n\t});\n}\n\nfunction removeFirstOldToolPair(messages: AgentMessage[], boundaryIndex: number): AgentMessage[] | undefined {\n\tfor (let index = 0; index < boundaryIndex; index++) {\n\t\tconst message = messages[index];\n\t\tif (!message) continue;\n\t\tif (message.role === \"assistant\" && getToolCallIds(message).size > 0)\n\t\t\treturn removeAssistantToolPair(messages, index);\n\t\tif (message.role === \"toolResult\") return messages.filter((_message, candidateIndex) => candidateIndex !== index);\n\t}\n\treturn undefined;\n}\n\nfunction removeFirstOldMessage(messages: AgentMessage[], boundaryIndex: number): AgentMessage[] | undefined {\n\tfor (let index = 0; index < boundaryIndex; index++) {\n\t\tconst message = messages[index];\n\t\tif (!message || message.role === \"toolResult\") continue;\n\t\tif (message.role === \"assistant\" && getToolCallIds(message).size > 0)\n\t\t\treturn removeAssistantToolPair(messages, index);\n\t\treturn messages.filter((_candidate, candidateIndex) => candidateIndex !== index);\n\t}\n\treturn undefined;\n}\n\nfunction pruneOldMessagesToBudget(messages: AgentMessage[], targetTokens: number): AgentMessage[] {\n\tlet pruned = messages;\n\twhile (estimateTotalTokens(pruned) > targetTokens) {\n\t\tconst boundaryIndex = findLastUserLikeIndex(pruned);\n\t\tconst next = removeFirstOldToolPair(pruned, boundaryIndex) ?? removeFirstOldMessage(pruned, boundaryIndex);\n\t\tif (!next || next.length === pruned.length) break;\n\t\tpruned = next;\n\t}\n\treturn pruned;\n}\n\nfunction pruneMessagesForOverflowRetry(messages: AgentMessage[], targetTokens: number): AgentMessage[] | undefined {\n\tconst budgetPruned = pruneOldMessagesToBudget(messages, targetTokens);\n\tif (budgetPruned.length < messages.length) return budgetPruned;\n\tconst boundaryIndex = findLastUserLikeIndex(messages);\n\treturn (\n\t\tremoveFirstOldToolPair(messages, boundaryIndex) ??\n\t\tremoveFirstOldMessage(messages, boundaryIndex) ??\n\t\t(messages.length > 1 ? messages.slice(1) : undefined)\n\t);\n}\n\nfunction estimateTotalTokens(messages: AgentMessage[]): number {\n\tlet total = 0;\n\tfor (const message of messages) total += estimateTokens(message);\n\treturn total;\n}\n\nexport function hardLimitEmergencyPrune(\n\tmessages: AgentMessage[],\n\tcontextWindow: number,\n): {\n\tmessages: AgentMessage[];\n\tneedsAggressiveCompaction: boolean;\n} {\n\tconst targetTokens = Math.floor(contextWindow * EMERGENCY_CONTEXT_TARGET_RATIO);\n\tconst noLlmPruned = truncateContextMessages(pruneToolResults(messages, contextWindow));\n\tif (estimateTotalTokens(noLlmPruned) <= targetTokens) {\n\t\treturn { messages: noLlmPruned, needsAggressiveCompaction: false };\n\t}\n\treturn {\n\t\tmessages: pruneOldMessagesToBudget(noLlmPruned, targetTokens),\n\t\tneedsAggressiveCompaction: true,\n\t};\n}\n\nexport function getPromptVariant(options: {\n\treason: string;\n\tpreparation: { previousSummary?: string; isSplitTurn: boolean };\n}): MergedCompactionPromptVariant {\n\tif (options.reason === \"branch\") return \"branch\";\n\tif (options.preparation.previousSummary) return \"update\";\n\tif (options.preparation.isSplitTurn) return \"turn_prefix\";\n\treturn \"default\";\n}\n\nexport function createSpeculativeCompactionSnapshot(\n\tcontext: SpeculativeCompactionContext,\n\toptions: { customInstructions?: string; generation: number },\n): SpeculativeCompactionSnapshot | undefined {\n\tconst model = context.model;\n\tif (!model) return undefined;\n\n\tconst expectedRevision = context.getMessageRevision();\n\tconst branchEntries = context.sessionManager.getBranch();\n\tconst contextWindow = context.getContextUsage()?.contextWindow ?? model.contextWindow ?? DEFAULT_CONTEXT_WINDOW;\n\tconst settings = context.getCompactionSettings?.() ?? DEFAULT_COMPACTION_SETTINGS;\n\tconst thresholdRatio = computeEffectiveThreshold(contextWindow);\n\tconst preparation = prepareCompaction(branchEntries, {\n\t\t...settings,\n\t\tkeepRecentTokens: computeEffectiveKeepRecentTokens(settings.keepRecentTokens, contextWindow, thresholdRatio),\n\t});\n\tif (!preparation) return undefined;\n\n\treturn {\n\t\tgeneration: options.generation,\n\t\texpectedRevision,\n\t\tmodel,\n\t\tcontextWindow,\n\t\tpreparation,\n\t\tpromptVariant: getPromptVariant({ reason: \"extension\", preparation }),\n\t\tcustomInstructions: options.customInstructions,\n\t};\n}\n\nexport async function runExtensionCompaction(\n\tcontext: SpeculativeCompactionContext,\n\tsnapshot: SpeculativeCompactionSnapshot,\n\tsignal?: AbortSignal,\n\tonProgress?: CompactionProgressCallback,\n): Promise<CompactionResult | undefined> {\n\tconst auth = await context.modelRegistry?.getApiKeyAndHeaders(snapshot.model);\n\tif (!auth?.ok || !auth.apiKey) return undefined;\n\n\tlet messages = pruneToolResults(\n\t\t[...snapshot.preparation.messagesToSummarize, ...snapshot.preparation.turnPrefixMessages],\n\t\tsnapshot.contextWindow,\n\t);\n\tconst prompt = buildPrompt({\n\t\tvariant: snapshot.promptVariant,\n\t\tpreviousSummary: snapshot.preparation.previousSummary,\n\t\tcustomInstructions: snapshot.customInstructions,\n\t});\n\n\tfor (let attempt = 0; attempt < 2; attempt++) {\n\t\tconst response = await generateSummaryMessage({\n\t\t\tmessages,\n\t\t\tonProgress,\n\t\t\tprompt,\n\t\t\tsignal,\n\t\t\tsnapshot,\n\t\t\tauth: {\n\t\t\t\tapiKey: auth.apiKey,\n\t\t\t\theaders: auth.headers,\n\t\t\t\textraBody: auth.extraBody,\n\t\t\t},\n\t\t});\n\t\tif (!response) return undefined;\n\n\t\tif (isAssistantMessage(response) && isContextOverflow(response, snapshot.contextWindow)) {\n\t\t\tconst retryMessages = pruneMessagesForOverflowRetry(\n\t\t\t\tmessages,\n\t\t\t\tMath.floor(snapshot.contextWindow * COMPACTION_RETRY_BUDGET_RATIO),\n\t\t\t);\n\t\t\tif (!retryMessages || retryMessages.length === messages.length) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tmessages = retryMessages;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst summary = getSummaryText(response);\n\t\tif (!summary) return undefined;\n\n\t\tconst tokenEstimate = estimateContextTokens(convertToLlm(messages)).tokens + approxTokens(summary);\n\t\tif (tokenEstimate > snapshot.contextWindow * COMPACTION_BUDGET_RATIO) return undefined;\n\n\t\treturn {\n\t\t\tsummary,\n\t\t\tfirstKeptEntryId: snapshot.preparation.firstKeptEntryId,\n\t\t\ttokensBefore: snapshot.preparation.tokensBefore,\n\t\t\tdetails: { schema: SUMMARY_SCHEMA, promptVariant: snapshot.promptVariant, tokenEstimate },\n\t\t};\n\t}\n\n\tthrow new Error(\"Compaction summary request exceeded the context window after retrying with a smaller input\");\n}\n\nexport async function applyGeneratedCompaction(\n\tcontext: SpeculativeCompactionContext,\n\tsnapshot: SpeculativeCompactionSnapshot | undefined,\n\tgetCurrentGeneration: () => number,\n\tcompaction: CompactionResult | undefined,\n): Promise<SpeculativeCompactionResult> {\n\tif (!snapshot || !compaction) return { applied: false, reason: \"unavailable\" };\n\n\tif (snapshot.generation !== getCurrentGeneration() || snapshot.expectedRevision !== context.getMessageRevision()) {\n\t\treturn { applied: false, reason: \"stale\" };\n\t}\n\n\treturn await context.applyCompaction(compaction, {\n\t\treason: \"extension\",\n\t\texpectedRevision: snapshot.expectedRevision,\n\t});\n}\n\nexport function snapshotExtensionCompaction(\n\tcontext: SpeculativeCompactionContext,\n\trequest: ExtensionCompactionRequest,\n): SpeculativeCompactionSnapshot | undefined {\n\treturn createSpeculativeCompactionSnapshot(context, request);\n}\n\nexport async function applySpeculativeCompaction(\n\tcontext: SpeculativeCompactionContext,\n\tsnapshot: SpeculativeCompactionSnapshot | undefined,\n\tgetCurrentGeneration: () => number,\n\tgenerate: () => Promise<CompactionResult | undefined>,\n): Promise<SpeculativeCompactionResult> {\n\tif (!snapshot) return { applied: false, reason: \"unavailable\" };\n\n\tconst compaction = await generate();\n\treturn await applyGeneratedCompaction(context, snapshot, getCurrentGeneration, compaction);\n}\n"]}
1
+ {"version":3,"file":"speculative.js","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/compaction/speculative.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentMessage } from \"@earendil-works/pi-agent-core\";\nimport {\n\ttype AssistantMessage,\n\tisContextOverflow,\n\ttype Message,\n\ttype Model,\n\tstream,\n\ttype TextContent,\n} from \"@earendil-works/pi-ai\";\nimport {\n\ttype CompactionPreparation,\n\ttype CompactionResult,\n\tDEFAULT_COMPACTION_SETTINGS,\n\testimateContextTokens,\n\testimateTokens,\n\tprepareCompaction,\n\tserializeConversation,\n} from \"../../../compaction/index.js\";\nimport { convertToLlm } from \"../../../messages.js\";\nimport type { ModelRegistry } from \"../../../model-registry.js\";\nimport type { ReadonlySessionManager } from \"../../../session-manager.js\";\nimport type { ApplyCompactionResult, ContextUsage } from \"../../types.js\";\nimport { computeEffectiveKeepRecentTokens, computeEffectiveThreshold } from \"./policy.js\";\nimport { buildPrompt, type MergedCompactionPromptVariant } from \"./prompts.js\";\nimport * as truncation from \"./tool-truncation.js\";\n\nconst DEFAULT_CONTEXT_WINDOW = 200_000;\nconst COMPACTION_BUDGET_RATIO = 0.6;\nconst EMERGENCY_CONTEXT_TARGET_RATIO = 0.95;\nconst MAX_SUMMARY_TOKENS = 8192;\nconst SUMMARY_SCHEMA = \"senpi.compaction.summary.v1\";\ntype CompactionProgressCallback = (delta: string) => void;\n\nexport interface SpeculativeCompactionContext {\n\tmodel: Model<any> | undefined;\n\tsessionManager: ReadonlySessionManager;\n\tmodelRegistry?: ModelRegistry;\n\tgetContextUsage(): ContextUsage | undefined;\n\tgetCompactionSettings?(): CompactionPreparation[\"settings\"];\n\tgetMessageRevision(): number;\n\tapplyCompaction(\n\t\tprecomputed: CompactionResult,\n\t\toptions: { reason: \"extension\"; expectedRevision: number },\n\t): Promise<ApplyCompactionResult>;\n}\n\nexport interface SpeculativeCompactionSnapshot {\n\tgeneration: number;\n\texpectedRevision: number;\n\tmodel: Model<any>;\n\tcontextWindow: number;\n\tpreparation: CompactionPreparation;\n\tpromptVariant: MergedCompactionPromptVariant;\n\tcustomInstructions?: string;\n}\n\nexport type SpeculativeCompactionResult = ApplyCompactionResult | { applied: false; reason: \"unavailable\" };\n\nexport type ExtensionCompactionRequest = {\n\tcustomInstructions?: string;\n\tgeneration: number;\n\tsignal?: AbortSignal;\n};\n\nfunction approxTokens(text: string): number {\n\treturn Math.ceil(text.length / 4);\n}\n\nfunction getSummaryText(message: Message): string {\n\tconst content = Array.isArray(message.content)\n\t\t? message.content\n\t\t: [{ type: \"text\" as const, text: message.content }];\n\treturn content\n\t\t.filter((content): content is TextContent => content.type === \"text\")\n\t\t.map((content) => content.text)\n\t\t.join(\"\\n\")\n\t\t.trim();\n}\n\nfunction isAssistantMessage(message: Message): message is AssistantMessage {\n\treturn message.role === \"assistant\" && \"stopReason\" in message;\n}\n\nasync function generateSummaryMessage(options: {\n\tmessages: AgentMessage[];\n\tonProgress?: CompactionProgressCallback;\n\tprompt: ReturnType<typeof buildPrompt>;\n\tsignal?: AbortSignal;\n\tsnapshot: SpeculativeCompactionSnapshot;\n\tauth: {\n\t\tapiKey: string;\n\t\theaders?: Record<string, string>;\n\t\textraBody?: Record<string, unknown>;\n\t};\n}): Promise<Message | undefined> {\n\tconst conversationText = serializeConversation(convertToLlm(options.messages));\n\tconst responseStream = stream(\n\t\toptions.snapshot.model,\n\t\t{\n\t\t\tsystemPrompt: options.prompt.system,\n\t\t\tmessages: [\n\t\t\t\t{\n\t\t\t\t\trole: \"user\",\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `${options.prompt.user}\\n\\n<conversation>\\n${conversationText}\\n</conversation>`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\t{\n\t\t\tapiKey: options.auth.apiKey,\n\t\t\theaders: options.auth.headers,\n\t\t\textraBody: options.auth.extraBody,\n\t\t\tmaxTokens: MAX_SUMMARY_TOKENS,\n\t\t\tsignal: options.signal,\n\t\t},\n\t);\n\tfor await (const event of responseStream) {\n\t\tif (event.type === \"text_delta\" && event.delta) {\n\t\t\toptions.onProgress?.(event.delta);\n\t\t}\n\t}\n\treturn await responseStream.result();\n}\n\nfunction pruneToolResults(messages: AgentMessage[], contextWindow: number): AgentMessage[] {\n\tconst toolResults = messages\n\t\t.filter((message) => message.role === \"toolResult\")\n\t\t.map((message) => ({ content: message.content, details: undefined }));\n\tif (toolResults.length === 0) return messages;\n\n\tconst prunedResults = truncation.prePruneToolOutputsToBudget(toolResults, contextWindow * COMPACTION_BUDGET_RATIO);\n\tlet resultIndex = 0;\n\treturn messages.map((message) => {\n\t\tif (message.role !== \"toolResult\") return message;\n\t\tconst pruned = prunedResults[resultIndex];\n\t\tresultIndex++;\n\t\treturn pruned ? { ...message, content: pruned.content } : message;\n\t});\n}\n\nexport function truncateContextMessages(messages: AgentMessage[]): AgentMessage[] {\n\tconst toolResults = messages\n\t\t.filter((message) => message.role === \"toolResult\")\n\t\t.map((message) => ({ content: message.content, details: undefined }));\n\tif (toolResults.length === 0) return messages;\n\n\tconst truncatedResults = truncation.truncateOversizedToolResults(toolResults);\n\tlet resultIndex = 0;\n\treturn messages.map((message) => {\n\t\tif (message.role !== \"toolResult\") return message;\n\t\tconst truncated = truncatedResults[resultIndex];\n\t\tresultIndex++;\n\t\treturn truncated ? { ...message, content: truncated.content } : message;\n\t});\n}\n\nfunction getToolCallIds(message: AgentMessage): Set<string> {\n\tconst ids = new Set<string>();\n\tif (message.role !== \"assistant\") return ids;\n\tfor (const block of message.content) {\n\t\tif (block.type === \"toolCall\") ids.add(block.id);\n\t}\n\treturn ids;\n}\n\nfunction findLastUserLikeIndex(messages: AgentMessage[]): number {\n\tfor (let index = messages.length - 1; index >= 0; index--) {\n\t\tconst role = messages[index]?.role;\n\t\tif (role === \"user\" || role === \"bashExecution\") return index;\n\t}\n\treturn messages.length;\n}\n\nfunction removeAssistantToolPair(messages: AgentMessage[], assistantIndex: number): AgentMessage[] {\n\tconst ids = getToolCallIds(messages[assistantIndex]);\n\treturn messages.filter((message, index) => {\n\t\tif (index === assistantIndex) return false;\n\t\treturn message.role !== \"toolResult\" || !ids.has(message.toolCallId);\n\t});\n}\n\nfunction removeFirstOldToolPair(messages: AgentMessage[], boundaryIndex: number): AgentMessage[] | undefined {\n\tfor (let index = 0; index < boundaryIndex; index++) {\n\t\tconst message = messages[index];\n\t\tif (!message) continue;\n\t\tif (message.role === \"assistant\" && getToolCallIds(message).size > 0)\n\t\t\treturn removeAssistantToolPair(messages, index);\n\t\tif (message.role === \"toolResult\") return messages.filter((_message, candidateIndex) => candidateIndex !== index);\n\t}\n\treturn undefined;\n}\n\nfunction removeFirstOldMessage(messages: AgentMessage[], boundaryIndex: number): AgentMessage[] | undefined {\n\tfor (let index = 0; index < boundaryIndex; index++) {\n\t\tconst message = messages[index];\n\t\tif (!message || message.role === \"toolResult\") continue;\n\t\tif (message.role === \"assistant\" && getToolCallIds(message).size > 0)\n\t\t\treturn removeAssistantToolPair(messages, index);\n\t\treturn messages.filter((_candidate, candidateIndex) => candidateIndex !== index);\n\t}\n\treturn undefined;\n}\n\nfunction pruneOldMessagesToBudget(messages: AgentMessage[], targetTokens: number): AgentMessage[] {\n\tlet pruned = messages;\n\twhile (estimateTotalTokens(pruned) > targetTokens) {\n\t\tconst boundaryIndex = findLastUserLikeIndex(pruned);\n\t\tconst next = removeFirstOldToolPair(pruned, boundaryIndex) ?? removeFirstOldMessage(pruned, boundaryIndex);\n\t\tif (!next || next.length === pruned.length) break;\n\t\tpruned = next;\n\t}\n\treturn pruned;\n}\n\nfunction removeOldestHistoryItemForOverflowRetry(messages: AgentMessage[]): AgentMessage[] | undefined {\n\tif (messages.length <= 1) return undefined;\n\tconst boundaryIndex = findLastUserLikeIndex(messages);\n\treturn (\n\t\tremoveFirstOldToolPair(messages, boundaryIndex) ??\n\t\tremoveFirstOldMessage(messages, boundaryIndex) ??\n\t\t(messages.length > 1 ? messages.slice(1) : undefined)\n\t);\n}\n\nfunction estimateTotalTokens(messages: AgentMessage[]): number {\n\tlet total = 0;\n\tfor (const message of messages) total += estimateTokens(message);\n\treturn total;\n}\n\nexport function hardLimitEmergencyPrune(\n\tmessages: AgentMessage[],\n\tcontextWindow: number,\n): {\n\tmessages: AgentMessage[];\n\tneedsAggressiveCompaction: boolean;\n} {\n\tconst targetTokens = Math.floor(contextWindow * EMERGENCY_CONTEXT_TARGET_RATIO);\n\tconst noLlmPruned = truncateContextMessages(pruneToolResults(messages, contextWindow));\n\tif (estimateTotalTokens(noLlmPruned) <= targetTokens) {\n\t\treturn { messages: noLlmPruned, needsAggressiveCompaction: false };\n\t}\n\treturn {\n\t\tmessages: pruneOldMessagesToBudget(noLlmPruned, targetTokens),\n\t\tneedsAggressiveCompaction: true,\n\t};\n}\n\nexport function getPromptVariant(options: {\n\treason: string;\n\tpreparation: { previousSummary?: string; isSplitTurn: boolean };\n}): MergedCompactionPromptVariant {\n\tif (options.reason === \"branch\") return \"branch\";\n\tif (options.preparation.previousSummary) return \"update\";\n\tif (options.preparation.isSplitTurn) return \"turn_prefix\";\n\treturn \"default\";\n}\n\nexport function createSpeculativeCompactionSnapshot(\n\tcontext: SpeculativeCompactionContext,\n\toptions: { customInstructions?: string; generation: number },\n): SpeculativeCompactionSnapshot | undefined {\n\tconst model = context.model;\n\tif (!model) return undefined;\n\n\tconst expectedRevision = context.getMessageRevision();\n\tconst branchEntries = context.sessionManager.getBranch();\n\tconst contextWindow = context.getContextUsage()?.contextWindow ?? model.contextWindow ?? DEFAULT_CONTEXT_WINDOW;\n\tconst settings = context.getCompactionSettings?.() ?? DEFAULT_COMPACTION_SETTINGS;\n\tconst thresholdRatio = computeEffectiveThreshold(contextWindow);\n\tconst preparation = prepareCompaction(branchEntries, {\n\t\t...settings,\n\t\tkeepRecentTokens: computeEffectiveKeepRecentTokens(settings.keepRecentTokens, contextWindow, thresholdRatio),\n\t});\n\tif (!preparation) return undefined;\n\n\treturn {\n\t\tgeneration: options.generation,\n\t\texpectedRevision,\n\t\tmodel,\n\t\tcontextWindow,\n\t\tpreparation,\n\t\tpromptVariant: getPromptVariant({ reason: \"extension\", preparation }),\n\t\tcustomInstructions: options.customInstructions,\n\t};\n}\n\nexport async function runExtensionCompaction(\n\tcontext: SpeculativeCompactionContext,\n\tsnapshot: SpeculativeCompactionSnapshot,\n\tsignal?: AbortSignal,\n\tonProgress?: CompactionProgressCallback,\n): Promise<CompactionResult | undefined> {\n\tconst auth = await context.modelRegistry?.getApiKeyAndHeaders(snapshot.model);\n\tif (!auth?.ok || !auth.apiKey) return undefined;\n\n\tlet messages = pruneToolResults(\n\t\t[...snapshot.preparation.messagesToSummarize, ...snapshot.preparation.turnPrefixMessages],\n\t\tsnapshot.contextWindow,\n\t);\n\tconst prompt = buildPrompt({\n\t\tvariant: snapshot.promptVariant,\n\t\tpreviousSummary: snapshot.preparation.previousSummary,\n\t\tcustomInstructions: snapshot.customInstructions,\n\t});\n\n\twhile (true) {\n\t\tconst response = await generateSummaryMessage({\n\t\t\tmessages,\n\t\t\tonProgress,\n\t\t\tprompt,\n\t\t\tsignal,\n\t\t\tsnapshot,\n\t\t\tauth: {\n\t\t\t\tapiKey: auth.apiKey,\n\t\t\t\theaders: auth.headers,\n\t\t\t\textraBody: auth.extraBody,\n\t\t\t},\n\t\t});\n\t\tif (!response) return undefined;\n\n\t\tif (isAssistantMessage(response) && isContextOverflow(response, snapshot.contextWindow)) {\n\t\t\tconst retryMessages = removeOldestHistoryItemForOverflowRetry(messages);\n\t\t\tif (!retryMessages || retryMessages.length === messages.length) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tmessages = retryMessages;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst summary = getSummaryText(response);\n\t\tif (!summary) return undefined;\n\n\t\tconst tokenEstimate = estimateContextTokens(convertToLlm(messages)).tokens + approxTokens(summary);\n\t\tif (tokenEstimate > snapshot.contextWindow * COMPACTION_BUDGET_RATIO) return undefined;\n\n\t\treturn {\n\t\t\tsummary,\n\t\t\tfirstKeptEntryId: snapshot.preparation.firstKeptEntryId,\n\t\t\ttokensBefore: snapshot.preparation.tokensBefore,\n\t\t\tdetails: { schema: SUMMARY_SCHEMA, promptVariant: snapshot.promptVariant, tokenEstimate },\n\t\t};\n\t}\n\n\tthrow new Error(\"Compaction summary request exceeded the context window after retrying with a smaller input\");\n}\n\nexport async function applyGeneratedCompaction(\n\tcontext: SpeculativeCompactionContext,\n\tsnapshot: SpeculativeCompactionSnapshot | undefined,\n\tgetCurrentGeneration: () => number,\n\tcompaction: CompactionResult | undefined,\n): Promise<SpeculativeCompactionResult> {\n\tif (!snapshot || !compaction) return { applied: false, reason: \"unavailable\" };\n\n\tif (snapshot.generation !== getCurrentGeneration() || snapshot.expectedRevision !== context.getMessageRevision()) {\n\t\treturn { applied: false, reason: \"stale\" };\n\t}\n\n\treturn await context.applyCompaction(compaction, {\n\t\treason: \"extension\",\n\t\texpectedRevision: snapshot.expectedRevision,\n\t});\n}\n\nexport function snapshotExtensionCompaction(\n\tcontext: SpeculativeCompactionContext,\n\trequest: ExtensionCompactionRequest,\n): SpeculativeCompactionSnapshot | undefined {\n\treturn createSpeculativeCompactionSnapshot(context, request);\n}\n\nexport async function applySpeculativeCompaction(\n\tcontext: SpeculativeCompactionContext,\n\tsnapshot: SpeculativeCompactionSnapshot | undefined,\n\tgetCurrentGeneration: () => number,\n\tgenerate: () => Promise<CompactionResult | undefined>,\n): Promise<SpeculativeCompactionResult> {\n\tif (!snapshot) return { applied: false, reason: \"unavailable\" };\n\n\tconst compaction = await generate();\n\treturn await applyGeneratedCompaction(context, snapshot, getCurrentGeneration, compaction);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/builtin/diff.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAQhD,MAAM,CAAC,OAAO,WAAW,EAAE,EAAE,YAAY,QA0MxC","sourcesContent":["/**\n * Diff Extension\n *\n * /diff command shows modified/deleted/new files from git status and opens\n * the selected file in VS Code's diff view.\n */\n\nimport { Container, Key, matchesKey, type SelectItem, SelectList, Text } from \"@earendil-works/pi-tui\";\nimport { DynamicBorder } from \"../../../modes/interactive/components/dynamic-border.js\";\nimport type { ExtensionAPI } from \"../types.js\";\n\ninterface FileInfo {\n\tstatus: string;\n\tstatusLabel: string;\n\tfile: string;\n}\n\nexport default function (pi: ExtensionAPI) {\n\tpi.registerCommand(\"diff\", {\n\t\tdescription: \"Show git changes and open in VS Code diff view\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tif (!ctx.hasUI) {\n\t\t\t\tctx.ui.notify(\"No UI available\", \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Get changed files from git status\n\t\t\tconst result = await pi.exec(\"git\", [\"status\", \"--porcelain\"], { cwd: ctx.cwd });\n\n\t\t\tif (result.code !== 0) {\n\t\t\t\tctx.ui.notify(`git status failed: ${result.stderr}`, \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!result.stdout || !result.stdout.trim()) {\n\t\t\t\tctx.ui.notify(\"No changes in working tree\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Parse git status output\n\t\t\t// Format: XY filename (where XY is two-letter status, then space, then filename)\n\t\t\tconst lines = result.stdout.split(\"\\n\");\n\t\t\tconst files: FileInfo[] = [];\n\n\t\t\tfor (const line of lines) {\n\t\t\t\tif (line.length < 4) continue; // Need at least \"XY f\"\n\n\t\t\t\tconst status = line.slice(0, 2);\n\t\t\t\tconst file = line.slice(2).trimStart();\n\n\t\t\t\t// Translate status codes to short labels\n\t\t\t\tlet statusLabel: string;\n\t\t\t\tif (status.includes(\"M\")) statusLabel = \"M\";\n\t\t\t\telse if (status.includes(\"A\")) statusLabel = \"A\";\n\t\t\t\telse if (status.includes(\"D\")) statusLabel = \"D\";\n\t\t\t\telse if (status.includes(\"?\")) statusLabel = \"?\";\n\t\t\t\telse if (status.includes(\"R\")) statusLabel = \"R\";\n\t\t\t\telse if (status.includes(\"C\")) statusLabel = \"C\";\n\t\t\t\telse statusLabel = status.trim() || \"~\";\n\n\t\t\t\tfiles.push({ status: statusLabel, statusLabel, file });\n\t\t\t}\n\n\t\t\tif (files.length === 0) {\n\t\t\t\tctx.ui.notify(\"No changes found\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst WINDOWS_UNSAFE_CMD_CHARS_RE = /[&|<>^%\\r\\n]/;\n\t\t\tconst quoteCmdArg = (value: string) => `\"${value.replace(/\"/g, '\"\"')}\"`;\n\n\t\t\tconst openWithCode = async (file: string) => {\n\t\t\t\tif (process.platform === \"win32\") {\n\t\t\t\t\tif (WINDOWS_UNSAFE_CMD_CHARS_RE.test(file)) {\n\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t`Refusing to open ${file}: path contains Windows cmd metacharacters (& | < > ^ % or newline).`,\n\t\t\t\t\t\t\t\"error\",\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\t\t\t\t\tconst commandLine = `code -g ${quoteCmdArg(file)}`;\n\t\t\t\t\treturn pi.exec(\"cmd\", [\"/d\", \"/s\", \"/c\", commandLine], { cwd: ctx.cwd });\n\t\t\t\t}\n\t\t\t\treturn pi.exec(\"code\", [\"-g\", file], { cwd: ctx.cwd });\n\t\t\t};\n\n\t\t\tconst openSelected = async (fileInfo: FileInfo): Promise<void> => {\n\t\t\t\ttry {\n\t\t\t\t\t// Open in VS Code diff view.\n\t\t\t\t\t// For untracked files, git difftool won't work, so fall back to just opening the file.\n\t\t\t\t\tif (fileInfo.status === \"?\") {\n\t\t\t\t\t\tconst openResult = await openWithCode(fileInfo.file);\n\t\t\t\t\t\tif (!openResult) return;\n\t\t\t\t\t\tif (openResult.code !== 0) {\n\t\t\t\t\t\t\tconst openStderr = openResult.stderr.trim();\n\t\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t\t`Failed to open ${fileInfo.file} (exit ${openResult.code})${openStderr ? `: ${openStderr}` : \"\"}`,\n\t\t\t\t\t\t\t\t\"error\",\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst diffResult = await pi.exec(\"git\", [\"difftool\", \"-y\", \"--tool=vscode\", fileInfo.file], {\n\t\t\t\t\t\tcwd: ctx.cwd,\n\t\t\t\t\t});\n\t\t\t\t\tif (diffResult.code !== 0) {\n\t\t\t\t\t\tconst diffStderr = diffResult.stderr.trim();\n\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t`Failed to show diff with vscode for ${fileInfo.file} (exit ${diffResult.code})${diffStderr ? `: ${diffStderr}` : \"\"}`,\n\t\t\t\t\t\t\t\"error\",\n\t\t\t\t\t\t);\n\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t\"Troubleshooting: check git difftool config (e.g. `git config --get difftool.vscode.cmd`).\",\n\t\t\t\t\t\t\t\"info\",\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tconst openResult = await openWithCode(fileInfo.file);\n\t\t\t\t\t\tif (!openResult) return;\n\t\t\t\t\t\tif (openResult.code !== 0) {\n\t\t\t\t\t\t\tconst openStderr = openResult.stderr.trim();\n\t\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t\t`Failed to open ${fileInfo.file} (exit ${openResult.code})${openStderr ? `: ${openStderr}` : \"\"}`,\n\t\t\t\t\t\t\t\t\"error\",\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\tctx.ui.notify(`Failed to open ${fileInfo.file}: ${message}`, \"error\");\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t// Show file picker with SelectList\n\t\t\tawait ctx.ui.custom<void>((tui, theme, _kb, done) => {\n\t\t\t\tconst container = new Container();\n\n\t\t\t\t// Top border\n\t\t\t\tcontainer.addChild(new DynamicBorder((s: string) => theme.fg(\"accent\", s)));\n\n\t\t\t\t// Title\n\t\t\t\tcontainer.addChild(new Text(theme.fg(\"accent\", theme.bold(\" Select file to diff\")), 0, 0));\n\n\t\t\t\t// Build select items with colored status\n\t\t\t\tconst filesByValue = new Map<string, FileInfo>();\n\t\t\t\tconst items: SelectItem[] = files.map((f, i) => {\n\t\t\t\t\tconst key = String(i);\n\t\t\t\t\tfilesByValue.set(key, f);\n\t\t\t\t\tlet statusColor: string;\n\t\t\t\t\tswitch (f.status) {\n\t\t\t\t\t\tcase \"M\":\n\t\t\t\t\t\t\tstatusColor = theme.fg(\"warning\", f.status);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"A\":\n\t\t\t\t\t\t\tstatusColor = theme.fg(\"success\", f.status);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"D\":\n\t\t\t\t\t\t\tstatusColor = theme.fg(\"error\", f.status);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"?\":\n\t\t\t\t\t\t\tstatusColor = theme.fg(\"muted\", f.status);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tstatusColor = theme.fg(\"dim\", f.status);\n\t\t\t\t\t}\n\t\t\t\t\treturn {\n\t\t\t\t\t\tvalue: key,\n\t\t\t\t\t\tlabel: `${statusColor} ${f.file}`,\n\t\t\t\t\t};\n\t\t\t\t});\n\n\t\t\t\tconst visibleRows = Math.min(files.length, 15);\n\t\t\t\tlet currentIndex = 0;\n\n\t\t\t\tconst selectList = new SelectList(items, visibleRows, {\n\t\t\t\t\tselectedPrefix: (t) => theme.fg(\"accent\", t),\n\t\t\t\t\tselectedText: (t) => t, // Keep existing colors\n\t\t\t\t\tdescription: (t) => theme.fg(\"muted\", t),\n\t\t\t\t\tscrollInfo: (t) => theme.fg(\"dim\", t),\n\t\t\t\t\tnoMatch: (t) => theme.fg(\"warning\", t),\n\t\t\t\t});\n\t\t\t\tselectList.onSelect = (item) => {\n\t\t\t\t\tconst fileInfo = filesByValue.get(item.value);\n\t\t\t\t\tif (fileInfo) void openSelected(fileInfo);\n\t\t\t\t};\n\t\t\t\tselectList.onCancel = () => done();\n\t\t\t\tselectList.onSelectionChange = (item) => {\n\t\t\t\t\tcurrentIndex = items.indexOf(item);\n\t\t\t\t};\n\t\t\t\tcontainer.addChild(selectList);\n\n\t\t\t\t// Help text\n\t\t\t\tcontainer.addChild(new Text(theme.fg(\"dim\", \" ↑↓ navigate • ←→ page • enter open • esc close\"), 0, 0));\n\n\t\t\t\t// Bottom border\n\t\t\t\tcontainer.addChild(new DynamicBorder((s: string) => theme.fg(\"accent\", s)));\n\n\t\t\t\treturn {\n\t\t\t\t\trender: (w) => container.render(w),\n\t\t\t\t\tinvalidate: () => container.invalidate(),\n\t\t\t\t\thandleInput: (data) => {\n\t\t\t\t\t\t// Add paging with left/right\n\t\t\t\t\t\tif (matchesKey(data, Key.left)) {\n\t\t\t\t\t\t\t// Page up - clamp to 0\n\t\t\t\t\t\t\tcurrentIndex = Math.max(0, currentIndex - visibleRows);\n\t\t\t\t\t\t\tselectList.setSelectedIndex(currentIndex);\n\t\t\t\t\t\t} else if (matchesKey(data, Key.right)) {\n\t\t\t\t\t\t\t// Page down - clamp to last\n\t\t\t\t\t\t\tcurrentIndex = Math.min(items.length - 1, currentIndex + visibleRows);\n\t\t\t\t\t\t\tselectList.setSelectedIndex(currentIndex);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tselectList.handleInput(data);\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttui.requestRender();\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t});\n\t\t},\n\t});\n}\n"]}
1
+ {"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/builtin/diff.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAQhD,MAAM,CAAC,OAAO,WAAW,EAAE,EAAE,YAAY,QA0MxC","sourcesContent":["/**\n * Diff Extension\n *\n * /diff command shows modified/deleted/new files from git status and opens\n * the selected file in VS Code's diff view.\n */\n\nimport { Container, Key, matchesKey, type SelectItem, SelectList, Text } from \"@earendil-works/pi-tui\";\nimport { DynamicBorder } from \"../../../modes/interactive/components/dynamic-border.js\";\nimport type { ExtensionAPI } from \"../types.js\";\n\ninterface FileInfo {\n\tstatus: string;\n\tstatusLabel: string;\n\tfile: string;\n}\n\nexport default function (pi: ExtensionAPI) {\n\tpi.registerCommand(\"diff\", {\n\t\tdescription: \"Show git changes and open in VS Code diff view\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tif (!ctx.hasUI) {\n\t\t\t\tctx.ui.notify(\"No UI available\", \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Get changed files from git status\n\t\t\tconst result = await pi.exec(\"git\", [\"status\", \"--porcelain\"], { cwd: ctx.cwd });\n\n\t\t\tif (result.code !== 0) {\n\t\t\t\tctx.ui.notify(`git status failed: ${result.stderr}`, \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!result.stdout?.trim()) {\n\t\t\t\tctx.ui.notify(\"No changes in working tree\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Parse git status output\n\t\t\t// Format: XY filename (where XY is two-letter status, then space, then filename)\n\t\t\tconst lines = result.stdout.split(\"\\n\");\n\t\t\tconst files: FileInfo[] = [];\n\n\t\t\tfor (const line of lines) {\n\t\t\t\tif (line.length < 4) continue; // Need at least \"XY f\"\n\n\t\t\t\tconst status = line.slice(0, 2);\n\t\t\t\tconst file = line.slice(2).trimStart();\n\n\t\t\t\t// Translate status codes to short labels\n\t\t\t\tlet statusLabel: string;\n\t\t\t\tif (status.includes(\"M\")) statusLabel = \"M\";\n\t\t\t\telse if (status.includes(\"A\")) statusLabel = \"A\";\n\t\t\t\telse if (status.includes(\"D\")) statusLabel = \"D\";\n\t\t\t\telse if (status.includes(\"?\")) statusLabel = \"?\";\n\t\t\t\telse if (status.includes(\"R\")) statusLabel = \"R\";\n\t\t\t\telse if (status.includes(\"C\")) statusLabel = \"C\";\n\t\t\t\telse statusLabel = status.trim() || \"~\";\n\n\t\t\t\tfiles.push({ status: statusLabel, statusLabel, file });\n\t\t\t}\n\n\t\t\tif (files.length === 0) {\n\t\t\t\tctx.ui.notify(\"No changes found\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst WINDOWS_UNSAFE_CMD_CHARS_RE = /[&|<>^%\\r\\n]/;\n\t\t\tconst quoteCmdArg = (value: string) => `\"${value.replace(/\"/g, '\"\"')}\"`;\n\n\t\t\tconst openWithCode = async (file: string) => {\n\t\t\t\tif (process.platform === \"win32\") {\n\t\t\t\t\tif (WINDOWS_UNSAFE_CMD_CHARS_RE.test(file)) {\n\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t`Refusing to open ${file}: path contains Windows cmd metacharacters (& | < > ^ % or newline).`,\n\t\t\t\t\t\t\t\"error\",\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\t\t\t\t\tconst commandLine = `code -g ${quoteCmdArg(file)}`;\n\t\t\t\t\treturn pi.exec(\"cmd\", [\"/d\", \"/s\", \"/c\", commandLine], { cwd: ctx.cwd });\n\t\t\t\t}\n\t\t\t\treturn pi.exec(\"code\", [\"-g\", file], { cwd: ctx.cwd });\n\t\t\t};\n\n\t\t\tconst openSelected = async (fileInfo: FileInfo): Promise<void> => {\n\t\t\t\ttry {\n\t\t\t\t\t// Open in VS Code diff view.\n\t\t\t\t\t// For untracked files, git difftool won't work, so fall back to just opening the file.\n\t\t\t\t\tif (fileInfo.status === \"?\") {\n\t\t\t\t\t\tconst openResult = await openWithCode(fileInfo.file);\n\t\t\t\t\t\tif (!openResult) return;\n\t\t\t\t\t\tif (openResult.code !== 0) {\n\t\t\t\t\t\t\tconst openStderr = openResult.stderr.trim();\n\t\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t\t`Failed to open ${fileInfo.file} (exit ${openResult.code})${openStderr ? `: ${openStderr}` : \"\"}`,\n\t\t\t\t\t\t\t\t\"error\",\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst diffResult = await pi.exec(\"git\", [\"difftool\", \"-y\", \"--tool=vscode\", fileInfo.file], {\n\t\t\t\t\t\tcwd: ctx.cwd,\n\t\t\t\t\t});\n\t\t\t\t\tif (diffResult.code !== 0) {\n\t\t\t\t\t\tconst diffStderr = diffResult.stderr.trim();\n\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t`Failed to show diff with vscode for ${fileInfo.file} (exit ${diffResult.code})${diffStderr ? `: ${diffStderr}` : \"\"}`,\n\t\t\t\t\t\t\t\"error\",\n\t\t\t\t\t\t);\n\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t\"Troubleshooting: check git difftool config (e.g. `git config --get difftool.vscode.cmd`).\",\n\t\t\t\t\t\t\t\"info\",\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tconst openResult = await openWithCode(fileInfo.file);\n\t\t\t\t\t\tif (!openResult) return;\n\t\t\t\t\t\tif (openResult.code !== 0) {\n\t\t\t\t\t\t\tconst openStderr = openResult.stderr.trim();\n\t\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t\t`Failed to open ${fileInfo.file} (exit ${openResult.code})${openStderr ? `: ${openStderr}` : \"\"}`,\n\t\t\t\t\t\t\t\t\"error\",\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\tctx.ui.notify(`Failed to open ${fileInfo.file}: ${message}`, \"error\");\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t// Show file picker with SelectList\n\t\t\tawait ctx.ui.custom<void>((tui, theme, _kb, done) => {\n\t\t\t\tconst container = new Container();\n\n\t\t\t\t// Top border\n\t\t\t\tcontainer.addChild(new DynamicBorder((s: string) => theme.fg(\"accent\", s)));\n\n\t\t\t\t// Title\n\t\t\t\tcontainer.addChild(new Text(theme.fg(\"accent\", theme.bold(\" Select file to diff\")), 0, 0));\n\n\t\t\t\t// Build select items with colored status\n\t\t\t\tconst filesByValue = new Map<string, FileInfo>();\n\t\t\t\tconst items: SelectItem[] = files.map((f, i) => {\n\t\t\t\t\tconst key = String(i);\n\t\t\t\t\tfilesByValue.set(key, f);\n\t\t\t\t\tlet statusColor: string;\n\t\t\t\t\tswitch (f.status) {\n\t\t\t\t\t\tcase \"M\":\n\t\t\t\t\t\t\tstatusColor = theme.fg(\"warning\", f.status);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"A\":\n\t\t\t\t\t\t\tstatusColor = theme.fg(\"success\", f.status);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"D\":\n\t\t\t\t\t\t\tstatusColor = theme.fg(\"error\", f.status);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"?\":\n\t\t\t\t\t\t\tstatusColor = theme.fg(\"muted\", f.status);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tstatusColor = theme.fg(\"dim\", f.status);\n\t\t\t\t\t}\n\t\t\t\t\treturn {\n\t\t\t\t\t\tvalue: key,\n\t\t\t\t\t\tlabel: `${statusColor} ${f.file}`,\n\t\t\t\t\t};\n\t\t\t\t});\n\n\t\t\t\tconst visibleRows = Math.min(files.length, 15);\n\t\t\t\tlet currentIndex = 0;\n\n\t\t\t\tconst selectList = new SelectList(items, visibleRows, {\n\t\t\t\t\tselectedPrefix: (t) => theme.fg(\"accent\", t),\n\t\t\t\t\tselectedText: (t) => t, // Keep existing colors\n\t\t\t\t\tdescription: (t) => theme.fg(\"muted\", t),\n\t\t\t\t\tscrollInfo: (t) => theme.fg(\"dim\", t),\n\t\t\t\t\tnoMatch: (t) => theme.fg(\"warning\", t),\n\t\t\t\t});\n\t\t\t\tselectList.onSelect = (item) => {\n\t\t\t\t\tconst fileInfo = filesByValue.get(item.value);\n\t\t\t\t\tif (fileInfo) void openSelected(fileInfo);\n\t\t\t\t};\n\t\t\t\tselectList.onCancel = () => done();\n\t\t\t\tselectList.onSelectionChange = (item) => {\n\t\t\t\t\tcurrentIndex = items.indexOf(item);\n\t\t\t\t};\n\t\t\t\tcontainer.addChild(selectList);\n\n\t\t\t\t// Help text\n\t\t\t\tcontainer.addChild(new Text(theme.fg(\"dim\", \" ↑↓ navigate • ←→ page • enter open • esc close\"), 0, 0));\n\n\t\t\t\t// Bottom border\n\t\t\t\tcontainer.addChild(new DynamicBorder((s: string) => theme.fg(\"accent\", s)));\n\n\t\t\t\treturn {\n\t\t\t\t\trender: (w) => container.render(w),\n\t\t\t\t\tinvalidate: () => container.invalidate(),\n\t\t\t\t\thandleInput: (data) => {\n\t\t\t\t\t\t// Add paging with left/right\n\t\t\t\t\t\tif (matchesKey(data, Key.left)) {\n\t\t\t\t\t\t\t// Page up - clamp to 0\n\t\t\t\t\t\t\tcurrentIndex = Math.max(0, currentIndex - visibleRows);\n\t\t\t\t\t\t\tselectList.setSelectedIndex(currentIndex);\n\t\t\t\t\t\t} else if (matchesKey(data, Key.right)) {\n\t\t\t\t\t\t\t// Page down - clamp to last\n\t\t\t\t\t\t\tcurrentIndex = Math.min(items.length - 1, currentIndex + visibleRows);\n\t\t\t\t\t\t\tselectList.setSelectedIndex(currentIndex);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tselectList.handleInput(data);\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttui.requestRender();\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t});\n\t\t},\n\t});\n}\n"]}
@@ -20,7 +20,7 @@ export default function (pi) {
20
20
  ctx.ui.notify(`git status failed: ${result.stderr}`, "error");
21
21
  return;
22
22
  }
23
- if (!result.stdout || !result.stdout.trim()) {
23
+ if (!result.stdout?.trim()) {
24
24
  ctx.ui.notify("No changes in working tree", "info");
25
25
  return;
26
26
  }
@@ -1 +1 @@
1
- {"version":3,"file":"diff.js","sourceRoot":"","sources":["../../../../src/core/extensions/builtin/diff.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Diff Extension\n *\n * /diff command shows modified/deleted/new files from git status and opens\n * the selected file in VS Code's diff view.\n */\n\nimport { Container, Key, matchesKey, type SelectItem, SelectList, Text } from \"@earendil-works/pi-tui\";\nimport { DynamicBorder } from \"../../../modes/interactive/components/dynamic-border.js\";\nimport type { ExtensionAPI } from \"../types.js\";\n\ninterface FileInfo {\n\tstatus: string;\n\tstatusLabel: string;\n\tfile: string;\n}\n\nexport default function (pi: ExtensionAPI) {\n\tpi.registerCommand(\"diff\", {\n\t\tdescription: \"Show git changes and open in VS Code diff view\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tif (!ctx.hasUI) {\n\t\t\t\tctx.ui.notify(\"No UI available\", \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Get changed files from git status\n\t\t\tconst result = await pi.exec(\"git\", [\"status\", \"--porcelain\"], { cwd: ctx.cwd });\n\n\t\t\tif (result.code !== 0) {\n\t\t\t\tctx.ui.notify(`git status failed: ${result.stderr}`, \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!result.stdout || !result.stdout.trim()) {\n\t\t\t\tctx.ui.notify(\"No changes in working tree\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Parse git status output\n\t\t\t// Format: XY filename (where XY is two-letter status, then space, then filename)\n\t\t\tconst lines = result.stdout.split(\"\\n\");\n\t\t\tconst files: FileInfo[] = [];\n\n\t\t\tfor (const line of lines) {\n\t\t\t\tif (line.length < 4) continue; // Need at least \"XY f\"\n\n\t\t\t\tconst status = line.slice(0, 2);\n\t\t\t\tconst file = line.slice(2).trimStart();\n\n\t\t\t\t// Translate status codes to short labels\n\t\t\t\tlet statusLabel: string;\n\t\t\t\tif (status.includes(\"M\")) statusLabel = \"M\";\n\t\t\t\telse if (status.includes(\"A\")) statusLabel = \"A\";\n\t\t\t\telse if (status.includes(\"D\")) statusLabel = \"D\";\n\t\t\t\telse if (status.includes(\"?\")) statusLabel = \"?\";\n\t\t\t\telse if (status.includes(\"R\")) statusLabel = \"R\";\n\t\t\t\telse if (status.includes(\"C\")) statusLabel = \"C\";\n\t\t\t\telse statusLabel = status.trim() || \"~\";\n\n\t\t\t\tfiles.push({ status: statusLabel, statusLabel, file });\n\t\t\t}\n\n\t\t\tif (files.length === 0) {\n\t\t\t\tctx.ui.notify(\"No changes found\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst WINDOWS_UNSAFE_CMD_CHARS_RE = /[&|<>^%\\r\\n]/;\n\t\t\tconst quoteCmdArg = (value: string) => `\"${value.replace(/\"/g, '\"\"')}\"`;\n\n\t\t\tconst openWithCode = async (file: string) => {\n\t\t\t\tif (process.platform === \"win32\") {\n\t\t\t\t\tif (WINDOWS_UNSAFE_CMD_CHARS_RE.test(file)) {\n\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t`Refusing to open ${file}: path contains Windows cmd metacharacters (& | < > ^ % or newline).`,\n\t\t\t\t\t\t\t\"error\",\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\t\t\t\t\tconst commandLine = `code -g ${quoteCmdArg(file)}`;\n\t\t\t\t\treturn pi.exec(\"cmd\", [\"/d\", \"/s\", \"/c\", commandLine], { cwd: ctx.cwd });\n\t\t\t\t}\n\t\t\t\treturn pi.exec(\"code\", [\"-g\", file], { cwd: ctx.cwd });\n\t\t\t};\n\n\t\t\tconst openSelected = async (fileInfo: FileInfo): Promise<void> => {\n\t\t\t\ttry {\n\t\t\t\t\t// Open in VS Code diff view.\n\t\t\t\t\t// For untracked files, git difftool won't work, so fall back to just opening the file.\n\t\t\t\t\tif (fileInfo.status === \"?\") {\n\t\t\t\t\t\tconst openResult = await openWithCode(fileInfo.file);\n\t\t\t\t\t\tif (!openResult) return;\n\t\t\t\t\t\tif (openResult.code !== 0) {\n\t\t\t\t\t\t\tconst openStderr = openResult.stderr.trim();\n\t\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t\t`Failed to open ${fileInfo.file} (exit ${openResult.code})${openStderr ? `: ${openStderr}` : \"\"}`,\n\t\t\t\t\t\t\t\t\"error\",\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst diffResult = await pi.exec(\"git\", [\"difftool\", \"-y\", \"--tool=vscode\", fileInfo.file], {\n\t\t\t\t\t\tcwd: ctx.cwd,\n\t\t\t\t\t});\n\t\t\t\t\tif (diffResult.code !== 0) {\n\t\t\t\t\t\tconst diffStderr = diffResult.stderr.trim();\n\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t`Failed to show diff with vscode for ${fileInfo.file} (exit ${diffResult.code})${diffStderr ? `: ${diffStderr}` : \"\"}`,\n\t\t\t\t\t\t\t\"error\",\n\t\t\t\t\t\t);\n\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t\"Troubleshooting: check git difftool config (e.g. `git config --get difftool.vscode.cmd`).\",\n\t\t\t\t\t\t\t\"info\",\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tconst openResult = await openWithCode(fileInfo.file);\n\t\t\t\t\t\tif (!openResult) return;\n\t\t\t\t\t\tif (openResult.code !== 0) {\n\t\t\t\t\t\t\tconst openStderr = openResult.stderr.trim();\n\t\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t\t`Failed to open ${fileInfo.file} (exit ${openResult.code})${openStderr ? `: ${openStderr}` : \"\"}`,\n\t\t\t\t\t\t\t\t\"error\",\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\tctx.ui.notify(`Failed to open ${fileInfo.file}: ${message}`, \"error\");\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t// Show file picker with SelectList\n\t\t\tawait ctx.ui.custom<void>((tui, theme, _kb, done) => {\n\t\t\t\tconst container = new Container();\n\n\t\t\t\t// Top border\n\t\t\t\tcontainer.addChild(new DynamicBorder((s: string) => theme.fg(\"accent\", s)));\n\n\t\t\t\t// Title\n\t\t\t\tcontainer.addChild(new Text(theme.fg(\"accent\", theme.bold(\" Select file to diff\")), 0, 0));\n\n\t\t\t\t// Build select items with colored status\n\t\t\t\tconst filesByValue = new Map<string, FileInfo>();\n\t\t\t\tconst items: SelectItem[] = files.map((f, i) => {\n\t\t\t\t\tconst key = String(i);\n\t\t\t\t\tfilesByValue.set(key, f);\n\t\t\t\t\tlet statusColor: string;\n\t\t\t\t\tswitch (f.status) {\n\t\t\t\t\t\tcase \"M\":\n\t\t\t\t\t\t\tstatusColor = theme.fg(\"warning\", f.status);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"A\":\n\t\t\t\t\t\t\tstatusColor = theme.fg(\"success\", f.status);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"D\":\n\t\t\t\t\t\t\tstatusColor = theme.fg(\"error\", f.status);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"?\":\n\t\t\t\t\t\t\tstatusColor = theme.fg(\"muted\", f.status);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tstatusColor = theme.fg(\"dim\", f.status);\n\t\t\t\t\t}\n\t\t\t\t\treturn {\n\t\t\t\t\t\tvalue: key,\n\t\t\t\t\t\tlabel: `${statusColor} ${f.file}`,\n\t\t\t\t\t};\n\t\t\t\t});\n\n\t\t\t\tconst visibleRows = Math.min(files.length, 15);\n\t\t\t\tlet currentIndex = 0;\n\n\t\t\t\tconst selectList = new SelectList(items, visibleRows, {\n\t\t\t\t\tselectedPrefix: (t) => theme.fg(\"accent\", t),\n\t\t\t\t\tselectedText: (t) => t, // Keep existing colors\n\t\t\t\t\tdescription: (t) => theme.fg(\"muted\", t),\n\t\t\t\t\tscrollInfo: (t) => theme.fg(\"dim\", t),\n\t\t\t\t\tnoMatch: (t) => theme.fg(\"warning\", t),\n\t\t\t\t});\n\t\t\t\tselectList.onSelect = (item) => {\n\t\t\t\t\tconst fileInfo = filesByValue.get(item.value);\n\t\t\t\t\tif (fileInfo) void openSelected(fileInfo);\n\t\t\t\t};\n\t\t\t\tselectList.onCancel = () => done();\n\t\t\t\tselectList.onSelectionChange = (item) => {\n\t\t\t\t\tcurrentIndex = items.indexOf(item);\n\t\t\t\t};\n\t\t\t\tcontainer.addChild(selectList);\n\n\t\t\t\t// Help text\n\t\t\t\tcontainer.addChild(new Text(theme.fg(\"dim\", \" ↑↓ navigate • ←→ page • enter open • esc close\"), 0, 0));\n\n\t\t\t\t// Bottom border\n\t\t\t\tcontainer.addChild(new DynamicBorder((s: string) => theme.fg(\"accent\", s)));\n\n\t\t\t\treturn {\n\t\t\t\t\trender: (w) => container.render(w),\n\t\t\t\t\tinvalidate: () => container.invalidate(),\n\t\t\t\t\thandleInput: (data) => {\n\t\t\t\t\t\t// Add paging with left/right\n\t\t\t\t\t\tif (matchesKey(data, Key.left)) {\n\t\t\t\t\t\t\t// Page up - clamp to 0\n\t\t\t\t\t\t\tcurrentIndex = Math.max(0, currentIndex - visibleRows);\n\t\t\t\t\t\t\tselectList.setSelectedIndex(currentIndex);\n\t\t\t\t\t\t} else if (matchesKey(data, Key.right)) {\n\t\t\t\t\t\t\t// Page down - clamp to last\n\t\t\t\t\t\t\tcurrentIndex = Math.min(items.length - 1, currentIndex + visibleRows);\n\t\t\t\t\t\t\tselectList.setSelectedIndex(currentIndex);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tselectList.handleInput(data);\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttui.requestRender();\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t});\n\t\t},\n\t});\n}\n"]}
1
+ {"version":3,"file":"diff.js","sourceRoot":"","sources":["../../../../src/core/extensions/builtin/diff.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Diff Extension\n *\n * /diff command shows modified/deleted/new files from git status and opens\n * the selected file in VS Code's diff view.\n */\n\nimport { Container, Key, matchesKey, type SelectItem, SelectList, Text } from \"@earendil-works/pi-tui\";\nimport { DynamicBorder } from \"../../../modes/interactive/components/dynamic-border.js\";\nimport type { ExtensionAPI } from \"../types.js\";\n\ninterface FileInfo {\n\tstatus: string;\n\tstatusLabel: string;\n\tfile: string;\n}\n\nexport default function (pi: ExtensionAPI) {\n\tpi.registerCommand(\"diff\", {\n\t\tdescription: \"Show git changes and open in VS Code diff view\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tif (!ctx.hasUI) {\n\t\t\t\tctx.ui.notify(\"No UI available\", \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Get changed files from git status\n\t\t\tconst result = await pi.exec(\"git\", [\"status\", \"--porcelain\"], { cwd: ctx.cwd });\n\n\t\t\tif (result.code !== 0) {\n\t\t\t\tctx.ui.notify(`git status failed: ${result.stderr}`, \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!result.stdout?.trim()) {\n\t\t\t\tctx.ui.notify(\"No changes in working tree\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Parse git status output\n\t\t\t// Format: XY filename (where XY is two-letter status, then space, then filename)\n\t\t\tconst lines = result.stdout.split(\"\\n\");\n\t\t\tconst files: FileInfo[] = [];\n\n\t\t\tfor (const line of lines) {\n\t\t\t\tif (line.length < 4) continue; // Need at least \"XY f\"\n\n\t\t\t\tconst status = line.slice(0, 2);\n\t\t\t\tconst file = line.slice(2).trimStart();\n\n\t\t\t\t// Translate status codes to short labels\n\t\t\t\tlet statusLabel: string;\n\t\t\t\tif (status.includes(\"M\")) statusLabel = \"M\";\n\t\t\t\telse if (status.includes(\"A\")) statusLabel = \"A\";\n\t\t\t\telse if (status.includes(\"D\")) statusLabel = \"D\";\n\t\t\t\telse if (status.includes(\"?\")) statusLabel = \"?\";\n\t\t\t\telse if (status.includes(\"R\")) statusLabel = \"R\";\n\t\t\t\telse if (status.includes(\"C\")) statusLabel = \"C\";\n\t\t\t\telse statusLabel = status.trim() || \"~\";\n\n\t\t\t\tfiles.push({ status: statusLabel, statusLabel, file });\n\t\t\t}\n\n\t\t\tif (files.length === 0) {\n\t\t\t\tctx.ui.notify(\"No changes found\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst WINDOWS_UNSAFE_CMD_CHARS_RE = /[&|<>^%\\r\\n]/;\n\t\t\tconst quoteCmdArg = (value: string) => `\"${value.replace(/\"/g, '\"\"')}\"`;\n\n\t\t\tconst openWithCode = async (file: string) => {\n\t\t\t\tif (process.platform === \"win32\") {\n\t\t\t\t\tif (WINDOWS_UNSAFE_CMD_CHARS_RE.test(file)) {\n\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t`Refusing to open ${file}: path contains Windows cmd metacharacters (& | < > ^ % or newline).`,\n\t\t\t\t\t\t\t\"error\",\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\t\t\t\t\tconst commandLine = `code -g ${quoteCmdArg(file)}`;\n\t\t\t\t\treturn pi.exec(\"cmd\", [\"/d\", \"/s\", \"/c\", commandLine], { cwd: ctx.cwd });\n\t\t\t\t}\n\t\t\t\treturn pi.exec(\"code\", [\"-g\", file], { cwd: ctx.cwd });\n\t\t\t};\n\n\t\t\tconst openSelected = async (fileInfo: FileInfo): Promise<void> => {\n\t\t\t\ttry {\n\t\t\t\t\t// Open in VS Code diff view.\n\t\t\t\t\t// For untracked files, git difftool won't work, so fall back to just opening the file.\n\t\t\t\t\tif (fileInfo.status === \"?\") {\n\t\t\t\t\t\tconst openResult = await openWithCode(fileInfo.file);\n\t\t\t\t\t\tif (!openResult) return;\n\t\t\t\t\t\tif (openResult.code !== 0) {\n\t\t\t\t\t\t\tconst openStderr = openResult.stderr.trim();\n\t\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t\t`Failed to open ${fileInfo.file} (exit ${openResult.code})${openStderr ? `: ${openStderr}` : \"\"}`,\n\t\t\t\t\t\t\t\t\"error\",\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst diffResult = await pi.exec(\"git\", [\"difftool\", \"-y\", \"--tool=vscode\", fileInfo.file], {\n\t\t\t\t\t\tcwd: ctx.cwd,\n\t\t\t\t\t});\n\t\t\t\t\tif (diffResult.code !== 0) {\n\t\t\t\t\t\tconst diffStderr = diffResult.stderr.trim();\n\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t`Failed to show diff with vscode for ${fileInfo.file} (exit ${diffResult.code})${diffStderr ? `: ${diffStderr}` : \"\"}`,\n\t\t\t\t\t\t\t\"error\",\n\t\t\t\t\t\t);\n\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t\"Troubleshooting: check git difftool config (e.g. `git config --get difftool.vscode.cmd`).\",\n\t\t\t\t\t\t\t\"info\",\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tconst openResult = await openWithCode(fileInfo.file);\n\t\t\t\t\t\tif (!openResult) return;\n\t\t\t\t\t\tif (openResult.code !== 0) {\n\t\t\t\t\t\t\tconst openStderr = openResult.stderr.trim();\n\t\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t\t`Failed to open ${fileInfo.file} (exit ${openResult.code})${openStderr ? `: ${openStderr}` : \"\"}`,\n\t\t\t\t\t\t\t\t\"error\",\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\tctx.ui.notify(`Failed to open ${fileInfo.file}: ${message}`, \"error\");\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t// Show file picker with SelectList\n\t\t\tawait ctx.ui.custom<void>((tui, theme, _kb, done) => {\n\t\t\t\tconst container = new Container();\n\n\t\t\t\t// Top border\n\t\t\t\tcontainer.addChild(new DynamicBorder((s: string) => theme.fg(\"accent\", s)));\n\n\t\t\t\t// Title\n\t\t\t\tcontainer.addChild(new Text(theme.fg(\"accent\", theme.bold(\" Select file to diff\")), 0, 0));\n\n\t\t\t\t// Build select items with colored status\n\t\t\t\tconst filesByValue = new Map<string, FileInfo>();\n\t\t\t\tconst items: SelectItem[] = files.map((f, i) => {\n\t\t\t\t\tconst key = String(i);\n\t\t\t\t\tfilesByValue.set(key, f);\n\t\t\t\t\tlet statusColor: string;\n\t\t\t\t\tswitch (f.status) {\n\t\t\t\t\t\tcase \"M\":\n\t\t\t\t\t\t\tstatusColor = theme.fg(\"warning\", f.status);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"A\":\n\t\t\t\t\t\t\tstatusColor = theme.fg(\"success\", f.status);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"D\":\n\t\t\t\t\t\t\tstatusColor = theme.fg(\"error\", f.status);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"?\":\n\t\t\t\t\t\t\tstatusColor = theme.fg(\"muted\", f.status);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tstatusColor = theme.fg(\"dim\", f.status);\n\t\t\t\t\t}\n\t\t\t\t\treturn {\n\t\t\t\t\t\tvalue: key,\n\t\t\t\t\t\tlabel: `${statusColor} ${f.file}`,\n\t\t\t\t\t};\n\t\t\t\t});\n\n\t\t\t\tconst visibleRows = Math.min(files.length, 15);\n\t\t\t\tlet currentIndex = 0;\n\n\t\t\t\tconst selectList = new SelectList(items, visibleRows, {\n\t\t\t\t\tselectedPrefix: (t) => theme.fg(\"accent\", t),\n\t\t\t\t\tselectedText: (t) => t, // Keep existing colors\n\t\t\t\t\tdescription: (t) => theme.fg(\"muted\", t),\n\t\t\t\t\tscrollInfo: (t) => theme.fg(\"dim\", t),\n\t\t\t\t\tnoMatch: (t) => theme.fg(\"warning\", t),\n\t\t\t\t});\n\t\t\t\tselectList.onSelect = (item) => {\n\t\t\t\t\tconst fileInfo = filesByValue.get(item.value);\n\t\t\t\t\tif (fileInfo) void openSelected(fileInfo);\n\t\t\t\t};\n\t\t\t\tselectList.onCancel = () => done();\n\t\t\t\tselectList.onSelectionChange = (item) => {\n\t\t\t\t\tcurrentIndex = items.indexOf(item);\n\t\t\t\t};\n\t\t\t\tcontainer.addChild(selectList);\n\n\t\t\t\t// Help text\n\t\t\t\tcontainer.addChild(new Text(theme.fg(\"dim\", \" ↑↓ navigate • ←→ page • enter open • esc close\"), 0, 0));\n\n\t\t\t\t// Bottom border\n\t\t\t\tcontainer.addChild(new DynamicBorder((s: string) => theme.fg(\"accent\", s)));\n\n\t\t\t\treturn {\n\t\t\t\t\trender: (w) => container.render(w),\n\t\t\t\t\tinvalidate: () => container.invalidate(),\n\t\t\t\t\thandleInput: (data) => {\n\t\t\t\t\t\t// Add paging with left/right\n\t\t\t\t\t\tif (matchesKey(data, Key.left)) {\n\t\t\t\t\t\t\t// Page up - clamp to 0\n\t\t\t\t\t\t\tcurrentIndex = Math.max(0, currentIndex - visibleRows);\n\t\t\t\t\t\t\tselectList.setSelectedIndex(currentIndex);\n\t\t\t\t\t\t} else if (matchesKey(data, Key.right)) {\n\t\t\t\t\t\t\t// Page down - clamp to last\n\t\t\t\t\t\t\tcurrentIndex = Math.min(items.length - 1, currentIndex + visibleRows);\n\t\t\t\t\t\t\tselectList.setSelectedIndex(currentIndex);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tselectList.handleInput(data);\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttui.requestRender();\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t});\n\t\t},\n\t});\n}\n"]}
@@ -1,7 +1,11 @@
1
- import type { Api } from "@earendil-works/pi-ai";
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
2
  import type { ExtensionAPI } from "../../types.js";
3
- export declare function addOpenAiWebSearchToPayload(api: Api | undefined, payload: unknown): unknown;
3
+ type OpenAiWebSearchModel = Pick<Model<Api>, "api" | "baseUrl" | "compat">;
4
+ type OpenAiWebSearchTarget = Api | OpenAiWebSearchModel | undefined;
5
+ export declare function supportsNativeOpenAiWebSearch(target: OpenAiWebSearchTarget): boolean;
6
+ export declare function addOpenAiWebSearchToPayload(target: OpenAiWebSearchTarget, payload: unknown): unknown;
4
7
  export declare function isOpenaiWebSearchEnabled(): boolean;
5
8
  export declare const OPENAI_WEB_SEARCH_SECTION = "\n## Web Search\n\nNative web search is available in this session.\nUse web search when the user asks for current or online information.\nPrefer web search over guessing when freshness matters.\n";
6
9
  export default function openaiWebSearchExtension(pi: ExtensionAPI): void;
10
+ export {};
7
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/openai-web-search/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,gBAAgB,CAAC;AAoHrE,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,GAAG,GAAG,SAAS,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAyC3F;AAED,wBAAgB,wBAAwB,IAAI,OAAO,CAElD;AAYD,eAAO,MAAM,yBAAyB,wMAMrC,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,wBAAwB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA8BvE","sourcesContent":["import type { Api } from \"@earendil-works/pi-ai\";\nimport type { ExtensionAPI, ExtensionContext } from \"../../types.js\";\n\ntype ToolDefinition = Record<string, unknown>;\n\nconst OPENAI_RESPONSES_APIS: ReadonlySet<Api> = new Set([\"openai-responses\", \"azure-openai-responses\"]);\nconst ENABLE_ENV = \"PI_OPENAI_WEB_SEARCH\";\nconst NATIVE_OPENAI_WEB_SEARCH_TYPE = \"web_search_preview\";\nconst WEB_SEARCH_SOURCES_INCLUDE = \"web_search_call.action.sources\";\nconst STATUS_KEY = \"openai-web-search\";\nconst WIDGET_KEY = \"openai-web-search\";\n\nfunction parseEnableEnv(envVar: string): boolean {\n\tconst envValue = process.env[envVar];\n\tif (!envValue) {\n\t\treturn true;\n\t}\n\n\tconst normalized = envValue.trim().toLowerCase();\n\tif (normalized === \"0\" || normalized === \"false\" || normalized === \"no\" || normalized === \"off\") {\n\t\treturn false;\n\t}\n\n\tif (normalized === \"1\" || normalized === \"true\" || normalized === \"yes\" || normalized === \"on\") {\n\t\treturn true;\n\t}\n\n\t// Unknown values fall back to default-on behavior.\n\treturn true;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null;\n}\n\nfunction isOpenAiResponsesApi(api: Api | undefined): api is \"openai-responses\" | \"azure-openai-responses\" {\n\treturn api !== undefined && OPENAI_RESPONSES_APIS.has(api);\n}\n\nfunction isNativeOpenAiWebSearchType(value: unknown): value is \"web_search_preview\" | \"web_search_preview_2025_03_11\" {\n\treturn value === \"web_search_preview\" || value === \"web_search_preview_2025_03_11\";\n}\n\nfunction isUnsupportedWebSearchType(value: unknown): boolean {\n\treturn (\n\t\ttypeof value === \"string\" &&\n\t\t(value === \"web_search\" || value.startsWith(\"web_search_\")) &&\n\t\t!isNativeOpenAiWebSearchType(value)\n\t);\n}\n\nfunction isAnthropicWebFetchType(value: unknown): boolean {\n\treturn typeof value === \"string\" && value.startsWith(\"web_fetch_\");\n}\n\ntype SanitizedTools = {\n\tchanged: boolean;\n\ttools: ToolDefinition[];\n};\n\ntype SanitizeToolsOptions = {\n\tstripFunctionWebSearch: boolean;\n};\n\nfunction stripNativeOpenAiWebSearch(payload: unknown): unknown {\n\tif (!isRecord(payload)) {\n\t\treturn payload;\n\t}\n\n\tconst tools = payload.tools;\n\tif (!Array.isArray(tools)) {\n\t\treturn payload;\n\t}\n\n\tlet changed = false;\n\tconst sanitized: unknown[] = [];\n\tfor (const tool of tools) {\n\t\tif (isRecord(tool) && isNativeOpenAiWebSearchType(tool.type)) {\n\t\t\tchanged = true;\n\t\t\tcontinue;\n\t\t}\n\t\tsanitized.push(tool);\n\t}\n\n\treturn changed ? { ...payload, tools: sanitized } : payload;\n}\n\nfunction sanitizeTools(tools: unknown[], options: SanitizeToolsOptions): SanitizedTools {\n\tconst sanitized: ToolDefinition[] = [];\n\tlet changed = false;\n\tfor (const tool of tools) {\n\t\tif (!isRecord(tool)) {\n\t\t\tchanged = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst type = tool.type;\n\t\tconst shouldStripFunctionVariant =\n\t\t\toptions.stripFunctionWebSearch && tool.name === \"web_search\" && !isNativeOpenAiWebSearchType(type);\n\t\tconst shouldStripProviderNativeVariant = isUnsupportedWebSearchType(type) || isAnthropicWebFetchType(type);\n\t\tif (shouldStripFunctionVariant || shouldStripProviderNativeVariant) {\n\t\t\tchanged = true;\n\t\t} else {\n\t\t\tsanitized.push(tool);\n\t\t}\n\t}\n\n\treturn { changed, tools: sanitized };\n}\n\nfunction includeWebSearchSources(payload: Record<string, unknown>): string[] {\n\tconst include = Array.isArray(payload.include)\n\t\t? payload.include.filter((value): value is string => typeof value === \"string\")\n\t\t: [];\n\treturn include.includes(WEB_SEARCH_SOURCES_INCLUDE) ? include : [...include, WEB_SEARCH_SOURCES_INCLUDE];\n}\n\nexport function addOpenAiWebSearchToPayload(api: Api | undefined, payload: unknown): unknown {\n\tif (!isOpenAiResponsesApi(api)) {\n\t\t// Defense in depth. `web_search_preview` is an OpenAI Responses-only tool\n\t\t// type, but proxies that translate openai-responses → anthropic-messages\n\t\t// (e.g., ccapi/quotio for Claude models) can forward it verbatim, which\n\t\t// Anthropic rejects with `tools.N: Input tag 'web_search_preview'...`.\n\t\t// Strip the OpenAI-native variants for any non-openai-responses payload\n\t\t// so they never leak to Anthropic or Chat Completions backends.\n\t\treturn stripNativeOpenAiWebSearch(payload);\n\t}\n\n\tif (!isRecord(payload)) {\n\t\treturn payload;\n\t}\n\n\tconst tools = Array.isArray(payload.tools) ? payload.tools : [];\n\tconst shouldInjectWebSearch = isOpenaiWebSearchEnabled();\n\tconst sanitized = sanitizeTools(tools, { stripFunctionWebSearch: shouldInjectWebSearch });\n\tconst sanitizedTools = sanitized.tools;\n\tif (!shouldInjectWebSearch) {\n\t\tif (!sanitized.changed) {\n\t\t\treturn payload;\n\t\t}\n\n\t\treturn {\n\t\t\t...payload,\n\t\t\ttools: sanitizedTools,\n\t\t};\n\t}\n\n\tconst hasNativeWebSearch = sanitizedTools.some((tool) => isNativeOpenAiWebSearchType(tool.type));\n\n\tif (!hasNativeWebSearch) {\n\t\tsanitizedTools.push({ type: NATIVE_OPENAI_WEB_SEARCH_TYPE });\n\t}\n\n\treturn {\n\t\t...payload,\n\t\ttools: sanitizedTools,\n\t\tinclude: includeWebSearchSources(payload),\n\t};\n}\n\nexport function isOpenaiWebSearchEnabled(): boolean {\n\treturn parseEnableEnv(ENABLE_ENV);\n}\n\nfunction clearUi(ctx: ExtensionContext): void {\n\tif (!ctx.hasUI) return;\n\tctx.ui.setStatus(STATUS_KEY, undefined);\n\tctx.ui.setWidget(WIDGET_KEY, undefined);\n}\n\nfunction syncUi(ctx: ExtensionContext): void {\n\tclearUi(ctx);\n}\n\nexport const OPENAI_WEB_SEARCH_SECTION = `\n## Web Search\n\nNative web search is available in this session.\nUse web search when the user asks for current or online information.\nPrefer web search over guessing when freshness matters.\n`;\n\nexport default function openaiWebSearchExtension(pi: ExtensionAPI): void {\n\tpi.on(\"before_provider_request\", (event, ctx) => {\n\t\treturn addOpenAiWebSearchToPayload(ctx.model?.api, event.payload);\n\t});\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tsyncUi(ctx);\n\t});\n\n\tpi.on(\"model_select\", async (_event, ctx) => {\n\t\tsyncUi(ctx);\n\t});\n\n\tpi.on(\"session_shutdown\", async (_event, ctx) => {\n\t\tclearUi(ctx);\n\t});\n\n\tpi.on(\"before_agent_start\", async (event, ctx) => {\n\t\tif (!isOpenAiResponsesApi(ctx.model?.api)) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tif (!isOpenaiWebSearchEnabled()) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn {\n\t\t\tsystemPrompt: `${event.systemPrompt}\\n${OPENAI_WEB_SEARCH_SECTION}`,\n\t\t};\n\t});\n}\n"]}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/openai-web-search/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAyB,MAAM,uBAAuB,CAAC;AAC/E,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,gBAAgB,CAAC;AAGrE,KAAK,oBAAoB,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,QAAQ,CAAC,CAAC;AAC3E,KAAK,qBAAqB,GAAG,GAAG,GAAG,oBAAoB,GAAG,SAAS,CAAC;AAsDpE,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAWpF;AA2FD,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,qBAAqB,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAoDpG;AAED,wBAAgB,wBAAwB,IAAI,OAAO,CAElD;AAYD,eAAO,MAAM,yBAAyB,wMAMrC,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,wBAAwB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA8BvE","sourcesContent":["import type { Api, Model, OpenAIResponsesCompat } from \"@earendil-works/pi-ai\";\nimport type { ExtensionAPI, ExtensionContext } from \"../../types.js\";\n\ntype ToolDefinition = Record<string, unknown>;\ntype OpenAiWebSearchModel = Pick<Model<Api>, \"api\" | \"baseUrl\" | \"compat\">;\ntype OpenAiWebSearchTarget = Api | OpenAiWebSearchModel | undefined;\n\nconst OPENAI_RESPONSES_APIS: ReadonlySet<Api> = new Set([\"openai-responses\", \"azure-openai-responses\"]);\nconst ENABLE_ENV = \"PI_OPENAI_WEB_SEARCH\";\nconst NATIVE_OPENAI_WEB_SEARCH_TYPE = \"web_search_preview\";\nconst WEB_SEARCH_SOURCES_INCLUDE = \"web_search_call.action.sources\";\nconst STATUS_KEY = \"openai-web-search\";\nconst WIDGET_KEY = \"openai-web-search\";\n\nfunction parseEnableEnv(envVar: string): boolean {\n\tconst envValue = process.env[envVar];\n\tif (!envValue) {\n\t\treturn true;\n\t}\n\n\tconst normalized = envValue.trim().toLowerCase();\n\tif (normalized === \"0\" || normalized === \"false\" || normalized === \"no\" || normalized === \"off\") {\n\t\treturn false;\n\t}\n\n\tif (normalized === \"1\" || normalized === \"true\" || normalized === \"yes\" || normalized === \"on\") {\n\t\treturn true;\n\t}\n\n\t// Unknown values fall back to default-on behavior.\n\treturn true;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null;\n}\n\nfunction isOpenAiResponsesApi(api: Api | undefined): api is \"openai-responses\" | \"azure-openai-responses\" {\n\treturn api !== undefined && OPENAI_RESPONSES_APIS.has(api);\n}\n\nfunction resolveTarget(target: OpenAiWebSearchTarget): OpenAiWebSearchModel | undefined {\n\tif (target === undefined) {\n\t\treturn undefined;\n\t}\n\tif (typeof target === \"string\") {\n\t\treturn { api: target, baseUrl: target === \"openai-responses\" ? \"https://api.openai.com/v1\" : \"\" };\n\t}\n\treturn target;\n}\n\nfunction isOpenAiResponsesNativeEndpoint(model: OpenAiWebSearchModel): boolean {\n\ttry {\n\t\treturn new URL(model.baseUrl || \"https://api.openai.com/v1\").hostname === \"api.openai.com\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nexport function supportsNativeOpenAiWebSearch(target: OpenAiWebSearchTarget): boolean {\n\tconst model = resolveTarget(target);\n\tif (!isOpenAiResponsesApi(model?.api)) {\n\t\treturn false;\n\t}\n\tif (model.api === \"azure-openai-responses\") {\n\t\treturn true;\n\t}\n\n\tconst compat = model.compat as OpenAIResponsesCompat | undefined;\n\treturn compat?.supportsWebSearchPreview ?? isOpenAiResponsesNativeEndpoint(model);\n}\n\nfunction isNativeOpenAiWebSearchType(value: unknown): value is \"web_search_preview\" | \"web_search_preview_2025_03_11\" {\n\treturn value === \"web_search_preview\" || value === \"web_search_preview_2025_03_11\";\n}\n\nfunction isUnsupportedWebSearchType(value: unknown): boolean {\n\treturn (\n\t\ttypeof value === \"string\" &&\n\t\t(value === \"web_search\" || value.startsWith(\"web_search_\")) &&\n\t\t!isNativeOpenAiWebSearchType(value)\n\t);\n}\n\nfunction isAnthropicWebFetchType(value: unknown): boolean {\n\treturn typeof value === \"string\" && value.startsWith(\"web_fetch_\");\n}\n\ntype SanitizedTools = {\n\tchanged: boolean;\n\ttools: ToolDefinition[];\n};\n\ntype SanitizeToolsOptions = {\n\tstripFunctionWebSearch: boolean;\n};\n\nfunction stripNativeOpenAiWebSearch(payload: unknown): unknown {\n\tif (!isRecord(payload)) {\n\t\treturn payload;\n\t}\n\n\tlet changed = false;\n\tconst sanitized: Record<string, unknown> = { ...payload };\n\n\tconst tools = payload.tools;\n\tif (Array.isArray(tools)) {\n\t\tconst sanitizedTools = tools.filter((tool) => !(isRecord(tool) && isNativeOpenAiWebSearchType(tool.type)));\n\t\tif (sanitizedTools.length !== tools.length) {\n\t\t\tchanged = true;\n\t\t\tsanitized.tools = sanitizedTools;\n\t\t}\n\t}\n\n\tconst include = payload.include;\n\tif (Array.isArray(include)) {\n\t\tconst sanitizedInclude = include.filter((value) => value !== WEB_SEARCH_SOURCES_INCLUDE);\n\t\tif (sanitizedInclude.length !== include.length) {\n\t\t\tchanged = true;\n\t\t\tsanitized.include = sanitizedInclude;\n\t\t}\n\t}\n\n\tif (isRecord(payload.tool_choice) && isNativeOpenAiWebSearchType(payload.tool_choice.type)) {\n\t\tchanged = true;\n\t\tdelete sanitized.tool_choice;\n\t}\n\n\treturn changed ? sanitized : payload;\n}\n\nfunction sanitizeTools(tools: unknown[], options: SanitizeToolsOptions): SanitizedTools {\n\tconst sanitized: ToolDefinition[] = [];\n\tlet changed = false;\n\tfor (const tool of tools) {\n\t\tif (!isRecord(tool)) {\n\t\t\tchanged = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst type = tool.type;\n\t\tconst shouldStripFunctionVariant =\n\t\t\toptions.stripFunctionWebSearch && tool.name === \"web_search\" && !isNativeOpenAiWebSearchType(type);\n\t\tconst shouldStripProviderNativeVariant = isUnsupportedWebSearchType(type) || isAnthropicWebFetchType(type);\n\t\tif (shouldStripFunctionVariant || shouldStripProviderNativeVariant) {\n\t\t\tchanged = true;\n\t\t} else {\n\t\t\tsanitized.push(tool);\n\t\t}\n\t}\n\n\treturn { changed, tools: sanitized };\n}\n\nfunction includeWebSearchSources(payload: Record<string, unknown>): string[] {\n\tconst include = Array.isArray(payload.include)\n\t\t? payload.include.filter((value): value is string => typeof value === \"string\")\n\t\t: [];\n\treturn include.includes(WEB_SEARCH_SOURCES_INCLUDE) ? include : [...include, WEB_SEARCH_SOURCES_INCLUDE];\n}\n\nexport function addOpenAiWebSearchToPayload(target: OpenAiWebSearchTarget, payload: unknown): unknown {\n\tconst model = resolveTarget(target);\n\tif (!isOpenAiResponsesApi(model?.api)) {\n\t\t// Defense in depth. `web_search_preview` is an OpenAI Responses-only tool\n\t\t// type, but proxies that translate openai-responses → anthropic-messages\n\t\t// (e.g., ccapi/quotio for Claude models) can forward it verbatim, which\n\t\t// Anthropic rejects with `tools.N: Input tag 'web_search_preview'...`.\n\t\t// Strip the OpenAI-native variants for any non-openai-responses payload\n\t\t// so they never leak to Anthropic or Chat Completions backends.\n\t\treturn stripNativeOpenAiWebSearch(payload);\n\t}\n\n\tif (!isRecord(payload)) {\n\t\treturn payload;\n\t}\n\n\tconst supportsNativeWebSearch = supportsNativeOpenAiWebSearch(model);\n\tconst tools = Array.isArray(payload.tools) ? payload.tools : [];\n\tconst shouldInjectWebSearch = supportsNativeWebSearch && isOpenaiWebSearchEnabled();\n\tconst strippedPayload = supportsNativeWebSearch ? payload : stripNativeOpenAiWebSearch(payload);\n\tif (!isRecord(strippedPayload)) {\n\t\treturn strippedPayload;\n\t}\n\n\tconst strippedTools = Array.isArray(strippedPayload.tools) ? strippedPayload.tools : [];\n\tconst activeTools = supportsNativeWebSearch ? tools : strippedTools;\n\tconst sanitized = sanitizeTools(tools, { stripFunctionWebSearch: shouldInjectWebSearch });\n\tconst sanitizedTools = sanitized.tools;\n\tif (!shouldInjectWebSearch) {\n\t\tconst nativeStripped = strippedPayload !== payload;\n\t\tconst passiveSanitized = sanitizeTools(activeTools, { stripFunctionWebSearch: false });\n\t\tif (!nativeStripped && !passiveSanitized.changed) {\n\t\t\treturn strippedPayload;\n\t\t}\n\n\t\treturn {\n\t\t\t...strippedPayload,\n\t\t\ttools: passiveSanitized.tools,\n\t\t};\n\t}\n\n\tconst hasNativeWebSearch = sanitizedTools.some((tool) => isNativeOpenAiWebSearchType(tool.type));\n\n\tif (!hasNativeWebSearch) {\n\t\tsanitizedTools.push({ type: NATIVE_OPENAI_WEB_SEARCH_TYPE });\n\t}\n\n\treturn {\n\t\t...payload,\n\t\ttools: sanitizedTools,\n\t\tinclude: includeWebSearchSources(payload),\n\t};\n}\n\nexport function isOpenaiWebSearchEnabled(): boolean {\n\treturn parseEnableEnv(ENABLE_ENV);\n}\n\nfunction clearUi(ctx: ExtensionContext): void {\n\tif (!ctx.hasUI) return;\n\tctx.ui.setStatus(STATUS_KEY, undefined);\n\tctx.ui.setWidget(WIDGET_KEY, undefined);\n}\n\nfunction syncUi(ctx: ExtensionContext): void {\n\tclearUi(ctx);\n}\n\nexport const OPENAI_WEB_SEARCH_SECTION = `\n## Web Search\n\nNative web search is available in this session.\nUse web search when the user asks for current or online information.\nPrefer web search over guessing when freshness matters.\n`;\n\nexport default function openaiWebSearchExtension(pi: ExtensionAPI): void {\n\tpi.on(\"before_provider_request\", (event, ctx) => {\n\t\treturn addOpenAiWebSearchToPayload(ctx.model, event.payload);\n\t});\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tsyncUi(ctx);\n\t});\n\n\tpi.on(\"model_select\", async (_event, ctx) => {\n\t\tsyncUi(ctx);\n\t});\n\n\tpi.on(\"session_shutdown\", async (_event, ctx) => {\n\t\tclearUi(ctx);\n\t});\n\n\tpi.on(\"before_agent_start\", async (event, ctx) => {\n\t\tif (!supportsNativeOpenAiWebSearch(ctx.model)) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tif (!isOpenaiWebSearchEnabled()) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn {\n\t\t\tsystemPrompt: `${event.systemPrompt}\\n${OPENAI_WEB_SEARCH_SECTION}`,\n\t\t};\n\t});\n}\n"]}
@@ -25,6 +25,34 @@ function isRecord(value) {
25
25
  function isOpenAiResponsesApi(api) {
26
26
  return api !== undefined && OPENAI_RESPONSES_APIS.has(api);
27
27
  }
28
+ function resolveTarget(target) {
29
+ if (target === undefined) {
30
+ return undefined;
31
+ }
32
+ if (typeof target === "string") {
33
+ return { api: target, baseUrl: target === "openai-responses" ? "https://api.openai.com/v1" : "" };
34
+ }
35
+ return target;
36
+ }
37
+ function isOpenAiResponsesNativeEndpoint(model) {
38
+ try {
39
+ return new URL(model.baseUrl || "https://api.openai.com/v1").hostname === "api.openai.com";
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ export function supportsNativeOpenAiWebSearch(target) {
46
+ const model = resolveTarget(target);
47
+ if (!isOpenAiResponsesApi(model?.api)) {
48
+ return false;
49
+ }
50
+ if (model.api === "azure-openai-responses") {
51
+ return true;
52
+ }
53
+ const compat = model.compat;
54
+ return compat?.supportsWebSearchPreview ?? isOpenAiResponsesNativeEndpoint(model);
55
+ }
28
56
  function isNativeOpenAiWebSearchType(value) {
29
57
  return value === "web_search_preview" || value === "web_search_preview_2025_03_11";
30
58
  }
@@ -40,20 +68,29 @@ function stripNativeOpenAiWebSearch(payload) {
40
68
  if (!isRecord(payload)) {
41
69
  return payload;
42
70
  }
71
+ let changed = false;
72
+ const sanitized = { ...payload };
43
73
  const tools = payload.tools;
44
- if (!Array.isArray(tools)) {
45
- return payload;
74
+ if (Array.isArray(tools)) {
75
+ const sanitizedTools = tools.filter((tool) => !(isRecord(tool) && isNativeOpenAiWebSearchType(tool.type)));
76
+ if (sanitizedTools.length !== tools.length) {
77
+ changed = true;
78
+ sanitized.tools = sanitizedTools;
79
+ }
46
80
  }
47
- let changed = false;
48
- const sanitized = [];
49
- for (const tool of tools) {
50
- if (isRecord(tool) && isNativeOpenAiWebSearchType(tool.type)) {
81
+ const include = payload.include;
82
+ if (Array.isArray(include)) {
83
+ const sanitizedInclude = include.filter((value) => value !== WEB_SEARCH_SOURCES_INCLUDE);
84
+ if (sanitizedInclude.length !== include.length) {
51
85
  changed = true;
52
- continue;
86
+ sanitized.include = sanitizedInclude;
53
87
  }
54
- sanitized.push(tool);
55
88
  }
56
- return changed ? { ...payload, tools: sanitized } : payload;
89
+ if (isRecord(payload.tool_choice) && isNativeOpenAiWebSearchType(payload.tool_choice.type)) {
90
+ changed = true;
91
+ delete sanitized.tool_choice;
92
+ }
93
+ return changed ? sanitized : payload;
57
94
  }
58
95
  function sanitizeTools(tools, options) {
59
96
  const sanitized = [];
@@ -81,8 +118,9 @@ function includeWebSearchSources(payload) {
81
118
  : [];
82
119
  return include.includes(WEB_SEARCH_SOURCES_INCLUDE) ? include : [...include, WEB_SEARCH_SOURCES_INCLUDE];
83
120
  }
84
- export function addOpenAiWebSearchToPayload(api, payload) {
85
- if (!isOpenAiResponsesApi(api)) {
121
+ export function addOpenAiWebSearchToPayload(target, payload) {
122
+ const model = resolveTarget(target);
123
+ if (!isOpenAiResponsesApi(model?.api)) {
86
124
  // Defense in depth. `web_search_preview` is an OpenAI Responses-only tool
87
125
  // type, but proxies that translate openai-responses → anthropic-messages
88
126
  // (e.g., ccapi/quotio for Claude models) can forward it verbatim, which
@@ -94,17 +132,26 @@ export function addOpenAiWebSearchToPayload(api, payload) {
94
132
  if (!isRecord(payload)) {
95
133
  return payload;
96
134
  }
135
+ const supportsNativeWebSearch = supportsNativeOpenAiWebSearch(model);
97
136
  const tools = Array.isArray(payload.tools) ? payload.tools : [];
98
- const shouldInjectWebSearch = isOpenaiWebSearchEnabled();
137
+ const shouldInjectWebSearch = supportsNativeWebSearch && isOpenaiWebSearchEnabled();
138
+ const strippedPayload = supportsNativeWebSearch ? payload : stripNativeOpenAiWebSearch(payload);
139
+ if (!isRecord(strippedPayload)) {
140
+ return strippedPayload;
141
+ }
142
+ const strippedTools = Array.isArray(strippedPayload.tools) ? strippedPayload.tools : [];
143
+ const activeTools = supportsNativeWebSearch ? tools : strippedTools;
99
144
  const sanitized = sanitizeTools(tools, { stripFunctionWebSearch: shouldInjectWebSearch });
100
145
  const sanitizedTools = sanitized.tools;
101
146
  if (!shouldInjectWebSearch) {
102
- if (!sanitized.changed) {
103
- return payload;
147
+ const nativeStripped = strippedPayload !== payload;
148
+ const passiveSanitized = sanitizeTools(activeTools, { stripFunctionWebSearch: false });
149
+ if (!nativeStripped && !passiveSanitized.changed) {
150
+ return strippedPayload;
104
151
  }
105
152
  return {
106
- ...payload,
107
- tools: sanitizedTools,
153
+ ...strippedPayload,
154
+ tools: passiveSanitized.tools,
108
155
  };
109
156
  }
110
157
  const hasNativeWebSearch = sanitizedTools.some((tool) => isNativeOpenAiWebSearchType(tool.type));
@@ -138,7 +185,7 @@ Prefer web search over guessing when freshness matters.
138
185
  `;
139
186
  export default function openaiWebSearchExtension(pi) {
140
187
  pi.on("before_provider_request", (event, ctx) => {
141
- return addOpenAiWebSearchToPayload(ctx.model?.api, event.payload);
188
+ return addOpenAiWebSearchToPayload(ctx.model, event.payload);
142
189
  });
143
190
  pi.on("session_start", async (_event, ctx) => {
144
191
  syncUi(ctx);
@@ -150,7 +197,7 @@ export default function openaiWebSearchExtension(pi) {
150
197
  clearUi(ctx);
151
198
  });
152
199
  pi.on("before_agent_start", async (event, ctx) => {
153
- if (!isOpenAiResponsesApi(ctx.model?.api)) {
200
+ if (!supportsNativeOpenAiWebSearch(ctx.model)) {
154
201
  return undefined;
155
202
  }
156
203
  if (!isOpenaiWebSearchEnabled()) {