{"version":3,"file":"agui-recorder.cjs","names":["https","http","getLastMessageIfToolResult","extractLastUserMessage","crypto","path"],"sources":["../src/agui-recorder.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as crypto from \"node:crypto\";\nimport type {\n  AGUIFixture,\n  AGUIFixtureMatch,\n  AGUIRecordConfig,\n  AGUIEvent,\n  AGUIRunAgentInput,\n} from \"./agui-types.js\";\nimport { extractLastUserMessage, getLastMessageIfToolResult } from \"./agui-handler.js\";\nimport type { Logger } from \"./logger.js\";\n\n/**\n * Sentinel `match.message` value written to disk when the request had no\n * extractable user text. Keeps the on-disk fixture serializable (predicate\n * matchers aren't) but won't match any real user input on replay.\n */\nexport const NO_USER_MESSAGE_SENTINEL = \"__NO_USER_MESSAGE__\";\n\n/**\n * Proxy an unmatched AG-UI request to a real upstream agent, record the\n * SSE event stream as a fixture on disk and in memory, and relay the\n * response back to the original client in real time.\n *\n * Returns the HTTP status code written to the client if the request was proxied,\n * or `false` if no upstream is configured.\n */\nexport async function proxyAndRecordAGUI(\n  req: http.IncomingMessage,\n  res: http.ServerResponse,\n  input: AGUIRunAgentInput,\n  fixtures: AGUIFixture[],\n  config: AGUIRecordConfig,\n  logger: Logger,\n): Promise<number | false> {\n  if (!config.upstream) {\n    logger.warn(\"No upstream URL configured for AG-UI recording — cannot proxy\");\n    return false;\n  }\n\n  let target: URL;\n  try {\n    target = new URL(config.upstream);\n  } catch (err) {\n    const detail = err instanceof Error ? err.message : String(err);\n    logger.error(`Invalid upstream AG-UI URL: ${config.upstream} — ${detail}`);\n    res.writeHead(502, { \"Content-Type\": \"application/json\" });\n    res.end(JSON.stringify({ error: \"Invalid upstream AG-UI URL\" }));\n    return 502;\n  }\n\n  logger.warn(`NO AG-UI FIXTURE MATCH — proxying to ${config.upstream}`);\n\n  // Build upstream request headers\n  const forwardHeaders: Record<string, string> = {\n    \"Content-Type\": \"application/json\",\n    Accept: \"text/event-stream\",\n  };\n  // Forward auth headers if present\n  const authorization = req.headers[\"authorization\"];\n  if (authorization) {\n    forwardHeaders[\"Authorization\"] = Array.isArray(authorization)\n      ? authorization.join(\", \")\n      : authorization;\n  }\n  const apiKey = req.headers[\"x-api-key\"];\n  if (apiKey) {\n    forwardHeaders[\"x-api-key\"] = Array.isArray(apiKey) ? apiKey.join(\", \") : apiKey;\n  }\n\n  const requestBody = JSON.stringify(input);\n\n  let status: number;\n  try {\n    status = await teeUpstreamStream(\n      target,\n      forwardHeaders,\n      requestBody,\n      res,\n      input,\n      fixtures,\n      config,\n      logger,\n    );\n  } catch (err) {\n    const msg = err instanceof Error ? err.message : \"Unknown proxy error\";\n    logger.error(`AG-UI proxy request failed: ${msg}`);\n    if (!res.headersSent) {\n      res.writeHead(502, { \"Content-Type\": \"application/json\" });\n      res.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n    } else if (!res.writableEnded) {\n      res.end();\n    }\n    status = 502;\n  }\n\n  return status;\n}\n\n// ---------------------------------------------------------------------------\n// Internal: tee the upstream SSE stream to the client and buffer for recording\n// ---------------------------------------------------------------------------\n\nfunction teeUpstreamStream(\n  target: URL,\n  headers: Record<string, string>,\n  body: string,\n  clientRes: http.ServerResponse,\n  input: AGUIRunAgentInput,\n  fixtures: AGUIFixture[],\n  config: AGUIRecordConfig,\n  logger: Logger,\n): Promise<number> {\n  return new Promise((resolve, reject) => {\n    const transport = target.protocol === \"https:\" ? https : http;\n    const UPSTREAM_TIMEOUT_MS = 30_000;\n\n    const upstreamReq = transport.request(\n      target,\n      {\n        method: \"POST\",\n        timeout: UPSTREAM_TIMEOUT_MS,\n        headers: {\n          ...headers,\n          \"Content-Length\": Buffer.byteLength(body).toString(),\n        },\n      },\n      (upstreamRes) => {\n        const upstreamStatus = upstreamRes.statusCode ?? 200;\n\n        // Normalize status codes: aimock acts as a gateway, so upstream\n        // provider details (429, 503, etc.) should not leak.\n        // Successes → 200, errors → 502 (Bad Gateway).\n        const clientStatus = upstreamStatus >= 200 && upstreamStatus < 300 ? 200 : 502;\n\n        // Set appropriate headers on the client response.\n        if (!clientRes.headersSent) {\n          if (clientStatus === 200) {\n            clientRes.writeHead(200, {\n              \"Content-Type\": \"text/event-stream\",\n              \"Cache-Control\": \"no-cache\",\n              Connection: \"keep-alive\",\n            });\n          } else {\n            const ct = upstreamRes.headers[\"content-type\"] || \"application/json\";\n            clientRes.writeHead(502, { \"Content-Type\": ct });\n          }\n        }\n\n        const chunks: Buffer[] = [];\n        let clientWriteFailed = false;\n\n        upstreamRes.on(\"data\", (chunk: Buffer) => {\n          // Relay to client in real time\n          try {\n            clientRes.write(chunk);\n          } catch (err) {\n            if (!clientWriteFailed) {\n              clientWriteFailed = true;\n              logger?.warn(\n                \"Client write failed during proxy relay:\",\n                err instanceof Error ? err.message : String(err),\n              );\n            }\n          }\n          // Buffer for fixture construction\n          chunks.push(chunk);\n        });\n\n        let settled = false;\n\n        upstreamRes.on(\"error\", (err) => {\n          if (settled) return;\n          settled = true;\n          try {\n            if (!clientRes.headersSent) {\n              clientRes.writeHead(502, { \"Content-Type\": \"application/json\" });\n              clientRes.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n            } else if (!clientRes.writableEnded) {\n              clientRes.end();\n            }\n          } catch (writeErr) {\n            logger.warn(\n              \"Failed to write error response to client:\",\n              writeErr instanceof Error ? writeErr.message : String(writeErr),\n            );\n          }\n          reject(err);\n        });\n\n        upstreamRes.on(\"end\", () => {\n          if (settled) return;\n          settled = true;\n\n          // Don't record fixtures for non-2xx upstream responses\n          if (clientStatus !== 200) {\n            try {\n              if (!clientRes.writableEnded) clientRes.end();\n            } catch (writeErr) {\n              logger.warn(\n                \"Failed to end client response:\",\n                writeErr instanceof Error ? writeErr.message : String(writeErr),\n              );\n            }\n            resolve(clientStatus);\n            return;\n          }\n          try {\n            if (!clientRes.writableEnded) clientRes.end();\n          } catch (writeErr) {\n            logger.warn(\n              \"Failed to end client response:\",\n              writeErr instanceof Error ? writeErr.message : String(writeErr),\n            );\n          }\n\n          // Parse buffered SSE events\n          const buffered = Buffer.concat(chunks).toString();\n          const events = parseSSEEvents(buffered, logger);\n\n          // Build fixture — three-way match priority:\n          // 1. Tool-result continuation (HITL): match by toolCallId\n          // 2. User message: match by last user message content\n          // 3. Fallback predicate: no user message present\n          let match: AGUIFixtureMatch;\n          const lastToolResult = getLastMessageIfToolResult(input);\n          if (lastToolResult?.toolCallId) {\n            match = { toolCallId: lastToolResult.toolCallId };\n            logger.info(`Recorded AG-UI fixture keyed on toolCallId=${lastToolResult.toolCallId}`);\n          } else {\n            const message = extractLastUserMessage(input);\n            if (message) {\n              match = { message };\n            } else {\n              match = {\n                predicate: (inp: AGUIRunAgentInput) =>\n                  !inp.messages?.length || !inp.messages.some((m) => m.role === \"user\"),\n              };\n              logger.warn(\n                \"Recorded AG-UI fixture has no user message — available in-memory only (predicate fixtures cannot be persisted to disk)\",\n              );\n            }\n          }\n          const fixture: AGUIFixture = { match, events };\n\n          if (!config.proxyOnly) {\n            // Register in memory first (always available even if disk write fails)\n            fixtures.push(fixture);\n\n            // Predicate fixtures (no user message, no toolCallId) cannot be\n            // meaningfully serialized — the sentinel becomes a literal string\n            // match that never matches real requests. Keep in-memory only.\n            if (fixture.match.predicate) {\n              logger.warn(\n                \"Skipping disk write for predicate fixture — in-memory only (cannot be persisted)\",\n              );\n            } else {\n              const serializableFixture = {\n                match: fixture.match,\n                events: fixture.events,\n                ...(fixture.delayMs !== undefined ? { delayMs: fixture.delayMs } : {}),\n              };\n\n              const fixturePath = config.fixturePath ?? \"./fixtures/agui-recorded\";\n              const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n              const filename = `agui-${timestamp}-${crypto.randomUUID().slice(0, 8)}.json`;\n              const filepath = path.join(fixturePath, filename);\n\n              try {\n                fs.mkdirSync(fixturePath, { recursive: true });\n                fs.writeFileSync(\n                  filepath,\n                  JSON.stringify({ fixtures: [serializableFixture] }, null, 2),\n                  \"utf-8\",\n                );\n                logger.warn(`AG-UI response recorded → ${filepath}`);\n              } catch (err) {\n                const msg = err instanceof Error ? err.message : \"Unknown filesystem error\";\n                logger.error(\n                  `Failed to save AG-UI fixture to disk: ${msg} (fixture retained in memory)`,\n                );\n              }\n            }\n          } else {\n            logger.info(\"Proxied AG-UI request (proxy-only mode)\");\n          }\n\n          resolve(clientStatus);\n        });\n      },\n    );\n\n    upstreamReq.on(\"timeout\", () => {\n      upstreamReq.destroy(\n        new Error(`Upstream AG-UI request timed out after ${UPSTREAM_TIMEOUT_MS / 1000}s`),\n      );\n    });\n\n    upstreamReq.on(\"error\", (err) => {\n      try {\n        if (!clientRes.headersSent) {\n          clientRes.writeHead(502, { \"Content-Type\": \"application/json\" });\n          clientRes.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n        } else if (!clientRes.writableEnded) {\n          clientRes.end();\n        }\n      } catch (writeErr) {\n        logger.warn(\n          \"Failed to write error response to client:\",\n          writeErr instanceof Error ? writeErr.message : String(writeErr),\n        );\n      }\n      reject(err);\n    });\n\n    upstreamReq.write(body);\n    upstreamReq.end();\n  });\n}\n\n/**\n * Parse SSE data lines from buffered stream text.\n */\nfunction parseSSEEvents(text: string, logger?: Logger): AGUIEvent[] {\n  const events: AGUIEvent[] = [];\n  const blocks = text.split(\"\\n\\n\");\n  for (const block of blocks) {\n    const lines = block.split(\"\\n\");\n    for (const line of lines) {\n      if (line.startsWith(\"data:\")) {\n        const payload = line.startsWith(\"data: \") ? line.slice(6) : line.slice(5);\n        try {\n          const parsed = JSON.parse(payload) as AGUIEvent;\n          events.push(parsed);\n        } catch (err) {\n          const msg = err instanceof Error ? err.message : String(err);\n          const warning = `Skipping unparseable SSE data line (${msg}): ${payload.slice(0, 200)}`;\n          if (logger) logger.warn(warning);\n          else console.warn(warning);\n        }\n      }\n    }\n  }\n  return events;\n}\n"],"mappings":"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"}