@librechat/agents 3.4.7 → 3.5.0
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.
- package/dist/cjs/agents/AgentContext.cjs +0 -356
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/agents/projection.cjs +0 -9
- package/dist/cjs/agents/projection.cjs.map +1 -1
- package/dist/cjs/common/constants.cjs +0 -26
- package/dist/cjs/common/constants.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +0 -85
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/events.cjs +0 -14
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +1 -909
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +7 -272
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/graphs/applyGraphRuntimeConfig.cjs.map +1 -1
- package/dist/cjs/graphs/createGraph.cjs.map +1 -1
- package/dist/cjs/hitl/askUserQuestion.cjs +0 -53
- package/dist/cjs/hitl/askUserQuestion.cjs.map +1 -1
- package/dist/cjs/hitl/askUserQuestions.cjs +0 -20
- package/dist/cjs/hitl/askUserQuestions.cjs.map +1 -1
- package/dist/cjs/hitl/askUserQuestionsInterrupt.cjs +0 -6
- package/dist/cjs/hitl/askUserQuestionsInterrupt.cjs.map +1 -1
- package/dist/cjs/hooks/HookRegistry.cjs +0 -127
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
- package/dist/cjs/hooks/createToolPolicyHook.cjs +0 -38
- package/dist/cjs/hooks/createToolPolicyHook.cjs.map +1 -1
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +0 -77
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs.map +1 -1
- package/dist/cjs/hooks/executeHooks.cjs +0 -79
- package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
- package/dist/cjs/hooks/index.cjs +0 -16
- package/dist/cjs/hooks/index.cjs.map +1 -1
- package/dist/cjs/hooks/matchers.cjs +0 -104
- package/dist/cjs/hooks/matchers.cjs.map +1 -1
- package/dist/cjs/hooks/types.cjs +0 -8
- package/dist/cjs/hooks/types.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +0 -14
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +0 -75
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/langfuseConfig.cjs.map +1 -1
- package/dist/cjs/langfuseRuntimeContext.cjs +0 -13
- package/dist/cjs/langfuseRuntimeContext.cjs.map +1 -1
- package/dist/cjs/langfuseRuntimeScope.cjs +0 -4
- package/dist/cjs/langfuseRuntimeScope.cjs.map +1 -1
- package/dist/cjs/langfuseSpanRegistry.cjs +0 -28
- package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +0 -3
- package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
- package/dist/cjs/langfuseTraceShaping.cjs +0 -74
- package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/index.cjs +0 -3
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +1 -111
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +0 -1
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/stream_events.cjs +0 -8
- package/dist/cjs/llm/anthropic/utils/stream_events.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/tools.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/cachePoints.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +1 -95
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/toolCache.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +0 -78
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_outputs.cjs +0 -24
- package/dist/cjs/llm/bedrock/utils/message_outputs.cjs.map +1 -1
- package/dist/cjs/llm/contextOverflowRecovery.cjs +0 -62
- package/dist/cjs/llm/contextOverflowRecovery.cjs.map +1 -1
- package/dist/cjs/llm/fake.cjs.map +1 -1
- package/dist/cjs/llm/google/index.cjs +2 -21
- package/dist/cjs/llm/google/index.cjs.map +1 -1
- package/dist/cjs/llm/google/utils/common.cjs +0 -35
- package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +0 -9
- package/dist/cjs/llm/init.cjs.map +1 -1
- package/dist/cjs/llm/invoke.cjs +1 -270
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/mistral/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +0 -99
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/streamMetadata.cjs +0 -21
- package/dist/cjs/llm/openai/streamMetadata.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +0 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/toolCache.cjs +0 -6
- package/dist/cjs/llm/openrouter/toolCache.cjs.map +1 -1
- package/dist/cjs/llm/preempt.cjs +0 -75
- package/dist/cjs/llm/preempt.cjs.map +1 -1
- package/dist/cjs/llm/providers.cjs.map +1 -1
- package/dist/cjs/llm/request.cjs +0 -10
- package/dist/cjs/llm/request.cjs.map +1 -1
- package/dist/cjs/llm/stream/chunkAdapters.cjs +0 -62
- package/dist/cjs/llm/stream/chunkAdapters.cjs.map +1 -1
- package/dist/cjs/llm/stream/smoother.cjs +0 -58
- package/dist/cjs/llm/stream/smoother.cjs.map +1 -1
- package/dist/cjs/llm/streamLimits.cjs +4 -293
- package/dist/cjs/llm/streamLimits.cjs.map +1 -1
- package/dist/cjs/llm/truncation.cjs +0 -36
- package/dist/cjs/llm/truncation.cjs.map +1 -1
- package/dist/cjs/llm/vertexai/index.cjs +0 -330
- package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
- package/dist/cjs/messages/alternation.cjs +0 -43
- package/dist/cjs/messages/alternation.cjs.map +1 -1
- package/dist/cjs/messages/anthropicToolCache.cjs +0 -45
- package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -1
- package/dist/cjs/messages/assistantPhase.cjs +0 -12
- package/dist/cjs/messages/assistantPhase.cjs.map +1 -1
- package/dist/cjs/messages/budget.cjs +0 -9
- package/dist/cjs/messages/budget.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +0 -172
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/content.cjs +0 -9
- package/dist/cjs/messages/content.cjs.map +1 -1
- package/dist/cjs/messages/contextPruning.cjs +0 -15
- package/dist/cjs/messages/contextPruning.cjs.map +1 -1
- package/dist/cjs/messages/contextPruningSettings.cjs +0 -3
- package/dist/cjs/messages/contextPruningSettings.cjs.map +1 -1
- package/dist/cjs/messages/core.cjs +0 -48
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +1 -345
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/handoffCue.cjs +0 -36
- package/dist/cjs/messages/handoffCue.cjs.map +1 -1
- package/dist/cjs/messages/ids.cjs.map +1 -1
- package/dist/cjs/messages/injected.cjs +0 -33
- package/dist/cjs/messages/injected.cjs.map +1 -1
- package/dist/cjs/messages/langchain.cjs +0 -14
- package/dist/cjs/messages/langchain.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +1 -184
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/messages/recency.cjs +0 -32
- package/dist/cjs/messages/recency.cjs.map +1 -1
- package/dist/cjs/messages/reducer.cjs +0 -30
- package/dist/cjs/messages/reducer.cjs.map +1 -1
- package/dist/cjs/messages/tools.cjs +0 -14
- package/dist/cjs/messages/tools.cjs.map +1 -1
- package/dist/cjs/openai/index.cjs.map +1 -1
- package/dist/cjs/prompts/activityLabel.cjs +9 -79
- package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
- package/dist/cjs/responses/index.cjs.map +1 -1
- package/dist/cjs/run.cjs +2 -538
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +1 -17
- package/dist/cjs/session/AgentSession.cjs.map +1 -1
- package/dist/cjs/session/JsonlSessionStore.cjs.map +1 -1
- package/dist/cjs/session/handlers.cjs +0 -7
- package/dist/cjs/session/handlers.cjs.map +1 -1
- package/dist/cjs/session/ids.cjs.map +1 -1
- package/dist/cjs/session/messageSerialization.cjs +0 -4
- package/dist/cjs/session/messageSerialization.cjs.map +1 -1
- package/dist/cjs/stream.cjs +1 -93
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/index.cjs +0 -27
- package/dist/cjs/summarization/index.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +0 -273
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +0 -35
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +0 -24
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/Calculator.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +0 -13
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/CodeSessionFileSummary.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +0 -101
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ReadFile.cjs.map +1 -1
- package/dist/cjs/tools/SkillTool.cjs +0 -5
- package/dist/cjs/tools/SkillTool.cjs.map +1 -1
- package/dist/cjs/tools/SubagentTool.cjs +0 -10
- package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +6 -1038
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +0 -179
- package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
- package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs.map +1 -1
- package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +0 -39
- package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -1
- package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs.map +1 -1
- package/dist/cjs/tools/eagerEventExecution.cjs +0 -6
- package/dist/cjs/tools/eagerEventExecution.cjs.map +1 -1
- package/dist/cjs/tools/handlers.cjs +1 -41
- package/dist/cjs/tools/handlers.cjs.map +1 -1
- package/dist/cjs/tools/intentArg.cjs +1 -132
- package/dist/cjs/tools/intentArg.cjs.map +1 -1
- package/dist/cjs/tools/local/CompileCheckTool.cjs +0 -25
- package/dist/cjs/tools/local/CompileCheckTool.cjs.map +1 -1
- package/dist/cjs/tools/local/FileCheckpointer.cjs +0 -23
- package/dist/cjs/tools/local/FileCheckpointer.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalCodingTools.cjs +0 -80
- package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs +0 -126
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalExecutionTools.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +0 -12
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/local/attachments.cjs +0 -44
- package/dist/cjs/tools/local/attachments.cjs.map +1 -1
- package/dist/cjs/tools/local/bashAst.cjs +0 -16
- package/dist/cjs/tools/local/bashAst.cjs.map +1 -1
- package/dist/cjs/tools/local/editStrategies.cjs +0 -14
- package/dist/cjs/tools/local/editStrategies.cjs.map +1 -1
- package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -1
- package/dist/cjs/tools/local/syntaxCheck.cjs +0 -31
- package/dist/cjs/tools/local/syntaxCheck.cjs.map +1 -1
- package/dist/cjs/tools/local/textEncoding.cjs +0 -11
- package/dist/cjs/tools/local/textEncoding.cjs.map +1 -1
- package/dist/cjs/tools/local/workspaceFS.cjs +0 -33
- package/dist/cjs/tools/local/workspaceFS.cjs.map +1 -1
- package/dist/cjs/tools/ptcTimeout.cjs.map +1 -1
- package/dist/cjs/tools/schema.cjs +0 -8
- package/dist/cjs/tools/schema.cjs.map +1 -1
- package/dist/cjs/tools/search/anthropic.cjs +0 -9
- package/dist/cjs/tools/search/anthropic.cjs.map +1 -1
- package/dist/cjs/tools/search/content.cjs.map +1 -1
- package/dist/cjs/tools/search/crw-scraper.cjs +0 -33
- package/dist/cjs/tools/search/crw-scraper.cjs.map +1 -1
- package/dist/cjs/tools/search/crw-search.cjs +0 -2
- package/dist/cjs/tools/search/crw-search.cjs.map +1 -1
- package/dist/cjs/tools/search/firecrawl.cjs +0 -25
- package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
- package/dist/cjs/tools/search/format.cjs +0 -30
- package/dist/cjs/tools/search/format.cjs.map +1 -1
- package/dist/cjs/tools/search/highlights.cjs +0 -16
- package/dist/cjs/tools/search/highlights.cjs.map +1 -1
- package/dist/cjs/tools/search/keenable-scraper.cjs +0 -8
- package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -1
- package/dist/cjs/tools/search/keenable-search.cjs +0 -7
- package/dist/cjs/tools/search/keenable-search.cjs.map +1 -1
- package/dist/cjs/tools/search/rerankers.cjs +0 -35
- package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
- package/dist/cjs/tools/search/schema.cjs +0 -1
- package/dist/cjs/tools/search/schema.cjs.map +1 -1
- package/dist/cjs/tools/search/search.cjs +1 -14
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/serper-scraper.cjs +0 -46
- package/dist/cjs/tools/search/serper-scraper.cjs.map +1 -1
- package/dist/cjs/tools/search/tavily-scraper.cjs.map +1 -1
- package/dist/cjs/tools/search/tavily-search.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +0 -25
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/search/utils.cjs +0 -8
- package/dist/cjs/tools/search/utils.cjs.map +1 -1
- package/dist/cjs/tools/skillCatalog.cjs +0 -6
- package/dist/cjs/tools/skillCatalog.cjs.map +1 -1
- package/dist/cjs/tools/streamedToolCallSeals.cjs +0 -6
- package/dist/cjs/tools/streamedToolCallSeals.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs +0 -2
- package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +3 -213
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
- package/dist/cjs/tools/subagent/childGraphConfig.cjs +0 -4
- package/dist/cjs/tools/subagent/childGraphConfig.cjs.map +1 -1
- package/dist/cjs/tools/subagent/runtimeLimits.cjs.map +1 -1
- package/dist/cjs/tools/toolOutputReferences.cjs +1 -322
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/types/hitl.cjs +0 -5
- package/dist/cjs/types/hitl.cjs.map +1 -1
- package/dist/cjs/utils/callbacks.cjs.map +1 -1
- package/dist/cjs/utils/errors.cjs +14 -165
- package/dist/cjs/utils/errors.cjs.map +1 -1
- package/dist/cjs/utils/events.cjs +0 -10
- package/dist/cjs/utils/events.cjs.map +1 -1
- package/dist/cjs/utils/graph.cjs.map +1 -1
- package/dist/cjs/utils/handlers.cjs +0 -3
- package/dist/cjs/utils/handlers.cjs.map +1 -1
- package/dist/cjs/utils/llm.cjs +0 -1
- package/dist/cjs/utils/llm.cjs.map +1 -1
- package/dist/cjs/utils/misc.cjs +0 -16
- package/dist/cjs/utils/misc.cjs.map +1 -1
- package/dist/cjs/utils/proxy.cjs +0 -26
- package/dist/cjs/utils/proxy.cjs.map +1 -1
- package/dist/cjs/utils/run.cjs +0 -6
- package/dist/cjs/utils/run.cjs.map +1 -1
- package/dist/cjs/utils/schema.cjs +0 -5
- package/dist/cjs/utils/schema.cjs.map +1 -1
- package/dist/cjs/utils/title.cjs +0 -6
- package/dist/cjs/utils/title.cjs.map +1 -1
- package/dist/cjs/utils/tokens.cjs +0 -113
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/cjs/utils/toolContent.cjs +0 -41
- package/dist/cjs/utils/toolContent.cjs.map +1 -1
- package/dist/cjs/utils/truncation.cjs +0 -61
- package/dist/cjs/utils/truncation.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +0 -356
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/agents/projection.mjs +0 -9
- package/dist/esm/agents/projection.mjs.map +1 -1
- package/dist/esm/common/constants.mjs +0 -26
- package/dist/esm/common/constants.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +0 -85
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/events.mjs +0 -14
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +1 -909
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +7 -272
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/graphs/applyGraphRuntimeConfig.mjs.map +1 -1
- package/dist/esm/graphs/createGraph.mjs.map +1 -1
- package/dist/esm/hitl/askUserQuestion.mjs +0 -53
- package/dist/esm/hitl/askUserQuestion.mjs.map +1 -1
- package/dist/esm/hitl/askUserQuestions.mjs +0 -20
- package/dist/esm/hitl/askUserQuestions.mjs.map +1 -1
- package/dist/esm/hitl/askUserQuestionsInterrupt.mjs +0 -6
- package/dist/esm/hitl/askUserQuestionsInterrupt.mjs.map +1 -1
- package/dist/esm/hooks/HookRegistry.mjs +0 -127
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
- package/dist/esm/hooks/createToolPolicyHook.mjs +0 -38
- package/dist/esm/hooks/createToolPolicyHook.mjs.map +1 -1
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs +0 -77
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs.map +1 -1
- package/dist/esm/hooks/executeHooks.mjs +0 -79
- package/dist/esm/hooks/executeHooks.mjs.map +1 -1
- package/dist/esm/hooks/index.mjs +0 -16
- package/dist/esm/hooks/index.mjs.map +1 -1
- package/dist/esm/hooks/matchers.mjs +0 -104
- package/dist/esm/hooks/matchers.mjs.map +1 -1
- package/dist/esm/hooks/types.mjs +0 -8
- package/dist/esm/hooks/types.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +0 -14
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +0 -75
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/langfuseConfig.mjs.map +1 -1
- package/dist/esm/langfuseRuntimeContext.mjs +0 -13
- package/dist/esm/langfuseRuntimeContext.mjs.map +1 -1
- package/dist/esm/langfuseRuntimeScope.mjs +0 -4
- package/dist/esm/langfuseRuntimeScope.mjs.map +1 -1
- package/dist/esm/langfuseSpanRegistry.mjs +0 -28
- package/dist/esm/langfuseSpanRegistry.mjs.map +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +0 -3
- package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
- package/dist/esm/langfuseTraceShaping.mjs +0 -74
- package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +0 -3
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/anthropic/types.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +1 -111
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs +0 -1
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/stream_events.mjs +0 -8
- package/dist/esm/llm/anthropic/utils/stream_events.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/tools.mjs.map +1 -1
- package/dist/esm/llm/bedrock/cachePoints.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +1 -95
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/bedrock/toolCache.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +0 -78
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_outputs.mjs +0 -24
- package/dist/esm/llm/bedrock/utils/message_outputs.mjs.map +1 -1
- package/dist/esm/llm/contextOverflowRecovery.mjs +0 -62
- package/dist/esm/llm/contextOverflowRecovery.mjs.map +1 -1
- package/dist/esm/llm/fake.mjs.map +1 -1
- package/dist/esm/llm/google/index.mjs +2 -21
- package/dist/esm/llm/google/index.mjs.map +1 -1
- package/dist/esm/llm/google/utils/common.mjs +0 -35
- package/dist/esm/llm/google/utils/common.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +0 -9
- package/dist/esm/llm/init.mjs.map +1 -1
- package/dist/esm/llm/invoke.mjs +1 -270
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/mistral/index.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +0 -99
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/streamMetadata.mjs +0 -21
- package/dist/esm/llm/openai/streamMetadata.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +0 -1
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/toolCache.mjs +0 -6
- package/dist/esm/llm/openrouter/toolCache.mjs.map +1 -1
- package/dist/esm/llm/preempt.mjs +0 -75
- package/dist/esm/llm/preempt.mjs.map +1 -1
- package/dist/esm/llm/providers.mjs.map +1 -1
- package/dist/esm/llm/request.mjs +0 -10
- package/dist/esm/llm/request.mjs.map +1 -1
- package/dist/esm/llm/stream/chunkAdapters.mjs +0 -62
- package/dist/esm/llm/stream/chunkAdapters.mjs.map +1 -1
- package/dist/esm/llm/stream/smoother.mjs +0 -58
- package/dist/esm/llm/stream/smoother.mjs.map +1 -1
- package/dist/esm/llm/streamLimits.mjs +4 -293
- package/dist/esm/llm/streamLimits.mjs.map +1 -1
- package/dist/esm/llm/truncation.mjs +0 -36
- package/dist/esm/llm/truncation.mjs.map +1 -1
- package/dist/esm/llm/vertexai/index.mjs +0 -330
- package/dist/esm/llm/vertexai/index.mjs.map +1 -1
- package/dist/esm/messages/alternation.mjs +0 -43
- package/dist/esm/messages/alternation.mjs.map +1 -1
- package/dist/esm/messages/anthropicToolCache.mjs +0 -45
- package/dist/esm/messages/anthropicToolCache.mjs.map +1 -1
- package/dist/esm/messages/assistantPhase.mjs +0 -12
- package/dist/esm/messages/assistantPhase.mjs.map +1 -1
- package/dist/esm/messages/budget.mjs +0 -9
- package/dist/esm/messages/budget.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +0 -172
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/content.mjs +0 -9
- package/dist/esm/messages/content.mjs.map +1 -1
- package/dist/esm/messages/contextPruning.mjs +0 -15
- package/dist/esm/messages/contextPruning.mjs.map +1 -1
- package/dist/esm/messages/contextPruningSettings.mjs +0 -3
- package/dist/esm/messages/contextPruningSettings.mjs.map +1 -1
- package/dist/esm/messages/core.mjs +0 -48
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +1 -345
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/handoffCue.mjs +0 -36
- package/dist/esm/messages/handoffCue.mjs.map +1 -1
- package/dist/esm/messages/ids.mjs.map +1 -1
- package/dist/esm/messages/injected.mjs +0 -33
- package/dist/esm/messages/injected.mjs.map +1 -1
- package/dist/esm/messages/langchain.mjs +0 -14
- package/dist/esm/messages/langchain.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +1 -184
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/messages/recency.mjs +0 -32
- package/dist/esm/messages/recency.mjs.map +1 -1
- package/dist/esm/messages/reducer.mjs +0 -30
- package/dist/esm/messages/reducer.mjs.map +1 -1
- package/dist/esm/messages/tools.mjs +0 -14
- package/dist/esm/messages/tools.mjs.map +1 -1
- package/dist/esm/openai/index.mjs.map +1 -1
- package/dist/esm/prompts/activityLabel.mjs +9 -79
- package/dist/esm/prompts/activityLabel.mjs.map +1 -1
- package/dist/esm/responses/index.mjs.map +1 -1
- package/dist/esm/run.mjs +2 -538
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +1 -17
- package/dist/esm/session/AgentSession.mjs.map +1 -1
- package/dist/esm/session/JsonlSessionStore.mjs.map +1 -1
- package/dist/esm/session/handlers.mjs +0 -7
- package/dist/esm/session/handlers.mjs.map +1 -1
- package/dist/esm/session/ids.mjs.map +1 -1
- package/dist/esm/session/messageSerialization.mjs +0 -4
- package/dist/esm/session/messageSerialization.mjs.map +1 -1
- package/dist/esm/stream.mjs +1 -93
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/index.mjs +0 -27
- package/dist/esm/summarization/index.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +0 -273
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +0 -35
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +0 -24
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/Calculator.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +0 -13
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/CodeSessionFileSummary.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +0 -101
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ReadFile.mjs.map +1 -1
- package/dist/esm/tools/SkillTool.mjs +0 -5
- package/dist/esm/tools/SkillTool.mjs.map +1 -1
- package/dist/esm/tools/SubagentTool.mjs +0 -10
- package/dist/esm/tools/SubagentTool.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +6 -1038
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +0 -179
- package/dist/esm/tools/ToolSearch.mjs.map +1 -1
- package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs.map +1 -1
- package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +0 -39
- package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -1
- package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs.map +1 -1
- package/dist/esm/tools/eagerEventExecution.mjs +0 -6
- package/dist/esm/tools/eagerEventExecution.mjs.map +1 -1
- package/dist/esm/tools/handlers.mjs +1 -41
- package/dist/esm/tools/handlers.mjs.map +1 -1
- package/dist/esm/tools/intentArg.mjs +1 -132
- package/dist/esm/tools/intentArg.mjs.map +1 -1
- package/dist/esm/tools/local/CompileCheckTool.mjs +0 -25
- package/dist/esm/tools/local/CompileCheckTool.mjs.map +1 -1
- package/dist/esm/tools/local/FileCheckpointer.mjs +0 -23
- package/dist/esm/tools/local/FileCheckpointer.mjs.map +1 -1
- package/dist/esm/tools/local/LocalCodingTools.mjs +0 -80
- package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -1
- package/dist/esm/tools/local/LocalExecutionEngine.mjs +0 -126
- package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
- package/dist/esm/tools/local/LocalExecutionTools.mjs.map +1 -1
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +0 -12
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/local/attachments.mjs +0 -44
- package/dist/esm/tools/local/attachments.mjs.map +1 -1
- package/dist/esm/tools/local/bashAst.mjs +0 -16
- package/dist/esm/tools/local/bashAst.mjs.map +1 -1
- package/dist/esm/tools/local/editStrategies.mjs +0 -14
- package/dist/esm/tools/local/editStrategies.mjs.map +1 -1
- package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -1
- package/dist/esm/tools/local/syntaxCheck.mjs +0 -31
- package/dist/esm/tools/local/syntaxCheck.mjs.map +1 -1
- package/dist/esm/tools/local/textEncoding.mjs +0 -11
- package/dist/esm/tools/local/textEncoding.mjs.map +1 -1
- package/dist/esm/tools/local/workspaceFS.mjs +0 -33
- package/dist/esm/tools/local/workspaceFS.mjs.map +1 -1
- package/dist/esm/tools/ptcTimeout.mjs.map +1 -1
- package/dist/esm/tools/schema.mjs +0 -8
- package/dist/esm/tools/schema.mjs.map +1 -1
- package/dist/esm/tools/search/anthropic.mjs +0 -9
- package/dist/esm/tools/search/anthropic.mjs.map +1 -1
- package/dist/esm/tools/search/content.mjs.map +1 -1
- package/dist/esm/tools/search/crw-scraper.mjs +0 -33
- package/dist/esm/tools/search/crw-scraper.mjs.map +1 -1
- package/dist/esm/tools/search/crw-search.mjs +0 -2
- package/dist/esm/tools/search/crw-search.mjs.map +1 -1
- package/dist/esm/tools/search/firecrawl.mjs +0 -25
- package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
- package/dist/esm/tools/search/format.mjs +0 -30
- package/dist/esm/tools/search/format.mjs.map +1 -1
- package/dist/esm/tools/search/highlights.mjs +0 -16
- package/dist/esm/tools/search/highlights.mjs.map +1 -1
- package/dist/esm/tools/search/keenable-scraper.mjs +0 -8
- package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -1
- package/dist/esm/tools/search/keenable-search.mjs +0 -7
- package/dist/esm/tools/search/keenable-search.mjs.map +1 -1
- package/dist/esm/tools/search/rerankers.mjs +0 -35
- package/dist/esm/tools/search/rerankers.mjs.map +1 -1
- package/dist/esm/tools/search/schema.mjs +0 -1
- package/dist/esm/tools/search/schema.mjs.map +1 -1
- package/dist/esm/tools/search/search.mjs +1 -14
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/serper-scraper.mjs +0 -46
- package/dist/esm/tools/search/serper-scraper.mjs.map +1 -1
- package/dist/esm/tools/search/tavily-scraper.mjs.map +1 -1
- package/dist/esm/tools/search/tavily-search.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +0 -25
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/search/utils.mjs +0 -8
- package/dist/esm/tools/search/utils.mjs.map +1 -1
- package/dist/esm/tools/skillCatalog.mjs +0 -6
- package/dist/esm/tools/skillCatalog.mjs.map +1 -1
- package/dist/esm/tools/streamedToolCallSeals.mjs +0 -6
- package/dist/esm/tools/streamedToolCallSeals.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs +0 -2
- package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +3 -213
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
- package/dist/esm/tools/subagent/childGraphConfig.mjs +0 -4
- package/dist/esm/tools/subagent/childGraphConfig.mjs.map +1 -1
- package/dist/esm/tools/subagent/runtimeLimits.mjs.map +1 -1
- package/dist/esm/tools/toolOutputReferences.mjs +1 -322
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/types/hitl.mjs +0 -5
- package/dist/esm/types/hitl.mjs.map +1 -1
- package/dist/esm/utils/callbacks.mjs.map +1 -1
- package/dist/esm/utils/errors.mjs +14 -165
- package/dist/esm/utils/errors.mjs.map +1 -1
- package/dist/esm/utils/events.mjs +0 -10
- package/dist/esm/utils/events.mjs.map +1 -1
- package/dist/esm/utils/graph.mjs.map +1 -1
- package/dist/esm/utils/handlers.mjs +0 -3
- package/dist/esm/utils/handlers.mjs.map +1 -1
- package/dist/esm/utils/llm.mjs +0 -1
- package/dist/esm/utils/llm.mjs.map +1 -1
- package/dist/esm/utils/misc.mjs +0 -16
- package/dist/esm/utils/misc.mjs.map +1 -1
- package/dist/esm/utils/proxy.mjs +0 -26
- package/dist/esm/utils/proxy.mjs.map +1 -1
- package/dist/esm/utils/run.mjs +0 -6
- package/dist/esm/utils/run.mjs.map +1 -1
- package/dist/esm/utils/schema.mjs +0 -5
- package/dist/esm/utils/schema.mjs.map +1 -1
- package/dist/esm/utils/title.mjs +0 -6
- package/dist/esm/utils/title.mjs.map +1 -1
- package/dist/esm/utils/tokens.mjs +0 -113
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/esm/utils/toolContent.mjs +0 -41
- package/dist/esm/utils/toolContent.mjs.map +1 -1
- package/dist/esm/utils/truncation.mjs +0 -61
- package/dist/esm/utils/truncation.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +3 -3
- package/dist/types/agents/__tests__/promptCacheLiveHelpers.d.ts +3 -3
- package/dist/types/agents/projection.d.ts +1 -1
- package/dist/types/events.d.ts +2 -2
- package/dist/types/graphs/Graph.d.ts +11 -11
- package/dist/types/graphs/MultiAgentGraph.d.ts +2 -2
- package/dist/types/graphs/createGraph.d.ts +4 -4
- package/dist/types/graphs/graphFactory.d.ts +1 -1
- package/dist/types/hitl/askUserQuestion.d.ts +1 -1
- package/dist/types/hitl/askUserQuestions.d.ts +1 -1
- package/dist/types/hitl/askUserQuestionsInterrupt.d.ts +1 -1
- package/dist/types/hooks/types.d.ts +1 -1
- package/dist/types/instrumentation.d.ts +1 -1
- package/dist/types/langfuse.d.ts +3 -3
- package/dist/types/langfuseConfig.d.ts +2 -2
- package/dist/types/langfuseRuntimeContext.d.ts +1 -1
- package/dist/types/langfuseRuntimeScope.d.ts +2 -2
- package/dist/types/langfuseSpanRegistry.d.ts +1 -1
- package/dist/types/langfuseToolOutputTracing.d.ts +3 -3
- package/dist/types/llm/anthropic/index.d.ts +1 -1
- package/dist/types/llm/bedrock/index.d.ts +1 -1
- package/dist/types/llm/bedrock/toolCache.d.ts +2 -2
- package/dist/types/llm/contextOverflowRecovery.d.ts +2 -2
- package/dist/types/llm/google/index.d.ts +1 -1
- package/dist/types/llm/init.d.ts +2 -2
- package/dist/types/llm/invoke.d.ts +6 -6
- package/dist/types/llm/mistral/index.d.ts +1 -1
- package/dist/types/llm/openai/index.d.ts +2 -2
- package/dist/types/llm/openrouter/index.d.ts +2 -2
- package/dist/types/llm/openrouter/toolCache.d.ts +2 -2
- package/dist/types/llm/providers.d.ts +2 -2
- package/dist/types/llm/request.d.ts +2 -2
- package/dist/types/llm/stream/chunkAdapters.d.ts +1 -1
- package/dist/types/llm/streamLimits.d.ts +1 -1
- package/dist/types/llm/truncation.d.ts +1 -1
- package/dist/types/llm/vertexai/index.d.ts +1 -1
- package/dist/types/messages/alternation.d.ts +1 -1
- package/dist/types/messages/anthropicToolCache.d.ts +2 -2
- package/dist/types/messages/assistantPhase.d.ts +3 -3
- package/dist/types/messages/budget.d.ts +1 -1
- package/dist/types/messages/cache.d.ts +1 -1
- package/dist/types/messages/contextPruning.d.ts +2 -2
- package/dist/types/messages/core.d.ts +2 -2
- package/dist/types/messages/format.d.ts +2 -2
- package/dist/types/messages/ids.d.ts +2 -2
- package/dist/types/messages/injected.d.ts +1 -1
- package/dist/types/messages/langchain.d.ts +1 -1
- package/dist/types/messages/prune.d.ts +3 -3
- package/dist/types/openai/index.d.ts +1 -1
- package/dist/types/prompts/activityLabel.d.ts +2 -2
- package/dist/types/responses/index.d.ts +1 -1
- package/dist/types/run.d.ts +3 -3
- package/dist/types/session/handlers.d.ts +1 -1
- package/dist/types/session/types.d.ts +1 -1
- package/dist/types/stream.d.ts +4 -4
- package/dist/types/summarization/index.d.ts +1 -1
- package/dist/types/summarization/node.d.ts +6 -6
- package/dist/types/test/mockTools.d.ts +1 -1
- package/dist/types/tools/BashExecutor.d.ts +2 -2
- package/dist/types/tools/BashProgrammaticToolCalling.d.ts +2 -2
- package/dist/types/tools/CodeExecutor.d.ts +3 -3
- package/dist/types/tools/CodeSessionFileSummary.d.ts +1 -1
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +2 -2
- package/dist/types/tools/ReadFile.d.ts +1 -1
- package/dist/types/tools/SkillTool.d.ts +1 -1
- package/dist/types/tools/SubagentTool.d.ts +3 -3
- package/dist/types/tools/ToolNode.d.ts +5 -5
- package/dist/types/tools/ToolSearch.d.ts +2 -2
- package/dist/types/tools/cloudflare/CloudflareBridgeRuntime.d.ts +1 -1
- package/dist/types/tools/cloudflare/CloudflareProgrammaticToolCalling.d.ts +1 -1
- package/dist/types/tools/cloudflare/CloudflareSandboxExecutionEngine.d.ts +2 -2
- package/dist/types/tools/cloudflare/CloudflareSandboxTools.d.ts +1 -1
- package/dist/types/tools/eagerEventExecution.d.ts +1 -1
- package/dist/types/tools/handlers.d.ts +3 -3
- package/dist/types/tools/intentArg.d.ts +1 -1
- package/dist/types/tools/local/CompileCheckTool.d.ts +2 -2
- package/dist/types/tools/local/FileCheckpointer.d.ts +1 -1
- package/dist/types/tools/local/LocalCodingTools.d.ts +2 -2
- package/dist/types/tools/local/LocalExecutionEngine.d.ts +1 -1
- package/dist/types/tools/local/LocalExecutionTools.d.ts +1 -1
- package/dist/types/tools/local/LocalProgrammaticToolCalling.d.ts +1 -1
- package/dist/types/tools/local/bashAst.d.ts +1 -1
- package/dist/types/tools/local/resolveLocalExecutionTools.d.ts +1 -1
- package/dist/types/tools/local/syntaxCheck.d.ts +1 -1
- package/dist/types/tools/ptcTimeout.d.ts +1 -1
- package/dist/types/tools/schema.d.ts +1 -1
- package/dist/types/tools/search/anthropic.d.ts +1 -1
- package/dist/types/tools/skillCatalog.d.ts +1 -1
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +5 -5
- package/dist/types/tools/subagent/SubagentReplay.d.ts +3 -3
- package/dist/types/tools/subagent/childGraphConfig.d.ts +2 -2
- package/dist/types/types/activityLabel.d.ts +3 -3
- package/dist/types/types/graph.d.ts +7 -7
- package/dist/types/types/llm.d.ts +9 -9
- package/dist/types/types/run.d.ts +7 -7
- package/dist/types/types/stream.d.ts +5 -5
- package/dist/types/types/summarize.d.ts +2 -2
- package/dist/types/types/tools.d.ts +5 -5
- package/dist/types/utils/__tests__/fixtures/contextOverflowSignatures.d.ts +1 -1
- package/dist/types/utils/errors.d.ts +1 -1
- package/dist/types/utils/events.d.ts +1 -1
- package/dist/types/utils/handlers.d.ts +3 -3
- package/dist/types/utils/llm.d.ts +1 -1
- package/dist/types/utils/llmConfig.d.ts +1 -1
- package/dist/types/utils/title.d.ts +1 -1
- package/package.json +10 -3
- package/src/__tests__/stream.eagerArgsDivergence.test.ts +0 -911
- package/src/__tests__/stream.eagerEventExecution.test.ts +0 -5046
- package/src/__tests__/stream.streamLimits.test.ts +0 -1982
- package/src/agents/__tests__/AgentContext.anthropic.live.test.ts +0 -448
- package/src/agents/__tests__/AgentContext.bedrock.live.test.ts +0 -655
- package/src/agents/__tests__/AgentContext.openrouter.live.test.ts +0 -128
- package/src/agents/__tests__/AgentContext.overflow.test.ts +0 -205
- package/src/agents/__tests__/AgentContext.test.ts +0 -2425
- package/src/agents/__tests__/projection.test.ts +0 -73
- package/src/agents/__tests__/promptCacheLiveHelpers.ts +0 -175
- package/src/aggregator.test.ts +0 -1214
- package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +0 -273
- package/src/graphs/__tests__/Graph.closeRunStep.test.ts +0 -385
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +0 -1766
- package/src/graphs/__tests__/Graph.preemptSignal.test.ts +0 -126
- package/src/graphs/__tests__/Graph.reasoning.test.ts +0 -1717
- package/src/graphs/__tests__/Graph.subagentResumeState.test.ts +0 -80
- package/src/graphs/__tests__/MultiAgentGraph.test.ts +0 -177
- package/src/graphs/__tests__/composition.smoke.test.ts +0 -509
- package/src/graphs/__tests__/createGraph.test.ts +0 -206
- package/src/hooks/__tests__/HookRegistry.test.ts +0 -334
- package/src/hooks/__tests__/compactHooks.test.ts +0 -250
- package/src/hooks/__tests__/createToolPolicyHook.test.ts +0 -268
- package/src/hooks/__tests__/createWorkspacePolicyHook.test.ts +0 -405
- package/src/hooks/__tests__/executeHooks.test.ts +0 -1294
- package/src/hooks/__tests__/integration.test.ts +0 -337
- package/src/hooks/__tests__/matchers.test.ts +0 -238
- package/src/hooks/__tests__/preemptBoundary.test.ts +0 -152
- package/src/hooks/__tests__/toolHooks.test.ts +0 -669
- package/src/llm/__tests__/contextOverflowRecovery.test.ts +0 -401
- package/src/llm/__tests__/fallbackOverflow.test.ts +0 -287
- package/src/llm/anthropic/Jacob_Lee_Resume_2023.pdf +0 -0
- package/src/llm/anthropic/inherited-content-utils.spec.ts +0 -249
- package/src/llm/anthropic/inherited-stream-events.spec.ts +0 -1259
- package/src/llm/anthropic/inherited-strict.spec.ts +0 -302
- package/src/llm/anthropic/llm.spec.ts +0 -3394
- package/src/llm/anthropic/utils/cross-provider-reasoning.test.ts +0 -383
- package/src/llm/anthropic/utils/cross-provider-server-tools.test.ts +0 -110
- package/src/llm/anthropic/utils/server-tool-inputs.test.ts +0 -508
- package/src/llm/anthropic/utils/streaming-tool-input.test.ts +0 -361
- package/src/llm/anthropic/utils/stripPrefillCache.test.ts +0 -111
- package/src/llm/anthropic/utils/tool-id-normalization.test.ts +0 -287
- package/src/llm/bedrock/inherited-cache.spec.ts +0 -144
- package/src/llm/bedrock/inherited.spec.ts +0 -724
- package/src/llm/bedrock/llm.spec.ts +0 -1533
- package/src/llm/bedrock/streamSealDispatch.test.ts +0 -331
- package/src/llm/bedrock/toolCache.test.ts +0 -194
- package/src/llm/bedrock/utils/cross-provider-reasoning.test.ts +0 -131
- package/src/llm/bedrock/utils/cross-provider-server-tools.test.ts +0 -122
- package/src/llm/bedrock/utils/message_inputs.test.ts +0 -650
- package/src/llm/bedrock/utils/message_outputs.test.ts +0 -85
- package/src/llm/bedrock/utils/toolResultCachePoint.test.ts +0 -118
- package/src/llm/custom-chat-models.smoke.test.ts +0 -1966
- package/src/llm/google/data/gettysburg10.wav +0 -0
- package/src/llm/google/data/hotdog.jpg +0 -0
- package/src/llm/google/inherited-stream-events.spec.ts +0 -350
- package/src/llm/google/llm.spec.ts +0 -1388
- package/src/llm/google/streamSmoothing.test.ts +0 -121
- package/src/llm/google/utils/common.test.ts +0 -196
- package/src/llm/invoke.alternation.test.ts +0 -87
- package/src/llm/invoke.handoffCue.test.ts +0 -112
- package/src/llm/invoke.streamLimits.test.ts +0 -142
- package/src/llm/invoke.test.ts +0 -1198
- package/src/llm/mistral/streamSmoothing.test.ts +0 -97
- package/src/llm/openai/cacheWriteTokens.test.ts +0 -112
- package/src/llm/openai/contentBlocks.test.ts +0 -381
- package/src/llm/openai/deepseek.test.ts +0 -747
- package/src/llm/openai/inherited-deepseek.spec.ts +0 -347
- package/src/llm/openai/inherited-xai.spec.ts +0 -416
- package/src/llm/openai/llm.spec.ts +0 -1774
- package/src/llm/openai/managedRequests.test.ts +0 -182
- package/src/llm/openai/sequentialToolCallSeals.test.ts +0 -199
- package/src/llm/openai/streamMetadata.spec.ts +0 -86
- package/src/llm/openai/streamMetadataDedup.spec.ts +0 -166
- package/src/llm/openai/utils/isReasoningModel.test.ts +0 -90
- package/src/llm/openai/utils/messages.test.ts +0 -488
- package/src/llm/openrouter/reasoning.test.ts +0 -561
- package/src/llm/openrouter/toolCache.test.ts +0 -134
- package/src/llm/preempt.test.ts +0 -323
- package/src/llm/stream/chunkAdapters.test.ts +0 -202
- package/src/llm/stream/reassembly.test.ts +0 -241
- package/src/llm/stream/smoother.bench.test.ts +0 -155
- package/src/llm/stream/smoother.test.ts +0 -519
- package/src/llm/streamLimits.test.ts +0 -450
- package/src/llm/truncation.test.ts +0 -242
- package/src/llm/vertexai/fixThoughtSignatures.test.ts +0 -154
- package/src/llm/vertexai/inherited-stream-events.spec.ts +0 -271
- package/src/llm/vertexai/llm.spec.ts +0 -159
- package/src/llm/vertexai/repairUsageMetadata.test.ts +0 -54
- package/src/llm/vertexai/sealStreamedToolCalls.test.ts +0 -88
- package/src/llm/vertexai/streamSealDispatch.test.ts +0 -148
- package/src/llm/vertexai/streamSmoothing.test.ts +0 -109
- package/src/messages/__tests__/anthropicToolCache.test.ts +0 -339
- package/src/messages/__tests__/observationMasking.test.ts +0 -312
- package/src/messages/__tests__/recency.test.ts +0 -267
- package/src/messages/__tests__/tools.test.ts +0 -473
- package/src/messages/alternation.test.ts +0 -212
- package/src/messages/assistantPhase.test.ts +0 -75
- package/src/messages/cache.tail.test.ts +0 -533
- package/src/messages/cache.test.ts +0 -2166
- package/src/messages/content.test.ts +0 -362
- package/src/messages/contextPruning.test.ts +0 -184
- package/src/messages/ensureThinkingBlock.test.ts +0 -1427
- package/src/messages/foldToollessToolBlocks.test.ts +0 -596
- package/src/messages/formatAgentMessages.reducer.test.ts +0 -162
- package/src/messages/formatAgentMessages.skills.test.ts +0 -513
- package/src/messages/formatAgentMessages.steer.test.ts +0 -598
- package/src/messages/formatAgentMessages.test.ts +0 -8111
- package/src/messages/formatAgentMessages.tools.test.ts +0 -522
- package/src/messages/formatMessage.test.ts +0 -693
- package/src/messages/handoffCue.test.ts +0 -96
- package/src/messages/injected.test.ts +0 -107
- package/src/messages/labelContentByAgent.test.ts +0 -962
- package/src/messages/reducer.spec.ts +0 -32
- package/src/messages/shiftIndexTokenCountMap.test.ts +0 -81
- package/src/messages/tailCacheConversion.test.ts +0 -161
- package/src/openai/__tests__/openai.test.ts +0 -337
- package/src/responses/__tests__/responses.test.ts +0 -652
- package/src/scripts/abort.ts +0 -157
- package/src/scripts/activity-labels/captured.json +0 -56
- package/src/scripts/activity-labels/checks.cjs +0 -205
- package/src/scripts/activity-labels/corpus.cjs +0 -473
- package/src/scripts/activity-labels/report.cjs +0 -203
- package/src/scripts/activity-labels/rescore.cjs +0 -102
- package/src/scripts/activity-labels/run.ts +0 -705
- package/src/scripts/activity-labels/variants.ts +0 -71
- package/src/scripts/ant_web_search.ts +0 -159
- package/src/scripts/ant_web_search_edge_case.ts +0 -163
- package/src/scripts/ant_web_search_error_edge_case.ts +0 -149
- package/src/scripts/args.ts +0 -48
- package/src/scripts/bedrock-cache-debug.ts +0 -250
- package/src/scripts/bedrock-content-aggregation-test.ts +0 -266
- package/src/scripts/bedrock-merge-test.ts +0 -107
- package/src/scripts/bedrock-parallel-tools-test.ts +0 -204
- package/src/scripts/bench-prompt-cache.ts +0 -479
- package/src/scripts/caching.ts +0 -132
- package/src/scripts/cli.ts +0 -172
- package/src/scripts/cli2.ts +0 -133
- package/src/scripts/cli3.ts +0 -184
- package/src/scripts/cli4.ts +0 -191
- package/src/scripts/cli5.ts +0 -191
- package/src/scripts/code_exec.ts +0 -214
- package/src/scripts/code_exec_files.ts +0 -237
- package/src/scripts/code_exec_multi_session.ts +0 -237
- package/src/scripts/code_exec_ptc.ts +0 -335
- package/src/scripts/code_exec_session.ts +0 -283
- package/src/scripts/code_exec_simple.ts +0 -148
- package/src/scripts/compare_pi_vs_ours.ts +0 -1259
- package/src/scripts/content.ts +0 -139
- package/src/scripts/context-overflow-probe.ts +0 -997
- package/src/scripts/empty_input.ts +0 -137
- package/src/scripts/handoff-test.ts +0 -134
- package/src/scripts/local_engine.ts +0 -166
- package/src/scripts/local_engine_checkpointer.ts +0 -205
- package/src/scripts/local_engine_compile.ts +0 -263
- package/src/scripts/local_engine_hooks.ts +0 -226
- package/src/scripts/local_engine_image.ts +0 -201
- package/src/scripts/local_engine_ptc.ts +0 -151
- package/src/scripts/local_engine_workspace.ts +0 -258
- package/src/scripts/memory.ts +0 -107
- package/src/scripts/multi-agent-chain.ts +0 -332
- package/src/scripts/multi-agent-conditional.ts +0 -222
- package/src/scripts/multi-agent-document-review-chain.ts +0 -198
- package/src/scripts/multi-agent-hybrid-flow.ts +0 -311
- package/src/scripts/multi-agent-parallel-start.ts +0 -265
- package/src/scripts/multi-agent-parallel.ts +0 -395
- package/src/scripts/multi-agent-sequence.ts +0 -217
- package/src/scripts/multi-agent-subagent.ts +0 -246
- package/src/scripts/multi-agent-supervisor.ts +0 -366
- package/src/scripts/multi-agent-test.ts +0 -187
- package/src/scripts/openrouter_prompt_cache_live.ts +0 -310
- package/src/scripts/parallel-asymmetric-tools-test.ts +0 -275
- package/src/scripts/parallel-full-metadata-test.ts +0 -241
- package/src/scripts/parallel-tools-test.ts +0 -341
- package/src/scripts/preempt-probe.ts +0 -330
- package/src/scripts/preempt-scenarios.ts +0 -388
- package/src/scripts/programmatic_exec.ts +0 -387
- package/src/scripts/programmatic_exec_agent.ts +0 -232
- package/src/scripts/search.ts +0 -147
- package/src/scripts/sequential-full-metadata-test.ts +0 -198
- package/src/scripts/session_live.ts +0 -548
- package/src/scripts/simple.ts +0 -226
- package/src/scripts/single-agent-metadata-test.ts +0 -196
- package/src/scripts/stream.ts +0 -141
- package/src/scripts/subagent-configurable-inheritance.ts +0 -252
- package/src/scripts/subagent-event-driven-debug.ts +0 -190
- package/src/scripts/subagent-tools-debug.ts +0 -160
- package/src/scripts/subagent-usage-sink.ts +0 -176
- package/src/scripts/summarization-recency.ts +0 -462
- package/src/scripts/test-custom-prompt-key.ts +0 -145
- package/src/scripts/test-handoff-input.ts +0 -170
- package/src/scripts/test-handoff-preamble.ts +0 -278
- package/src/scripts/test-handoff-steering.ts +0 -430
- package/src/scripts/test-multi-agent-list-handoff.ts +0 -418
- package/src/scripts/test-parallel-agent-labeling.ts +0 -327
- package/src/scripts/test-parallel-handoffs.ts +0 -292
- package/src/scripts/test-thinking-handoff-bedrock.ts +0 -154
- package/src/scripts/test-thinking-handoff.ts +0 -156
- package/src/scripts/test-thinking-to-thinking-handoff-bedrock.ts +0 -166
- package/src/scripts/test-tool-before-handoff-role-order.ts +0 -276
- package/src/scripts/test-tools-before-handoff.ts +0 -222
- package/src/scripts/test_code_api.ts +0 -354
- package/src/scripts/thinking-bedrock.ts +0 -160
- package/src/scripts/thinking-vertexai.ts +0 -168
- package/src/scripts/thinking.ts +0 -172
- package/src/scripts/tool_search.ts +0 -153
- package/src/scripts/tools.ts +0 -163
- package/src/session/__tests__/JsonlSessionStore.test.ts +0 -1463
- package/src/session/__tests__/handlers.test.ts +0 -161
- package/src/specs/activity-label-observability.live.test.ts +0 -262
- package/src/specs/activity-label-observability.test.ts +0 -167
- package/src/specs/activity-label-prompt.test.ts +0 -359
- package/src/specs/activity-label-trace-seed.test.ts +0 -47
- package/src/specs/activity-phase-label.test.ts +0 -306
- package/src/specs/agent-handoffs.live.test.ts +0 -140
- package/src/specs/agent-handoffs.test.ts +0 -2465
- package/src/specs/anthropic.simple.test.ts +0 -453
- package/src/specs/ask-user-question-batch.test.ts +0 -289
- package/src/specs/ask-user-questions.live.test.ts +0 -185
- package/src/specs/ask-user-questions.test.ts +0 -293
- package/src/specs/azure.simple.test.ts +0 -502
- package/src/specs/bedrock-toolless.live.test.ts +0 -123
- package/src/specs/bedrock-truncation.live.test.ts +0 -191
- package/src/specs/cache.simple.test.ts +0 -416
- package/src/specs/context-accuracy.live.test.ts +0 -409
- package/src/specs/context-overflow-recovery.live.test.ts +0 -213
- package/src/specs/context-usage-event.test.ts +0 -117
- package/src/specs/context-usage.live.test.ts +0 -297
- package/src/specs/custom-event-await.test.ts +0 -304
- package/src/specs/deepseek.simple.test.ts +0 -290
- package/src/specs/deterministic-trace-id.test.ts +0 -43
- package/src/specs/discovered-tools.test.ts +0 -217
- package/src/specs/durability-checkpoint.integration.test.ts +0 -243
- package/src/specs/graph-subagent.live.test.ts +0 -242
- package/src/specs/graph-subagent.test.ts +0 -322
- package/src/specs/handoffCue.test.ts +0 -165
- package/src/specs/langfuse-callbacks.test.ts +0 -1578
- package/src/specs/langfuse-config.test.ts +0 -247
- package/src/specs/langfuse-instrumentation.test.ts +0 -393
- package/src/specs/langfuse-metadata.test.ts +0 -245
- package/src/specs/langfuse-routing.integration.test.ts +0 -844
- package/src/specs/langfuse-runtime-context.test.ts +0 -92
- package/src/specs/langfuse-span-registry.test.ts +0 -87
- package/src/specs/langfuse-tool-output-tracing.test.ts +0 -1858
- package/src/specs/langfuse-trace-shaping.test.ts +0 -591
- package/src/specs/moonshot.simple.test.ts +0 -367
- package/src/specs/multi-agent-summarization.test.ts +0 -396
- package/src/specs/openai.simple.test.ts +0 -315
- package/src/specs/openrouter.simple.test.ts +0 -275
- package/src/specs/preemptSeal.test.ts +0 -678
- package/src/specs/prune.test.ts +0 -3741
- package/src/specs/reasoning.test.ts +0 -201
- package/src/specs/run-step-timestamps.test.ts +0 -414
- package/src/specs/spec.utils.ts +0 -15
- package/src/specs/subagent.test.ts +0 -864
- package/src/specs/summarization-unit.test.ts +0 -973
- package/src/specs/summarization.test.ts +0 -3823
- package/src/specs/summarize-prune.test.ts +0 -376
- package/src/specs/thinking-handoff.test.ts +0 -621
- package/src/specs/thinking-prune.test.ts +0 -827
- package/src/specs/token-accounting-e2e.test.ts +0 -1034
- package/src/specs/token-accounting-pipeline.test.ts +0 -1008
- package/src/specs/token-distribution-edge-case.test.ts +0 -315
- package/src/specs/token-memoization.test.ts +0 -40
- package/src/specs/tokens.test.ts +0 -1000
- package/src/specs/tool-error-resume.test.ts +0 -194
- package/src/specs/tool-error.test.ts +0 -198
- package/src/specs/vllm-reasoning-toolcalls.test.ts +0 -340
- package/src/stream.dispatch.test.ts +0 -63
- package/src/stream.test.ts +0 -144
- package/src/summarization/__tests__/aggregator.test.ts +0 -236
- package/src/summarization/__tests__/node.test.ts +0 -1996
- package/src/summarization/__tests__/trigger.test.ts +0 -149
- package/src/summarization/chunkHandler.test.ts +0 -196
- package/src/test/mockTools.ts +0 -386
- package/src/tools/Calculator.test.ts +0 -278
- package/src/tools/__tests__/BashExecutor.test.ts +0 -108
- package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +0 -939
- package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +0 -858
- package/src/tools/__tests__/CodeExecutor.stateful.test.ts +0 -124
- package/src/tools/__tests__/LocalExecutionRoots.test.ts +0 -8
- package/src/tools/__tests__/LocalExecutionTools.test.ts +0 -2733
- package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +0 -318
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +0 -1557
- package/src/tools/__tests__/ReadFile.test.ts +0 -44
- package/src/tools/__tests__/SkillTool.test.ts +0 -442
- package/src/tools/__tests__/SubagentExecutor.test.ts +0 -3728
- package/src/tools/__tests__/SubagentReplay.test.ts +0 -329
- package/src/tools/__tests__/SubagentTool.test.ts +0 -149
- package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +0 -595
- package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +0 -1191
- package/src/tools/__tests__/ToolNode.invalidToolCalls.test.ts +0 -757
- package/src/tools/__tests__/ToolNode.langfuse.test.ts +0 -57
- package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +0 -736
- package/src/tools/__tests__/ToolNode.outputReferences.test.ts +0 -1710
- package/src/tools/__tests__/ToolNode.runtimeState.test.ts +0 -120
- package/src/tools/__tests__/ToolNode.session.test.ts +0 -1352
- package/src/tools/__tests__/ToolNode.streamLimits.test.ts +0 -69
- package/src/tools/__tests__/ToolSearch.integration.test.ts +0 -161
- package/src/tools/__tests__/ToolSearch.test.ts +0 -1227
- package/src/tools/__tests__/annotateMessagesForLLM.test.ts +0 -561
- package/src/tools/__tests__/directToolHITLResumeScope.test.ts +0 -980
- package/src/tools/__tests__/directToolHooks.test.ts +0 -700
- package/src/tools/__tests__/eagerEventExecution.session.test.ts +0 -92
- package/src/tools/__tests__/handlers.test.ts +0 -997
- package/src/tools/__tests__/hitl.test.ts +0 -4663
- package/src/tools/__tests__/intentArg.test.ts +0 -357
- package/src/tools/__tests__/intentCoverage.test.ts +0 -139
- package/src/tools/__tests__/localToolNames.test.ts +0 -75
- package/src/tools/__tests__/skillCatalog.test.ts +0 -161
- package/src/tools/__tests__/subagentHooks.test.ts +0 -1651
- package/src/tools/__tests__/toolOutputReferences.test.ts +0 -515
- package/src/tools/__tests__/workspaceSeam.test.ts +0 -147
- package/src/tools/local/__tests__/FileCheckpointer.test.ts +0 -162
- package/src/tools/local/__tests__/editStrategies.test.ts +0 -134
- package/src/tools/search/content.test.ts +0 -173
- package/src/tools/search/crw.test.ts +0 -836
- package/src/tools/search/format.test.ts +0 -242
- package/src/tools/search/http-agent.test.ts +0 -133
- package/src/tools/search/jina-reranker.test.ts +0 -306
- package/src/tools/search/keenable-scraper.test.ts +0 -153
- package/src/tools/search/keenable.test.ts +0 -183
- package/src/tools/search/outcome.test.ts +0 -90
- package/src/tools/search/output.md +0 -2775
- package/src/tools/search/rag-api-reranker.test.ts +0 -891
- package/src/tools/search/source-processing.test.ts +0 -516
- package/src/tools/search/tavily.test.ts +0 -965
- package/src/tools/search/test.html +0 -884
- package/src/tools/search/test.md +0 -643
- package/src/tools/subagent/__tests__/SubagentExecutionRegistry.test.ts +0 -637
- package/src/tools/subagent/__tests__/SubagentExecutor.lazy.test.ts +0 -3054
- package/src/tools/subagent/__tests__/childGraphConfig.test.ts +0 -521
- package/src/utils/__tests__/apportion.test.ts +0 -32
- package/src/utils/__tests__/errors.test.ts +0 -270
- package/src/utils/__tests__/fixtures/contextOverflowSignatures.ts +0 -336
- package/src/utils/__tests__/redactSecrets.test.ts +0 -56
- package/src/utils/__tests__/truncation.test.ts +0 -66
- package/src/utils/llm.test.ts +0 -18
- package/src/utils/proxy.test.ts +0 -176
- package/src/utils/toolContent.test.ts +0 -1432
package/src/specs/prune.test.ts
DELETED
|
@@ -1,3741 +0,0 @@
|
|
|
1
|
-
// src/specs/prune.test.ts
|
|
2
|
-
import { config } from 'dotenv';
|
|
3
|
-
config();
|
|
4
|
-
import {
|
|
5
|
-
AIMessage,
|
|
6
|
-
BaseMessage,
|
|
7
|
-
ToolMessage,
|
|
8
|
-
HumanMessage,
|
|
9
|
-
isBaseMessage,
|
|
10
|
-
SystemMessage,
|
|
11
|
-
AIMessageChunk,
|
|
12
|
-
ChatMessage,
|
|
13
|
-
} from '@langchain/core/messages';
|
|
14
|
-
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
15
|
-
import type { UsageMetadata } from '@langchain/core/messages';
|
|
16
|
-
import type * as t from '@/types';
|
|
17
|
-
import {
|
|
18
|
-
getMessagesWithinTokenLimit as realGetMessagesWithinTokenLimit,
|
|
19
|
-
preFlightTruncateToolCallInputs,
|
|
20
|
-
repairOrphanedToolMessages,
|
|
21
|
-
sanitizeOrphanToolBlocks,
|
|
22
|
-
enforceOriginalContentCap,
|
|
23
|
-
ORIGINAL_CONTENT_MAX_CHARS,
|
|
24
|
-
calculateMaxToolCallInputChars,
|
|
25
|
-
createPruneMessages,
|
|
26
|
-
projectToolCallInputs,
|
|
27
|
-
serializeToolCallInput,
|
|
28
|
-
} from '@/messages/prune';
|
|
29
|
-
import { getLLMConfig } from '@/utils/llmConfig';
|
|
30
|
-
import { ensureThinkingBlockInMessages } from '@/messages/format';
|
|
31
|
-
import { Providers, ContentTypes } from '@/common';
|
|
32
|
-
import { Run } from '@/run';
|
|
33
|
-
|
|
34
|
-
// Create a simple token counter for testing
|
|
35
|
-
const createTestTokenCounter = (): t.TokenCounter => {
|
|
36
|
-
// This simple token counter just counts characters as tokens for predictable testing
|
|
37
|
-
return (message: BaseMessage): number => {
|
|
38
|
-
// Use type assertion to help TypeScript understand the type
|
|
39
|
-
const content = message.content as
|
|
40
|
-
| string
|
|
41
|
-
| Array<t.MessageContentComplex | string>
|
|
42
|
-
| undefined;
|
|
43
|
-
|
|
44
|
-
// Handle string content
|
|
45
|
-
if (typeof content === 'string') {
|
|
46
|
-
return content.length;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Handle array content
|
|
50
|
-
if (Array.isArray(content)) {
|
|
51
|
-
let totalLength = 0;
|
|
52
|
-
|
|
53
|
-
for (const item of content) {
|
|
54
|
-
if (typeof item === 'string') {
|
|
55
|
-
totalLength += item.length;
|
|
56
|
-
} else if (typeof item === 'object') {
|
|
57
|
-
if ('text' in item && typeof item.text === 'string') {
|
|
58
|
-
totalLength += item.text.length;
|
|
59
|
-
}
|
|
60
|
-
// Count tool_use input fields (serialized args contribute to token count)
|
|
61
|
-
if ('input' in item && item.input != null) {
|
|
62
|
-
const input = item.input;
|
|
63
|
-
totalLength +=
|
|
64
|
-
typeof input === 'string'
|
|
65
|
-
? input.length
|
|
66
|
-
: JSON.stringify(input).length;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return totalLength;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Default case - if content is null, undefined, or any other type
|
|
75
|
-
return 0;
|
|
76
|
-
};
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
// Since the internal functions in prune.ts are not exported, we'll reimplement them here for testing
|
|
80
|
-
// This is based on the implementation in src/messages/prune.ts
|
|
81
|
-
function calculateTotalTokens(usage: Partial<UsageMetadata>): UsageMetadata {
|
|
82
|
-
const baseInputTokens = Number(usage.input_tokens) || 0;
|
|
83
|
-
const cacheCreation = Number(usage.input_token_details?.cache_creation) || 0;
|
|
84
|
-
const cacheRead = Number(usage.input_token_details?.cache_read) || 0;
|
|
85
|
-
|
|
86
|
-
const totalInputTokens = baseInputTokens + cacheCreation + cacheRead;
|
|
87
|
-
const totalOutputTokens = Number(usage.output_tokens) || 0;
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
input_tokens: totalInputTokens,
|
|
91
|
-
output_tokens: totalOutputTokens,
|
|
92
|
-
total_tokens: totalInputTokens + totalOutputTokens,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function getMessagesWithinTokenLimit({
|
|
97
|
-
messages: _messages,
|
|
98
|
-
maxContextTokens,
|
|
99
|
-
indexTokenCountMap,
|
|
100
|
-
startType,
|
|
101
|
-
}: {
|
|
102
|
-
messages: BaseMessage[];
|
|
103
|
-
maxContextTokens: number;
|
|
104
|
-
indexTokenCountMap: Record<string, number>;
|
|
105
|
-
startType?: string;
|
|
106
|
-
}): {
|
|
107
|
-
context: BaseMessage[];
|
|
108
|
-
remainingContextTokens: number;
|
|
109
|
-
messagesToRefine: BaseMessage[];
|
|
110
|
-
summaryIndex: number;
|
|
111
|
-
} {
|
|
112
|
-
// Every reply is primed with <|start|>assistant<|message|>, so we
|
|
113
|
-
// start with 3 tokens for the label after all messages have been counted.
|
|
114
|
-
let summaryIndex = -1;
|
|
115
|
-
let currentTokenCount = 3;
|
|
116
|
-
const instructions =
|
|
117
|
-
_messages[0]?.getType() === 'system' ? _messages[0] : undefined;
|
|
118
|
-
const instructionsTokenCount =
|
|
119
|
-
instructions != null ? indexTokenCountMap[0] : 0;
|
|
120
|
-
let remainingContextTokens = maxContextTokens - instructionsTokenCount;
|
|
121
|
-
const messages = [..._messages];
|
|
122
|
-
const context: BaseMessage[] = [];
|
|
123
|
-
|
|
124
|
-
if (currentTokenCount < remainingContextTokens) {
|
|
125
|
-
let currentIndex = messages.length;
|
|
126
|
-
while (
|
|
127
|
-
messages.length > 0 &&
|
|
128
|
-
currentTokenCount < remainingContextTokens &&
|
|
129
|
-
currentIndex > 1
|
|
130
|
-
) {
|
|
131
|
-
currentIndex--;
|
|
132
|
-
if (messages.length === 1 && instructions) {
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
const poppedMessage = messages.pop();
|
|
136
|
-
if (!poppedMessage) continue;
|
|
137
|
-
|
|
138
|
-
const tokenCount = indexTokenCountMap[currentIndex] || 0;
|
|
139
|
-
|
|
140
|
-
if (currentTokenCount + tokenCount <= remainingContextTokens) {
|
|
141
|
-
context.push(poppedMessage);
|
|
142
|
-
currentTokenCount += tokenCount;
|
|
143
|
-
} else {
|
|
144
|
-
messages.push(poppedMessage);
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// If startType is specified, discard messages until we find one of the required type
|
|
150
|
-
if (startType != null && startType && context.length > 0) {
|
|
151
|
-
const requiredTypeIndex = context.findIndex(
|
|
152
|
-
(msg) => msg.getType() === startType
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
if (requiredTypeIndex > 0) {
|
|
156
|
-
// If we found a message of the required type, discard all messages before it
|
|
157
|
-
const remainingMessages = context.slice(requiredTypeIndex);
|
|
158
|
-
context.length = 0; // Clear the array
|
|
159
|
-
context.push(...remainingMessages);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (instructions && _messages.length > 0) {
|
|
165
|
-
context.push(_messages[0] as BaseMessage);
|
|
166
|
-
messages.shift();
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const prunedMemory = messages;
|
|
170
|
-
summaryIndex = prunedMemory.length - 1;
|
|
171
|
-
remainingContextTokens -= currentTokenCount;
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
summaryIndex,
|
|
175
|
-
remainingContextTokens,
|
|
176
|
-
context: context.reverse(),
|
|
177
|
-
messagesToRefine: prunedMemory,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function checkValidNumber(value: unknown): value is number {
|
|
182
|
-
return typeof value === 'number' && !isNaN(value) && value > 0;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
describe('Prune Messages Tests', () => {
|
|
186
|
-
jest.setTimeout(30000);
|
|
187
|
-
|
|
188
|
-
describe('calculateTotalTokens', () => {
|
|
189
|
-
it('should calculate total tokens correctly with all fields present', () => {
|
|
190
|
-
const usage: Partial<UsageMetadata> = {
|
|
191
|
-
input_tokens: 100,
|
|
192
|
-
output_tokens: 50,
|
|
193
|
-
input_token_details: {
|
|
194
|
-
cache_creation: 10,
|
|
195
|
-
cache_read: 5,
|
|
196
|
-
},
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
const result = calculateTotalTokens(usage);
|
|
200
|
-
|
|
201
|
-
expect(result.input_tokens).toBe(115); // 100 + 10 + 5
|
|
202
|
-
expect(result.output_tokens).toBe(50);
|
|
203
|
-
expect(result.total_tokens).toBe(165); // 115 + 50
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('should handle missing fields gracefully', () => {
|
|
207
|
-
const usage: Partial<UsageMetadata> = {
|
|
208
|
-
input_tokens: 100,
|
|
209
|
-
output_tokens: 50,
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const result = calculateTotalTokens(usage);
|
|
213
|
-
|
|
214
|
-
expect(result.input_tokens).toBe(100);
|
|
215
|
-
expect(result.output_tokens).toBe(50);
|
|
216
|
-
expect(result.total_tokens).toBe(150);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('should handle empty usage object', () => {
|
|
220
|
-
const usage: Partial<UsageMetadata> = {};
|
|
221
|
-
|
|
222
|
-
const result = calculateTotalTokens(usage);
|
|
223
|
-
|
|
224
|
-
expect(result.input_tokens).toBe(0);
|
|
225
|
-
expect(result.output_tokens).toBe(0);
|
|
226
|
-
expect(result.total_tokens).toBe(0);
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
describe('getMessagesWithinTokenLimit', () => {
|
|
231
|
-
it('should include all messages when under token limit', () => {
|
|
232
|
-
const messages = [
|
|
233
|
-
new SystemMessage('System instruction'),
|
|
234
|
-
new HumanMessage('Hello'),
|
|
235
|
-
new AIMessage('Hi there'),
|
|
236
|
-
];
|
|
237
|
-
|
|
238
|
-
const indexTokenCountMap = {
|
|
239
|
-
0: 17, // "System instruction"
|
|
240
|
-
1: 5, // "Hello"
|
|
241
|
-
2: 8, // "Hi there"
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
const result = getMessagesWithinTokenLimit({
|
|
245
|
-
messages,
|
|
246
|
-
maxContextTokens: 100,
|
|
247
|
-
indexTokenCountMap,
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
expect(result.context.length).toBe(3);
|
|
251
|
-
expect(result.context[0]).toBe(messages[0]); // System message
|
|
252
|
-
expect(result.context[0].getType()).toBe('system'); // System message
|
|
253
|
-
expect(result.remainingContextTokens).toBe(100 - 17 - 5 - 8 - 3); // -3 for the assistant label tokens
|
|
254
|
-
expect(result.messagesToRefine.length).toBe(0);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it('should prune oldest messages when over token limit', () => {
|
|
258
|
-
const messages = [
|
|
259
|
-
new SystemMessage('System instruction'),
|
|
260
|
-
new HumanMessage('Message 1'),
|
|
261
|
-
new AIMessage('Response 1'),
|
|
262
|
-
new HumanMessage('Message 2'),
|
|
263
|
-
new AIMessage('Response 2'),
|
|
264
|
-
];
|
|
265
|
-
|
|
266
|
-
const indexTokenCountMap = {
|
|
267
|
-
0: 17, // "System instruction"
|
|
268
|
-
1: 9, // "Message 1"
|
|
269
|
-
2: 10, // "Response 1"
|
|
270
|
-
3: 9, // "Message 2"
|
|
271
|
-
4: 10, // "Response 2"
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
// Set a limit that can only fit the system message and the last two messages
|
|
275
|
-
const result = getMessagesWithinTokenLimit({
|
|
276
|
-
messages,
|
|
277
|
-
maxContextTokens: 40,
|
|
278
|
-
indexTokenCountMap,
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
// Should include system message and the last two messages
|
|
282
|
-
expect(result.context.length).toBe(3);
|
|
283
|
-
expect(result.context[0]).toBe(messages[0]); // System message
|
|
284
|
-
expect(result.context[0].getType()).toBe('system'); // System message
|
|
285
|
-
expect(result.context[1]).toBe(messages[3]); // Message 2
|
|
286
|
-
expect(result.context[2]).toBe(messages[4]); // Response 2
|
|
287
|
-
|
|
288
|
-
// Should have the first two messages in messagesToRefine
|
|
289
|
-
expect(result.messagesToRefine.length).toBe(2);
|
|
290
|
-
expect(result.messagesToRefine[0]).toBe(messages[1]); // Message 1
|
|
291
|
-
expect(result.messagesToRefine[1]).toBe(messages[2]); // Response 1
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
it('should always include system message even when at token limit', () => {
|
|
295
|
-
const messages = [
|
|
296
|
-
new SystemMessage('System instruction'),
|
|
297
|
-
new HumanMessage('Hello'),
|
|
298
|
-
new AIMessage('Hi there'),
|
|
299
|
-
];
|
|
300
|
-
|
|
301
|
-
const indexTokenCountMap = {
|
|
302
|
-
0: 17, // "System instruction"
|
|
303
|
-
1: 5, // "Hello"
|
|
304
|
-
2: 8, // "Hi there"
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
// Set a limit that can only fit the system message
|
|
308
|
-
const result = getMessagesWithinTokenLimit({
|
|
309
|
-
messages,
|
|
310
|
-
maxContextTokens: 20,
|
|
311
|
-
indexTokenCountMap,
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
expect(result.context.length).toBe(1);
|
|
315
|
-
expect(result.context[0]).toBe(messages[0]); // System message
|
|
316
|
-
|
|
317
|
-
expect(result.messagesToRefine.length).toBe(2);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it('should start context with a specific message type when startType is specified', () => {
|
|
321
|
-
const messages = [
|
|
322
|
-
new SystemMessage('System instruction'),
|
|
323
|
-
new AIMessage('AI message 1'),
|
|
324
|
-
new HumanMessage('Human message 1'),
|
|
325
|
-
new AIMessage('AI message 2'),
|
|
326
|
-
new HumanMessage('Human message 2'),
|
|
327
|
-
];
|
|
328
|
-
|
|
329
|
-
const indexTokenCountMap = {
|
|
330
|
-
0: 17, // "System instruction"
|
|
331
|
-
1: 12, // "AI message 1"
|
|
332
|
-
2: 15, // "Human message 1"
|
|
333
|
-
3: 12, // "AI message 2"
|
|
334
|
-
4: 15, // "Human message 2"
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
// Set a limit that can fit all messages
|
|
338
|
-
const result = getMessagesWithinTokenLimit({
|
|
339
|
-
messages,
|
|
340
|
-
maxContextTokens: 100,
|
|
341
|
-
indexTokenCountMap,
|
|
342
|
-
startType: 'human',
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
// All messages should be included since we're under the token limit
|
|
346
|
-
expect(result.context.length).toBe(5);
|
|
347
|
-
expect(result.context[0]).toBe(messages[0]); // System message
|
|
348
|
-
expect(result.context[1]).toBe(messages[1]); // AI message 1
|
|
349
|
-
expect(result.context[2]).toBe(messages[2]); // Human message 1
|
|
350
|
-
expect(result.context[3]).toBe(messages[3]); // AI message 2
|
|
351
|
-
expect(result.context[4]).toBe(messages[4]); // Human message 2
|
|
352
|
-
|
|
353
|
-
// All messages should be included since we're under the token limit
|
|
354
|
-
expect(result.messagesToRefine.length).toBe(0);
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it('should keep all messages if no message of required type is found', () => {
|
|
358
|
-
const messages = [
|
|
359
|
-
new SystemMessage('System instruction'),
|
|
360
|
-
new AIMessage('AI message 1'),
|
|
361
|
-
new AIMessage('AI message 2'),
|
|
362
|
-
];
|
|
363
|
-
|
|
364
|
-
const indexTokenCountMap = {
|
|
365
|
-
0: 17, // "System instruction"
|
|
366
|
-
1: 12, // "AI message 1"
|
|
367
|
-
2: 12, // "AI message 2"
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
// Set a limit that can fit all messages
|
|
371
|
-
const result = getMessagesWithinTokenLimit({
|
|
372
|
-
messages,
|
|
373
|
-
maxContextTokens: 100,
|
|
374
|
-
indexTokenCountMap,
|
|
375
|
-
startType: 'human',
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
// Should include all messages since no human messages exist to start from
|
|
379
|
-
expect(result.context.length).toBe(3);
|
|
380
|
-
expect(result.context[0]).toBe(messages[0]); // System message
|
|
381
|
-
expect(result.context[1]).toBe(messages[1]); // AI message 1
|
|
382
|
-
expect(result.context[2]).toBe(messages[2]); // AI message 2
|
|
383
|
-
|
|
384
|
-
expect(result.messagesToRefine.length).toBe(0);
|
|
385
|
-
});
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
describe('checkValidNumber', () => {
|
|
389
|
-
it('should return true for valid positive numbers', () => {
|
|
390
|
-
expect(checkValidNumber(5)).toBe(true);
|
|
391
|
-
expect(checkValidNumber(1.5)).toBe(true);
|
|
392
|
-
expect(checkValidNumber(Number.MAX_SAFE_INTEGER)).toBe(true);
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it('should return false for zero, negative numbers, and NaN', () => {
|
|
396
|
-
expect(checkValidNumber(0)).toBe(false);
|
|
397
|
-
expect(checkValidNumber(-5)).toBe(false);
|
|
398
|
-
expect(checkValidNumber(NaN)).toBe(false);
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
it('should return false for non-number types', () => {
|
|
402
|
-
expect(checkValidNumber('5')).toBe(false);
|
|
403
|
-
expect(checkValidNumber(null)).toBe(false);
|
|
404
|
-
expect(checkValidNumber(undefined)).toBe(false);
|
|
405
|
-
expect(checkValidNumber({})).toBe(false);
|
|
406
|
-
expect(checkValidNumber([])).toBe(false);
|
|
407
|
-
});
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
describe('createPruneMessages', () => {
|
|
411
|
-
it('should return all messages when under token limit', () => {
|
|
412
|
-
const tokenCounter = createTestTokenCounter();
|
|
413
|
-
const messages = [
|
|
414
|
-
new SystemMessage('System instruction'),
|
|
415
|
-
new HumanMessage('Hello'),
|
|
416
|
-
new AIMessage('Hi there'),
|
|
417
|
-
];
|
|
418
|
-
|
|
419
|
-
const indexTokenCountMap = {
|
|
420
|
-
0: tokenCounter(messages[0]),
|
|
421
|
-
1: tokenCounter(messages[1]),
|
|
422
|
-
2: tokenCounter(messages[2]),
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
const pruneMessages = createPruneMessages({
|
|
426
|
-
maxTokens: 100,
|
|
427
|
-
startIndex: 0,
|
|
428
|
-
tokenCounter,
|
|
429
|
-
indexTokenCountMap,
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
const result = pruneMessages({ messages });
|
|
433
|
-
|
|
434
|
-
expect(result.context.length).toBe(3);
|
|
435
|
-
expect(result.context).toEqual(messages);
|
|
436
|
-
expect(result.messagesToRefine).toEqual([]);
|
|
437
|
-
expect(result.remainingContextTokens).toBe(
|
|
438
|
-
95 -
|
|
439
|
-
Object.values(indexTokenCountMap).reduce(
|
|
440
|
-
(total, count) => total + count,
|
|
441
|
-
0
|
|
442
|
-
) -
|
|
443
|
-
3
|
|
444
|
-
);
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
it('recounts and compacts omitted structured tool results before sending', () => {
|
|
448
|
-
const tokenCounter: t.TokenCounter = (message) => {
|
|
449
|
-
const content =
|
|
450
|
-
typeof message.content === 'string'
|
|
451
|
-
? message.content
|
|
452
|
-
: JSON.stringify(message.content);
|
|
453
|
-
return content.length;
|
|
454
|
-
};
|
|
455
|
-
const toolCallId = 'tc-structured';
|
|
456
|
-
const messages: BaseMessage[] = [
|
|
457
|
-
new HumanMessage('query the table'),
|
|
458
|
-
new AIMessage({
|
|
459
|
-
content: '',
|
|
460
|
-
tool_calls: [
|
|
461
|
-
{
|
|
462
|
-
id: toolCallId,
|
|
463
|
-
name: 'run_select_query',
|
|
464
|
-
args: {},
|
|
465
|
-
type: 'tool_call',
|
|
466
|
-
},
|
|
467
|
-
],
|
|
468
|
-
}),
|
|
469
|
-
new ToolMessage({
|
|
470
|
-
content: [
|
|
471
|
-
{
|
|
472
|
-
type: ContentTypes.TEXT,
|
|
473
|
-
text: JSON.stringify(
|
|
474
|
-
Array.from({ length: 20 }, (_, index) => ({
|
|
475
|
-
id: index,
|
|
476
|
-
value: `${'x'.repeat(100)}-${index}`,
|
|
477
|
-
}))
|
|
478
|
-
),
|
|
479
|
-
},
|
|
480
|
-
],
|
|
481
|
-
tool_call_id: toolCallId,
|
|
482
|
-
name: 'run_select_query',
|
|
483
|
-
}),
|
|
484
|
-
new AIMessage('The query returned 20 rows.'),
|
|
485
|
-
new HumanMessage('compact context'),
|
|
486
|
-
];
|
|
487
|
-
const indexTokenCountMap: Record<string, number | undefined> = {
|
|
488
|
-
0: tokenCounter(messages[0]),
|
|
489
|
-
1: tokenCounter(messages[1]),
|
|
490
|
-
3: tokenCounter(messages[3]),
|
|
491
|
-
4: tokenCounter(messages[4]),
|
|
492
|
-
};
|
|
493
|
-
const pruneMessages = createPruneMessages({
|
|
494
|
-
maxTokens: 2_000,
|
|
495
|
-
startIndex: messages.length,
|
|
496
|
-
tokenCounter,
|
|
497
|
-
indexTokenCountMap,
|
|
498
|
-
reserveRatio: 0,
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
const result = pruneMessages({ messages });
|
|
502
|
-
|
|
503
|
-
const structuredResult = result.context.find(
|
|
504
|
-
(message) => message.getType() === 'tool'
|
|
505
|
-
);
|
|
506
|
-
expect(structuredResult).toBeDefined();
|
|
507
|
-
expect(typeof structuredResult?.content).toBe('string');
|
|
508
|
-
expect(structuredResult?.content).toContain('truncated');
|
|
509
|
-
expect(result.indexTokenCountMap[2]).toBeGreaterThan(0);
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
it('reconciles a stale cached legacy function-call count', () => {
|
|
513
|
-
const tokenCounter: t.TokenCounter = (message) => {
|
|
514
|
-
const functionCall = message.additional_kwargs.function_call;
|
|
515
|
-
return (
|
|
516
|
-
(typeof message.content === 'string' ? message.content.length : 0) +
|
|
517
|
-
(functionCall != null ? JSON.stringify(functionCall).length : 0) +
|
|
518
|
-
1
|
|
519
|
-
);
|
|
520
|
-
};
|
|
521
|
-
const message = new AIMessage({
|
|
522
|
-
content: '',
|
|
523
|
-
additional_kwargs: {
|
|
524
|
-
function_call: {
|
|
525
|
-
name: 'legacy_lookup',
|
|
526
|
-
arguments: `{"query":"${'x'.repeat(10_000)}"}`,
|
|
527
|
-
},
|
|
528
|
-
},
|
|
529
|
-
});
|
|
530
|
-
const messages: BaseMessage[] = [message];
|
|
531
|
-
const pruneMessages = createPruneMessages({
|
|
532
|
-
maxTokens: 2_000,
|
|
533
|
-
startIndex: messages.length,
|
|
534
|
-
tokenCounter,
|
|
535
|
-
indexTokenCountMap: { 0: 1 },
|
|
536
|
-
reserveRatio: 0,
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
const result = pruneMessages({ messages });
|
|
540
|
-
const projectedFunctionCall = result.context[0].additional_kwargs
|
|
541
|
-
.function_call as { arguments: string };
|
|
542
|
-
|
|
543
|
-
expect(result.context[0]).not.toBe(message);
|
|
544
|
-
expect(projectedFunctionCall.arguments.length).toBeLessThanOrEqual(
|
|
545
|
-
calculateMaxToolCallInputChars(2_000)
|
|
546
|
-
);
|
|
547
|
-
expect(result.indexTokenCountMap[0]).toBe(
|
|
548
|
-
tokenCounter(result.context[0])
|
|
549
|
-
);
|
|
550
|
-
expect(result.indexTokenCountMap[0]).toBeGreaterThan(1);
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
it('preserves provider output usage for an uncounted current legacy call', () => {
|
|
554
|
-
const message = new AIMessage({
|
|
555
|
-
content: '',
|
|
556
|
-
additional_kwargs: {
|
|
557
|
-
function_call: {
|
|
558
|
-
name: 'legacy_lookup',
|
|
559
|
-
arguments: '{}',
|
|
560
|
-
},
|
|
561
|
-
},
|
|
562
|
-
});
|
|
563
|
-
const pruneMessages = createPruneMessages({
|
|
564
|
-
maxTokens: 2_000,
|
|
565
|
-
startIndex: 0,
|
|
566
|
-
tokenCounter: () => 123,
|
|
567
|
-
indexTokenCountMap: {},
|
|
568
|
-
reserveRatio: 0,
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
const result = pruneMessages({
|
|
572
|
-
messages: [message],
|
|
573
|
-
usageMetadata: {
|
|
574
|
-
input_tokens: 10,
|
|
575
|
-
output_tokens: 7,
|
|
576
|
-
total_tokens: 17,
|
|
577
|
-
},
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
expect(result.indexTokenCountMap[0]).toBe(7);
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
it('excludes a corrected tiny new tool count from calibration input', () => {
|
|
584
|
-
const tokenCounter = createTestTokenCounter();
|
|
585
|
-
const history: BaseMessage[] = [
|
|
586
|
-
new HumanMessage('h'.repeat(50)),
|
|
587
|
-
new AIMessage('a'.repeat(50)),
|
|
588
|
-
];
|
|
589
|
-
const indexTokenCountMap: Record<string, number | undefined> = {
|
|
590
|
-
0: 50,
|
|
591
|
-
1: 50,
|
|
592
|
-
};
|
|
593
|
-
const pruneMessages = createPruneMessages({
|
|
594
|
-
maxTokens: 10_000,
|
|
595
|
-
startIndex: history.length,
|
|
596
|
-
tokenCounter,
|
|
597
|
-
indexTokenCountMap,
|
|
598
|
-
reserveRatio: 0,
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
const first = pruneMessages({
|
|
602
|
-
messages: history,
|
|
603
|
-
usageMetadata: {
|
|
604
|
-
input_tokens: 100,
|
|
605
|
-
output_tokens: 10,
|
|
606
|
-
total_tokens: 110,
|
|
607
|
-
},
|
|
608
|
-
});
|
|
609
|
-
expect(first.calibrationRatio).toBe(1);
|
|
610
|
-
|
|
611
|
-
const toolCallId = 'new-output-with-tiny-count';
|
|
612
|
-
const secondMessages: BaseMessage[] = [
|
|
613
|
-
...history,
|
|
614
|
-
new HumanMessage('q'.repeat(50)),
|
|
615
|
-
new AIMessage({
|
|
616
|
-
content: '',
|
|
617
|
-
tool_calls: [
|
|
618
|
-
{
|
|
619
|
-
id: toolCallId,
|
|
620
|
-
name: 'run_select_query',
|
|
621
|
-
args: {},
|
|
622
|
-
type: 'tool_call',
|
|
623
|
-
},
|
|
624
|
-
],
|
|
625
|
-
}),
|
|
626
|
-
new ToolMessage({
|
|
627
|
-
content: 'r'.repeat(50),
|
|
628
|
-
tool_call_id: toolCallId,
|
|
629
|
-
}),
|
|
630
|
-
];
|
|
631
|
-
indexTokenCountMap[2] = 50;
|
|
632
|
-
indexTokenCountMap[4] = 1;
|
|
633
|
-
|
|
634
|
-
const second = pruneMessages({
|
|
635
|
-
messages: secondMessages,
|
|
636
|
-
usageMetadata: {
|
|
637
|
-
input_tokens: 100,
|
|
638
|
-
output_tokens: 10,
|
|
639
|
-
total_tokens: 110,
|
|
640
|
-
},
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
expect(second.indexTokenCountMap[4]).toBe(50);
|
|
644
|
-
expect(second.calibrationRatio).toBe(1);
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
it('preserves full structured output for summarization before masking', () => {
|
|
648
|
-
const tokenCounter: t.TokenCounter = (message) => {
|
|
649
|
-
const content =
|
|
650
|
-
typeof message.content === 'string'
|
|
651
|
-
? message.content
|
|
652
|
-
: JSON.stringify(message.content);
|
|
653
|
-
return content.length;
|
|
654
|
-
};
|
|
655
|
-
const fullResult = JSON.stringify(
|
|
656
|
-
Array.from({ length: 50 }, (_, index) => ({
|
|
657
|
-
id: index,
|
|
658
|
-
value: `${'x'.repeat(100)}-${index}`,
|
|
659
|
-
}))
|
|
660
|
-
);
|
|
661
|
-
const messages: BaseMessage[] = [
|
|
662
|
-
new HumanMessage('query the table'),
|
|
663
|
-
new AIMessage({
|
|
664
|
-
content: '',
|
|
665
|
-
tool_calls: [
|
|
666
|
-
{
|
|
667
|
-
id: 'tc-summary-structured',
|
|
668
|
-
name: 'run_select_query',
|
|
669
|
-
args: {},
|
|
670
|
-
type: 'tool_call',
|
|
671
|
-
},
|
|
672
|
-
],
|
|
673
|
-
}),
|
|
674
|
-
new ToolMessage({
|
|
675
|
-
content: [{ type: ContentTypes.TEXT, text: fullResult }],
|
|
676
|
-
tool_call_id: 'tc-summary-structured',
|
|
677
|
-
}),
|
|
678
|
-
new AIMessage('The query returned 50 rows.'),
|
|
679
|
-
new HumanMessage('summarize the context'),
|
|
680
|
-
];
|
|
681
|
-
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
682
|
-
for (let i = 0; i < messages.length; i++) {
|
|
683
|
-
indexTokenCountMap[i] = tokenCounter(messages[i]);
|
|
684
|
-
}
|
|
685
|
-
const pruneMessages = createPruneMessages({
|
|
686
|
-
maxTokens: 500,
|
|
687
|
-
startIndex: messages.length,
|
|
688
|
-
tokenCounter,
|
|
689
|
-
indexTokenCountMap,
|
|
690
|
-
reserveRatio: 0,
|
|
691
|
-
summarizationEnabled: true,
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
const result = pruneMessages({ messages });
|
|
695
|
-
|
|
696
|
-
expect(result.newOriginalToolContent?.get(2)).toBe(fullResult);
|
|
697
|
-
expect(messages[2].content).not.toEqual([
|
|
698
|
-
{ type: ContentTypes.TEXT, text: fullResult },
|
|
699
|
-
]);
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
it('should prune messages when over token limit', () => {
|
|
703
|
-
const tokenCounter = createTestTokenCounter();
|
|
704
|
-
const messages = [
|
|
705
|
-
new SystemMessage('System instruction'),
|
|
706
|
-
new HumanMessage('Message 1'),
|
|
707
|
-
new AIMessage('Response 1'),
|
|
708
|
-
new HumanMessage('Message 2'),
|
|
709
|
-
new AIMessage('Response 2'),
|
|
710
|
-
];
|
|
711
|
-
|
|
712
|
-
const indexTokenCountMap = {
|
|
713
|
-
0: tokenCounter(messages[0]),
|
|
714
|
-
1: tokenCounter(messages[1]),
|
|
715
|
-
2: tokenCounter(messages[2]),
|
|
716
|
-
3: tokenCounter(messages[3]),
|
|
717
|
-
4: tokenCounter(messages[4]),
|
|
718
|
-
};
|
|
719
|
-
|
|
720
|
-
// Set a limit that can only fit the system message and the last two messages
|
|
721
|
-
const pruneMessages = createPruneMessages({
|
|
722
|
-
maxTokens: 40,
|
|
723
|
-
startIndex: 0,
|
|
724
|
-
tokenCounter,
|
|
725
|
-
indexTokenCountMap,
|
|
726
|
-
reserveRatio: 0,
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
const result = pruneMessages({ messages });
|
|
730
|
-
|
|
731
|
-
// Should include system message and the last two messages
|
|
732
|
-
expect(result.context.length).toBe(3);
|
|
733
|
-
expect(result.context[0]).toBe(messages[0]); // System message
|
|
734
|
-
expect(result.context[1]).toBe(messages[3]); // Message 2
|
|
735
|
-
expect(result.context[2]).toBe(messages[4]); // Response 2
|
|
736
|
-
expect(Array.isArray(result.messagesToRefine)).toBe(true);
|
|
737
|
-
expect(result.messagesToRefine?.length).toBe(2);
|
|
738
|
-
expect(typeof result.remainingContextTokens).toBe('number');
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
it('should return remaining tokens in calibrated units when pruning with calibration', () => {
|
|
742
|
-
const tokenCounter = createTestTokenCounter();
|
|
743
|
-
const messages = [
|
|
744
|
-
new SystemMessage('System instruction'),
|
|
745
|
-
new HumanMessage('Message 1'),
|
|
746
|
-
new AIMessage('Response 1'),
|
|
747
|
-
new HumanMessage('Message 2'),
|
|
748
|
-
new AIMessage('Response 2'),
|
|
749
|
-
];
|
|
750
|
-
|
|
751
|
-
const indexTokenCountMap = {
|
|
752
|
-
0: tokenCounter(messages[0]),
|
|
753
|
-
1: tokenCounter(messages[1]),
|
|
754
|
-
2: tokenCounter(messages[2]),
|
|
755
|
-
3: tokenCounter(messages[3]),
|
|
756
|
-
4: tokenCounter(messages[4]),
|
|
757
|
-
};
|
|
758
|
-
|
|
759
|
-
const calibrationRatio = 2;
|
|
760
|
-
const maxTokens = 80;
|
|
761
|
-
const pruneMessages = createPruneMessages({
|
|
762
|
-
maxTokens,
|
|
763
|
-
startIndex: 0,
|
|
764
|
-
tokenCounter,
|
|
765
|
-
indexTokenCountMap,
|
|
766
|
-
reserveRatio: 0,
|
|
767
|
-
calibrationRatio,
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
const result = pruneMessages({ messages });
|
|
771
|
-
|
|
772
|
-
expect(result.messagesToRefine?.length).toBeGreaterThan(0);
|
|
773
|
-
|
|
774
|
-
/** Pruning selects within rawSpaceBudget = maxTokens / ratio (raw units,
|
|
775
|
-
* minus the 3-token assistant label); the returned remaining must be
|
|
776
|
-
* scaled back so `budget - remaining` reflects provider-space usage */
|
|
777
|
-
const keptRaw = result.context.reduce(
|
|
778
|
-
(sum, msg) => sum + tokenCounter(msg),
|
|
779
|
-
0
|
|
780
|
-
);
|
|
781
|
-
const rawSpaceBudget = Math.round(maxTokens / calibrationRatio);
|
|
782
|
-
const expectedRemaining =
|
|
783
|
-
(rawSpaceBudget - keptRaw - 3) * calibrationRatio;
|
|
784
|
-
expect(result.remainingContextTokens).toBe(expectedRemaining);
|
|
785
|
-
expect(result.contextBudget).toBe(maxTokens);
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
it('should respect startType parameter', () => {
|
|
789
|
-
const tokenCounter = createTestTokenCounter();
|
|
790
|
-
const messages = [
|
|
791
|
-
new SystemMessage('System instruction'),
|
|
792
|
-
new AIMessage('AI message 1'),
|
|
793
|
-
new HumanMessage('Human message 1'),
|
|
794
|
-
new AIMessage('AI message 2'),
|
|
795
|
-
new HumanMessage('Human message 2'),
|
|
796
|
-
];
|
|
797
|
-
|
|
798
|
-
const indexTokenCountMap = {
|
|
799
|
-
0: tokenCounter(messages[0]),
|
|
800
|
-
1: tokenCounter(messages[1]),
|
|
801
|
-
2: tokenCounter(messages[2]),
|
|
802
|
-
3: tokenCounter(messages[3]),
|
|
803
|
-
4: tokenCounter(messages[4]),
|
|
804
|
-
};
|
|
805
|
-
|
|
806
|
-
// Set a limit that can fit all messages
|
|
807
|
-
const pruneMessages = createPruneMessages({
|
|
808
|
-
maxTokens: 100,
|
|
809
|
-
startIndex: 0,
|
|
810
|
-
tokenCounter,
|
|
811
|
-
indexTokenCountMap: { ...indexTokenCountMap },
|
|
812
|
-
});
|
|
813
|
-
|
|
814
|
-
const result = pruneMessages({
|
|
815
|
-
messages,
|
|
816
|
-
startType: 'human',
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
// All messages should be included since we're under the token limit
|
|
820
|
-
expect(result.context.length).toBe(5);
|
|
821
|
-
expect(result.context[0]).toBe(messages[0]); // System message
|
|
822
|
-
expect(result.context[1]).toBe(messages[1]); // AI message 1
|
|
823
|
-
expect(result.context[2]).toBe(messages[2]); // Human message 1
|
|
824
|
-
expect(result.context[3]).toBe(messages[3]); // AI message 2
|
|
825
|
-
expect(result.context[4]).toBe(messages[4]); // Human message 2
|
|
826
|
-
});
|
|
827
|
-
|
|
828
|
-
it('should update token counts when usage metadata is provided', () => {
|
|
829
|
-
const tokenCounter = createTestTokenCounter();
|
|
830
|
-
const messages = [
|
|
831
|
-
new SystemMessage('System instruction'),
|
|
832
|
-
new HumanMessage('Hello'),
|
|
833
|
-
new AIMessage('Hi there'),
|
|
834
|
-
];
|
|
835
|
-
|
|
836
|
-
const indexTokenCountMap = {
|
|
837
|
-
0: tokenCounter(messages[0]),
|
|
838
|
-
1: tokenCounter(messages[1]),
|
|
839
|
-
2: tokenCounter(messages[2]),
|
|
840
|
-
};
|
|
841
|
-
|
|
842
|
-
const pruneMessages = createPruneMessages({
|
|
843
|
-
maxTokens: 100,
|
|
844
|
-
startIndex: 0,
|
|
845
|
-
tokenCounter,
|
|
846
|
-
indexTokenCountMap: { ...indexTokenCountMap },
|
|
847
|
-
});
|
|
848
|
-
|
|
849
|
-
// Provide usage metadata that indicates different token counts
|
|
850
|
-
const usageMetadata: Partial<UsageMetadata> = {
|
|
851
|
-
input_tokens: 50,
|
|
852
|
-
output_tokens: 25,
|
|
853
|
-
total_tokens: 75,
|
|
854
|
-
};
|
|
855
|
-
|
|
856
|
-
const result = pruneMessages({
|
|
857
|
-
messages,
|
|
858
|
-
usageMetadata,
|
|
859
|
-
});
|
|
860
|
-
|
|
861
|
-
// Map stays in raw tiktoken space — calibrationRatio captures the multiplier.
|
|
862
|
-
// rawSum * calibrationRatio should approximate input_tokens (50).
|
|
863
|
-
const rawSum = Object.values(result.indexTokenCountMap).reduce(
|
|
864
|
-
(a = 0, b = 0) => a + b,
|
|
865
|
-
0
|
|
866
|
-
) as number;
|
|
867
|
-
const calibratedEstimate = Math.round(
|
|
868
|
-
rawSum * (result.calibrationRatio ?? 1)
|
|
869
|
-
);
|
|
870
|
-
expect(Math.abs(calibratedEstimate - 50)).toBeLessThanOrEqual(3);
|
|
871
|
-
});
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
describe('Tool Message Handling', () => {
|
|
875
|
-
it('should drop orphan tool messages that no longer have matching AI tool calls', () => {
|
|
876
|
-
const tokenCounter = createTestTokenCounter();
|
|
877
|
-
const context = [
|
|
878
|
-
new SystemMessage('System instruction'),
|
|
879
|
-
new ToolMessage({
|
|
880
|
-
content: 'Orphan result',
|
|
881
|
-
tool_call_id: 'tool-orphan',
|
|
882
|
-
}),
|
|
883
|
-
new AIMessage({
|
|
884
|
-
content: [
|
|
885
|
-
{ type: 'text', text: 'I will call a tool now' },
|
|
886
|
-
{
|
|
887
|
-
type: 'tool_use',
|
|
888
|
-
id: 'tool-valid',
|
|
889
|
-
name: 'read_file',
|
|
890
|
-
input: '{"path":"README.md"}',
|
|
891
|
-
},
|
|
892
|
-
],
|
|
893
|
-
}),
|
|
894
|
-
new ToolMessage({
|
|
895
|
-
content: 'Valid result',
|
|
896
|
-
tool_call_id: 'tool-valid',
|
|
897
|
-
}),
|
|
898
|
-
];
|
|
899
|
-
|
|
900
|
-
const indexTokenCountMap = {
|
|
901
|
-
0: tokenCounter(context[0]),
|
|
902
|
-
1: tokenCounter(context[1]),
|
|
903
|
-
2: tokenCounter(context[2]),
|
|
904
|
-
3: tokenCounter(context[3]),
|
|
905
|
-
};
|
|
906
|
-
|
|
907
|
-
const repaired = repairOrphanedToolMessages({
|
|
908
|
-
context,
|
|
909
|
-
allMessages: context,
|
|
910
|
-
tokenCounter,
|
|
911
|
-
indexTokenCountMap,
|
|
912
|
-
});
|
|
913
|
-
|
|
914
|
-
expect(repaired.context).toHaveLength(3);
|
|
915
|
-
expect(repaired.context[0]).toBe(context[0]);
|
|
916
|
-
expect(repaired.context[1]).toBe(context[2]);
|
|
917
|
-
expect(repaired.context[2]).toBe(context[3]);
|
|
918
|
-
expect(repaired.droppedOrphanCount).toBe(1);
|
|
919
|
-
expect(repaired.reclaimedTokens).toBe(indexTokenCountMap[1]);
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
it('should strip orphan tool_use blocks from AI messages when ToolMessages are not in context', () => {
|
|
923
|
-
const tokenCounter = createTestTokenCounter();
|
|
924
|
-
const context = [
|
|
925
|
-
new HumanMessage('Show me something cool'),
|
|
926
|
-
new AIMessage({
|
|
927
|
-
content: [
|
|
928
|
-
{ type: 'text', text: 'Let me create an animation.' },
|
|
929
|
-
{
|
|
930
|
-
type: 'tool_use',
|
|
931
|
-
id: 'tool-navigate',
|
|
932
|
-
name: 'navigate_page',
|
|
933
|
-
input: '{"url":"about:blank"}',
|
|
934
|
-
},
|
|
935
|
-
{
|
|
936
|
-
type: 'tool_use',
|
|
937
|
-
id: 'tool-script',
|
|
938
|
-
name: 'evaluate_script',
|
|
939
|
-
input: '{"function":"' + 'x'.repeat(3000) + '"}',
|
|
940
|
-
},
|
|
941
|
-
],
|
|
942
|
-
tool_calls: [
|
|
943
|
-
{
|
|
944
|
-
id: 'tool-navigate',
|
|
945
|
-
name: 'navigate_page',
|
|
946
|
-
args: { url: 'about:blank' },
|
|
947
|
-
},
|
|
948
|
-
{
|
|
949
|
-
id: 'tool-script',
|
|
950
|
-
name: 'evaluate_script',
|
|
951
|
-
args: { fn: 'x'.repeat(3000) },
|
|
952
|
-
},
|
|
953
|
-
],
|
|
954
|
-
}),
|
|
955
|
-
// ToolMessages for both tool calls are NOT in context (pruned)
|
|
956
|
-
];
|
|
957
|
-
|
|
958
|
-
const indexTokenCountMap = {
|
|
959
|
-
0: tokenCounter(context[0]),
|
|
960
|
-
1: tokenCounter(context[1]),
|
|
961
|
-
};
|
|
962
|
-
|
|
963
|
-
const repaired = repairOrphanedToolMessages({
|
|
964
|
-
context,
|
|
965
|
-
allMessages: context,
|
|
966
|
-
tokenCounter,
|
|
967
|
-
indexTokenCountMap,
|
|
968
|
-
});
|
|
969
|
-
|
|
970
|
-
// AI message should survive but with tool_use blocks stripped
|
|
971
|
-
expect(repaired.context).toHaveLength(2);
|
|
972
|
-
const repairedAI = repaired.context[1] as AIMessage;
|
|
973
|
-
expect(repairedAI.getType()).toBe('ai');
|
|
974
|
-
|
|
975
|
-
// Should only have the text block, no tool_use blocks
|
|
976
|
-
const content = repairedAI.content as Array<{ type: string }>;
|
|
977
|
-
expect(content).toHaveLength(1);
|
|
978
|
-
expect(content[0].type).toBe('text');
|
|
979
|
-
|
|
980
|
-
// tool_calls should be empty
|
|
981
|
-
expect(repairedAI.tool_calls ?? []).toHaveLength(0);
|
|
982
|
-
|
|
983
|
-
// Token savings from stripping the large tool_use blocks
|
|
984
|
-
expect(repaired.reclaimedTokens).toBeGreaterThan(0);
|
|
985
|
-
});
|
|
986
|
-
|
|
987
|
-
it('strips orphan raw Responses computer calls with parsed tool calls', () => {
|
|
988
|
-
const tokenCounter = createTestTokenCounter();
|
|
989
|
-
const computerCall = new AIMessage({
|
|
990
|
-
content: 'Taking a screenshot.',
|
|
991
|
-
tool_calls: [
|
|
992
|
-
{
|
|
993
|
-
id: 'computer-orphan',
|
|
994
|
-
name: 'computer_use',
|
|
995
|
-
args: { action: { type: 'screenshot' } },
|
|
996
|
-
type: 'tool_call',
|
|
997
|
-
},
|
|
998
|
-
],
|
|
999
|
-
response_metadata: {
|
|
1000
|
-
output: [
|
|
1001
|
-
{
|
|
1002
|
-
type: 'computer_call',
|
|
1003
|
-
id: 'computer-item',
|
|
1004
|
-
call_id: 'computer-orphan',
|
|
1005
|
-
action: { type: 'screenshot' },
|
|
1006
|
-
},
|
|
1007
|
-
],
|
|
1008
|
-
},
|
|
1009
|
-
});
|
|
1010
|
-
const context: BaseMessage[] = [
|
|
1011
|
-
new HumanMessage('take a screenshot'),
|
|
1012
|
-
computerCall,
|
|
1013
|
-
];
|
|
1014
|
-
|
|
1015
|
-
const repaired = repairOrphanedToolMessages({
|
|
1016
|
-
context,
|
|
1017
|
-
allMessages: context,
|
|
1018
|
-
tokenCounter,
|
|
1019
|
-
indexTokenCountMap: {
|
|
1020
|
-
0: tokenCounter(context[0]),
|
|
1021
|
-
1: tokenCounter(context[1]),
|
|
1022
|
-
},
|
|
1023
|
-
});
|
|
1024
|
-
const repairedAI = repaired.context[1] as AIMessage;
|
|
1025
|
-
|
|
1026
|
-
expect(repairedAI.tool_calls ?? []).toHaveLength(0);
|
|
1027
|
-
expect(repairedAI.response_metadata.output).toEqual([]);
|
|
1028
|
-
});
|
|
1029
|
-
|
|
1030
|
-
it('strips orphan computer calls from the LangChain tool_outputs fallback', () => {
|
|
1031
|
-
const tokenCounter = createTestTokenCounter();
|
|
1032
|
-
const computerCall = new AIMessage({
|
|
1033
|
-
content: 'Taking a screenshot.',
|
|
1034
|
-
additional_kwargs: {
|
|
1035
|
-
tool_outputs: [
|
|
1036
|
-
{
|
|
1037
|
-
type: 'computer_call',
|
|
1038
|
-
call_id: 'computer-fallback-orphan',
|
|
1039
|
-
action: { type: 'screenshot' },
|
|
1040
|
-
},
|
|
1041
|
-
],
|
|
1042
|
-
},
|
|
1043
|
-
});
|
|
1044
|
-
const context: BaseMessage[] = [
|
|
1045
|
-
new HumanMessage('take a screenshot'),
|
|
1046
|
-
computerCall,
|
|
1047
|
-
];
|
|
1048
|
-
|
|
1049
|
-
const repaired = repairOrphanedToolMessages({
|
|
1050
|
-
context,
|
|
1051
|
-
allMessages: context,
|
|
1052
|
-
tokenCounter,
|
|
1053
|
-
indexTokenCountMap: {
|
|
1054
|
-
0: tokenCounter(context[0]),
|
|
1055
|
-
1: tokenCounter(context[1]),
|
|
1056
|
-
},
|
|
1057
|
-
});
|
|
1058
|
-
const repairedAI = repaired.context[1] as AIMessage;
|
|
1059
|
-
|
|
1060
|
-
expect(repairedAI.additional_kwargs.tool_outputs).toEqual([]);
|
|
1061
|
-
});
|
|
1062
|
-
|
|
1063
|
-
it('retains an empty-content AI parent when one raw call remains paired', () => {
|
|
1064
|
-
const tokenCounter = createTestTokenCounter();
|
|
1065
|
-
const computerCall = new AIMessage({
|
|
1066
|
-
content: [],
|
|
1067
|
-
tool_calls: [
|
|
1068
|
-
{
|
|
1069
|
-
id: 'computer-keep',
|
|
1070
|
-
name: 'computer_use',
|
|
1071
|
-
args: { action: { type: 'screenshot' } },
|
|
1072
|
-
type: 'tool_call',
|
|
1073
|
-
},
|
|
1074
|
-
{
|
|
1075
|
-
id: 'computer-drop',
|
|
1076
|
-
name: 'computer_use',
|
|
1077
|
-
args: { action: { type: 'screenshot' } },
|
|
1078
|
-
type: 'tool_call',
|
|
1079
|
-
},
|
|
1080
|
-
],
|
|
1081
|
-
response_metadata: {
|
|
1082
|
-
output: [
|
|
1083
|
-
{
|
|
1084
|
-
type: 'computer_call',
|
|
1085
|
-
call_id: 'computer-keep',
|
|
1086
|
-
action: { type: 'screenshot' },
|
|
1087
|
-
},
|
|
1088
|
-
{
|
|
1089
|
-
type: 'computer_call',
|
|
1090
|
-
call_id: 'computer-drop',
|
|
1091
|
-
action: { type: 'screenshot' },
|
|
1092
|
-
},
|
|
1093
|
-
],
|
|
1094
|
-
},
|
|
1095
|
-
});
|
|
1096
|
-
const computerOutput = new ToolMessage({
|
|
1097
|
-
content: 'data:image/png;base64,AA==',
|
|
1098
|
-
tool_call_id: 'computer-keep',
|
|
1099
|
-
additional_kwargs: { type: 'computer_call_output' },
|
|
1100
|
-
});
|
|
1101
|
-
const context: BaseMessage[] = [computerCall, computerOutput];
|
|
1102
|
-
|
|
1103
|
-
const repaired = repairOrphanedToolMessages({
|
|
1104
|
-
context,
|
|
1105
|
-
allMessages: context,
|
|
1106
|
-
tokenCounter,
|
|
1107
|
-
indexTokenCountMap: {
|
|
1108
|
-
0: tokenCounter(computerCall),
|
|
1109
|
-
1: tokenCounter(computerOutput),
|
|
1110
|
-
},
|
|
1111
|
-
});
|
|
1112
|
-
|
|
1113
|
-
expect(repaired.context).toHaveLength(2);
|
|
1114
|
-
const repairedAI = repaired.context[0] as AIMessage;
|
|
1115
|
-
expect(repairedAI.tool_calls?.map((call) => call.id)).toEqual([
|
|
1116
|
-
'computer-keep',
|
|
1117
|
-
]);
|
|
1118
|
-
expect(repairedAI.response_metadata.output).toEqual([
|
|
1119
|
-
expect.objectContaining({ call_id: 'computer-keep' }),
|
|
1120
|
-
]);
|
|
1121
|
-
expect(repaired.context[1]).toBe(computerOutput);
|
|
1122
|
-
});
|
|
1123
|
-
|
|
1124
|
-
it('recognizes raw computer calls on generic assistant messages', () => {
|
|
1125
|
-
const tokenCounter = createTestTokenCounter();
|
|
1126
|
-
const computerCall = new ChatMessage({
|
|
1127
|
-
role: 'assistant',
|
|
1128
|
-
content: '',
|
|
1129
|
-
response_metadata: {
|
|
1130
|
-
output: [
|
|
1131
|
-
{
|
|
1132
|
-
type: 'computer_call',
|
|
1133
|
-
call_id: 'generic-computer',
|
|
1134
|
-
action: { type: 'screenshot' },
|
|
1135
|
-
},
|
|
1136
|
-
],
|
|
1137
|
-
},
|
|
1138
|
-
});
|
|
1139
|
-
const computerOutput = new ToolMessage({
|
|
1140
|
-
content: 'data:image/png;base64,AA==',
|
|
1141
|
-
tool_call_id: 'generic-computer',
|
|
1142
|
-
additional_kwargs: { type: 'computer_call_output' },
|
|
1143
|
-
});
|
|
1144
|
-
const context: BaseMessage[] = [computerCall, computerOutput];
|
|
1145
|
-
|
|
1146
|
-
const repaired = repairOrphanedToolMessages({
|
|
1147
|
-
context,
|
|
1148
|
-
allMessages: context,
|
|
1149
|
-
tokenCounter,
|
|
1150
|
-
indexTokenCountMap: {
|
|
1151
|
-
0: tokenCounter(computerCall),
|
|
1152
|
-
1: tokenCounter(computerOutput),
|
|
1153
|
-
},
|
|
1154
|
-
});
|
|
1155
|
-
|
|
1156
|
-
expect(repaired.context).toEqual(context);
|
|
1157
|
-
expect(repaired.droppedOrphanCount).toBe(0);
|
|
1158
|
-
});
|
|
1159
|
-
|
|
1160
|
-
it('should drop AI message entirely when it has only tool_use blocks with no text', () => {
|
|
1161
|
-
const tokenCounter = createTestTokenCounter();
|
|
1162
|
-
const context = [
|
|
1163
|
-
new HumanMessage('Do something'),
|
|
1164
|
-
new AIMessage({
|
|
1165
|
-
content: [
|
|
1166
|
-
{
|
|
1167
|
-
type: 'tool_use',
|
|
1168
|
-
id: 'tool-only',
|
|
1169
|
-
name: 'some_tool',
|
|
1170
|
-
input: '{"query":"test"}',
|
|
1171
|
-
},
|
|
1172
|
-
],
|
|
1173
|
-
tool_calls: [
|
|
1174
|
-
{ id: 'tool-only', name: 'some_tool', args: { query: 'test' } },
|
|
1175
|
-
],
|
|
1176
|
-
}),
|
|
1177
|
-
];
|
|
1178
|
-
|
|
1179
|
-
const indexTokenCountMap = {
|
|
1180
|
-
0: tokenCounter(context[0]),
|
|
1181
|
-
1: tokenCounter(context[1]),
|
|
1182
|
-
};
|
|
1183
|
-
|
|
1184
|
-
const repaired = repairOrphanedToolMessages({
|
|
1185
|
-
context,
|
|
1186
|
-
allMessages: context,
|
|
1187
|
-
tokenCounter,
|
|
1188
|
-
indexTokenCountMap,
|
|
1189
|
-
});
|
|
1190
|
-
|
|
1191
|
-
// AI message should be dropped since it had only tool_use blocks
|
|
1192
|
-
expect(repaired.context).toHaveLength(1);
|
|
1193
|
-
expect(repaired.context[0].getType()).toBe('human');
|
|
1194
|
-
expect(repaired.droppedOrphanCount).toBe(1);
|
|
1195
|
-
});
|
|
1196
|
-
|
|
1197
|
-
it('should keep tool_use blocks when their ToolMessages ARE in context', () => {
|
|
1198
|
-
const tokenCounter = createTestTokenCounter();
|
|
1199
|
-
const context = [
|
|
1200
|
-
new HumanMessage('Do something'),
|
|
1201
|
-
new AIMessage({
|
|
1202
|
-
content: [
|
|
1203
|
-
{ type: 'text', text: 'Calling tool' },
|
|
1204
|
-
{
|
|
1205
|
-
type: 'tool_use',
|
|
1206
|
-
id: 'tool-present',
|
|
1207
|
-
name: 'read_file',
|
|
1208
|
-
input: '{"path":"test.txt"}',
|
|
1209
|
-
},
|
|
1210
|
-
],
|
|
1211
|
-
tool_calls: [
|
|
1212
|
-
{
|
|
1213
|
-
id: 'tool-present',
|
|
1214
|
-
name: 'read_file',
|
|
1215
|
-
args: { path: 'test.txt' },
|
|
1216
|
-
},
|
|
1217
|
-
],
|
|
1218
|
-
}),
|
|
1219
|
-
new ToolMessage({
|
|
1220
|
-
content: 'File contents here',
|
|
1221
|
-
tool_call_id: 'tool-present',
|
|
1222
|
-
}),
|
|
1223
|
-
];
|
|
1224
|
-
|
|
1225
|
-
const indexTokenCountMap = {
|
|
1226
|
-
0: tokenCounter(context[0]),
|
|
1227
|
-
1: tokenCounter(context[1]),
|
|
1228
|
-
2: tokenCounter(context[2]),
|
|
1229
|
-
};
|
|
1230
|
-
|
|
1231
|
-
const repaired = repairOrphanedToolMessages({
|
|
1232
|
-
context,
|
|
1233
|
-
allMessages: context,
|
|
1234
|
-
tokenCounter,
|
|
1235
|
-
indexTokenCountMap,
|
|
1236
|
-
});
|
|
1237
|
-
|
|
1238
|
-
// Nothing should change — all tool_use blocks have matching ToolMessages
|
|
1239
|
-
expect(repaired.context).toHaveLength(3);
|
|
1240
|
-
expect(repaired.reclaimedTokens).toBe(0);
|
|
1241
|
-
expect(repaired.droppedOrphanCount).toBe(0);
|
|
1242
|
-
});
|
|
1243
|
-
|
|
1244
|
-
it('should ensure context does not start with a tool message by finding an AI message', () => {
|
|
1245
|
-
const tokenCounter = createTestTokenCounter();
|
|
1246
|
-
const messages = [
|
|
1247
|
-
new SystemMessage('System instruction'),
|
|
1248
|
-
new AIMessage({
|
|
1249
|
-
content: [{ type: 'text', text: 'AI msg 1' }],
|
|
1250
|
-
tool_calls: [{ id: 'tool1', name: 'read_file', args: { p: '1' } }],
|
|
1251
|
-
}),
|
|
1252
|
-
new ToolMessage({ content: 'Tool result 1', tool_call_id: 'tool1' }),
|
|
1253
|
-
new AIMessage({
|
|
1254
|
-
content: [{ type: 'text', text: 'AI msg 2' }],
|
|
1255
|
-
tool_calls: [{ id: 'tool2', name: 'read_file', args: { p: '2' } }],
|
|
1256
|
-
}),
|
|
1257
|
-
new ToolMessage({ content: 'Tool result 2', tool_call_id: 'tool2' }),
|
|
1258
|
-
];
|
|
1259
|
-
|
|
1260
|
-
const indexTokenCountMap = {
|
|
1261
|
-
0: 17, // System instruction
|
|
1262
|
-
1: 12, // AI message 1
|
|
1263
|
-
2: 13, // Tool result 1
|
|
1264
|
-
3: 12, // AI message 2
|
|
1265
|
-
4: 13, // Tool result 2
|
|
1266
|
-
};
|
|
1267
|
-
|
|
1268
|
-
// Create a pruneMessages function with a token limit that will only include the last few messages
|
|
1269
|
-
const pruneMessages = createPruneMessages({
|
|
1270
|
-
maxTokens: 58, // Only enough for system + last 3 messages + 3, but should not include a parent-less tool message
|
|
1271
|
-
startIndex: 0,
|
|
1272
|
-
tokenCounter,
|
|
1273
|
-
indexTokenCountMap: { ...indexTokenCountMap },
|
|
1274
|
-
});
|
|
1275
|
-
|
|
1276
|
-
const result = pruneMessages({ messages });
|
|
1277
|
-
|
|
1278
|
-
// The context should include the system message, AI message 2, and Tool result 2
|
|
1279
|
-
// AI message 1 + Tool result 1 are pruned. Tool result 1 is orphaned (AI 1 pruned).
|
|
1280
|
-
expect(result.context.length).toBe(3);
|
|
1281
|
-
expect(result.context[0]).toBe(messages[0]); // System message
|
|
1282
|
-
expect(result.context[1].getType()).toBe('ai'); // AI message 2
|
|
1283
|
-
expect(result.context[2]).toBe(messages[4]); // Tool result 2
|
|
1284
|
-
});
|
|
1285
|
-
|
|
1286
|
-
it('should ensure context does not start with a tool message by finding a human message', () => {
|
|
1287
|
-
const tokenCounter = createTestTokenCounter();
|
|
1288
|
-
const messages = [
|
|
1289
|
-
new SystemMessage('System instruction'),
|
|
1290
|
-
new HumanMessage('Human message 1'),
|
|
1291
|
-
new AIMessage('AI message 1'),
|
|
1292
|
-
new ToolMessage({ content: 'Tool result 1', tool_call_id: 'tool1' }),
|
|
1293
|
-
new HumanMessage('Human message 2'),
|
|
1294
|
-
// Tool result 2 has no parent AI tool_call — this is an orphan
|
|
1295
|
-
new ToolMessage({ content: 'Tool result 2', tool_call_id: 'tool2' }),
|
|
1296
|
-
];
|
|
1297
|
-
|
|
1298
|
-
const indexTokenCountMap = {
|
|
1299
|
-
0: 17, // System instruction
|
|
1300
|
-
1: 15, // Human message 1
|
|
1301
|
-
2: 12, // AI message 1
|
|
1302
|
-
3: 13, // Tool result 1
|
|
1303
|
-
4: 15, // Human message 2
|
|
1304
|
-
5: 13, // Tool result 2
|
|
1305
|
-
};
|
|
1306
|
-
|
|
1307
|
-
// Create a pruneMessages function with a token limit that will only include the last few messages
|
|
1308
|
-
const pruneMessages = createPruneMessages({
|
|
1309
|
-
maxTokens: 48, // Only enough for system + last 2 messages
|
|
1310
|
-
startIndex: 0,
|
|
1311
|
-
tokenCounter,
|
|
1312
|
-
indexTokenCountMap: { ...indexTokenCountMap },
|
|
1313
|
-
reserveRatio: 0,
|
|
1314
|
-
});
|
|
1315
|
-
|
|
1316
|
-
const result = pruneMessages({ messages });
|
|
1317
|
-
|
|
1318
|
-
// Tool result 2 is an orphan (no AI message with tool_call_id 'tool2' in context)
|
|
1319
|
-
// so it gets dropped. Context is system + human message 2.
|
|
1320
|
-
expect(result.context.length).toBe(2);
|
|
1321
|
-
expect(result.context[0]).toBe(messages[0]); // System message
|
|
1322
|
-
expect(result.context[1]).toBe(messages[4]); // Human message 2
|
|
1323
|
-
});
|
|
1324
|
-
|
|
1325
|
-
it('should handle the case where a tool message is followed by an AI message', () => {
|
|
1326
|
-
const tokenCounter = createTestTokenCounter();
|
|
1327
|
-
const messages = [
|
|
1328
|
-
new SystemMessage('System instruction'),
|
|
1329
|
-
new HumanMessage('Human message'),
|
|
1330
|
-
new AIMessage('AI message with tool use'),
|
|
1331
|
-
new ToolMessage({ content: 'Tool result', tool_call_id: 'tool1' }),
|
|
1332
|
-
new AIMessage('AI message after tool'),
|
|
1333
|
-
];
|
|
1334
|
-
|
|
1335
|
-
const indexTokenCountMap = {
|
|
1336
|
-
0: 17, // System instruction
|
|
1337
|
-
1: 13, // Human message
|
|
1338
|
-
2: 22, // AI message with tool use
|
|
1339
|
-
3: 11, // Tool result
|
|
1340
|
-
4: 19, // AI message after tool
|
|
1341
|
-
};
|
|
1342
|
-
|
|
1343
|
-
const pruneMessages = createPruneMessages({
|
|
1344
|
-
maxTokens: 50,
|
|
1345
|
-
startIndex: 0,
|
|
1346
|
-
tokenCounter,
|
|
1347
|
-
indexTokenCountMap: { ...indexTokenCountMap },
|
|
1348
|
-
});
|
|
1349
|
-
|
|
1350
|
-
const result = pruneMessages({ messages });
|
|
1351
|
-
|
|
1352
|
-
expect(result.context.length).toBe(2);
|
|
1353
|
-
expect(result.context[0]).toBe(messages[0]); // System message
|
|
1354
|
-
expect(result.context[1]).toBe(messages[4]); // AI message after tool
|
|
1355
|
-
});
|
|
1356
|
-
|
|
1357
|
-
it('should handle the case where a tool message is followed by a human message', () => {
|
|
1358
|
-
const tokenCounter = createTestTokenCounter();
|
|
1359
|
-
const messages = [
|
|
1360
|
-
new SystemMessage('System instruction'),
|
|
1361
|
-
new HumanMessage('Human message 1'),
|
|
1362
|
-
new AIMessage('AI message with tool use'),
|
|
1363
|
-
new ToolMessage({ content: 'Tool result', tool_call_id: 'tool1' }),
|
|
1364
|
-
new HumanMessage('Human message 2'),
|
|
1365
|
-
];
|
|
1366
|
-
|
|
1367
|
-
const indexTokenCountMap = {
|
|
1368
|
-
0: 17, // System instruction
|
|
1369
|
-
1: 15, // Human message 1
|
|
1370
|
-
2: 22, // AI message with tool use
|
|
1371
|
-
3: 11, // Tool result
|
|
1372
|
-
4: 15, // Human message 2
|
|
1373
|
-
};
|
|
1374
|
-
|
|
1375
|
-
const pruneMessages = createPruneMessages({
|
|
1376
|
-
maxTokens: 46,
|
|
1377
|
-
startIndex: 0,
|
|
1378
|
-
tokenCounter,
|
|
1379
|
-
indexTokenCountMap: { ...indexTokenCountMap },
|
|
1380
|
-
});
|
|
1381
|
-
|
|
1382
|
-
const result = pruneMessages({ messages });
|
|
1383
|
-
|
|
1384
|
-
expect(result.context.length).toBe(2);
|
|
1385
|
-
expect(result.context[0]).toBe(messages[0]); // System message
|
|
1386
|
-
expect(result.context[1]).toBe(messages[4]); // Human message 2
|
|
1387
|
-
});
|
|
1388
|
-
|
|
1389
|
-
it('should handle complex sequence with multiple tool messages', () => {
|
|
1390
|
-
const tokenCounter = createTestTokenCounter();
|
|
1391
|
-
const messages = [
|
|
1392
|
-
new SystemMessage('System instruction'),
|
|
1393
|
-
new HumanMessage('Human message 1'),
|
|
1394
|
-
new AIMessage({
|
|
1395
|
-
content: [{ type: 'text', text: 'AI message 1' }],
|
|
1396
|
-
tool_calls: [{ id: 'tool1', name: 'read_file', args: { path: 'a' } }],
|
|
1397
|
-
}),
|
|
1398
|
-
new ToolMessage({ content: 'Tool result 1', tool_call_id: 'tool1' }),
|
|
1399
|
-
new AIMessage({
|
|
1400
|
-
content: [{ type: 'text', text: 'AI message 2' }],
|
|
1401
|
-
tool_calls: [{ id: 'tool2', name: 'read_file', args: { path: 'b' } }],
|
|
1402
|
-
}),
|
|
1403
|
-
new ToolMessage({ content: 'Tool result 2', tool_call_id: 'tool2' }),
|
|
1404
|
-
new AIMessage({
|
|
1405
|
-
content: [{ type: 'text', text: 'AI message 3' }],
|
|
1406
|
-
tool_calls: [{ id: 'tool3', name: 'read_file', args: { path: 'c' } }],
|
|
1407
|
-
}),
|
|
1408
|
-
new ToolMessage({ content: 'Tool result 3', tool_call_id: 'tool3' }),
|
|
1409
|
-
];
|
|
1410
|
-
|
|
1411
|
-
const indexTokenCountMap = {
|
|
1412
|
-
0: 17, // System instruction
|
|
1413
|
-
1: 15, // Human message 1
|
|
1414
|
-
2: 26, // AI message 1 with tool use
|
|
1415
|
-
3: 13, // Tool result 1
|
|
1416
|
-
4: 26, // AI message 2 with tool use
|
|
1417
|
-
5: 13, // Tool result 2
|
|
1418
|
-
6: 26, // AI message 3 with tool use
|
|
1419
|
-
7: 13, // Tool result 3
|
|
1420
|
-
};
|
|
1421
|
-
|
|
1422
|
-
const pruneMessages = createPruneMessages({
|
|
1423
|
-
maxTokens: 111,
|
|
1424
|
-
startIndex: 0,
|
|
1425
|
-
tokenCounter,
|
|
1426
|
-
indexTokenCountMap: { ...indexTokenCountMap },
|
|
1427
|
-
});
|
|
1428
|
-
|
|
1429
|
-
const result = pruneMessages({ messages });
|
|
1430
|
-
|
|
1431
|
-
// AI messages 2 & 3 with their ToolMessages fit; AI1+Tool1 pruned; Tool1 is orphan (AI1 pruned)
|
|
1432
|
-
expect(result.context.length).toBe(5);
|
|
1433
|
-
expect(result.context[0]).toBe(messages[0]); // System message
|
|
1434
|
-
expect(result.context[1].getType()).toBe('ai'); // AI message 2
|
|
1435
|
-
expect(result.context[2]).toBe(messages[5]); // Tool result 2
|
|
1436
|
-
expect(result.context[3].getType()).toBe('ai'); // AI message 3
|
|
1437
|
-
expect(result.context[4]).toBe(messages[7]); // Tool result 3
|
|
1438
|
-
});
|
|
1439
|
-
});
|
|
1440
|
-
|
|
1441
|
-
describe('projectToolCallInputs', () => {
|
|
1442
|
-
it('uses the shared 15%-of-context cap with a 200K ceiling', () => {
|
|
1443
|
-
expect(calculateMaxToolCallInputChars()).toBe(200_000);
|
|
1444
|
-
expect(calculateMaxToolCallInputChars(0)).toBe(200_000);
|
|
1445
|
-
expect(calculateMaxToolCallInputChars(1_000)).toBe(600);
|
|
1446
|
-
expect(calculateMaxToolCallInputChars(1_000_000)).toBe(200_000);
|
|
1447
|
-
// Even below the envelope floor, serialized args stay a JSON object —
|
|
1448
|
-
// 'null' here poisoned replayed tool calls (Anthropic 400s a non-object
|
|
1449
|
-
// tool_use.input).
|
|
1450
|
-
expect(serializeToolCallInput(undefined, 4)).toBe('{}');
|
|
1451
|
-
});
|
|
1452
|
-
|
|
1453
|
-
it('never nulls an input when the cap is below the truncation envelope', () => {
|
|
1454
|
-
// Regression: a tight summarization budget can shrink the per-input cap
|
|
1455
|
-
// below the `{_truncated, _originalChars}` envelope size (~38 chars).
|
|
1456
|
-
// The projection used to return `null` for BOTH the inline block input
|
|
1457
|
-
// and the tool_calls args; the nulls were written back into graph state
|
|
1458
|
-
// by preFlightTruncateToolCallInputs and later replayed to Anthropic as
|
|
1459
|
-
// `tool_use.input: null` → 400 "Input should be an object".
|
|
1460
|
-
const chunk = new AIMessageChunk({
|
|
1461
|
-
content: [
|
|
1462
|
-
{
|
|
1463
|
-
type: 'tool_use',
|
|
1464
|
-
id: 'tiny-cap-call',
|
|
1465
|
-
name: 'calculator',
|
|
1466
|
-
// Streaming leaves the raw JSON string on the block.
|
|
1467
|
-
input: '{"input": "670592745 / 99991"}',
|
|
1468
|
-
},
|
|
1469
|
-
],
|
|
1470
|
-
tool_calls: [
|
|
1471
|
-
{
|
|
1472
|
-
id: 'tiny-cap-call',
|
|
1473
|
-
name: 'calculator',
|
|
1474
|
-
args: { input: '670592745 / 99991' },
|
|
1475
|
-
},
|
|
1476
|
-
],
|
|
1477
|
-
});
|
|
1478
|
-
|
|
1479
|
-
const [projected] = projectToolCallInputs([chunk], 20);
|
|
1480
|
-
const block = (
|
|
1481
|
-
(projected as AIMessageChunk).content as Array<Record<string, unknown>>
|
|
1482
|
-
)[0];
|
|
1483
|
-
expect(block.input).toEqual({});
|
|
1484
|
-
expect((projected as AIMessageChunk).tool_calls?.[0].args).toEqual({});
|
|
1485
|
-
});
|
|
1486
|
-
|
|
1487
|
-
it('returns the original array when every input is already safe and bounded', () => {
|
|
1488
|
-
const messages: BaseMessage[] = [
|
|
1489
|
-
new HumanMessage('Run it'),
|
|
1490
|
-
new AIMessage({
|
|
1491
|
-
content: '',
|
|
1492
|
-
tool_calls: [
|
|
1493
|
-
{ id: 'safe-call', name: 'lookup', args: { query: 'safe' } },
|
|
1494
|
-
],
|
|
1495
|
-
}),
|
|
1496
|
-
];
|
|
1497
|
-
|
|
1498
|
-
const projected = projectToolCallInputs(messages, 1_000);
|
|
1499
|
-
|
|
1500
|
-
expect(projected).toBe(messages);
|
|
1501
|
-
expect(projected[1]).toBe(messages[1]);
|
|
1502
|
-
});
|
|
1503
|
-
|
|
1504
|
-
it('copy-on-write projects inline input and tool_calls args on AIMessageChunk', () => {
|
|
1505
|
-
let toJSONCalls = 0;
|
|
1506
|
-
const chunk = new AIMessageChunk({
|
|
1507
|
-
content: [
|
|
1508
|
-
{ type: 'text', text: 'Running.' },
|
|
1509
|
-
{
|
|
1510
|
-
type: 'tool_use',
|
|
1511
|
-
id: 'chunk-call',
|
|
1512
|
-
name: 'evaluate_script',
|
|
1513
|
-
input: { code: 'x'.repeat(2_000) },
|
|
1514
|
-
},
|
|
1515
|
-
],
|
|
1516
|
-
id: 'chunk-id',
|
|
1517
|
-
additional_kwargs: { marker: 'keep' },
|
|
1518
|
-
response_metadata: { model: 'chunk-model' },
|
|
1519
|
-
tool_calls: [
|
|
1520
|
-
{
|
|
1521
|
-
id: 'chunk-call',
|
|
1522
|
-
name: 'evaluate_script',
|
|
1523
|
-
args: {
|
|
1524
|
-
query: 'safe',
|
|
1525
|
-
toJSON() {
|
|
1526
|
-
toJSONCalls++;
|
|
1527
|
-
return { code: 'y'.repeat(2_000_000) };
|
|
1528
|
-
},
|
|
1529
|
-
},
|
|
1530
|
-
},
|
|
1531
|
-
],
|
|
1532
|
-
});
|
|
1533
|
-
chunk.tool_call_chunks = [
|
|
1534
|
-
{
|
|
1535
|
-
id: 'chunk-call',
|
|
1536
|
-
index: 0,
|
|
1537
|
-
name: 'evaluate_script',
|
|
1538
|
-
args: '',
|
|
1539
|
-
type: 'tool_call_chunk',
|
|
1540
|
-
},
|
|
1541
|
-
];
|
|
1542
|
-
const messages: BaseMessage[] = [chunk];
|
|
1543
|
-
|
|
1544
|
-
const projected = projectToolCallInputs(messages, 200);
|
|
1545
|
-
|
|
1546
|
-
expect(projected).not.toBe(messages);
|
|
1547
|
-
expect(projected[0]).not.toBe(chunk);
|
|
1548
|
-
expect(projected[0]).toBeInstanceOf(AIMessageChunk);
|
|
1549
|
-
expect(toJSONCalls).toBe(0);
|
|
1550
|
-
const projectedChunk = projected[0] as AIMessageChunk;
|
|
1551
|
-
expect(projectedChunk.id).toBe('chunk-id');
|
|
1552
|
-
expect(projectedChunk.additional_kwargs).toEqual({ marker: 'keep' });
|
|
1553
|
-
expect(projectedChunk.response_metadata).toEqual({
|
|
1554
|
-
model: 'chunk-model',
|
|
1555
|
-
});
|
|
1556
|
-
expect(projectedChunk.tool_call_chunks).toEqual(chunk.tool_call_chunks);
|
|
1557
|
-
const inlineToolUse = (
|
|
1558
|
-
projectedChunk.content as Array<Record<string, unknown>>
|
|
1559
|
-
).find((block) => block.type === 'tool_use');
|
|
1560
|
-
expect(inlineToolUse?.input).toMatchObject({
|
|
1561
|
-
_truncated: expect.stringContaining('truncated'),
|
|
1562
|
-
_originalChars: expect.any(Number),
|
|
1563
|
-
});
|
|
1564
|
-
expect(projectedChunk.tool_calls?.[0].args).toEqual({ query: 'safe' });
|
|
1565
|
-
expect(
|
|
1566
|
-
(
|
|
1567
|
-
(chunk.content as Array<Record<string, unknown>>)[1].input as {
|
|
1568
|
-
code: string;
|
|
1569
|
-
}
|
|
1570
|
-
).code
|
|
1571
|
-
).toHaveLength(2_000);
|
|
1572
|
-
});
|
|
1573
|
-
|
|
1574
|
-
it('normalizes cyclic and bigint args into provider-safe JSON values', () => {
|
|
1575
|
-
const cyclic: Record<string, unknown> = { query: 'safe' };
|
|
1576
|
-
cyclic.self = cyclic;
|
|
1577
|
-
const messages: BaseMessage[] = [
|
|
1578
|
-
new AIMessage({
|
|
1579
|
-
content: '',
|
|
1580
|
-
tool_calls: [
|
|
1581
|
-
{
|
|
1582
|
-
id: 'non-json-call',
|
|
1583
|
-
name: 'lookup',
|
|
1584
|
-
args: { cyclic, count: 7n },
|
|
1585
|
-
},
|
|
1586
|
-
],
|
|
1587
|
-
}),
|
|
1588
|
-
];
|
|
1589
|
-
|
|
1590
|
-
const projected = projectToolCallInputs(messages, 1_000);
|
|
1591
|
-
|
|
1592
|
-
expect(projected).not.toBe(messages);
|
|
1593
|
-
const args = (projected[0] as AIMessage).tool_calls?.[0].args as {
|
|
1594
|
-
cyclic: { self: string };
|
|
1595
|
-
count: string;
|
|
1596
|
-
};
|
|
1597
|
-
expect(args.cyclic.self).toBe('[Circular]');
|
|
1598
|
-
expect(args.count).toBe('7');
|
|
1599
|
-
expect(() => JSON.stringify(args)).not.toThrow();
|
|
1600
|
-
});
|
|
1601
|
-
|
|
1602
|
-
it('bounds legacy additional_kwargs.function_call arguments', () => {
|
|
1603
|
-
const message = new AIMessage({
|
|
1604
|
-
content: '',
|
|
1605
|
-
additional_kwargs: {
|
|
1606
|
-
marker: 'preserved',
|
|
1607
|
-
function_call: {
|
|
1608
|
-
name: 'legacy_lookup',
|
|
1609
|
-
arguments: `{"query":"${'x'.repeat(2_000)}"}`,
|
|
1610
|
-
},
|
|
1611
|
-
},
|
|
1612
|
-
});
|
|
1613
|
-
const messages: BaseMessage[] = [message];
|
|
1614
|
-
|
|
1615
|
-
const projected = projectToolCallInputs(messages, 200);
|
|
1616
|
-
|
|
1617
|
-
expect(projected).not.toBe(messages);
|
|
1618
|
-
expect(projected[0]).not.toBe(message);
|
|
1619
|
-
expect(projected[0].additional_kwargs.marker).toBe('preserved');
|
|
1620
|
-
const projectedFunctionCall = projected[0].additional_kwargs
|
|
1621
|
-
.function_call as {
|
|
1622
|
-
name: string;
|
|
1623
|
-
arguments: string;
|
|
1624
|
-
};
|
|
1625
|
-
expect(projectedFunctionCall.name).toBe('legacy_lookup');
|
|
1626
|
-
expect(projectedFunctionCall.arguments.length).toBeLessThanOrEqual(200);
|
|
1627
|
-
expect(() => JSON.parse(projectedFunctionCall.arguments)).not.toThrow();
|
|
1628
|
-
expect(
|
|
1629
|
-
(
|
|
1630
|
-
message.additional_kwargs.function_call as {
|
|
1631
|
-
arguments: string;
|
|
1632
|
-
}
|
|
1633
|
-
).arguments.length
|
|
1634
|
-
).toBeGreaterThan(2_000);
|
|
1635
|
-
});
|
|
1636
|
-
|
|
1637
|
-
it('normalizes legacy function calls without invoking serialization hooks', () => {
|
|
1638
|
-
const toJSON = jest.fn(() => ({
|
|
1639
|
-
name: 'legacy_lookup',
|
|
1640
|
-
arguments: 'x'.repeat(500_000),
|
|
1641
|
-
}));
|
|
1642
|
-
const originalFunctionCall = {
|
|
1643
|
-
name: 'legacy_lookup',
|
|
1644
|
-
arguments: '{}',
|
|
1645
|
-
toJSON,
|
|
1646
|
-
};
|
|
1647
|
-
const message = new AIMessage({
|
|
1648
|
-
content: '',
|
|
1649
|
-
additional_kwargs: {
|
|
1650
|
-
function_call: originalFunctionCall,
|
|
1651
|
-
},
|
|
1652
|
-
});
|
|
1653
|
-
|
|
1654
|
-
const [projected] = projectToolCallInputs([message], 200);
|
|
1655
|
-
const projectedFunctionCall = projected.additional_kwargs
|
|
1656
|
-
.function_call as {
|
|
1657
|
-
name: string;
|
|
1658
|
-
arguments: string;
|
|
1659
|
-
};
|
|
1660
|
-
const serialized = JSON.stringify(projected.additional_kwargs);
|
|
1661
|
-
|
|
1662
|
-
expect(projected).not.toBe(message);
|
|
1663
|
-
expect(projectedFunctionCall).toEqual({
|
|
1664
|
-
name: 'legacy_lookup',
|
|
1665
|
-
arguments: '{}',
|
|
1666
|
-
});
|
|
1667
|
-
expect(Object.getPrototypeOf(projectedFunctionCall)).toBe(
|
|
1668
|
-
Object.prototype
|
|
1669
|
-
);
|
|
1670
|
-
expect(serialized.length).toBeLessThanOrEqual(300);
|
|
1671
|
-
expect(toJSON).not.toHaveBeenCalled();
|
|
1672
|
-
expect(projectToolCallInputs([projected], 200)[0]).toBe(projected);
|
|
1673
|
-
});
|
|
1674
|
-
|
|
1675
|
-
it('omits a proxied legacy function call without propagating descriptor traps', () => {
|
|
1676
|
-
let descriptorCalls = 0;
|
|
1677
|
-
const legacyProxy = new Proxy(
|
|
1678
|
-
{},
|
|
1679
|
-
{
|
|
1680
|
-
getOwnPropertyDescriptor() {
|
|
1681
|
-
descriptorCalls++;
|
|
1682
|
-
throw new Error('descriptor trap');
|
|
1683
|
-
},
|
|
1684
|
-
}
|
|
1685
|
-
);
|
|
1686
|
-
const message = new AIMessage({
|
|
1687
|
-
content: '',
|
|
1688
|
-
additional_kwargs: {},
|
|
1689
|
-
});
|
|
1690
|
-
(message.additional_kwargs as Record<string, unknown>).function_call =
|
|
1691
|
-
legacyProxy;
|
|
1692
|
-
|
|
1693
|
-
expect(() => projectToolCallInputs([message], 200)).not.toThrow();
|
|
1694
|
-
const [projected] = projectToolCallInputs([message], 200);
|
|
1695
|
-
|
|
1696
|
-
expect(projected.additional_kwargs.function_call).toBeUndefined();
|
|
1697
|
-
expect(descriptorCalls).toBe(0);
|
|
1698
|
-
expect(projectToolCallInputs([projected], 200)[0]).toBe(projected);
|
|
1699
|
-
});
|
|
1700
|
-
|
|
1701
|
-
it('shadows proxy-backed legacy metadata without invoking its get trap', () => {
|
|
1702
|
-
let getCalls = 0;
|
|
1703
|
-
const hugeFunctionCall = {
|
|
1704
|
-
name: 'legacy_lookup',
|
|
1705
|
-
arguments: 'x'.repeat(500_000),
|
|
1706
|
-
};
|
|
1707
|
-
const message = new AIMessage({
|
|
1708
|
-
content: '',
|
|
1709
|
-
additional_kwargs: {},
|
|
1710
|
-
});
|
|
1711
|
-
message.additional_kwargs = new Proxy(
|
|
1712
|
-
{},
|
|
1713
|
-
{
|
|
1714
|
-
get(_target, property) {
|
|
1715
|
-
getCalls++;
|
|
1716
|
-
return property === 'function_call' ? hugeFunctionCall : undefined;
|
|
1717
|
-
},
|
|
1718
|
-
getOwnPropertyDescriptor() {
|
|
1719
|
-
return undefined;
|
|
1720
|
-
},
|
|
1721
|
-
getPrototypeOf() {
|
|
1722
|
-
return null;
|
|
1723
|
-
},
|
|
1724
|
-
}
|
|
1725
|
-
);
|
|
1726
|
-
|
|
1727
|
-
const [projected] = projectToolCallInputs([message], 200);
|
|
1728
|
-
|
|
1729
|
-
expect(getCalls).toBe(0);
|
|
1730
|
-
expect(projected).not.toBe(message);
|
|
1731
|
-
expect(projected.additional_kwargs.function_call).toBeUndefined();
|
|
1732
|
-
expect(projectToolCallInputs([projected], 200)[0]).toBe(projected);
|
|
1733
|
-
});
|
|
1734
|
-
|
|
1735
|
-
it('shadows legacy calls beyond the bounded prototype walk', () => {
|
|
1736
|
-
const inherited = {
|
|
1737
|
-
function_call: {
|
|
1738
|
-
name: 'legacy_lookup',
|
|
1739
|
-
arguments: 'x'.repeat(500_000),
|
|
1740
|
-
},
|
|
1741
|
-
};
|
|
1742
|
-
let additionalKwargs = inherited;
|
|
1743
|
-
for (let i = 0; i < 101; i++) {
|
|
1744
|
-
additionalKwargs = Object.create(additionalKwargs) as typeof inherited;
|
|
1745
|
-
}
|
|
1746
|
-
const message = new AIMessage({
|
|
1747
|
-
content: '',
|
|
1748
|
-
additional_kwargs: {},
|
|
1749
|
-
});
|
|
1750
|
-
message.additional_kwargs = additionalKwargs;
|
|
1751
|
-
|
|
1752
|
-
const [projected] = projectToolCallInputs([message], 200);
|
|
1753
|
-
|
|
1754
|
-
expect(projected).not.toBe(message);
|
|
1755
|
-
expect(
|
|
1756
|
-
Object.prototype.hasOwnProperty.call(
|
|
1757
|
-
projected.additional_kwargs,
|
|
1758
|
-
'function_call'
|
|
1759
|
-
)
|
|
1760
|
-
).toBe(true);
|
|
1761
|
-
expect(projected.additional_kwargs.function_call).toBeUndefined();
|
|
1762
|
-
expect(projectToolCallInputs([projected], 200)[0]).toBe(projected);
|
|
1763
|
-
});
|
|
1764
|
-
|
|
1765
|
-
it('stabilizes an omitted invalid legacy function call', () => {
|
|
1766
|
-
const message = new AIMessage({
|
|
1767
|
-
content: '',
|
|
1768
|
-
additional_kwargs: {},
|
|
1769
|
-
});
|
|
1770
|
-
(message.additional_kwargs as Record<string, unknown>).function_call =
|
|
1771
|
-
null;
|
|
1772
|
-
|
|
1773
|
-
const [first] = projectToolCallInputs([message], 200);
|
|
1774
|
-
const [second] = projectToolCallInputs([first], 200);
|
|
1775
|
-
|
|
1776
|
-
expect(first).not.toBe(message);
|
|
1777
|
-
expect(first.additional_kwargs.function_call).toBeUndefined();
|
|
1778
|
-
expect(second).toBe(first);
|
|
1779
|
-
});
|
|
1780
|
-
|
|
1781
|
-
it('projects every provider-consumed tool-call representation to the same bounded wire args', () => {
|
|
1782
|
-
let getterCalls = 0;
|
|
1783
|
-
let toJSONCalls = 0;
|
|
1784
|
-
const adversarialArgs: Record<string, unknown> = {
|
|
1785
|
-
query: 'safe',
|
|
1786
|
-
payload: 'x'.repeat(2_000),
|
|
1787
|
-
};
|
|
1788
|
-
Object.defineProperty(adversarialArgs, 'expanded', {
|
|
1789
|
-
enumerable: true,
|
|
1790
|
-
get() {
|
|
1791
|
-
getterCalls++;
|
|
1792
|
-
return 'y'.repeat(2_000_000);
|
|
1793
|
-
},
|
|
1794
|
-
});
|
|
1795
|
-
adversarialArgs.self = adversarialArgs;
|
|
1796
|
-
adversarialArgs.toJSON = (): Record<string, string> => {
|
|
1797
|
-
toJSONCalls++;
|
|
1798
|
-
return { payload: 'z'.repeat(2_000_000) };
|
|
1799
|
-
};
|
|
1800
|
-
|
|
1801
|
-
const message = new AIMessageChunk({
|
|
1802
|
-
content: [
|
|
1803
|
-
{
|
|
1804
|
-
type: 'tool_use',
|
|
1805
|
-
id: 'inline-use',
|
|
1806
|
-
name: 'evaluate',
|
|
1807
|
-
input: { code: 'i'.repeat(2_000) },
|
|
1808
|
-
},
|
|
1809
|
-
{
|
|
1810
|
-
type: 'tool_call',
|
|
1811
|
-
id: 'inline-call',
|
|
1812
|
-
name: 'evaluate',
|
|
1813
|
-
args: { code: 'a'.repeat(2_000) },
|
|
1814
|
-
tool_call: {
|
|
1815
|
-
type: 'tool_call',
|
|
1816
|
-
id: 'nested-call',
|
|
1817
|
-
name: 'evaluate',
|
|
1818
|
-
args: { code: 'n'.repeat(2_000) },
|
|
1819
|
-
},
|
|
1820
|
-
},
|
|
1821
|
-
],
|
|
1822
|
-
tool_calls: [
|
|
1823
|
-
{
|
|
1824
|
-
id: 'shared-call',
|
|
1825
|
-
name: 'evaluate',
|
|
1826
|
-
args: adversarialArgs,
|
|
1827
|
-
},
|
|
1828
|
-
{
|
|
1829
|
-
id: 'custom-call',
|
|
1830
|
-
name: 'shell',
|
|
1831
|
-
args: { input: 'c'.repeat(2_000) },
|
|
1832
|
-
},
|
|
1833
|
-
{
|
|
1834
|
-
id: 'computer-call',
|
|
1835
|
-
name: 'computer_use',
|
|
1836
|
-
args: { action: { type: 'type', text: 't'.repeat(2_000) } },
|
|
1837
|
-
},
|
|
1838
|
-
],
|
|
1839
|
-
additional_kwargs: {
|
|
1840
|
-
tool_calls: [
|
|
1841
|
-
{
|
|
1842
|
-
id: 'shared-call',
|
|
1843
|
-
type: 'function',
|
|
1844
|
-
function: {
|
|
1845
|
-
name: 'evaluate',
|
|
1846
|
-
arguments: `{"code":"${'r'.repeat(2_000)}"}`,
|
|
1847
|
-
},
|
|
1848
|
-
},
|
|
1849
|
-
{
|
|
1850
|
-
id: 'raw-only',
|
|
1851
|
-
type: 'function',
|
|
1852
|
-
function: {
|
|
1853
|
-
name: 'evaluate',
|
|
1854
|
-
arguments: `{"code":"${'o'.repeat(2_000)}"}`,
|
|
1855
|
-
},
|
|
1856
|
-
},
|
|
1857
|
-
],
|
|
1858
|
-
},
|
|
1859
|
-
response_metadata: {
|
|
1860
|
-
output: [
|
|
1861
|
-
{
|
|
1862
|
-
type: 'function_call',
|
|
1863
|
-
id: 'fc_shared',
|
|
1864
|
-
call_id: 'shared-call',
|
|
1865
|
-
name: 'evaluate',
|
|
1866
|
-
arguments: `{"code":"${'w'.repeat(2_000)}"}`,
|
|
1867
|
-
},
|
|
1868
|
-
{
|
|
1869
|
-
type: 'function_call',
|
|
1870
|
-
id: 'fc_raw',
|
|
1871
|
-
call_id: 'response-only',
|
|
1872
|
-
name: 'evaluate',
|
|
1873
|
-
arguments: `{"code":"${'q'.repeat(2_000)}"}`,
|
|
1874
|
-
},
|
|
1875
|
-
{
|
|
1876
|
-
type: 'custom_tool_call',
|
|
1877
|
-
id: 'ctc_shared',
|
|
1878
|
-
call_id: 'custom-call',
|
|
1879
|
-
name: 'shell',
|
|
1880
|
-
input: 'c'.repeat(2_000),
|
|
1881
|
-
},
|
|
1882
|
-
{
|
|
1883
|
-
type: 'computer_call',
|
|
1884
|
-
id: 'cc_shared',
|
|
1885
|
-
call_id: 'computer-call',
|
|
1886
|
-
action: { type: 'type', text: 't'.repeat(2_000) },
|
|
1887
|
-
},
|
|
1888
|
-
],
|
|
1889
|
-
},
|
|
1890
|
-
});
|
|
1891
|
-
Object.defineProperty(message, 'projectionMarker', {
|
|
1892
|
-
configurable: false,
|
|
1893
|
-
enumerable: false,
|
|
1894
|
-
value: 'preserved',
|
|
1895
|
-
writable: false,
|
|
1896
|
-
});
|
|
1897
|
-
const messages: BaseMessage[] = [message];
|
|
1898
|
-
|
|
1899
|
-
const projected = projectToolCallInputs(messages, 200);
|
|
1900
|
-
|
|
1901
|
-
expect(projected).not.toBe(messages);
|
|
1902
|
-
expect(projected[0]).toBeInstanceOf(AIMessageChunk);
|
|
1903
|
-
expect(getterCalls).toBe(0);
|
|
1904
|
-
expect(toJSONCalls).toBe(0);
|
|
1905
|
-
expect(
|
|
1906
|
-
Object.getOwnPropertyDescriptor(projected[0], 'projectionMarker')
|
|
1907
|
-
).toEqual(Object.getOwnPropertyDescriptor(message, 'projectionMarker'));
|
|
1908
|
-
|
|
1909
|
-
const projectedMessage = projected[0] as AIMessageChunk;
|
|
1910
|
-
const content = projectedMessage.content as Array<
|
|
1911
|
-
Record<string, unknown>
|
|
1912
|
-
>;
|
|
1913
|
-
const inlineUse = content[0];
|
|
1914
|
-
const inlineCall = content[1];
|
|
1915
|
-
const nestedCall = inlineCall.tool_call as Record<string, unknown>;
|
|
1916
|
-
for (const input of [
|
|
1917
|
-
inlineUse.input,
|
|
1918
|
-
inlineCall.args,
|
|
1919
|
-
nestedCall.args,
|
|
1920
|
-
projectedMessage.tool_calls?.[0].args,
|
|
1921
|
-
]) {
|
|
1922
|
-
const serialized = serializeToolCallInput(input, 200);
|
|
1923
|
-
expect(serialized.length).toBeLessThanOrEqual(200);
|
|
1924
|
-
expect(() => JSON.parse(serialized)).not.toThrow();
|
|
1925
|
-
}
|
|
1926
|
-
|
|
1927
|
-
const canonicalArguments = serializeToolCallInput(
|
|
1928
|
-
projectedMessage.tool_calls?.[0].args,
|
|
1929
|
-
200
|
|
1930
|
-
);
|
|
1931
|
-
const rawToolCalls = projectedMessage.additional_kwargs
|
|
1932
|
-
.tool_calls as Array<{
|
|
1933
|
-
id: string;
|
|
1934
|
-
function: { arguments: string };
|
|
1935
|
-
}>;
|
|
1936
|
-
expect(rawToolCalls[0].function.arguments).toBe(canonicalArguments);
|
|
1937
|
-
expect(rawToolCalls[1].function.arguments.length).toBeLessThanOrEqual(
|
|
1938
|
-
200
|
|
1939
|
-
);
|
|
1940
|
-
expect(() =>
|
|
1941
|
-
JSON.parse(rawToolCalls[1].function.arguments)
|
|
1942
|
-
).not.toThrow();
|
|
1943
|
-
|
|
1944
|
-
const responseOutput = projectedMessage.response_metadata
|
|
1945
|
-
.output as Array<{
|
|
1946
|
-
type: string;
|
|
1947
|
-
call_id: string;
|
|
1948
|
-
arguments?: string;
|
|
1949
|
-
input?: string;
|
|
1950
|
-
action?: unknown;
|
|
1951
|
-
}>;
|
|
1952
|
-
expect(responseOutput[0].arguments).toBe(canonicalArguments);
|
|
1953
|
-
expect(responseOutput[1].arguments?.length).toBeLessThanOrEqual(200);
|
|
1954
|
-
expect(() => JSON.parse(responseOutput[1].arguments ?? '')).not.toThrow();
|
|
1955
|
-
expect(responseOutput[2].input?.length).toBeLessThanOrEqual(200);
|
|
1956
|
-
expect(
|
|
1957
|
-
serializeToolCallInput(responseOutput[3].action, 200).length
|
|
1958
|
-
).toBeLessThanOrEqual(200);
|
|
1959
|
-
|
|
1960
|
-
expect(
|
|
1961
|
-
(
|
|
1962
|
-
message.additional_kwargs.tool_calls as Array<{
|
|
1963
|
-
function: { arguments: string };
|
|
1964
|
-
}>
|
|
1965
|
-
)[0].function.arguments.length
|
|
1966
|
-
).toBeGreaterThan(2_000);
|
|
1967
|
-
expect(
|
|
1968
|
-
(
|
|
1969
|
-
message.response_metadata.output as Array<{
|
|
1970
|
-
arguments: string;
|
|
1971
|
-
}>
|
|
1972
|
-
)[0].arguments.length
|
|
1973
|
-
).toBeGreaterThan(2_000);
|
|
1974
|
-
});
|
|
1975
|
-
});
|
|
1976
|
-
|
|
1977
|
-
describe('preFlightTruncateToolCallInputs', () => {
|
|
1978
|
-
it('should truncate oversized tool_use input fields in AI messages', () => {
|
|
1979
|
-
const tokenCounter = createTestTokenCounter();
|
|
1980
|
-
const largeInput = '{"function":"' + 'x'.repeat(5000) + '"}';
|
|
1981
|
-
const messages: BaseMessage[] = [
|
|
1982
|
-
new HumanMessage('Run this script'),
|
|
1983
|
-
new AIMessage({
|
|
1984
|
-
content: [
|
|
1985
|
-
{ type: 'text', text: 'I will execute the script.' },
|
|
1986
|
-
{
|
|
1987
|
-
type: 'tool_use',
|
|
1988
|
-
id: 'tool-exec',
|
|
1989
|
-
name: 'evaluate_script',
|
|
1990
|
-
input: largeInput,
|
|
1991
|
-
},
|
|
1992
|
-
],
|
|
1993
|
-
tool_calls: [
|
|
1994
|
-
{
|
|
1995
|
-
id: 'tool-exec',
|
|
1996
|
-
name: 'evaluate_script',
|
|
1997
|
-
args: { function: 'x'.repeat(5000) },
|
|
1998
|
-
},
|
|
1999
|
-
],
|
|
2000
|
-
}),
|
|
2001
|
-
new ToolMessage({ content: 'Result: OK', tool_call_id: 'tool-exec' }),
|
|
2002
|
-
];
|
|
2003
|
-
|
|
2004
|
-
const indexTokenCountMap: Record<string, number | undefined> = {
|
|
2005
|
-
0: tokenCounter(messages[0]),
|
|
2006
|
-
1: tokenCounter(messages[1]),
|
|
2007
|
-
2: tokenCounter(messages[2]),
|
|
2008
|
-
};
|
|
2009
|
-
|
|
2010
|
-
const originalTokens = indexTokenCountMap[1] as number;
|
|
2011
|
-
expect(originalTokens).toBeGreaterThan(5000); // Large input counted
|
|
2012
|
-
|
|
2013
|
-
// maxContextTokens: 1000 → maxInputChars = floor(1000 * 0.15) * 4 = 600
|
|
2014
|
-
const truncated = preFlightTruncateToolCallInputs({
|
|
2015
|
-
messages,
|
|
2016
|
-
maxContextTokens: 1000,
|
|
2017
|
-
indexTokenCountMap,
|
|
2018
|
-
tokenCounter,
|
|
2019
|
-
});
|
|
2020
|
-
|
|
2021
|
-
expect(truncated).toBe(1);
|
|
2022
|
-
const newTokens = indexTokenCountMap[1] as number;
|
|
2023
|
-
expect(newTokens).toBeLessThan(originalTokens);
|
|
2024
|
-
|
|
2025
|
-
// Verify the content block was truncated
|
|
2026
|
-
const aiMsg = messages[1] as AIMessage;
|
|
2027
|
-
const toolUseBlock = (
|
|
2028
|
-
aiMsg.content as Array<Record<string, unknown>>
|
|
2029
|
-
).find((b) => b.type === 'tool_use');
|
|
2030
|
-
expect(toolUseBlock).toBeDefined();
|
|
2031
|
-
const truncatedInput = toolUseBlock!.input as {
|
|
2032
|
-
_truncated: string;
|
|
2033
|
-
_originalChars: number;
|
|
2034
|
-
};
|
|
2035
|
-
expect(truncatedInput._truncated).toContain('truncated');
|
|
2036
|
-
expect(truncatedInput._originalChars).toBeGreaterThan(600);
|
|
2037
|
-
|
|
2038
|
-
// Verify tool_calls args were also truncated
|
|
2039
|
-
expect(aiMsg.tool_calls).toBeDefined();
|
|
2040
|
-
const tc = aiMsg.tool_calls![0];
|
|
2041
|
-
expect(tc.args).toHaveProperty('_truncated');
|
|
2042
|
-
});
|
|
2043
|
-
|
|
2044
|
-
it('truncates tool_calls-only args and preserves AI message metadata', () => {
|
|
2045
|
-
let recounts = 0;
|
|
2046
|
-
const tokenCounter: t.TokenCounter = () => {
|
|
2047
|
-
recounts++;
|
|
2048
|
-
return 17;
|
|
2049
|
-
};
|
|
2050
|
-
const messages: BaseMessage[] = [
|
|
2051
|
-
new AIMessage({
|
|
2052
|
-
content: '',
|
|
2053
|
-
id: 'ai-tool-call-only',
|
|
2054
|
-
name: 'assistant',
|
|
2055
|
-
additional_kwargs: { trace_marker: 'keep' },
|
|
2056
|
-
response_metadata: { model: 'test-model' },
|
|
2057
|
-
tool_calls: [
|
|
2058
|
-
{
|
|
2059
|
-
id: 'tool-exec',
|
|
2060
|
-
name: 'evaluate_script',
|
|
2061
|
-
args: { code: 'x'.repeat(5_000) },
|
|
2062
|
-
},
|
|
2063
|
-
],
|
|
2064
|
-
}),
|
|
2065
|
-
];
|
|
2066
|
-
const indexTokenCountMap: Record<string, number | undefined> = { 0: 1 };
|
|
2067
|
-
|
|
2068
|
-
const truncated = preFlightTruncateToolCallInputs({
|
|
2069
|
-
messages,
|
|
2070
|
-
maxContextTokens: 1_000,
|
|
2071
|
-
indexTokenCountMap,
|
|
2072
|
-
tokenCounter,
|
|
2073
|
-
});
|
|
2074
|
-
|
|
2075
|
-
expect(truncated).toBe(1);
|
|
2076
|
-
expect(recounts).toBe(1);
|
|
2077
|
-
expect(indexTokenCountMap[0]).toBe(17);
|
|
2078
|
-
const projected = messages[0] as AIMessage;
|
|
2079
|
-
expect(projected.content).toBe('');
|
|
2080
|
-
expect(projected.id).toBe('ai-tool-call-only');
|
|
2081
|
-
expect(projected.name).toBe('assistant');
|
|
2082
|
-
expect(projected.additional_kwargs).toEqual({ trace_marker: 'keep' });
|
|
2083
|
-
expect(projected.response_metadata).toEqual({ model: 'test-model' });
|
|
2084
|
-
expect(projected.tool_calls?.[0].args).toMatchObject({
|
|
2085
|
-
_truncated: expect.stringContaining('truncated'),
|
|
2086
|
-
_originalChars: expect.any(Number),
|
|
2087
|
-
});
|
|
2088
|
-
});
|
|
2089
|
-
|
|
2090
|
-
it('neutralizes adversarial tool-call args without invoking toJSON', () => {
|
|
2091
|
-
let toJSONCalls = 0;
|
|
2092
|
-
const messages: BaseMessage[] = [
|
|
2093
|
-
new AIMessage({
|
|
2094
|
-
content: '',
|
|
2095
|
-
tool_calls: [
|
|
2096
|
-
{
|
|
2097
|
-
id: 'tool-adversarial',
|
|
2098
|
-
name: 'lookup',
|
|
2099
|
-
args: {
|
|
2100
|
-
query: 'safe',
|
|
2101
|
-
toJSON() {
|
|
2102
|
-
toJSONCalls++;
|
|
2103
|
-
return { query: 'x'.repeat(2_000_000) };
|
|
2104
|
-
},
|
|
2105
|
-
},
|
|
2106
|
-
},
|
|
2107
|
-
],
|
|
2108
|
-
}),
|
|
2109
|
-
];
|
|
2110
|
-
const indexTokenCountMap: Record<string, number | undefined> = { 0: 1 };
|
|
2111
|
-
|
|
2112
|
-
const changed = preFlightTruncateToolCallInputs({
|
|
2113
|
-
messages,
|
|
2114
|
-
maxContextTokens: 1_000,
|
|
2115
|
-
indexTokenCountMap,
|
|
2116
|
-
tokenCounter: () => 5,
|
|
2117
|
-
});
|
|
2118
|
-
|
|
2119
|
-
expect(changed).toBe(1);
|
|
2120
|
-
expect(toJSONCalls).toBe(0);
|
|
2121
|
-
expect((messages[0] as AIMessage).tool_calls?.[0].args).toEqual({
|
|
2122
|
-
query: 'safe',
|
|
2123
|
-
});
|
|
2124
|
-
});
|
|
2125
|
-
|
|
2126
|
-
it('should not truncate inputs that fit within the budget', () => {
|
|
2127
|
-
const tokenCounter = createTestTokenCounter();
|
|
2128
|
-
const messages: BaseMessage[] = [
|
|
2129
|
-
new HumanMessage('Read a file'),
|
|
2130
|
-
new AIMessage({
|
|
2131
|
-
content: [
|
|
2132
|
-
{ type: 'text', text: 'Reading file.' },
|
|
2133
|
-
{
|
|
2134
|
-
type: 'tool_use',
|
|
2135
|
-
id: 'tool-read',
|
|
2136
|
-
name: 'read_file',
|
|
2137
|
-
input: '{"path":"test.txt"}',
|
|
2138
|
-
},
|
|
2139
|
-
],
|
|
2140
|
-
tool_calls: [
|
|
2141
|
-
{ id: 'tool-read', name: 'read_file', args: { path: 'test.txt' } },
|
|
2142
|
-
],
|
|
2143
|
-
}),
|
|
2144
|
-
];
|
|
2145
|
-
|
|
2146
|
-
const indexTokenCountMap: Record<string, number | undefined> = {
|
|
2147
|
-
0: tokenCounter(messages[0]),
|
|
2148
|
-
1: tokenCounter(messages[1]),
|
|
2149
|
-
};
|
|
2150
|
-
|
|
2151
|
-
const originalTokens = indexTokenCountMap[1];
|
|
2152
|
-
|
|
2153
|
-
const truncated = preFlightTruncateToolCallInputs({
|
|
2154
|
-
messages,
|
|
2155
|
-
maxContextTokens: 1000,
|
|
2156
|
-
indexTokenCountMap,
|
|
2157
|
-
tokenCounter,
|
|
2158
|
-
});
|
|
2159
|
-
|
|
2160
|
-
expect(truncated).toBe(0);
|
|
2161
|
-
expect(indexTokenCountMap[1]).toBe(originalTokens);
|
|
2162
|
-
});
|
|
2163
|
-
|
|
2164
|
-
it('should skip non-AI messages', () => {
|
|
2165
|
-
const tokenCounter = createTestTokenCounter();
|
|
2166
|
-
const messages: BaseMessage[] = [
|
|
2167
|
-
new HumanMessage('Hello'),
|
|
2168
|
-
new ToolMessage({ content: 'x'.repeat(5000), tool_call_id: 'tool-1' }),
|
|
2169
|
-
];
|
|
2170
|
-
|
|
2171
|
-
const indexTokenCountMap: Record<string, number | undefined> = {
|
|
2172
|
-
0: tokenCounter(messages[0]),
|
|
2173
|
-
1: tokenCounter(messages[1]),
|
|
2174
|
-
};
|
|
2175
|
-
|
|
2176
|
-
const truncated = preFlightTruncateToolCallInputs({
|
|
2177
|
-
messages,
|
|
2178
|
-
maxContextTokens: 1000,
|
|
2179
|
-
indexTokenCountMap,
|
|
2180
|
-
tokenCounter,
|
|
2181
|
-
});
|
|
2182
|
-
|
|
2183
|
-
// Should not touch ToolMessages (that's preFlightTruncateToolResults' job)
|
|
2184
|
-
expect(truncated).toBe(0);
|
|
2185
|
-
});
|
|
2186
|
-
});
|
|
2187
|
-
|
|
2188
|
-
describe('Instruction token budget reservation (getInstructionTokens)', () => {
|
|
2189
|
-
it('should reserve budget for instruction tokens when no system message is present', () => {
|
|
2190
|
-
const tokenCounter = createTestTokenCounter();
|
|
2191
|
-
// Agent flow: messages do NOT include a system message.
|
|
2192
|
-
// The system message is prepended later by buildSystemRunnable.
|
|
2193
|
-
const messages = [
|
|
2194
|
-
new HumanMessage('Hello there'), // 11 chars
|
|
2195
|
-
new AIMessage('Hi'), // 2 chars
|
|
2196
|
-
new HumanMessage('How are you?'), // 12 chars
|
|
2197
|
-
new AIMessage('Good'), // 4 chars
|
|
2198
|
-
];
|
|
2199
|
-
|
|
2200
|
-
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
2201
|
-
for (let i = 0; i < messages.length; i++) {
|
|
2202
|
-
indexTokenCountMap[i] = tokenCounter(messages[i]);
|
|
2203
|
-
}
|
|
2204
|
-
|
|
2205
|
-
// Total message tokens: 11 + 2 + 12 + 4 = 29
|
|
2206
|
-
// Instruction tokens: 20 (simulating system prompt overhead)
|
|
2207
|
-
// Effective budget: 52 - 20 - 3 reply primer = 29 → exact fit
|
|
2208
|
-
const pruneMessages = createPruneMessages({
|
|
2209
|
-
maxTokens: 52,
|
|
2210
|
-
startIndex: 0,
|
|
2211
|
-
tokenCounter,
|
|
2212
|
-
indexTokenCountMap,
|
|
2213
|
-
reserveRatio: 0,
|
|
2214
|
-
getInstructionTokens: () => 20,
|
|
2215
|
-
});
|
|
2216
|
-
|
|
2217
|
-
const result = pruneMessages({ messages });
|
|
2218
|
-
|
|
2219
|
-
// All messages fit exactly with instructions and reply framing.
|
|
2220
|
-
expect(result.context.length).toBe(4);
|
|
2221
|
-
expect(result.context).toEqual(messages);
|
|
2222
|
-
expect(result.messagesToRefine).toEqual([]);
|
|
2223
|
-
});
|
|
2224
|
-
|
|
2225
|
-
it('should prune when messages + instruction tokens exceed budget', () => {
|
|
2226
|
-
const tokenCounter = createTestTokenCounter();
|
|
2227
|
-
const messages = [
|
|
2228
|
-
new HumanMessage('Hello there'), // 11 chars
|
|
2229
|
-
new AIMessage('Hi'), // 2 chars
|
|
2230
|
-
new HumanMessage('How are you?'), // 12 chars
|
|
2231
|
-
new AIMessage('Good'), // 4 chars
|
|
2232
|
-
];
|
|
2233
|
-
|
|
2234
|
-
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
2235
|
-
for (let i = 0; i < messages.length; i++) {
|
|
2236
|
-
indexTokenCountMap[i] = tokenCounter(messages[i]);
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
// Total message tokens: 29
|
|
2240
|
-
// Instruction tokens: 25 (simulating large tool schema overhead)
|
|
2241
|
-
// Effective budget: 40 - 25 = 15 → must prune older messages
|
|
2242
|
-
const pruneMessages = createPruneMessages({
|
|
2243
|
-
maxTokens: 40,
|
|
2244
|
-
startIndex: 0,
|
|
2245
|
-
tokenCounter,
|
|
2246
|
-
indexTokenCountMap,
|
|
2247
|
-
getInstructionTokens: () => 25,
|
|
2248
|
-
});
|
|
2249
|
-
|
|
2250
|
-
const result = pruneMessages({ messages });
|
|
2251
|
-
|
|
2252
|
-
// Should prune older messages to fit within 15 available tokens.
|
|
2253
|
-
// Working backwards: "Good" (4) + "How are you?" (12) = 16 > 15
|
|
2254
|
-
// So only "Good" (4) fits, context starts on that AI message.
|
|
2255
|
-
// But startType may require a human message...
|
|
2256
|
-
// Actually with no startType and 3 tokens of overhead,
|
|
2257
|
-
// available = 15 - 3 = 12: "Good" (4) fits, "How are you?" (12) → 4+12=16 > 12
|
|
2258
|
-
// So only "Good" (4) fits.
|
|
2259
|
-
expect(result.context.length).toBeLessThan(4);
|
|
2260
|
-
expect(Array.isArray(result.messagesToRefine)).toBe(true);
|
|
2261
|
-
expect(result.messagesToRefine!.length).toBeGreaterThan(0);
|
|
2262
|
-
});
|
|
2263
|
-
|
|
2264
|
-
it('should correctly account for instruction tokens in early-return path', () => {
|
|
2265
|
-
const tokenCounter = createTestTokenCounter();
|
|
2266
|
-
const messages = [
|
|
2267
|
-
new HumanMessage('Hi'), // 2 chars
|
|
2268
|
-
new AIMessage('Hello'), // 5 chars
|
|
2269
|
-
];
|
|
2270
|
-
|
|
2271
|
-
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
2272
|
-
for (let i = 0; i < messages.length; i++) {
|
|
2273
|
-
indexTokenCountMap[i] = tokenCounter(messages[i]);
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
|
-
// Message tokens: 7
|
|
2277
|
-
// Instruction tokens: 100 (simulating 26 MCP tools ~5000 chars)
|
|
2278
|
-
// Budget: 50 → 7 + 100 = 107 > 50, so early-return should NOT fire
|
|
2279
|
-
const pruneMessages = createPruneMessages({
|
|
2280
|
-
maxTokens: 50,
|
|
2281
|
-
startIndex: 0,
|
|
2282
|
-
tokenCounter,
|
|
2283
|
-
indexTokenCountMap,
|
|
2284
|
-
getInstructionTokens: () => 100,
|
|
2285
|
-
});
|
|
2286
|
-
|
|
2287
|
-
const result = pruneMessages({ messages });
|
|
2288
|
-
|
|
2289
|
-
// Even though messages alone (7) fit in 50, the instruction overhead (100)
|
|
2290
|
-
// means pruning must occur. With only 50 - 100 = -50 effective budget,
|
|
2291
|
-
// nothing fits → all messages pruned.
|
|
2292
|
-
expect(result.messagesToRefine!.length).toBeGreaterThan(0);
|
|
2293
|
-
});
|
|
2294
|
-
|
|
2295
|
-
it('should not double-subtract when messages include a system message', () => {
|
|
2296
|
-
const tokenCounter = createTestTokenCounter();
|
|
2297
|
-
const messages = [
|
|
2298
|
-
new SystemMessage('System'), // 6 chars
|
|
2299
|
-
new HumanMessage('Hello there'), // 11 chars
|
|
2300
|
-
new AIMessage('Hi'), // 2 chars
|
|
2301
|
-
];
|
|
2302
|
-
|
|
2303
|
-
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
2304
|
-
for (let i = 0; i < messages.length; i++) {
|
|
2305
|
-
indexTokenCountMap[i] = tokenCounter(messages[i]);
|
|
2306
|
-
}
|
|
2307
|
-
|
|
2308
|
-
// When messages[0] IS a system message, getMessagesWithinTokenLimit uses
|
|
2309
|
-
// indexTokenCountMap[0] (6) to subtract from budget, ignoring instructionTokens.
|
|
2310
|
-
// getInstructionTokens is only used when no system message is at index 0.
|
|
2311
|
-
const pruneMessages = createPruneMessages({
|
|
2312
|
-
maxTokens: 30,
|
|
2313
|
-
startIndex: 0,
|
|
2314
|
-
tokenCounter,
|
|
2315
|
-
indexTokenCountMap,
|
|
2316
|
-
getInstructionTokens: () => 999, // Should be ignored for system message path
|
|
2317
|
-
});
|
|
2318
|
-
|
|
2319
|
-
const result = pruneMessages({ messages });
|
|
2320
|
-
|
|
2321
|
-
// Budget: 30 - 6 (system) = 24 available.
|
|
2322
|
-
// "Hi" (2) + "Hello there" (11) + 3 overhead = 16, fits in 24.
|
|
2323
|
-
// All messages should be kept.
|
|
2324
|
-
expect(result.context.length).toBe(3);
|
|
2325
|
-
expect(result.context[0]).toBe(messages[0]); // System message preserved
|
|
2326
|
-
});
|
|
2327
|
-
|
|
2328
|
-
it('index 0 should NOT be inflated when getInstructionTokens is provided', () => {
|
|
2329
|
-
const tokenCounter = createTestTokenCounter();
|
|
2330
|
-
const messages = [
|
|
2331
|
-
new HumanMessage('Hello there'), // 11 chars
|
|
2332
|
-
new AIMessage('Hi'), // 2 chars
|
|
2333
|
-
];
|
|
2334
|
-
|
|
2335
|
-
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
2336
|
-
for (let i = 0; i < messages.length; i++) {
|
|
2337
|
-
indexTokenCountMap[i] = tokenCounter(messages[i]);
|
|
2338
|
-
}
|
|
2339
|
-
|
|
2340
|
-
const pruneMessages = createPruneMessages({
|
|
2341
|
-
maxTokens: 50,
|
|
2342
|
-
startIndex: 0,
|
|
2343
|
-
tokenCounter,
|
|
2344
|
-
indexTokenCountMap,
|
|
2345
|
-
getInstructionTokens: () => 10,
|
|
2346
|
-
});
|
|
2347
|
-
|
|
2348
|
-
// Before and after pruning, index 0 should remain 11 (the real token count)
|
|
2349
|
-
expect(indexTokenCountMap[0]).toBe(11);
|
|
2350
|
-
pruneMessages({ messages });
|
|
2351
|
-
// The returned indexTokenCountMap should still have the real count at index 0
|
|
2352
|
-
expect(indexTokenCountMap[0]).toBe(11);
|
|
2353
|
-
});
|
|
2354
|
-
|
|
2355
|
-
it('pre-flight truncation uses effective budget after instruction overhead', () => {
|
|
2356
|
-
const tokenCounter = createTestTokenCounter();
|
|
2357
|
-
// Simulate the real scenario: AI message has a massive tool_call input
|
|
2358
|
-
// (like the chrome-devtools evaluate_script with a 7000-char JS payload)
|
|
2359
|
-
const hugeInput = 'x'.repeat(7000);
|
|
2360
|
-
const messages = [
|
|
2361
|
-
new HumanMessage('show me something'), // 17 chars
|
|
2362
|
-
new AIMessage({
|
|
2363
|
-
content: [
|
|
2364
|
-
{ type: 'text', text: 'Creating animation' },
|
|
2365
|
-
{
|
|
2366
|
-
type: 'tool_use',
|
|
2367
|
-
id: 'tool_1',
|
|
2368
|
-
name: 'evaluate_script',
|
|
2369
|
-
input: { function: hugeInput },
|
|
2370
|
-
},
|
|
2371
|
-
],
|
|
2372
|
-
tool_calls: [
|
|
2373
|
-
{
|
|
2374
|
-
id: 'tool_1',
|
|
2375
|
-
name: 'evaluate_script',
|
|
2376
|
-
args: { function: hugeInput },
|
|
2377
|
-
type: 'tool_call' as const,
|
|
2378
|
-
},
|
|
2379
|
-
],
|
|
2380
|
-
}),
|
|
2381
|
-
new ToolMessage({ content: 'Script executed', tool_call_id: 'tool_1' }),
|
|
2382
|
-
];
|
|
2383
|
-
|
|
2384
|
-
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
2385
|
-
for (let i = 0; i < messages.length; i++) {
|
|
2386
|
-
indexTokenCountMap[i] = tokenCounter(messages[i]);
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2389
|
-
// Pre-flight truncation uses maxTokens for the truncation threshold:
|
|
2390
|
-
// Math.floor(8000*0.15)*4 = 4800 chars. The AI message's tool_use
|
|
2391
|
-
// input (~7015 chars) shrinks to ~4800 chars, giving an AI token
|
|
2392
|
-
// count of ~4850.
|
|
2393
|
-
//
|
|
2394
|
-
// The effective pruning budget subtracts instruction overhead:
|
|
2395
|
-
// effectiveMax = 8000 - 2000 = 6000, which is enough for all three
|
|
2396
|
-
// messages (~4850 + 17 + 15 ≈ 4882).
|
|
2397
|
-
const instructionTokens = 2000;
|
|
2398
|
-
const pruneMessages = createPruneMessages({
|
|
2399
|
-
maxTokens: 8000,
|
|
2400
|
-
startIndex: 0,
|
|
2401
|
-
tokenCounter,
|
|
2402
|
-
indexTokenCountMap,
|
|
2403
|
-
getInstructionTokens: () => instructionTokens,
|
|
2404
|
-
reserveRatio: 0,
|
|
2405
|
-
});
|
|
2406
|
-
|
|
2407
|
-
const result = pruneMessages({ messages });
|
|
2408
|
-
|
|
2409
|
-
// The AI message should survive pruning (not be in messagesToRefine)
|
|
2410
|
-
// because pre-flight truncation used the effective budget
|
|
2411
|
-
const aiMessagesInContext = result.context.filter(
|
|
2412
|
-
(m) => m.getType() === 'ai'
|
|
2413
|
-
);
|
|
2414
|
-
expect(aiMessagesInContext.length).toBe(1);
|
|
2415
|
-
expect(result.context.length).toBe(3); // All 3 messages fit after truncation
|
|
2416
|
-
});
|
|
2417
|
-
|
|
2418
|
-
it('emergency truncation recovers when initial prune produces empty context', () => {
|
|
2419
|
-
const tokenCounter = createTestTokenCounter();
|
|
2420
|
-
// Simulate post-summarization state: only 4 messages remain, but one
|
|
2421
|
-
// has a huge tool_call input that exceeds available budget alone.
|
|
2422
|
-
// With char-based counter, the AI message with 4000-char input is ~4000 tokens.
|
|
2423
|
-
// Available budget: 5000 - 4500 = 500. Nothing fits on first pass.
|
|
2424
|
-
const hugeInput = 'x'.repeat(4000);
|
|
2425
|
-
const messages = [
|
|
2426
|
-
new AIMessage({
|
|
2427
|
-
content: [
|
|
2428
|
-
{ type: 'text', text: 'Running script' },
|
|
2429
|
-
{
|
|
2430
|
-
type: 'tool_use',
|
|
2431
|
-
id: 'tool_1',
|
|
2432
|
-
name: 'evaluate_script',
|
|
2433
|
-
input: { function: hugeInput },
|
|
2434
|
-
},
|
|
2435
|
-
],
|
|
2436
|
-
tool_calls: [
|
|
2437
|
-
{
|
|
2438
|
-
id: 'tool_1',
|
|
2439
|
-
name: 'evaluate_script',
|
|
2440
|
-
args: { function: hugeInput },
|
|
2441
|
-
type: 'tool_call' as const,
|
|
2442
|
-
},
|
|
2443
|
-
],
|
|
2444
|
-
}),
|
|
2445
|
-
new ToolMessage({
|
|
2446
|
-
content: 'Script ran successfully',
|
|
2447
|
-
tool_call_id: 'tool_1',
|
|
2448
|
-
}),
|
|
2449
|
-
new HumanMessage('that looks great'),
|
|
2450
|
-
new AIMessage('Thanks! Want more?'),
|
|
2451
|
-
];
|
|
2452
|
-
|
|
2453
|
-
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
2454
|
-
for (let i = 0; i < messages.length; i++) {
|
|
2455
|
-
indexTokenCountMap[i] = tokenCounter(messages[i]);
|
|
2456
|
-
}
|
|
2457
|
-
|
|
2458
|
-
// Available budget is extremely tight: 500 tokens for messages.
|
|
2459
|
-
// The AI message alone is ~4000+ tokens. Initial prune: nothing fits.
|
|
2460
|
-
// Emergency truncation should reduce tool inputs to 150 chars,
|
|
2461
|
-
// making the AI message fit.
|
|
2462
|
-
const pruneMessages = createPruneMessages({
|
|
2463
|
-
maxTokens: 5000,
|
|
2464
|
-
startIndex: 0,
|
|
2465
|
-
tokenCounter,
|
|
2466
|
-
indexTokenCountMap,
|
|
2467
|
-
getInstructionTokens: () => 4500,
|
|
2468
|
-
});
|
|
2469
|
-
|
|
2470
|
-
const result = pruneMessages({ messages });
|
|
2471
|
-
|
|
2472
|
-
// Emergency truncation should have recovered — context is NOT empty
|
|
2473
|
-
expect(result.context.length).toBeGreaterThan(0);
|
|
2474
|
-
// At minimum, the newest messages should be present
|
|
2475
|
-
const types = result.context.map((m) => m.getType());
|
|
2476
|
-
expect(types).toContain('human');
|
|
2477
|
-
});
|
|
2478
|
-
|
|
2479
|
-
it('emergency-truncates tool_calls-only args when content is empty', () => {
|
|
2480
|
-
const tokenCounter: t.TokenCounter = (message) => {
|
|
2481
|
-
let chars =
|
|
2482
|
-
typeof message.content === 'string' ? message.content.length : 0;
|
|
2483
|
-
if (message.getType() === 'ai') {
|
|
2484
|
-
for (const toolCall of (message as AIMessage).tool_calls ?? []) {
|
|
2485
|
-
chars +=
|
|
2486
|
-
toolCall.name.length + JSON.stringify(toolCall.args).length;
|
|
2487
|
-
}
|
|
2488
|
-
}
|
|
2489
|
-
return Math.max(1, Math.ceil(chars / 4));
|
|
2490
|
-
};
|
|
2491
|
-
const messages: BaseMessage[] = [
|
|
2492
|
-
new AIMessage({
|
|
2493
|
-
content: '',
|
|
2494
|
-
tool_calls: [
|
|
2495
|
-
{
|
|
2496
|
-
id: 'tool-call-only',
|
|
2497
|
-
name: 'evaluate_script',
|
|
2498
|
-
args: { code: 'x'.repeat(4_000) },
|
|
2499
|
-
},
|
|
2500
|
-
],
|
|
2501
|
-
}),
|
|
2502
|
-
new ToolMessage({
|
|
2503
|
-
content: 'OK',
|
|
2504
|
-
tool_call_id: 'tool-call-only',
|
|
2505
|
-
name: 'evaluate_script',
|
|
2506
|
-
}),
|
|
2507
|
-
];
|
|
2508
|
-
const indexTokenCountMap: Record<string, number | undefined> = {
|
|
2509
|
-
0: tokenCounter(messages[0]),
|
|
2510
|
-
1: tokenCounter(messages[1]),
|
|
2511
|
-
};
|
|
2512
|
-
const pruneMessages = createPruneMessages({
|
|
2513
|
-
maxTokens: 1_000,
|
|
2514
|
-
startIndex: 0,
|
|
2515
|
-
tokenCounter,
|
|
2516
|
-
indexTokenCountMap,
|
|
2517
|
-
getInstructionTokens: () => 900,
|
|
2518
|
-
reserveRatio: 0,
|
|
2519
|
-
});
|
|
2520
|
-
|
|
2521
|
-
const result = pruneMessages({ messages });
|
|
2522
|
-
|
|
2523
|
-
const projectedAI = result.context.find(
|
|
2524
|
-
(message) => message.getType() === 'ai'
|
|
2525
|
-
) as AIMessage | undefined;
|
|
2526
|
-
expect(projectedAI).toBeDefined();
|
|
2527
|
-
expect(projectedAI?.content).toBe('');
|
|
2528
|
-
const projectedArgs = projectedAI?.tool_calls?.[0].args as
|
|
2529
|
-
| { _truncated?: string; _originalChars?: number }
|
|
2530
|
-
| undefined;
|
|
2531
|
-
expect(projectedArgs?._truncated).toContain('truncated');
|
|
2532
|
-
expect(projectedArgs?._truncated?.length).toBeLessThanOrEqual(200);
|
|
2533
|
-
expect(projectedArgs?._originalChars).toBeGreaterThan(200);
|
|
2534
|
-
});
|
|
2535
|
-
});
|
|
2536
|
-
|
|
2537
|
-
describe('Empty messages guard', () => {
|
|
2538
|
-
it('returns empty context without crashing when messages array is empty', () => {
|
|
2539
|
-
const tokenCounter = createTestTokenCounter();
|
|
2540
|
-
const pruneMessages = createPruneMessages({
|
|
2541
|
-
maxTokens: 8000,
|
|
2542
|
-
startIndex: 0,
|
|
2543
|
-
tokenCounter,
|
|
2544
|
-
indexTokenCountMap: {},
|
|
2545
|
-
getInstructionTokens: () => 4000,
|
|
2546
|
-
});
|
|
2547
|
-
|
|
2548
|
-
// Simulate post-summarization state where REMOVE_ALL left an empty messages array
|
|
2549
|
-
const result = pruneMessages({
|
|
2550
|
-
messages: [],
|
|
2551
|
-
usageMetadata: {
|
|
2552
|
-
input_tokens: 100,
|
|
2553
|
-
output_tokens: 50,
|
|
2554
|
-
total_tokens: 150,
|
|
2555
|
-
} as UsageMetadata,
|
|
2556
|
-
});
|
|
2557
|
-
|
|
2558
|
-
expect(result.context).toEqual([]);
|
|
2559
|
-
expect(result.messagesToRefine).toEqual([]);
|
|
2560
|
-
expect(result.prePruneContextTokens).toBe(0);
|
|
2561
|
-
/** Reserve-adjusted budget minus instructions and reply primer */
|
|
2562
|
-
expect(result.contextBudget).toBe(7600);
|
|
2563
|
-
expect(result.effectiveInstructionTokens).toBe(4000);
|
|
2564
|
-
expect(result.remainingContextTokens).toBe(3597);
|
|
2565
|
-
});
|
|
2566
|
-
});
|
|
2567
|
-
|
|
2568
|
-
describe('Dropped orphan ToolMessages appear in messagesToRefine', () => {
|
|
2569
|
-
it('appends orphan ToolMessage (whose parent AI was pruned) to messagesToRefine for summarization', () => {
|
|
2570
|
-
const tokenCounter = createTestTokenCounter();
|
|
2571
|
-
|
|
2572
|
-
// Build messages where the large AI(evaluate) won't fit in a tight budget,
|
|
2573
|
-
// but its smaller ToolMessage(evaluate) does. After backward iteration,
|
|
2574
|
-
// the ToolMessage lands in context while its parent AI is in prunedMemory.
|
|
2575
|
-
// repairOrphanedToolMessages then drops the orphan ToolMessage from context.
|
|
2576
|
-
// The fix: that dropped ToolMessage must appear in messagesToRefine so
|
|
2577
|
-
// summarization sees the tool result (otherwise summary says "in progress").
|
|
2578
|
-
const messages: BaseMessage[] = [
|
|
2579
|
-
new HumanMessage('Build me a solar system simulation'),
|
|
2580
|
-
new AIMessage({
|
|
2581
|
-
content: [
|
|
2582
|
-
{ type: 'text', text: 'I will write the code now.' },
|
|
2583
|
-
{
|
|
2584
|
-
type: 'tool_use',
|
|
2585
|
-
id: 'tc_eval',
|
|
2586
|
-
name: 'evaluate_script',
|
|
2587
|
-
// Large input that consumes most of the budget
|
|
2588
|
-
input: { code: 'x'.repeat(3000) },
|
|
2589
|
-
},
|
|
2590
|
-
],
|
|
2591
|
-
tool_calls: [
|
|
2592
|
-
{
|
|
2593
|
-
id: 'tc_eval',
|
|
2594
|
-
name: 'evaluate_script',
|
|
2595
|
-
args: { code: 'x'.repeat(3000) },
|
|
2596
|
-
type: 'tool_call' as const,
|
|
2597
|
-
},
|
|
2598
|
-
],
|
|
2599
|
-
}),
|
|
2600
|
-
new ToolMessage({
|
|
2601
|
-
// Small result — fits in budget individually
|
|
2602
|
-
content: 'Solar system simulation launched successfully!',
|
|
2603
|
-
tool_call_id: 'tc_eval',
|
|
2604
|
-
name: 'evaluate_script',
|
|
2605
|
-
}),
|
|
2606
|
-
];
|
|
2607
|
-
|
|
2608
|
-
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
2609
|
-
for (let i = 0; i < messages.length; i++) {
|
|
2610
|
-
indexTokenCountMap[i] = tokenCounter(messages[i]);
|
|
2611
|
-
}
|
|
2612
|
-
|
|
2613
|
-
// Budget is tight enough that the large AI message won't fit
|
|
2614
|
-
// even after emergency truncation, but HumanMessage and ToolMessage
|
|
2615
|
-
// individually can. Budget must be low enough that proportional
|
|
2616
|
-
// emergency truncation (budget / messages * 4 chars) still leaves
|
|
2617
|
-
// the AI message too large to fit.
|
|
2618
|
-
const pruneMessages = createPruneMessages({
|
|
2619
|
-
maxTokens: 100,
|
|
2620
|
-
startIndex: 0,
|
|
2621
|
-
tokenCounter,
|
|
2622
|
-
indexTokenCountMap,
|
|
2623
|
-
getInstructionTokens: () => 0,
|
|
2624
|
-
});
|
|
2625
|
-
|
|
2626
|
-
const result = pruneMessages({ messages });
|
|
2627
|
-
|
|
2628
|
-
// The orphan ToolMessage(evaluate) should NOT be in context
|
|
2629
|
-
// (its parent AI was pruned away)
|
|
2630
|
-
const contextToolMsgs = result.context.filter(
|
|
2631
|
-
(m) => m.getType() === 'tool'
|
|
2632
|
-
);
|
|
2633
|
-
const orphanInContext = contextToolMsgs.some(
|
|
2634
|
-
(m) => (m as ToolMessage).tool_call_id === 'tc_eval'
|
|
2635
|
-
);
|
|
2636
|
-
expect(orphanInContext).toBe(false);
|
|
2637
|
-
|
|
2638
|
-
// The key assertion: the dropped ToolMessage MUST appear in messagesToRefine
|
|
2639
|
-
// so that summarization can see "Solar system simulation launched successfully!"
|
|
2640
|
-
expect(result.messagesToRefine).toBeDefined();
|
|
2641
|
-
const refineToolMsgs = result.messagesToRefine!.filter(
|
|
2642
|
-
(m) => m.getType() === 'tool'
|
|
2643
|
-
);
|
|
2644
|
-
const toolInRefine = refineToolMsgs.some(
|
|
2645
|
-
(m) => (m as ToolMessage).tool_call_id === 'tc_eval'
|
|
2646
|
-
);
|
|
2647
|
-
expect(toolInRefine).toBe(true);
|
|
2648
|
-
|
|
2649
|
-
// The parent AI message should also be in messagesToRefine (from prunedMemory)
|
|
2650
|
-
const refineAiMsgs = result.messagesToRefine!.filter(
|
|
2651
|
-
(m) => m.getType() === 'ai'
|
|
2652
|
-
);
|
|
2653
|
-
const aiInRefine = refineAiMsgs.some((m) =>
|
|
2654
|
-
((m as AIMessage).tool_calls ?? []).some((tc) => tc.id === 'tc_eval')
|
|
2655
|
-
);
|
|
2656
|
-
expect(aiInRefine).toBe(true);
|
|
2657
|
-
});
|
|
2658
|
-
});
|
|
2659
|
-
|
|
2660
|
-
describe('Integration with Run', () => {
|
|
2661
|
-
it('should initialize Run with custom token counter and process messages', async () => {
|
|
2662
|
-
const provider = Providers.OPENAI;
|
|
2663
|
-
const llmConfig = getLLMConfig(provider);
|
|
2664
|
-
const tokenCounter = createTestTokenCounter();
|
|
2665
|
-
|
|
2666
|
-
const run = await Run.create<t.IState>({
|
|
2667
|
-
runId: 'test-prune-run',
|
|
2668
|
-
graphConfig: {
|
|
2669
|
-
type: 'standard',
|
|
2670
|
-
llmConfig,
|
|
2671
|
-
instructions: 'You are a helpful assistant.',
|
|
2672
|
-
maxContextTokens: 1000,
|
|
2673
|
-
},
|
|
2674
|
-
returnContent: true,
|
|
2675
|
-
skipCleanup: true,
|
|
2676
|
-
tokenCounter,
|
|
2677
|
-
indexTokenCountMap: {},
|
|
2678
|
-
});
|
|
2679
|
-
|
|
2680
|
-
// Override the model to use a fake LLM
|
|
2681
|
-
run.Graph?.overrideTestModel(['This is a test response'], 1);
|
|
2682
|
-
|
|
2683
|
-
const messages = [new HumanMessage('Hello, how are you?')];
|
|
2684
|
-
|
|
2685
|
-
const config: Partial<RunnableConfig> & {
|
|
2686
|
-
version: 'v1' | 'v2';
|
|
2687
|
-
streamMode: string;
|
|
2688
|
-
} = {
|
|
2689
|
-
configurable: {
|
|
2690
|
-
thread_id: 'test-thread',
|
|
2691
|
-
},
|
|
2692
|
-
streamMode: 'values',
|
|
2693
|
-
version: 'v2' as const,
|
|
2694
|
-
};
|
|
2695
|
-
|
|
2696
|
-
await run.processStream({ messages }, config);
|
|
2697
|
-
|
|
2698
|
-
const finalMessages = run.getRunMessages();
|
|
2699
|
-
expect(finalMessages).toBeDefined();
|
|
2700
|
-
expect(finalMessages?.length).toBeGreaterThan(0);
|
|
2701
|
-
});
|
|
2702
|
-
});
|
|
2703
|
-
|
|
2704
|
-
describe('enforceOriginalContentCap', () => {
|
|
2705
|
-
it('is a no-op when total chars are below the cap', () => {
|
|
2706
|
-
const map = new Map<number, string>([
|
|
2707
|
-
[0, 'a'.repeat(100)],
|
|
2708
|
-
[1, 'b'.repeat(200)],
|
|
2709
|
-
]);
|
|
2710
|
-
enforceOriginalContentCap(map);
|
|
2711
|
-
expect(map.size).toBe(2);
|
|
2712
|
-
expect(map.get(0)?.length).toBe(100);
|
|
2713
|
-
expect(map.get(1)?.length).toBe(200);
|
|
2714
|
-
});
|
|
2715
|
-
|
|
2716
|
-
it('evicts oldest entries (by Map insertion order) until under the cap', () => {
|
|
2717
|
-
const map = new Map<number, string>();
|
|
2718
|
-
// Insert 4 entries totaling well over the cap, in insertion order
|
|
2719
|
-
// 0, 1, 2, 3. Each entry is roughly 700_000 chars (>1/3 of cap).
|
|
2720
|
-
const big = 'x'.repeat(700_000);
|
|
2721
|
-
map.set(0, big);
|
|
2722
|
-
map.set(1, big);
|
|
2723
|
-
map.set(2, big);
|
|
2724
|
-
map.set(3, big);
|
|
2725
|
-
|
|
2726
|
-
// 4 * 700_000 = 2_800_000 > 2_000_000 cap. Eviction should drop
|
|
2727
|
-
// the oldest entry (key 0) — leaving 3 * 700_000 = 2_100_000 still
|
|
2728
|
-
// > cap, so key 1 is also dropped — 2 * 700_000 = 1_400_000 ≤ cap.
|
|
2729
|
-
enforceOriginalContentCap(map);
|
|
2730
|
-
expect(map.has(0)).toBe(false);
|
|
2731
|
-
expect(map.has(1)).toBe(false);
|
|
2732
|
-
expect(map.has(2)).toBe(true);
|
|
2733
|
-
expect(map.has(3)).toBe(true);
|
|
2734
|
-
});
|
|
2735
|
-
|
|
2736
|
-
it('exposes the cap as a constant for callers', () => {
|
|
2737
|
-
expect(ORIGINAL_CONTENT_MAX_CHARS).toBe(2_000_000);
|
|
2738
|
-
});
|
|
2739
|
-
});
|
|
2740
|
-
});
|
|
2741
|
-
|
|
2742
|
-
describe('sanitizeOrphanToolBlocks', () => {
|
|
2743
|
-
it('strips orphan tool_use blocks from AI messages with no matching ToolMessage', () => {
|
|
2744
|
-
const messages: BaseMessage[] = [
|
|
2745
|
-
new HumanMessage('Hello'),
|
|
2746
|
-
new AIMessage({
|
|
2747
|
-
content: [
|
|
2748
|
-
{ type: 'text', text: 'Let me check.' },
|
|
2749
|
-
{ type: 'tool_use', id: 'tool_1', name: 'calc', input: { x: 1 } },
|
|
2750
|
-
],
|
|
2751
|
-
tool_calls: [
|
|
2752
|
-
{ id: 'tool_1', name: 'calc', args: { x: 1 }, type: 'tool_call' },
|
|
2753
|
-
],
|
|
2754
|
-
}),
|
|
2755
|
-
// No ToolMessage for tool_1 — orphan
|
|
2756
|
-
];
|
|
2757
|
-
|
|
2758
|
-
const result = sanitizeOrphanToolBlocks(messages);
|
|
2759
|
-
// The stripped AI message was the last message → dropped (incomplete tool call)
|
|
2760
|
-
expect(result).toHaveLength(1);
|
|
2761
|
-
expect(result[0].getType()).toBe('human');
|
|
2762
|
-
});
|
|
2763
|
-
|
|
2764
|
-
it('drops orphan ToolMessages whose AI message is missing', () => {
|
|
2765
|
-
const messages: BaseMessage[] = [
|
|
2766
|
-
new HumanMessage('Hello'),
|
|
2767
|
-
new ToolMessage({
|
|
2768
|
-
content: 'result',
|
|
2769
|
-
tool_call_id: 'tool_orphan',
|
|
2770
|
-
name: 'calc',
|
|
2771
|
-
}),
|
|
2772
|
-
new AIMessage('Some response'),
|
|
2773
|
-
];
|
|
2774
|
-
|
|
2775
|
-
const result = sanitizeOrphanToolBlocks(messages);
|
|
2776
|
-
expect(result).toHaveLength(2); // HumanMessage + AIMessage, orphan ToolMessage dropped
|
|
2777
|
-
expect(result[0].getType()).toBe('human');
|
|
2778
|
-
expect(result[1].getType()).toBe('ai');
|
|
2779
|
-
});
|
|
2780
|
-
|
|
2781
|
-
it('preserves correctly paired tool_use and ToolMessages', () => {
|
|
2782
|
-
const messages: BaseMessage[] = [
|
|
2783
|
-
new HumanMessage('Compute 1+1'),
|
|
2784
|
-
new AIMessage({
|
|
2785
|
-
content: [
|
|
2786
|
-
{ type: 'text', text: 'Let me calculate.' },
|
|
2787
|
-
{ type: 'tool_use', id: 'tool_a', name: 'calc', input: { x: 1 } },
|
|
2788
|
-
],
|
|
2789
|
-
tool_calls: [
|
|
2790
|
-
{ id: 'tool_a', name: 'calc', args: { x: 1 }, type: 'tool_call' },
|
|
2791
|
-
],
|
|
2792
|
-
}),
|
|
2793
|
-
new ToolMessage({
|
|
2794
|
-
content: '2',
|
|
2795
|
-
tool_call_id: 'tool_a',
|
|
2796
|
-
name: 'calc',
|
|
2797
|
-
}),
|
|
2798
|
-
new AIMessage('The answer is 2.'),
|
|
2799
|
-
];
|
|
2800
|
-
|
|
2801
|
-
const result = sanitizeOrphanToolBlocks(messages);
|
|
2802
|
-
expect(result).toHaveLength(4); // All messages preserved
|
|
2803
|
-
expect(result.map((m) => m.getType())).toEqual([
|
|
2804
|
-
'human',
|
|
2805
|
-
'ai',
|
|
2806
|
-
'tool',
|
|
2807
|
-
'ai',
|
|
2808
|
-
]);
|
|
2809
|
-
});
|
|
2810
|
-
|
|
2811
|
-
it('drops AI message entirely when it only contained orphan tool_use blocks', () => {
|
|
2812
|
-
const messages: BaseMessage[] = [
|
|
2813
|
-
new HumanMessage('Do something'),
|
|
2814
|
-
new AIMessage({
|
|
2815
|
-
content: [{ type: 'tool_use', id: 'tool_x', name: 'run', input: {} }],
|
|
2816
|
-
tool_calls: [
|
|
2817
|
-
{ id: 'tool_x', name: 'run', args: {}, type: 'tool_call' },
|
|
2818
|
-
],
|
|
2819
|
-
}),
|
|
2820
|
-
// No ToolMessage for tool_x
|
|
2821
|
-
];
|
|
2822
|
-
|
|
2823
|
-
const result = sanitizeOrphanToolBlocks(messages);
|
|
2824
|
-
// The AI message had only tool_use blocks, stripping them leaves nothing → dropped
|
|
2825
|
-
expect(result).toHaveLength(1);
|
|
2826
|
-
expect(result[0].getType()).toBe('human');
|
|
2827
|
-
});
|
|
2828
|
-
|
|
2829
|
-
it('keeps stripped AI message in the middle but drops stripped trailing AI', () => {
|
|
2830
|
-
const messages: BaseMessage[] = [
|
|
2831
|
-
new HumanMessage('First question'),
|
|
2832
|
-
new AIMessage({
|
|
2833
|
-
content: [
|
|
2834
|
-
{ type: 'text', text: 'Let me use two tools.' },
|
|
2835
|
-
{ type: 'tool_use', id: 'tool_a', name: 'calc', input: { x: 1 } },
|
|
2836
|
-
{
|
|
2837
|
-
type: 'tool_use',
|
|
2838
|
-
id: 'tool_orphan',
|
|
2839
|
-
name: 'search',
|
|
2840
|
-
input: { q: 'test' },
|
|
2841
|
-
},
|
|
2842
|
-
],
|
|
2843
|
-
tool_calls: [
|
|
2844
|
-
{ id: 'tool_a', name: 'calc', args: { x: 1 }, type: 'tool_call' },
|
|
2845
|
-
{
|
|
2846
|
-
id: 'tool_orphan',
|
|
2847
|
-
name: 'search',
|
|
2848
|
-
args: { q: 'test' },
|
|
2849
|
-
type: 'tool_call',
|
|
2850
|
-
},
|
|
2851
|
-
],
|
|
2852
|
-
}),
|
|
2853
|
-
new ToolMessage({
|
|
2854
|
-
content: '42',
|
|
2855
|
-
tool_call_id: 'tool_a',
|
|
2856
|
-
name: 'calc',
|
|
2857
|
-
}),
|
|
2858
|
-
// No ToolMessage for tool_orphan, but conversation continues:
|
|
2859
|
-
new AIMessage({
|
|
2860
|
-
content: [{ type: 'text', text: 'Got the calc result.' }],
|
|
2861
|
-
tool_calls: [
|
|
2862
|
-
{ id: 'tool_b', name: 'run', args: {}, type: 'tool_call' },
|
|
2863
|
-
],
|
|
2864
|
-
}),
|
|
2865
|
-
// tool_b is also orphan → stripped, and this AI is last → dropped
|
|
2866
|
-
];
|
|
2867
|
-
|
|
2868
|
-
const result = sanitizeOrphanToolBlocks(messages);
|
|
2869
|
-
// message[1]: AI has tool_orphan stripped but tool_a kept → stays (middle, not trailing)
|
|
2870
|
-
// message[3]: AI has tool_b stripped, is trailing → dropped
|
|
2871
|
-
expect(result).toHaveLength(3); // HumanMessage, stripped AI (kept tool_a), ToolMessage
|
|
2872
|
-
const ai = result[1] as AIMessage;
|
|
2873
|
-
expect(ai.tool_calls).toHaveLength(1);
|
|
2874
|
-
expect(ai.tool_calls![0].id).toBe('tool_a');
|
|
2875
|
-
expect(result[2].getType()).toBe('tool');
|
|
2876
|
-
});
|
|
2877
|
-
|
|
2878
|
-
it('keeps unmodified trailing AI message (no orphan tool_use)', () => {
|
|
2879
|
-
const messages: BaseMessage[] = [
|
|
2880
|
-
new HumanMessage('Hello'),
|
|
2881
|
-
new ToolMessage({
|
|
2882
|
-
content: 'result',
|
|
2883
|
-
tool_call_id: 'tool_orphan',
|
|
2884
|
-
name: 'calc',
|
|
2885
|
-
}),
|
|
2886
|
-
new AIMessage('Final response without tool calls.'),
|
|
2887
|
-
];
|
|
2888
|
-
|
|
2889
|
-
const result = sanitizeOrphanToolBlocks(messages);
|
|
2890
|
-
// orphan ToolMessage dropped, trailing AI kept (was not stripped)
|
|
2891
|
-
expect(result).toHaveLength(2);
|
|
2892
|
-
expect(result[0].getType()).toBe('human');
|
|
2893
|
-
expect(result[1].getType()).toBe('ai');
|
|
2894
|
-
});
|
|
2895
|
-
|
|
2896
|
-
it('preserves BaseMessage prototype on stripped AIMessage instances', () => {
|
|
2897
|
-
const messages: BaseMessage[] = [
|
|
2898
|
-
new HumanMessage('Hello'),
|
|
2899
|
-
new AIMessage({
|
|
2900
|
-
content: [
|
|
2901
|
-
{ type: 'text', text: 'Let me search and calculate.' },
|
|
2902
|
-
{
|
|
2903
|
-
type: 'tool_use',
|
|
2904
|
-
id: 'tool_a',
|
|
2905
|
-
name: 'search',
|
|
2906
|
-
input: { q: 'test' },
|
|
2907
|
-
},
|
|
2908
|
-
{ type: 'tool_use', id: 'tool_b', name: 'calc', input: { x: 1 } },
|
|
2909
|
-
],
|
|
2910
|
-
tool_calls: [
|
|
2911
|
-
{
|
|
2912
|
-
id: 'tool_a',
|
|
2913
|
-
name: 'search',
|
|
2914
|
-
args: { q: 'test' },
|
|
2915
|
-
type: 'tool_call' as const,
|
|
2916
|
-
},
|
|
2917
|
-
{
|
|
2918
|
-
id: 'tool_b',
|
|
2919
|
-
name: 'calc',
|
|
2920
|
-
args: { x: 1 },
|
|
2921
|
-
type: 'tool_call' as const,
|
|
2922
|
-
},
|
|
2923
|
-
],
|
|
2924
|
-
}),
|
|
2925
|
-
new ToolMessage({ content: 'result', tool_call_id: 'tool_b' }),
|
|
2926
|
-
// No ToolMessage for tool_a — orphan
|
|
2927
|
-
];
|
|
2928
|
-
|
|
2929
|
-
const result = sanitizeOrphanToolBlocks(messages);
|
|
2930
|
-
// AI message should survive (tool_a stripped, tool_b kept)
|
|
2931
|
-
expect(result).toHaveLength(3);
|
|
2932
|
-
|
|
2933
|
-
// Every output message must pass isBaseMessage and have getType()
|
|
2934
|
-
for (const msg of result) {
|
|
2935
|
-
expect(isBaseMessage(msg)).toBe(true);
|
|
2936
|
-
expect(typeof msg.getType()).toBe('string');
|
|
2937
|
-
}
|
|
2938
|
-
expect(result[1].getType()).toBe('ai');
|
|
2939
|
-
expect(result[1]).toBeInstanceOf(AIMessage);
|
|
2940
|
-
});
|
|
2941
|
-
|
|
2942
|
-
it('preserves AIMessageChunk prototype on stripped messages', () => {
|
|
2943
|
-
// Simulate what happens in real graph execution: model returns AIMessageChunk,
|
|
2944
|
-
// state passes through LangGraph, sanitizeOrphanToolBlocks strips orphan server tools.
|
|
2945
|
-
const chunk = new AIMessageChunk({
|
|
2946
|
-
content: [
|
|
2947
|
-
{ type: 'text', text: 'Searching...' },
|
|
2948
|
-
{ type: 'tool_use', id: 'srvtoolu_1', name: 'web_search', input: '' },
|
|
2949
|
-
{ type: 'tool_use', id: 'toolu_2', name: 'calculator', input: '2+2' },
|
|
2950
|
-
],
|
|
2951
|
-
tool_call_chunks: [
|
|
2952
|
-
{ id: 'srvtoolu_1', index: 0, name: 'web_search', args: '' },
|
|
2953
|
-
{ id: 'toolu_2', index: 2, name: 'calculator', args: '2+2' },
|
|
2954
|
-
],
|
|
2955
|
-
});
|
|
2956
|
-
|
|
2957
|
-
const messages: BaseMessage[] = [
|
|
2958
|
-
new HumanMessage('Search and calculate'),
|
|
2959
|
-
chunk,
|
|
2960
|
-
new ToolMessage({ content: '4', tool_call_id: 'toolu_2' }),
|
|
2961
|
-
// No ToolMessage for srvtoolu_1 — server tool, orphan
|
|
2962
|
-
];
|
|
2963
|
-
|
|
2964
|
-
const result = sanitizeOrphanToolBlocks(messages);
|
|
2965
|
-
expect(result).toHaveLength(3);
|
|
2966
|
-
|
|
2967
|
-
// The AIMessageChunk must retain its prototype so LangChain's
|
|
2968
|
-
// coerceMessageLikeToMessage recognizes it as a BaseMessage.
|
|
2969
|
-
const aiMsg = result[1];
|
|
2970
|
-
expect(isBaseMessage(aiMsg)).toBe(true);
|
|
2971
|
-
expect(typeof aiMsg.getType()).toBe('string');
|
|
2972
|
-
expect(aiMsg.getType()).toBe('ai');
|
|
2973
|
-
});
|
|
2974
|
-
|
|
2975
|
-
it('preserves prototype on plain-object messages with duck-typed patching', () => {
|
|
2976
|
-
// Simulate deserialized messages that still have a prototype (e.g. from
|
|
2977
|
-
// LangGraph subgraph state transfer) but aren't class instances.
|
|
2978
|
-
const proto = { _getType: (): string => 'ai', getType: (): string => 'ai' };
|
|
2979
|
-
const plainAi = Object.create(proto);
|
|
2980
|
-
Object.assign(plainAi, {
|
|
2981
|
-
role: 'assistant',
|
|
2982
|
-
content: [
|
|
2983
|
-
{ type: 'text', text: 'checking' },
|
|
2984
|
-
{ type: 'tool_use', id: 'orphan_1', name: 'tool', input: {} },
|
|
2985
|
-
],
|
|
2986
|
-
tool_calls: [
|
|
2987
|
-
{ id: 'orphan_1', name: 'tool', args: {}, type: 'tool_call' },
|
|
2988
|
-
],
|
|
2989
|
-
});
|
|
2990
|
-
|
|
2991
|
-
const messages = [plainAi] as BaseMessage[];
|
|
2992
|
-
sanitizeOrphanToolBlocks(messages);
|
|
2993
|
-
|
|
2994
|
-
// Stripped AI was trailing → dropped. But if we add a human after:
|
|
2995
|
-
const messages2 = [
|
|
2996
|
-
new HumanMessage('hi'),
|
|
2997
|
-
plainAi,
|
|
2998
|
-
new HumanMessage('follow up'),
|
|
2999
|
-
] as BaseMessage[];
|
|
3000
|
-
const result2 = sanitizeOrphanToolBlocks(messages2);
|
|
3001
|
-
|
|
3002
|
-
// The patched message in the middle must still have _getType from proto
|
|
3003
|
-
const middleMsg = result2[1];
|
|
3004
|
-
expect(typeof middleMsg._getType).toBe('function');
|
|
3005
|
-
expect(middleMsg._getType()).toBe('ai');
|
|
3006
|
-
});
|
|
3007
|
-
|
|
3008
|
-
it('handles plain objects (non-BaseMessage instances) via duck typing', () => {
|
|
3009
|
-
// Simulate messages that have lost their class instances (LangGraph state serialization)
|
|
3010
|
-
const plainMessages = [
|
|
3011
|
-
{ role: 'user', content: 'Hello', _type: 'human' },
|
|
3012
|
-
{
|
|
3013
|
-
role: 'assistant',
|
|
3014
|
-
_type: 'ai',
|
|
3015
|
-
content: [
|
|
3016
|
-
{ type: 'text', text: 'Let me check.' },
|
|
3017
|
-
{ type: 'tool_use', id: 'tool_1', name: 'calc', input: { x: 1 } },
|
|
3018
|
-
],
|
|
3019
|
-
tool_calls: [
|
|
3020
|
-
{ id: 'tool_1', name: 'calc', args: { x: 1 }, type: 'tool_call' },
|
|
3021
|
-
],
|
|
3022
|
-
},
|
|
3023
|
-
// No ToolMessage for tool_1 — orphan
|
|
3024
|
-
] as unknown as BaseMessage[];
|
|
3025
|
-
|
|
3026
|
-
// Should not throw "getType is not a function"
|
|
3027
|
-
const result = sanitizeOrphanToolBlocks(plainMessages);
|
|
3028
|
-
// The stripped AI message was the last message → dropped (incomplete tool call)
|
|
3029
|
-
expect(result).toHaveLength(1);
|
|
3030
|
-
});
|
|
3031
|
-
});
|
|
3032
|
-
|
|
3033
|
-
describe('prunedMemory ordering with thinking enabled', () => {
|
|
3034
|
-
it('messagesToRefine preserves chronological order when thinking search pops multiple messages', () => {
|
|
3035
|
-
const tokenCounter = createTestTokenCounter();
|
|
3036
|
-
const messages: BaseMessage[] = [
|
|
3037
|
-
new HumanMessage('Hello'),
|
|
3038
|
-
new AIMessage({
|
|
3039
|
-
content: [
|
|
3040
|
-
{
|
|
3041
|
-
type: ContentTypes.REASONING_CONTENT,
|
|
3042
|
-
reasoningText: {
|
|
3043
|
-
text: 'Thinking about navigation...',
|
|
3044
|
-
signature: 'sig1',
|
|
3045
|
-
},
|
|
3046
|
-
},
|
|
3047
|
-
{ type: 'text', text: 'Navigating now.' },
|
|
3048
|
-
],
|
|
3049
|
-
tool_calls: [
|
|
3050
|
-
{
|
|
3051
|
-
id: 'tc_nav',
|
|
3052
|
-
name: 'navigate',
|
|
3053
|
-
args: { url: 'about:blank' },
|
|
3054
|
-
type: 'tool_call',
|
|
3055
|
-
},
|
|
3056
|
-
],
|
|
3057
|
-
}),
|
|
3058
|
-
new ToolMessage({
|
|
3059
|
-
content: 'Navigated to about:blank.',
|
|
3060
|
-
tool_call_id: 'tc_nav',
|
|
3061
|
-
name: 'navigate',
|
|
3062
|
-
}),
|
|
3063
|
-
new AIMessage({
|
|
3064
|
-
content: [
|
|
3065
|
-
{
|
|
3066
|
-
type: ContentTypes.REASONING_CONTENT,
|
|
3067
|
-
reasoningText: {
|
|
3068
|
-
text: 'Now I will write code...',
|
|
3069
|
-
signature: 'sig2',
|
|
3070
|
-
},
|
|
3071
|
-
},
|
|
3072
|
-
{ type: 'text', text: 'Running script.' },
|
|
3073
|
-
],
|
|
3074
|
-
tool_calls: [
|
|
3075
|
-
{
|
|
3076
|
-
id: 'tc_eval',
|
|
3077
|
-
name: 'evaluate',
|
|
3078
|
-
args: { code: 'x'.repeat(5000) },
|
|
3079
|
-
type: 'tool_call',
|
|
3080
|
-
},
|
|
3081
|
-
],
|
|
3082
|
-
}),
|
|
3083
|
-
new ToolMessage({
|
|
3084
|
-
content: 'y'.repeat(5000), // large tool result
|
|
3085
|
-
tool_call_id: 'tc_eval',
|
|
3086
|
-
name: 'evaluate',
|
|
3087
|
-
}),
|
|
3088
|
-
];
|
|
3089
|
-
|
|
3090
|
-
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
3091
|
-
for (let i = 0; i < messages.length; i++) {
|
|
3092
|
-
indexTokenCountMap[i] = tokenCounter(messages[i]);
|
|
3093
|
-
}
|
|
3094
|
-
|
|
3095
|
-
// Use a very tight budget so the backward iteration must prune messages
|
|
3096
|
-
// The thinking search will cause the loop to `continue` past the large ToolMessage
|
|
3097
|
-
const result = realGetMessagesWithinTokenLimit({
|
|
3098
|
-
messages,
|
|
3099
|
-
maxContextTokens: 200, // very tight
|
|
3100
|
-
indexTokenCountMap,
|
|
3101
|
-
thinkingEnabled: true,
|
|
3102
|
-
tokenCounter,
|
|
3103
|
-
reasoningType: ContentTypes.REASONING_CONTENT,
|
|
3104
|
-
});
|
|
3105
|
-
|
|
3106
|
-
// The key assertion: messagesToRefine must be in chronological order.
|
|
3107
|
-
// AI(evaluate) at index 3 must come BEFORE ToolMessage(evaluate) at index 4.
|
|
3108
|
-
for (let i = 0; i < result.messagesToRefine.length - 1; i++) {
|
|
3109
|
-
const current = result.messagesToRefine[i];
|
|
3110
|
-
const next = result.messagesToRefine[i + 1];
|
|
3111
|
-
// A ToolMessage should never come before its AI message
|
|
3112
|
-
if (next.getType() === 'ai' && current.getType() === 'tool') {
|
|
3113
|
-
const toolId = (current as ToolMessage).tool_call_id;
|
|
3114
|
-
const aiToolIds = ((next as AIMessage).tool_calls ?? []).map(
|
|
3115
|
-
(tc) => tc.id
|
|
3116
|
-
);
|
|
3117
|
-
expect(aiToolIds).not.toContain(toolId);
|
|
3118
|
-
}
|
|
3119
|
-
}
|
|
3120
|
-
|
|
3121
|
-
// Verify the specific ordering: if both AI(evaluate) and Tool(evaluate) are in
|
|
3122
|
-
// messagesToRefine, AI must come first.
|
|
3123
|
-
const evalAiIdx = result.messagesToRefine.findIndex(
|
|
3124
|
-
(m) =>
|
|
3125
|
-
m.getType() === 'ai' &&
|
|
3126
|
-
((m as AIMessage).tool_calls ?? []).some((tc) => tc.id === 'tc_eval')
|
|
3127
|
-
);
|
|
3128
|
-
const evalToolIdx = result.messagesToRefine.findIndex(
|
|
3129
|
-
(m) =>
|
|
3130
|
-
m.getType() === 'tool' && (m as ToolMessage).tool_call_id === 'tc_eval'
|
|
3131
|
-
);
|
|
3132
|
-
if (evalAiIdx >= 0 && evalToolIdx >= 0) {
|
|
3133
|
-
expect(evalAiIdx).toBeLessThan(evalToolIdx);
|
|
3134
|
-
}
|
|
3135
|
-
});
|
|
3136
|
-
});
|
|
3137
|
-
|
|
3138
|
-
describe('thinking enabled — tail tool_use without a thinking block (issue #115)', () => {
|
|
3139
|
-
it('does not throw when the trailing AI message issued a tool call without a thinking block', () => {
|
|
3140
|
-
const tokenCounter = createTestTokenCounter();
|
|
3141
|
-
const messages: BaseMessage[] = [
|
|
3142
|
-
new HumanMessage('first turn'),
|
|
3143
|
-
new AIMessage({
|
|
3144
|
-
content: [
|
|
3145
|
-
{
|
|
3146
|
-
type: ContentTypes.THINKING,
|
|
3147
|
-
thinking: 'thinking about the first response',
|
|
3148
|
-
signature: 'sig0',
|
|
3149
|
-
},
|
|
3150
|
-
{ type: 'text', text: 'first reply' },
|
|
3151
|
-
],
|
|
3152
|
-
}),
|
|
3153
|
-
new HumanMessage('please read this doc and tell me X'),
|
|
3154
|
-
// Anthropic may emit a tool_use without an accompanying thinking block —
|
|
3155
|
-
// valid API behavior that the pruner must tolerate.
|
|
3156
|
-
new AIMessage({
|
|
3157
|
-
content: [
|
|
3158
|
-
{
|
|
3159
|
-
type: 'tool_use',
|
|
3160
|
-
id: 'tc_get_doc',
|
|
3161
|
-
name: 'get_doc_content',
|
|
3162
|
-
input: { docId: 'abc' },
|
|
3163
|
-
},
|
|
3164
|
-
],
|
|
3165
|
-
tool_calls: [
|
|
3166
|
-
{
|
|
3167
|
-
id: 'tc_get_doc',
|
|
3168
|
-
name: 'get_doc_content',
|
|
3169
|
-
args: { docId: 'abc' },
|
|
3170
|
-
type: 'tool_call',
|
|
3171
|
-
},
|
|
3172
|
-
],
|
|
3173
|
-
}),
|
|
3174
|
-
new ToolMessage({
|
|
3175
|
-
content: 'a'.repeat(8000), // huge tool result that pushes us past budget
|
|
3176
|
-
tool_call_id: 'tc_get_doc',
|
|
3177
|
-
name: 'get_doc_content',
|
|
3178
|
-
}),
|
|
3179
|
-
];
|
|
3180
|
-
|
|
3181
|
-
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
3182
|
-
for (let i = 0; i < messages.length; i++) {
|
|
3183
|
-
indexTokenCountMap[i] = tokenCounter(messages[i]);
|
|
3184
|
-
}
|
|
3185
|
-
|
|
3186
|
-
expect(() =>
|
|
3187
|
-
realGetMessagesWithinTokenLimit({
|
|
3188
|
-
messages,
|
|
3189
|
-
maxContextTokens: 200, // tight budget so pruning actually runs
|
|
3190
|
-
indexTokenCountMap,
|
|
3191
|
-
thinkingEnabled: true,
|
|
3192
|
-
tokenCounter,
|
|
3193
|
-
reasoningType: ContentTypes.THINKING,
|
|
3194
|
-
})
|
|
3195
|
-
).not.toThrow();
|
|
3196
|
-
});
|
|
3197
|
-
|
|
3198
|
-
it('returns a prunable context for the [AI tool_use, Tool] tail without a thinking block', () => {
|
|
3199
|
-
const tokenCounter = createTestTokenCounter();
|
|
3200
|
-
const messages: BaseMessage[] = [
|
|
3201
|
-
new HumanMessage('please read this doc'),
|
|
3202
|
-
new AIMessage({
|
|
3203
|
-
content: [
|
|
3204
|
-
{
|
|
3205
|
-
type: 'tool_use',
|
|
3206
|
-
id: 'tc_get_doc',
|
|
3207
|
-
name: 'get_doc_content',
|
|
3208
|
-
input: { docId: 'abc' },
|
|
3209
|
-
},
|
|
3210
|
-
],
|
|
3211
|
-
tool_calls: [
|
|
3212
|
-
{
|
|
3213
|
-
id: 'tc_get_doc',
|
|
3214
|
-
name: 'get_doc_content',
|
|
3215
|
-
args: { docId: 'abc' },
|
|
3216
|
-
type: 'tool_call',
|
|
3217
|
-
},
|
|
3218
|
-
],
|
|
3219
|
-
}),
|
|
3220
|
-
new ToolMessage({
|
|
3221
|
-
content: 'b'.repeat(6000),
|
|
3222
|
-
tool_call_id: 'tc_get_doc',
|
|
3223
|
-
name: 'get_doc_content',
|
|
3224
|
-
}),
|
|
3225
|
-
];
|
|
3226
|
-
|
|
3227
|
-
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
3228
|
-
for (let i = 0; i < messages.length; i++) {
|
|
3229
|
-
indexTokenCountMap[i] = tokenCounter(messages[i]);
|
|
3230
|
-
}
|
|
3231
|
-
|
|
3232
|
-
const result = realGetMessagesWithinTokenLimit({
|
|
3233
|
-
messages,
|
|
3234
|
-
maxContextTokens: 200,
|
|
3235
|
-
indexTokenCountMap,
|
|
3236
|
-
thinkingEnabled: true,
|
|
3237
|
-
tokenCounter,
|
|
3238
|
-
reasoningType: ContentTypes.THINKING,
|
|
3239
|
-
});
|
|
3240
|
-
|
|
3241
|
-
expect(result.context).toBeDefined();
|
|
3242
|
-
expect(result.messagesToRefine.length).toBeGreaterThan(0);
|
|
3243
|
-
expect(result.thinkingStartIndex).toBeUndefined();
|
|
3244
|
-
});
|
|
3245
|
-
|
|
3246
|
-
it('handles consecutive tool calls without any thinking block in the tail', () => {
|
|
3247
|
-
const tokenCounter = createTestTokenCounter();
|
|
3248
|
-
const messages: BaseMessage[] = [
|
|
3249
|
-
new HumanMessage('do two things'),
|
|
3250
|
-
new AIMessage({
|
|
3251
|
-
content: [
|
|
3252
|
-
{
|
|
3253
|
-
type: 'tool_use',
|
|
3254
|
-
id: 'tc_1',
|
|
3255
|
-
name: 'tool_a',
|
|
3256
|
-
input: { x: 1 },
|
|
3257
|
-
},
|
|
3258
|
-
],
|
|
3259
|
-
tool_calls: [
|
|
3260
|
-
{ id: 'tc_1', name: 'tool_a', args: { x: 1 }, type: 'tool_call' },
|
|
3261
|
-
],
|
|
3262
|
-
}),
|
|
3263
|
-
new ToolMessage({
|
|
3264
|
-
content: 'result_a',
|
|
3265
|
-
tool_call_id: 'tc_1',
|
|
3266
|
-
name: 'tool_a',
|
|
3267
|
-
}),
|
|
3268
|
-
new AIMessage({
|
|
3269
|
-
content: [
|
|
3270
|
-
{
|
|
3271
|
-
type: 'tool_use',
|
|
3272
|
-
id: 'tc_2',
|
|
3273
|
-
name: 'tool_b',
|
|
3274
|
-
input: { y: 2 },
|
|
3275
|
-
},
|
|
3276
|
-
],
|
|
3277
|
-
tool_calls: [
|
|
3278
|
-
{ id: 'tc_2', name: 'tool_b', args: { y: 2 }, type: 'tool_call' },
|
|
3279
|
-
],
|
|
3280
|
-
}),
|
|
3281
|
-
new ToolMessage({
|
|
3282
|
-
content: 'd'.repeat(6000),
|
|
3283
|
-
tool_call_id: 'tc_2',
|
|
3284
|
-
name: 'tool_b',
|
|
3285
|
-
}),
|
|
3286
|
-
];
|
|
3287
|
-
|
|
3288
|
-
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
3289
|
-
for (let i = 0; i < messages.length; i++) {
|
|
3290
|
-
indexTokenCountMap[i] = tokenCounter(messages[i]);
|
|
3291
|
-
}
|
|
3292
|
-
|
|
3293
|
-
const result = realGetMessagesWithinTokenLimit({
|
|
3294
|
-
messages,
|
|
3295
|
-
maxContextTokens: 200,
|
|
3296
|
-
indexTokenCountMap,
|
|
3297
|
-
thinkingEnabled: true,
|
|
3298
|
-
tokenCounter,
|
|
3299
|
-
reasoningType: ContentTypes.THINKING,
|
|
3300
|
-
});
|
|
3301
|
-
expect(result.thinkingStartIndex).toBeUndefined();
|
|
3302
|
-
});
|
|
3303
|
-
|
|
3304
|
-
it('honors prior runThinkingStartIndex carry-over when the next call has a no-thinking tail', () => {
|
|
3305
|
-
// First call's tight budget forces pruning, which makes the closure
|
|
3306
|
-
// record the AI(thinking) message's index in runThinkingStartIndex.
|
|
3307
|
-
// Second call's tail is AI(tool_use) without a thinking block; the
|
|
3308
|
-
// pre-loaded thinkingBlock from the carry-over keeps the new guard
|
|
3309
|
-
// dormant and the existing reattachment path runs. Verifies the fix
|
|
3310
|
-
// doesn't disturb the carry-over interaction.
|
|
3311
|
-
const tokenCounter = createTestTokenCounter();
|
|
3312
|
-
const firstTurn: BaseMessage[] = [
|
|
3313
|
-
new HumanMessage('h'.repeat(120)),
|
|
3314
|
-
new AIMessage({
|
|
3315
|
-
content: [
|
|
3316
|
-
{
|
|
3317
|
-
type: ContentTypes.THINKING,
|
|
3318
|
-
thinking: 'planning the response',
|
|
3319
|
-
signature: 'sig-prior',
|
|
3320
|
-
},
|
|
3321
|
-
{ type: 'text', text: 'hi' },
|
|
3322
|
-
],
|
|
3323
|
-
}),
|
|
3324
|
-
];
|
|
3325
|
-
|
|
3326
|
-
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
3327
|
-
for (let i = 0; i < firstTurn.length; i++) {
|
|
3328
|
-
indexTokenCountMap[i] = tokenCounter(firstTurn[i]);
|
|
3329
|
-
}
|
|
3330
|
-
|
|
3331
|
-
const pruneMessages = createPruneMessages({
|
|
3332
|
-
maxTokens: 68,
|
|
3333
|
-
startIndex: 0,
|
|
3334
|
-
tokenCounter,
|
|
3335
|
-
indexTokenCountMap,
|
|
3336
|
-
thinkingEnabled: true,
|
|
3337
|
-
reserveRatio: 0,
|
|
3338
|
-
});
|
|
3339
|
-
|
|
3340
|
-
const firstResult = pruneMessages({ messages: firstTurn });
|
|
3341
|
-
expect(firstResult.messagesToRefine?.length).toBeGreaterThan(0);
|
|
3342
|
-
expect(firstResult.context.some((m) => m.getType() === 'ai')).toBe(true);
|
|
3343
|
-
|
|
3344
|
-
const secondTurn: BaseMessage[] = [
|
|
3345
|
-
...firstTurn,
|
|
3346
|
-
new HumanMessage('please read the doc'),
|
|
3347
|
-
new AIMessage({
|
|
3348
|
-
content: [
|
|
3349
|
-
{
|
|
3350
|
-
type: 'tool_use',
|
|
3351
|
-
id: 'tc_get_doc',
|
|
3352
|
-
name: 'get_doc_content',
|
|
3353
|
-
input: { docId: 'abc' },
|
|
3354
|
-
},
|
|
3355
|
-
],
|
|
3356
|
-
tool_calls: [
|
|
3357
|
-
{
|
|
3358
|
-
id: 'tc_get_doc',
|
|
3359
|
-
name: 'get_doc_content',
|
|
3360
|
-
args: { docId: 'abc' },
|
|
3361
|
-
type: 'tool_call',
|
|
3362
|
-
},
|
|
3363
|
-
],
|
|
3364
|
-
}),
|
|
3365
|
-
new ToolMessage({
|
|
3366
|
-
content: 'e'.repeat(40),
|
|
3367
|
-
tool_call_id: 'tc_get_doc',
|
|
3368
|
-
name: 'get_doc_content',
|
|
3369
|
-
}),
|
|
3370
|
-
];
|
|
3371
|
-
|
|
3372
|
-
let secondResult: ReturnType<typeof pruneMessages> | undefined;
|
|
3373
|
-
expect(() => {
|
|
3374
|
-
secondResult = pruneMessages({ messages: secondTurn });
|
|
3375
|
-
}).not.toThrow();
|
|
3376
|
-
|
|
3377
|
-
// Carry-over reattachment: even though the trailing AI(tool_use) has
|
|
3378
|
-
// no thinking block of its own, the closure's runThinkingStartIndex
|
|
3379
|
-
// points at the prior AI(thinking) and that block gets prepended to
|
|
3380
|
-
// the surviving AI message in context.
|
|
3381
|
-
const trailingAi = secondResult!.context.find(
|
|
3382
|
-
(m) =>
|
|
3383
|
-
m.getType() === 'ai' &&
|
|
3384
|
-
Array.isArray(m.content) &&
|
|
3385
|
-
(m.content as t.ExtendedMessageContent[]).some(
|
|
3386
|
-
(c) => typeof c === 'object' && c.type === 'tool_use'
|
|
3387
|
-
)
|
|
3388
|
-
);
|
|
3389
|
-
expect(trailingAi).toBeDefined();
|
|
3390
|
-
expect(
|
|
3391
|
-
(trailingAi!.content as t.ExtendedMessageContent[]).some(
|
|
3392
|
-
(c) => typeof c === 'object' && c.type === ContentTypes.THINKING
|
|
3393
|
-
)
|
|
3394
|
-
).toBe(true);
|
|
3395
|
-
});
|
|
3396
|
-
|
|
3397
|
-
it('integrates with ensureThinkingBlockInMessages so the API-bound payload stays valid', () => {
|
|
3398
|
-
// Models the full Graph.ts pipeline: pruner runs first, then
|
|
3399
|
-
// ensureThinkingBlockInMessages on the pruned context. The pruner used
|
|
3400
|
-
// to throw on the issue #115 tail; with the fix it returns the
|
|
3401
|
-
// messages, and ensureThinkingBlockInMessages folds the orphan
|
|
3402
|
-
// AI(tool_use)+Tool tail into a `[Previous agent context]`
|
|
3403
|
-
// HumanMessage. The Tool size is tuned so the trailing sequence
|
|
3404
|
-
// actually survives pruning — otherwise the assertions would be
|
|
3405
|
-
// vacuous.
|
|
3406
|
-
const tokenCounter = createTestTokenCounter();
|
|
3407
|
-
const messages: BaseMessage[] = [
|
|
3408
|
-
new HumanMessage('please read this doc and tell me X'),
|
|
3409
|
-
new AIMessage({
|
|
3410
|
-
content: [
|
|
3411
|
-
{
|
|
3412
|
-
type: 'tool_use',
|
|
3413
|
-
id: 'tc_get_doc',
|
|
3414
|
-
name: 'get_doc_content',
|
|
3415
|
-
input: { docId: 'abc' },
|
|
3416
|
-
},
|
|
3417
|
-
],
|
|
3418
|
-
tool_calls: [
|
|
3419
|
-
{
|
|
3420
|
-
id: 'tc_get_doc',
|
|
3421
|
-
name: 'get_doc_content',
|
|
3422
|
-
args: { docId: 'abc' },
|
|
3423
|
-
type: 'tool_call',
|
|
3424
|
-
},
|
|
3425
|
-
],
|
|
3426
|
-
}),
|
|
3427
|
-
new ToolMessage({
|
|
3428
|
-
content: 'f'.repeat(100),
|
|
3429
|
-
tool_call_id: 'tc_get_doc',
|
|
3430
|
-
name: 'get_doc_content',
|
|
3431
|
-
}),
|
|
3432
|
-
];
|
|
3433
|
-
|
|
3434
|
-
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
3435
|
-
for (let i = 0; i < messages.length; i++) {
|
|
3436
|
-
indexTokenCountMap[i] = tokenCounter(messages[i]);
|
|
3437
|
-
}
|
|
3438
|
-
|
|
3439
|
-
const pruneResult = realGetMessagesWithinTokenLimit({
|
|
3440
|
-
messages,
|
|
3441
|
-
maxContextTokens: 300,
|
|
3442
|
-
indexTokenCountMap,
|
|
3443
|
-
thinkingEnabled: true,
|
|
3444
|
-
tokenCounter,
|
|
3445
|
-
reasoningType: ContentTypes.THINKING,
|
|
3446
|
-
});
|
|
3447
|
-
|
|
3448
|
-
expect(pruneResult.context.length).toBe(3);
|
|
3449
|
-
|
|
3450
|
-
const finalMessages = ensureThinkingBlockInMessages(
|
|
3451
|
-
pruneResult.context,
|
|
3452
|
-
Providers.ANTHROPIC
|
|
3453
|
-
);
|
|
3454
|
-
|
|
3455
|
-
// ensureThinkingBlockInMessages should fold the orphan AI(tool_use)+Tool
|
|
3456
|
-
// into a synthetic HumanMessage carrying the `[Previous agent context]`
|
|
3457
|
-
// marker, leaving no AI(tool_use) in the outgoing payload.
|
|
3458
|
-
expect(finalMessages.length).toBe(2);
|
|
3459
|
-
expect(finalMessages[0]).toBeInstanceOf(HumanMessage);
|
|
3460
|
-
expect(finalMessages[1]).toBeInstanceOf(HumanMessage);
|
|
3461
|
-
|
|
3462
|
-
const folded = finalMessages[1] as HumanMessage;
|
|
3463
|
-
const foldedContent = folded.content;
|
|
3464
|
-
const foldedText = Array.isArray(foldedContent)
|
|
3465
|
-
? (foldedContent as t.ExtendedMessageContent[])
|
|
3466
|
-
.filter((c) => typeof c === 'object' && c.type === 'text')
|
|
3467
|
-
.map((c) => String(c.text ?? ''))
|
|
3468
|
-
.join('\n')
|
|
3469
|
-
: String(foldedContent);
|
|
3470
|
-
expect(foldedText).toContain('[Previous agent context]');
|
|
3471
|
-
|
|
3472
|
-
const hasOrphanToolUse = finalMessages.some((m) => {
|
|
3473
|
-
if (m.getType() !== 'ai') {
|
|
3474
|
-
return false;
|
|
3475
|
-
}
|
|
3476
|
-
const content = (m as AIMessage).content;
|
|
3477
|
-
if (!Array.isArray(content)) {
|
|
3478
|
-
return false;
|
|
3479
|
-
}
|
|
3480
|
-
return content.some(
|
|
3481
|
-
(c) => typeof c === 'object' && c.type === 'tool_use'
|
|
3482
|
-
);
|
|
3483
|
-
});
|
|
3484
|
-
expect(hasOrphanToolUse).toBe(false);
|
|
3485
|
-
});
|
|
3486
|
-
|
|
3487
|
-
it('still preserves the thinking block when the trailing AI message has one', () => {
|
|
3488
|
-
const tokenCounter = createTestTokenCounter();
|
|
3489
|
-
const messages: BaseMessage[] = [
|
|
3490
|
-
new HumanMessage('hi'),
|
|
3491
|
-
new AIMessage({
|
|
3492
|
-
content: [
|
|
3493
|
-
{
|
|
3494
|
-
type: ContentTypes.THINKING,
|
|
3495
|
-
thinking: 'older thinking',
|
|
3496
|
-
signature: 'sig-old',
|
|
3497
|
-
},
|
|
3498
|
-
{ type: 'text', text: 'older reply' },
|
|
3499
|
-
],
|
|
3500
|
-
}),
|
|
3501
|
-
new HumanMessage('please read this doc'),
|
|
3502
|
-
new AIMessage({
|
|
3503
|
-
content: [
|
|
3504
|
-
{
|
|
3505
|
-
type: ContentTypes.THINKING,
|
|
3506
|
-
thinking: 'I will fetch the doc',
|
|
3507
|
-
signature: 'sig-new',
|
|
3508
|
-
},
|
|
3509
|
-
{
|
|
3510
|
-
type: 'tool_use',
|
|
3511
|
-
id: 'tc_get_doc',
|
|
3512
|
-
name: 'get_doc_content',
|
|
3513
|
-
input: { docId: 'abc' },
|
|
3514
|
-
},
|
|
3515
|
-
],
|
|
3516
|
-
tool_calls: [
|
|
3517
|
-
{
|
|
3518
|
-
id: 'tc_get_doc',
|
|
3519
|
-
name: 'get_doc_content',
|
|
3520
|
-
args: { docId: 'abc' },
|
|
3521
|
-
type: 'tool_call',
|
|
3522
|
-
},
|
|
3523
|
-
],
|
|
3524
|
-
}),
|
|
3525
|
-
new ToolMessage({
|
|
3526
|
-
content: 'c'.repeat(6000),
|
|
3527
|
-
tool_call_id: 'tc_get_doc',
|
|
3528
|
-
name: 'get_doc_content',
|
|
3529
|
-
}),
|
|
3530
|
-
];
|
|
3531
|
-
|
|
3532
|
-
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
3533
|
-
for (let i = 0; i < messages.length; i++) {
|
|
3534
|
-
indexTokenCountMap[i] = tokenCounter(messages[i]);
|
|
3535
|
-
}
|
|
3536
|
-
|
|
3537
|
-
const result = realGetMessagesWithinTokenLimit({
|
|
3538
|
-
messages,
|
|
3539
|
-
maxContextTokens: 200,
|
|
3540
|
-
indexTokenCountMap,
|
|
3541
|
-
thinkingEnabled: true,
|
|
3542
|
-
tokenCounter,
|
|
3543
|
-
reasoningType: ContentTypes.THINKING,
|
|
3544
|
-
});
|
|
3545
|
-
|
|
3546
|
-
expect(result.thinkingStartIndex).toBeGreaterThanOrEqual(0);
|
|
3547
|
-
});
|
|
3548
|
-
});
|
|
3549
|
-
|
|
3550
|
-
describe('thinking enabled — non-Anthropic reasoning_content blocks (issue #191)', () => {
|
|
3551
|
-
it('locates a trailing reasoning_content block even when reasoningType defaults to THINKING (DeepSeek/Qwen)', () => {
|
|
3552
|
-
// DeepSeek-R1 and DashScope/Qwen-thinking route through the non-Bedrock
|
|
3553
|
-
// branch, so the caller passes reasoningType: THINKING — but their blocks
|
|
3554
|
-
// are tagged `reasoning_content` and are not normalized upstream. With a
|
|
3555
|
-
// system prompt at index 0 and an all-AI/tool tail, the consume loop never
|
|
3556
|
-
// pops a human to clear thinkingEndIndex (the issue #116 escape hatch), so
|
|
3557
|
-
// searching only for `thinking` missed the present block and threw a fatal
|
|
3558
|
-
// that permanently bricked the thread. The pruner must find the block by
|
|
3559
|
-
// its actual shape instead.
|
|
3560
|
-
const tokenCounter = createTestTokenCounter();
|
|
3561
|
-
const messages: BaseMessage[] = [
|
|
3562
|
-
new SystemMessage('you are a helpful assistant'),
|
|
3563
|
-
new AIMessage({
|
|
3564
|
-
content: [
|
|
3565
|
-
{
|
|
3566
|
-
type: ContentTypes.REASONING_CONTENT,
|
|
3567
|
-
reasoningText: {
|
|
3568
|
-
text: 'I will fetch the doc',
|
|
3569
|
-
signature: 'sig-new',
|
|
3570
|
-
},
|
|
3571
|
-
},
|
|
3572
|
-
{
|
|
3573
|
-
type: 'tool_use',
|
|
3574
|
-
id: 'tc_get_doc',
|
|
3575
|
-
name: 'get_doc_content',
|
|
3576
|
-
input: { docId: 'abc' },
|
|
3577
|
-
},
|
|
3578
|
-
],
|
|
3579
|
-
tool_calls: [
|
|
3580
|
-
{
|
|
3581
|
-
id: 'tc_get_doc',
|
|
3582
|
-
name: 'get_doc_content',
|
|
3583
|
-
args: { docId: 'abc' },
|
|
3584
|
-
type: 'tool_call',
|
|
3585
|
-
},
|
|
3586
|
-
],
|
|
3587
|
-
}),
|
|
3588
|
-
new ToolMessage({
|
|
3589
|
-
content: 'c'.repeat(6000),
|
|
3590
|
-
tool_call_id: 'tc_get_doc',
|
|
3591
|
-
name: 'get_doc_content',
|
|
3592
|
-
}),
|
|
3593
|
-
];
|
|
3594
|
-
|
|
3595
|
-
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
3596
|
-
for (let i = 0; i < messages.length; i++) {
|
|
3597
|
-
indexTokenCountMap[i] = tokenCounter(messages[i]);
|
|
3598
|
-
}
|
|
3599
|
-
|
|
3600
|
-
let result: ReturnType<typeof realGetMessagesWithinTokenLimit> | undefined;
|
|
3601
|
-
expect(() => {
|
|
3602
|
-
result = realGetMessagesWithinTokenLimit({
|
|
3603
|
-
messages,
|
|
3604
|
-
maxContextTokens: 200,
|
|
3605
|
-
indexTokenCountMap,
|
|
3606
|
-
thinkingEnabled: true,
|
|
3607
|
-
tokenCounter,
|
|
3608
|
-
reasoningType: ContentTypes.THINKING,
|
|
3609
|
-
});
|
|
3610
|
-
}).not.toThrow();
|
|
3611
|
-
|
|
3612
|
-
// thinkingStartIndex is only set when the reasoning block is actually
|
|
3613
|
-
// located — isolating the find fix (B) from the graceful-degradation
|
|
3614
|
-
// safety net (C), which would swallow the throw without finding anything.
|
|
3615
|
-
expect(result!.thinkingStartIndex).toBeGreaterThanOrEqual(0);
|
|
3616
|
-
});
|
|
3617
|
-
|
|
3618
|
-
it('does not throw when a carried-over thinking sequence has no locatable block', () => {
|
|
3619
|
-
// Models a stale runThinkingStartIndex carry-over pointing at an assistant
|
|
3620
|
-
// message that has no reasoning block. The pruner cannot find a block, but
|
|
3621
|
-
// a trailing AI/tool sequence keeps thinkingEndIndex set, so it used to
|
|
3622
|
-
// reach the fatal "no thinking block found" throw. Defense in depth: a
|
|
3623
|
-
// misconfiguration upstream of the pruner must not be able to brick the
|
|
3624
|
-
// thread — degrade to the partially-pruned context instead.
|
|
3625
|
-
const tokenCounter = createTestTokenCounter();
|
|
3626
|
-
const messages: BaseMessage[] = [
|
|
3627
|
-
new HumanMessage('h'.repeat(100)),
|
|
3628
|
-
new AIMessage({
|
|
3629
|
-
content: [{ type: 'text', text: 'a reply with no reasoning block' }],
|
|
3630
|
-
}),
|
|
3631
|
-
new HumanMessage('please read the doc'),
|
|
3632
|
-
new AIMessage({
|
|
3633
|
-
content: [
|
|
3634
|
-
{
|
|
3635
|
-
type: 'tool_use',
|
|
3636
|
-
id: 'tc_get_doc',
|
|
3637
|
-
name: 'get_doc_content',
|
|
3638
|
-
input: { docId: 'abc' },
|
|
3639
|
-
},
|
|
3640
|
-
],
|
|
3641
|
-
tool_calls: [
|
|
3642
|
-
{
|
|
3643
|
-
id: 'tc_get_doc',
|
|
3644
|
-
name: 'get_doc_content',
|
|
3645
|
-
args: { docId: 'abc' },
|
|
3646
|
-
type: 'tool_call',
|
|
3647
|
-
},
|
|
3648
|
-
],
|
|
3649
|
-
}),
|
|
3650
|
-
new ToolMessage({
|
|
3651
|
-
content: 'x'.repeat(150),
|
|
3652
|
-
tool_call_id: 'tc_get_doc',
|
|
3653
|
-
name: 'get_doc_content',
|
|
3654
|
-
}),
|
|
3655
|
-
];
|
|
3656
|
-
|
|
3657
|
-
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
3658
|
-
for (let i = 0; i < messages.length; i++) {
|
|
3659
|
-
indexTokenCountMap[i] = tokenCounter(messages[i]);
|
|
3660
|
-
}
|
|
3661
|
-
|
|
3662
|
-
let result: ReturnType<typeof realGetMessagesWithinTokenLimit> | undefined;
|
|
3663
|
-
expect(() => {
|
|
3664
|
-
result = realGetMessagesWithinTokenLimit({
|
|
3665
|
-
messages,
|
|
3666
|
-
maxContextTokens: 200,
|
|
3667
|
-
indexTokenCountMap,
|
|
3668
|
-
thinkingEnabled: true,
|
|
3669
|
-
tokenCounter,
|
|
3670
|
-
thinkingStartIndex: 1,
|
|
3671
|
-
reasoningType: ContentTypes.THINKING,
|
|
3672
|
-
});
|
|
3673
|
-
}).not.toThrow();
|
|
3674
|
-
|
|
3675
|
-
expect(result!.context.length).toBeGreaterThan(0);
|
|
3676
|
-
expect(result!.messagesToRefine.length).toBeGreaterThan(0);
|
|
3677
|
-
// The stale carried-over index must NOT be propagated: createPruneMessages
|
|
3678
|
-
// persists it as runThinkingStartIndex, and a stale value would suppress
|
|
3679
|
-
// the trailing scan on later turns and miss a real reasoning block.
|
|
3680
|
-
expect(result!.thinkingStartIndex).toBeUndefined();
|
|
3681
|
-
});
|
|
3682
|
-
|
|
3683
|
-
it('does not match an Anthropic thinking block for a Bedrock (reasoning_content) run', () => {
|
|
3684
|
-
// The cross-type fallback is one-directional: REASONING_CONTENT (Bedrock)
|
|
3685
|
-
// must not match a `thinking` block, since the Bedrock input converter
|
|
3686
|
-
// rejects `thinking` blocks and reattaching one would break the request.
|
|
3687
|
-
const tokenCounter = createTestTokenCounter();
|
|
3688
|
-
const messages: BaseMessage[] = [
|
|
3689
|
-
new SystemMessage('you are a helpful assistant'),
|
|
3690
|
-
new AIMessage({
|
|
3691
|
-
content: [
|
|
3692
|
-
{
|
|
3693
|
-
type: ContentTypes.THINKING,
|
|
3694
|
-
thinking: 'inherited Anthropic-style reasoning',
|
|
3695
|
-
signature: 'sig-anthropic',
|
|
3696
|
-
},
|
|
3697
|
-
{
|
|
3698
|
-
type: 'tool_use',
|
|
3699
|
-
id: 'tc_get_doc',
|
|
3700
|
-
name: 'get_doc_content',
|
|
3701
|
-
input: { docId: 'abc' },
|
|
3702
|
-
},
|
|
3703
|
-
],
|
|
3704
|
-
tool_calls: [
|
|
3705
|
-
{
|
|
3706
|
-
id: 'tc_get_doc',
|
|
3707
|
-
name: 'get_doc_content',
|
|
3708
|
-
args: { docId: 'abc' },
|
|
3709
|
-
type: 'tool_call',
|
|
3710
|
-
},
|
|
3711
|
-
],
|
|
3712
|
-
}),
|
|
3713
|
-
new ToolMessage({
|
|
3714
|
-
content: 'c'.repeat(6000),
|
|
3715
|
-
tool_call_id: 'tc_get_doc',
|
|
3716
|
-
name: 'get_doc_content',
|
|
3717
|
-
}),
|
|
3718
|
-
];
|
|
3719
|
-
|
|
3720
|
-
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
3721
|
-
for (let i = 0; i < messages.length; i++) {
|
|
3722
|
-
indexTokenCountMap[i] = tokenCounter(messages[i]);
|
|
3723
|
-
}
|
|
3724
|
-
|
|
3725
|
-
let result: ReturnType<typeof realGetMessagesWithinTokenLimit> | undefined;
|
|
3726
|
-
expect(() => {
|
|
3727
|
-
result = realGetMessagesWithinTokenLimit({
|
|
3728
|
-
messages,
|
|
3729
|
-
maxContextTokens: 200,
|
|
3730
|
-
indexTokenCountMap,
|
|
3731
|
-
thinkingEnabled: true,
|
|
3732
|
-
tokenCounter,
|
|
3733
|
-
reasoningType: ContentTypes.REASONING_CONTENT,
|
|
3734
|
-
});
|
|
3735
|
-
}).not.toThrow();
|
|
3736
|
-
|
|
3737
|
-
// The thinking block is intentionally not located for a Bedrock run, so no
|
|
3738
|
-
// index is reported and nothing gets reattached.
|
|
3739
|
-
expect(result!.thinkingStartIndex).toBeUndefined();
|
|
3740
|
-
});
|
|
3741
|
-
});
|