@stackfactor/agent-utils 1.4.3 → 1.8.0

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 (46) hide show
  1. package/README.md +81 -6
  2. package/dist/cjs/errorHandling.d.ts.map +1 -1
  3. package/dist/cjs/errorHandling.js +10 -5
  4. package/dist/cjs/index.d.ts +8 -2
  5. package/dist/cjs/index.d.ts.map +1 -1
  6. package/dist/cjs/index.js +9 -1
  7. package/dist/cjs/langChain.d.ts +25 -5
  8. package/dist/cjs/langChain.d.ts.map +1 -1
  9. package/dist/cjs/langChain.js +244 -179
  10. package/dist/cjs/runtimeContext.d.ts +3 -0
  11. package/dist/cjs/runtimeContext.d.ts.map +1 -1
  12. package/dist/cjs/serve.d.ts +88 -0
  13. package/dist/cjs/serve.d.ts.map +1 -1
  14. package/dist/cjs/serve.js +230 -3
  15. package/dist/cjs/stackfactorKnowledge.d.ts +182 -0
  16. package/dist/cjs/stackfactorKnowledge.d.ts.map +1 -0
  17. package/dist/cjs/stackfactorKnowledge.js +274 -0
  18. package/dist/cjs/tavily.d.ts +4 -0
  19. package/dist/cjs/tavily.d.ts.map +1 -1
  20. package/dist/cjs/tavily.js +17 -18
  21. package/dist/cjs/usage.d.ts +74 -0
  22. package/dist/cjs/usage.d.ts.map +1 -0
  23. package/dist/cjs/usage.js +160 -0
  24. package/dist/esm/errorHandling.d.ts.map +1 -1
  25. package/dist/esm/errorHandling.js +10 -5
  26. package/dist/esm/index.d.ts +8 -2
  27. package/dist/esm/index.d.ts.map +1 -1
  28. package/dist/esm/index.js +6 -2
  29. package/dist/esm/langChain.d.ts +25 -5
  30. package/dist/esm/langChain.d.ts.map +1 -1
  31. package/dist/esm/langChain.js +244 -179
  32. package/dist/esm/runtimeContext.d.ts +3 -0
  33. package/dist/esm/runtimeContext.d.ts.map +1 -1
  34. package/dist/esm/serve.d.ts +88 -0
  35. package/dist/esm/serve.d.ts.map +1 -1
  36. package/dist/esm/serve.js +226 -3
  37. package/dist/esm/stackfactorKnowledge.d.ts +182 -0
  38. package/dist/esm/stackfactorKnowledge.d.ts.map +1 -0
  39. package/dist/esm/stackfactorKnowledge.js +266 -0
  40. package/dist/esm/tavily.d.ts +4 -0
  41. package/dist/esm/tavily.d.ts.map +1 -1
  42. package/dist/esm/tavily.js +17 -18
  43. package/dist/esm/usage.d.ts +74 -0
  44. package/dist/esm/usage.d.ts.map +1 -0
  45. package/dist/esm/usage.js +150 -0
  46. package/package.json +14 -8
@@ -16,6 +16,7 @@ const logger_js_1 = __importDefault(require("./logger.js"));
16
16
  const zod_1 = require("zod");
17
17
  const transform_json_schema_1 = require("@anthropic-ai/sdk/lib/transform-json-schema");
18
18
  const runtimeContext_js_1 = require("./runtimeContext.js");
19
+ const usage_js_1 = require("./usage.js");
19
20
  const tavily_js_1 = require("./tavily.js");
20
21
  const JSON_ESCAPE_INSTRUCTION = `
21
22
  CRITICAL - Your response must be valid JSON. Escape ALL special characters in string values:
@@ -104,29 +105,36 @@ const checkIfAIProviderConfigured = (config) => {
104
105
  }
105
106
  };
106
107
  /**
107
- * Reads a per-model rate from the flat cost constants the integration defines
108
- * in its `config.yaml` (exposed on the config object), e.g.
109
- * `claude-opus-4-7-input-token-costs`. Returns `0` when the constant is
110
- * absent or not numeric, which leaves cost contribution zero.
108
+ * Counts `amount` of `unit` for `modelName`: into the request's usage ledger,
109
+ * which is reported to the platform, and into the caller's tracker when it
110
+ * passed one. Nothing is counted for an amount that is not positive.
111
111
  */
112
- const getModelRate = (modelName, config, kind) => {
113
- if (!config || !modelName)
114
- return 0;
115
- const rate = Number(config[`${modelName}-${kind}-costs`]);
116
- return Number.isFinite(rate) ? rate : 0;
112
+ const countUsage = (tracker, modelName, unit, amount) => {
113
+ if (!Number.isFinite(amount) || amount <= 0)
114
+ return;
115
+ (0, usage_js_1.recordUsage)(modelName, unit, amount);
116
+ if (!tracker)
117
+ return;
118
+ const key = `${modelName}_${unit}`;
119
+ tracker.tokens[key] = (tracker.tokens[key] || 0) + amount;
117
120
  };
118
- /**
119
- * Adds a single call's usage to the caller-supplied tracker. Initializes
120
- * `cost` and `tokens` lazily so callers can pass `{}`. No-ops when the
121
- * tracker is null/undefined.
122
- */
123
- const updateUsageTracker = (tracker, modelName, usage, config) => {
124
- if (!tracker || !usage || !modelName)
121
+ /** Makes `{}` a tracker. `cost` stays 0: the platform prices what was used. */
122
+ const prepareTracker = (tracker) => {
123
+ if (!tracker)
125
124
  return;
126
125
  if (typeof tracker.cost !== "number")
127
126
  tracker.cost = 0;
128
127
  if (!tracker.tokens || typeof tracker.tokens !== "object")
129
128
  tracker.tokens = {};
129
+ };
130
+ /**
131
+ * Counts a single call's token usage. `config` is no longer read: it carried
132
+ * the rates, and what usage costs is the platform's to say.
133
+ */
134
+ const updateUsageTracker = (tracker, modelName, usage, _config) => {
135
+ if (!usage || !modelName)
136
+ return;
137
+ prepareTracker(tracker);
130
138
  const inputTokens = usage.input_tokens || 0;
131
139
  const visibleOutputTokens = usage.output_tokens || 0;
132
140
  const totalTokens = usage.total_tokens || 0;
@@ -135,40 +143,24 @@ const updateUsageTracker = (tracker, modelName, usage, config) => {
135
143
  // `total - input - visibleOutput` is the thinking output the caller was still
136
144
  // billed for. Providers that already fold reasoning into `output_tokens`
137
145
  // (Anthropic, OpenAI) report `total == input + output`, so this adds 0.
138
- // Reasoning is charged at the output rate, so we treat it as output for both
139
- // the token counters and the cost.
146
+ // Reasoning is charged at the output rate, so it is counted as output.
140
147
  const reasoningTokens = totalTokens > 0
141
148
  ? Math.max(0, totalTokens - inputTokens - visibleOutputTokens)
142
149
  : 0;
143
- const outputTokens = visibleOutputTokens + reasoningTokens;
144
- const inputRate = getModelRate(modelName, config, "input-token");
145
- const outputRate = getModelRate(modelName, config, "output-token");
146
- const addedCost = (inputTokens / 1_000_000) * inputRate +
147
- (outputTokens / 1_000_000) * outputRate;
148
- if (Number.isFinite(addedCost) && addedCost > 0)
149
- tracker.cost += addedCost;
150
- const inputKey = `${modelName}_inputTokens`;
151
- const outputKey = `${modelName}_outputTokens`;
152
- tracker.tokens[inputKey] = (tracker.tokens[inputKey] || 0) + inputTokens;
153
- tracker.tokens[outputKey] = (tracker.tokens[outputKey] || 0) + outputTokens;
150
+ countUsage(tracker, modelName, "inputTokens", inputTokens);
151
+ countUsage(tracker, modelName, "outputTokens", visibleOutputTokens + reasoningTokens);
154
152
  };
155
153
  /**
156
- * Adds a single image-generation call's usage to the caller-supplied tracker.
157
- * Extracts token counts from provider-specific response shapes: OpenAI returns
158
- * `response.usage` with `input_tokens_details.{text_tokens,image_tokens}` and
159
- * `output_tokens`; Google returns `response.usageMetadata` with
160
- * `promptTokensDetails` (modality-keyed) and `candidatesTokenCount` for the
161
- * generated image. Reads rates from `<model>-input-token-costs`,
162
- * `<model>-image-input-token-costs`, and `<model>-image-output-token-costs`.
163
- * No-ops when the tracker or response usage info is absent.
154
+ * Counts a single image-generation call's usage. Extracts token counts from
155
+ * provider-specific response shapes: OpenAI returns `response.usage` with
156
+ * `input_tokens_details.{text_tokens,image_tokens}` and `output_tokens`;
157
+ * Google returns `response.usageMetadata` with `promptTokensDetails`
158
+ * (modality-keyed) and `candidatesTokenCount` for the generated image.
159
+ * No-ops when the response carries no usage.
164
160
  */
165
- const updateImageUsageTracker = (tracker, modelName, response, config, provider) => {
166
- if (!tracker || !modelName || !response)
161
+ const updateImageUsageTracker = (tracker, modelName, response, _config, provider) => {
162
+ if (!modelName || !response)
167
163
  return;
168
- if (typeof tracker.cost !== "number")
169
- tracker.cost = 0;
170
- if (!tracker.tokens || typeof tracker.tokens !== "object")
171
- tracker.tokens = {};
172
164
  let textInputTokens = 0;
173
165
  let imageInputTokens = 0;
174
166
  let imageOutputTokens = 0;
@@ -205,85 +197,42 @@ const updateImageUsageTracker = (tracker, modelName, response, config, provider)
205
197
  }
206
198
  imageOutputTokens = Number(um.candidatesTokenCount) || 0;
207
199
  }
208
- const textInputRate = getModelRate(modelName, config, "input-token");
209
- // Reference-image input tokens are billed at the model's input rate. Configs
210
- // that don't define a dedicated `<model>-image-input-token-costs` (the common
211
- // case — image models charge all input at one rate and only differ on output)
212
- // fall back to the text input rate rather than silently billing image input
213
- // at $0.
214
- const imageInputRate = getModelRate(modelName, config, "image-input-token") || textInputRate;
215
- const imageOutputRate = getModelRate(modelName, config, "image-output-token");
216
- const addedCost = (textInputTokens / 1_000_000) * textInputRate +
217
- (imageInputTokens / 1_000_000) * imageInputRate +
218
- (imageOutputTokens / 1_000_000) * imageOutputRate;
219
- if (Number.isFinite(addedCost) && addedCost > 0)
220
- tracker.cost += addedCost;
221
- if (textInputTokens > 0) {
222
- const key = `${modelName}_inputTokens`;
223
- tracker.tokens[key] = (tracker.tokens[key] || 0) + textInputTokens;
224
- }
225
- if (imageInputTokens > 0) {
226
- const key = `${modelName}_imageInputTokens`;
227
- tracker.tokens[key] = (tracker.tokens[key] || 0) + imageInputTokens;
228
- }
229
- if (imageOutputTokens > 0) {
230
- const key = `${modelName}_imageOutputTokens`;
231
- tracker.tokens[key] = (tracker.tokens[key] || 0) + imageOutputTokens;
232
- }
200
+ prepareTracker(tracker);
201
+ countUsage(tracker, modelName, "inputTokens", textInputTokens);
202
+ // The platform prices an image a model is given as its other input, unless
203
+ // the model has a rate of its own for it.
204
+ countUsage(tracker, modelName, "imageInputTokens", imageInputTokens);
205
+ countUsage(tracker, modelName, "imageOutputTokens", imageOutputTokens);
233
206
  };
234
207
  /**
235
- * Adds character-billed usage (e.g. ElevenLabs TTS) to the caller-supplied
236
- * tracker. There is no provider response to parse — billing is deterministic
237
- * from the input character count. Reads the rate from
238
- * `<model>-character-costs` (USD per million characters) and accumulates
239
- * under `<model>_characters`. No-ops on missing tracker / model / count.
208
+ * Counts character-billed usage (e.g. ElevenLabs TTS). There is no provider
209
+ * response to parse — billing is deterministic from the input character
210
+ * count. No-ops on a missing model or count.
240
211
  */
241
- const updateUsageTrackerForCharacters = (tracker, modelName, characterCount, config) => {
242
- if (!tracker || !modelName || !characterCount)
212
+ const updateUsageTrackerForCharacters = (tracker, modelName, characterCount, _config) => {
213
+ if (!modelName || !characterCount)
243
214
  return;
244
- if (typeof tracker.cost !== "number")
245
- tracker.cost = 0;
246
- if (!tracker.tokens || typeof tracker.tokens !== "object")
247
- tracker.tokens = {};
248
- const rate = getModelRate(modelName, config, "character");
249
- const addedCost = (characterCount / 1_000_000) * rate;
250
- if (Number.isFinite(addedCost) && addedCost > 0)
251
- tracker.cost += addedCost;
252
- const key = `${modelName}_characters`;
253
- tracker.tokens[key] = (tracker.tokens[key] || 0) + characterCount;
215
+ prepareTracker(tracker);
216
+ countUsage(tracker, modelName, "characters", characterCount);
254
217
  };
255
218
  /**
256
- * Adds second-billed usage (video generation) to the caller-supplied tracker.
257
- *
258
- * Video is the first output here that providers bill by DURATION rather than by
259
- * tokens or characters, so the rate constant `<model>-second-costs` is read as
260
- * USD PER SECOND — not per million, as the `input-token` and `character` rates
261
- * are. A Veo or Sora second costs cents, and a per-million reading would bill
262
- * it as effectively free.
219
+ * Counts second-billed usage (video generation): providers bill video by
220
+ * DURATION rather than by tokens or characters.
263
221
  *
264
- * The rate is per model, not per model-and-resolution, so a caller that lets a
265
- * tenant change resolution on a model whose price varies by it (sora-2-pro is
266
- * $0.30/$0.50/$0.70 at 720p/1024p/1080p) must pin the resolution its constant
267
- * describes, or it will bill the wrong number.
222
+ * The platform's rate is per model, not per model-and-resolution, so a caller
223
+ * that lets a tenant change resolution on a model whose price varies by it
224
+ * (sora-2-pro is $0.30/$0.50/$0.70 at 720p/1024p/1080p) must pin the
225
+ * resolution the rate describes, or the wrong number is charged.
268
226
  *
269
- * @param tracker - The accumulator to add to; no-ops when null
270
- * @param modelName - The video model id, used for both the rate and the counter key
227
+ * @param tracker - The caller's accumulator, when it passed one
228
+ * @param modelName - The video model id
271
229
  * @param seconds - Billed duration of the generated clip
272
- * @param config - Integration config carrying `<model>-second-costs`
273
230
  */
274
- const updateVideoUsageTracker = (tracker, modelName, seconds, config) => {
275
- if (!tracker || !modelName || !seconds)
231
+ const updateVideoUsageTracker = (tracker, modelName, seconds, _config) => {
232
+ if (!modelName || !seconds)
276
233
  return;
277
- if (typeof tracker.cost !== "number")
278
- tracker.cost = 0;
279
- if (!tracker.tokens || typeof tracker.tokens !== "object")
280
- tracker.tokens = {};
281
- const rate = getModelRate(modelName, config, "second");
282
- const addedCost = seconds * rate;
283
- if (Number.isFinite(addedCost) && addedCost > 0)
284
- tracker.cost += addedCost;
285
- const key = `${modelName}_seconds`;
286
- tracker.tokens[key] = (tracker.tokens[key] || 0) + seconds;
234
+ prepareTracker(tracker);
235
+ countUsage(tracker, modelName, "seconds", seconds);
287
236
  };
288
237
  /**
289
238
  * Extracts a normalized token-usage object from a single LangChain `invoke()`
@@ -384,8 +333,14 @@ const formatZodErrors = (zodError) => {
384
333
  const path = err.path.length > 0 ? err.path.join(".") : "root";
385
334
  // Provide more context based on error type
386
335
  switch (err.code) {
387
- case "invalid_type":
388
- return `Field "${path}": Expected ${err.expected}, but received ${err.received}`;
336
+ case "invalid_type": {
337
+ // zod v3 sets `received`; zod v4 names it only in the message
338
+ // ("Invalid input: expected string, received number").
339
+ const received = err.received ??
340
+ /received (\w+)/.exec(String(err.message ?? ""))?.[1] ??
341
+ "a different type";
342
+ return `Field "${path}": Expected ${err.expected}, but received ${received}`;
343
+ }
389
344
  case "invalid_literal":
390
345
  return `Field "${path}": Expected literal value ${JSON.stringify(err.expected)}, but received ${JSON.stringify(err.received)}`;
391
346
  case "unrecognized_keys":
@@ -822,19 +777,30 @@ const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat,
822
777
  * @param onProgress - Optional callback invoked with progress event objects throughout
823
778
  * agent execution
824
779
  * @param usageTracker - Optional usage accumulator updated on successful completion
780
+ * @param progress - The percentage every progress event reports (the stage's
781
+ * current one; the agent loop has no percentage of its own). Defaults to 0.
825
782
  * @returns The raw response object returned by the agent's `invoke` method
826
783
  */
827
- const runAgent = async (agent, prompt, config, onProgress = null, usageTracker = null) => {
784
+ const runAgent = async (agent, prompt, config, onProgress = null, usageTracker = null, progress = 0) => {
828
785
  const startTime = Date.now();
786
+ // Every event carries `progress`: callers forward these to the platform's
787
+ // `{ progress, message }` progress bar, where a missing value would read as
788
+ // 0% and make the bar jump back during a tool call. The agent loop reports
789
+ // no percentage of its own, so the stage's current one is repeated.
829
790
  const callbacks = onProgress
830
791
  ? [
831
792
  {
832
- handleToolStart: (toolInfo) => {
833
- if (toolInfo.name)
793
+ // LangChain calls handleToolStart(serializedTool, input, runId,
794
+ // parentRunId, tags, metadata, runName): the serialized tool
795
+ // ({ lc, type, id }) has no name; the tool's name is runName.
796
+ handleToolStart: (toolInfo, _input, _runId, _parentRunId, _tags, _metadata, runName) => {
797
+ const name = runName || toolInfo?.name || toolInfo?.kwargs?.name;
798
+ if (name)
834
799
  onProgress({
835
800
  type: "tool_start",
836
- message: `Using "${toolInfo.name}" tool`,
837
- tool: toolInfo.name,
801
+ message: `Using "${name}" tool`,
802
+ tool: name,
803
+ progress,
838
804
  });
839
805
  },
840
806
  handleAgentAction: (action) => {
@@ -843,12 +809,14 @@ const runAgent = async (agent, prompt, config, onProgress = null, usageTracker =
843
809
  message: action.log || `Calling "${action.tool}" action`,
844
810
  tool: action.tool,
845
811
  input: action.toolInput,
812
+ progress,
846
813
  });
847
814
  },
848
815
  handleChainError: (err) => {
849
816
  onProgress({
850
817
  type: "error",
851
818
  message: `Error: ${err?.message || String(err)}`,
819
+ progress,
852
820
  });
853
821
  },
854
822
  },
@@ -1100,6 +1068,65 @@ const parseAndValidateJSONResponse = (rawContent, schema) => {
1100
1068
  * original base) so that across multiple retries the full failure history
1101
1069
  * accumulates and the model can avoid oscillating between previous broken outputs.
1102
1070
  */
1071
+ /**
1072
+ * Whether a response that fails schema validation is retried with the errors
1073
+ * appended (buildValidationRetryMessages), up to MAX_VALIDATION_RETRIES.
1074
+ *
1075
+ * This used to be skipped whenever the model supports native structured
1076
+ * output, on the reasoning that a natively constrained response cannot be
1077
+ * invalid, so a failure had to be a schema bug worth surfacing at once. It
1078
+ * can be invalid, and routinely is: constrained decoding enforces the JSON
1079
+ * *shape*, not the Zod schema. Anthropic's SDK strips `enum` into a
1080
+ * description before sending, Gemini ignores `additionalProperties`, and no
1081
+ * provider runs a refine or a superRefine. Production, 2026-09-23/24: one
1082
+ * agent failed one call in six on extra keys the strict schema refused, and
1083
+ * another was rejected three runs in a row at its last stage on a value off
1084
+ * an enumerated list — every one on a native-schema model, every one reported
1085
+ * straight to the user, and every one restarted from zero by the caller
1086
+ * because the stage that failed was never asked again.
1087
+ *
1088
+ * So the retry now applies whenever a schema is in play. A genuine schema bug
1089
+ * still surfaces: it fails the same way three times and the last error is
1090
+ * thrown as before, at the cost of two extra calls.
1091
+ * @param expectsJsonResponse - Whether the call expects JSON at all
1092
+ * @param schema - The Zod schema the response is validated against, if any
1093
+ * @returns Whether a validation failure is retried
1094
+ */
1095
+ const canRetryValidation = (expectsJsonResponse, schema) => !!expectsJsonResponse && !!schema;
1096
+ /**
1097
+ * Whether a failure of the JSON stage is one the model can fix if asked again:
1098
+ * a response that failed schema validation, or one that was not JSON at all
1099
+ * (prose, or JSON with a preamble the extractor could not recover). Everything
1100
+ * else — no messages, a provider error, a transport error — is not.
1101
+ * @param error - The failure
1102
+ * @returns Whether a retry with the errors appended makes sense
1103
+ */
1104
+ const isRetryableJsonFailure = (error) => error?.code === const_js_1.default.HTTP_CODES.UNPROCESSABLE_ENTITY ||
1105
+ /non-JSON response when JSON was expected/.test(String(error?.message ?? ""));
1106
+ /**
1107
+ * The agentic run takes one user prompt rather than a message list, so a retry
1108
+ * carries the failed output and the errors inside that prompt: the model sees
1109
+ * what it produced and what was wrong with it, the same information
1110
+ * buildValidationRetryMessages gives the non-agentic paths. Previous attempts
1111
+ * accumulate so the model does not oscillate between earlier broken outputs.
1112
+ * @param userPrompt - The prompt of the attempt that failed
1113
+ * @param rawContent - What the model answered
1114
+ * @param error - Why it was refused
1115
+ * @returns The user prompt for the next attempt
1116
+ */
1117
+ const buildAgenticRetryPrompt = (userPrompt, rawContent, error) => {
1118
+ const previous = typeof rawContent === "string" ? rawContent : JSON.stringify(rawContent);
1119
+ return `${userPrompt}
1120
+
1121
+ ---
1122
+ Your previous response was rejected:
1123
+ ${String(error?.message ?? error)}
1124
+
1125
+ Your previous response was:
1126
+ ${previous}
1127
+
1128
+ Return a corrected JSON response that fixes ALL of these issues. Respond with the JSON only.`;
1129
+ };
1103
1130
  const buildValidationRetryMessages = (priorMessages, rawContent, validationError) => {
1104
1131
  const assistantContent = typeof rawContent === "string" ? rawContent : JSON.stringify(rawContent);
1105
1132
  return [
@@ -1111,6 +1138,58 @@ const buildValidationRetryMessages = (priorMessages, rawContent, validationError
1111
1138
  },
1112
1139
  ];
1113
1140
  };
1141
+ /**
1142
+ * The agentic path's JSON stage: the agent's final text as validated JSON.
1143
+ * Direct parse when it looks like JSON, the markdown/preamble extractor
1144
+ * otherwise; a schema failure throws UNPROCESSABLE_ENTITY (validateWithSchema),
1145
+ * anything the extractor cannot recover throws the "non-JSON response" error.
1146
+ * Both are what runPromptWithModel's agentic retry loop asks the model to fix.
1147
+ * @param rawContent - The agent's final message text
1148
+ * @param schema - The Zod schema to validate against, if any
1149
+ * @returns The JSON, re-serialised
1150
+ */
1151
+ const parseAgenticJsonResponse = (rawContent, schema) => {
1152
+ if (typeof rawContent === "string") {
1153
+ const trimmed = rawContent.trim();
1154
+ if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
1155
+ (trimmed.startsWith("[") && trimmed.endsWith("]"))) {
1156
+ try {
1157
+ const parsed = JSON.parse(trimmed);
1158
+ if (schema) {
1159
+ validateWithSchema(parsed, schema);
1160
+ }
1161
+ return JSON.stringify(parsed);
1162
+ }
1163
+ catch (parseError) {
1164
+ if (parseError?.code === const_js_1.default.HTTP_CODES.UNPROCESSABLE_ENTITY) {
1165
+ throw parseError;
1166
+ }
1167
+ }
1168
+ }
1169
+ // Try to extract JSON from markdown or other wrapping
1170
+ const extracted = extractJSONFromResponse(rawContent);
1171
+ if (extracted) {
1172
+ if (schema) {
1173
+ validateWithSchema(extracted, schema);
1174
+ }
1175
+ return JSON.stringify(extracted);
1176
+ }
1177
+ }
1178
+ let preview = "";
1179
+ if (typeof rawContent === "string") {
1180
+ preview = rawContent.substring(0, 100);
1181
+ }
1182
+ else if (typeof rawContent === "object" && rawContent !== null) {
1183
+ preview = JSON.stringify(rawContent).substring(0, 100);
1184
+ }
1185
+ else if (rawContent !== undefined && rawContent !== null) {
1186
+ preview = String(rawContent).substring(0, 100);
1187
+ }
1188
+ else {
1189
+ preview = "[empty response]";
1190
+ }
1191
+ throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `Agent returned non-JSON response when JSON was expected: ${preview}...`);
1192
+ };
1114
1193
  /**
1115
1194
  * Sends a prompt to an LLM and returns the response, with support for streaming,
1116
1195
  * agentic execution, progress reporting, JSON extraction, and Zod schema validation.
@@ -1193,65 +1272,43 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1193
1272
  // being reconciled from the response afterwards.
1194
1273
  const agent = createAgent(agentName, modelName, systemPrompt, [...tools], null, // responseFormat
1195
1274
  config, usageTracker);
1196
- // Run the agent with progress callback
1197
- const response = await runAgent(agent, userPrompt, config, onProgressReport || null, usageTracker);
1198
- // Extract content from agent response
1199
- const messages = response?.messages || [];
1200
- if (messages.length === 0) {
1201
- throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, "Agent returned no messages");
1202
- }
1203
- const lastMessage = messages[messages.length - 1];
1204
- // Flattens the array content blocks that Gemini/Claude agent responses and
1205
- // any server-tool turn (e.g. web search) return.
1206
- const rawContent = extractTextContent(lastMessage?.content);
1207
- // If not expecting JSON, return raw content directly
1208
- if (!expectsJsonResponse) {
1209
- return rawContent;
1210
- }
1211
- // Process JSON response similar to non-agentic path
1212
- if (typeof rawContent === "string") {
1213
- const trimmed = rawContent.trim();
1214
- if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
1215
- (trimmed.startsWith("[") && trimmed.endsWith("]"))) {
1216
- try {
1217
- const parsed = JSON.parse(trimmed);
1218
- if (schema) {
1219
- validateWithSchema(parsed, schema);
1220
- }
1221
- return JSON.stringify(parsed);
1222
- }
1223
- catch (parseError) {
1224
- if (parseError?.code === const_js_1.default.HTTP_CODES.UNPROCESSABLE_ENTITY) {
1225
- throw parseError;
1226
- }
1227
- }
1228
- }
1229
- // Try to extract JSON from markdown or other wrapping
1230
- const extracted = extractJSONFromResponse(rawContent);
1231
- if (extracted) {
1232
- if (schema) {
1233
- validateWithSchema(extracted, schema);
1234
- }
1235
- return JSON.stringify(extracted);
1236
- }
1237
- }
1238
- if (expectsJsonResponse) {
1239
- let preview = "";
1240
- if (typeof rawContent === "string") {
1241
- preview = rawContent.substring(0, 100);
1275
+ // The JSON stage is retried like the non-agentic paths (canRetryValidation):
1276
+ // the agent is run again with its rejected output and the errors appended
1277
+ // to the user prompt, up to MAX_VALIDATION_RETRIES. Until now this path
1278
+ // had no retry at all, and it is the one every tool-using generation takes.
1279
+ let activeUserPrompt = userPrompt;
1280
+ let attempt = 0;
1281
+ for (;;) {
1282
+ // Run the agent with progress callback
1283
+ const response = await runAgent(agent, activeUserPrompt, config, onProgressReport || null, usageTracker, minPercent);
1284
+ // Extract content from agent response
1285
+ const messages = response?.messages || [];
1286
+ if (messages.length === 0) {
1287
+ throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, "Agent returned no messages");
1242
1288
  }
1243
- else if (typeof rawContent === "object" && rawContent !== null) {
1244
- preview = JSON.stringify(rawContent).substring(0, 100);
1289
+ const lastMessage = messages[messages.length - 1];
1290
+ // Flattens the array content blocks that Gemini/Claude agent responses and
1291
+ // any server-tool turn (e.g. web search) return.
1292
+ const rawContent = extractTextContent(lastMessage?.content);
1293
+ // If not expecting JSON, return raw content directly
1294
+ if (!expectsJsonResponse) {
1295
+ return rawContent;
1245
1296
  }
1246
- else if (rawContent !== undefined && rawContent !== null) {
1247
- preview = String(rawContent).substring(0, 100);
1297
+ try {
1298
+ return parseAgenticJsonResponse(rawContent, schema);
1248
1299
  }
1249
- else {
1250
- preview = "[empty response]";
1300
+ catch (err) {
1301
+ if (canRetryValidation(expectsJsonResponse, schema) &&
1302
+ isRetryableJsonFailure(err) &&
1303
+ attempt < MAX_VALIDATION_RETRIES) {
1304
+ attempt++;
1305
+ logger_js_1.default.log(null, logger_js_1.default.levels.warn, `Schema validation failed for "${modelName}", retrying (${attempt}/${MAX_VALIDATION_RETRIES}): ${err.message}`);
1306
+ activeUserPrompt = buildAgenticRetryPrompt(activeUserPrompt, rawContent, err);
1307
+ continue;
1308
+ }
1309
+ throw err;
1251
1310
  }
1252
- throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `Agent returned non-JSON response when JSON was expected: ${preview}...`);
1253
1311
  }
1254
- return rawContent;
1255
1312
  }
1256
1313
  let messages;
1257
1314
  if (typeof prompt === "string") {
@@ -1296,13 +1353,13 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1296
1353
  else {
1297
1354
  messagesToSend = messages;
1298
1355
  }
1299
- // Retry once on schema validation failure. The retry is silent to the progress
1356
+ // Retry on schema validation failure. The retry is silent to the progress
1300
1357
  // callback — same "Generating content..." message and the progress curve
1301
1358
  // continues from the original startTime so the bar doesn't visibly reset.
1302
- // Skipped when OpenAI native response_format is in use (a 422 there is a
1303
- // server-side schema bug, not a model output issue). Each attempt accumulates
1304
- // independently into usageTracker so retry usage is still visible.
1305
- const canRetryOnValidation = expectsJsonResponse && !!schema && !useNativeSchema;
1359
+ // Each attempt accumulates independently into usageTracker so retry usage
1360
+ // is still visible. Native structured output no longer exempts a model:
1361
+ // see canRetryValidation.
1362
+ const canRetryOnValidation = canRetryValidation(expectsJsonResponse, schema);
1306
1363
  const progressReportInterval = 10; // Report every N chunks
1307
1364
  const overallStartTime = Date.now();
1308
1365
  // Use a time-based asymptotic curve: progress approaches maxPercent but never
@@ -1454,9 +1511,9 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1454
1511
  else {
1455
1512
  messagesToSend = messages;
1456
1513
  }
1457
- // Retry once on schema validation failure (see streaming branch for rationale).
1514
+ // Retry on schema validation failure (see streaming branch for rationale).
1458
1515
  // Each attempt accumulates independently into usageTracker.
1459
- const canRetryOnValidation = expectsJsonResponse && !!schema && !useNativeSchema;
1516
+ const canRetryOnValidation = canRetryValidation(expectsJsonResponse, schema);
1460
1517
  let activeMessages = messagesToSend;
1461
1518
  let attempt = 0;
1462
1519
  while (true) {
@@ -1965,6 +2022,11 @@ const runPromptWithModelForVideoGeneration = async (modelName, config, prompt, o
1965
2022
  }
1966
2023
  };
1967
2024
  exports.default = {
2025
+ canRetryValidation,
2026
+ isRetryableJsonFailure,
2027
+ buildAgenticRetryPrompt,
2028
+ parseAgenticJsonResponse,
2029
+ supportsNativeSchema,
1968
2030
  checkIfAIProviderConfigured,
1969
2031
  createAgent,
1970
2032
  runAgent,
@@ -1972,6 +2034,9 @@ exports.default = {
1972
2034
  runPromptWithModelForImageGeneration,
1973
2035
  runPromptWithModelForVideoGeneration,
1974
2036
  throwErrorIfNotSuccessful,
2037
+ updateImageUsageTracker,
2038
+ updateUsageTracker,
1975
2039
  updateUsageTrackerForCharacters,
2040
+ updateVideoUsageTracker,
1976
2041
  validateModel,
1977
2042
  };
@@ -1,3 +1,4 @@
1
+ import type { UsageLedger } from "./usage.js";
1
2
  /**
2
3
  * Per-request execution context for an agent invocation.
3
4
  *
@@ -25,6 +26,8 @@ export interface RequestContext {
25
26
  user: any;
26
27
  tenant: any;
27
28
  } | null;
29
+ /** What the request used and has not reported to the platform yet (see usage). */
30
+ usage?: UsageLedger;
28
31
  }
29
32
  /** Run `fn` with `ctx` bound as the active request context. */
30
33
  export declare const runWithContext: <T>(ctx: RequestContext, fn: () => Promise<T>) => Promise<T>;
@@ -1 +1 @@
1
- {"version":3,"file":"runtimeContext.d.ts","sourceRoot":"","sources":["../../src/runtimeContext.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,GAAG,CAAC;IACZ,MAAM,EAAE,GAAG,CAAC;IACZ,OAAO,EAAE,GAAG,CAAC;IACb,KAAK,EAAE,GAAG,CAAC;IACX,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IAC7E,0EAA0E;IAC1E,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3B,4EAA4E;IAC5E,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,GAAG,CAAC;QAAC,MAAM,EAAE,GAAG,CAAA;KAAE,GAAG,IAAI,CAAC;CAC7C;AAID,+DAA+D;AAC/D,eAAO,MAAM,cAAc,GAAI,CAAC,EAC9B,KAAK,cAAc,EACnB,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,KACnB,OAAO,CAAC,CAAC,CAAyB,CAAC;AAEtC,oEAAoE;AACpE,eAAO,MAAM,UAAU,QAAO,cAAc,GAAG,SAA+B,CAAC;AAE/E,oDAAoD;AACpD,eAAO,MAAM,cAAc,QAAO,WAAW,GAAG,IACZ,CAAC;AAIrC;;;;;GAKG;AACH,eAAO,MAAM,cAAc,QAAO,IAUjC,CAAC"}
1
+ {"version":3,"file":"runtimeContext.d.ts","sourceRoot":"","sources":["../../src/runtimeContext.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;;GAUG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,GAAG,CAAC;IACZ,MAAM,EAAE,GAAG,CAAC;IACZ,OAAO,EAAE,GAAG,CAAC;IACb,KAAK,EAAE,GAAG,CAAC;IACX,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IAC7E,0EAA0E;IAC1E,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3B,4EAA4E;IAC5E,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,GAAG,CAAC;QAAC,MAAM,EAAE,GAAG,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5C,kFAAkF;IAClF,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AAID,+DAA+D;AAC/D,eAAO,MAAM,cAAc,GAAI,CAAC,EAC9B,KAAK,cAAc,EACnB,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,KACnB,OAAO,CAAC,CAAC,CAAyB,CAAC;AAEtC,oEAAoE;AACpE,eAAO,MAAM,UAAU,QAAO,cAAc,GAAG,SAA+B,CAAC;AAE/E,oDAAoD;AACpD,eAAO,MAAM,cAAc,QAAO,WAAW,GAAG,IACZ,CAAC;AAIrC;;;;;GAKG;AACH,eAAO,MAAM,cAAc,QAAO,IAUjC,CAAC"}