{"version":3,"file":"stream-object.mjs","names":[],"sources":["../../../../../../../ai/src/object-stream/stream-object.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { Message } from \"../contracts/conversation-message.type\";\nimport type { ModelCallOptions, ModelContract } from \"../contracts/model.contract\";\nimport type { Usage } from \"../contracts/result/usage.type\";\nimport { AIError, SchemaValidationError } from \"../errors\";\nimport { parsePartialJson } from \"./parse-partial-json\";\n\n/**\n * One event in a {@link streamObject} run.\n *\n * - `text-delta` — the raw token text as it streams (for a \"typing\" view).\n * - `partial` — a best-effort snapshot of the object so far, re-parsed\n *   from the accumulated text on each delta (only emitted when it changed).\n * - `done` — terminal: the final text is strictly parsed and validated\n *   against the schema. `valid` + `value` on success; `valid: false` +\n *   `error` when the output wasn't valid JSON or failed the schema.\n */\nexport type ObjectStreamEvent<T> =\n  | { type: \"text-delta\"; delta: string }\n  | { type: \"partial\"; value: unknown }\n  | { type: \"done\"; valid: true; value: T; usage: Usage }\n  | { type: \"done\"; valid: false; error: AIError; usage: Usage };\n\n/** Parameters for {@link streamObject}. */\nexport type StreamObjectParams<T> = {\n  /** The model to stream from (e.g. `sdk.model({ name })`). */\n  model: ModelContract;\n  /** The prompt messages. */\n  messages: Message[];\n  /** Standard Schema the final object is validated against. */\n  schema: StandardSchemaV1<T>;\n  /** Extra model call options (e.g. `responseSchema`, `temperature`). */\n  options?: ModelCallOptions;\n};\n\n/**\n * Stream a structured object: emit raw token deltas, progressively-parsed\n * partial-object snapshots, and a final strictly-validated object — the\n * first-class structured-output streaming primitive (A1). Reuses the\n * model's existing `stream()` seam; the partial snapshots come from a\n * tolerant {@link parsePartialJson}, while the terminal `done` event is a\n * strict `JSON.parse` + schema validation, so an over-eager partial parse\n * never affects the authoritative result.\n *\n * Pair it with a `structuredOutput`-capable model and a `responseSchema`\n * (via `options`) for the cleanest JSON; otherwise prompt the model to\n * reply with JSON only.\n *\n * @example\n * for await (const event of streamObject({ model, messages, schema })) {\n *   if (event.type === \"partial\") render(event.value);          // live UI\n *   if (event.type === \"done\" && event.valid) save(event.value); // final\n * }\n */\nexport async function* streamObject<T>(\n  params: StreamObjectParams<T>,\n): AsyncIterable<ObjectStreamEvent<T>> {\n  const { model, messages, schema, options } = params;\n\n  let accumulated = \"\";\n  let lastPartialKey: string | undefined;\n  let usage: Usage = { input: 0, output: 0, total: 0 };\n\n  for await (const chunk of model.stream(messages, options)) {\n    if (chunk.type === \"delta\") {\n      accumulated += chunk.content;\n      yield { type: \"text-delta\", delta: chunk.content };\n\n      const partial = parsePartialJson(accumulated);\n      if (partial !== undefined) {\n        const key = safeStringify(partial);\n        if (key !== lastPartialKey) {\n          lastPartialKey = key;\n          yield { type: \"partial\", value: partial };\n        }\n      }\n    } else if (chunk.type === \"done\") {\n      usage = chunk.usage;\n    }\n  }\n\n  yield await finalize(accumulated, schema, usage);\n}\n\n/** Strict parse + schema validation of the complete streamed text. */\nasync function finalize<T>(\n  text: string,\n  schema: StandardSchemaV1<T>,\n  usage: Usage,\n): Promise<ObjectStreamEvent<T>> {\n  const cleaned = stripJsonFences(text).trim();\n\n  let parsed: unknown;\n  try {\n    parsed = JSON.parse(cleaned);\n  } catch (cause) {\n    return {\n      type: \"done\",\n      valid: false,\n      error: new SchemaValidationError(\n        \"streamObject: the final streamed output was not valid JSON\",\n        { cause },\n      ),\n      usage,\n    };\n  }\n\n  const result = await schema[\"~standard\"].validate(parsed);\n  if (\"issues\" in result && result.issues) {\n    return {\n      type: \"done\",\n      valid: false,\n      error: new SchemaValidationError(\n        \"streamObject: the streamed object failed schema validation\",\n        { issues: result.issues },\n      ),\n      usage,\n    };\n  }\n\n  return { type: \"done\", valid: true, value: (result as { value: T }).value, usage };\n}\n\n/** Collect a {@link streamObject} run down to just its terminal event. */\nexport async function collectStreamObject<T>(\n  stream: AsyncIterable<ObjectStreamEvent<T>>,\n): Promise<Extract<ObjectStreamEvent<T>, { type: \"done\" }>> {\n  let done: Extract<ObjectStreamEvent<T>, { type: \"done\" }> | undefined;\n  for await (const event of stream) {\n    if (event.type === \"done\") done = event;\n  }\n  if (!done) {\n    throw new SchemaValidationError(\"streamObject: stream ended without a terminal event\");\n  }\n  return done;\n}\n\n/** Strip a leading/trailing ```json fence the model may wrap output in. */\nfunction stripJsonFences(text: string): string {\n  const fenced = text.match(/```(?:json)?\\s*([\\s\\S]*?)\\s*```/i);\n  return fenced ? fenced[1] : text;\n}\n\nfunction safeStringify(value: unknown): string {\n  try {\n    return JSON.stringify(value);\n  } catch {\n    return String(value);\n  }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAsDA,gBAAuB,aACrB,QACqC;CACrC,MAAM,EAAE,OAAO,UAAU,QAAQ,YAAY;CAE7C,IAAI,cAAc;CAClB,IAAI;CACJ,IAAI,QAAe;EAAE,OAAO;EAAG,QAAQ;EAAG,OAAO;CAAE;CAEnD,WAAW,MAAM,SAAS,MAAM,OAAO,UAAU,OAAO,GACtD,IAAI,MAAM,SAAS,SAAS;EAC1B,eAAe,MAAM;EACrB,MAAM;GAAE,MAAM;GAAc,OAAO,MAAM;EAAQ;EAEjD,MAAM,UAAU,iBAAiB,WAAW;EAC5C,IAAI,YAAY,QAAW;GACzB,MAAM,MAAM,cAAc,OAAO;GACjC,IAAI,QAAQ,gBAAgB;IAC1B,iBAAiB;IACjB,MAAM;KAAE,MAAM;KAAW,OAAO;IAAQ;GAC1C;EACF;CACF,OAAO,IAAI,MAAM,SAAS,QACxB,QAAQ,MAAM;CAIlB,MAAM,MAAM,SAAS,aAAa,QAAQ,KAAK;AACjD;;AAGA,eAAe,SACb,MACA,QACA,OAC+B;CAC/B,MAAM,UAAU,gBAAgB,IAAI,CAAC,CAAC,KAAK;CAE3C,IAAI;CACJ,IAAI;EACF,SAAS,KAAK,MAAM,OAAO;CAC7B,SAAS,OAAO;EACd,OAAO;GACL,MAAM;GACN,OAAO;GACP,OAAO,IAAI,sBACT,8DACA,EAAE,MAAM,CACV;GACA;EACF;CACF;CAEA,MAAM,SAAS,MAAM,OAAO,YAAY,CAAC,SAAS,MAAM;CACxD,IAAI,YAAY,UAAU,OAAO,QAC/B,OAAO;EACL,MAAM;EACN,OAAO;EACP,OAAO,IAAI,sBACT,8DACA,EAAE,QAAQ,OAAO,OAAO,CAC1B;EACA;CACF;CAGF,OAAO;EAAE,MAAM;EAAQ,OAAO;EAAM,OAAQ,OAAwB;EAAO;CAAM;AACnF;;AAGA,eAAsB,oBACpB,QAC0D;CAC1D,IAAI;CACJ,WAAW,MAAM,SAAS,QACxB,IAAI,MAAM,SAAS,QAAQ,OAAO;CAEpC,IAAI,CAAC,MACH,MAAM,IAAI,sBAAsB,qDAAqD;CAEvF,OAAO;AACT;;AAGA,SAAS,gBAAgB,MAAsB;CAC7C,MAAM,SAAS,KAAK,MAAM,kCAAkC;CAC5D,OAAO,SAAS,OAAO,KAAK;AAC9B;AAEA,SAAS,cAAc,OAAwB;CAC7C,IAAI;EACF,OAAO,KAAK,UAAU,KAAK;CAC7B,QAAQ;EACN,OAAO,OAAO,KAAK;CACrB;AACF"}