@stackfactor/agent-utils 1.4.3 → 1.7.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/index.d.ts +5 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +7 -1
- package/dist/cjs/langChain.d.ts +24 -4
- package/dist/cjs/langChain.d.ts.map +1 -1
- package/dist/cjs/langChain.js +218 -172
- 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/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/index.d.ts +5 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +4 -2
- package/dist/esm/langChain.d.ts +24 -4
- package/dist/esm/langChain.d.ts.map +1 -1
- package/dist/esm/langChain.js +218 -172
- 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/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 +9 -4
package/dist/cjs/langChain.js
CHANGED
|
@@ -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
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
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
|
|
113
|
-
if (!
|
|
114
|
-
return
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* `
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
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,
|
|
166
|
-
if (!
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
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,
|
|
242
|
-
if (!
|
|
212
|
+
const updateUsageTrackerForCharacters = (tracker, modelName, characterCount, _config) => {
|
|
213
|
+
if (!modelName || !characterCount)
|
|
243
214
|
return;
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
*
|
|
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
|
|
265
|
-
* tenant change resolution on a model whose price varies by it
|
|
266
|
-
* $0.30/$0.50/$0.70 at 720p/1024p/1080p) must pin the
|
|
267
|
-
* describes, or
|
|
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
|
|
270
|
-
* @param modelName - The video model id
|
|
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,
|
|
275
|
-
if (!
|
|
231
|
+
const updateVideoUsageTracker = (tracker, modelName, seconds, _config) => {
|
|
232
|
+
if (!modelName || !seconds)
|
|
276
233
|
return;
|
|
277
|
-
|
|
278
|
-
|
|
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()`
|
|
@@ -1100,6 +1049,65 @@ const parseAndValidateJSONResponse = (rawContent, schema) => {
|
|
|
1100
1049
|
* original base) so that across multiple retries the full failure history
|
|
1101
1050
|
* accumulates and the model can avoid oscillating between previous broken outputs.
|
|
1102
1051
|
*/
|
|
1052
|
+
/**
|
|
1053
|
+
* Whether a response that fails schema validation is retried with the errors
|
|
1054
|
+
* appended (buildValidationRetryMessages), up to MAX_VALIDATION_RETRIES.
|
|
1055
|
+
*
|
|
1056
|
+
* This used to be skipped whenever the model supports native structured
|
|
1057
|
+
* output, on the reasoning that a natively constrained response cannot be
|
|
1058
|
+
* invalid, so a failure had to be a schema bug worth surfacing at once. It
|
|
1059
|
+
* can be invalid, and routinely is: constrained decoding enforces the JSON
|
|
1060
|
+
* *shape*, not the Zod schema. Anthropic's SDK strips `enum` into a
|
|
1061
|
+
* description before sending, Gemini ignores `additionalProperties`, and no
|
|
1062
|
+
* provider runs a refine or a superRefine. Production, 2026-09-23/24: one
|
|
1063
|
+
* agent failed one call in six on extra keys the strict schema refused, and
|
|
1064
|
+
* another was rejected three runs in a row at its last stage on a value off
|
|
1065
|
+
* an enumerated list — every one on a native-schema model, every one reported
|
|
1066
|
+
* straight to the user, and every one restarted from zero by the caller
|
|
1067
|
+
* because the stage that failed was never asked again.
|
|
1068
|
+
*
|
|
1069
|
+
* So the retry now applies whenever a schema is in play. A genuine schema bug
|
|
1070
|
+
* still surfaces: it fails the same way three times and the last error is
|
|
1071
|
+
* thrown as before, at the cost of two extra calls.
|
|
1072
|
+
* @param expectsJsonResponse - Whether the call expects JSON at all
|
|
1073
|
+
* @param schema - The Zod schema the response is validated against, if any
|
|
1074
|
+
* @returns Whether a validation failure is retried
|
|
1075
|
+
*/
|
|
1076
|
+
const canRetryValidation = (expectsJsonResponse, schema) => !!expectsJsonResponse && !!schema;
|
|
1077
|
+
/**
|
|
1078
|
+
* Whether a failure of the JSON stage is one the model can fix if asked again:
|
|
1079
|
+
* a response that failed schema validation, or one that was not JSON at all
|
|
1080
|
+
* (prose, or JSON with a preamble the extractor could not recover). Everything
|
|
1081
|
+
* else — no messages, a provider error, a transport error — is not.
|
|
1082
|
+
* @param error - The failure
|
|
1083
|
+
* @returns Whether a retry with the errors appended makes sense
|
|
1084
|
+
*/
|
|
1085
|
+
const isRetryableJsonFailure = (error) => error?.code === const_js_1.default.HTTP_CODES.UNPROCESSABLE_ENTITY ||
|
|
1086
|
+
/non-JSON response when JSON was expected/.test(String(error?.message ?? ""));
|
|
1087
|
+
/**
|
|
1088
|
+
* The agentic run takes one user prompt rather than a message list, so a retry
|
|
1089
|
+
* carries the failed output and the errors inside that prompt: the model sees
|
|
1090
|
+
* what it produced and what was wrong with it, the same information
|
|
1091
|
+
* buildValidationRetryMessages gives the non-agentic paths. Previous attempts
|
|
1092
|
+
* accumulate so the model does not oscillate between earlier broken outputs.
|
|
1093
|
+
* @param userPrompt - The prompt of the attempt that failed
|
|
1094
|
+
* @param rawContent - What the model answered
|
|
1095
|
+
* @param error - Why it was refused
|
|
1096
|
+
* @returns The user prompt for the next attempt
|
|
1097
|
+
*/
|
|
1098
|
+
const buildAgenticRetryPrompt = (userPrompt, rawContent, error) => {
|
|
1099
|
+
const previous = typeof rawContent === "string" ? rawContent : JSON.stringify(rawContent);
|
|
1100
|
+
return `${userPrompt}
|
|
1101
|
+
|
|
1102
|
+
---
|
|
1103
|
+
Your previous response was rejected:
|
|
1104
|
+
${String(error?.message ?? error)}
|
|
1105
|
+
|
|
1106
|
+
Your previous response was:
|
|
1107
|
+
${previous}
|
|
1108
|
+
|
|
1109
|
+
Return a corrected JSON response that fixes ALL of these issues. Respond with the JSON only.`;
|
|
1110
|
+
};
|
|
1103
1111
|
const buildValidationRetryMessages = (priorMessages, rawContent, validationError) => {
|
|
1104
1112
|
const assistantContent = typeof rawContent === "string" ? rawContent : JSON.stringify(rawContent);
|
|
1105
1113
|
return [
|
|
@@ -1111,6 +1119,58 @@ const buildValidationRetryMessages = (priorMessages, rawContent, validationError
|
|
|
1111
1119
|
},
|
|
1112
1120
|
];
|
|
1113
1121
|
};
|
|
1122
|
+
/**
|
|
1123
|
+
* The agentic path's JSON stage: the agent's final text as validated JSON.
|
|
1124
|
+
* Direct parse when it looks like JSON, the markdown/preamble extractor
|
|
1125
|
+
* otherwise; a schema failure throws UNPROCESSABLE_ENTITY (validateWithSchema),
|
|
1126
|
+
* anything the extractor cannot recover throws the "non-JSON response" error.
|
|
1127
|
+
* Both are what runPromptWithModel's agentic retry loop asks the model to fix.
|
|
1128
|
+
* @param rawContent - The agent's final message text
|
|
1129
|
+
* @param schema - The Zod schema to validate against, if any
|
|
1130
|
+
* @returns The JSON, re-serialised
|
|
1131
|
+
*/
|
|
1132
|
+
const parseAgenticJsonResponse = (rawContent, schema) => {
|
|
1133
|
+
if (typeof rawContent === "string") {
|
|
1134
|
+
const trimmed = rawContent.trim();
|
|
1135
|
+
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
1136
|
+
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
|
1137
|
+
try {
|
|
1138
|
+
const parsed = JSON.parse(trimmed);
|
|
1139
|
+
if (schema) {
|
|
1140
|
+
validateWithSchema(parsed, schema);
|
|
1141
|
+
}
|
|
1142
|
+
return JSON.stringify(parsed);
|
|
1143
|
+
}
|
|
1144
|
+
catch (parseError) {
|
|
1145
|
+
if (parseError?.code === const_js_1.default.HTTP_CODES.UNPROCESSABLE_ENTITY) {
|
|
1146
|
+
throw parseError;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
// Try to extract JSON from markdown or other wrapping
|
|
1151
|
+
const extracted = extractJSONFromResponse(rawContent);
|
|
1152
|
+
if (extracted) {
|
|
1153
|
+
if (schema) {
|
|
1154
|
+
validateWithSchema(extracted, schema);
|
|
1155
|
+
}
|
|
1156
|
+
return JSON.stringify(extracted);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
let preview = "";
|
|
1160
|
+
if (typeof rawContent === "string") {
|
|
1161
|
+
preview = rawContent.substring(0, 100);
|
|
1162
|
+
}
|
|
1163
|
+
else if (typeof rawContent === "object" && rawContent !== null) {
|
|
1164
|
+
preview = JSON.stringify(rawContent).substring(0, 100);
|
|
1165
|
+
}
|
|
1166
|
+
else if (rawContent !== undefined && rawContent !== null) {
|
|
1167
|
+
preview = String(rawContent).substring(0, 100);
|
|
1168
|
+
}
|
|
1169
|
+
else {
|
|
1170
|
+
preview = "[empty response]";
|
|
1171
|
+
}
|
|
1172
|
+
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}...`);
|
|
1173
|
+
};
|
|
1114
1174
|
/**
|
|
1115
1175
|
* Sends a prompt to an LLM and returns the response, with support for streaming,
|
|
1116
1176
|
* agentic execution, progress reporting, JSON extraction, and Zod schema validation.
|
|
@@ -1193,65 +1253,43 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1193
1253
|
// being reconciled from the response afterwards.
|
|
1194
1254
|
const agent = createAgent(agentName, modelName, systemPrompt, [...tools], null, // responseFormat
|
|
1195
1255
|
config, usageTracker);
|
|
1196
|
-
//
|
|
1197
|
-
|
|
1198
|
-
//
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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);
|
|
1256
|
+
// The JSON stage is retried like the non-agentic paths (canRetryValidation):
|
|
1257
|
+
// the agent is run again with its rejected output and the errors appended
|
|
1258
|
+
// to the user prompt, up to MAX_VALIDATION_RETRIES. Until now this path
|
|
1259
|
+
// had no retry at all, and it is the one every tool-using generation takes.
|
|
1260
|
+
let activeUserPrompt = userPrompt;
|
|
1261
|
+
let attempt = 0;
|
|
1262
|
+
for (;;) {
|
|
1263
|
+
// Run the agent with progress callback
|
|
1264
|
+
const response = await runAgent(agent, activeUserPrompt, config, onProgressReport || null, usageTracker);
|
|
1265
|
+
// Extract content from agent response
|
|
1266
|
+
const messages = response?.messages || [];
|
|
1267
|
+
if (messages.length === 0) {
|
|
1268
|
+
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, "Agent returned no messages");
|
|
1242
1269
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1270
|
+
const lastMessage = messages[messages.length - 1];
|
|
1271
|
+
// Flattens the array content blocks that Gemini/Claude agent responses and
|
|
1272
|
+
// any server-tool turn (e.g. web search) return.
|
|
1273
|
+
const rawContent = extractTextContent(lastMessage?.content);
|
|
1274
|
+
// If not expecting JSON, return raw content directly
|
|
1275
|
+
if (!expectsJsonResponse) {
|
|
1276
|
+
return rawContent;
|
|
1245
1277
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1278
|
+
try {
|
|
1279
|
+
return parseAgenticJsonResponse(rawContent, schema);
|
|
1248
1280
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1281
|
+
catch (err) {
|
|
1282
|
+
if (canRetryValidation(expectsJsonResponse, schema) &&
|
|
1283
|
+
isRetryableJsonFailure(err) &&
|
|
1284
|
+
attempt < MAX_VALIDATION_RETRIES) {
|
|
1285
|
+
attempt++;
|
|
1286
|
+
logger_js_1.default.log(null, logger_js_1.default.levels.warn, `Schema validation failed for "${modelName}", retrying (${attempt}/${MAX_VALIDATION_RETRIES}): ${err.message}`);
|
|
1287
|
+
activeUserPrompt = buildAgenticRetryPrompt(activeUserPrompt, rawContent, err);
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
throw err;
|
|
1251
1291
|
}
|
|
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
1292
|
}
|
|
1254
|
-
return rawContent;
|
|
1255
1293
|
}
|
|
1256
1294
|
let messages;
|
|
1257
1295
|
if (typeof prompt === "string") {
|
|
@@ -1296,13 +1334,13 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1296
1334
|
else {
|
|
1297
1335
|
messagesToSend = messages;
|
|
1298
1336
|
}
|
|
1299
|
-
// Retry
|
|
1337
|
+
// Retry on schema validation failure. The retry is silent to the progress
|
|
1300
1338
|
// callback — same "Generating content..." message and the progress curve
|
|
1301
1339
|
// continues from the original startTime so the bar doesn't visibly reset.
|
|
1302
|
-
//
|
|
1303
|
-
//
|
|
1304
|
-
//
|
|
1305
|
-
const canRetryOnValidation = expectsJsonResponse
|
|
1340
|
+
// Each attempt accumulates independently into usageTracker so retry usage
|
|
1341
|
+
// is still visible. Native structured output no longer exempts a model:
|
|
1342
|
+
// see canRetryValidation.
|
|
1343
|
+
const canRetryOnValidation = canRetryValidation(expectsJsonResponse, schema);
|
|
1306
1344
|
const progressReportInterval = 10; // Report every N chunks
|
|
1307
1345
|
const overallStartTime = Date.now();
|
|
1308
1346
|
// Use a time-based asymptotic curve: progress approaches maxPercent but never
|
|
@@ -1454,9 +1492,9 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1454
1492
|
else {
|
|
1455
1493
|
messagesToSend = messages;
|
|
1456
1494
|
}
|
|
1457
|
-
// Retry
|
|
1495
|
+
// Retry on schema validation failure (see streaming branch for rationale).
|
|
1458
1496
|
// Each attempt accumulates independently into usageTracker.
|
|
1459
|
-
const canRetryOnValidation = expectsJsonResponse
|
|
1497
|
+
const canRetryOnValidation = canRetryValidation(expectsJsonResponse, schema);
|
|
1460
1498
|
let activeMessages = messagesToSend;
|
|
1461
1499
|
let attempt = 0;
|
|
1462
1500
|
while (true) {
|
|
@@ -1965,6 +2003,11 @@ const runPromptWithModelForVideoGeneration = async (modelName, config, prompt, o
|
|
|
1965
2003
|
}
|
|
1966
2004
|
};
|
|
1967
2005
|
exports.default = {
|
|
2006
|
+
canRetryValidation,
|
|
2007
|
+
isRetryableJsonFailure,
|
|
2008
|
+
buildAgenticRetryPrompt,
|
|
2009
|
+
parseAgenticJsonResponse,
|
|
2010
|
+
supportsNativeSchema,
|
|
1968
2011
|
checkIfAIProviderConfigured,
|
|
1969
2012
|
createAgent,
|
|
1970
2013
|
runAgent,
|
|
@@ -1972,6 +2015,9 @@ exports.default = {
|
|
|
1972
2015
|
runPromptWithModelForImageGeneration,
|
|
1973
2016
|
runPromptWithModelForVideoGeneration,
|
|
1974
2017
|
throwErrorIfNotSuccessful,
|
|
2018
|
+
updateImageUsageTracker,
|
|
2019
|
+
updateUsageTracker,
|
|
1975
2020
|
updateUsageTrackerForCharacters,
|
|
2021
|
+
updateVideoUsageTracker,
|
|
1976
2022
|
validateModel,
|
|
1977
2023
|
};
|
|
@@ -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"}
|
package/dist/cjs/serve.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as grpc from "@grpc/grpc-js";
|
|
2
|
+
import { type ReportUsage } from "./usage.js";
|
|
2
3
|
/**
|
|
3
4
|
* The agent entry point. Reads `params` / `config` / `onProgress` from globals
|
|
4
5
|
* (populated per-request by {@link serve}) and returns the structured result.
|
|
@@ -22,7 +23,94 @@ export interface ServeOptions {
|
|
|
22
23
|
host?: string;
|
|
23
24
|
/** Override the auth check (e.g. in tests). Defaults to {@link defaultAuthenticate}. */
|
|
24
25
|
authenticate?: Authenticator;
|
|
26
|
+
/** Run-once mode only: override the envelope fetch (e.g. in tests). */
|
|
27
|
+
fetchEnvelope?: (batchJobId: string, token: string) => Promise<any>;
|
|
28
|
+
/** Run-once mode only: override the run lifecycle (e.g. in tests). */
|
|
29
|
+
lifecycle?: RunLifecycle;
|
|
30
|
+
/** Run-once mode only: override process exit (e.g. in tests). */
|
|
31
|
+
exit?: (code: number) => void;
|
|
32
|
+
/** Override how what the work used is reported to the platform (e.g. in tests). */
|
|
33
|
+
reportUsage?: ReportUsage;
|
|
34
|
+
/** Override how often running work reports what it used so far (e.g. in tests). */
|
|
35
|
+
usageReportIntervalMs?: number;
|
|
25
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* How an execution reports its own run to the platform: it claims the run
|
|
39
|
+
* before doing the work, says it is alive while it does, and settles it with
|
|
40
|
+
* the outcome. Every call carries the execution's AGENT token, which the
|
|
41
|
+
* platform minted for this run and which names it — which run is never the
|
|
42
|
+
* execution's to assert.
|
|
43
|
+
*/
|
|
44
|
+
export interface RunLifecycle {
|
|
45
|
+
claim: (token: string) => Promise<{
|
|
46
|
+
claimed: boolean;
|
|
47
|
+
}>;
|
|
48
|
+
heartbeat: (token: string) => Promise<void>;
|
|
49
|
+
settle: (status: number, outcome: unknown, token: string) => Promise<{
|
|
50
|
+
settled: boolean;
|
|
51
|
+
}>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Environment a Cloud Run JOB execution is started with by the platform
|
|
55
|
+
* (stackfactor-batch-jobs, agentJobExecutions.ts): references only — the
|
|
56
|
+
* event record, the integration, the tenant, and a token minted for this
|
|
57
|
+
* execution alone. Their presence is what switches {@link serve} into run-once
|
|
58
|
+
* mode; the payload and the tenant's configuration are fetched, never passed.
|
|
59
|
+
*/
|
|
60
|
+
export declare const EXECUTION_ENV: {
|
|
61
|
+
readonly BATCH_JOB_ID: "SF_EXECUTION_BATCH_JOB_ID";
|
|
62
|
+
readonly INTEGRATION_ID: "SF_EXECUTION_INTEGRATION_ID";
|
|
63
|
+
readonly TENANT_ID: "SF_EXECUTION_TENANT_ID";
|
|
64
|
+
readonly TOKEN: "SF_EXECUTION_TOKEN";
|
|
65
|
+
};
|
|
66
|
+
/** True when this process was started as a job execution rather than a service. */
|
|
67
|
+
export declare const isRunOnceExecution: (env?: NodeJS.ProcessEnv) => boolean;
|
|
68
|
+
/**
|
|
69
|
+
* True for the one kind of run that is ALLOWED to carry no StackFactor token: a
|
|
70
|
+
* delivery to a webhook declared `requireToken: false`. There the platform
|
|
71
|
+
* authenticates nobody and passes the provider's request to the integration as
|
|
72
|
+
* it arrived, so there is no token to validate and no session to build — the
|
|
73
|
+
* handler verifies its provider itself, and brings its own API key (from its
|
|
74
|
+
* configuration) if it has to act on the platform.
|
|
75
|
+
*
|
|
76
|
+
* This does not open the agent up. The Execute RPC is reachable only by callers
|
|
77
|
+
* holding Cloud Run's invoker role — the backend and the batch worker — so a
|
|
78
|
+
* request with no token means the platform CHOSE to send none, and it only ever
|
|
79
|
+
* does that for such a webhook. A token that IS present is always validated,
|
|
80
|
+
* whatever the event, and every other event still requires one.
|
|
81
|
+
*/
|
|
82
|
+
export declare const isAnonymousWebhookRun: (event: any, request: any) => boolean;
|
|
83
|
+
/**
|
|
84
|
+
* True for the runs that are ALLOWED to carry no StackFactor token, because
|
|
85
|
+
* the platform starts them with no identity on purpose:
|
|
86
|
+
* - a delivery to a `requireToken: false` webhook (isAnonymousWebhookRun);
|
|
87
|
+
* - a scheduled run: the clock is nobody.
|
|
88
|
+
* Both run with `session: null`. Since backend-core's decision record §11 the
|
|
89
|
+
* platform starts both with a token of their own (the integration's managed
|
|
90
|
+
* account in the tenant), so against a current platform neither is ever
|
|
91
|
+
* tokenless and this answers false; it stays for a platform that predates
|
|
92
|
+
* that, and can go once none does. The reasoning of isAnonymousWebhookRun holds
|
|
93
|
+
* for both: only the platform can reach Execute or start an execution, so no
|
|
94
|
+
* token means the platform chose to send none. A token that IS present is
|
|
95
|
+
* always validated, and every other event still requires one.
|
|
96
|
+
*/
|
|
97
|
+
export declare const isAnonymousRun: (event: any, request: any) => boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Run-once mode: the same agent, started as one Cloud Run JOB execution for
|
|
100
|
+
* one platform event (`runAs: job`). Fetches the execution's envelope from the
|
|
101
|
+
* backend with the execution's token — exactly what a service call would have
|
|
102
|
+
* received in its Execute request — binds the same {@link RequestContext} the
|
|
103
|
+
* gRPC path binds, runs the handler once, and exits: 0 on success, 1 on any
|
|
104
|
+
* failure so Cloud Run retries the task. SIGTERM (Cloud Run cancelling the
|
|
105
|
+
* execution or its task timeout) aborts the in-flight work.
|
|
106
|
+
*
|
|
107
|
+
* The execution reports its own run: it claims it before doing the work (an
|
|
108
|
+
* execution that loses the claim exits 0 without repeating it), says it is
|
|
109
|
+
* alive while it runs, and settles it with the outcome. The handler is
|
|
110
|
+
* written once and does not know which runtime it is on, nor that any of this
|
|
111
|
+
* is happening.
|
|
112
|
+
*/
|
|
113
|
+
export declare const runOnce: (main: AgentMain, options?: ServeOptions) => Promise<void>;
|
|
26
114
|
/**
|
|
27
115
|
* Start a gRPC server that exposes `main` as the fleet-standard `Agent.Execute`
|
|
28
116
|
* RPC. Per request it binds a {@link RequestContext} (so the agent's globals are
|
package/dist/cjs/serve.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AAWtC,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,YAAY,CAAC;AAwPpB;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,GAAG,KACT,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,uEAAuE;IACvE,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IACpE,sEAAsE;IACtE,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB,iEAAiE;IACjE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,mFAAmF;IACnF,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,mFAAmF;IACnF,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACxD,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,MAAM,EAAE,CACN,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,KACV,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACpC;AAKD;;;;;;GAMG;AACH,eAAO,MAAM,aAAa;;;;;CAKhB,CAAC;AAEX,mFAAmF;AACnF,eAAO,MAAM,kBAAkB,GAAI,MAAK,MAAM,CAAC,UAAwB,KAAG,OACJ,CAAC;AAKvE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO,GAAG,EAAE,SAAS,GAAG,KAAG,OAEhB,CAAC;AAKlD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,GAAG,EAAE,SAAS,GAAG,KAAG,OAGN,CAAC;AA4JrD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,OAAO,GAClB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,OAAO,CAAC,IAAI,CA8Id,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,IAAI,CAAC,MAyOP,CAAC"}
|