@illuma-ai/agents 1.0.90 → 1.0.94
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 +98 -49
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/constants.cjs +25 -0
- package/dist/cjs/common/constants.cjs.map +1 -0
- package/dist/cjs/common/enum.cjs +30 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/events.cjs +9 -4
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +397 -92
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +223 -92
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +30 -14
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/index.cjs +43 -11
- 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 +10 -7
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +32 -0
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/tools.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +129 -101
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +489 -0
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -0
- package/dist/cjs/llm/bedrock/utils/message_outputs.cjs +176 -0
- package/dist/cjs/llm/bedrock/utils/message_outputs.cjs.map +1 -0
- package/dist/cjs/llm/fake.cjs.map +1 -1
- package/dist/cjs/llm/google/index.cjs.map +1 -1
- package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +1 -1
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +59 -5
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/llm/providers.cjs.map +1 -1
- package/dist/cjs/llm/text.cjs.map +1 -1
- package/dist/cjs/llm/vertexai/index.cjs +80 -2
- package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +60 -27
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +131 -108
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/content.cjs.map +1 -1
- package/dist/cjs/messages/core.cjs +3 -0
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +265 -47
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/ids.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +55 -2
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/messages/summarize.cjs +170 -0
- package/dist/cjs/messages/summarize.cjs.map +1 -0
- package/dist/cjs/messages/tools.cjs.map +1 -1
- package/dist/cjs/run.cjs +87 -30
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/schemas/validate.cjs.map +1 -1
- package/dist/cjs/splitStream.cjs.map +1 -1
- package/dist/cjs/stream.cjs +59 -25
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/AskUser.cjs +131 -0
- package/dist/cjs/tools/AskUser.cjs.map +1 -0
- package/dist/cjs/tools/BrowserTools.cjs +11 -7
- package/dist/cjs/tools/BrowserTools.cjs.map +1 -1
- package/dist/cjs/tools/Calculator.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +60 -5
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +36 -53
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/StreamingToolCallBuffer.cjs +208 -0
- package/dist/cjs/tools/StreamingToolCallBuffer.cjs.map +1 -0
- package/dist/cjs/tools/ToolNode.cjs +333 -30
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +66 -30
- package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
- package/dist/cjs/tools/handlers.cjs +94 -8
- package/dist/cjs/tools/handlers.cjs.map +1 -1
- package/dist/cjs/tools/schema.cjs.map +1 -1
- package/dist/cjs/tools/search/content.cjs.map +1 -1
- package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
- package/dist/cjs/tools/search/format.cjs.map +1 -1
- package/dist/cjs/tools/search/highlights.cjs.map +1 -1
- package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
- package/dist/cjs/tools/search/schema.cjs.map +1 -1
- package/dist/cjs/tools/search/search.cjs +1 -0
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/serper-scraper.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/search/utils.cjs.map +1 -1
- package/dist/cjs/types/graph.cjs +1 -1
- package/dist/cjs/types/graph.cjs.map +1 -1
- package/dist/cjs/utils/contextAnalytics.cjs +23 -6
- package/dist/cjs/utils/contextAnalytics.cjs.map +1 -1
- package/dist/cjs/utils/events.cjs.map +1 -1
- package/dist/cjs/utils/graph.cjs.map +1 -1
- package/dist/cjs/utils/handlers.cjs.map +1 -1
- package/dist/cjs/utils/llm.cjs.map +1 -1
- package/dist/cjs/utils/misc.cjs.map +1 -1
- package/dist/cjs/utils/run.cjs +3 -1
- package/dist/cjs/utils/run.cjs.map +1 -1
- package/dist/cjs/utils/schema.cjs.map +1 -1
- package/dist/cjs/utils/title.cjs.map +1 -1
- package/dist/cjs/utils/tokens.cjs +33 -58
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/cjs/utils/toolCallContinuation.cjs +55 -0
- package/dist/cjs/utils/toolCallContinuation.cjs.map +1 -0
- package/dist/cjs/utils/toonFormat.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +98 -49
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/constants.mjs +22 -0
- package/dist/esm/common/constants.mjs.map +1 -0
- package/dist/esm/common/enum.mjs +31 -1
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/events.mjs +9 -4
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +393 -88
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +224 -93
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +30 -14
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +43 -11
- 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 +10 -7
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs +32 -0
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/tools.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +128 -101
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +484 -0
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -0
- package/dist/esm/llm/bedrock/utils/message_outputs.mjs +171 -0
- package/dist/esm/llm/bedrock/utils/message_outputs.mjs.map +1 -0
- package/dist/esm/llm/fake.mjs.map +1 -1
- package/dist/esm/llm/google/index.mjs.map +1 -1
- package/dist/esm/llm/google/utils/common.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +1 -1
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +59 -5
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/llm/providers.mjs.map +1 -1
- package/dist/esm/llm/text.mjs.map +1 -1
- package/dist/esm/llm/vertexai/index.mjs +80 -2
- package/dist/esm/llm/vertexai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +8 -3
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +131 -108
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/content.mjs.map +1 -1
- package/dist/esm/messages/core.mjs +4 -1
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +267 -49
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/ids.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +56 -4
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/messages/summarize.mjs +161 -0
- package/dist/esm/messages/summarize.mjs.map +1 -0
- package/dist/esm/messages/tools.mjs.map +1 -1
- package/dist/esm/run.mjs +88 -31
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/schemas/validate.mjs.map +1 -1
- package/dist/esm/splitStream.mjs.map +1 -1
- package/dist/esm/stream.mjs +60 -26
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/AskUser.mjs +125 -0
- package/dist/esm/tools/AskUser.mjs.map +1 -0
- package/dist/esm/tools/BrowserTools.mjs +11 -7
- package/dist/esm/tools/BrowserTools.mjs.map +1 -1
- package/dist/esm/tools/Calculator.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +60 -5
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +37 -54
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/StreamingToolCallBuffer.mjs +206 -0
- package/dist/esm/tools/StreamingToolCallBuffer.mjs.map +1 -0
- package/dist/esm/tools/ToolNode.mjs +333 -30
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +66 -30
- package/dist/esm/tools/ToolSearch.mjs.map +1 -1
- package/dist/esm/tools/handlers.mjs +95 -9
- package/dist/esm/tools/handlers.mjs.map +1 -1
- package/dist/esm/tools/schema.mjs.map +1 -1
- package/dist/esm/tools/search/content.mjs.map +1 -1
- package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
- package/dist/esm/tools/search/format.mjs.map +1 -1
- package/dist/esm/tools/search/highlights.mjs.map +1 -1
- package/dist/esm/tools/search/rerankers.mjs.map +1 -1
- package/dist/esm/tools/search/schema.mjs.map +1 -1
- package/dist/esm/tools/search/search.mjs +1 -0
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/serper-scraper.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/search/utils.mjs.map +1 -1
- package/dist/esm/types/graph.mjs +1 -1
- package/dist/esm/types/graph.mjs.map +1 -1
- package/dist/esm/utils/contextAnalytics.mjs +23 -6
- package/dist/esm/utils/contextAnalytics.mjs.map +1 -1
- package/dist/esm/utils/events.mjs.map +1 -1
- package/dist/esm/utils/graph.mjs.map +1 -1
- package/dist/esm/utils/handlers.mjs.map +1 -1
- package/dist/esm/utils/llm.mjs.map +1 -1
- package/dist/esm/utils/misc.mjs.map +1 -1
- package/dist/esm/utils/run.mjs +3 -1
- package/dist/esm/utils/run.mjs.map +1 -1
- package/dist/esm/utils/schema.mjs.map +1 -1
- package/dist/esm/utils/title.mjs.map +1 -1
- package/dist/esm/utils/tokens.mjs +33 -59
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/esm/utils/toolCallContinuation.mjs +52 -0
- package/dist/esm/utils/toolCallContinuation.mjs.map +1 -0
- package/dist/esm/utils/toonFormat.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +14 -7
- package/dist/types/common/constants.d.ts +18 -0
- package/dist/types/common/enum.d.ts +28 -0
- package/dist/types/common/index.d.ts +1 -0
- package/dist/types/events.d.ts +10 -3
- package/dist/types/graphs/Graph.d.ts +37 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/llm/anthropic/index.d.ts +7 -1
- package/dist/types/llm/anthropic/types.d.ts +5 -2
- package/dist/types/llm/anthropic/utils/message_outputs.d.ts +1 -1
- package/dist/types/llm/bedrock/index.d.ts +40 -33
- package/dist/types/llm/bedrock/utils/message_outputs.d.ts +1 -1
- package/dist/types/llm/google/index.d.ts +2 -3
- package/dist/types/llm/openrouter/index.d.ts +21 -1
- package/dist/types/llm/vertexai/index.d.ts +3 -2
- package/dist/types/messages/cache.d.ts +1 -1
- package/dist/types/messages/index.d.ts +1 -0
- package/dist/types/messages/prune.d.ts +2 -7
- package/dist/types/messages/summarize.d.ts +33 -0
- package/dist/types/run.d.ts +6 -0
- package/dist/types/tools/AskUser.d.ts +408 -0
- package/dist/types/tools/BrowserTools.d.ts +2 -2
- package/dist/types/tools/CodeExecutor.d.ts +28 -4
- package/dist/types/tools/StreamingToolCallBuffer.d.ts +106 -0
- package/dist/types/tools/ToolNode.d.ts +55 -3
- package/dist/types/tools/ToolSearch.d.ts +9 -5
- package/dist/types/tools/handlers.d.ts +2 -2
- package/dist/types/types/graph.d.ts +9 -2
- package/dist/types/types/llm.d.ts +8 -3
- package/dist/types/types/run.d.ts +2 -0
- package/dist/types/types/tools.d.ts +20 -2
- package/dist/types/utils/contextAnalytics.d.ts +5 -4
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/tokens.d.ts +6 -19
- package/dist/types/utils/toolCallContinuation.d.ts +30 -0
- package/package.json +15 -8
- package/src/agents/AgentContext.js +782 -0
- package/src/agents/AgentContext.js.map +1 -0
- package/src/agents/AgentContext.test.js +421 -0
- package/src/agents/AgentContext.test.js.map +1 -0
- package/src/agents/AgentContext.ts +132 -64
- package/src/agents/__tests__/AgentContext.test.js +678 -0
- package/src/agents/__tests__/AgentContext.test.js.map +1 -0
- package/src/agents/__tests__/AgentContext.test.ts +25 -4
- package/src/agents/__tests__/resolveStructuredOutputMode.test.js +117 -0
- package/src/agents/__tests__/resolveStructuredOutputMode.test.js.map +1 -0
- package/src/common/__tests__/enum.test.ts +135 -0
- package/src/common/constants.ts +21 -0
- package/src/common/enum.js +192 -0
- package/src/common/enum.js.map +1 -0
- package/src/common/enum.ts +30 -0
- package/src/common/index.js +3 -0
- package/src/common/index.js.map +1 -0
- package/src/common/index.ts +2 -1
- package/src/events.js +166 -0
- package/src/events.js.map +1 -0
- package/src/events.ts +11 -14
- package/src/graphs/Graph.js +1857 -0
- package/src/graphs/Graph.js.map +1 -0
- package/src/graphs/Graph.ts +580 -162
- package/src/graphs/MultiAgentGraph.js +1092 -0
- package/src/graphs/MultiAgentGraph.js.map +1 -0
- package/src/graphs/MultiAgentGraph.ts +331 -112
- package/src/graphs/__tests__/adaptive-thinking.test.ts +369 -0
- package/src/graphs/__tests__/graph-direct-tool-names.test.ts +210 -0
- package/src/graphs/__tests__/multi-agent-edges.test.ts +237 -0
- package/src/graphs/__tests__/structured-output.integration.test.js +624 -0
- package/src/graphs/__tests__/structured-output.integration.test.js.map +1 -0
- package/src/graphs/__tests__/structured-output.test.js +144 -0
- package/src/graphs/__tests__/structured-output.test.js.map +1 -0
- package/src/graphs/contextManagement.e2e.test.js +718 -0
- package/src/graphs/contextManagement.e2e.test.js.map +1 -0
- package/src/graphs/contextManagement.e2e.test.ts +990 -0
- package/src/graphs/contextManagement.test.js +485 -0
- package/src/graphs/contextManagement.test.js.map +1 -0
- package/src/graphs/contextManagement.test.ts +625 -0
- package/src/graphs/handoffValidation.test.js +276 -0
- package/src/graphs/handoffValidation.test.js.map +1 -0
- package/src/graphs/handoffValidation.test.ts +353 -0
- package/src/graphs/index.js +3 -0
- package/src/graphs/index.js.map +1 -0
- package/src/index.js +28 -0
- package/src/index.js.map +1 -0
- package/src/index.ts +13 -0
- package/src/instrumentation.js +21 -0
- package/src/instrumentation.js.map +1 -0
- package/src/instrumentation.ts +38 -17
- package/src/llm/anthropic/index.js +319 -0
- package/src/llm/anthropic/index.js.map +1 -0
- package/src/llm/anthropic/index.ts +68 -15
- package/src/llm/anthropic/llm.spec.ts +402 -0
- package/src/llm/anthropic/types.js +46 -0
- package/src/llm/anthropic/types.js.map +1 -0
- package/src/llm/anthropic/types.ts +8 -2
- package/src/llm/anthropic/utils/message_inputs.js +627 -0
- package/src/llm/anthropic/utils/message_inputs.js.map +1 -0
- package/src/llm/anthropic/utils/message_inputs.ts +16 -33
- package/src/llm/anthropic/utils/message_outputs.js +290 -0
- package/src/llm/anthropic/utils/message_outputs.js.map +1 -0
- package/src/llm/anthropic/utils/message_outputs.ts +40 -1
- package/src/llm/anthropic/utils/output_parsers.js +89 -0
- package/src/llm/anthropic/utils/output_parsers.js.map +1 -0
- package/src/llm/anthropic/utils/tools.js +25 -0
- package/src/llm/anthropic/utils/tools.js.map +1 -0
- package/src/llm/bedrock/__tests__/bedrock-caching.test.js +392 -0
- package/src/llm/bedrock/__tests__/bedrock-caching.test.js.map +1 -0
- package/src/llm/bedrock/__tests__/bedrock-caching.test.ts +24 -40
- package/src/llm/bedrock/index.js +303 -0
- package/src/llm/bedrock/index.js.map +1 -0
- package/src/llm/bedrock/index.ts +171 -134
- package/src/llm/bedrock/llm.spec.ts +395 -52
- package/src/llm/bedrock/types.js +2 -0
- package/src/llm/bedrock/types.js.map +1 -0
- package/src/llm/bedrock/utils/index.js +6 -0
- package/src/llm/bedrock/utils/index.js.map +1 -0
- package/src/llm/bedrock/utils/message_inputs.js +463 -0
- package/src/llm/bedrock/utils/message_inputs.js.map +1 -0
- package/src/llm/bedrock/utils/message_inputs.ts +30 -5
- package/src/llm/bedrock/utils/message_outputs.js +269 -0
- package/src/llm/bedrock/utils/message_outputs.js.map +1 -0
- package/src/llm/bedrock/utils/message_outputs.ts +70 -22
- package/src/llm/fake.js +92 -0
- package/src/llm/fake.js.map +1 -0
- package/src/llm/google/index.js +215 -0
- package/src/llm/google/index.js.map +1 -0
- package/src/llm/google/index.ts +2 -3
- package/src/llm/google/types.js +12 -0
- package/src/llm/google/types.js.map +1 -0
- package/src/llm/google/utils/common.js +670 -0
- package/src/llm/google/utils/common.js.map +1 -0
- package/src/llm/google/utils/tools.js +111 -0
- package/src/llm/google/utils/tools.js.map +1 -0
- package/src/llm/google/utils/zod_to_genai_parameters.js +47 -0
- package/src/llm/google/utils/zod_to_genai_parameters.js.map +1 -0
- package/src/llm/openai/index.js +1033 -0
- package/src/llm/openai/index.js.map +1 -0
- package/src/llm/openai/types.js +2 -0
- package/src/llm/openai/types.js.map +1 -0
- package/src/llm/openai/utils/index.js +756 -0
- package/src/llm/openai/utils/index.js.map +1 -0
- package/src/llm/openai/utils/isReasoningModel.test.js +79 -0
- package/src/llm/openai/utils/isReasoningModel.test.js.map +1 -0
- package/src/llm/openrouter/index.js +261 -0
- package/src/llm/openrouter/index.js.map +1 -0
- package/src/llm/openrouter/index.ts +117 -6
- package/src/llm/openrouter/reasoning.test.js +181 -0
- package/src/llm/openrouter/reasoning.test.js.map +1 -0
- package/src/llm/openrouter/reasoning.test.ts +207 -0
- package/src/llm/providers.js +36 -0
- package/src/llm/providers.js.map +1 -0
- package/src/llm/text.js +65 -0
- package/src/llm/text.js.map +1 -0
- package/src/llm/vertexai/index.js +402 -0
- package/src/llm/vertexai/index.js.map +1 -0
- package/src/llm/vertexai/index.ts +115 -5
- package/src/llm/vertexai/llm.spec.ts +114 -0
- package/src/messages/__tests__/tools.test.js +392 -0
- package/src/messages/__tests__/tools.test.js.map +1 -0
- package/src/messages/cache.js +404 -0
- package/src/messages/cache.js.map +1 -0
- package/src/messages/cache.test.js +1167 -0
- package/src/messages/cache.test.js.map +1 -0
- package/src/messages/cache.test.ts +178 -16
- package/src/messages/cache.ts +152 -147
- package/src/messages/content.js +48 -0
- package/src/messages/content.js.map +1 -0
- package/src/messages/content.test.js +314 -0
- package/src/messages/content.test.js.map +1 -0
- package/src/messages/core.js +359 -0
- package/src/messages/core.js.map +1 -0
- package/src/messages/core.ts +5 -0
- package/src/messages/ensureThinkingBlock.test.js +997 -0
- package/src/messages/ensureThinkingBlock.test.js.map +1 -0
- package/src/messages/ensureThinkingBlock.test.ts +751 -10
- package/src/messages/format.js +973 -0
- package/src/messages/format.js.map +1 -0
- package/src/messages/format.ts +334 -57
- package/src/messages/formatAgentMessages.test.js +2278 -0
- package/src/messages/formatAgentMessages.test.js.map +1 -0
- package/src/messages/formatAgentMessages.test.ts +1175 -1
- package/src/messages/formatAgentMessages.tools.test.js +362 -0
- package/src/messages/formatAgentMessages.tools.test.js.map +1 -0
- package/src/messages/formatMessage.test.js +608 -0
- package/src/messages/formatMessage.test.js.map +1 -0
- package/src/messages/ids.js +18 -0
- package/src/messages/ids.js.map +1 -0
- package/src/messages/index.js +9 -0
- package/src/messages/index.js.map +1 -0
- package/src/messages/index.ts +1 -0
- package/src/messages/labelContentByAgent.test.js +725 -0
- package/src/messages/labelContentByAgent.test.js.map +1 -0
- package/src/messages/prune.js +438 -0
- package/src/messages/prune.js.map +1 -0
- package/src/messages/prune.ts +87 -25
- package/src/messages/reducer.js +60 -0
- package/src/messages/reducer.js.map +1 -0
- package/src/messages/shiftIndexTokenCountMap.test.js +63 -0
- package/src/messages/shiftIndexTokenCountMap.test.js.map +1 -0
- package/src/messages/summarize.js +146 -0
- package/src/messages/summarize.js.map +1 -0
- package/src/messages/summarize.test.js +332 -0
- package/src/messages/summarize.test.js.map +1 -0
- package/src/messages/summarize.test.ts +466 -0
- package/src/messages/summarize.ts +222 -0
- package/src/messages/tools.js +90 -0
- package/src/messages/tools.js.map +1 -0
- package/src/mockStream.js +81 -0
- package/src/mockStream.js.map +1 -0
- package/src/prompts/collab.js +7 -0
- package/src/prompts/collab.js.map +1 -0
- package/src/prompts/index.js +3 -0
- package/src/prompts/index.js.map +1 -0
- package/src/prompts/taskmanager.js +58 -0
- package/src/prompts/taskmanager.js.map +1 -0
- package/src/run.js +427 -0
- package/src/run.js.map +1 -0
- package/src/run.ts +101 -33
- package/src/schemas/index.js +3 -0
- package/src/schemas/index.js.map +1 -0
- package/src/schemas/schema-preparation.test.js +370 -0
- package/src/schemas/schema-preparation.test.js.map +1 -0
- package/src/schemas/validate.js +314 -0
- package/src/schemas/validate.js.map +1 -0
- package/src/schemas/validate.test.js +264 -0
- package/src/schemas/validate.test.js.map +1 -0
- package/src/scripts/abort.js +127 -0
- package/src/scripts/abort.js.map +1 -0
- package/src/scripts/ant_web_search.js +130 -0
- package/src/scripts/ant_web_search.js.map +1 -0
- package/src/scripts/ant_web_search.ts +1 -0
- package/src/scripts/ant_web_search_edge_case.js +133 -0
- package/src/scripts/ant_web_search_edge_case.js.map +1 -0
- package/src/scripts/ant_web_search_edge_case.ts +1 -0
- package/src/scripts/ant_web_search_error_edge_case.js +119 -0
- package/src/scripts/ant_web_search_error_edge_case.js.map +1 -0
- package/src/scripts/ant_web_search_error_edge_case.ts +1 -0
- package/src/scripts/args.js +41 -0
- package/src/scripts/args.js.map +1 -0
- package/src/scripts/bedrock-cache-debug.js +186 -0
- package/src/scripts/bedrock-cache-debug.js.map +1 -0
- package/src/scripts/bedrock-cache-debug.ts +250 -0
- package/src/scripts/bedrock-content-aggregation-test.js +195 -0
- package/src/scripts/bedrock-content-aggregation-test.js.map +1 -0
- package/src/scripts/bedrock-content-aggregation-test.ts +266 -0
- package/src/scripts/bedrock-merge-test.js +80 -0
- package/src/scripts/bedrock-merge-test.js.map +1 -0
- package/src/scripts/bedrock-merge-test.ts +107 -0
- package/src/scripts/bedrock-parallel-tools-test.js +150 -0
- package/src/scripts/bedrock-parallel-tools-test.js.map +1 -0
- package/src/scripts/bedrock-parallel-tools-test.ts +204 -0
- package/src/scripts/caching.js +106 -0
- package/src/scripts/caching.js.map +1 -0
- package/src/scripts/caching.ts +1 -0
- package/src/scripts/cli.js +152 -0
- package/src/scripts/cli.js.map +1 -0
- package/src/scripts/cli2.js +119 -0
- package/src/scripts/cli2.js.map +1 -0
- package/src/scripts/cli3.js +163 -0
- package/src/scripts/cli3.js.map +1 -0
- package/src/scripts/cli4.js +165 -0
- package/src/scripts/cli4.js.map +1 -0
- package/src/scripts/cli5.js +165 -0
- package/src/scripts/cli5.js.map +1 -0
- package/src/scripts/code_exec.js +171 -0
- package/src/scripts/code_exec.js.map +1 -0
- package/src/scripts/code_exec.ts +1 -0
- package/src/scripts/code_exec_files.js +180 -0
- package/src/scripts/code_exec_files.js.map +1 -0
- package/src/scripts/code_exec_files.ts +1 -0
- package/src/scripts/code_exec_multi_session.js +185 -0
- package/src/scripts/code_exec_multi_session.js.map +1 -0
- package/src/scripts/code_exec_multi_session.ts +9 -13
- package/src/scripts/code_exec_ptc.js +265 -0
- package/src/scripts/code_exec_ptc.js.map +1 -0
- package/src/scripts/code_exec_ptc.ts +1 -0
- package/src/scripts/code_exec_session.js +217 -0
- package/src/scripts/code_exec_session.js.map +1 -0
- package/src/scripts/code_exec_session.ts +1 -0
- package/src/scripts/code_exec_simple.js +120 -0
- package/src/scripts/code_exec_simple.js.map +1 -0
- package/src/scripts/code_exec_simple.ts +1 -0
- package/src/scripts/content.js +111 -0
- package/src/scripts/content.js.map +1 -0
- package/src/scripts/content.ts +1 -0
- package/src/scripts/empty_input.js +125 -0
- package/src/scripts/empty_input.js.map +1 -0
- package/src/scripts/handoff-test.js +96 -0
- package/src/scripts/handoff-test.js.map +1 -0
- package/src/scripts/image.js +138 -0
- package/src/scripts/image.js.map +1 -0
- package/src/scripts/image.ts +3 -1
- package/src/scripts/memory.js +83 -0
- package/src/scripts/memory.js.map +1 -0
- package/src/scripts/memory.ts +16 -6
- package/src/scripts/multi-agent-chain.js +271 -0
- package/src/scripts/multi-agent-chain.js.map +1 -0
- package/src/scripts/multi-agent-chain.ts +1 -0
- package/src/scripts/multi-agent-conditional.js +185 -0
- package/src/scripts/multi-agent-conditional.js.map +1 -0
- package/src/scripts/multi-agent-conditional.ts +1 -0
- package/src/scripts/multi-agent-document-review-chain.js +171 -0
- package/src/scripts/multi-agent-document-review-chain.js.map +1 -0
- package/src/scripts/multi-agent-document-review-chain.ts +1 -0
- package/src/scripts/multi-agent-hybrid-flow.js +264 -0
- package/src/scripts/multi-agent-hybrid-flow.js.map +1 -0
- package/src/scripts/multi-agent-hybrid-flow.ts +1 -0
- package/src/scripts/multi-agent-parallel-start.js +214 -0
- package/src/scripts/multi-agent-parallel-start.js.map +1 -0
- package/src/scripts/multi-agent-parallel-start.ts +4 -4
- package/src/scripts/multi-agent-parallel.js +346 -0
- package/src/scripts/multi-agent-parallel.js.map +1 -0
- package/src/scripts/multi-agent-parallel.ts +1 -0
- package/src/scripts/multi-agent-sequence.js +184 -0
- package/src/scripts/multi-agent-sequence.js.map +1 -0
- package/src/scripts/multi-agent-sequence.ts +4 -4
- package/src/scripts/multi-agent-supervisor.js +324 -0
- package/src/scripts/multi-agent-supervisor.js.map +1 -0
- package/src/scripts/multi-agent-supervisor.ts +1 -0
- package/src/scripts/multi-agent-test.js +147 -0
- package/src/scripts/multi-agent-test.js.map +1 -0
- package/src/scripts/multi-agent-test.ts +1 -0
- package/src/scripts/parallel-asymmetric-tools-test.js +202 -0
- package/src/scripts/parallel-asymmetric-tools-test.js.map +1 -0
- package/src/scripts/parallel-asymmetric-tools-test.ts +1 -0
- package/src/scripts/parallel-full-metadata-test.js +176 -0
- package/src/scripts/parallel-full-metadata-test.js.map +1 -0
- package/src/scripts/parallel-full-metadata-test.ts +1 -0
- package/src/scripts/parallel-tools-test.js +256 -0
- package/src/scripts/parallel-tools-test.js.map +1 -0
- package/src/scripts/parallel-tools-test.ts +1 -0
- package/src/scripts/poc-multi-agent-comprehensive.ts +1222 -0
- package/src/scripts/programmatic_exec.js +277 -0
- package/src/scripts/programmatic_exec.js.map +1 -0
- package/src/scripts/programmatic_exec_agent.js +168 -0
- package/src/scripts/programmatic_exec_agent.js.map +1 -0
- package/src/scripts/programmatic_exec_agent.ts +1 -0
- package/src/scripts/search.js +118 -0
- package/src/scripts/search.js.map +1 -0
- package/src/scripts/search.ts +1 -0
- package/src/scripts/sequential-full-metadata-test.js +143 -0
- package/src/scripts/sequential-full-metadata-test.js.map +1 -0
- package/src/scripts/sequential-full-metadata-test.ts +1 -0
- package/src/scripts/simple.js +174 -0
- package/src/scripts/simple.js.map +1 -0
- package/src/scripts/simple.ts +2 -1
- package/src/scripts/single-agent-metadata-test.js +152 -0
- package/src/scripts/single-agent-metadata-test.js.map +1 -0
- package/src/scripts/single-agent-metadata-test.ts +4 -6
- package/src/scripts/stream.js +113 -0
- package/src/scripts/stream.js.map +1 -0
- package/src/scripts/stream.ts +1 -0
- package/src/scripts/test-custom-prompt-key.js +132 -0
- package/src/scripts/test-custom-prompt-key.js.map +1 -0
- package/src/scripts/test-handoff-input.js +143 -0
- package/src/scripts/test-handoff-input.js.map +1 -0
- package/src/scripts/test-handoff-preamble.js +227 -0
- package/src/scripts/test-handoff-preamble.js.map +1 -0
- package/src/scripts/test-handoff-preamble.ts +1 -0
- package/src/scripts/test-handoff-steering.js +353 -0
- package/src/scripts/test-handoff-steering.js.map +1 -0
- package/src/scripts/test-handoff-steering.ts +430 -0
- package/src/scripts/test-multi-agent-list-handoff.js +318 -0
- package/src/scripts/test-multi-agent-list-handoff.js.map +1 -0
- package/src/scripts/test-multi-agent-list-handoff.ts +1 -0
- package/src/scripts/test-parallel-agent-labeling.js +253 -0
- package/src/scripts/test-parallel-agent-labeling.js.map +1 -0
- package/src/scripts/test-parallel-agent-labeling.ts +2 -0
- package/src/scripts/test-parallel-handoffs.js +229 -0
- package/src/scripts/test-parallel-handoffs.js.map +1 -0
- package/src/scripts/test-parallel-handoffs.ts +1 -0
- package/src/scripts/test-thinking-handoff-bedrock.js +132 -0
- package/src/scripts/test-thinking-handoff-bedrock.js.map +1 -0
- package/src/scripts/test-thinking-handoff-bedrock.ts +1 -0
- package/src/scripts/test-thinking-handoff.js +132 -0
- package/src/scripts/test-thinking-handoff.js.map +1 -0
- package/src/scripts/test-thinking-handoff.ts +1 -0
- package/src/scripts/test-thinking-to-thinking-handoff-bedrock.js +140 -0
- package/src/scripts/test-thinking-to-thinking-handoff-bedrock.js.map +1 -0
- package/src/scripts/test-thinking-to-thinking-handoff-bedrock.ts +166 -0
- package/src/scripts/test-tool-before-handoff-role-order.js +223 -0
- package/src/scripts/test-tool-before-handoff-role-order.js.map +1 -0
- package/src/scripts/test-tool-before-handoff-role-order.ts +276 -0
- package/src/scripts/test-tools-before-handoff.js +187 -0
- package/src/scripts/test-tools-before-handoff.js.map +1 -0
- package/src/scripts/test-tools-before-handoff.ts +4 -8
- package/src/scripts/test_code_api.js +263 -0
- package/src/scripts/test_code_api.js.map +1 -0
- package/src/scripts/thinking-bedrock.js +128 -0
- package/src/scripts/thinking-bedrock.js.map +1 -0
- package/src/scripts/thinking-bedrock.ts +1 -0
- package/src/scripts/thinking-vertexai.js +130 -0
- package/src/scripts/thinking-vertexai.js.map +1 -0
- package/src/scripts/thinking-vertexai.ts +168 -0
- package/src/scripts/thinking.js +134 -0
- package/src/scripts/thinking.js.map +1 -0
- package/src/scripts/thinking.ts +1 -0
- package/src/scripts/tool_search.js +114 -0
- package/src/scripts/tool_search.js.map +1 -0
- package/src/scripts/tools.js +125 -0
- package/src/scripts/tools.js.map +1 -0
- package/src/scripts/tools.ts +5 -19
- package/src/specs/agent-handoffs-bedrock.integration.test.js +280 -0
- package/src/specs/agent-handoffs-bedrock.integration.test.js.map +1 -0
- package/src/specs/agent-handoffs-bedrock.integration.test.ts +412 -375
- package/src/specs/agent-handoffs.test.js +924 -0
- package/src/specs/agent-handoffs.test.js.map +1 -0
- package/src/specs/agent-handoffs.test.ts +152 -39
- package/src/specs/anthropic.simple.test.js +287 -0
- package/src/specs/anthropic.simple.test.js.map +1 -0
- package/src/specs/anthropic.simple.test.ts +7 -4
- package/src/specs/azure.simple.test.js +381 -0
- package/src/specs/azure.simple.test.js.map +1 -0
- package/src/specs/azure.simple.test.ts +143 -5
- package/src/specs/cache.simple.test.js +282 -0
- package/src/specs/cache.simple.test.js.map +1 -0
- package/src/specs/cache.simple.test.ts +9 -2
- package/src/specs/custom-event-await.test.js +148 -0
- package/src/specs/custom-event-await.test.js.map +1 -0
- package/src/specs/custom-event-await.test.ts +215 -0
- package/src/specs/deepseek.simple.test.js +189 -0
- package/src/specs/deepseek.simple.test.js.map +1 -0
- package/src/specs/deepseek.simple.test.ts +4 -2
- package/src/specs/emergency-prune.test.js +308 -0
- package/src/specs/emergency-prune.test.js.map +1 -0
- package/src/specs/moonshot.simple.test.js +237 -0
- package/src/specs/moonshot.simple.test.js.map +1 -0
- package/src/specs/moonshot.simple.test.ts +6 -2
- package/src/specs/observability.integration.test.js +1337 -0
- package/src/specs/observability.integration.test.js.map +1 -0
- package/src/specs/observability.integration.test.ts +2223 -0
- package/src/specs/openai.simple.test.js +233 -0
- package/src/specs/openai.simple.test.js.map +1 -0
- package/src/specs/openai.simple.test.ts +4 -2
- package/src/specs/openrouter.simple.test.js +202 -0
- package/src/specs/openrouter.simple.test.js.map +1 -0
- package/src/specs/openrouter.simple.test.ts +165 -4
- package/src/specs/prune.test.js +733 -0
- package/src/specs/prune.test.js.map +1 -0
- package/src/specs/prune.test.ts +1 -0
- package/src/specs/reasoning.test.js +144 -0
- package/src/specs/reasoning.test.js.map +1 -0
- package/src/specs/reasoning.test.ts +2 -2
- package/src/specs/spec.utils.js +4 -0
- package/src/specs/spec.utils.js.map +1 -0
- package/src/specs/thinking-handoff.test.js +486 -0
- package/src/specs/thinking-handoff.test.js.map +1 -0
- package/src/specs/thinking-handoff.test.ts +3 -2
- package/src/specs/thinking-prune.test.js +600 -0
- package/src/specs/thinking-prune.test.js.map +1 -0
- package/src/specs/token-distribution-edge-case.test.js +246 -0
- package/src/specs/token-distribution-edge-case.test.js.map +1 -0
- package/src/specs/token-memoization.test.js +32 -0
- package/src/specs/token-memoization.test.js.map +1 -0
- package/src/specs/token-memoization.test.ts +14 -5
- package/src/specs/tokens.test.js +49 -0
- package/src/specs/tokens.test.js.map +1 -0
- package/src/specs/tokens.test.ts +64 -0
- package/src/specs/tool-error.test.js +139 -0
- package/src/specs/tool-error.test.js.map +1 -0
- package/src/specs/tool-error.test.ts +2 -2
- package/src/splitStream.js +204 -0
- package/src/splitStream.js.map +1 -0
- package/src/splitStream.test.js +504 -0
- package/src/splitStream.test.js.map +1 -0
- package/src/stream.js +650 -0
- package/src/stream.js.map +1 -0
- package/src/stream.test.js +225 -0
- package/src/stream.test.js.map +1 -0
- package/src/stream.test.ts +25 -15
- package/src/stream.ts +82 -32
- package/src/test/mockTools.js +340 -0
- package/src/test/mockTools.js.map +1 -0
- package/src/tools/AskUser.ts +159 -0
- package/src/tools/BrowserTools.js +245 -0
- package/src/tools/BrowserTools.js.map +1 -0
- package/src/tools/BrowserTools.ts +12 -8
- package/src/tools/Calculator.js +38 -0
- package/src/tools/Calculator.js.map +1 -0
- package/src/tools/Calculator.test.js +225 -0
- package/src/tools/Calculator.test.js.map +1 -0
- package/src/tools/CodeExecutor.js +233 -0
- package/src/tools/CodeExecutor.js.map +1 -0
- package/src/tools/CodeExecutor.selfhealing.test.ts +435 -0
- package/src/tools/CodeExecutor.ts +82 -5
- package/src/tools/ProgrammaticToolCalling.js +602 -0
- package/src/tools/ProgrammaticToolCalling.js.map +1 -0
- package/src/tools/ProgrammaticToolCalling.ts +40 -52
- package/src/tools/StreamingToolCallBuffer.js +179 -0
- package/src/tools/StreamingToolCallBuffer.js.map +1 -0
- package/src/tools/StreamingToolCallBuffer.ts +218 -0
- package/src/tools/ToolNode.js +930 -0
- package/src/tools/ToolNode.js.map +1 -0
- package/src/tools/ToolNode.ts +454 -41
- package/src/tools/ToolSearch.js +904 -0
- package/src/tools/ToolSearch.js.map +1 -0
- package/src/tools/ToolSearch.ts +84 -33
- package/src/tools/__tests__/AskUser.test.ts +537 -0
- package/src/tools/__tests__/BrowserTools.test.js +306 -0
- package/src/tools/__tests__/BrowserTools.test.js.map +1 -0
- package/src/tools/__tests__/BrowserTools.test.ts +131 -6
- package/src/tools/__tests__/CodeExecutor.test.ts +76 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.js +276 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.js.map +1 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.test.js +807 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.test.js.map +1 -0
- package/src/tools/__tests__/StreamingToolCallBuffer.test.js +175 -0
- package/src/tools/__tests__/StreamingToolCallBuffer.test.js.map +1 -0
- package/src/tools/__tests__/StreamingToolCallBuffer.test.ts +263 -0
- package/src/tools/__tests__/ToolApproval.test.js +675 -0
- package/src/tools/__tests__/ToolApproval.test.js.map +1 -0
- package/src/tools/__tests__/ToolApproval.test.ts +194 -20
- package/src/tools/__tests__/ToolNode.hitl.test.ts +267 -0
- package/src/tools/__tests__/ToolNode.recovery.test.js +200 -0
- package/src/tools/__tests__/ToolNode.recovery.test.js.map +1 -0
- package/src/tools/__tests__/ToolNode.recovery.test.ts +276 -0
- package/src/tools/__tests__/ToolNode.session.test.js +319 -0
- package/src/tools/__tests__/ToolNode.session.test.js.map +1 -0
- package/src/tools/__tests__/ToolNode.session.test.ts +465 -0
- package/src/tools/__tests__/ToolSearch.integration.test.js +125 -0
- package/src/tools/__tests__/ToolSearch.integration.test.js.map +1 -0
- package/src/tools/__tests__/ToolSearch.test.js +812 -0
- package/src/tools/__tests__/ToolSearch.test.js.map +1 -0
- package/src/tools/__tests__/ToolSearch.test.ts +78 -5
- package/src/tools/__tests__/handlers.test.js +799 -0
- package/src/tools/__tests__/handlers.test.js.map +1 -0
- package/src/tools/__tests__/handlers.test.ts +1100 -0
- package/src/tools/__tests__/truncation-recovery.integration.test.js +362 -0
- package/src/tools/__tests__/truncation-recovery.integration.test.js.map +1 -0
- package/src/tools/__tests__/truncation-recovery.integration.test.ts +560 -0
- package/src/tools/handlers.js +306 -0
- package/src/tools/handlers.js.map +1 -0
- package/src/tools/handlers.ts +119 -16
- package/src/tools/schema.js +25 -0
- package/src/tools/schema.js.map +1 -0
- package/src/tools/search/anthropic.js +34 -0
- package/src/tools/search/anthropic.js.map +1 -0
- package/src/tools/search/content.js +116 -0
- package/src/tools/search/content.js.map +1 -0
- package/src/tools/search/content.test.js +133 -0
- package/src/tools/search/content.test.js.map +1 -0
- package/src/tools/search/firecrawl.js +173 -0
- package/src/tools/search/firecrawl.js.map +1 -0
- package/src/tools/search/format.js +198 -0
- package/src/tools/search/format.js.map +1 -0
- package/src/tools/search/highlights.js +241 -0
- package/src/tools/search/highlights.js.map +1 -0
- package/src/tools/search/index.js +3 -0
- package/src/tools/search/index.js.map +1 -0
- package/src/tools/search/jina-reranker.test.js +106 -0
- package/src/tools/search/jina-reranker.test.js.map +1 -0
- package/src/tools/search/rerankers.js +165 -0
- package/src/tools/search/rerankers.js.map +1 -0
- package/src/tools/search/schema.js +102 -0
- package/src/tools/search/schema.js.map +1 -0
- package/src/tools/search/search.js +561 -0
- package/src/tools/search/search.js.map +1 -0
- package/src/tools/search/serper-scraper.js +126 -0
- package/src/tools/search/serper-scraper.js.map +1 -0
- package/src/tools/search/test.js +129 -0
- package/src/tools/search/test.js.map +1 -0
- package/src/tools/search/tool.js +453 -0
- package/src/tools/search/tool.js.map +1 -0
- package/src/tools/search/types.js +2 -0
- package/src/tools/search/types.js.map +1 -0
- package/src/tools/search/utils.js +59 -0
- package/src/tools/search/utils.js.map +1 -0
- package/src/types/graph.js +24 -0
- package/src/types/graph.js.map +1 -0
- package/src/types/graph.test.js +192 -0
- package/src/types/graph.test.js.map +1 -0
- package/src/types/graph.ts +26 -6
- package/src/types/index.js +7 -0
- package/src/types/index.js.map +1 -0
- package/src/types/llm.js +2 -0
- package/src/types/llm.js.map +1 -0
- package/src/types/llm.ts +8 -3
- package/src/types/messages.js +2 -0
- package/src/types/messages.js.map +1 -0
- package/src/types/run.js +2 -0
- package/src/types/run.js.map +1 -0
- package/src/types/run.ts +2 -0
- package/src/types/stream.js +2 -0
- package/src/types/stream.js.map +1 -0
- package/src/types/tools.js +2 -0
- package/src/types/tools.js.map +1 -0
- package/src/types/tools.ts +21 -2
- package/src/utils/contextAnalytics.js +79 -0
- package/src/utils/contextAnalytics.js.map +1 -0
- package/src/utils/contextAnalytics.test.js +166 -0
- package/src/utils/contextAnalytics.test.js.map +1 -0
- package/src/utils/contextAnalytics.test.ts +222 -0
- package/src/utils/contextAnalytics.ts +27 -9
- package/src/utils/events.js +26 -0
- package/src/utils/events.js.map +1 -0
- package/src/utils/graph.js +11 -0
- package/src/utils/graph.js.map +1 -0
- package/src/utils/handlers.js +65 -0
- package/src/utils/handlers.js.map +1 -0
- package/src/utils/index.js +10 -0
- package/src/utils/index.js.map +1 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/llm.js +21 -0
- package/src/utils/llm.js.map +1 -0
- package/src/utils/llmConfig.js +205 -0
- package/src/utils/llmConfig.js.map +1 -0
- package/src/utils/llmConfig.ts +5 -5
- package/src/utils/logging.js +37 -0
- package/src/utils/logging.js.map +1 -0
- package/src/utils/misc.js +51 -0
- package/src/utils/misc.js.map +1 -0
- package/src/utils/run.js +69 -0
- package/src/utils/run.js.map +1 -0
- package/src/utils/run.ts +108 -106
- package/src/utils/schema.js +21 -0
- package/src/utils/schema.js.map +1 -0
- package/src/utils/title.js +119 -0
- package/src/utils/title.js.map +1 -0
- package/src/utils/tokens.js +92 -0
- package/src/utils/tokens.js.map +1 -0
- package/src/utils/tokens.ts +118 -142
- package/src/utils/toolCallContinuation.ts +55 -0
- package/src/utils/toonFormat.js +379 -0
- package/src/utils/toonFormat.js.map +1 -0
|
@@ -0,0 +1,2223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for Illuma Observability SDK integration in agents.
|
|
3
|
+
*
|
|
4
|
+
* These tests verify that the ObservabilityCallbackHandler and IllumaSpanProcessor
|
|
5
|
+
* correctly route LangChain/OTel events to trace payloads with proper structure,
|
|
6
|
+
* parent-child relationships, and token usage metrics.
|
|
7
|
+
*
|
|
8
|
+
* Uses a mock client to capture enqueued events without hitting a real server.
|
|
9
|
+
*/
|
|
10
|
+
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-unused-vars, @typescript-eslint/no-require-imports, import/no-unresolved */
|
|
11
|
+
|
|
12
|
+
import { ObservabilityCallbackHandler } from '@illuma-ai/observability-langchain';
|
|
13
|
+
import type { Serialized } from '@langchain/core/load/serializable';
|
|
14
|
+
import type { LLMResult } from '@langchain/core/outputs';
|
|
15
|
+
import type { Document } from '@langchain/core/documents';
|
|
16
|
+
|
|
17
|
+
/** Minimal IngestionEvent type for our mock (avoids importing core) */
|
|
18
|
+
interface IngestionEvent {
|
|
19
|
+
id: string;
|
|
20
|
+
type: string;
|
|
21
|
+
timestamp: string;
|
|
22
|
+
body: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Mock Client - captures enqueued events for assertions
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
class MockObservabilityClient {
|
|
30
|
+
events: IngestionEvent[] = [];
|
|
31
|
+
flushed = false;
|
|
32
|
+
shutdownCalled = false;
|
|
33
|
+
|
|
34
|
+
enqueue(event: IngestionEvent): void {
|
|
35
|
+
this.events.push(event);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async flush(): Promise<void> {
|
|
39
|
+
this.flushed = true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async shutdown(): Promise<void> {
|
|
43
|
+
this.shutdownCalled = true;
|
|
44
|
+
await this.flush();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Get events filtered by type */
|
|
48
|
+
getByType(type: string): IngestionEvent[] {
|
|
49
|
+
return this.events.filter((e) => e.type === type);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Get all event types in order */
|
|
53
|
+
getEventTypes(): string[] {
|
|
54
|
+
return this.events.map((e) => e.type);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Pretty-print all events for debugging */
|
|
58
|
+
printEvents(): void {
|
|
59
|
+
for (const event of this.events) {
|
|
60
|
+
const body = event.body as Record<string, unknown>;
|
|
61
|
+
const usage = body.usage as Record<string, unknown> | undefined;
|
|
62
|
+
console.log(
|
|
63
|
+
` ${event.type} | name=${body.name ?? '—'} | id=${body.id ?? '—'} | traceId=${body.traceId ?? '—'} | parentObsId=${body.parentObservationId ?? '—'}` +
|
|
64
|
+
(usage
|
|
65
|
+
? ` | usage={prompt:${usage.promptTokens ?? '—'},completion:${usage.completionTokens ?? '—'},total:${usage.totalTokens ?? '—'}}`
|
|
66
|
+
: '') +
|
|
67
|
+
(body.model ? ` | model=${body.model}` : '') +
|
|
68
|
+
(body.provider ? ` | provider=${body.provider}` : '') +
|
|
69
|
+
(body.level === 'ERROR' ? ` | ERROR: ${body.statusMessage}` : '')
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Helpers
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
const serialized = (name: string): Serialized => ({
|
|
80
|
+
lc: 1,
|
|
81
|
+
type: 'not_implemented',
|
|
82
|
+
id: ['langchain', name],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
function makeUUID(): string {
|
|
86
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
87
|
+
const r = (Math.random() * 16) | 0;
|
|
88
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
89
|
+
return v.toString(16);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Tests
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
describe('ObservabilityCallbackHandler integration', () => {
|
|
98
|
+
let client: MockObservabilityClient;
|
|
99
|
+
let handler: ObservabilityCallbackHandler;
|
|
100
|
+
|
|
101
|
+
beforeEach(() => {
|
|
102
|
+
client = new MockObservabilityClient();
|
|
103
|
+
handler = new ObservabilityCallbackHandler({
|
|
104
|
+
client,
|
|
105
|
+
traceName: 'test-agent-run',
|
|
106
|
+
userId: 'user-42',
|
|
107
|
+
sessionId: 'session-abc',
|
|
108
|
+
tags: ['integration-test'],
|
|
109
|
+
metadata: { messageId: 'msg-001' },
|
|
110
|
+
environment: 'test',
|
|
111
|
+
debug: false,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// -----------------------------------------------------------------------
|
|
116
|
+
// 1. Root trace creation
|
|
117
|
+
// -----------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
describe('root trace creation', () => {
|
|
120
|
+
it('should create a trace on the first chain start', async () => {
|
|
121
|
+
const chainRunId = makeUUID();
|
|
122
|
+
await handler.handleChainStart(
|
|
123
|
+
serialized('RunnableSequence'),
|
|
124
|
+
{ input: 'Hello agent' },
|
|
125
|
+
chainRunId
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const traceEvents = client.getByType('trace-create');
|
|
129
|
+
expect(traceEvents).toHaveLength(1);
|
|
130
|
+
|
|
131
|
+
const trace = traceEvents[0].body as Record<string, unknown>;
|
|
132
|
+
expect(trace.name).toBe('test-agent-run');
|
|
133
|
+
expect(trace.userId).toBe('user-42');
|
|
134
|
+
expect(trace.sessionId).toBe('session-abc');
|
|
135
|
+
expect(trace.tags).toEqual(['integration-test']);
|
|
136
|
+
expect(trace.metadata).toEqual({ messageId: 'msg-001' });
|
|
137
|
+
expect(trace.environment).toBe('test');
|
|
138
|
+
expect(trace.input).toEqual({ input: 'Hello agent' });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should not create a second trace for nested chains', async () => {
|
|
142
|
+
const outerRunId = makeUUID();
|
|
143
|
+
const innerRunId = makeUUID();
|
|
144
|
+
|
|
145
|
+
await handler.handleChainStart(
|
|
146
|
+
serialized('RunnableSequence'),
|
|
147
|
+
{ input: 'outer' },
|
|
148
|
+
outerRunId
|
|
149
|
+
);
|
|
150
|
+
await handler.handleChainStart(
|
|
151
|
+
serialized('ChatPromptTemplate'),
|
|
152
|
+
{ input: 'inner' },
|
|
153
|
+
innerRunId,
|
|
154
|
+
outerRunId
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const traceEvents = client.getByType('trace-create');
|
|
158
|
+
expect(traceEvents).toHaveLength(1); // Only one trace, not two
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should return the trace ID after creation', async () => {
|
|
162
|
+
expect(handler.getTraceId()).toBeNull();
|
|
163
|
+
|
|
164
|
+
await handler.handleChainStart(
|
|
165
|
+
serialized('RunnableSequence'),
|
|
166
|
+
{ input: 'test' },
|
|
167
|
+
makeUUID()
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
expect(handler.getTraceId()).toBeTruthy();
|
|
171
|
+
expect(typeof handler.getTraceId()).toBe('string');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// -----------------------------------------------------------------------
|
|
176
|
+
// 2. LLM generation events
|
|
177
|
+
// -----------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
describe('LLM generation tracing', () => {
|
|
180
|
+
it('should create generation-create with input prompts', async () => {
|
|
181
|
+
const chainRunId = makeUUID();
|
|
182
|
+
const llmRunId = makeUUID();
|
|
183
|
+
|
|
184
|
+
await handler.handleChainStart(
|
|
185
|
+
serialized('RunnableSequence'),
|
|
186
|
+
{ input: 'test' },
|
|
187
|
+
chainRunId
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
await handler.handleLLMStart(
|
|
191
|
+
serialized('ChatAnthropic'),
|
|
192
|
+
['You are a helpful assistant.\n\nHuman: Hello'],
|
|
193
|
+
llmRunId,
|
|
194
|
+
chainRunId,
|
|
195
|
+
undefined,
|
|
196
|
+
undefined,
|
|
197
|
+
{ ls_model_name: 'claude-sonnet-4-20250514', ls_provider: 'anthropic' }
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const genEvents = client.getByType('generation-create');
|
|
201
|
+
expect(genEvents).toHaveLength(1);
|
|
202
|
+
|
|
203
|
+
const gen = genEvents[0].body as Record<string, unknown>;
|
|
204
|
+
expect(gen.name).toBe('ChatAnthropic');
|
|
205
|
+
expect(gen.model).toBe('claude-sonnet-4-20250514');
|
|
206
|
+
expect(gen.provider).toBe('anthropic');
|
|
207
|
+
expect(gen.input).toEqual([
|
|
208
|
+
'You are a helpful assistant.\n\nHuman: Hello',
|
|
209
|
+
]);
|
|
210
|
+
expect(gen.traceId).toBe(handler.getTraceId());
|
|
211
|
+
|
|
212
|
+
// Parent should be the chain
|
|
213
|
+
const chainEvents = client.getByType('chain-create');
|
|
214
|
+
const chainObsId = (chainEvents[0].body as Record<string, unknown>).id;
|
|
215
|
+
expect(gen.parentObservationId).toBe(chainObsId);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should create generation-update with token usage on LLM end', async () => {
|
|
219
|
+
const chainRunId = makeUUID();
|
|
220
|
+
const llmRunId = makeUUID();
|
|
221
|
+
|
|
222
|
+
await handler.handleChainStart(
|
|
223
|
+
serialized('RunnableSequence'),
|
|
224
|
+
{ input: 'test' },
|
|
225
|
+
chainRunId
|
|
226
|
+
);
|
|
227
|
+
await handler.handleLLMStart(
|
|
228
|
+
serialized('ChatOpenAI'),
|
|
229
|
+
['Hello'],
|
|
230
|
+
llmRunId,
|
|
231
|
+
chainRunId
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const llmResult: LLMResult = {
|
|
235
|
+
generations: [
|
|
236
|
+
[{ text: 'Hi there! How can I help?', generationInfo: {} }],
|
|
237
|
+
],
|
|
238
|
+
llmOutput: {
|
|
239
|
+
tokenUsage: {
|
|
240
|
+
promptTokens: 150,
|
|
241
|
+
completionTokens: 42,
|
|
242
|
+
totalTokens: 192,
|
|
243
|
+
},
|
|
244
|
+
modelName: 'gpt-4o',
|
|
245
|
+
provider: 'openai',
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
await handler.handleLLMEnd(llmResult, llmRunId);
|
|
250
|
+
|
|
251
|
+
const genUpdates = client.getByType('generation-update');
|
|
252
|
+
expect(genUpdates).toHaveLength(1);
|
|
253
|
+
|
|
254
|
+
const update = genUpdates[0].body as Record<string, unknown>;
|
|
255
|
+
expect(update.output).toBe('Hi there! How can I help?');
|
|
256
|
+
expect(update.model).toBe('gpt-4o');
|
|
257
|
+
expect(update.provider).toBe('openai');
|
|
258
|
+
expect(update.endTime).toBeDefined();
|
|
259
|
+
|
|
260
|
+
const usage = update.usage as Record<string, unknown>;
|
|
261
|
+
expect(usage.promptTokens).toBe(150);
|
|
262
|
+
expect(usage.completionTokens).toBe(42);
|
|
263
|
+
expect(usage.totalTokens).toBe(192);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should handle LLM errors with ERROR level', async () => {
|
|
267
|
+
const chainRunId = makeUUID();
|
|
268
|
+
const llmRunId = makeUUID();
|
|
269
|
+
|
|
270
|
+
await handler.handleChainStart(
|
|
271
|
+
serialized('RunnableSequence'),
|
|
272
|
+
{ input: 'test' },
|
|
273
|
+
chainRunId
|
|
274
|
+
);
|
|
275
|
+
await handler.handleLLMStart(
|
|
276
|
+
serialized('ChatAnthropic'),
|
|
277
|
+
['Hello'],
|
|
278
|
+
llmRunId,
|
|
279
|
+
chainRunId
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
await handler.handleLLMError(new Error('Rate limit exceeded'), llmRunId);
|
|
283
|
+
|
|
284
|
+
const genUpdates = client.getByType('generation-update');
|
|
285
|
+
expect(genUpdates).toHaveLength(1);
|
|
286
|
+
|
|
287
|
+
const update = genUpdates[0].body as Record<string, unknown>;
|
|
288
|
+
expect(update.level).toBe('ERROR');
|
|
289
|
+
expect(update.statusMessage).toBe('Rate limit exceeded');
|
|
290
|
+
expect(update.endTime).toBeDefined();
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// -----------------------------------------------------------------------
|
|
295
|
+
// 3. Chain events
|
|
296
|
+
// -----------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
describe('chain tracing', () => {
|
|
299
|
+
it('should create chain-create events with input', async () => {
|
|
300
|
+
const chainRunId = makeUUID();
|
|
301
|
+
await handler.handleChainStart(
|
|
302
|
+
serialized('RunnableSequence'),
|
|
303
|
+
{ query: 'What is AI?' },
|
|
304
|
+
chainRunId
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const chainEvents = client.getByType('chain-create');
|
|
308
|
+
expect(chainEvents).toHaveLength(1);
|
|
309
|
+
|
|
310
|
+
const chain = chainEvents[0].body as Record<string, unknown>;
|
|
311
|
+
expect(chain.name).toBe('RunnableSequence');
|
|
312
|
+
expect(chain.input).toEqual({ query: 'What is AI?' });
|
|
313
|
+
expect(chain.traceId).toBe(handler.getTraceId());
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should create span-update on chain end with output', async () => {
|
|
317
|
+
const chainRunId = makeUUID();
|
|
318
|
+
await handler.handleChainStart(
|
|
319
|
+
serialized('RunnableSequence'),
|
|
320
|
+
{ input: 'test' },
|
|
321
|
+
chainRunId
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
await handler.handleChainEnd(
|
|
325
|
+
{ output: 'AI is artificial intelligence.' },
|
|
326
|
+
chainRunId
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const spanUpdates = client.getByType('span-update');
|
|
330
|
+
expect(spanUpdates).toHaveLength(1);
|
|
331
|
+
|
|
332
|
+
const update = spanUpdates[0].body as Record<string, unknown>;
|
|
333
|
+
expect(update.output).toEqual({
|
|
334
|
+
output: 'AI is artificial intelligence.',
|
|
335
|
+
});
|
|
336
|
+
expect(update.endTime).toBeDefined();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should handle chain error with ERROR level', async () => {
|
|
340
|
+
const chainRunId = makeUUID();
|
|
341
|
+
await handler.handleChainStart(
|
|
342
|
+
serialized('RunnableSequence'),
|
|
343
|
+
{ input: 'test' },
|
|
344
|
+
chainRunId
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
await handler.handleChainError(
|
|
348
|
+
new Error('Chain failed: missing tool'),
|
|
349
|
+
chainRunId
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const spanUpdates = client.getByType('span-update');
|
|
353
|
+
const errorUpdate = spanUpdates.find(
|
|
354
|
+
(e) => (e.body as Record<string, unknown>).level === 'ERROR'
|
|
355
|
+
);
|
|
356
|
+
expect(errorUpdate).toBeDefined();
|
|
357
|
+
expect((errorUpdate!.body as Record<string, unknown>).statusMessage).toBe(
|
|
358
|
+
'Chain failed: missing tool'
|
|
359
|
+
);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// -----------------------------------------------------------------------
|
|
364
|
+
// 4. Tool events
|
|
365
|
+
// -----------------------------------------------------------------------
|
|
366
|
+
|
|
367
|
+
describe('tool tracing', () => {
|
|
368
|
+
it('should create tool-create events', async () => {
|
|
369
|
+
const chainRunId = makeUUID();
|
|
370
|
+
const toolRunId = makeUUID();
|
|
371
|
+
|
|
372
|
+
await handler.handleChainStart(
|
|
373
|
+
serialized('RunnableSequence'),
|
|
374
|
+
{ input: 'test' },
|
|
375
|
+
chainRunId
|
|
376
|
+
);
|
|
377
|
+
await handler.handleToolStart(
|
|
378
|
+
serialized('web_search'),
|
|
379
|
+
'{"query": "latest AI news"}',
|
|
380
|
+
toolRunId,
|
|
381
|
+
chainRunId
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
const toolEvents = client.getByType('tool-create');
|
|
385
|
+
expect(toolEvents).toHaveLength(1);
|
|
386
|
+
|
|
387
|
+
const tool = toolEvents[0].body as Record<string, unknown>;
|
|
388
|
+
expect(tool.name).toBe('Tool: web_search');
|
|
389
|
+
expect(tool.input).toBe('{"query": "latest AI news"}');
|
|
390
|
+
expect(tool.traceId).toBe(handler.getTraceId());
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('should create span-update on tool end', async () => {
|
|
394
|
+
const chainRunId = makeUUID();
|
|
395
|
+
const toolRunId = makeUUID();
|
|
396
|
+
|
|
397
|
+
await handler.handleChainStart(
|
|
398
|
+
serialized('RunnableSequence'),
|
|
399
|
+
{ input: 'test' },
|
|
400
|
+
chainRunId
|
|
401
|
+
);
|
|
402
|
+
await handler.handleToolStart(
|
|
403
|
+
serialized('calculator'),
|
|
404
|
+
'{"expression": "2+2"}',
|
|
405
|
+
toolRunId,
|
|
406
|
+
chainRunId
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
await handler.handleToolEnd('4', toolRunId);
|
|
410
|
+
|
|
411
|
+
const spanUpdates = client.getByType('span-update');
|
|
412
|
+
expect(spanUpdates).toHaveLength(1);
|
|
413
|
+
expect((spanUpdates[0].body as Record<string, unknown>).output).toBe('4');
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('should handle tool error', async () => {
|
|
417
|
+
const chainRunId = makeUUID();
|
|
418
|
+
const toolRunId = makeUUID();
|
|
419
|
+
|
|
420
|
+
await handler.handleChainStart(
|
|
421
|
+
serialized('RunnableSequence'),
|
|
422
|
+
{ input: 'test' },
|
|
423
|
+
chainRunId
|
|
424
|
+
);
|
|
425
|
+
await handler.handleToolStart(
|
|
426
|
+
serialized('api_call'),
|
|
427
|
+
'{"url": "https://example.com"}',
|
|
428
|
+
toolRunId,
|
|
429
|
+
chainRunId
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
await handler.handleToolError(new Error('Connection timeout'), toolRunId);
|
|
433
|
+
|
|
434
|
+
const spanUpdates = client.getByType('span-update');
|
|
435
|
+
const errorUpdate = spanUpdates.find(
|
|
436
|
+
(e) => (e.body as Record<string, unknown>).level === 'ERROR'
|
|
437
|
+
);
|
|
438
|
+
expect(errorUpdate).toBeDefined();
|
|
439
|
+
expect((errorUpdate!.body as Record<string, unknown>).statusMessage).toBe(
|
|
440
|
+
'Connection timeout'
|
|
441
|
+
);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// -----------------------------------------------------------------------
|
|
446
|
+
// 5. Retriever events
|
|
447
|
+
// -----------------------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
describe('retriever tracing', () => {
|
|
450
|
+
it('should create retriever-create events', async () => {
|
|
451
|
+
const chainRunId = makeUUID();
|
|
452
|
+
const retrieverRunId = makeUUID();
|
|
453
|
+
|
|
454
|
+
await handler.handleChainStart(
|
|
455
|
+
serialized('RunnableSequence'),
|
|
456
|
+
{ input: 'test' },
|
|
457
|
+
chainRunId
|
|
458
|
+
);
|
|
459
|
+
await handler.handleRetrieverStart(
|
|
460
|
+
serialized('VectorStoreRetriever'),
|
|
461
|
+
'How does photosynthesis work?',
|
|
462
|
+
retrieverRunId,
|
|
463
|
+
chainRunId
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
const retrieverEvents = client.getByType('retriever-create');
|
|
467
|
+
expect(retrieverEvents).toHaveLength(1);
|
|
468
|
+
|
|
469
|
+
const retriever = retrieverEvents[0].body as Record<string, unknown>;
|
|
470
|
+
expect(retriever.name).toBe('Retriever: VectorStoreRetriever');
|
|
471
|
+
expect(retriever.input).toBe('How does photosynthesis work?');
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('should produce span-update with documents on retriever end', async () => {
|
|
475
|
+
const chainRunId = makeUUID();
|
|
476
|
+
const retrieverRunId = makeUUID();
|
|
477
|
+
|
|
478
|
+
await handler.handleChainStart(
|
|
479
|
+
serialized('RunnableSequence'),
|
|
480
|
+
{ input: 'test' },
|
|
481
|
+
chainRunId
|
|
482
|
+
);
|
|
483
|
+
await handler.handleRetrieverStart(
|
|
484
|
+
serialized('VectorStoreRetriever'),
|
|
485
|
+
'photosynthesis',
|
|
486
|
+
retrieverRunId,
|
|
487
|
+
chainRunId
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
const documents: Document[] = [
|
|
491
|
+
{
|
|
492
|
+
pageContent: 'Photosynthesis is the process...',
|
|
493
|
+
metadata: { source: 'wiki', page: 1 },
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
pageContent: 'Plants use chlorophyll...',
|
|
497
|
+
metadata: { source: 'textbook', page: 42 },
|
|
498
|
+
},
|
|
499
|
+
];
|
|
500
|
+
|
|
501
|
+
await handler.handleRetrieverEnd(documents, retrieverRunId);
|
|
502
|
+
|
|
503
|
+
const spanUpdates = client.getByType('span-update');
|
|
504
|
+
expect(spanUpdates).toHaveLength(1);
|
|
505
|
+
|
|
506
|
+
const update = spanUpdates[0].body as Record<string, unknown>;
|
|
507
|
+
const output = update.output as Array<Record<string, unknown>>;
|
|
508
|
+
expect(output).toHaveLength(2);
|
|
509
|
+
expect(output[0].pageContent).toBe('Photosynthesis is the process...');
|
|
510
|
+
expect(output[0].metadata).toEqual({ source: 'wiki', page: 1 });
|
|
511
|
+
|
|
512
|
+
const meta = update.metadata as Record<string, unknown>;
|
|
513
|
+
expect(meta.documentCount).toBe(2);
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// -----------------------------------------------------------------------
|
|
518
|
+
// 6. Parent-child hierarchy
|
|
519
|
+
// -----------------------------------------------------------------------
|
|
520
|
+
|
|
521
|
+
describe('parent-child hierarchy', () => {
|
|
522
|
+
it('should correctly link nested observations to parent', async () => {
|
|
523
|
+
const outerChainId = makeUUID();
|
|
524
|
+
const innerChainId = makeUUID();
|
|
525
|
+
const llmRunId = makeUUID();
|
|
526
|
+
const toolRunId = makeUUID();
|
|
527
|
+
|
|
528
|
+
// Outer chain starts
|
|
529
|
+
await handler.handleChainStart(
|
|
530
|
+
serialized('RunnableSequence'),
|
|
531
|
+
{ input: 'orchestrate' },
|
|
532
|
+
outerChainId
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
// Inner chain nested under outer
|
|
536
|
+
await handler.handleChainStart(
|
|
537
|
+
serialized('ChatPromptTemplate'),
|
|
538
|
+
{ input: 'format prompt' },
|
|
539
|
+
innerChainId,
|
|
540
|
+
outerChainId
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
// LLM nested under inner chain
|
|
544
|
+
await handler.handleLLMStart(
|
|
545
|
+
serialized('ChatOpenAI'),
|
|
546
|
+
['formatted prompt'],
|
|
547
|
+
llmRunId,
|
|
548
|
+
innerChainId
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
// Tool nested under outer chain
|
|
552
|
+
await handler.handleToolStart(
|
|
553
|
+
serialized('calculator'),
|
|
554
|
+
'2+2',
|
|
555
|
+
toolRunId,
|
|
556
|
+
outerChainId
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
// Verify hierarchy
|
|
560
|
+
const chainCreates = client.getByType('chain-create');
|
|
561
|
+
const genCreates = client.getByType('generation-create');
|
|
562
|
+
const toolCreates = client.getByType('tool-create');
|
|
563
|
+
|
|
564
|
+
// All share the same traceId
|
|
565
|
+
const traceId = handler.getTraceId();
|
|
566
|
+
expect((chainCreates[0].body as Record<string, unknown>).traceId).toBe(
|
|
567
|
+
traceId
|
|
568
|
+
);
|
|
569
|
+
expect((chainCreates[1].body as Record<string, unknown>).traceId).toBe(
|
|
570
|
+
traceId
|
|
571
|
+
);
|
|
572
|
+
expect((genCreates[0].body as Record<string, unknown>).traceId).toBe(
|
|
573
|
+
traceId
|
|
574
|
+
);
|
|
575
|
+
expect((toolCreates[0].body as Record<string, unknown>).traceId).toBe(
|
|
576
|
+
traceId
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
// Inner chain -> parent is outer chain's observationId
|
|
580
|
+
const outerChainObsId = (chainCreates[0].body as Record<string, unknown>)
|
|
581
|
+
.id;
|
|
582
|
+
const innerChainObsId = (chainCreates[1].body as Record<string, unknown>)
|
|
583
|
+
.id;
|
|
584
|
+
expect(
|
|
585
|
+
(chainCreates[1].body as Record<string, unknown>).parentObservationId
|
|
586
|
+
).toBe(outerChainObsId);
|
|
587
|
+
|
|
588
|
+
// LLM -> parent is inner chain
|
|
589
|
+
expect(
|
|
590
|
+
(genCreates[0].body as Record<string, unknown>).parentObservationId
|
|
591
|
+
).toBe(innerChainObsId);
|
|
592
|
+
|
|
593
|
+
// Tool -> parent is outer chain
|
|
594
|
+
expect(
|
|
595
|
+
(toolCreates[0].body as Record<string, unknown>).parentObservationId
|
|
596
|
+
).toBe(outerChainObsId);
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// -----------------------------------------------------------------------
|
|
601
|
+
// 7. Full agent pipeline simulation
|
|
602
|
+
// -----------------------------------------------------------------------
|
|
603
|
+
|
|
604
|
+
describe('full agent pipeline simulation', () => {
|
|
605
|
+
it('should produce correct event sequence for a RAG agent with tools', async () => {
|
|
606
|
+
const chainRunId = makeUUID();
|
|
607
|
+
const agentChainId = makeUUID();
|
|
608
|
+
const retrieverRunId = makeUUID();
|
|
609
|
+
const llmRunId1 = makeUUID();
|
|
610
|
+
const toolRunId = makeUUID();
|
|
611
|
+
const llmRunId2 = makeUUID();
|
|
612
|
+
|
|
613
|
+
// 1. Root chain starts (the main graph)
|
|
614
|
+
await handler.handleChainStart(
|
|
615
|
+
serialized('CompiledStateGraph'),
|
|
616
|
+
{
|
|
617
|
+
messages: [
|
|
618
|
+
{
|
|
619
|
+
role: 'user',
|
|
620
|
+
content: 'What papers did OpenAI publish in 2024?',
|
|
621
|
+
},
|
|
622
|
+
],
|
|
623
|
+
},
|
|
624
|
+
chainRunId
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
// 2. Agent chain starts (nested)
|
|
628
|
+
await handler.handleChainStart(
|
|
629
|
+
serialized('AgentExecutor'),
|
|
630
|
+
{ input: 'What papers did OpenAI publish in 2024?' },
|
|
631
|
+
agentChainId,
|
|
632
|
+
chainRunId
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
// 3. First LLM call (decides to use tool)
|
|
636
|
+
await handler.handleLLMStart(
|
|
637
|
+
serialized('ChatAnthropic'),
|
|
638
|
+
['System: You are a research assistant.\nHuman: What papers...'],
|
|
639
|
+
llmRunId1,
|
|
640
|
+
agentChainId,
|
|
641
|
+
undefined,
|
|
642
|
+
undefined,
|
|
643
|
+
{ ls_model_name: 'claude-sonnet-4-20250514', ls_provider: 'anthropic' }
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
await handler.handleLLMEnd(
|
|
647
|
+
{
|
|
648
|
+
generations: [
|
|
649
|
+
[
|
|
650
|
+
{
|
|
651
|
+
text: "I'll search for OpenAI papers from 2024.",
|
|
652
|
+
generationInfo: {},
|
|
653
|
+
},
|
|
654
|
+
],
|
|
655
|
+
],
|
|
656
|
+
llmOutput: {
|
|
657
|
+
tokenUsage: {
|
|
658
|
+
promptTokens: 200,
|
|
659
|
+
completionTokens: 30,
|
|
660
|
+
totalTokens: 230,
|
|
661
|
+
},
|
|
662
|
+
modelName: 'claude-sonnet-4-20250514',
|
|
663
|
+
provider: 'anthropic',
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
llmRunId1
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
// 4. Retriever runs
|
|
670
|
+
await handler.handleRetrieverStart(
|
|
671
|
+
serialized('VectorStoreRetriever'),
|
|
672
|
+
'OpenAI papers 2024',
|
|
673
|
+
retrieverRunId,
|
|
674
|
+
agentChainId
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
await handler.handleRetrieverEnd(
|
|
678
|
+
[
|
|
679
|
+
{
|
|
680
|
+
pageContent: 'GPT-4o: omni-model for text, vision, audio...',
|
|
681
|
+
metadata: { source: 'arxiv', year: 2024 },
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
pageContent: 'Sora: text-to-video generation model...',
|
|
685
|
+
metadata: { source: 'blog', year: 2024 },
|
|
686
|
+
},
|
|
687
|
+
] as Document[],
|
|
688
|
+
retrieverRunId
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
// 5. Tool call (web search)
|
|
692
|
+
await handler.handleToolStart(
|
|
693
|
+
serialized('web_search'),
|
|
694
|
+
'{"query": "OpenAI 2024 research papers list"}',
|
|
695
|
+
toolRunId,
|
|
696
|
+
agentChainId
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
await handler.handleToolEnd(
|
|
700
|
+
JSON.stringify([
|
|
701
|
+
{ title: 'GPT-4o Technical Report', url: 'https://arxiv.org/...' },
|
|
702
|
+
{ title: 'Sora', url: 'https://openai.com/sora' },
|
|
703
|
+
]),
|
|
704
|
+
toolRunId
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
// 6. Second LLM call (final answer)
|
|
708
|
+
await handler.handleLLMStart(
|
|
709
|
+
serialized('ChatAnthropic'),
|
|
710
|
+
['System: ...\nContext: ...\nHuman: What papers...'],
|
|
711
|
+
llmRunId2,
|
|
712
|
+
agentChainId,
|
|
713
|
+
undefined,
|
|
714
|
+
undefined,
|
|
715
|
+
{ ls_model_name: 'claude-sonnet-4-20250514', ls_provider: 'anthropic' }
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
await handler.handleLLMEnd(
|
|
719
|
+
{
|
|
720
|
+
generations: [
|
|
721
|
+
[
|
|
722
|
+
{
|
|
723
|
+
text: 'OpenAI published several notable papers in 2024 including GPT-4o and Sora...',
|
|
724
|
+
generationInfo: {},
|
|
725
|
+
},
|
|
726
|
+
],
|
|
727
|
+
],
|
|
728
|
+
llmOutput: {
|
|
729
|
+
tokenUsage: {
|
|
730
|
+
promptTokens: 800,
|
|
731
|
+
completionTokens: 150,
|
|
732
|
+
totalTokens: 950,
|
|
733
|
+
},
|
|
734
|
+
modelName: 'claude-sonnet-4-20250514',
|
|
735
|
+
provider: 'anthropic',
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
llmRunId2
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
// 7. Chains end
|
|
742
|
+
await handler.handleChainEnd(
|
|
743
|
+
{ output: 'OpenAI published several notable papers...' },
|
|
744
|
+
agentChainId
|
|
745
|
+
);
|
|
746
|
+
await handler.handleChainEnd(
|
|
747
|
+
{
|
|
748
|
+
messages: [
|
|
749
|
+
{
|
|
750
|
+
role: 'assistant',
|
|
751
|
+
content: 'OpenAI published several notable papers...',
|
|
752
|
+
},
|
|
753
|
+
],
|
|
754
|
+
},
|
|
755
|
+
chainRunId
|
|
756
|
+
);
|
|
757
|
+
|
|
758
|
+
// Flush
|
|
759
|
+
await handler.flushAsync();
|
|
760
|
+
expect(client.flushed).toBe(true);
|
|
761
|
+
|
|
762
|
+
// --- Assertions ---
|
|
763
|
+
|
|
764
|
+
// Uncomment to debug: client.printEvents();
|
|
765
|
+
|
|
766
|
+
// Verify event counts
|
|
767
|
+
const eventTypes = client.getEventTypes();
|
|
768
|
+
const typeCounts = eventTypes.reduce(
|
|
769
|
+
(acc, t) => {
|
|
770
|
+
acc[t] = (acc[t] || 0) + 1;
|
|
771
|
+
return acc;
|
|
772
|
+
},
|
|
773
|
+
{} as Record<string, number>
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
expect(typeCounts['trace-create']).toBe(1); // Root trace
|
|
777
|
+
expect(typeCounts['chain-create']).toBe(1); // Outer chain only
|
|
778
|
+
expect(typeCounts['agent-create']).toBe(1); // AgentExecutor detected as agent!
|
|
779
|
+
expect(typeCounts['generation-create']).toBe(2); // Two LLM calls
|
|
780
|
+
expect(typeCounts['generation-update']).toBe(2); // Two LLM ends
|
|
781
|
+
expect(typeCounts['retriever-create']).toBe(1); // One retriever
|
|
782
|
+
expect(typeCounts['tool-create']).toBe(1); // One tool
|
|
783
|
+
expect(typeCounts['span-update']).toBe(4); // retriever end + tool end + 2 chain ends
|
|
784
|
+
|
|
785
|
+
// Verify all events share the same traceId
|
|
786
|
+
const traceId = handler.getTraceId()!;
|
|
787
|
+
for (const event of client.events) {
|
|
788
|
+
const body = event.body as Record<string, unknown>;
|
|
789
|
+
if (body.traceId) {
|
|
790
|
+
expect(body.traceId).toBe(traceId);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Verify token usage in generation-update events
|
|
795
|
+
const genUpdates = client.getByType('generation-update');
|
|
796
|
+
|
|
797
|
+
// First LLM call usage
|
|
798
|
+
const firstLLMUpdate = genUpdates[0].body as Record<string, unknown>;
|
|
799
|
+
const firstUsage = firstLLMUpdate.usage as Record<string, unknown>;
|
|
800
|
+
expect(firstUsage.promptTokens).toBe(200);
|
|
801
|
+
expect(firstUsage.completionTokens).toBe(30);
|
|
802
|
+
expect(firstUsage.totalTokens).toBe(230);
|
|
803
|
+
expect(firstLLMUpdate.model).toBe('claude-sonnet-4-20250514');
|
|
804
|
+
expect(firstLLMUpdate.provider).toBe('anthropic');
|
|
805
|
+
|
|
806
|
+
// Second LLM call usage
|
|
807
|
+
const secondLLMUpdate = genUpdates[1].body as Record<string, unknown>;
|
|
808
|
+
const secondUsage = secondLLMUpdate.usage as Record<string, unknown>;
|
|
809
|
+
expect(secondUsage.promptTokens).toBe(800);
|
|
810
|
+
expect(secondUsage.completionTokens).toBe(150);
|
|
811
|
+
expect(secondUsage.totalTokens).toBe(950);
|
|
812
|
+
|
|
813
|
+
// Verify total token computation
|
|
814
|
+
const totalPrompt = 200 + 800;
|
|
815
|
+
const totalCompletion = 30 + 150;
|
|
816
|
+
const totalTokens = totalPrompt + totalCompletion;
|
|
817
|
+
expect(totalPrompt).toBe(1000);
|
|
818
|
+
expect(totalCompletion).toBe(180);
|
|
819
|
+
expect(totalTokens).toBe(1180);
|
|
820
|
+
|
|
821
|
+
// Total: 1180 tokens (1000 prompt + 180 completion) across 2 LLM calls
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
// -----------------------------------------------------------------------
|
|
826
|
+
// 8. Token usage edge cases
|
|
827
|
+
// -----------------------------------------------------------------------
|
|
828
|
+
|
|
829
|
+
describe('token usage edge cases', () => {
|
|
830
|
+
it('should handle different token usage field names (prompt_tokens vs promptTokens)', async () => {
|
|
831
|
+
const chainRunId = makeUUID();
|
|
832
|
+
const llmRunId = makeUUID();
|
|
833
|
+
|
|
834
|
+
await handler.handleChainStart(
|
|
835
|
+
serialized('Chain'),
|
|
836
|
+
{ input: 'test' },
|
|
837
|
+
chainRunId
|
|
838
|
+
);
|
|
839
|
+
await handler.handleLLMStart(
|
|
840
|
+
serialized('ChatOpenAI'),
|
|
841
|
+
['test'],
|
|
842
|
+
llmRunId,
|
|
843
|
+
chainRunId
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
// OpenAI style: uses prompt_tokens (snake_case)
|
|
847
|
+
await handler.handleLLMEnd(
|
|
848
|
+
{
|
|
849
|
+
generations: [[{ text: 'response', generationInfo: {} }]],
|
|
850
|
+
llmOutput: {
|
|
851
|
+
tokenUsage: {
|
|
852
|
+
prompt_tokens: 100,
|
|
853
|
+
completion_tokens: 50,
|
|
854
|
+
total_tokens: 150,
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
},
|
|
858
|
+
llmRunId
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
const updates = client.getByType('generation-update');
|
|
862
|
+
const usage = (updates[0].body as Record<string, unknown>)
|
|
863
|
+
.usage as Record<string, unknown>;
|
|
864
|
+
|
|
865
|
+
// The handler should normalize both field naming conventions
|
|
866
|
+
expect(usage.promptTokens ?? usage.prompt_tokens).toBeTruthy();
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
it('should handle missing token usage gracefully', async () => {
|
|
870
|
+
const chainRunId = makeUUID();
|
|
871
|
+
const llmRunId = makeUUID();
|
|
872
|
+
|
|
873
|
+
await handler.handleChainStart(
|
|
874
|
+
serialized('Chain'),
|
|
875
|
+
{ input: 'test' },
|
|
876
|
+
chainRunId
|
|
877
|
+
);
|
|
878
|
+
await handler.handleLLMStart(
|
|
879
|
+
serialized('ChatOpenAI'),
|
|
880
|
+
['test'],
|
|
881
|
+
llmRunId,
|
|
882
|
+
chainRunId
|
|
883
|
+
);
|
|
884
|
+
|
|
885
|
+
// No llmOutput at all
|
|
886
|
+
await handler.handleLLMEnd(
|
|
887
|
+
{ generations: [[{ text: 'response', generationInfo: {} }]] },
|
|
888
|
+
llmRunId
|
|
889
|
+
);
|
|
890
|
+
|
|
891
|
+
const updates = client.getByType('generation-update');
|
|
892
|
+
expect(updates).toHaveLength(1);
|
|
893
|
+
// Should not crash, usage should be present but with undefined values
|
|
894
|
+
const usage = (updates[0].body as Record<string, unknown>)
|
|
895
|
+
.usage as Record<string, unknown>;
|
|
896
|
+
expect(usage).toBeDefined();
|
|
897
|
+
});
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
// -----------------------------------------------------------------------
|
|
901
|
+
// 9. Flush and shutdown
|
|
902
|
+
// -----------------------------------------------------------------------
|
|
903
|
+
|
|
904
|
+
describe('flush and shutdown', () => {
|
|
905
|
+
it('flushAsync should call client.flush()', async () => {
|
|
906
|
+
await handler.handleChainStart(
|
|
907
|
+
serialized('Chain'),
|
|
908
|
+
{ input: 'test' },
|
|
909
|
+
makeUUID()
|
|
910
|
+
);
|
|
911
|
+
await handler.flushAsync();
|
|
912
|
+
expect(client.flushed).toBe(true);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
it('shutdown should call client.flush() (not shutdown) for external client', async () => {
|
|
916
|
+
await handler.handleChainStart(
|
|
917
|
+
serialized('Chain'),
|
|
918
|
+
{ input: 'test' },
|
|
919
|
+
makeUUID()
|
|
920
|
+
);
|
|
921
|
+
await handler.shutdown();
|
|
922
|
+
// Since we provided an external client, it should only flush, not shutdown
|
|
923
|
+
expect(client.flushed).toBe(true);
|
|
924
|
+
expect(client.shutdownCalled).toBe(false);
|
|
925
|
+
});
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
// -----------------------------------------------------------------------
|
|
929
|
+
// 10. Agent detection
|
|
930
|
+
// -----------------------------------------------------------------------
|
|
931
|
+
|
|
932
|
+
describe('agent detection', () => {
|
|
933
|
+
it('should emit agent-create for chains with langgraph_node agent= metadata', async () => {
|
|
934
|
+
const outerChainId = makeUUID();
|
|
935
|
+
const agentChainId = makeUUID();
|
|
936
|
+
|
|
937
|
+
await handler.handleChainStart(
|
|
938
|
+
serialized('CompiledStateGraph'),
|
|
939
|
+
{ messages: [{ role: 'user', content: 'Hello' }] },
|
|
940
|
+
outerChainId
|
|
941
|
+
);
|
|
942
|
+
|
|
943
|
+
// Agent chain with langgraph_node metadata
|
|
944
|
+
// Cast to any to pass tags/metadata params (ts-jest resolves different .d.ts for BaseCallbackHandler)
|
|
945
|
+
await (handler as any).handleChainStart(
|
|
946
|
+
serialized('RunnableSequence'),
|
|
947
|
+
{ input: 'agent task' },
|
|
948
|
+
agentChainId,
|
|
949
|
+
outerChainId,
|
|
950
|
+
undefined, // tags
|
|
951
|
+
{ langgraph_node: 'agent=research-agent' } // metadata
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
const agentEvents = client.getByType('agent-create');
|
|
955
|
+
expect(agentEvents).toHaveLength(1);
|
|
956
|
+
|
|
957
|
+
const agentBody = agentEvents[0].body as Record<string, unknown>;
|
|
958
|
+
expect(agentBody.name).toBe('Agent: research-agent');
|
|
959
|
+
expect(agentBody.traceId).toBe(handler.getTraceId());
|
|
960
|
+
|
|
961
|
+
const meta = agentBody.metadata as Record<string, unknown>;
|
|
962
|
+
expect(meta.agentId).toBe('research-agent');
|
|
963
|
+
expect(meta.langchainMetadata).toEqual({
|
|
964
|
+
langgraph_node: 'agent=research-agent',
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
it('should emit agent-create for AgentExecutor chain name', async () => {
|
|
969
|
+
const agentRunId = makeUUID();
|
|
970
|
+
|
|
971
|
+
await handler.handleChainStart(
|
|
972
|
+
serialized('AgentExecutor'),
|
|
973
|
+
{ input: 'do something' },
|
|
974
|
+
agentRunId
|
|
975
|
+
);
|
|
976
|
+
|
|
977
|
+
const agentEvents = client.getByType('agent-create');
|
|
978
|
+
expect(agentEvents).toHaveLength(1);
|
|
979
|
+
expect((agentEvents[0].body as Record<string, unknown>).name).toBe(
|
|
980
|
+
'AgentExecutor'
|
|
981
|
+
);
|
|
982
|
+
|
|
983
|
+
// Should NOT produce a chain-create
|
|
984
|
+
const chainEvents = client.getByType('chain-create');
|
|
985
|
+
expect(chainEvents).toHaveLength(0);
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
it('should emit agent-create for chain names ending in Agent', async () => {
|
|
989
|
+
const agentRunId = makeUUID();
|
|
990
|
+
|
|
991
|
+
await handler.handleChainStart(
|
|
992
|
+
serialized('ReactAgent'),
|
|
993
|
+
{ input: 'think and act' },
|
|
994
|
+
agentRunId
|
|
995
|
+
);
|
|
996
|
+
|
|
997
|
+
const agentEvents = client.getByType('agent-create');
|
|
998
|
+
expect(agentEvents).toHaveLength(1);
|
|
999
|
+
expect((agentEvents[0].body as Record<string, unknown>).name).toBe(
|
|
1000
|
+
'ReactAgent'
|
|
1001
|
+
);
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
it('should NOT emit agent-create for regular chains', async () => {
|
|
1005
|
+
const chainRunId = makeUUID();
|
|
1006
|
+
|
|
1007
|
+
await handler.handleChainStart(
|
|
1008
|
+
serialized('RunnableSequence'),
|
|
1009
|
+
{ input: 'regular chain' },
|
|
1010
|
+
chainRunId
|
|
1011
|
+
);
|
|
1012
|
+
|
|
1013
|
+
const agentEvents = client.getByType('agent-create');
|
|
1014
|
+
expect(agentEvents).toHaveLength(0);
|
|
1015
|
+
|
|
1016
|
+
const chainEvents = client.getByType('chain-create');
|
|
1017
|
+
expect(chainEvents).toHaveLength(1);
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
it('should correctly end agent observations with span-update', async () => {
|
|
1021
|
+
const agentRunId = makeUUID();
|
|
1022
|
+
|
|
1023
|
+
await (handler as any).handleChainStart(
|
|
1024
|
+
serialized('RunnableSequence'),
|
|
1025
|
+
{ input: 'agent task' },
|
|
1026
|
+
agentRunId,
|
|
1027
|
+
undefined, // parentRunId
|
|
1028
|
+
undefined, // tags
|
|
1029
|
+
{ langgraph_node: 'agent=planner' } // metadata
|
|
1030
|
+
);
|
|
1031
|
+
|
|
1032
|
+
await handler.handleChainEnd({ output: 'agent completed' }, agentRunId);
|
|
1033
|
+
|
|
1034
|
+
const agentEvents = client.getByType('agent-create');
|
|
1035
|
+
expect(agentEvents).toHaveLength(1);
|
|
1036
|
+
|
|
1037
|
+
const spanUpdates = client.getByType('span-update');
|
|
1038
|
+
expect(spanUpdates).toHaveLength(1);
|
|
1039
|
+
|
|
1040
|
+
const update = spanUpdates[0].body as Record<string, unknown>;
|
|
1041
|
+
expect(update.output).toEqual({ output: 'agent completed' });
|
|
1042
|
+
expect(update.endTime).toBeDefined();
|
|
1043
|
+
// The span-update should reference the same observation ID as agent-create
|
|
1044
|
+
expect(update.id).toBe(
|
|
1045
|
+
(agentEvents[0].body as Record<string, unknown>).id
|
|
1046
|
+
);
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
it('should handle agent error with ERROR level', async () => {
|
|
1050
|
+
const agentRunId = makeUUID();
|
|
1051
|
+
|
|
1052
|
+
await handler.handleChainStart(
|
|
1053
|
+
serialized('AgentExecutor'),
|
|
1054
|
+
{ input: 'fail task' },
|
|
1055
|
+
agentRunId
|
|
1056
|
+
);
|
|
1057
|
+
|
|
1058
|
+
await handler.handleChainError(
|
|
1059
|
+
new Error('Agent failed: tool not found'),
|
|
1060
|
+
agentRunId
|
|
1061
|
+
);
|
|
1062
|
+
|
|
1063
|
+
const agentEvents = client.getByType('agent-create');
|
|
1064
|
+
expect(agentEvents).toHaveLength(1);
|
|
1065
|
+
|
|
1066
|
+
const spanUpdates = client.getByType('span-update');
|
|
1067
|
+
const errorUpdate = spanUpdates.find(
|
|
1068
|
+
(e) => (e.body as Record<string, unknown>).level === 'ERROR'
|
|
1069
|
+
);
|
|
1070
|
+
expect(errorUpdate).toBeDefined();
|
|
1071
|
+
expect((errorUpdate!.body as Record<string, unknown>).statusMessage).toBe(
|
|
1072
|
+
'Agent failed: tool not found'
|
|
1073
|
+
);
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
it('should integrate agent-create in full pipeline with correct counts', async () => {
|
|
1077
|
+
const rootChainId = makeUUID();
|
|
1078
|
+
const agentChainId = makeUUID();
|
|
1079
|
+
const llmRunId = makeUUID();
|
|
1080
|
+
|
|
1081
|
+
// Root chain
|
|
1082
|
+
await handler.handleChainStart(
|
|
1083
|
+
serialized('CompiledStateGraph'),
|
|
1084
|
+
{ messages: [{ role: 'user', content: 'Hello' }] },
|
|
1085
|
+
rootChainId
|
|
1086
|
+
);
|
|
1087
|
+
|
|
1088
|
+
// Agent chain (detected via metadata)
|
|
1089
|
+
await (handler as any).handleChainStart(
|
|
1090
|
+
serialized('RunnableSequence'),
|
|
1091
|
+
{ input: 'agent work' },
|
|
1092
|
+
agentChainId,
|
|
1093
|
+
rootChainId,
|
|
1094
|
+
undefined, // tags
|
|
1095
|
+
{ langgraph_node: 'agent=qa-agent' } // metadata
|
|
1096
|
+
);
|
|
1097
|
+
|
|
1098
|
+
// LLM inside agent
|
|
1099
|
+
await handler.handleLLMStart(
|
|
1100
|
+
serialized('ChatAnthropic'),
|
|
1101
|
+
['prompt'],
|
|
1102
|
+
llmRunId,
|
|
1103
|
+
agentChainId,
|
|
1104
|
+
undefined,
|
|
1105
|
+
undefined,
|
|
1106
|
+
{ ls_model_name: 'claude-sonnet-4-20250514', ls_provider: 'anthropic' }
|
|
1107
|
+
);
|
|
1108
|
+
|
|
1109
|
+
await handler.handleLLMEnd(
|
|
1110
|
+
{
|
|
1111
|
+
generations: [[{ text: 'answer', generationInfo: {} }]],
|
|
1112
|
+
llmOutput: {
|
|
1113
|
+
tokenUsage: {
|
|
1114
|
+
promptTokens: 100,
|
|
1115
|
+
completionTokens: 20,
|
|
1116
|
+
totalTokens: 120,
|
|
1117
|
+
},
|
|
1118
|
+
modelName: 'claude-sonnet-4-20250514',
|
|
1119
|
+
},
|
|
1120
|
+
},
|
|
1121
|
+
llmRunId
|
|
1122
|
+
);
|
|
1123
|
+
|
|
1124
|
+
await handler.handleChainEnd({ output: 'done' }, agentChainId);
|
|
1125
|
+
await handler.handleChainEnd({ output: 'done' }, rootChainId);
|
|
1126
|
+
|
|
1127
|
+
const typeCounts = client.getEventTypes().reduce(
|
|
1128
|
+
(acc, t) => {
|
|
1129
|
+
acc[t] = (acc[t] || 0) + 1;
|
|
1130
|
+
return acc;
|
|
1131
|
+
},
|
|
1132
|
+
{} as Record<string, number>
|
|
1133
|
+
);
|
|
1134
|
+
|
|
1135
|
+
expect(typeCounts['trace-create']).toBe(1);
|
|
1136
|
+
expect(typeCounts['chain-create']).toBe(1); // Only root chain
|
|
1137
|
+
expect(typeCounts['agent-create']).toBe(1); // Agent detected!
|
|
1138
|
+
expect(typeCounts['generation-create']).toBe(1);
|
|
1139
|
+
expect(typeCounts['generation-update']).toBe(1);
|
|
1140
|
+
expect(typeCounts['span-update']).toBe(2); // agent end + root chain end
|
|
1141
|
+
|
|
1142
|
+
// LLM parent should be the agent observation
|
|
1143
|
+
const agentObsId = (
|
|
1144
|
+
client.getByType('agent-create')[0].body as Record<string, unknown>
|
|
1145
|
+
).id;
|
|
1146
|
+
const genCreate = client.getByType('generation-create')[0].body as Record<
|
|
1147
|
+
string,
|
|
1148
|
+
unknown
|
|
1149
|
+
>;
|
|
1150
|
+
expect(genCreate.parentObservationId).toBe(agentObsId);
|
|
1151
|
+
});
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
// -----------------------------------------------------------------------
|
|
1155
|
+
// 11. Event structure validation
|
|
1156
|
+
// -----------------------------------------------------------------------
|
|
1157
|
+
|
|
1158
|
+
describe('event structure validation', () => {
|
|
1159
|
+
it('every event should have id, type, timestamp, and body', async () => {
|
|
1160
|
+
const chainRunId = makeUUID();
|
|
1161
|
+
const llmRunId = makeUUID();
|
|
1162
|
+
|
|
1163
|
+
await handler.handleChainStart(
|
|
1164
|
+
serialized('Chain'),
|
|
1165
|
+
{ input: 'test' },
|
|
1166
|
+
chainRunId
|
|
1167
|
+
);
|
|
1168
|
+
await handler.handleLLMStart(
|
|
1169
|
+
serialized('LLM'),
|
|
1170
|
+
['test'],
|
|
1171
|
+
llmRunId,
|
|
1172
|
+
chainRunId
|
|
1173
|
+
);
|
|
1174
|
+
await handler.handleLLMEnd(
|
|
1175
|
+
{
|
|
1176
|
+
generations: [[{ text: 'ok', generationInfo: {} }]],
|
|
1177
|
+
llmOutput: { tokenUsage: {} },
|
|
1178
|
+
},
|
|
1179
|
+
llmRunId
|
|
1180
|
+
);
|
|
1181
|
+
await handler.handleChainEnd({ output: 'done' }, chainRunId);
|
|
1182
|
+
|
|
1183
|
+
for (const event of client.events) {
|
|
1184
|
+
expect(event.id).toBeDefined();
|
|
1185
|
+
expect(typeof event.id).toBe('string');
|
|
1186
|
+
expect(event.type).toBeDefined();
|
|
1187
|
+
expect(event.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
1188
|
+
expect(event.body).toBeDefined();
|
|
1189
|
+
expect(typeof event.body).toBe('object');
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
it('observation events should have traceId and observationId', async () => {
|
|
1194
|
+
const chainRunId = makeUUID();
|
|
1195
|
+
await handler.handleChainStart(
|
|
1196
|
+
serialized('Chain'),
|
|
1197
|
+
{ input: 'test' },
|
|
1198
|
+
chainRunId
|
|
1199
|
+
);
|
|
1200
|
+
await handler.handleChainEnd({ output: 'done' }, chainRunId);
|
|
1201
|
+
|
|
1202
|
+
const nonTraceEvents = client.events.filter(
|
|
1203
|
+
(e) => e.type !== 'trace-create'
|
|
1204
|
+
);
|
|
1205
|
+
for (const event of nonTraceEvents) {
|
|
1206
|
+
const body = event.body as Record<string, unknown>;
|
|
1207
|
+
expect(body.traceId).toBe(handler.getTraceId());
|
|
1208
|
+
expect(body.id).toBeDefined();
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1211
|
+
});
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
// ---------------------------------------------------------------------------
|
|
1215
|
+
// IllumaSpanProcessor unit tests (OTel integration)
|
|
1216
|
+
// ---------------------------------------------------------------------------
|
|
1217
|
+
|
|
1218
|
+
describe('IllumaSpanProcessor integration', () => {
|
|
1219
|
+
it('should be importable from @illuma-ai/observability-otel', async () => {
|
|
1220
|
+
const { IllumaSpanProcessor } = await import(
|
|
1221
|
+
'@illuma-ai/observability-otel'
|
|
1222
|
+
);
|
|
1223
|
+
expect(IllumaSpanProcessor).toBeDefined();
|
|
1224
|
+
expect(typeof IllumaSpanProcessor).toBe('function');
|
|
1225
|
+
});
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
// ---------------------------------------------------------------------------
|
|
1229
|
+
// Core SDK features: sampling, PII masking, observation types, prompt/dataset
|
|
1230
|
+
// ---------------------------------------------------------------------------
|
|
1231
|
+
|
|
1232
|
+
describe('Core SDK features', () => {
|
|
1233
|
+
// Use require to bypass ts-jest module resolution issues with dynamic imports
|
|
1234
|
+
|
|
1235
|
+
const {
|
|
1236
|
+
ObservabilityCoreClient,
|
|
1237
|
+
TraceClient,
|
|
1238
|
+
SpanClient,
|
|
1239
|
+
IngestionEventType,
|
|
1240
|
+
} = require('@illuma-ai/observability-core');
|
|
1241
|
+
|
|
1242
|
+
// Create a concrete subclass at runtime to avoid TS abstract class issues
|
|
1243
|
+
const TestClientClass = class extends (ObservabilityCoreClient as any) {
|
|
1244
|
+
protected async fetchWithRetry(): Promise<any> {
|
|
1245
|
+
return {
|
|
1246
|
+
status: 200,
|
|
1247
|
+
statusText: 'OK',
|
|
1248
|
+
ok: true,
|
|
1249
|
+
json: async () => ({}),
|
|
1250
|
+
text: async () => '',
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
function createTestClient(config: Record<string, unknown> = {}): {
|
|
1256
|
+
client: any;
|
|
1257
|
+
events: any[];
|
|
1258
|
+
} {
|
|
1259
|
+
const events: any[] = [];
|
|
1260
|
+
|
|
1261
|
+
const client = new (TestClientClass as any)({
|
|
1262
|
+
publicKey: 'pk-test',
|
|
1263
|
+
secretKey: 'sk-test',
|
|
1264
|
+
baseUrl: 'http://localhost:9999',
|
|
1265
|
+
flushInterval: 0,
|
|
1266
|
+
...config,
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
// Intercept enqueue to capture events
|
|
1270
|
+
const origEnqueue = client.enqueue.bind(client);
|
|
1271
|
+
client.enqueue = (event: any) => {
|
|
1272
|
+
origEnqueue(event);
|
|
1273
|
+
events.push(event);
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1276
|
+
return { client, events };
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// -----------------------------------------------------------------------
|
|
1280
|
+
// 12. Sampling rate
|
|
1281
|
+
// -----------------------------------------------------------------------
|
|
1282
|
+
|
|
1283
|
+
describe('sampling rate', () => {
|
|
1284
|
+
it('should trace everything when sampleRate=1.0 (default)', () => {
|
|
1285
|
+
const { client, events } = createTestClient({ sampleRate: 1.0 });
|
|
1286
|
+
|
|
1287
|
+
for (let i = 0; i < 20; i++) {
|
|
1288
|
+
client.trace({ name: `trace-${i}` });
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// All 20 traces should be created (each produces a trace-create event queued)
|
|
1292
|
+
const traceEvents = events.filter((e: any) => e.type === 'trace-create');
|
|
1293
|
+
expect(traceEvents.length).toBe(20);
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
it('should trace nothing when sampleRate=0.0', () => {
|
|
1297
|
+
const { client, events } = createTestClient({ sampleRate: 0.0 });
|
|
1298
|
+
|
|
1299
|
+
for (let i = 0; i < 20; i++) {
|
|
1300
|
+
const trace = client.trace({ name: `trace-${i}` });
|
|
1301
|
+
// Even child events should be silently dropped
|
|
1302
|
+
trace.generation({ name: 'gen', model: 'gpt-4o' });
|
|
1303
|
+
trace.span({ name: 'span' });
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// No events should be enqueued at all
|
|
1307
|
+
expect(events.length).toBe(0);
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
it('should sample approximately the right percentage', () => {
|
|
1311
|
+
// Use a fixed seed approach: run many traces with 50% sample rate
|
|
1312
|
+
const { client, events } = createTestClient({ sampleRate: 0.5 });
|
|
1313
|
+
|
|
1314
|
+
const iterations = 1000;
|
|
1315
|
+
for (let i = 0; i < iterations; i++) {
|
|
1316
|
+
client.trace({ name: `trace-${i}` });
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
const traceEvents = events.filter((e: any) => e.type === 'trace-create');
|
|
1320
|
+
// With 1000 iterations at 50%, expect roughly 400-600 traces
|
|
1321
|
+
expect(traceEvents.length).toBeGreaterThan(300);
|
|
1322
|
+
expect(traceEvents.length).toBeLessThan(700);
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
it('should include all children once a trace is sampled in', () => {
|
|
1326
|
+
const { client, events } = createTestClient({ sampleRate: 1.0 });
|
|
1327
|
+
|
|
1328
|
+
const trace = client.trace({ name: 'sampled-trace' });
|
|
1329
|
+
trace.span({ name: 'child-span' });
|
|
1330
|
+
trace.generation({ name: 'child-gen', model: 'gpt-4o' });
|
|
1331
|
+
trace.agent({ name: 'child-agent' });
|
|
1332
|
+
trace.tool({ name: 'child-tool' });
|
|
1333
|
+
|
|
1334
|
+
// 1 trace + 4 children = 5 events
|
|
1335
|
+
expect(events.length).toBe(5);
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
it('should clamp sampleRate to [0, 1] range', () => {
|
|
1339
|
+
// sampleRate > 1 should be clamped to 1 (trace everything)
|
|
1340
|
+
const { events: events1 } = createTestClient({ sampleRate: 5.0 });
|
|
1341
|
+
// sampleRate < 0 should be clamped to 0 (trace nothing)
|
|
1342
|
+
const { client: client2, events: events2 } = createTestClient({
|
|
1343
|
+
sampleRate: -1,
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
client2.trace({ name: 'should-not-appear' });
|
|
1347
|
+
expect(events2.length).toBe(0);
|
|
1348
|
+
});
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
// -----------------------------------------------------------------------
|
|
1352
|
+
// 13. PII masking
|
|
1353
|
+
// -----------------------------------------------------------------------
|
|
1354
|
+
|
|
1355
|
+
describe('PII masking', () => {
|
|
1356
|
+
it('should apply mask function to event bodies', () => {
|
|
1357
|
+
const maskFunction = (body: Record<string, unknown>) => {
|
|
1358
|
+
const masked = { ...body };
|
|
1359
|
+
if (typeof masked.input === 'string') {
|
|
1360
|
+
masked.input = masked.input.replace(
|
|
1361
|
+
/[\w.-]+@[\w.-]+\.\w+/g,
|
|
1362
|
+
'[EMAIL]'
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
return masked;
|
|
1366
|
+
};
|
|
1367
|
+
|
|
1368
|
+
const { client } = createTestClient({ maskFunction });
|
|
1369
|
+
|
|
1370
|
+
const trace = client.trace({ name: 'pii-test' });
|
|
1371
|
+
trace.span({
|
|
1372
|
+
name: 'user-query',
|
|
1373
|
+
input: 'My email is john@example.com and I need help' as any,
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
// Check the queued events - the span should have masked input
|
|
1377
|
+
const queue = client.getQueue();
|
|
1378
|
+
const spanEvent = [...queue].find((e: any) => e.type === 'span-create');
|
|
1379
|
+
if (spanEvent) {
|
|
1380
|
+
expect((spanEvent.body as any).input).toBe(
|
|
1381
|
+
'My email is [EMAIL] and I need help'
|
|
1382
|
+
);
|
|
1383
|
+
expect((spanEvent.body as any).input).not.toContain('john@example.com');
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
it('should mask multiple PII patterns', () => {
|
|
1388
|
+
const maskFunction = (body: Record<string, unknown>) => {
|
|
1389
|
+
const masked = { ...body };
|
|
1390
|
+
const str = JSON.stringify(masked);
|
|
1391
|
+
const redacted = str
|
|
1392
|
+
.replace(/[\w.-]+@[\w.-]+\.\w+/g, '[EMAIL]')
|
|
1393
|
+
.replace(/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g, '[PHONE]')
|
|
1394
|
+
.replace(/\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g, '[CARD]');
|
|
1395
|
+
return JSON.parse(redacted);
|
|
1396
|
+
};
|
|
1397
|
+
|
|
1398
|
+
const { client } = createTestClient({ maskFunction });
|
|
1399
|
+
|
|
1400
|
+
const trace = client.trace({
|
|
1401
|
+
name: 'multi-pii',
|
|
1402
|
+
metadata: { userEmail: 'test@corp.com', phone: '555-123-4567' },
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
const queue = client.getQueue();
|
|
1406
|
+
const traceEvent = [...queue].find((e: any) => e.type === 'trace-create');
|
|
1407
|
+
if (traceEvent) {
|
|
1408
|
+
const meta = (traceEvent.body as any).metadata;
|
|
1409
|
+
expect(meta.userEmail).toBe('[EMAIL]');
|
|
1410
|
+
expect(meta.phone).toBe('[PHONE]');
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
it('should gracefully handle mask function errors', () => {
|
|
1415
|
+
const maskFunction = () => {
|
|
1416
|
+
throw new Error('Mask function exploded');
|
|
1417
|
+
};
|
|
1418
|
+
|
|
1419
|
+
const { client } = createTestClient({ maskFunction });
|
|
1420
|
+
|
|
1421
|
+
// Should not throw — falls back to unmasked
|
|
1422
|
+
expect(() => {
|
|
1423
|
+
client.trace({ name: 'should-not-crash' });
|
|
1424
|
+
}).not.toThrow();
|
|
1425
|
+
|
|
1426
|
+
const queue = client.getQueue();
|
|
1427
|
+
expect(queue.length).toBeGreaterThan(0);
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
it('should not modify events when no mask function is set', () => {
|
|
1431
|
+
const { client } = createTestClient();
|
|
1432
|
+
|
|
1433
|
+
client.trace({
|
|
1434
|
+
name: 'no-mask',
|
|
1435
|
+
metadata: { email: 'visible@example.com' },
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
const queue = client.getQueue();
|
|
1439
|
+
const traceEvent = [...queue].find((e: any) => e.type === 'trace-create');
|
|
1440
|
+
expect((traceEvent!.body as any).metadata.email).toBe(
|
|
1441
|
+
'visible@example.com'
|
|
1442
|
+
);
|
|
1443
|
+
});
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
// -----------------------------------------------------------------------
|
|
1447
|
+
// 14. All observation types (TraceClient + SpanClient)
|
|
1448
|
+
// -----------------------------------------------------------------------
|
|
1449
|
+
|
|
1450
|
+
describe('observation types completeness', () => {
|
|
1451
|
+
it('TraceClient should have all observation type methods', () => {
|
|
1452
|
+
const enqueue = jest.fn();
|
|
1453
|
+
const trace = new TraceClient('trace-123', enqueue);
|
|
1454
|
+
|
|
1455
|
+
// All typed observation methods should exist and return SpanClient or GenerationClient
|
|
1456
|
+
expect(typeof trace.span).toBe('function');
|
|
1457
|
+
expect(typeof trace.agent).toBe('function');
|
|
1458
|
+
expect(typeof trace.tool).toBe('function');
|
|
1459
|
+
expect(typeof trace.chain).toBe('function');
|
|
1460
|
+
expect(typeof trace.retriever).toBe('function');
|
|
1461
|
+
expect(typeof trace.guardrail).toBe('function');
|
|
1462
|
+
expect(typeof trace.evaluator).toBe('function');
|
|
1463
|
+
expect(typeof trace.embedding).toBe('function');
|
|
1464
|
+
expect(typeof trace.generation).toBe('function');
|
|
1465
|
+
expect(typeof trace.event).toBe('function');
|
|
1466
|
+
expect(typeof trace.score).toBe('function');
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
it('SpanClient should have all observation type methods', () => {
|
|
1470
|
+
const enqueue = jest.fn();
|
|
1471
|
+
const span = new SpanClient('span-123', 'trace-123', enqueue);
|
|
1472
|
+
|
|
1473
|
+
expect(typeof span.span).toBe('function');
|
|
1474
|
+
expect(typeof span.agent).toBe('function');
|
|
1475
|
+
expect(typeof span.tool).toBe('function');
|
|
1476
|
+
expect(typeof span.chain).toBe('function');
|
|
1477
|
+
expect(typeof span.retriever).toBe('function');
|
|
1478
|
+
expect(typeof span.guardrail).toBe('function');
|
|
1479
|
+
expect(typeof span.evaluator).toBe('function');
|
|
1480
|
+
expect(typeof span.embedding).toBe('function');
|
|
1481
|
+
expect(typeof span.generation).toBe('function');
|
|
1482
|
+
expect(typeof span.event).toBe('function');
|
|
1483
|
+
expect(typeof span.score).toBe('function');
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
it('should emit correct event types for each observation method on TraceClient', () => {
|
|
1487
|
+
const events: any[] = [];
|
|
1488
|
+
const enqueue = (event: any) => events.push(event);
|
|
1489
|
+
const trace = new TraceClient('trace-456', enqueue);
|
|
1490
|
+
|
|
1491
|
+
trace.span({ name: 'span' });
|
|
1492
|
+
trace.agent({ name: 'agent' });
|
|
1493
|
+
trace.tool({ name: 'tool' });
|
|
1494
|
+
trace.chain({ name: 'chain' });
|
|
1495
|
+
trace.retriever({ name: 'retriever' });
|
|
1496
|
+
trace.guardrail({ name: 'guardrail' });
|
|
1497
|
+
trace.evaluator({ name: 'evaluator' });
|
|
1498
|
+
trace.embedding({ name: 'embedding' });
|
|
1499
|
+
trace.generation({ name: 'generation' });
|
|
1500
|
+
trace.event({ name: 'event' });
|
|
1501
|
+
|
|
1502
|
+
const types = events.map((e) => e.type);
|
|
1503
|
+
expect(types).toContain('span-create');
|
|
1504
|
+
expect(types).toContain('agent-create');
|
|
1505
|
+
expect(types).toContain('tool-create');
|
|
1506
|
+
expect(types).toContain('chain-create');
|
|
1507
|
+
expect(types).toContain('retriever-create');
|
|
1508
|
+
expect(types).toContain('guardrail-create');
|
|
1509
|
+
expect(types).toContain('evaluator-create');
|
|
1510
|
+
expect(types).toContain('embedding-create');
|
|
1511
|
+
expect(types).toContain('generation-create');
|
|
1512
|
+
expect(types).toContain('event-create');
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
it('should emit correct event types for each observation method on SpanClient', () => {
|
|
1516
|
+
const events: any[] = [];
|
|
1517
|
+
const enqueue = (event: any) => events.push(event);
|
|
1518
|
+
const span = new SpanClient('span-789', 'trace-789', enqueue);
|
|
1519
|
+
|
|
1520
|
+
span.span({ name: 'child-span' });
|
|
1521
|
+
span.agent({ name: 'child-agent' });
|
|
1522
|
+
span.tool({ name: 'child-tool' });
|
|
1523
|
+
span.chain({ name: 'child-chain' });
|
|
1524
|
+
span.retriever({ name: 'child-retriever' });
|
|
1525
|
+
span.guardrail({ name: 'child-guardrail' });
|
|
1526
|
+
span.evaluator({ name: 'child-evaluator' });
|
|
1527
|
+
span.embedding({ name: 'child-embedding' });
|
|
1528
|
+
span.generation({ name: 'child-generation' });
|
|
1529
|
+
span.event({ name: 'child-event' });
|
|
1530
|
+
|
|
1531
|
+
const types = events.map((e) => e.type);
|
|
1532
|
+
expect(types).toContain('span-create');
|
|
1533
|
+
expect(types).toContain('agent-create');
|
|
1534
|
+
expect(types).toContain('tool-create');
|
|
1535
|
+
expect(types).toContain('chain-create');
|
|
1536
|
+
expect(types).toContain('retriever-create');
|
|
1537
|
+
expect(types).toContain('guardrail-create');
|
|
1538
|
+
expect(types).toContain('evaluator-create');
|
|
1539
|
+
expect(types).toContain('embedding-create');
|
|
1540
|
+
expect(types).toContain('generation-create');
|
|
1541
|
+
expect(types).toContain('event-create');
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
it('SpanClient children should set parentObservationId automatically', () => {
|
|
1545
|
+
const events: any[] = [];
|
|
1546
|
+
const enqueue = (event: any) => events.push(event);
|
|
1547
|
+
const span = new SpanClient('parent-span', 'trace-abc', enqueue);
|
|
1548
|
+
|
|
1549
|
+
span.guardrail({ name: 'guardrail-check' });
|
|
1550
|
+
span.evaluator({ name: 'eval-run' });
|
|
1551
|
+
span.embedding({ name: 'embed-op' });
|
|
1552
|
+
|
|
1553
|
+
for (const event of events) {
|
|
1554
|
+
expect(event.body.parentObservationId).toBe('parent-span');
|
|
1555
|
+
expect(event.body.traceId).toBe('trace-abc');
|
|
1556
|
+
}
|
|
1557
|
+
});
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
// -----------------------------------------------------------------------
|
|
1561
|
+
// 15. Prompt & Dataset SDK methods
|
|
1562
|
+
// -----------------------------------------------------------------------
|
|
1563
|
+
|
|
1564
|
+
describe('prompt and dataset SDK methods', () => {
|
|
1565
|
+
it('core client should expose getPrompt method', () => {
|
|
1566
|
+
const { client } = createTestClient();
|
|
1567
|
+
expect(typeof client.getPrompt).toBe('function');
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
it('core client should expose createPrompt method', () => {
|
|
1571
|
+
const { client } = createTestClient();
|
|
1572
|
+
expect(typeof client.createPrompt).toBe('function');
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
it('core client should expose getDataset method', () => {
|
|
1576
|
+
const { client } = createTestClient();
|
|
1577
|
+
expect(typeof client.getDataset).toBe('function');
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
it('core client should expose createDataset method', () => {
|
|
1581
|
+
const { client } = createTestClient();
|
|
1582
|
+
expect(typeof client.createDataset).toBe('function');
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
it('core client should expose createDatasetItem method', () => {
|
|
1586
|
+
const { client } = createTestClient();
|
|
1587
|
+
expect(typeof client.createDatasetItem).toBe('function');
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
it('getPrompt should return parsed response from server', async () => {
|
|
1591
|
+
const { client } = createTestClient();
|
|
1592
|
+
// Mock fetchWithRetry returns { ok: true, json: async () => ({}) }
|
|
1593
|
+
const result = await client.getPrompt({ name: 'my-prompt' });
|
|
1594
|
+
expect(result).toBeDefined();
|
|
1595
|
+
expect(result).toEqual({});
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
it('getDataset should return parsed response from server', async () => {
|
|
1599
|
+
const { client } = createTestClient();
|
|
1600
|
+
const result = await client.getDataset({ name: 'my-dataset' });
|
|
1601
|
+
expect(result).toBeDefined();
|
|
1602
|
+
expect(result).toEqual({});
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
it('prompt/dataset methods should return null when disabled', async () => {
|
|
1606
|
+
const { client } = createTestClient({ enabled: false });
|
|
1607
|
+
expect(await client.getPrompt({ name: 'test' })).toBeNull();
|
|
1608
|
+
expect(
|
|
1609
|
+
await client.createPrompt({ name: 'test', prompt: 'hello' })
|
|
1610
|
+
).toBeNull();
|
|
1611
|
+
expect(await client.getDataset({ name: 'test' })).toBeNull();
|
|
1612
|
+
expect(await client.createDataset({ name: 'test' })).toBeNull();
|
|
1613
|
+
expect(
|
|
1614
|
+
await client.createDatasetItem({ datasetName: 'test', input: 'x' })
|
|
1615
|
+
).toBeNull();
|
|
1616
|
+
});
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1619
|
+
// -----------------------------------------------------------------------
|
|
1620
|
+
// 16. Node SDK: sampling + PII masking propagation
|
|
1621
|
+
// -----------------------------------------------------------------------
|
|
1622
|
+
|
|
1623
|
+
describe('node SDK config propagation', () => {
|
|
1624
|
+
const { Observability } = require('@illuma-ai/observability-node');
|
|
1625
|
+
|
|
1626
|
+
it('Observability class should accept sampleRate option', async () => {
|
|
1627
|
+
expect(Observability).toBeDefined();
|
|
1628
|
+
|
|
1629
|
+
const obs = new Observability({
|
|
1630
|
+
publicKey: 'pk-test',
|
|
1631
|
+
secretKey: 'sk-test',
|
|
1632
|
+
baseUrl: 'http://localhost:9999',
|
|
1633
|
+
sampleRate: 0.5,
|
|
1634
|
+
flushInterval: 0,
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
expect(obs).toBeDefined();
|
|
1638
|
+
await obs.shutdown();
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1641
|
+
it('Observability class should accept maskFunction option', async () => {
|
|
1642
|
+
const obs = new Observability({
|
|
1643
|
+
publicKey: 'pk-test',
|
|
1644
|
+
secretKey: 'sk-test',
|
|
1645
|
+
baseUrl: 'http://localhost:9999',
|
|
1646
|
+
maskFunction: (body: Record<string, unknown>) => ({
|
|
1647
|
+
...body,
|
|
1648
|
+
input: '[REDACTED]',
|
|
1649
|
+
}),
|
|
1650
|
+
flushInterval: 0,
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
expect(obs).toBeDefined();
|
|
1654
|
+
await obs.shutdown();
|
|
1655
|
+
});
|
|
1656
|
+
});
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
// ---------------------------------------------------------------------------
|
|
1660
|
+
// 17. Guardrail tracing via ObservabilityCallbackHandler
|
|
1661
|
+
// ---------------------------------------------------------------------------
|
|
1662
|
+
|
|
1663
|
+
/** Re-use MockObservabilityClient shape for guardrail tests (same file, top-level scope) */
|
|
1664
|
+
class GuardrailMockClient {
|
|
1665
|
+
events: {
|
|
1666
|
+
id: string;
|
|
1667
|
+
type: string;
|
|
1668
|
+
timestamp: string;
|
|
1669
|
+
body: Record<string, unknown>;
|
|
1670
|
+
}[] = [];
|
|
1671
|
+
flushed = false;
|
|
1672
|
+
shutdownCalled = false;
|
|
1673
|
+
enqueue(event: {
|
|
1674
|
+
id: string;
|
|
1675
|
+
type: string;
|
|
1676
|
+
timestamp: string;
|
|
1677
|
+
body: Record<string, unknown>;
|
|
1678
|
+
}): void {
|
|
1679
|
+
this.events.push(event);
|
|
1680
|
+
}
|
|
1681
|
+
async flush(): Promise<void> {
|
|
1682
|
+
this.flushed = true;
|
|
1683
|
+
}
|
|
1684
|
+
async shutdown(): Promise<void> {
|
|
1685
|
+
this.shutdownCalled = true;
|
|
1686
|
+
}
|
|
1687
|
+
getByType(type: string) {
|
|
1688
|
+
return this.events.filter((e) => e.type === type);
|
|
1689
|
+
}
|
|
1690
|
+
getEventTypes() {
|
|
1691
|
+
return this.events.map((e) => e.type);
|
|
1692
|
+
}
|
|
1693
|
+
printEvents(): void {
|
|
1694
|
+
for (const event of this.events) {
|
|
1695
|
+
const body = event.body;
|
|
1696
|
+
const usage = body.usage as Record<string, unknown> | undefined;
|
|
1697
|
+
console.log(
|
|
1698
|
+
` ${event.type} | name=${body.name ?? '—'} | id=${body.id ?? '—'} | traceId=${body.traceId ?? '—'} | parentObsId=${body.parentObservationId ?? '—'}` +
|
|
1699
|
+
(usage
|
|
1700
|
+
? ` | usage={prompt:${usage.promptTokens ?? '—'},completion:${usage.completionTokens ?? '—'},total:${usage.totalTokens ?? '—'}}`
|
|
1701
|
+
: '') +
|
|
1702
|
+
(body.model ? ` | model=${body.model}` : '') +
|
|
1703
|
+
(body.level === 'ERROR' || body.level === 'WARNING'
|
|
1704
|
+
? ` | ${body.level}: ${body.statusMessage}`
|
|
1705
|
+
: '')
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
const grSerialized = (name: string): Serialized => ({
|
|
1712
|
+
lc: 1,
|
|
1713
|
+
type: 'not_implemented',
|
|
1714
|
+
id: ['langchain', name],
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
describe('Guardrail tracing', () => {
|
|
1718
|
+
let grClient: GuardrailMockClient;
|
|
1719
|
+
let grHandler: any; // Use any to bypass ts-jest type resolution for traceGuardrail
|
|
1720
|
+
|
|
1721
|
+
beforeEach(() => {
|
|
1722
|
+
grClient = new GuardrailMockClient();
|
|
1723
|
+
grHandler = new ObservabilityCallbackHandler({
|
|
1724
|
+
client: grClient as any,
|
|
1725
|
+
});
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1728
|
+
it('outcome=passed should create guardrail-create + span-update with DEFAULT level', async () => {
|
|
1729
|
+
const chainRunId = makeUUID();
|
|
1730
|
+
await grHandler.handleChainStart(
|
|
1731
|
+
grSerialized('RunnableSequence'),
|
|
1732
|
+
{ input: 'hello' },
|
|
1733
|
+
chainRunId
|
|
1734
|
+
);
|
|
1735
|
+
|
|
1736
|
+
const result = grHandler.traceGuardrail({
|
|
1737
|
+
name: 'Output Moderation',
|
|
1738
|
+
guardrailId: 'bedrock-guardrail-123',
|
|
1739
|
+
guardrailVersion: '1',
|
|
1740
|
+
outcome: 'passed',
|
|
1741
|
+
actionApplied: false,
|
|
1742
|
+
action: 'NONE',
|
|
1743
|
+
reason: 'passed',
|
|
1744
|
+
source: 'OUTPUT',
|
|
1745
|
+
input: 'What is the weather?',
|
|
1746
|
+
output: 'The weather is sunny.',
|
|
1747
|
+
violations: [],
|
|
1748
|
+
assessments: [{ contentPolicy: { filters: [] } }],
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1751
|
+
expect(result).not.toBeNull();
|
|
1752
|
+
|
|
1753
|
+
const guardrailEvents = grClient.getByType('guardrail-create');
|
|
1754
|
+
expect(guardrailEvents).toHaveLength(1);
|
|
1755
|
+
|
|
1756
|
+
const grBody = guardrailEvents[0].body;
|
|
1757
|
+
expect(grBody.name).toBe('Output Moderation');
|
|
1758
|
+
expect(grBody.input).toBe('What is the weather?');
|
|
1759
|
+
expect((grBody.metadata as Record<string, unknown>).guardrailId).toBe(
|
|
1760
|
+
'bedrock-guardrail-123'
|
|
1761
|
+
);
|
|
1762
|
+
expect((grBody.metadata as Record<string, unknown>).outcome).toBe('passed');
|
|
1763
|
+
expect((grBody.metadata as Record<string, unknown>).source).toBe('OUTPUT');
|
|
1764
|
+
|
|
1765
|
+
// span-update should show PASSED
|
|
1766
|
+
const spanUpdates = grClient.getByType('span-update');
|
|
1767
|
+
const guardrailUpdate = spanUpdates.find((e: any) => e.body.id === result);
|
|
1768
|
+
expect(guardrailUpdate).toBeDefined();
|
|
1769
|
+
expect(guardrailUpdate!.body.output).toBe('The weather is sunny.');
|
|
1770
|
+
expect(guardrailUpdate!.body.level).toBe('DEFAULT');
|
|
1771
|
+
expect(guardrailUpdate!.body.statusMessage).toBe('PASSED');
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
it('outcome=blocked should mark with ERROR level when enforced', async () => {
|
|
1775
|
+
const chainRunId = makeUUID();
|
|
1776
|
+
await grHandler.handleChainStart(
|
|
1777
|
+
grSerialized('RunnableSequence'),
|
|
1778
|
+
{ input: 'test' },
|
|
1779
|
+
chainRunId
|
|
1780
|
+
);
|
|
1781
|
+
|
|
1782
|
+
grHandler.traceGuardrail({
|
|
1783
|
+
name: 'Input Moderation',
|
|
1784
|
+
guardrailId: 'bedrock-guardrail-456',
|
|
1785
|
+
outcome: 'blocked',
|
|
1786
|
+
actionApplied: true,
|
|
1787
|
+
action: 'GUARDRAIL_INTERVENED',
|
|
1788
|
+
reason: 'policy_violation',
|
|
1789
|
+
source: 'INPUT',
|
|
1790
|
+
input: 'How to hack a system?',
|
|
1791
|
+
output: 'Sorry, I cannot help with that.',
|
|
1792
|
+
violations: [
|
|
1793
|
+
{ type: 'CONTENT_POLICY', category: 'VIOLENCE', action: 'BLOCKED' },
|
|
1794
|
+
],
|
|
1795
|
+
});
|
|
1796
|
+
|
|
1797
|
+
const spanUpdates = grClient.getByType('span-update');
|
|
1798
|
+
const guardrailUpdate = spanUpdates[spanUpdates.length - 1];
|
|
1799
|
+
expect(guardrailUpdate.body.level).toBe('ERROR');
|
|
1800
|
+
expect(guardrailUpdate.body.statusMessage).toBe(
|
|
1801
|
+
'BLOCKED: policy_violation'
|
|
1802
|
+
);
|
|
1803
|
+
expect(
|
|
1804
|
+
(guardrailUpdate.body.metadata as Record<string, unknown>).outcome
|
|
1805
|
+
).toBe('blocked');
|
|
1806
|
+
expect(
|
|
1807
|
+
(guardrailUpdate.body.metadata as Record<string, unknown>).actionApplied
|
|
1808
|
+
).toBe(true);
|
|
1809
|
+
expect(
|
|
1810
|
+
(guardrailUpdate.body.metadata as Record<string, unknown>).violations
|
|
1811
|
+
).toHaveLength(1);
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
it('outcome=blocked with actionApplied=false should note "not enforced"', async () => {
|
|
1815
|
+
const chainRunId = makeUUID();
|
|
1816
|
+
await grHandler.handleChainStart(
|
|
1817
|
+
grSerialized('RunnableSequence'),
|
|
1818
|
+
{ input: 'test' },
|
|
1819
|
+
chainRunId
|
|
1820
|
+
);
|
|
1821
|
+
|
|
1822
|
+
grHandler.traceGuardrail({
|
|
1823
|
+
name: 'Input Moderation',
|
|
1824
|
+
outcome: 'blocked',
|
|
1825
|
+
actionApplied: false,
|
|
1826
|
+
reason: 'policy_violation',
|
|
1827
|
+
source: 'INPUT',
|
|
1828
|
+
input: 'Some flagged content',
|
|
1829
|
+
});
|
|
1830
|
+
|
|
1831
|
+
const spanUpdates = grClient.getByType('span-update');
|
|
1832
|
+
const guardrailUpdate = spanUpdates[spanUpdates.length - 1];
|
|
1833
|
+
expect(guardrailUpdate.body.level).toBe('ERROR');
|
|
1834
|
+
expect(guardrailUpdate.body.statusMessage).toContain('not enforced');
|
|
1835
|
+
expect(
|
|
1836
|
+
(guardrailUpdate.body.metadata as Record<string, unknown>).actionApplied
|
|
1837
|
+
).toBe(false);
|
|
1838
|
+
});
|
|
1839
|
+
|
|
1840
|
+
it('outcome=anonymized should mark with WARNING and include modified content', async () => {
|
|
1841
|
+
const chainRunId = makeUUID();
|
|
1842
|
+
await grHandler.handleChainStart(
|
|
1843
|
+
grSerialized('RunnableSequence'),
|
|
1844
|
+
{ input: 'test' },
|
|
1845
|
+
chainRunId
|
|
1846
|
+
);
|
|
1847
|
+
|
|
1848
|
+
grHandler.traceGuardrail({
|
|
1849
|
+
name: 'Output Moderation',
|
|
1850
|
+
outcome: 'anonymized',
|
|
1851
|
+
actionApplied: true,
|
|
1852
|
+
action: 'GUARDRAIL_INTERVENED',
|
|
1853
|
+
reason: 'anonymized',
|
|
1854
|
+
source: 'OUTPUT',
|
|
1855
|
+
input: 'My email is john@example.com and SSN is 123-45-6789',
|
|
1856
|
+
originalContent: 'My email is john@example.com and SSN is 123-45-6789',
|
|
1857
|
+
modifiedContent: 'My email is [EMAIL] and SSN is [SSN]',
|
|
1858
|
+
violations: [
|
|
1859
|
+
{ type: 'PII_POLICY', category: 'EMAIL', action: 'ANONYMIZED' },
|
|
1860
|
+
{ type: 'PII_POLICY', category: 'SSN', action: 'ANONYMIZED' },
|
|
1861
|
+
],
|
|
1862
|
+
});
|
|
1863
|
+
|
|
1864
|
+
const spanUpdates = grClient.getByType('span-update');
|
|
1865
|
+
const guardrailUpdate = spanUpdates[spanUpdates.length - 1];
|
|
1866
|
+
expect(guardrailUpdate.body.level).toBe('WARNING');
|
|
1867
|
+
expect(guardrailUpdate.body.statusMessage).toBe(
|
|
1868
|
+
'ANONYMIZED: PII detected and masked'
|
|
1869
|
+
);
|
|
1870
|
+
// output should be the modified content
|
|
1871
|
+
expect(guardrailUpdate.body.output).toBe(
|
|
1872
|
+
'My email is [EMAIL] and SSN is [SSN]'
|
|
1873
|
+
);
|
|
1874
|
+
const meta = guardrailUpdate.body.metadata as Record<string, unknown>;
|
|
1875
|
+
expect(meta.originalContent).toBe(
|
|
1876
|
+
'My email is john@example.com and SSN is 123-45-6789'
|
|
1877
|
+
);
|
|
1878
|
+
expect(meta.modifiedContent).toBe('My email is [EMAIL] and SSN is [SSN]');
|
|
1879
|
+
expect(meta.violations).toHaveLength(2);
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1882
|
+
it('outcome=intervened should mark with WARNING level', async () => {
|
|
1883
|
+
const chainRunId = makeUUID();
|
|
1884
|
+
await grHandler.handleChainStart(
|
|
1885
|
+
grSerialized('RunnableSequence'),
|
|
1886
|
+
{ input: 'test' },
|
|
1887
|
+
chainRunId
|
|
1888
|
+
);
|
|
1889
|
+
|
|
1890
|
+
grHandler.traceGuardrail({
|
|
1891
|
+
name: 'Output Moderation',
|
|
1892
|
+
outcome: 'intervened',
|
|
1893
|
+
actionApplied: true,
|
|
1894
|
+
action: 'GUARDRAIL_INTERVENED',
|
|
1895
|
+
reason: 'intervened_passthrough',
|
|
1896
|
+
source: 'OUTPUT',
|
|
1897
|
+
input: 'Original response text',
|
|
1898
|
+
modifiedContent: 'Modified response text',
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
const spanUpdates = grClient.getByType('span-update');
|
|
1902
|
+
const guardrailUpdate = spanUpdates[spanUpdates.length - 1];
|
|
1903
|
+
expect(guardrailUpdate.body.level).toBe('WARNING');
|
|
1904
|
+
expect(guardrailUpdate.body.statusMessage).toBe(
|
|
1905
|
+
'INTERVENED: intervened_passthrough'
|
|
1906
|
+
);
|
|
1907
|
+
});
|
|
1908
|
+
|
|
1909
|
+
it('traceGuardrail() should return null when no trace exists', () => {
|
|
1910
|
+
const freshClient = new GuardrailMockClient();
|
|
1911
|
+
const freshHandler: any = new ObservabilityCallbackHandler({
|
|
1912
|
+
client: freshClient as any,
|
|
1913
|
+
});
|
|
1914
|
+
|
|
1915
|
+
const result = freshHandler.traceGuardrail({
|
|
1916
|
+
name: 'Pre-trace Guardrail',
|
|
1917
|
+
outcome: 'passed',
|
|
1918
|
+
actionApplied: false,
|
|
1919
|
+
source: 'INPUT',
|
|
1920
|
+
});
|
|
1921
|
+
|
|
1922
|
+
expect(result).toBeNull();
|
|
1923
|
+
expect(freshClient.getByType('guardrail-create')).toHaveLength(0);
|
|
1924
|
+
});
|
|
1925
|
+
|
|
1926
|
+
it('traceGuardrail() should use custom startTime and endTime', async () => {
|
|
1927
|
+
const chainRunId = makeUUID();
|
|
1928
|
+
await grHandler.handleChainStart(
|
|
1929
|
+
grSerialized('Chain'),
|
|
1930
|
+
{ input: 'test' },
|
|
1931
|
+
chainRunId
|
|
1932
|
+
);
|
|
1933
|
+
|
|
1934
|
+
const startTime = '2024-01-01T00:00:00.000Z';
|
|
1935
|
+
const endTime = '2024-01-01T00:00:01.500Z';
|
|
1936
|
+
|
|
1937
|
+
grHandler.traceGuardrail({
|
|
1938
|
+
name: 'Timed Guardrail',
|
|
1939
|
+
outcome: 'passed',
|
|
1940
|
+
actionApplied: false,
|
|
1941
|
+
source: 'OUTPUT',
|
|
1942
|
+
startTime,
|
|
1943
|
+
endTime,
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1946
|
+
const guardrailEvents = grClient.getByType('guardrail-create');
|
|
1947
|
+
expect(guardrailEvents[0].body.startTime).toBe(startTime);
|
|
1948
|
+
|
|
1949
|
+
const spanUpdates = grClient.getByType('span-update');
|
|
1950
|
+
const lastUpdate = spanUpdates[spanUpdates.length - 1];
|
|
1951
|
+
expect(lastUpdate.body.endTime).toBe(endTime);
|
|
1952
|
+
});
|
|
1953
|
+
|
|
1954
|
+
it('should trace both input and output guardrails on the same trace', async () => {
|
|
1955
|
+
const chainRunId = makeUUID();
|
|
1956
|
+
await grHandler.handleChainStart(
|
|
1957
|
+
grSerialized('RunnableSequence'),
|
|
1958
|
+
{ input: 'test' },
|
|
1959
|
+
chainRunId
|
|
1960
|
+
);
|
|
1961
|
+
|
|
1962
|
+
const inputId = grHandler.traceGuardrail({
|
|
1963
|
+
name: 'Input Moderation',
|
|
1964
|
+
outcome: 'passed',
|
|
1965
|
+
actionApplied: false,
|
|
1966
|
+
source: 'INPUT',
|
|
1967
|
+
input: 'What is 2+2?',
|
|
1968
|
+
});
|
|
1969
|
+
|
|
1970
|
+
const outputId = grHandler.traceGuardrail({
|
|
1971
|
+
name: 'Output Moderation',
|
|
1972
|
+
outcome: 'passed',
|
|
1973
|
+
actionApplied: false,
|
|
1974
|
+
source: 'OUTPUT',
|
|
1975
|
+
input: 'The answer is 4.',
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
expect(inputId).not.toBeNull();
|
|
1979
|
+
expect(outputId).not.toBeNull();
|
|
1980
|
+
expect(inputId).not.toBe(outputId);
|
|
1981
|
+
|
|
1982
|
+
const guardrailEvents = grClient.getByType('guardrail-create');
|
|
1983
|
+
expect(guardrailEvents).toHaveLength(2);
|
|
1984
|
+
|
|
1985
|
+
const traceId = guardrailEvents[0].body.traceId;
|
|
1986
|
+
expect(guardrailEvents[1].body.traceId).toBe(traceId);
|
|
1987
|
+
|
|
1988
|
+
expect(
|
|
1989
|
+
(guardrailEvents[0].body.metadata as Record<string, unknown>).source
|
|
1990
|
+
).toBe('INPUT');
|
|
1991
|
+
expect(
|
|
1992
|
+
(guardrailEvents[1].body.metadata as Record<string, unknown>).source
|
|
1993
|
+
).toBe('OUTPUT');
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
it('should include full AWS Bedrock assessments and violations in metadata', async () => {
|
|
1997
|
+
const chainRunId = makeUUID();
|
|
1998
|
+
await grHandler.handleChainStart(
|
|
1999
|
+
grSerialized('Chain'),
|
|
2000
|
+
{ input: 'test' },
|
|
2001
|
+
chainRunId
|
|
2002
|
+
);
|
|
2003
|
+
|
|
2004
|
+
grHandler.traceGuardrail({
|
|
2005
|
+
name: 'Detailed Guardrail',
|
|
2006
|
+
outcome: 'blocked',
|
|
2007
|
+
actionApplied: true,
|
|
2008
|
+
action: 'GUARDRAIL_INTERVENED',
|
|
2009
|
+
reason: 'policy_violation',
|
|
2010
|
+
source: 'INPUT',
|
|
2011
|
+
violations: [
|
|
2012
|
+
{
|
|
2013
|
+
type: 'CONTENT_POLICY',
|
|
2014
|
+
category: 'VIOLENCE',
|
|
2015
|
+
confidence: 'HIGH',
|
|
2016
|
+
action: 'BLOCKED',
|
|
2017
|
+
},
|
|
2018
|
+
{ type: 'PII_POLICY', category: 'SSN', action: 'BLOCKED' },
|
|
2019
|
+
],
|
|
2020
|
+
assessments: [
|
|
2021
|
+
{
|
|
2022
|
+
contentPolicy: {
|
|
2023
|
+
filters: [
|
|
2024
|
+
{ type: 'VIOLENCE', confidence: 'HIGH', action: 'BLOCKED' },
|
|
2025
|
+
],
|
|
2026
|
+
},
|
|
2027
|
+
sensitiveInformationPolicy: {
|
|
2028
|
+
piiEntities: [{ type: 'SSN', action: 'BLOCKED' }],
|
|
2029
|
+
},
|
|
2030
|
+
},
|
|
2031
|
+
],
|
|
2032
|
+
});
|
|
2033
|
+
|
|
2034
|
+
const spanUpdates = grClient.getByType('span-update');
|
|
2035
|
+
const lastUpdate = spanUpdates[spanUpdates.length - 1];
|
|
2036
|
+
const meta = lastUpdate.body.metadata as Record<string, unknown>;
|
|
2037
|
+
|
|
2038
|
+
expect(meta.violations).toHaveLength(2);
|
|
2039
|
+
expect(meta.assessments).toHaveLength(1);
|
|
2040
|
+
expect((meta.violations as any[])[0]).toEqual({
|
|
2041
|
+
type: 'CONTENT_POLICY',
|
|
2042
|
+
category: 'VIOLENCE',
|
|
2043
|
+
confidence: 'HIGH',
|
|
2044
|
+
action: 'BLOCKED',
|
|
2045
|
+
});
|
|
2046
|
+
});
|
|
2047
|
+
});
|
|
2048
|
+
|
|
2049
|
+
// ---------------------------------------------------------------------------
|
|
2050
|
+
// 18. Guardrail tracing with debug logging — full pipeline with all 4 outcomes
|
|
2051
|
+
// ---------------------------------------------------------------------------
|
|
2052
|
+
|
|
2053
|
+
describe('Guardrail tracing with debug output', () => {
|
|
2054
|
+
it('should produce debug-friendly trace output for manual verification', async () => {
|
|
2055
|
+
const debugClient = new GuardrailMockClient();
|
|
2056
|
+
const debugHandler: any = new ObservabilityCallbackHandler({
|
|
2057
|
+
client: debugClient as any,
|
|
2058
|
+
traceName: 'guardrail-debug-test',
|
|
2059
|
+
userId: 'user-123',
|
|
2060
|
+
sessionId: 'session-456',
|
|
2061
|
+
debug: true,
|
|
2062
|
+
});
|
|
2063
|
+
|
|
2064
|
+
// 1. Chain starts (creates trace)
|
|
2065
|
+
const chainRunId = makeUUID();
|
|
2066
|
+
await debugHandler.handleChainStart(
|
|
2067
|
+
grSerialized('RunnableSequence'),
|
|
2068
|
+
{ messages: [{ role: 'user', content: 'Explain quantum computing' }] },
|
|
2069
|
+
chainRunId
|
|
2070
|
+
);
|
|
2071
|
+
|
|
2072
|
+
// 2. LLM call
|
|
2073
|
+
const llmRunId = makeUUID();
|
|
2074
|
+
await debugHandler.handleLLMStart(
|
|
2075
|
+
grSerialized('ChatOpenAI'),
|
|
2076
|
+
['Explain quantum computing'],
|
|
2077
|
+
llmRunId,
|
|
2078
|
+
chainRunId
|
|
2079
|
+
);
|
|
2080
|
+
const llmResult: LLMResult = {
|
|
2081
|
+
generations: [
|
|
2082
|
+
[{ text: 'Quantum computing uses qubits...', generationInfo: {} }],
|
|
2083
|
+
],
|
|
2084
|
+
llmOutput: {
|
|
2085
|
+
tokenUsage: { promptTokens: 10, completionTokens: 25, totalTokens: 35 },
|
|
2086
|
+
modelName: 'gpt-4o',
|
|
2087
|
+
},
|
|
2088
|
+
};
|
|
2089
|
+
await debugHandler.handleLLMEnd(llmResult, llmRunId);
|
|
2090
|
+
await debugHandler.handleChainEnd(
|
|
2091
|
+
{ output: 'Quantum computing uses qubits...' },
|
|
2092
|
+
chainRunId
|
|
2093
|
+
);
|
|
2094
|
+
|
|
2095
|
+
// 3. Input guardrail — passed
|
|
2096
|
+
const inputGuardrailId = debugHandler.traceGuardrail({
|
|
2097
|
+
name: 'Input Moderation (Bedrock)',
|
|
2098
|
+
guardrailId: 'arn:aws:bedrock:us-east-1:123456:guardrail/abc123',
|
|
2099
|
+
guardrailVersion: '3',
|
|
2100
|
+
outcome: 'passed',
|
|
2101
|
+
actionApplied: false,
|
|
2102
|
+
action: 'NONE',
|
|
2103
|
+
reason: 'passed',
|
|
2104
|
+
source: 'INPUT',
|
|
2105
|
+
input: 'Explain quantum computing',
|
|
2106
|
+
startTime: '2024-06-15T10:00:00.000Z',
|
|
2107
|
+
endTime: '2024-06-15T10:00:00.150Z',
|
|
2108
|
+
});
|
|
2109
|
+
|
|
2110
|
+
// 4. Output guardrail — passed
|
|
2111
|
+
const outputGuardrailId = debugHandler.traceGuardrail({
|
|
2112
|
+
name: 'Output Moderation (Bedrock)',
|
|
2113
|
+
guardrailId: 'arn:aws:bedrock:us-east-1:123456:guardrail/abc123',
|
|
2114
|
+
guardrailVersion: '3',
|
|
2115
|
+
outcome: 'passed',
|
|
2116
|
+
actionApplied: false,
|
|
2117
|
+
action: 'NONE',
|
|
2118
|
+
reason: 'passed',
|
|
2119
|
+
source: 'OUTPUT',
|
|
2120
|
+
input: 'Quantum computing uses qubits...',
|
|
2121
|
+
violations: [],
|
|
2122
|
+
assessments: [
|
|
2123
|
+
{ topicPolicy: { topics: [] }, contentPolicy: { filters: [] } },
|
|
2124
|
+
],
|
|
2125
|
+
startTime: '2024-06-15T10:00:01.000Z',
|
|
2126
|
+
endTime: '2024-06-15T10:00:01.200Z',
|
|
2127
|
+
});
|
|
2128
|
+
|
|
2129
|
+
expect(inputGuardrailId).not.toBeNull();
|
|
2130
|
+
expect(outputGuardrailId).not.toBeNull();
|
|
2131
|
+
|
|
2132
|
+
console.log('\n=== SAMPLE GUARDRAIL TRACE OUTPUT ===');
|
|
2133
|
+
debugClient.printEvents();
|
|
2134
|
+
console.log('=== END TRACE ===\n');
|
|
2135
|
+
|
|
2136
|
+
// Verify complete event sequence
|
|
2137
|
+
const types = debugClient.getEventTypes();
|
|
2138
|
+
expect(types).toEqual([
|
|
2139
|
+
'trace-create',
|
|
2140
|
+
'chain-create',
|
|
2141
|
+
'generation-create',
|
|
2142
|
+
'generation-update',
|
|
2143
|
+
'span-update', // chain end
|
|
2144
|
+
'guardrail-create', // input guardrail
|
|
2145
|
+
'span-update', // input guardrail update
|
|
2146
|
+
'guardrail-create', // output guardrail
|
|
2147
|
+
'span-update', // output guardrail update
|
|
2148
|
+
]);
|
|
2149
|
+
|
|
2150
|
+
// Verify trace ID consistency
|
|
2151
|
+
const traceId = debugClient.getByType('trace-create')[0].body.id;
|
|
2152
|
+
for (const evt of debugClient.events) {
|
|
2153
|
+
const body = evt.body;
|
|
2154
|
+
if (evt.type !== 'trace-create') {
|
|
2155
|
+
expect(body.traceId).toBe(traceId);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
});
|
|
2159
|
+
|
|
2160
|
+
it('should trace all 4 guardrail outcomes with correct levels', async () => {
|
|
2161
|
+
const debugClient = new GuardrailMockClient();
|
|
2162
|
+
const handler: any = new ObservabilityCallbackHandler({
|
|
2163
|
+
client: debugClient as any,
|
|
2164
|
+
debug: true,
|
|
2165
|
+
});
|
|
2166
|
+
|
|
2167
|
+
// Create trace
|
|
2168
|
+
const chainRunId = makeUUID();
|
|
2169
|
+
await handler.handleChainStart(
|
|
2170
|
+
grSerialized('Chain'),
|
|
2171
|
+
{ input: 'test' },
|
|
2172
|
+
chainRunId
|
|
2173
|
+
);
|
|
2174
|
+
|
|
2175
|
+
// All 4 outcomes
|
|
2176
|
+
handler.traceGuardrail({
|
|
2177
|
+
name: 'Passed',
|
|
2178
|
+
outcome: 'passed',
|
|
2179
|
+
actionApplied: false,
|
|
2180
|
+
source: 'INPUT',
|
|
2181
|
+
});
|
|
2182
|
+
handler.traceGuardrail({
|
|
2183
|
+
name: 'Blocked',
|
|
2184
|
+
outcome: 'blocked',
|
|
2185
|
+
actionApplied: true,
|
|
2186
|
+
reason: 'policy_violation',
|
|
2187
|
+
source: 'INPUT',
|
|
2188
|
+
});
|
|
2189
|
+
handler.traceGuardrail({
|
|
2190
|
+
name: 'Anonymized',
|
|
2191
|
+
outcome: 'anonymized',
|
|
2192
|
+
actionApplied: true,
|
|
2193
|
+
reason: 'anonymized',
|
|
2194
|
+
source: 'OUTPUT',
|
|
2195
|
+
modifiedContent: '[EMAIL]',
|
|
2196
|
+
});
|
|
2197
|
+
handler.traceGuardrail({
|
|
2198
|
+
name: 'Intervened',
|
|
2199
|
+
outcome: 'intervened',
|
|
2200
|
+
actionApplied: true,
|
|
2201
|
+
reason: 'intervened_passthrough',
|
|
2202
|
+
source: 'OUTPUT',
|
|
2203
|
+
});
|
|
2204
|
+
|
|
2205
|
+
console.log('\n=== ALL 4 GUARDRAIL OUTCOMES ===');
|
|
2206
|
+
debugClient.printEvents();
|
|
2207
|
+
console.log('=== END ===\n');
|
|
2208
|
+
|
|
2209
|
+
const guardrailEvents = debugClient.getByType('guardrail-create');
|
|
2210
|
+
expect(guardrailEvents).toHaveLength(4);
|
|
2211
|
+
|
|
2212
|
+
const spanUpdates = debugClient.getByType('span-update');
|
|
2213
|
+
// 4 guardrail updates (chain not ended in this test)
|
|
2214
|
+
expect(spanUpdates).toHaveLength(4);
|
|
2215
|
+
|
|
2216
|
+
// Check levels: passed=DEFAULT, blocked=ERROR, anonymized=WARNING, intervened=WARNING
|
|
2217
|
+
const guardrailUpdates = spanUpdates;
|
|
2218
|
+
expect(guardrailUpdates[0].body.level).toBe('DEFAULT');
|
|
2219
|
+
expect(guardrailUpdates[1].body.level).toBe('ERROR');
|
|
2220
|
+
expect(guardrailUpdates[2].body.level).toBe('WARNING');
|
|
2221
|
+
expect(guardrailUpdates[3].body.level).toBe('WARNING');
|
|
2222
|
+
});
|
|
2223
|
+
});
|