{"version":3,"file":"StreamingToolCallBuffer.mjs","sources":["../../../src/tools/StreamingToolCallBuffer.ts"],"sourcesContent":["/**\n * StreamingToolCallBuffer — Accumulates raw tool call argument strings\n * during streaming, providing a fallback when LangChain's parsePartialJson\n * loses data (e.g., when max_tokens truncates a tool call mid-JSON).\n *\n * Architecture:\n * - handleToolCallChunks feeds raw arg fragments into this buffer\n * - When a tool call is finalized (or truncated), the ToolNode can access\n *   the accumulated raw string and extract field values that parsePartialJson\n *   may have dropped\n * - This enables \"streaming write\" semantics: content that streamed to the UI\n *   is never lost, even if the JSON wrapper was incomplete\n *\n * @example\n * ```ts\n * const buffer = new StreamingToolCallBuffer();\n *\n * // During streaming (called by handleToolCallChunks):\n * buffer.append(toolCallId, '{\"action\":\"write\",\"content\":\"func');\n * buffer.append(toolCallId, 'tion App() {');\n * buffer.setToolName(toolCallId, 'content_tool');\n *\n * // When ToolNode needs the content:\n * const content = buffer.extractFieldValue(toolCallId, 'content');\n * // => 'function App() {'\n * ```\n */\n\nexport class StreamingToolCallBuffer {\n  /** Raw accumulated arg strings keyed by tool call ID */\n  private argBuffers: Map<string, string> = new Map();\n  /** Tool names keyed by tool call ID */\n  private toolNames: Map<string, string> = new Map();\n  /**\n   * Maps chunk index → tool call ID for providers (e.g., Bedrock) that send\n   * START chunks with {id, name, index} and DELTA chunks with {args, index} (no id).\n   * Without this mapping, DELTA chunks cannot be associated with the correct buffer entry.\n   */\n  private indexToIdMap: Map<number, string> = new Map();\n  /**\n   * Tracks the last appended args fragment per tool call ID.\n   * Used to skip exact duplicate fragments caused by LangChain's `streamEvents`\n   * emitting the same `on_chat_model_stream` event multiple times when the model\n   * is wrapped in nested runnables (e.g., `.bind()`, `.withConfig()`).\n   */\n  private lastChunks: Map<string, string> = new Map();\n\n  /**\n   * Append a raw argument string chunk for a tool call.\n   * Called during streaming as each ToolCallChunk arrives.\n   *\n   * Skips exact duplicate fragments that arrive when LangChain emits the same\n   * streaming event multiple times from nested runnable wrappers. Without this,\n   * each token's args appear twice, producing malformed JSON like:\n   * `{\"action\": \"write\"action\": \"write\"...`\n   *\n   * @returns true if the chunk was appended, false if it was a duplicate\n   */\n  append(toolCallId: string, argsChunk: string): boolean {\n    if (!argsChunk) return false;\n\n    // Skip exact duplicate of the last appended fragment\n    const lastChunk = this.lastChunks.get(toolCallId);\n    if (lastChunk === argsChunk) {\n      return false;\n    }\n    this.lastChunks.set(toolCallId, argsChunk);\n\n    const existing = this.argBuffers.get(toolCallId) ?? '';\n    this.argBuffers.set(toolCallId, existing + argsChunk);\n    return true;\n  }\n\n  /**\n   * Record the tool name for a tool call ID.\n   * Usually available from the first chunk.\n   */\n  setToolName(toolCallId: string, name: string): void {\n    if (name && !this.toolNames.has(toolCallId)) {\n      this.toolNames.set(toolCallId, name);\n    }\n  }\n\n  /**\n   * Get the tool name for a tool call ID.\n   */\n  getToolName(toolCallId: string): string | undefined {\n    return this.toolNames.get(toolCallId);\n  }\n\n  /**\n   * Get the raw accumulated argument string for a tool call.\n   */\n  getRawArgs(toolCallId: string): string | undefined {\n    return this.argBuffers.get(toolCallId);\n  }\n\n  /**\n   * Extract a string field value from the raw accumulated JSON args.\n   *\n   * Uses simple string parsing (not JSON.parse) to handle truncated JSON.\n   * Finds `\"fieldName\":\"` and extracts everything after it until the next\n   * unescaped quote or end of buffer.\n   *\n   * @param toolCallId - The tool call ID\n   * @param fieldName - The JSON field name to extract (e.g., 'content')\n   * @returns The extracted value, or undefined if the field wasn't found\n   */\n  extractFieldValue(toolCallId: string, fieldName: string): string | undefined {\n    const raw = this.argBuffers.get(toolCallId);\n    if (!raw) return undefined;\n\n    // Find the field key in the JSON string: \"fieldName\":\"\n    // Handle both `\"fieldName\":\"value\"` and `\"fieldName\": \"value\"` (with space)\n    const patterns = [\n      `\"${fieldName}\":\"`,\n      `\"${fieldName}\": \"`,\n      `\"${fieldName}\" : \"`,\n    ];\n\n    let startIdx = -1;\n    for (const pattern of patterns) {\n      const idx = raw.indexOf(pattern);\n      if (idx !== -1) {\n        startIdx = idx + pattern.length;\n        break;\n      }\n    }\n\n    if (startIdx === -1) return undefined;\n\n    // Extract the value — handle escaped characters\n    let value = '';\n    let escaped = false;\n    for (let i = startIdx; i < raw.length; i++) {\n      const ch = raw[i];\n      if (escaped) {\n        // Handle common escape sequences\n        switch (ch) {\n          case 'n':\n            value += '\\n';\n            break;\n          case 't':\n            value += '\\t';\n            break;\n          case 'r':\n            value += '\\r';\n            break;\n          case '\"':\n            value += '\"';\n            break;\n          case '\\\\':\n            value += '\\\\';\n            break;\n          default:\n            value += ch;\n            break;\n        }\n        escaped = false;\n        continue;\n      }\n      if (ch === '\\\\') {\n        escaped = true;\n        continue;\n      }\n      if (ch === '\"') {\n        // End of the string value\n        break;\n      }\n      value += ch;\n    }\n\n    return value || undefined;\n  }\n\n  /**\n   * Store an index → tool call ID mapping.\n   * Called when a START chunk arrives with both `id` and `index` (e.g., Bedrock).\n   * Subsequent DELTA chunks that only have `index` can resolve the ID via getIdByIndex.\n   */\n  setIndexMapping(index: number, toolCallId: string): void {\n    this.indexToIdMap.set(index, toolCallId);\n  }\n\n  /**\n   * Resolve a tool call ID from a chunk index.\n   * Returns undefined if no mapping exists (e.g., provider sends id on every chunk).\n   */\n  getIdByIndex(index: number): string | undefined {\n    return this.indexToIdMap.get(index);\n  }\n\n  /**\n   * Check if a tool call has accumulated content.\n   */\n  has(toolCallId: string): boolean {\n    return this.argBuffers.has(toolCallId);\n  }\n\n  /**\n   * Clear the buffer for a specific tool call (after processing).\n   */\n  clear(toolCallId: string): void {\n    this.argBuffers.delete(toolCallId);\n    this.toolNames.delete(toolCallId);\n    this.lastChunks.delete(toolCallId);\n  }\n\n  /**\n   * Clear all buffers (at the start of a new callModel cycle).\n   */\n  clearAll(): void {\n    this.argBuffers.clear();\n    this.toolNames.clear();\n    this.indexToIdMap.clear();\n    this.lastChunks.clear();\n  }\n}\n"],"names":[],"mappings":"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"}