@juspay/neurolink 12.59.0 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +13 -2
  2. package/dist/auth/anthropicOAuth.js +3 -3
  3. package/dist/auth/middleware/AuthMiddleware.d.ts +1 -1
  4. package/dist/auth/sessionManager.js +4 -4
  5. package/dist/auth/tokenStore.js +4 -4
  6. package/dist/browser/neurolink.min.js +474 -473
  7. package/dist/cli/commands/observability.d.ts +1 -6
  8. package/dist/cli/commands/observability.js +0 -106
  9. package/dist/cli/commands/proxy.js +9 -18
  10. package/dist/cli/commands/telemetry.d.ts +3 -13
  11. package/dist/cli/commands/telemetry.js +7 -354
  12. package/dist/cli/loop/session.js +1 -1
  13. package/dist/client/httpClient.js +1 -1
  14. package/dist/client/sseClient.js +1 -1
  15. package/dist/client/streamingClient.js +2 -2
  16. package/dist/client/wsClient.js +1 -1
  17. package/dist/constants/langfuse.d.ts +25 -0
  18. package/dist/constants/langfuse.js +38 -0
  19. package/dist/constants/telemetry.d.ts +20 -0
  20. package/dist/constants/telemetry.js +34 -0
  21. package/dist/core/baseProvider.d.ts +2 -1
  22. package/dist/core/baseProvider.js +4 -11
  23. package/dist/core/conversationMemoryManager.js +5 -8
  24. package/dist/core/factory.js +1 -1
  25. package/dist/core/geminiLoopAdapter.d.ts +8 -1
  26. package/dist/core/geminiLoopAdapter.js +25 -1
  27. package/dist/core/loopEngine.js +69 -40
  28. package/dist/core/modules/MessageBuilder.js +2 -2
  29. package/dist/core/modules/StreamHandler.js +2 -2
  30. package/dist/core/modules/TelemetryHandler.d.ts +2 -18
  31. package/dist/core/modules/TelemetryHandler.js +1 -66
  32. package/dist/core/modules/ToolsManager.js +40 -31
  33. package/dist/core/nativeGenerateLoop.js +78 -8
  34. package/dist/core/redisConversationMemoryManager.js +8 -15
  35. package/dist/core/toolExecutionGuards.js +1 -13
  36. package/dist/index.d.ts +5 -12
  37. package/dist/index.js +5 -37
  38. package/dist/mcp/batching/requestBatcher.js +1 -1
  39. package/dist/mcp/externalServerManager.js +3 -21
  40. package/dist/mcp/toolDiscoveryService.js +4 -16
  41. package/dist/mcp/toolRegistry.js +2 -5
  42. package/dist/memory/memoryRetrievalTools.js +1 -1
  43. package/dist/neurolink.d.ts +16 -27
  44. package/dist/neurolink.js +788 -792
  45. package/dist/observability/exporterSpanProcessor.d.ts +50 -0
  46. package/dist/observability/exporterSpanProcessor.js +181 -0
  47. package/dist/observability/exporters/langfuseExporter.d.ts +13 -34
  48. package/dist/observability/exporters/langfuseExporter.js +101 -154
  49. package/dist/observability/index.d.ts +1 -0
  50. package/dist/observability/index.js +1 -0
  51. package/dist/observability/langfuseMasking.d.ts +8 -0
  52. package/dist/observability/langfuseMasking.js +47 -0
  53. package/dist/observability/otlpEncoding.d.ts +20 -0
  54. package/dist/observability/otlpEncoding.js +85 -0
  55. package/dist/providers/amazonBedrock/client.js +142 -176
  56. package/dist/providers/amazonBedrock/loopAdapter.js +14 -1
  57. package/dist/providers/amazonSagemaker.js +118 -80
  58. package/dist/providers/anthropic/client.js +3 -52
  59. package/dist/providers/anthropic/loopAdapter.js +24 -1
  60. package/dist/providers/anthropic/rateLimitCapture.js +9 -12
  61. package/dist/providers/googleAiStudio/client.d.ts +4 -5
  62. package/dist/providers/googleAiStudio/client.js +38 -40
  63. package/dist/providers/googleVertex/client.d.ts +2 -7
  64. package/dist/providers/googleVertex/client.js +282 -378
  65. package/dist/providers/googleVertex/constants.js +2 -2
  66. package/dist/providers/litellm/client.d.ts +2 -9
  67. package/dist/providers/litellm/client.js +1 -63
  68. package/dist/providers/openAI/client.d.ts +1 -8
  69. package/dist/providers/openAI/client.js +0 -65
  70. package/dist/providers/openaiChatCompletionsBase.d.ts +1 -0
  71. package/dist/providers/openaiChatCompletionsBase.js +61 -4
  72. package/dist/providers/replicate.js +22 -4
  73. package/dist/proxy/codexUsage.js +7 -0
  74. package/dist/proxy/proxyFetch.js +19 -25
  75. package/dist/proxy/proxyTracer.d.ts +14 -7
  76. package/dist/proxy/proxyTracer.js +129 -83
  77. package/dist/proxy/proxyTranslationEngine.js +0 -1
  78. package/dist/proxy/requestLogger.d.ts +1 -1
  79. package/dist/proxy/requestLogger.js +39 -45
  80. package/dist/server/middleware/common.js +1 -1
  81. package/dist/server/routes/agentRoutes.js +4 -4
  82. package/dist/server/routes/claudeProxyRoutes.js +99 -196
  83. package/dist/server/routes/codexProxyRoutes.js +621 -591
  84. package/dist/server/routes/healthRoutes.js +6 -6
  85. package/dist/server/routes/mcpRoutes.js +7 -7
  86. package/dist/server/routes/memoryRoutes.js +7 -7
  87. package/dist/server/routes/openApiRoutes.js +3 -3
  88. package/dist/server/routes/toolRoutes.js +5 -5
  89. package/dist/server/routes/transcribeRoutes.js +1 -1
  90. package/dist/server/utils/validation.d.ts +3 -3
  91. package/dist/services/server/ai/observability/instrumentation.d.ts +34 -201
  92. package/dist/services/server/ai/observability/instrumentation.js +220 -1267
  93. package/dist/services/server/ai/observability/langfuseSetup.d.ts +14 -0
  94. package/dist/services/server/ai/observability/langfuseSetup.js +55 -0
  95. package/dist/services/server/ai/observability/otlpSetup.d.ts +16 -0
  96. package/dist/services/server/ai/observability/otlpSetup.js +58 -0
  97. package/dist/session/globalSessionState.js +0 -11
  98. package/dist/telemetry/attributes.d.ts +23 -46
  99. package/dist/telemetry/attributes.js +76 -162
  100. package/dist/telemetry/chatMessages.d.ts +13 -0
  101. package/dist/telemetry/chatMessages.js +59 -0
  102. package/dist/telemetry/index.d.ts +5 -19
  103. package/dist/telemetry/index.js +5 -26
  104. package/dist/telemetry/observation.d.ts +37 -0
  105. package/dist/telemetry/observation.js +112 -0
  106. package/dist/telemetry/observationNames.d.ts +18 -0
  107. package/dist/telemetry/observationNames.js +24 -0
  108. package/dist/telemetry/spanSanitizer.d.ts +39 -0
  109. package/dist/telemetry/spanSanitizer.js +92 -0
  110. package/dist/telemetry/spanSerialization.d.ts +13 -0
  111. package/dist/telemetry/spanSerialization.js +46 -0
  112. package/dist/telemetry/tracers.d.ts +37 -18
  113. package/dist/telemetry/tracers.js +97 -19
  114. package/dist/telemetry/withSpan.d.ts +47 -25
  115. package/dist/telemetry/withSpan.js +158 -71
  116. package/dist/types/cli.d.ts +0 -11
  117. package/dist/types/decision.d.ts +3 -0
  118. package/dist/types/exporter.d.ts +54 -0
  119. package/dist/types/exporter.js +0 -4
  120. package/dist/types/generate.d.ts +4 -1
  121. package/dist/types/isolatedAgent.d.ts +5 -4
  122. package/dist/types/loopEngine.d.ts +18 -28
  123. package/dist/types/observability.d.ts +127 -160
  124. package/dist/types/providers.d.ts +15 -0
  125. package/dist/types/proxy.d.ts +41 -2
  126. package/dist/utils/anthropicTraceSanitizer.d.ts +2 -2
  127. package/dist/utils/anthropicTraceSanitizer.js +3 -1
  128. package/dist/utils/conversationMemory.js +4 -4
  129. package/dist/utils/fileDetector.js +1 -1
  130. package/dist/utils/messageBuilder.js +1 -1
  131. package/dist/utils/noOutputSentinel.d.ts +4 -16
  132. package/dist/utils/noOutputSentinel.js +12 -28
  133. package/dist/utils/providerRetry.js +4 -4
  134. package/dist/voice/livekit/brain.js +1 -1
  135. package/dist/voice/livekit/realtimeMemory.d.ts +1 -1
  136. package/dist/voice/livekit/realtimeMemory.js +66 -1
  137. package/dist/voice/livekit/realtimeVoiceAgent.js +1 -1
  138. package/docs-site/static/search-index.json +41 -63
  139. package/package.json +5 -4
  140. package/dist/telemetry/telemetryService.d.ts +0 -79
  141. package/dist/telemetry/telemetryService.js +0 -572
  142. package/dist/utils/observabilityHelpers.d.ts +0 -27
  143. package/dist/utils/observabilityHelpers.js +0 -47
@@ -0,0 +1,20 @@
1
+ import type { LangfuseAttributePolicy, OtlpKeyValue, OtlpSpan, SpanAttributes, SpanData } from "../types/index.js";
2
+ /**
3
+ * @description Encodes attributes as OTLP/JSON key-values, dropping `undefined` and `null` entries.
4
+ * @param attributes {SpanAttributes} - Attribute record to encode.
5
+ * @returns {OtlpKeyValue[]} One typed key-value per kept attribute.
6
+ */
7
+ export declare function toOtlpAttributes(attributes: SpanAttributes): OtlpKeyValue[];
8
+ /**
9
+ * @description Encodes one span as OTLP/JSON, applying the exporter's attribute policy to its attributes.
10
+ * @param span {SpanData} - Span to encode.
11
+ * @param policy {LangfuseAttributePolicy} - Environment, release, redaction and mask settings.
12
+ * @returns {OtlpSpan} The span in the shape Langfuse's ingestion endpoint reads.
13
+ */
14
+ export declare function toOtlpSpan(span: SpanData, policy: LangfuseAttributePolicy): OtlpSpan;
15
+ /**
16
+ * @description Splits encoded spans into request-sized batches, bounded by span count and serialized bytes.
17
+ * @param spans {OtlpSpan[]} - Encoded spans, in export order.
18
+ * @returns {OtlpSpan[][]} Batches that each fit one export request; a single oversized span still gets its own batch.
19
+ */
20
+ export declare function chunkOtlpSpans(spans: OtlpSpan[]): OtlpSpan[][];
@@ -0,0 +1,85 @@
1
+ import { MAX_BYTES_PER_REQUEST, MAX_SPANS_PER_REQUEST, OTLP_STATUS_CODES, } from "../constants/langfuse.js";
2
+ import { SpanStatus } from "../types/index.js";
3
+ import { protectAttributes } from "./langfuseMasking.js";
4
+ const SPAN_KIND_INTERNAL = 1;
5
+ function toNanos(iso) {
6
+ return (BigInt(Date.parse(iso)) * 1000000n).toString();
7
+ }
8
+ function toOtlpValue(value) {
9
+ if (typeof value === "boolean") {
10
+ return { boolValue: value };
11
+ }
12
+ if (typeof value === "number") {
13
+ return Number.isInteger(value)
14
+ ? { intValue: String(value) }
15
+ : { doubleValue: value };
16
+ }
17
+ if (Array.isArray(value)) {
18
+ return { arrayValue: { values: value.map(toOtlpValue) } };
19
+ }
20
+ return { stringValue: String(value) };
21
+ }
22
+ /**
23
+ * @description Encodes attributes as OTLP/JSON key-values, dropping `undefined` and `null` entries.
24
+ * @param attributes {SpanAttributes} - Attribute record to encode.
25
+ * @returns {OtlpKeyValue[]} One typed key-value per kept attribute.
26
+ */
27
+ export function toOtlpAttributes(attributes) {
28
+ return Object.entries(attributes)
29
+ .filter(([, value]) => value !== undefined && value !== null)
30
+ .map(([key, value]) => ({ key, value: toOtlpValue(value) }));
31
+ }
32
+ /**
33
+ * @description Encodes one span as OTLP/JSON, applying the exporter's attribute policy to its attributes.
34
+ * @param span {SpanData} - Span to encode.
35
+ * @param policy {LangfuseAttributePolicy} - Environment, release, redaction and mask settings.
36
+ * @returns {OtlpSpan} The span in the shape Langfuse's ingestion endpoint reads.
37
+ */
38
+ export function toOtlpSpan(span, policy) {
39
+ const failed = span.status === SpanStatus.ERROR;
40
+ return {
41
+ traceId: span.traceId,
42
+ spanId: span.spanId,
43
+ ...(span.parentSpanId && { parentSpanId: span.parentSpanId }),
44
+ name: span.name,
45
+ kind: SPAN_KIND_INTERNAL,
46
+ startTimeUnixNano: toNanos(span.startTime),
47
+ endTimeUnixNano: toNanos(span.endTime ?? span.startTime),
48
+ attributes: toOtlpAttributes(protectAttributes(span.attributes, policy)),
49
+ events: span.events.map((event) => ({
50
+ name: event.name,
51
+ timeUnixNano: toNanos(event.timestamp),
52
+ attributes: toOtlpAttributes(event.attributes ?? {}),
53
+ })),
54
+ status: {
55
+ code: OTLP_STATUS_CODES[span.status],
56
+ ...(failed && span.statusMessage && { message: span.statusMessage }),
57
+ },
58
+ };
59
+ }
60
+ /**
61
+ * @description Splits encoded spans into request-sized batches, bounded by span count and serialized bytes.
62
+ * @param spans {OtlpSpan[]} - Encoded spans, in export order.
63
+ * @returns {OtlpSpan[][]} Batches that each fit one export request; a single oversized span still gets its own batch.
64
+ */
65
+ export function chunkOtlpSpans(spans) {
66
+ const chunks = [];
67
+ let current = [];
68
+ let bytes = 0;
69
+ for (const span of spans) {
70
+ const size = Buffer.byteLength(JSON.stringify(span));
71
+ if (current.length > 0 &&
72
+ (current.length >= MAX_SPANS_PER_REQUEST ||
73
+ bytes + size > MAX_BYTES_PER_REQUEST)) {
74
+ chunks.push(current);
75
+ current = [];
76
+ bytes = 0;
77
+ }
78
+ current.push(span);
79
+ bytes += size;
80
+ }
81
+ if (current.length > 0) {
82
+ chunks.push(current);
83
+ }
84
+ return chunks;
85
+ }
@@ -601,10 +601,7 @@ export class AmazonBedrockProvider extends BaseProvider {
601
601
  async executeSingleTool(tools, toolName, args, _toolUseId) {
602
602
  return bedrockTracer.startActiveSpan("bedrock.tool.execute", {
603
603
  kind: SpanKind.CLIENT,
604
- attributes: {
605
- "gen_ai.tool.name": toolName,
606
- "gen_ai.system": "aws.bedrock",
607
- },
604
+ attributes: { "tool.name": toolName },
608
605
  }, async (span) => {
609
606
  try {
610
607
  logger.debug(`[AmazonBedrockProvider] Executing single tool: ${toolName}`, {
@@ -711,8 +708,9 @@ export class AmazonBedrockProvider extends BaseProvider {
711
708
  if (disableTools) {
712
709
  return {};
713
710
  }
714
- const aiTools = optionTools ??
715
- (await this.getAllTools());
711
+ const aiTools = optionTools
712
+ ? this.toolsManager.wrapExternalToolsWithEvents(optionTools)
713
+ : await this.getAllTools();
716
714
  return this.convertAISDKToolsToToolDefinitions(aiTools);
717
715
  }
718
716
  /**
@@ -896,177 +894,149 @@ export class AmazonBedrockProvider extends BaseProvider {
896
894
  async executeStream(options) {
897
895
  logger.debug("[TRACE] executeStream ENTRY - starting streaming attempt");
898
896
  logger.info("[AmazonBedrockProvider] Attempting real streaming with ConverseStreamCommand");
899
- return bedrockTracer.startActiveSpan("bedrock.stream", {
900
- kind: SpanKind.CLIENT,
901
- attributes: {
902
- "gen_ai.system": "aws.bedrock",
903
- "gen_ai.request.model": this.modelName || this.getDefaultModel(),
904
- "gen_ai.operation.name": "stream",
905
- },
906
- }, async (streamSpan) => {
907
- try {
908
- logger.debug("[TRACE] executeStream TRY block - about to call streamingConversationLoop");
909
- // Clear conversation history for new streaming session
910
- this.conversationHistory = [];
911
- // Check for multimodal input (images, PDFs, CSVs, files)
912
- const hasMultimodalInput = !!(options.input?.images?.length ||
913
- options.input?.content?.length ||
914
- options.input?.files?.length ||
915
- options.input?.csvFiles?.length ||
916
- options.input?.pdfFiles?.length);
917
- if (hasMultimodalInput) {
918
- logger.debug(`[AmazonBedrockProvider] Detected multimodal input, using multimodal message builder`, {
919
- hasImages: !!options.input?.images?.length,
920
- imageCount: options.input?.images?.length || 0,
921
- hasContent: !!options.input?.content?.length,
922
- contentCount: options.input?.content?.length || 0,
923
- hasFiles: !!options.input?.files?.length,
924
- fileCount: options.input?.files?.length || 0,
925
- hasCSVFiles: !!options.input?.csvFiles?.length,
926
- csvFileCount: options.input?.csvFiles?.length || 0,
927
- hasPDFFiles: !!options.input?.pdfFiles?.length,
928
- pdfFileCount: options.input?.pdfFiles?.length || 0,
929
- });
930
- // Same instance-default fold as the generate() path above.
931
- this.applyToolReplayDefault(options);
932
- const multimodalOptions = buildMultimodalOptions(options, this.providerName, this.modelName);
933
- const multimodalMessages = await buildMultimodalMessagesArray(multimodalOptions, this.providerName, this.modelName);
934
- // Convert to Bedrock format
935
- this.conversationHistory =
936
- this.convertToBedrockMessages(multimodalMessages);
937
- }
938
- else {
939
- logger.debug(`[AmazonBedrockProvider] Text-only input, using simple message builder`);
940
- // Add user message to conversation - simple text-only case
941
- const userMessage = {
942
- role: "user",
943
- content: [{ text: options.input.text }],
944
- };
945
- this.conversationHistory.push(userMessage);
946
- }
947
- logger.debug(`[AmazonBedrockProvider] Starting streaming conversation with ${this.conversationHistory.length} message(s)`);
948
- // Call the actual streaming implementation that already exists
949
- logger.debug("[TRACE] executeStream - calling streamingConversationLoop NOW");
950
- const result = await this.streamingConversationLoop(options, streamSpan);
951
- logger.debug("[TRACE] executeStream - streamingConversationLoop SUCCESS, returning result");
952
- streamSpan.setStatus({ code: SpanStatusCode.OK });
953
- streamSpan.end();
954
- return result;
897
+ try {
898
+ logger.debug("[TRACE] executeStream TRY block - about to call streamingConversationLoop");
899
+ // Clear conversation history for new streaming session
900
+ this.conversationHistory = [];
901
+ // Check for multimodal input (images, PDFs, CSVs, files)
902
+ const hasMultimodalInput = !!(options.input?.images?.length ||
903
+ options.input?.content?.length ||
904
+ options.input?.files?.length ||
905
+ options.input?.csvFiles?.length ||
906
+ options.input?.pdfFiles?.length);
907
+ if (hasMultimodalInput) {
908
+ logger.debug(`[AmazonBedrockProvider] Detected multimodal input, using multimodal message builder`, {
909
+ hasImages: !!options.input?.images?.length,
910
+ imageCount: options.input?.images?.length || 0,
911
+ hasContent: !!options.input?.content?.length,
912
+ contentCount: options.input?.content?.length || 0,
913
+ hasFiles: !!options.input?.files?.length,
914
+ fileCount: options.input?.files?.length || 0,
915
+ hasCSVFiles: !!options.input?.csvFiles?.length,
916
+ csvFileCount: options.input?.csvFiles?.length || 0,
917
+ hasPDFFiles: !!options.input?.pdfFiles?.length,
918
+ pdfFileCount: options.input?.pdfFiles?.length || 0,
919
+ });
920
+ // Same instance-default fold as the generate() path above.
921
+ this.applyToolReplayDefault(options);
922
+ const multimodalOptions = buildMultimodalOptions(options, this.providerName, this.modelName);
923
+ const multimodalMessages = await buildMultimodalMessagesArray(multimodalOptions, this.providerName, this.modelName);
924
+ // Convert to Bedrock format
925
+ this.conversationHistory =
926
+ this.convertToBedrockMessages(multimodalMessages);
955
927
  }
956
- catch (error) {
957
- logger.debug("[TRACE] executeStream CATCH - error caught from streamingConversationLoop");
958
- const errorObj = error;
959
- // Check if error is related to streaming permissions. Gated on the
960
- // real AWS SDK service-exception shape ($metadata) first: a
961
- // middleware's wrapStream/transformParams can throw an arbitrary
962
- // Error whose message happens to contain "streaming" or whose name
963
- // happens to collide with an AWS exception name, and without this
964
- // guard that error was silently swallowed and retried as if AWS
965
- // itself had rejected the streaming call (see
966
- // BedrockRuntimeServiceException in the AWS SDK, which carries
967
- // $fault/$metadata on every genuine service exception).
968
- const hasAwsServiceExceptionShape = typeof errorObj === "object" &&
969
- errorObj !== null &&
970
- "$metadata" in errorObj;
971
- const isPermissionError = hasAwsServiceExceptionShape &&
972
- (errorObj?.name === "AccessDeniedException" ||
973
- errorObj?.name === "UnauthorizedOperation" ||
974
- errorObj?.message?.includes("bedrock:InvokeModelWithResponseStream") ||
975
- errorObj?.message?.includes("streaming") ||
976
- errorObj?.message?.includes("ConverseStream"));
977
- logger.debug("[TRACE] executeStream CATCH - checking if permission error");
978
- logger.debug(`[TRACE] executeStream CATCH - isPermissionError=${isPermissionError}`);
979
- if (isPermissionError) {
980
- logger.debug("[TRACE] executeStream CATCH - PERMISSION ERROR DETECTED, starting fallback");
981
- logger.warn(`[AmazonBedrockProvider] Streaming permissions not available, falling back to generate method: ${errorObj.message}`);
982
- streamSpan.addEvent("stream.fallback_to_generate", {
983
- reason: errorObj.message,
984
- });
985
- // Fallback to generate method and convert to streaming format
986
- const generateResult = await this.generate({
987
- prompt: options.input.text,
988
- input: options.input,
989
- maxTokens: options.maxTokens,
990
- temperature: options.temperature,
991
- systemPrompt: options.systemPrompt,
992
- });
993
- if (!generateResult) {
994
- streamSpan.setStatus({
995
- code: SpanStatusCode.ERROR,
996
- message: "Generate method returned null result",
997
- });
998
- streamSpan.end();
999
- // eslint-disable-next-line preserve-caught-error
1000
- throw new Error("Generate method returned null result");
1001
- }
1002
- streamSpan.setAttribute("gen_ai.response.stop_reason", "fallback_end_turn");
1003
- streamSpan.setStatus({ code: SpanStatusCode.OK });
1004
- streamSpan.end();
1005
- // Convert generate result to streaming format.
1006
- // Use whitespace-preserving split (matches BaseProvider's
1007
- // executeFakeStreaming) so newlines, tabs, indentation, code
1008
- // blocks, and markdown tables aren't collapsed to single spaces.
1009
- const stream = new ReadableStream({
1010
- start(controller) {
1011
- const responseText = generateResult.content || "";
1012
- const tokens = responseText.split(/(\s+)/);
1013
- let buffer = "";
1014
- for (let i = 0; i < tokens.length; i++) {
1015
- buffer += tokens[i];
1016
- const shouldYield = i === tokens.length - 1 ||
1017
- buffer.length > 50 ||
1018
- /[.!?;,]\s*$/.test(buffer);
1019
- if (shouldYield && buffer.length > 0) {
1020
- controller.enqueue({ content: buffer });
1021
- buffer = "";
1022
- }
1023
- }
1024
- if (buffer.length > 0) {
928
+ else {
929
+ logger.debug(`[AmazonBedrockProvider] Text-only input, using simple message builder`);
930
+ // Add user message to conversation - simple text-only case
931
+ const userMessage = {
932
+ role: "user",
933
+ content: [{ text: options.input.text }],
934
+ };
935
+ this.conversationHistory.push(userMessage);
936
+ }
937
+ logger.debug(`[AmazonBedrockProvider] Starting streaming conversation with ${this.conversationHistory.length} message(s)`);
938
+ // Call the actual streaming implementation that already exists
939
+ logger.debug("[TRACE] executeStream - calling streamingConversationLoop NOW");
940
+ const result = await this.streamingConversationLoop(options);
941
+ logger.debug("[TRACE] executeStream - streamingConversationLoop SUCCESS, returning result");
942
+ return result;
943
+ }
944
+ catch (error) {
945
+ logger.debug("[TRACE] executeStream CATCH - error caught from streamingConversationLoop");
946
+ const errorObj = error;
947
+ // Check if error is related to streaming permissions. Gated on the
948
+ // real AWS SDK service-exception shape ($metadata) first: a
949
+ // middleware's wrapStream/transformParams can throw an arbitrary
950
+ // Error whose message happens to contain "streaming" or whose name
951
+ // happens to collide with an AWS exception name, and without this
952
+ // guard that error was silently swallowed and retried as if AWS
953
+ // itself had rejected the streaming call (see
954
+ // BedrockRuntimeServiceException in the AWS SDK, which carries
955
+ // $fault/$metadata on every genuine service exception).
956
+ const hasAwsServiceExceptionShape = typeof errorObj === "object" &&
957
+ errorObj !== null &&
958
+ "$metadata" in errorObj;
959
+ const isPermissionError = hasAwsServiceExceptionShape &&
960
+ (errorObj?.name === "AccessDeniedException" ||
961
+ errorObj?.name === "UnauthorizedOperation" ||
962
+ errorObj?.message?.includes("bedrock:InvokeModelWithResponseStream") ||
963
+ errorObj?.message?.includes("streaming") ||
964
+ errorObj?.message?.includes("ConverseStream"));
965
+ logger.debug("[TRACE] executeStream CATCH - checking if permission error");
966
+ logger.debug(`[TRACE] executeStream CATCH - isPermissionError=${isPermissionError}`);
967
+ if (isPermissionError) {
968
+ logger.debug("[TRACE] executeStream CATCH - PERMISSION ERROR DETECTED, starting fallback");
969
+ logger.warn(`[AmazonBedrockProvider] Streaming permissions not available, falling back to generate method: ${errorObj.message}`);
970
+ // Fallback to generate method and convert to streaming format
971
+ const generateResult = await this.generate({
972
+ prompt: options.input.text,
973
+ input: options.input,
974
+ maxTokens: options.maxTokens,
975
+ temperature: options.temperature,
976
+ systemPrompt: options.systemPrompt,
977
+ });
978
+ if (!generateResult) {
979
+ // eslint-disable-next-line preserve-caught-error
980
+ throw new Error("Generate method returned null result");
981
+ }
982
+ // Convert generate result to streaming format.
983
+ // Use whitespace-preserving split (matches BaseProvider's
984
+ // executeFakeStreaming) so newlines, tabs, indentation, code
985
+ // blocks, and markdown tables aren't collapsed to single spaces.
986
+ const stream = new ReadableStream({
987
+ start(controller) {
988
+ const responseText = generateResult.content || "";
989
+ const tokens = responseText.split(/(\s+)/);
990
+ let buffer = "";
991
+ for (let i = 0; i < tokens.length; i++) {
992
+ buffer += tokens[i];
993
+ const shouldYield = i === tokens.length - 1 ||
994
+ buffer.length > 50 ||
995
+ /[.!?;,]\s*$/.test(buffer);
996
+ if (shouldYield && buffer.length > 0) {
1025
997
  controller.enqueue({ content: buffer });
998
+ buffer = "";
1026
999
  }
1027
- controller.close();
1028
- },
1029
- });
1030
- // Convert ReadableStream to AsyncIterable like streamingConversationLoop does
1031
- const asyncIterable = {
1032
- async *[Symbol.asyncIterator]() {
1033
- const reader = stream.getReader();
1034
- try {
1035
- while (true) {
1036
- const { done, value } = await reader.read();
1037
- if (done) {
1038
- break;
1039
- }
1040
- yield value;
1000
+ }
1001
+ if (buffer.length > 0) {
1002
+ controller.enqueue({ content: buffer });
1003
+ }
1004
+ controller.close();
1005
+ },
1006
+ });
1007
+ // Convert ReadableStream to AsyncIterable like streamingConversationLoop does
1008
+ const asyncIterable = {
1009
+ async *[Symbol.asyncIterator]() {
1010
+ const reader = stream.getReader();
1011
+ try {
1012
+ while (true) {
1013
+ const { done, value } = await reader.read();
1014
+ if (done) {
1015
+ break;
1041
1016
  }
1017
+ yield value;
1042
1018
  }
1043
- finally {
1044
- reader.releaseLock();
1045
- }
1046
- },
1047
- };
1048
- return {
1049
- stream: asyncIterable,
1050
- // The generate() fallback already computed real usage — a
1051
- // hardcoded zero object here threw it away.
1052
- usage: generateResult.usage,
1053
- model: this.modelName || this.getDefaultModel(),
1054
- provider: this.getProviderName(),
1055
- metadata: {
1056
- fallback: true,
1057
- },
1058
- };
1059
- }
1060
- // Re-throw non-permission errors
1061
- streamSpan.setStatus({
1062
- code: SpanStatusCode.ERROR,
1063
- message: errorObj instanceof Error ? errorObj.message : String(errorObj),
1064
- });
1065
- streamSpan.recordException(errorObj instanceof Error ? errorObj : new Error(String(errorObj)));
1066
- streamSpan.end();
1067
- throw error;
1019
+ }
1020
+ finally {
1021
+ reader.releaseLock();
1022
+ }
1023
+ },
1024
+ };
1025
+ return {
1026
+ stream: asyncIterable,
1027
+ // The generate() fallback already computed real usage — a
1028
+ // hardcoded zero object here threw it away.
1029
+ usage: generateResult.usage,
1030
+ model: this.modelName || this.getDefaultModel(),
1031
+ provider: this.getProviderName(),
1032
+ metadata: {
1033
+ fallback: true,
1034
+ },
1035
+ };
1068
1036
  }
1069
- });
1037
+ // Re-throw non-permission errors
1038
+ throw error;
1039
+ }
1070
1040
  }
1071
1041
  /**
1072
1042
  * A multimodal turn skips model middleware (see modelBridge.ts's header).
@@ -1214,7 +1184,7 @@ export class AmazonBedrockProvider extends BaseProvider {
1214
1184
  ...(cacheWrite > 0 && { cacheCreationTokens: cacheWrite }),
1215
1185
  };
1216
1186
  }
1217
- async streamingConversationLoop(options, streamSpan) {
1187
+ async streamingConversationLoop(options) {
1218
1188
  const startTime = Date.now();
1219
1189
  const maxSteps = options.maxSteps || DEFAULT_MAX_STEPS;
1220
1190
  // Resolved once for the whole turn. Bedrock has no mid-turn tool
@@ -1290,9 +1260,6 @@ export class AmazonBedrockProvider extends BaseProvider {
1290
1260
  return stepResult;
1291
1261
  },
1292
1262
  };
1293
- streamSpan.addEvent("stream.api_call", {
1294
- "bedrock.tool_count": toolConfig?.tools?.length ?? 0,
1295
- });
1296
1263
  return runAgenticLoop(adapter, conversation, {
1297
1264
  tools: this.toEngineTools(tools),
1298
1265
  abortSignal: signal,
@@ -1397,7 +1364,6 @@ export class AmazonBedrockProvider extends BaseProvider {
1397
1364
  // Read after drain by the facade's fallbackOnMaxSteps gate. Keep
1398
1365
  // the engine's own outcome; raw tool_use alone is not a step cap.
1399
1366
  metadata.stopReason = outcome.result.stopReason;
1400
- streamSpan.setAttribute("gen_ai.response.stop_reason", outcome.result.rawStopReason ?? "unknown");
1401
1367
  }
1402
1368
  // Analytics is resolved off `settled` above, so it lands whether or
1403
1369
  // not anyone drains the stream.
@@ -211,7 +211,20 @@ export function createBedrockLoopAdapter(config) {
211
211
  // No toolFailureBreaker: Bedrock has never had strike counting — a failing
212
212
  // tool becomes one error tool-result with no cross-step memory. Preserved.
213
213
  buildStepRequest(conversation, step) {
214
- return { raw: config.buildCommandInput(conversation, step) };
214
+ const input = config.buildCommandInput(conversation, step);
215
+ return {
216
+ raw: input,
217
+ generation: {
218
+ provider: "bedrock",
219
+ model: input.modelId,
220
+ modelParameters: {
221
+ temperature: input.inferenceConfig?.temperature,
222
+ maxTokens: input.inferenceConfig?.maxTokens,
223
+ topP: input.inferenceConfig?.topP,
224
+ },
225
+ input: { system: input.system, messages: input.messages },
226
+ },
227
+ };
215
228
  },
216
229
  async executeStep(request, channel, signal) {
217
230
  const commandInput = request.raw;