{"version":3,"file":"batch.mjs","names":[],"sources":["../../../../../../../ai/src/batch/batch.ts"],"sourcesContent":["import type { BaseReport } from \"../contracts/result/base-report.type\";\nimport { REPORT_SCHEMA_VERSION } from \"../contracts/result/base-report.type\";\nimport type { BaseResult } from \"../contracts/result/base-result.type\";\nimport type { ExecutableContract } from \"../contracts/executable.contract\";\nimport type { ExecuteResult } from \"../contracts/result/execute-result.type\";\nimport type { Usage } from \"../contracts/result/usage.type\";\nimport { accumulateCost } from \"../utils/compute-cost\";\nimport { generateRunId } from \"../utils/generate-run-id\";\nimport { stampReportLineage } from \"../utils/stamp-report-lineage\";\nimport type {\n  BatchItemResult,\n  BatchOptions,\n  BatchReport,\n  BatchResult,\n} from \"./batch.type\";\nimport { runBatchItem } from \"./run-batch-item\";\nimport { runWithConcurrency } from \"./run-with-concurrency\";\n\n/** Batch size above which an unset (unbounded) concurrency warns once (D5). */\nconst BATCH_UNBOUNDED_WARN_THRESHOLD = 50;\n\n/** Process-lifetime flag so the unbounded-batch warning fires at most once. */\nlet warnedUnboundedBatch = false;\n\n/**\n * Run an executable AI primitive (agent, workflow, supervisor, tool,\n * or anything satisfying {@link ExecutableContract}) over a dataset\n * with bounded concurrency and per-item retry, returning per-item\n * outcomes plus rolled-up usage and a walkable report tree.\n *\n * **Role.** The fan-out primitive of `@warlock.js/ai`. Where an agent\n * runs once, `batch` runs the SAME executable N times — once per item\n * — and aggregates the results into the unified {@link ExecuteResult}\n * envelope, so a batch slots into cost dashboards and trace tooling\n * exactly like a single run does.\n *\n * **Isolation.** Items are independent: one item's failure (after its\n * retries are exhausted) never cancels a sibling, and the batch as a\n * whole never rejects — failures live on each {@link BatchItemResult}.\n * Reach for `result.report.failed` / `item.status` to inspect them.\n *\n * **Usage rollup.** `result.usage` and `result.report.usage` sum every\n * item's usage, satisfying the universal rollup invariant (\"own cost\n * + sum of children\"; a batch has zero own cost). Each item's own\n * report is attached under `report.children[]`, in original item\n * order, so a trace walker sees every run.\n *\n * @example\n * const result = await batch(summarizer, articles, {\n *   concurrency: 4,\n *   retry: { attempts: 3, backoff: \"exponential\" },\n *   onItem: (item) => log.info(\"batch\", \"item\", \"settled\", { index: item.index }),\n * });\n *\n * console.log(`${result.report.succeeded}/${result.report.total} ok`);\n * console.log(`${result.usage.total} tokens total`);\n */\nexport async function batch<TInput, TOptions, TResult extends BaseResult = ExecuteResult>(\n  executable: ExecutableContract<TInput, TOptions, TResult>,\n  items: readonly TInput[],\n  options: BatchOptions<TResult> = {},\n): Promise<BatchResult<TResult>> {\n  return new BatchRun(executable, items, options).run();\n}\n\n/**\n * Per-call orchestration state for one {@link batch} invocation.\n * Instantiated fresh inside the factory so the mutable accumulators\n * (`results`, `usage`) are never shared across batches. Unexported —\n * callers only ever see the plain {@link BatchResult}.\n */\nclass BatchRun<TInput, TOptions, TResult extends BaseResult> {\n  private readonly runId: string;\n  private readonly results: BatchItemResult<TResult>[];\n  private readonly startedAt = new Date().toISOString();\n  private readonly startPerf = performance.now();\n\n  public constructor(\n    private readonly executable: ExecutableContract<TInput, TOptions, TResult>,\n    private readonly items: readonly TInput[],\n    private readonly options: BatchOptions<TResult>,\n  ) {\n    this.runId = generateRunId(\"batch\");\n    this.results = new Array<BatchItemResult<TResult>>(items.length);\n  }\n\n  /**\n   * Dispatch every item through the concurrency pool, then assemble\n   * the rolled-up {@link BatchResult}. Runs once per `batch()` call.\n   */\n  public async run(): Promise<BatchResult<TResult>> {\n    const concurrency = this.resolveConcurrency();\n\n    await runWithConcurrency(this.items.length, concurrency, (index) =>\n      this.processItem(index),\n    );\n\n    return this.buildResult();\n  }\n\n  /**\n   * Resolve the effective concurrency from {@link BatchOptions.concurrency}\n   * (D5). An explicit number or `\"unbounded\"` is honored as-is; an omitted\n   * value runs unbounded for back-compat but warns once (outside tests)\n   * for a large batch so an accidental all-at-once run is visible.\n   */\n  private resolveConcurrency(): number {\n    const configured = this.options.concurrency;\n\n    if (configured === \"unbounded\") {\n      return this.items.length;\n    }\n    if (typeof configured === \"number\") {\n      return configured;\n    }\n\n    if (\n      this.items.length > BATCH_UNBOUNDED_WARN_THRESHOLD &&\n      !warnedUnboundedBatch &&\n      !process.env.VITEST &&\n      process.env.NODE_ENV !== \"test\"\n    ) {\n      warnedUnboundedBatch = true;\n      console.warn(\n        `[warlock-ai] ai.batch() is running ${this.items.length} items with unbounded concurrency (no \\`concurrency\\` set). ` +\n          'Each concurrent item consumes tokens/quota/memory — pass an explicit `concurrency` cap, or `concurrency: \"unbounded\"` to silence this.',\n      );\n    }\n\n    return this.items.length;\n  }\n\n  /**\n   * Run a single item with retry, record it positionally, then fire\n   * the `onItem` hook. A throw from the hook is swallowed — a progress\n   * callback must never break the batch.\n   */\n  private async processItem(index: number): Promise<void> {\n    const item = await runBatchItem({\n      index,\n      input: this.items[index] as TInput,\n      executable: this.executable,\n      retry: this.options.retry,\n      signal: this.options.signal,\n    });\n\n    this.results[index] = item;\n\n    if (this.options.onItem) {\n      try {\n        await this.options.onItem(item);\n      } catch {\n        // A progress hook must never break the batch — swallow its throw.\n      }\n    }\n  }\n\n  /**\n   * Fold the per-item outcomes into rolled-up usage, the child report\n   * list, and the final {@link BatchResult}, then stamp lineage across\n   * the whole subtree so every child shares this batch's root run id.\n   */\n  private buildResult(): BatchResult<TResult> {\n    const usage: Usage = { input: 0, output: 0, total: 0 };\n    const children: BaseReport[] = [];\n    const data: (unknown | undefined)[] = new Array(this.items.length).fill(undefined);\n\n    let succeeded = 0;\n    let failed = 0;\n    let cancelled = 0;\n\n    for (const item of this.results) {\n      if (item.status === \"completed\") {\n        succeeded += 1;\n      } else if (item.status === \"failed\") {\n        failed += 1;\n      } else {\n        cancelled += 1;\n      }\n\n      const itemResult = item.result;\n      if (itemResult) {\n        this.mergeUsage(usage, itemResult.usage);\n\n        if (\"report\" in itemResult && itemResult.report) {\n          children.push(itemResult.report as BaseReport);\n        }\n\n        if (item.status === \"completed\" && \"data\" in itemResult) {\n          data[item.index] = (itemResult as { data?: unknown }).data;\n        }\n      }\n    }\n\n    const report = this.buildReport(usage, children, { succeeded, failed, cancelled });\n\n    stampReportLineage(report, {\n      rootRunId: this.runId,\n      sessionId: this.options.sessionId,\n    });\n\n    return {\n      type: \"batch\",\n      data,\n      usage,\n      report,\n      items: this.results,\n    };\n  }\n\n  /**\n   * Add a child's usage into the running batch total. Scalar token\n   * channels sum directly; the optional cost breakdown merges via\n   * {@link accumulateCost} so a single unpriced child can't erase the\n   * cost of priced siblings. Optional token sub-channels\n   * (`cachedTokens`, etc.) accumulate only when some child reports\n   * them, preserving the \"never reported anywhere\" signal.\n   */\n  private mergeUsage(target: Usage, child: Usage): void {\n    target.input += child.input;\n    target.output += child.output;\n    target.total += child.total;\n\n    if (child.cachedTokens !== undefined) {\n      target.cachedTokens = (target.cachedTokens ?? 0) + child.cachedTokens;\n    }\n\n    if (child.reasoningTokens !== undefined) {\n      target.reasoningTokens = (target.reasoningTokens ?? 0) + child.reasoningTokens;\n    }\n\n    if (child.cacheWriteTokens !== undefined) {\n      target.cacheWriteTokens = (target.cacheWriteTokens ?? 0) + child.cacheWriteTokens;\n    }\n\n    const mergedCost = accumulateCost(target.cost, child.cost);\n    if (mergedCost !== undefined) {\n      target.cost = mergedCost;\n    }\n  }\n\n  /**\n   * Build the batch's own {@link BatchReport} node. `parentRunId` /\n   * `rootRunId` are placeholders here — {@link stampReportLineage}\n   * rewrites them across the whole subtree right after.\n   */\n  private buildReport(\n    usage: Usage,\n    children: BaseReport[],\n    counts: { succeeded: number; failed: number; cancelled: number },\n  ): BatchReport {\n    const status = counts.failed > 0 || counts.cancelled > 0 ? \"failed\" : \"completed\";\n\n    return {\n      runId: this.runId,\n      rootRunId: this.runId,\n      name: this.options.name ?? \"batch\",\n      type: \"batch\",\n      status,\n      startedAt: this.startedAt,\n      endedAt: new Date().toISOString(),\n      duration: performance.now() - this.startPerf,\n      usage,\n      children,\n      total: this.items.length,\n      succeeded: counts.succeeded,\n      failed: counts.failed,\n      cancelled: counts.cancelled,\n      reportSchemaVersion: REPORT_SCHEMA_VERSION,\n    };\n  }\n}\n"],"mappings":"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"}