@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.
- package/README.md +81 -6
- package/dist/cjs/errorHandling.d.ts.map +1 -1
- package/dist/cjs/errorHandling.js +10 -5
- package/dist/cjs/index.d.ts +8 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +9 -1
- package/dist/cjs/langChain.d.ts +25 -5
- package/dist/cjs/langChain.d.ts.map +1 -1
- package/dist/cjs/langChain.js +244 -179
- package/dist/cjs/runtimeContext.d.ts +3 -0
- package/dist/cjs/runtimeContext.d.ts.map +1 -1
- package/dist/cjs/serve.d.ts +88 -0
- package/dist/cjs/serve.d.ts.map +1 -1
- package/dist/cjs/serve.js +230 -3
- package/dist/cjs/stackfactorKnowledge.d.ts +182 -0
- package/dist/cjs/stackfactorKnowledge.d.ts.map +1 -0
- package/dist/cjs/stackfactorKnowledge.js +274 -0
- package/dist/cjs/tavily.d.ts +4 -0
- package/dist/cjs/tavily.d.ts.map +1 -1
- package/dist/cjs/tavily.js +17 -18
- package/dist/cjs/usage.d.ts +74 -0
- package/dist/cjs/usage.d.ts.map +1 -0
- package/dist/cjs/usage.js +160 -0
- package/dist/esm/errorHandling.d.ts.map +1 -1
- package/dist/esm/errorHandling.js +10 -5
- package/dist/esm/index.d.ts +8 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +6 -2
- package/dist/esm/langChain.d.ts +25 -5
- package/dist/esm/langChain.d.ts.map +1 -1
- package/dist/esm/langChain.js +244 -179
- package/dist/esm/runtimeContext.d.ts +3 -0
- package/dist/esm/runtimeContext.d.ts.map +1 -1
- package/dist/esm/serve.d.ts +88 -0
- package/dist/esm/serve.d.ts.map +1 -1
- package/dist/esm/serve.js +226 -3
- package/dist/esm/stackfactorKnowledge.d.ts +182 -0
- package/dist/esm/stackfactorKnowledge.d.ts.map +1 -0
- package/dist/esm/stackfactorKnowledge.js +266 -0
- package/dist/esm/tavily.d.ts +4 -0
- package/dist/esm/tavily.d.ts.map +1 -1
- package/dist/esm/tavily.js +17 -18
- package/dist/esm/usage.d.ts +74 -0
- package/dist/esm/usage.d.ts.map +1 -0
- package/dist/esm/usage.js +150 -0
- package/package.json +14 -8
package/dist/esm/langChain.js
CHANGED
|
@@ -10,6 +10,7 @@ import logger from "./logger.js";
|
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import { transformJSONSchema } from "@anthropic-ai/sdk/lib/transform-json-schema";
|
|
12
12
|
import { getAbortSignal } from "./runtimeContext.js";
|
|
13
|
+
import { recordUsage } from "./usage.js";
|
|
13
14
|
import { assertTavilyUsable, buildTavilyTools, getTavilyOptions, } from "./tavily.js";
|
|
14
15
|
const JSON_ESCAPE_INSTRUCTION = `
|
|
15
16
|
CRITICAL - Your response must be valid JSON. Escape ALL special characters in string values:
|
|
@@ -98,29 +99,36 @@ const checkIfAIProviderConfigured = (config) => {
|
|
|
98
99
|
}
|
|
99
100
|
};
|
|
100
101
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* absent or not numeric, which leaves cost contribution zero.
|
|
102
|
+
* Counts `amount` of `unit` for `modelName`: into the request's usage ledger,
|
|
103
|
+
* which is reported to the platform, and into the caller's tracker when it
|
|
104
|
+
* passed one. Nothing is counted for an amount that is not positive.
|
|
105
105
|
*/
|
|
106
|
-
const
|
|
107
|
-
if (!
|
|
108
|
-
return
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
const countUsage = (tracker, modelName, unit, amount) => {
|
|
107
|
+
if (!Number.isFinite(amount) || amount <= 0)
|
|
108
|
+
return;
|
|
109
|
+
recordUsage(modelName, unit, amount);
|
|
110
|
+
if (!tracker)
|
|
111
|
+
return;
|
|
112
|
+
const key = `${modelName}_${unit}`;
|
|
113
|
+
tracker.tokens[key] = (tracker.tokens[key] || 0) + amount;
|
|
111
114
|
};
|
|
112
|
-
/**
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
* tracker is null/undefined.
|
|
116
|
-
*/
|
|
117
|
-
const updateUsageTracker = (tracker, modelName, usage, config) => {
|
|
118
|
-
if (!tracker || !usage || !modelName)
|
|
115
|
+
/** Makes `{}` a tracker. `cost` stays 0: the platform prices what was used. */
|
|
116
|
+
const prepareTracker = (tracker) => {
|
|
117
|
+
if (!tracker)
|
|
119
118
|
return;
|
|
120
119
|
if (typeof tracker.cost !== "number")
|
|
121
120
|
tracker.cost = 0;
|
|
122
121
|
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
123
122
|
tracker.tokens = {};
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Counts a single call's token usage. `config` is no longer read: it carried
|
|
126
|
+
* the rates, and what usage costs is the platform's to say.
|
|
127
|
+
*/
|
|
128
|
+
const updateUsageTracker = (tracker, modelName, usage, _config) => {
|
|
129
|
+
if (!usage || !modelName)
|
|
130
|
+
return;
|
|
131
|
+
prepareTracker(tracker);
|
|
124
132
|
const inputTokens = usage.input_tokens || 0;
|
|
125
133
|
const visibleOutputTokens = usage.output_tokens || 0;
|
|
126
134
|
const totalTokens = usage.total_tokens || 0;
|
|
@@ -129,40 +137,24 @@ const updateUsageTracker = (tracker, modelName, usage, config) => {
|
|
|
129
137
|
// `total - input - visibleOutput` is the thinking output the caller was still
|
|
130
138
|
// billed for. Providers that already fold reasoning into `output_tokens`
|
|
131
139
|
// (Anthropic, OpenAI) report `total == input + output`, so this adds 0.
|
|
132
|
-
// Reasoning is charged at the output rate, so
|
|
133
|
-
// the token counters and the cost.
|
|
140
|
+
// Reasoning is charged at the output rate, so it is counted as output.
|
|
134
141
|
const reasoningTokens = totalTokens > 0
|
|
135
142
|
? Math.max(0, totalTokens - inputTokens - visibleOutputTokens)
|
|
136
143
|
: 0;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const outputRate = getModelRate(modelName, config, "output-token");
|
|
140
|
-
const addedCost = (inputTokens / 1_000_000) * inputRate +
|
|
141
|
-
(outputTokens / 1_000_000) * outputRate;
|
|
142
|
-
if (Number.isFinite(addedCost) && addedCost > 0)
|
|
143
|
-
tracker.cost += addedCost;
|
|
144
|
-
const inputKey = `${modelName}_inputTokens`;
|
|
145
|
-
const outputKey = `${modelName}_outputTokens`;
|
|
146
|
-
tracker.tokens[inputKey] = (tracker.tokens[inputKey] || 0) + inputTokens;
|
|
147
|
-
tracker.tokens[outputKey] = (tracker.tokens[outputKey] || 0) + outputTokens;
|
|
144
|
+
countUsage(tracker, modelName, "inputTokens", inputTokens);
|
|
145
|
+
countUsage(tracker, modelName, "outputTokens", visibleOutputTokens + reasoningTokens);
|
|
148
146
|
};
|
|
149
147
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* `
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* `<model>-image-input-token-costs`, and `<model>-image-output-token-costs`.
|
|
157
|
-
* No-ops when the tracker or response usage info is absent.
|
|
148
|
+
* Counts a single image-generation call's usage. Extracts token counts from
|
|
149
|
+
* provider-specific response shapes: OpenAI returns `response.usage` with
|
|
150
|
+
* `input_tokens_details.{text_tokens,image_tokens}` and `output_tokens`;
|
|
151
|
+
* Google returns `response.usageMetadata` with `promptTokensDetails`
|
|
152
|
+
* (modality-keyed) and `candidatesTokenCount` for the generated image.
|
|
153
|
+
* No-ops when the response carries no usage.
|
|
158
154
|
*/
|
|
159
|
-
const updateImageUsageTracker = (tracker, modelName, response,
|
|
160
|
-
if (!
|
|
155
|
+
const updateImageUsageTracker = (tracker, modelName, response, _config, provider) => {
|
|
156
|
+
if (!modelName || !response)
|
|
161
157
|
return;
|
|
162
|
-
if (typeof tracker.cost !== "number")
|
|
163
|
-
tracker.cost = 0;
|
|
164
|
-
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
165
|
-
tracker.tokens = {};
|
|
166
158
|
let textInputTokens = 0;
|
|
167
159
|
let imageInputTokens = 0;
|
|
168
160
|
let imageOutputTokens = 0;
|
|
@@ -199,85 +191,42 @@ const updateImageUsageTracker = (tracker, modelName, response, config, provider)
|
|
|
199
191
|
}
|
|
200
192
|
imageOutputTokens = Number(um.candidatesTokenCount) || 0;
|
|
201
193
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const imageInputRate = getModelRate(modelName, config, "image-input-token") || textInputRate;
|
|
209
|
-
const imageOutputRate = getModelRate(modelName, config, "image-output-token");
|
|
210
|
-
const addedCost = (textInputTokens / 1_000_000) * textInputRate +
|
|
211
|
-
(imageInputTokens / 1_000_000) * imageInputRate +
|
|
212
|
-
(imageOutputTokens / 1_000_000) * imageOutputRate;
|
|
213
|
-
if (Number.isFinite(addedCost) && addedCost > 0)
|
|
214
|
-
tracker.cost += addedCost;
|
|
215
|
-
if (textInputTokens > 0) {
|
|
216
|
-
const key = `${modelName}_inputTokens`;
|
|
217
|
-
tracker.tokens[key] = (tracker.tokens[key] || 0) + textInputTokens;
|
|
218
|
-
}
|
|
219
|
-
if (imageInputTokens > 0) {
|
|
220
|
-
const key = `${modelName}_imageInputTokens`;
|
|
221
|
-
tracker.tokens[key] = (tracker.tokens[key] || 0) + imageInputTokens;
|
|
222
|
-
}
|
|
223
|
-
if (imageOutputTokens > 0) {
|
|
224
|
-
const key = `${modelName}_imageOutputTokens`;
|
|
225
|
-
tracker.tokens[key] = (tracker.tokens[key] || 0) + imageOutputTokens;
|
|
226
|
-
}
|
|
194
|
+
prepareTracker(tracker);
|
|
195
|
+
countUsage(tracker, modelName, "inputTokens", textInputTokens);
|
|
196
|
+
// The platform prices an image a model is given as its other input, unless
|
|
197
|
+
// the model has a rate of its own for it.
|
|
198
|
+
countUsage(tracker, modelName, "imageInputTokens", imageInputTokens);
|
|
199
|
+
countUsage(tracker, modelName, "imageOutputTokens", imageOutputTokens);
|
|
227
200
|
};
|
|
228
201
|
/**
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
* `<model>-character-costs` (USD per million characters) and accumulates
|
|
233
|
-
* under `<model>_characters`. No-ops on missing tracker / model / count.
|
|
202
|
+
* Counts character-billed usage (e.g. ElevenLabs TTS). There is no provider
|
|
203
|
+
* response to parse — billing is deterministic from the input character
|
|
204
|
+
* count. No-ops on a missing model or count.
|
|
234
205
|
*/
|
|
235
|
-
const updateUsageTrackerForCharacters = (tracker, modelName, characterCount,
|
|
236
|
-
if (!
|
|
206
|
+
const updateUsageTrackerForCharacters = (tracker, modelName, characterCount, _config) => {
|
|
207
|
+
if (!modelName || !characterCount)
|
|
237
208
|
return;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
241
|
-
tracker.tokens = {};
|
|
242
|
-
const rate = getModelRate(modelName, config, "character");
|
|
243
|
-
const addedCost = (characterCount / 1_000_000) * rate;
|
|
244
|
-
if (Number.isFinite(addedCost) && addedCost > 0)
|
|
245
|
-
tracker.cost += addedCost;
|
|
246
|
-
const key = `${modelName}_characters`;
|
|
247
|
-
tracker.tokens[key] = (tracker.tokens[key] || 0) + characterCount;
|
|
209
|
+
prepareTracker(tracker);
|
|
210
|
+
countUsage(tracker, modelName, "characters", characterCount);
|
|
248
211
|
};
|
|
249
212
|
/**
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* Video is the first output here that providers bill by DURATION rather than by
|
|
253
|
-
* tokens or characters, so the rate constant `<model>-second-costs` is read as
|
|
254
|
-
* USD PER SECOND — not per million, as the `input-token` and `character` rates
|
|
255
|
-
* are. A Veo or Sora second costs cents, and a per-million reading would bill
|
|
256
|
-
* it as effectively free.
|
|
213
|
+
* Counts second-billed usage (video generation): providers bill video by
|
|
214
|
+
* DURATION rather than by tokens or characters.
|
|
257
215
|
*
|
|
258
|
-
* The rate is per model, not per model-and-resolution, so a caller
|
|
259
|
-
* tenant change resolution on a model whose price varies by it
|
|
260
|
-
* $0.30/$0.50/$0.70 at 720p/1024p/1080p) must pin the
|
|
261
|
-
* describes, or
|
|
216
|
+
* The platform's rate is per model, not per model-and-resolution, so a caller
|
|
217
|
+
* that lets a tenant change resolution on a model whose price varies by it
|
|
218
|
+
* (sora-2-pro is $0.30/$0.50/$0.70 at 720p/1024p/1080p) must pin the
|
|
219
|
+
* resolution the rate describes, or the wrong number is charged.
|
|
262
220
|
*
|
|
263
|
-
* @param tracker - The accumulator
|
|
264
|
-
* @param modelName - The video model id
|
|
221
|
+
* @param tracker - The caller's accumulator, when it passed one
|
|
222
|
+
* @param modelName - The video model id
|
|
265
223
|
* @param seconds - Billed duration of the generated clip
|
|
266
|
-
* @param config - Integration config carrying `<model>-second-costs`
|
|
267
224
|
*/
|
|
268
|
-
const updateVideoUsageTracker = (tracker, modelName, seconds,
|
|
269
|
-
if (!
|
|
225
|
+
const updateVideoUsageTracker = (tracker, modelName, seconds, _config) => {
|
|
226
|
+
if (!modelName || !seconds)
|
|
270
227
|
return;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
274
|
-
tracker.tokens = {};
|
|
275
|
-
const rate = getModelRate(modelName, config, "second");
|
|
276
|
-
const addedCost = seconds * rate;
|
|
277
|
-
if (Number.isFinite(addedCost) && addedCost > 0)
|
|
278
|
-
tracker.cost += addedCost;
|
|
279
|
-
const key = `${modelName}_seconds`;
|
|
280
|
-
tracker.tokens[key] = (tracker.tokens[key] || 0) + seconds;
|
|
228
|
+
prepareTracker(tracker);
|
|
229
|
+
countUsage(tracker, modelName, "seconds", seconds);
|
|
281
230
|
};
|
|
282
231
|
/**
|
|
283
232
|
* Extracts a normalized token-usage object from a single LangChain `invoke()`
|
|
@@ -378,8 +327,14 @@ const formatZodErrors = (zodError) => {
|
|
|
378
327
|
const path = err.path.length > 0 ? err.path.join(".") : "root";
|
|
379
328
|
// Provide more context based on error type
|
|
380
329
|
switch (err.code) {
|
|
381
|
-
case "invalid_type":
|
|
382
|
-
|
|
330
|
+
case "invalid_type": {
|
|
331
|
+
// zod v3 sets `received`; zod v4 names it only in the message
|
|
332
|
+
// ("Invalid input: expected string, received number").
|
|
333
|
+
const received = err.received ??
|
|
334
|
+
/received (\w+)/.exec(String(err.message ?? ""))?.[1] ??
|
|
335
|
+
"a different type";
|
|
336
|
+
return `Field "${path}": Expected ${err.expected}, but received ${received}`;
|
|
337
|
+
}
|
|
383
338
|
case "invalid_literal":
|
|
384
339
|
return `Field "${path}": Expected literal value ${JSON.stringify(err.expected)}, but received ${JSON.stringify(err.received)}`;
|
|
385
340
|
case "unrecognized_keys":
|
|
@@ -815,19 +770,30 @@ const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat,
|
|
|
815
770
|
* @param onProgress - Optional callback invoked with progress event objects throughout
|
|
816
771
|
* agent execution
|
|
817
772
|
* @param usageTracker - Optional usage accumulator updated on successful completion
|
|
773
|
+
* @param progress - The percentage every progress event reports (the stage's
|
|
774
|
+
* current one; the agent loop has no percentage of its own). Defaults to 0.
|
|
818
775
|
* @returns The raw response object returned by the agent's `invoke` method
|
|
819
776
|
*/
|
|
820
|
-
const runAgent = async (agent, prompt, config, onProgress = null, usageTracker = null) => {
|
|
777
|
+
const runAgent = async (agent, prompt, config, onProgress = null, usageTracker = null, progress = 0) => {
|
|
821
778
|
const startTime = Date.now();
|
|
779
|
+
// Every event carries `progress`: callers forward these to the platform's
|
|
780
|
+
// `{ progress, message }` progress bar, where a missing value would read as
|
|
781
|
+
// 0% and make the bar jump back during a tool call. The agent loop reports
|
|
782
|
+
// no percentage of its own, so the stage's current one is repeated.
|
|
822
783
|
const callbacks = onProgress
|
|
823
784
|
? [
|
|
824
785
|
{
|
|
825
|
-
handleToolStart
|
|
826
|
-
|
|
786
|
+
// LangChain calls handleToolStart(serializedTool, input, runId,
|
|
787
|
+
// parentRunId, tags, metadata, runName): the serialized tool
|
|
788
|
+
// ({ lc, type, id }) has no name; the tool's name is runName.
|
|
789
|
+
handleToolStart: (toolInfo, _input, _runId, _parentRunId, _tags, _metadata, runName) => {
|
|
790
|
+
const name = runName || toolInfo?.name || toolInfo?.kwargs?.name;
|
|
791
|
+
if (name)
|
|
827
792
|
onProgress({
|
|
828
793
|
type: "tool_start",
|
|
829
|
-
message: `Using "${
|
|
830
|
-
tool:
|
|
794
|
+
message: `Using "${name}" tool`,
|
|
795
|
+
tool: name,
|
|
796
|
+
progress,
|
|
831
797
|
});
|
|
832
798
|
},
|
|
833
799
|
handleAgentAction: (action) => {
|
|
@@ -836,12 +802,14 @@ const runAgent = async (agent, prompt, config, onProgress = null, usageTracker =
|
|
|
836
802
|
message: action.log || `Calling "${action.tool}" action`,
|
|
837
803
|
tool: action.tool,
|
|
838
804
|
input: action.toolInput,
|
|
805
|
+
progress,
|
|
839
806
|
});
|
|
840
807
|
},
|
|
841
808
|
handleChainError: (err) => {
|
|
842
809
|
onProgress({
|
|
843
810
|
type: "error",
|
|
844
811
|
message: `Error: ${err?.message || String(err)}`,
|
|
812
|
+
progress,
|
|
845
813
|
});
|
|
846
814
|
},
|
|
847
815
|
},
|
|
@@ -1093,6 +1061,65 @@ const parseAndValidateJSONResponse = (rawContent, schema) => {
|
|
|
1093
1061
|
* original base) so that across multiple retries the full failure history
|
|
1094
1062
|
* accumulates and the model can avoid oscillating between previous broken outputs.
|
|
1095
1063
|
*/
|
|
1064
|
+
/**
|
|
1065
|
+
* Whether a response that fails schema validation is retried with the errors
|
|
1066
|
+
* appended (buildValidationRetryMessages), up to MAX_VALIDATION_RETRIES.
|
|
1067
|
+
*
|
|
1068
|
+
* This used to be skipped whenever the model supports native structured
|
|
1069
|
+
* output, on the reasoning that a natively constrained response cannot be
|
|
1070
|
+
* invalid, so a failure had to be a schema bug worth surfacing at once. It
|
|
1071
|
+
* can be invalid, and routinely is: constrained decoding enforces the JSON
|
|
1072
|
+
* *shape*, not the Zod schema. Anthropic's SDK strips `enum` into a
|
|
1073
|
+
* description before sending, Gemini ignores `additionalProperties`, and no
|
|
1074
|
+
* provider runs a refine or a superRefine. Production, 2026-09-23/24: one
|
|
1075
|
+
* agent failed one call in six on extra keys the strict schema refused, and
|
|
1076
|
+
* another was rejected three runs in a row at its last stage on a value off
|
|
1077
|
+
* an enumerated list — every one on a native-schema model, every one reported
|
|
1078
|
+
* straight to the user, and every one restarted from zero by the caller
|
|
1079
|
+
* because the stage that failed was never asked again.
|
|
1080
|
+
*
|
|
1081
|
+
* So the retry now applies whenever a schema is in play. A genuine schema bug
|
|
1082
|
+
* still surfaces: it fails the same way three times and the last error is
|
|
1083
|
+
* thrown as before, at the cost of two extra calls.
|
|
1084
|
+
* @param expectsJsonResponse - Whether the call expects JSON at all
|
|
1085
|
+
* @param schema - The Zod schema the response is validated against, if any
|
|
1086
|
+
* @returns Whether a validation failure is retried
|
|
1087
|
+
*/
|
|
1088
|
+
const canRetryValidation = (expectsJsonResponse, schema) => !!expectsJsonResponse && !!schema;
|
|
1089
|
+
/**
|
|
1090
|
+
* Whether a failure of the JSON stage is one the model can fix if asked again:
|
|
1091
|
+
* a response that failed schema validation, or one that was not JSON at all
|
|
1092
|
+
* (prose, or JSON with a preamble the extractor could not recover). Everything
|
|
1093
|
+
* else — no messages, a provider error, a transport error — is not.
|
|
1094
|
+
* @param error - The failure
|
|
1095
|
+
* @returns Whether a retry with the errors appended makes sense
|
|
1096
|
+
*/
|
|
1097
|
+
const isRetryableJsonFailure = (error) => error?.code === constants.HTTP_CODES.UNPROCESSABLE_ENTITY ||
|
|
1098
|
+
/non-JSON response when JSON was expected/.test(String(error?.message ?? ""));
|
|
1099
|
+
/**
|
|
1100
|
+
* The agentic run takes one user prompt rather than a message list, so a retry
|
|
1101
|
+
* carries the failed output and the errors inside that prompt: the model sees
|
|
1102
|
+
* what it produced and what was wrong with it, the same information
|
|
1103
|
+
* buildValidationRetryMessages gives the non-agentic paths. Previous attempts
|
|
1104
|
+
* accumulate so the model does not oscillate between earlier broken outputs.
|
|
1105
|
+
* @param userPrompt - The prompt of the attempt that failed
|
|
1106
|
+
* @param rawContent - What the model answered
|
|
1107
|
+
* @param error - Why it was refused
|
|
1108
|
+
* @returns The user prompt for the next attempt
|
|
1109
|
+
*/
|
|
1110
|
+
const buildAgenticRetryPrompt = (userPrompt, rawContent, error) => {
|
|
1111
|
+
const previous = typeof rawContent === "string" ? rawContent : JSON.stringify(rawContent);
|
|
1112
|
+
return `${userPrompt}
|
|
1113
|
+
|
|
1114
|
+
---
|
|
1115
|
+
Your previous response was rejected:
|
|
1116
|
+
${String(error?.message ?? error)}
|
|
1117
|
+
|
|
1118
|
+
Your previous response was:
|
|
1119
|
+
${previous}
|
|
1120
|
+
|
|
1121
|
+
Return a corrected JSON response that fixes ALL of these issues. Respond with the JSON only.`;
|
|
1122
|
+
};
|
|
1096
1123
|
const buildValidationRetryMessages = (priorMessages, rawContent, validationError) => {
|
|
1097
1124
|
const assistantContent = typeof rawContent === "string" ? rawContent : JSON.stringify(rawContent);
|
|
1098
1125
|
return [
|
|
@@ -1104,6 +1131,58 @@ const buildValidationRetryMessages = (priorMessages, rawContent, validationError
|
|
|
1104
1131
|
},
|
|
1105
1132
|
];
|
|
1106
1133
|
};
|
|
1134
|
+
/**
|
|
1135
|
+
* The agentic path's JSON stage: the agent's final text as validated JSON.
|
|
1136
|
+
* Direct parse when it looks like JSON, the markdown/preamble extractor
|
|
1137
|
+
* otherwise; a schema failure throws UNPROCESSABLE_ENTITY (validateWithSchema),
|
|
1138
|
+
* anything the extractor cannot recover throws the "non-JSON response" error.
|
|
1139
|
+
* Both are what runPromptWithModel's agentic retry loop asks the model to fix.
|
|
1140
|
+
* @param rawContent - The agent's final message text
|
|
1141
|
+
* @param schema - The Zod schema to validate against, if any
|
|
1142
|
+
* @returns The JSON, re-serialised
|
|
1143
|
+
*/
|
|
1144
|
+
const parseAgenticJsonResponse = (rawContent, schema) => {
|
|
1145
|
+
if (typeof rawContent === "string") {
|
|
1146
|
+
const trimmed = rawContent.trim();
|
|
1147
|
+
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
1148
|
+
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
|
1149
|
+
try {
|
|
1150
|
+
const parsed = JSON.parse(trimmed);
|
|
1151
|
+
if (schema) {
|
|
1152
|
+
validateWithSchema(parsed, schema);
|
|
1153
|
+
}
|
|
1154
|
+
return JSON.stringify(parsed);
|
|
1155
|
+
}
|
|
1156
|
+
catch (parseError) {
|
|
1157
|
+
if (parseError?.code === constants.HTTP_CODES.UNPROCESSABLE_ENTITY) {
|
|
1158
|
+
throw parseError;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
// Try to extract JSON from markdown or other wrapping
|
|
1163
|
+
const extracted = extractJSONFromResponse(rawContent);
|
|
1164
|
+
if (extracted) {
|
|
1165
|
+
if (schema) {
|
|
1166
|
+
validateWithSchema(extracted, schema);
|
|
1167
|
+
}
|
|
1168
|
+
return JSON.stringify(extracted);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
let preview = "";
|
|
1172
|
+
if (typeof rawContent === "string") {
|
|
1173
|
+
preview = rawContent.substring(0, 100);
|
|
1174
|
+
}
|
|
1175
|
+
else if (typeof rawContent === "object" && rawContent !== null) {
|
|
1176
|
+
preview = JSON.stringify(rawContent).substring(0, 100);
|
|
1177
|
+
}
|
|
1178
|
+
else if (rawContent !== undefined && rawContent !== null) {
|
|
1179
|
+
preview = String(rawContent).substring(0, 100);
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
preview = "[empty response]";
|
|
1183
|
+
}
|
|
1184
|
+
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `Agent returned non-JSON response when JSON was expected: ${preview}...`);
|
|
1185
|
+
};
|
|
1107
1186
|
/**
|
|
1108
1187
|
* Sends a prompt to an LLM and returns the response, with support for streaming,
|
|
1109
1188
|
* agentic execution, progress reporting, JSON extraction, and Zod schema validation.
|
|
@@ -1186,65 +1265,43 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1186
1265
|
// being reconciled from the response afterwards.
|
|
1187
1266
|
const agent = createAgent(agentName, modelName, systemPrompt, [...tools], null, // responseFormat
|
|
1188
1267
|
config, usageTracker);
|
|
1189
|
-
//
|
|
1190
|
-
|
|
1191
|
-
//
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
return rawContent;
|
|
1203
|
-
}
|
|
1204
|
-
// Process JSON response similar to non-agentic path
|
|
1205
|
-
if (typeof rawContent === "string") {
|
|
1206
|
-
const trimmed = rawContent.trim();
|
|
1207
|
-
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
1208
|
-
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
|
1209
|
-
try {
|
|
1210
|
-
const parsed = JSON.parse(trimmed);
|
|
1211
|
-
if (schema) {
|
|
1212
|
-
validateWithSchema(parsed, schema);
|
|
1213
|
-
}
|
|
1214
|
-
return JSON.stringify(parsed);
|
|
1215
|
-
}
|
|
1216
|
-
catch (parseError) {
|
|
1217
|
-
if (parseError?.code === constants.HTTP_CODES.UNPROCESSABLE_ENTITY) {
|
|
1218
|
-
throw parseError;
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
// Try to extract JSON from markdown or other wrapping
|
|
1223
|
-
const extracted = extractJSONFromResponse(rawContent);
|
|
1224
|
-
if (extracted) {
|
|
1225
|
-
if (schema) {
|
|
1226
|
-
validateWithSchema(extracted, schema);
|
|
1227
|
-
}
|
|
1228
|
-
return JSON.stringify(extracted);
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
if (expectsJsonResponse) {
|
|
1232
|
-
let preview = "";
|
|
1233
|
-
if (typeof rawContent === "string") {
|
|
1234
|
-
preview = rawContent.substring(0, 100);
|
|
1268
|
+
// The JSON stage is retried like the non-agentic paths (canRetryValidation):
|
|
1269
|
+
// the agent is run again with its rejected output and the errors appended
|
|
1270
|
+
// to the user prompt, up to MAX_VALIDATION_RETRIES. Until now this path
|
|
1271
|
+
// had no retry at all, and it is the one every tool-using generation takes.
|
|
1272
|
+
let activeUserPrompt = userPrompt;
|
|
1273
|
+
let attempt = 0;
|
|
1274
|
+
for (;;) {
|
|
1275
|
+
// Run the agent with progress callback
|
|
1276
|
+
const response = await runAgent(agent, activeUserPrompt, config, onProgressReport || null, usageTracker, minPercent);
|
|
1277
|
+
// Extract content from agent response
|
|
1278
|
+
const messages = response?.messages || [];
|
|
1279
|
+
if (messages.length === 0) {
|
|
1280
|
+
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, "Agent returned no messages");
|
|
1235
1281
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1282
|
+
const lastMessage = messages[messages.length - 1];
|
|
1283
|
+
// Flattens the array content blocks that Gemini/Claude agent responses and
|
|
1284
|
+
// any server-tool turn (e.g. web search) return.
|
|
1285
|
+
const rawContent = extractTextContent(lastMessage?.content);
|
|
1286
|
+
// If not expecting JSON, return raw content directly
|
|
1287
|
+
if (!expectsJsonResponse) {
|
|
1288
|
+
return rawContent;
|
|
1238
1289
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1290
|
+
try {
|
|
1291
|
+
return parseAgenticJsonResponse(rawContent, schema);
|
|
1241
1292
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1293
|
+
catch (err) {
|
|
1294
|
+
if (canRetryValidation(expectsJsonResponse, schema) &&
|
|
1295
|
+
isRetryableJsonFailure(err) &&
|
|
1296
|
+
attempt < MAX_VALIDATION_RETRIES) {
|
|
1297
|
+
attempt++;
|
|
1298
|
+
logger.log(null, logger.levels.warn, `Schema validation failed for "${modelName}", retrying (${attempt}/${MAX_VALIDATION_RETRIES}): ${err.message}`);
|
|
1299
|
+
activeUserPrompt = buildAgenticRetryPrompt(activeUserPrompt, rawContent, err);
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
throw err;
|
|
1244
1303
|
}
|
|
1245
|
-
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `Agent returned non-JSON response when JSON was expected: ${preview}...`);
|
|
1246
1304
|
}
|
|
1247
|
-
return rawContent;
|
|
1248
1305
|
}
|
|
1249
1306
|
let messages;
|
|
1250
1307
|
if (typeof prompt === "string") {
|
|
@@ -1289,13 +1346,13 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1289
1346
|
else {
|
|
1290
1347
|
messagesToSend = messages;
|
|
1291
1348
|
}
|
|
1292
|
-
// Retry
|
|
1349
|
+
// Retry on schema validation failure. The retry is silent to the progress
|
|
1293
1350
|
// callback — same "Generating content..." message and the progress curve
|
|
1294
1351
|
// continues from the original startTime so the bar doesn't visibly reset.
|
|
1295
|
-
//
|
|
1296
|
-
//
|
|
1297
|
-
//
|
|
1298
|
-
const canRetryOnValidation = expectsJsonResponse
|
|
1352
|
+
// Each attempt accumulates independently into usageTracker so retry usage
|
|
1353
|
+
// is still visible. Native structured output no longer exempts a model:
|
|
1354
|
+
// see canRetryValidation.
|
|
1355
|
+
const canRetryOnValidation = canRetryValidation(expectsJsonResponse, schema);
|
|
1299
1356
|
const progressReportInterval = 10; // Report every N chunks
|
|
1300
1357
|
const overallStartTime = Date.now();
|
|
1301
1358
|
// Use a time-based asymptotic curve: progress approaches maxPercent but never
|
|
@@ -1447,9 +1504,9 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1447
1504
|
else {
|
|
1448
1505
|
messagesToSend = messages;
|
|
1449
1506
|
}
|
|
1450
|
-
// Retry
|
|
1507
|
+
// Retry on schema validation failure (see streaming branch for rationale).
|
|
1451
1508
|
// Each attempt accumulates independently into usageTracker.
|
|
1452
|
-
const canRetryOnValidation = expectsJsonResponse
|
|
1509
|
+
const canRetryOnValidation = canRetryValidation(expectsJsonResponse, schema);
|
|
1453
1510
|
let activeMessages = messagesToSend;
|
|
1454
1511
|
let attempt = 0;
|
|
1455
1512
|
while (true) {
|
|
@@ -1958,6 +2015,11 @@ const runPromptWithModelForVideoGeneration = async (modelName, config, prompt, o
|
|
|
1958
2015
|
}
|
|
1959
2016
|
};
|
|
1960
2017
|
export default {
|
|
2018
|
+
canRetryValidation,
|
|
2019
|
+
isRetryableJsonFailure,
|
|
2020
|
+
buildAgenticRetryPrompt,
|
|
2021
|
+
parseAgenticJsonResponse,
|
|
2022
|
+
supportsNativeSchema,
|
|
1961
2023
|
checkIfAIProviderConfigured,
|
|
1962
2024
|
createAgent,
|
|
1963
2025
|
runAgent,
|
|
@@ -1965,6 +2027,9 @@ export default {
|
|
|
1965
2027
|
runPromptWithModelForImageGeneration,
|
|
1966
2028
|
runPromptWithModelForVideoGeneration,
|
|
1967
2029
|
throwErrorIfNotSuccessful,
|
|
2030
|
+
updateImageUsageTracker,
|
|
2031
|
+
updateUsageTracker,
|
|
1968
2032
|
updateUsageTrackerForCharacters,
|
|
2033
|
+
updateVideoUsageTracker,
|
|
1969
2034
|
validateModel,
|
|
1970
2035
|
};
|
|
@@ -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":"
|
|
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"}
|