@aglyn/plugins-ai 1.0.0-beta.250 → 1.0.0-beta.253

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 (126) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-brief-dialog.component.js +15 -1
  3. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  4. package/src/lib/components/ai-job-links.js +1 -1
  5. package/src/lib/components/ai-job-links.js.map +1 -1
  6. package/src/lib/components/ai-job-plan.component.js +20 -2
  7. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  8. package/src/lib/components/ai-jobs-store.d.ts +11 -0
  9. package/src/lib/components/ai-jobs-store.js +22 -0
  10. package/src/lib/components/ai-jobs-store.js.map +1 -1
  11. package/src/lib/jobs/ai-job-crm-step.js +7 -4
  12. package/src/lib/jobs/ai-job-crm-step.js.map +1 -1
  13. package/src/lib/jobs/ai-job-drafts.d.ts +24 -0
  14. package/src/lib/jobs/ai-job-drafts.js +46 -0
  15. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  16. package/src/lib/jobs/ai-job-experiment-step.js +6 -1
  17. package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
  18. package/src/lib/jobs/ai-job-form-dataset.d.ts +6 -1
  19. package/src/lib/jobs/ai-job-form-dataset.js +7 -2
  20. package/src/lib/jobs/ai-job-form-dataset.js.map +1 -1
  21. package/src/lib/jobs/ai-job-form-reuse.d.ts +78 -0
  22. package/src/lib/jobs/ai-job-form-reuse.js +136 -0
  23. package/src/lib/jobs/ai-job-form-reuse.js.map +1 -0
  24. package/src/lib/jobs/ai-job-form-step.d.ts +17 -0
  25. package/src/lib/jobs/ai-job-form-step.js +63 -11
  26. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  27. package/src/lib/jobs/ai-job-insight-step.js +6 -1
  28. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  29. package/src/lib/jobs/ai-job-page-language.d.ts +10 -5
  30. package/src/lib/jobs/ai-job-page-language.js +10 -5
  31. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  32. package/src/lib/jobs/ai-job-plan-step.d.ts +32 -1
  33. package/src/lib/jobs/ai-job-plan-step.js +53 -2
  34. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  35. package/src/lib/jobs/ai-job-seo-step.js +5 -3
  36. package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
  37. package/src/lib/jobs/ai-job-site-content.d.ts +7 -5
  38. package/src/lib/jobs/ai-job-site-content.js +7 -5
  39. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  40. package/src/lib/jobs/ai-job-site-datasets.d.ts +1 -1
  41. package/src/lib/jobs/ai-job-site-datasets.js +2 -2
  42. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  43. package/src/lib/jobs/ai-job-site-step.js +2 -1
  44. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  45. package/src/lib/jobs/ai-job-text-step.js +6 -3
  46. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  47. package/src/lib/jobs/ai-layout-stock-photos.d.ts +11 -7
  48. package/src/lib/jobs/ai-layout-stock-photos.js +25 -10
  49. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  50. package/src/lib/jobs/ai-stock-business-nouns.d.ts +62 -0
  51. package/src/lib/jobs/ai-stock-business-nouns.js +264 -0
  52. package/src/lib/jobs/ai-stock-business-nouns.js.map +1 -0
  53. package/src/lib/layout-language/ai-layout-language.js +5 -1
  54. package/src/lib/layout-language/ai-layout-language.js.map +1 -1
  55. package/src/lib/layout-language/ai-layout-links.js +5 -2
  56. package/src/lib/layout-language/ai-layout-links.js.map +1 -1
  57. package/src/lib/model/ai-build-plan.d.ts +7 -3
  58. package/src/lib/model/ai-build-plan.js +11 -1
  59. package/src/lib/model/ai-build-plan.js.map +1 -1
  60. package/src/lib/model/ai-credit-estimate.d.ts +39 -19
  61. package/src/lib/model/ai-credit-estimate.js +43 -19
  62. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  63. package/src/lib/model/ai-job-activity.d.ts +30 -0
  64. package/src/lib/model/ai-job-activity.js +35 -0
  65. package/src/lib/model/ai-job-activity.js.map +1 -1
  66. package/src/lib/model/ai-media-credits.d.ts +5 -3
  67. package/src/lib/model/ai-media-credits.js +7 -5
  68. package/src/lib/model/ai-media-credits.js.map +1 -1
  69. package/src/lib/model/ai-site-job.js +6 -3
  70. package/src/lib/model/ai-site-job.js.map +1 -1
  71. package/src/lib/model/ai-site-start-seo.d.ts +11 -2
  72. package/src/lib/model/ai-site-start-seo.js +29 -15
  73. package/src/lib/model/ai-site-start-seo.js.map +1 -1
  74. package/src/lib/model/ai-site-store-pages.d.ts +5 -0
  75. package/src/lib/model/ai-site-store-pages.js +2 -0
  76. package/src/lib/model/ai-site-store-pages.js.map +1 -1
  77. package/src/lib/plugin-config.js +2 -2
  78. package/src/lib/plugin-config.js.map +1 -1
  79. package/src/lib/providers/anthropic-crm-wording-superseded.d.ts +29 -0
  80. package/src/lib/providers/anthropic-crm-wording-superseded.js +31 -0
  81. package/src/lib/providers/anthropic-crm-wording-superseded.js.map +1 -0
  82. package/src/lib/providers/anthropic.js +29 -7
  83. package/src/lib/providers/anthropic.js.map +1 -1
  84. package/src/lib/providers/catalog.d.ts +24 -17
  85. package/src/lib/providers/catalog.js +63 -30
  86. package/src/lib/providers/catalog.js.map +1 -1
  87. package/src/lib/providers/contract.d.ts +19 -1
  88. package/src/lib/providers/contract.js +10 -0
  89. package/src/lib/providers/contract.js.map +1 -1
  90. package/src/lib/providers/model-choice.d.ts +1 -1
  91. package/src/lib/providers/model-choice.js +39 -30
  92. package/src/lib/providers/model-choice.js.map +1 -1
  93. package/src/lib/providers/routing.d.ts +20 -1
  94. package/src/lib/providers/routing.js +71 -12
  95. package/src/lib/providers/routing.js.map +1 -1
  96. package/src/lib/runtime/ai-dev-env.d.ts +58 -1
  97. package/src/lib/runtime/ai-dev-env.js +86 -6
  98. package/src/lib/runtime/ai-dev-env.js.map +1 -1
  99. package/src/lib/runtime/ai-dev-replay.d.ts +4 -0
  100. package/src/lib/runtime/ai-dev-replay.js +4 -0
  101. package/src/lib/runtime/ai-dev-replay.js.map +1 -1
  102. package/src/lib/runtime/ai-doctrine-validators.js +13 -3
  103. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  104. package/src/lib/runtime/ai-doctrine.js +8 -4
  105. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  106. package/src/lib/runtime/ai-eval-live.d.ts +43 -1
  107. package/src/lib/runtime/ai-eval-live.js +79 -9
  108. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  109. package/src/lib/runtime/ai-live-batch.d.ts +20 -4
  110. package/src/lib/runtime/ai-live-batch.js +57 -10
  111. package/src/lib/runtime/ai-live-batch.js.map +1 -1
  112. package/src/lib/runtime/ai-palette.generated.js +4 -0
  113. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  114. package/src/lib/runtime/ai-runtime.js +21 -4
  115. package/src/lib/runtime/ai-runtime.js.map +1 -1
  116. package/src/lib/runtime/seo-fields.js +4 -3
  117. package/src/lib/runtime/seo-fields.js.map +1 -1
  118. package/src/lib/server/ai-assist.js +3 -2
  119. package/src/lib/server/ai-assist.js.map +1 -1
  120. package/src/lib/server/ai-media-image.d.ts +5 -1
  121. package/src/lib/server/ai-media-image.js +6 -2
  122. package/src/lib/server/ai-media-image.js.map +1 -1
  123. package/src/lib/server/ai-media-svg.js +4 -0
  124. package/src/lib/server/ai-media-svg.js.map +1 -1
  125. package/src/lib/usage/ai-allotment-alerts.js +2 -5
  126. package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
@@ -1,3 +1,4 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
1
2
  /**
2
3
  * @license
3
4
  * Copyright 2026 Aglyn LLC
@@ -13,7 +14,8 @@
13
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
15
  * See the License for the specific language governing permissions and
15
16
  * limitations under the License.
16
- */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ */ import { AiBatchUnfinishedError } from "../providers/contract.js";
18
+ import { aiLiveBatchWaitMs } from "./ai-dev-env.js";
17
19
  /**
18
20
  * A LIVE EVAL ROUND AS ONE MESSAGE BATCH (AGL-3660), at half the price.
19
21
  *
@@ -31,11 +33,20 @@
31
33
  * suits the full sweep an agent runs before landing, not the one brief it is
32
34
  * iterating on. Streams are never batched. Development only: refused in any
33
35
  * deployed server, where nothing should wait minutes for an answer.
36
+ *
37
+ * A batch is waited on for `AGLYN_LIVE_AI_BATCH_WAIT_MS` (half an hour) and
38
+ * no longer (AGL-3781). What it answered by then is kept; each request it left
39
+ * unanswered is asked on its own — the same request, cache breakpoints and
40
+ * all, the first alone so it writes the cached prefix the rest then read —
41
+ * and counted as a live request at full price, not a batched one. One line
42
+ * says so when it happens. A slow batch costs a round part of its discount,
43
+ * never its answers.
34
44
  */ /** How long the queue waits for one more request before it sends the batch. */ export const AI_LIVE_BATCH_WINDOW_ENV = 'AGLYN_LIVE_AI_BATCH_WINDOW_MS';
35
45
  const queues = new Map();
36
- /** How many batches this process has sent, and how many requests rode in them. */ const counts = {
46
+ const counts = {
37
47
  batches: 0,
38
- requests: 0
48
+ requests: 0,
49
+ fallbacks: 0
39
50
  };
40
51
  export function aiLiveBatchStats() {
41
52
  return _extends({}, counts);
@@ -43,6 +54,31 @@ export function aiLiveBatchStats() {
43
54
  export function resetAiLiveBatchStats() {
44
55
  counts.batches = 0;
45
56
  counts.requests = 0;
57
+ counts.fallbacks = 0;
58
+ }
59
+ /** Results a batch did not answer, answered by a request of their own — billed at full price. */ const answeredOnTheirOwn = new WeakSet();
60
+ /** Whether this result came from a request of its own after its batch's wait ran out (AGL-3781). */ export function aiAnsweredOutsideBatch(result) {
61
+ return answeredOnTheirOwn.has(result);
62
+ }
63
+ async function answerOnItsOwn(provider, entry) {
64
+ try {
65
+ const result = await provider.complete(entry.request);
66
+ answeredOnTheirOwn.add(result);
67
+ entry.resolve(result);
68
+ } catch (error) {
69
+ entry.reject(error);
70
+ }
71
+ }
72
+ /**
73
+ * The requests a batch left unanswered, each asked on its own: the first
74
+ * alone, so the cached prefix it writes is there for the others to read,
75
+ * then the rest together.
76
+ */ async function finishOnTheirOwn(provider, left, of, waitMs) {
77
+ counts.fallbacks += left.length;
78
+ console.warn(`[ai] live batch: ${left.length} of ${of} requests unanswered after ${waitMs} ms; ` + 'asked on their own at full price');
79
+ const [first, ...rest] = left;
80
+ await answerOnItsOwn(provider, first);
81
+ await Promise.all(rest.map((entry)=>answerOnItsOwn(provider, entry)));
46
82
  }
47
83
  function windowMs(env) {
48
84
  const parsed = Number(env[AI_LIVE_BATCH_WINDOW_ENV]);
@@ -55,17 +91,27 @@ async function flush(provider) {
55
91
  queues.delete(provider);
56
92
  if (!entries.length || !provider.completeBatch) return;
57
93
  counts.batches += 1;
58
- counts.requests += entries.length;
94
+ const waitMs = aiLiveBatchWaitMs(queue.env);
95
+ let results;
59
96
  try {
60
- const results = await provider.completeBatch(entries.map((entry)=>entry.request));
61
- entries.forEach((entry, index)=>{
62
- const result = results[index];
63
- if (result instanceof Error || !result) entry.reject(result != null ? result : new Error('the batch returned no result'));
64
- else entry.resolve(result);
97
+ results = await provider.completeBatch(entries.map((entry)=>entry.request), {
98
+ timeoutMs: waitMs
65
99
  });
66
100
  } catch (error) {
67
101
  for (const entry of entries)entry.reject(error);
102
+ return;
68
103
  }
104
+ const left = [];
105
+ entries.forEach((entry, index)=>{
106
+ const result = results[index];
107
+ if (result instanceof AiBatchUnfinishedError) left.push(entry);
108
+ else if (result instanceof Error || !result) entry.reject(result != null ? result : new Error('the batch returned no result'));
109
+ else {
110
+ counts.requests += 1;
111
+ entry.resolve(result);
112
+ }
113
+ });
114
+ if (left.length) await finishOnTheirOwn(provider, left, entries.length, waitMs);
69
115
  }
70
116
  /** One request as an entry of the next batch to `provider`. */ export function aiCompleteInBatch(provider, request, env = process.env) {
71
117
  if (!provider.completeBatch) return provider.complete(request);
@@ -73,7 +119,8 @@ async function flush(provider) {
73
119
  var _queues_get;
74
120
  const queue = (_queues_get = queues.get(provider)) != null ? _queues_get : {
75
121
  entries: [],
76
- timer: null
122
+ timer: null,
123
+ env
77
124
  };
78
125
  queues.set(provider, queue);
79
126
  queue.entries.push({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-live-batch.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AiProvider, AiProviderRequest, AiResult } from '../providers/contract'\nimport type { AiDevEnv as Env } from './ai-dev-env'\n\n/**\n * A LIVE EVAL ROUND AS ONE MESSAGE BATCH (AGL-3660), at half the price.\n *\n * `AGLYN_LIVE_AI_BATCH=1` turns every non-streaming request a live eval sends\n * into an entry of a batch instead of a request of its own. The eval code does\n * not change: each request still awaits its own answer. Requests that arrive\n * together — a `Promise.all` over the briefs — gather into one batch, which\n * is sent once nothing new has arrived for `AGLYN_LIVE_AI_BATCH_WINDOW_MS`\n * (2 s), and every request resolves when the batch has ended. A re-ask is\n * asked only once its answer has been read, so the re-asks of a round gather\n * into the NEXT batch on their own: a round with re-asks is two or three\n * batches, never a request apiece.\n *\n * A batch takes minutes rather than seconds (most end within the hour), so it\n * suits the full sweep an agent runs before landing, not the one brief it is\n * iterating on. Streams are never batched. Development only: refused in any\n * deployed server, where nothing should wait minutes for an answer.\n */\n\n/** How long the queue waits for one more request before it sends the batch. */\nexport const AI_LIVE_BATCH_WINDOW_ENV = 'AGLYN_LIVE_AI_BATCH_WINDOW_MS'\n\n\ninterface Queued {\n request: AiProviderRequest\n resolve: (result: AiResult) => void\n reject: (error: unknown) => void\n}\n\nconst queues = new Map<AiProvider, { entries: Queued[]; timer: ReturnType<typeof setTimeout> | null }>()\n\n/** How many batches this process has sent, and how many requests rode in them. */\nconst counts = { batches: 0, requests: 0 }\n\nexport function aiLiveBatchStats(): { batches: number; requests: number } {\n return { ...counts }\n}\n\nexport function resetAiLiveBatchStats(): void {\n counts.batches = 0\n counts.requests = 0\n}\n\nfunction windowMs(env: Env): number {\n const parsed = Number(env[AI_LIVE_BATCH_WINDOW_ENV])\n return Number.isFinite(parsed) && parsed >= 0 ? parsed : 2_000\n}\n\nasync function flush(provider: AiProvider): Promise<void> {\n const queue = queues.get(provider)\n if (!queue) return\n const entries = queue.entries\n queues.delete(provider)\n if (!entries.length || !provider.completeBatch) return\n counts.batches += 1\n counts.requests += entries.length\n try {\n const results = await provider.completeBatch(entries.map((entry) => entry.request))\n entries.forEach((entry, index) => {\n const result = results[index]\n if (result instanceof Error || !result) entry.reject(result ?? new Error('the batch returned no result'))\n else entry.resolve(result)\n })\n } catch (error) {\n for (const entry of entries) entry.reject(error)\n }\n}\n\n/** One request as an entry of the next batch to `provider`. */\nexport function aiCompleteInBatch(\n provider: AiProvider,\n request: AiProviderRequest,\n env: Env = process.env,\n): Promise<AiResult> {\n if (!provider.completeBatch) return provider.complete(request)\n return new Promise<AiResult>((resolve, reject) => {\n const queue = queues.get(provider) ?? { entries: [], timer: null }\n queues.set(provider, queue)\n queue.entries.push({ request, resolve, reject })\n if (queue.timer) clearTimeout(queue.timer)\n queue.timer = setTimeout(() => void flush(provider), windowMs(env))\n })\n}\n"],"names":["AI_LIVE_BATCH_WINDOW_ENV","queues","Map","counts","batches","requests","aiLiveBatchStats","resetAiLiveBatchStats","windowMs","env","parsed","Number","isFinite","flush","provider","queue","get","entries","delete","length","completeBatch","results","map","entry","request","forEach","index","result","Error","reject","resolve","error","aiCompleteInBatch","process","complete","Promise","timer","set","push","clearTimeout","setTimeout"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAKD;;;;;;;;;;;;;;;;;CAiBC,GAED,6EAA6E,GAC7E,OAAO,MAAMA,2BAA2B,gCAA+B;AASvE,MAAMC,SAAS,IAAIC;AAEnB,gFAAgF,GAChF,MAAMC,SAAS;IAAEC,SAAS;IAAGC,UAAU;AAAE;AAEzC,OAAO,SAASC;IACd,OAAO,aAAKH;AACd;AAEA,OAAO,SAASI;IACdJ,OAAOC,OAAO,GAAG;IACjBD,OAAOE,QAAQ,GAAG;AACpB;AAEA,SAASG,SAASC,GAAQ;IACxB,MAAMC,SAASC,OAAOF,GAAG,CAACT,yBAAyB;IACnD,OAAOW,OAAOC,QAAQ,CAACF,WAAWA,UAAU,IAAIA,SAAS;AAC3D;AAEA,eAAeG,MAAMC,QAAoB;IACvC,MAAMC,QAAQd,OAAOe,GAAG,CAACF;IACzB,IAAI,CAACC,OAAO;IACZ,MAAME,UAAUF,MAAME,OAAO;IAC7BhB,OAAOiB,MAAM,CAACJ;IACd,IAAI,CAACG,QAAQE,MAAM,IAAI,CAACL,SAASM,aAAa,EAAE;IAChDjB,OAAOC,OAAO,IAAI;IAClBD,OAAOE,QAAQ,IAAIY,QAAQE,MAAM;IACjC,IAAI;QACF,MAAME,UAAU,MAAMP,SAASM,aAAa,CAACH,QAAQK,GAAG,CAAC,CAACC,QAAUA,MAAMC,OAAO;QACjFP,QAAQQ,OAAO,CAAC,CAACF,OAAOG;YACtB,MAAMC,SAASN,OAAO,CAACK,MAAM;YAC7B,IAAIC,kBAAkBC,SAAS,CAACD,QAAQJ,MAAMM,MAAM,CAACF,iBAAAA,SAAU,IAAIC,MAAM;iBACpEL,MAAMO,OAAO,CAACH;QACrB;IACF,EAAE,OAAOI,OAAO;QACd,KAAK,MAAMR,SAASN,QAASM,MAAMM,MAAM,CAACE;IAC5C;AACF;AAEA,6DAA6D,GAC7D,OAAO,SAASC,kBACdlB,QAAoB,EACpBU,OAA0B,EAC1Bf,MAAWwB,QAAQxB,GAAG;IAEtB,IAAI,CAACK,SAASM,aAAa,EAAE,OAAON,SAASoB,QAAQ,CAACV;IACtD,OAAO,IAAIW,QAAkB,CAACL,SAASD;YACvB5B;QAAd,MAAMc,SAAQd,cAAAA,OAAOe,GAAG,CAACF,qBAAXb,cAAwB;YAAEgB,SAAS,EAAE;YAAEmB,OAAO;QAAK;QACjEnC,OAAOoC,GAAG,CAACvB,UAAUC;QACrBA,MAAME,OAAO,CAACqB,IAAI,CAAC;YAAEd;YAASM;YAASD;QAAO;QAC9C,IAAId,MAAMqB,KAAK,EAAEG,aAAaxB,MAAMqB,KAAK;QACzCrB,MAAMqB,KAAK,GAAGI,WAAW,IAAM,KAAK3B,MAAMC,WAAWN,SAASC;IAChE;AACF"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-live-batch.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { AiBatchUnfinishedError, type AiProvider, type AiProviderRequest, type AiResult } from '../providers/contract'\nimport { aiLiveBatchWaitMs, type AiDevEnv as Env } from './ai-dev-env'\n\n/**\n * A LIVE EVAL ROUND AS ONE MESSAGE BATCH (AGL-3660), at half the price.\n *\n * `AGLYN_LIVE_AI_BATCH=1` turns every non-streaming request a live eval sends\n * into an entry of a batch instead of a request of its own. The eval code does\n * not change: each request still awaits its own answer. Requests that arrive\n * together — a `Promise.all` over the briefs — gather into one batch, which\n * is sent once nothing new has arrived for `AGLYN_LIVE_AI_BATCH_WINDOW_MS`\n * (2 s), and every request resolves when the batch has ended. A re-ask is\n * asked only once its answer has been read, so the re-asks of a round gather\n * into the NEXT batch on their own: a round with re-asks is two or three\n * batches, never a request apiece.\n *\n * A batch takes minutes rather than seconds (most end within the hour), so it\n * suits the full sweep an agent runs before landing, not the one brief it is\n * iterating on. Streams are never batched. Development only: refused in any\n * deployed server, where nothing should wait minutes for an answer.\n *\n * A batch is waited on for `AGLYN_LIVE_AI_BATCH_WAIT_MS` (half an hour) and\n * no longer (AGL-3781). What it answered by then is kept; each request it left\n * unanswered is asked on its own — the same request, cache breakpoints and\n * all, the first alone so it writes the cached prefix the rest then read —\n * and counted as a live request at full price, not a batched one. One line\n * says so when it happens. A slow batch costs a round part of its discount,\n * never its answers.\n */\n\n/** How long the queue waits for one more request before it sends the batch. */\nexport const AI_LIVE_BATCH_WINDOW_ENV = 'AGLYN_LIVE_AI_BATCH_WINDOW_MS'\n\n\ninterface Queued {\n request: AiProviderRequest\n resolve: (result: AiResult) => void\n reject: (error: unknown) => void\n}\n\nconst queues = new Map<\n AiProvider,\n { entries: Queued[]; timer: ReturnType<typeof setTimeout> | null; env: Env }\n>()\n\n/** What this process's batches did. */\nexport interface AiLiveBatchStats {\n /** Batches sent. */\n batches: number\n /** Requests a batch answered, at the batch's half price. */\n requests: number\n /** Requests a batch left unanswered past its wait, asked on their own at full price. */\n fallbacks: number\n}\n\nconst counts: AiLiveBatchStats = { batches: 0, requests: 0, fallbacks: 0 }\n\nexport function aiLiveBatchStats(): AiLiveBatchStats {\n return { ...counts }\n}\n\nexport function resetAiLiveBatchStats(): void {\n counts.batches = 0\n counts.requests = 0\n counts.fallbacks = 0\n}\n\n/** Results a batch did not answer, answered by a request of their own — billed at full price. */\nconst answeredOnTheirOwn = new WeakSet<AiResult>()\n\n/** Whether this result came from a request of its own after its batch's wait ran out (AGL-3781). */\nexport function aiAnsweredOutsideBatch(result: AiResult): boolean {\n return answeredOnTheirOwn.has(result)\n}\n\nasync function answerOnItsOwn(provider: AiProvider, entry: Queued): Promise<void> {\n try {\n const result = await provider.complete(entry.request)\n answeredOnTheirOwn.add(result)\n entry.resolve(result)\n } catch (error) {\n entry.reject(error)\n }\n}\n\n/**\n * The requests a batch left unanswered, each asked on its own: the first\n * alone, so the cached prefix it writes is there for the others to read,\n * then the rest together.\n */\nasync function finishOnTheirOwn(provider: AiProvider, left: readonly Queued[], of: number, waitMs: number) {\n counts.fallbacks += left.length\n console.warn(\n `[ai] live batch: ${left.length} of ${of} requests unanswered after ${waitMs} ms; ` +\n 'asked on their own at full price',\n )\n const [first, ...rest] = left\n await answerOnItsOwn(provider, first)\n await Promise.all(rest.map((entry) => answerOnItsOwn(provider, entry)))\n}\n\nfunction windowMs(env: Env): number {\n const parsed = Number(env[AI_LIVE_BATCH_WINDOW_ENV])\n return Number.isFinite(parsed) && parsed >= 0 ? parsed : 2_000\n}\n\nasync function flush(provider: AiProvider): Promise<void> {\n const queue = queues.get(provider)\n if (!queue) return\n const entries = queue.entries\n queues.delete(provider)\n if (!entries.length || !provider.completeBatch) return\n counts.batches += 1\n const waitMs = aiLiveBatchWaitMs(queue.env)\n let results: Array<AiResult | Error>\n try {\n results = await provider.completeBatch(\n entries.map((entry) => entry.request),\n { timeoutMs: waitMs },\n )\n } catch (error) {\n for (const entry of entries) entry.reject(error)\n return\n }\n const left: Queued[] = []\n entries.forEach((entry, index) => {\n const result = results[index]\n if (result instanceof AiBatchUnfinishedError) left.push(entry)\n else if (result instanceof Error || !result) entry.reject(result ?? new Error('the batch returned no result'))\n else {\n counts.requests += 1\n entry.resolve(result)\n }\n })\n if (left.length) await finishOnTheirOwn(provider, left, entries.length, waitMs)\n}\n\n/** One request as an entry of the next batch to `provider`. */\nexport function aiCompleteInBatch(\n provider: AiProvider,\n request: AiProviderRequest,\n env: Env = process.env,\n): Promise<AiResult> {\n if (!provider.completeBatch) return provider.complete(request)\n return new Promise<AiResult>((resolve, reject) => {\n const queue = queues.get(provider) ?? { entries: [], timer: null, env }\n queues.set(provider, queue)\n queue.entries.push({ request, resolve, reject })\n if (queue.timer) clearTimeout(queue.timer)\n queue.timer = setTimeout(() => void flush(provider), windowMs(env))\n })\n}\n"],"names":["AiBatchUnfinishedError","aiLiveBatchWaitMs","AI_LIVE_BATCH_WINDOW_ENV","queues","Map","counts","batches","requests","fallbacks","aiLiveBatchStats","resetAiLiveBatchStats","answeredOnTheirOwn","WeakSet","aiAnsweredOutsideBatch","result","has","answerOnItsOwn","provider","entry","complete","request","add","resolve","error","reject","finishOnTheirOwn","left","of","waitMs","length","console","warn","first","rest","Promise","all","map","windowMs","env","parsed","Number","isFinite","flush","queue","get","entries","delete","completeBatch","results","timeoutMs","forEach","index","push","Error","aiCompleteInBatch","process","timer","set","clearTimeout","setTimeout"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,sBAAsB,QAAgE,2BAAuB;AACtH,SAASC,iBAAiB,QAA8B,kBAAc;AAEtE;;;;;;;;;;;;;;;;;;;;;;;;;CAyBC,GAED,6EAA6E,GAC7E,OAAO,MAAMC,2BAA2B,gCAA+B;AASvE,MAAMC,SAAS,IAAIC;AAenB,MAAMC,SAA2B;IAAEC,SAAS;IAAGC,UAAU;IAAGC,WAAW;AAAE;AAEzE,OAAO,SAASC;IACd,OAAO,aAAKJ;AACd;AAEA,OAAO,SAASK;IACdL,OAAOC,OAAO,GAAG;IACjBD,OAAOE,QAAQ,GAAG;IAClBF,OAAOG,SAAS,GAAG;AACrB;AAEA,+FAA+F,GAC/F,MAAMG,qBAAqB,IAAIC;AAE/B,kGAAkG,GAClG,OAAO,SAASC,uBAAuBC,MAAgB;IACrD,OAAOH,mBAAmBI,GAAG,CAACD;AAChC;AAEA,eAAeE,eAAeC,QAAoB,EAAEC,KAAa;IAC/D,IAAI;QACF,MAAMJ,SAAS,MAAMG,SAASE,QAAQ,CAACD,MAAME,OAAO;QACpDT,mBAAmBU,GAAG,CAACP;QACvBI,MAAMI,OAAO,CAACR;IAChB,EAAE,OAAOS,OAAO;QACdL,MAAMM,MAAM,CAACD;IACf;AACF;AAEA;;;;CAIC,GACD,eAAeE,iBAAiBR,QAAoB,EAAES,IAAuB,EAAEC,EAAU,EAAEC,MAAc;IACvGvB,OAAOG,SAAS,IAAIkB,KAAKG,MAAM;IAC/BC,QAAQC,IAAI,CACV,CAAC,iBAAiB,EAAEL,KAAKG,MAAM,CAAC,IAAI,EAAEF,GAAG,2BAA2B,EAAEC,OAAO,KAAK,CAAC,GACjF;IAEJ,MAAM,CAACI,OAAO,GAAGC,KAAK,GAAGP;IACzB,MAAMV,eAAeC,UAAUe;IAC/B,MAAME,QAAQC,GAAG,CAACF,KAAKG,GAAG,CAAC,CAAClB,QAAUF,eAAeC,UAAUC;AACjE;AAEA,SAASmB,SAASC,GAAQ;IACxB,MAAMC,SAASC,OAAOF,GAAG,CAACpC,yBAAyB;IACnD,OAAOsC,OAAOC,QAAQ,CAACF,WAAWA,UAAU,IAAIA,SAAS;AAC3D;AAEA,eAAeG,MAAMzB,QAAoB;IACvC,MAAM0B,QAAQxC,OAAOyC,GAAG,CAAC3B;IACzB,IAAI,CAAC0B,OAAO;IACZ,MAAME,UAAUF,MAAME,OAAO;IAC7B1C,OAAO2C,MAAM,CAAC7B;IACd,IAAI,CAAC4B,QAAQhB,MAAM,IAAI,CAACZ,SAAS8B,aAAa,EAAE;IAChD1C,OAAOC,OAAO,IAAI;IAClB,MAAMsB,SAAS3B,kBAAkB0C,MAAML,GAAG;IAC1C,IAAIU;IACJ,IAAI;QACFA,UAAU,MAAM/B,SAAS8B,aAAa,CACpCF,QAAQT,GAAG,CAAC,CAAClB,QAAUA,MAAME,OAAO,GACpC;YAAE6B,WAAWrB;QAAO;IAExB,EAAE,OAAOL,OAAO;QACd,KAAK,MAAML,SAAS2B,QAAS3B,MAAMM,MAAM,CAACD;QAC1C;IACF;IACA,MAAMG,OAAiB,EAAE;IACzBmB,QAAQK,OAAO,CAAC,CAAChC,OAAOiC;QACtB,MAAMrC,SAASkC,OAAO,CAACG,MAAM;QAC7B,IAAIrC,kBAAkBd,wBAAwB0B,KAAK0B,IAAI,CAAClC;aACnD,IAAIJ,kBAAkBuC,SAAS,CAACvC,QAAQI,MAAMM,MAAM,CAACV,iBAAAA,SAAU,IAAIuC,MAAM;aACzE;YACHhD,OAAOE,QAAQ,IAAI;YACnBW,MAAMI,OAAO,CAACR;QAChB;IACF;IACA,IAAIY,KAAKG,MAAM,EAAE,MAAMJ,iBAAiBR,UAAUS,MAAMmB,QAAQhB,MAAM,EAAED;AAC1E;AAEA,6DAA6D,GAC7D,OAAO,SAAS0B,kBACdrC,QAAoB,EACpBG,OAA0B,EAC1BkB,MAAWiB,QAAQjB,GAAG;IAEtB,IAAI,CAACrB,SAAS8B,aAAa,EAAE,OAAO9B,SAASE,QAAQ,CAACC;IACtD,OAAO,IAAIc,QAAkB,CAACZ,SAASE;YACvBrB;QAAd,MAAMwC,SAAQxC,cAAAA,OAAOyC,GAAG,CAAC3B,qBAAXd,cAAwB;YAAE0C,SAAS,EAAE;YAAEW,OAAO;YAAMlB;QAAI;QACtEnC,OAAOsD,GAAG,CAACxC,UAAU0B;QACrBA,MAAME,OAAO,CAACO,IAAI,CAAC;YAAEhC;YAASE;YAASE;QAAO;QAC9C,IAAImB,MAAMa,KAAK,EAAEE,aAAaf,MAAMa,KAAK;QACzCb,MAAMa,KAAK,GAAGG,WAAW,IAAM,KAAKjB,MAAMzB,WAAWoB,SAASC;IAChE;AACF"}
@@ -3045,6 +3045,9 @@
3045
3045
  offset: {
3046
3046
  type: 'string'
3047
3047
  },
3048
+ fillLastRow: {
3049
+ type: 'boolean'
3050
+ },
3048
3051
  spacing: {
3049
3052
  type: 'string',
3050
3053
  maxLength: 200
@@ -3101,6 +3104,7 @@
3101
3104
  container: 'switch',
3102
3105
  size: 'breakpoint-span',
3103
3106
  offset: 'breakpoint-span',
3107
+ fillLastRow: 'switch',
3104
3108
  spacing: 'text-field',
3105
3109
  rowSpacing: 'text-field',
3106
3110
  columnSpacing: 'text-field',