@stackfactor/agent-utils 1.0.24 → 1.1.1
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/dist/cjs/index.d.ts +4 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +38 -1
- package/dist/cjs/langChain.d.ts.map +1 -1
- package/dist/cjs/langChain.js +68 -31
- package/dist/cjs/runtimeContext.d.ts +42 -0
- package/dist/cjs/runtimeContext.d.ts.map +1 -0
- package/dist/cjs/runtimeContext.js +33 -0
- package/dist/cjs/serve.d.ts +38 -0
- package/dist/cjs/serve.d.ts.map +1 -0
- package/dist/cjs/serve.js +234 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +4 -0
- package/dist/esm/langChain.d.ts.map +1 -1
- package/dist/esm/langChain.js +68 -31
- package/dist/esm/runtimeContext.d.ts +42 -0
- package/dist/esm/runtimeContext.d.ts.map +1 -0
- package/dist/esm/runtimeContext.js +26 -0
- package/dist/esm/serve.d.ts +38 -0
- package/dist/esm/serve.d.ts.map +1 -0
- package/dist/esm/serve.js +194 -0
- package/package.json +4 -1
package/dist/cjs/index.d.ts
CHANGED
|
@@ -2,8 +2,12 @@ import constants from "./const.js";
|
|
|
2
2
|
import errorHandling from "./errorHandling.js";
|
|
3
3
|
import langChain from "./langChain.js";
|
|
4
4
|
import logger from "./logger.js";
|
|
5
|
+
import { serve } from "./serve.js";
|
|
6
|
+
import * as runtimeContext from "./runtimeContext.js";
|
|
5
7
|
export { constants };
|
|
6
8
|
export { errorHandling };
|
|
7
9
|
export { langChain };
|
|
8
10
|
export { logger };
|
|
11
|
+
export { serve };
|
|
12
|
+
export { runtimeContext };
|
|
9
13
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/cjs/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,aAAa,MAAM,oBAAoB,CAAC;AAC/C,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,MAAM,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,aAAa,MAAM,oBAAoB,CAAC;AAC/C,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,aAAa,EAAE,CAAC;AAEzB,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,MAAM,EAAE,CAAC;AAElB,OAAO,EAAE,KAAK,EAAE,CAAC;AAEjB,OAAO,EAAE,cAAc,EAAE,CAAC"}
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,9 +1,42 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
5
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.logger = exports.langChain = exports.errorHandling = exports.constants = void 0;
|
|
39
|
+
exports.runtimeContext = exports.serve = exports.logger = exports.langChain = exports.errorHandling = exports.constants = void 0;
|
|
7
40
|
const const_js_1 = __importDefault(require("./const.js"));
|
|
8
41
|
exports.constants = const_js_1.default;
|
|
9
42
|
const errorHandling_js_1 = __importDefault(require("./errorHandling.js"));
|
|
@@ -12,3 +45,7 @@ const langChain_js_1 = __importDefault(require("./langChain.js"));
|
|
|
12
45
|
exports.langChain = langChain_js_1.default;
|
|
13
46
|
const logger_js_1 = __importDefault(require("./logger.js"));
|
|
14
47
|
exports.logger = logger_js_1.default;
|
|
48
|
+
const serve_js_1 = require("./serve.js");
|
|
49
|
+
Object.defineProperty(exports, "serve", { enumerable: true, get: function () { return serve_js_1.serve; } });
|
|
50
|
+
const runtimeContext = __importStar(require("./runtimeContext.js"));
|
|
51
|
+
exports.runtimeContext = runtimeContext;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":";0CAiD6C,GAAG,KAAG,IAAI;wBAqgB/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBAyBG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,KAC1B,OAAO,CAAC,GAAG,CAAC;oCAqXF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,KACX,OAAO,CAAC,GAAG,CAAC;sDAosBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,KACX,OAAO,CAAC,GAAG,CAAC;0CAn+B8B,GAAG,KAAG,MAAM;mCAoBxC,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AAwgCT,wBAQE"}
|
package/dist/cjs/langChain.js
CHANGED
|
@@ -13,6 +13,7 @@ const langchain_1 = require("langchain");
|
|
|
13
13
|
const errorHandling_js_1 = __importDefault(require("./errorHandling.js"));
|
|
14
14
|
const logger_js_1 = __importDefault(require("./logger.js"));
|
|
15
15
|
const zod_to_json_schema_1 = require("zod-to-json-schema");
|
|
16
|
+
const runtimeContext_js_1 = require("./runtimeContext.js");
|
|
16
17
|
const JSON_ESCAPE_INSTRUCTION = `
|
|
17
18
|
CRITICAL - Your response must be valid JSON. Escape ALL special characters in string values:
|
|
18
19
|
- Newlines → \\n
|
|
@@ -67,40 +68,68 @@ const recordCost = (cost) => {
|
|
|
67
68
|
}
|
|
68
69
|
};
|
|
69
70
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
71
|
+
* Reads a per-model rate from the flat cost constants the integration defines
|
|
72
|
+
* in its `config.yaml` (exposed on the config object), e.g.
|
|
73
|
+
* `claude-opus-4-7-input-token-costs`. Returns `null` when the constant is
|
|
74
|
+
* absent or not numeric, in which case cost recording becomes a no-op (the
|
|
75
|
+
* call still succeeds — pricing is the integration's configuration concern).
|
|
74
76
|
*/
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
if (!pricing)
|
|
77
|
+
const getModelRate = (modelName, config, kind) => {
|
|
78
|
+
if (!config || !modelName)
|
|
78
79
|
return null;
|
|
79
|
-
|
|
80
|
+
const rate = Number(config[`${modelName}-${kind}-costs`]);
|
|
81
|
+
return Number.isFinite(rate) ? rate : null;
|
|
80
82
|
};
|
|
81
83
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
+
* Accumulates the per-model cost breakdown on `global.quota.byModel` so the
|
|
85
|
+
* agent can report input vs output spend per model to the UI. No-ops when the
|
|
86
|
+
* quota global is absent.
|
|
84
87
|
*/
|
|
85
|
-
const
|
|
88
|
+
const recordModelBreakdown = (modelName, inputCost, outputCost, imageCost) => {
|
|
89
|
+
const quota = globalThis.quota;
|
|
90
|
+
if (!quota || !modelName)
|
|
91
|
+
return;
|
|
92
|
+
if (!quota.byModel || typeof quota.byModel !== "object")
|
|
93
|
+
quota.byModel = {};
|
|
94
|
+
const entry = (quota.byModel[modelName] ||= {
|
|
95
|
+
inputCost: 0,
|
|
96
|
+
outputCost: 0,
|
|
97
|
+
imageCost: 0,
|
|
98
|
+
});
|
|
99
|
+
if (Number.isFinite(inputCost) && inputCost > 0)
|
|
100
|
+
entry.inputCost += inputCost;
|
|
101
|
+
if (Number.isFinite(outputCost) && outputCost > 0)
|
|
102
|
+
entry.outputCost += outputCost;
|
|
103
|
+
if (Number.isFinite(imageCost) && imageCost > 0)
|
|
104
|
+
entry.imageCost += imageCost;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Computes and records the USD cost of a text LLM call from the
|
|
108
|
+
* `<model>-input-token-costs` / `<model>-output-token-costs` constants
|
|
109
|
+
* (dollars per million tokens). Updates both the session total
|
|
110
|
+
* (`quota.usedThisSession` / `quota.remaining`) and the per-model breakdown
|
|
111
|
+
* (`quota.byModel`).
|
|
112
|
+
*/
|
|
113
|
+
const recordTextCost = (modelName, usage, config) => {
|
|
86
114
|
if (!usage)
|
|
87
|
-
return
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
115
|
+
return;
|
|
116
|
+
const inputRate = getModelRate(modelName, config, "input-token") || 0;
|
|
117
|
+
const outputRate = getModelRate(modelName, config, "output-token") || 0;
|
|
118
|
+
const inputCost = ((usage.input_tokens || 0) / 1_000_000) * inputRate;
|
|
119
|
+
const outputCost = ((usage.output_tokens || 0) / 1_000_000) * outputRate;
|
|
120
|
+
recordCost(inputCost + outputCost);
|
|
121
|
+
recordModelBreakdown(modelName, inputCost, outputCost, 0);
|
|
94
122
|
};
|
|
95
123
|
/**
|
|
96
|
-
* Computes USD cost
|
|
97
|
-
*
|
|
124
|
+
* Computes and records the USD cost of an image generation call from the
|
|
125
|
+
* `<model>-image-costs` constant (dollars per generated image). Updates both
|
|
126
|
+
* the session total and the per-model breakdown.
|
|
98
127
|
*/
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
128
|
+
const recordImageCost = (modelName, numImages, config) => {
|
|
129
|
+
const rate = getModelRate(modelName, config, "image") || 0;
|
|
130
|
+
const imageCost = (numImages || 0) * rate;
|
|
131
|
+
recordCost(imageCost);
|
|
132
|
+
recordModelBreakdown(modelName, 0, 0, imageCost);
|
|
104
133
|
};
|
|
105
134
|
/**
|
|
106
135
|
* Extracts a normalized token-usage object from a single LangChain `invoke()`
|
|
@@ -568,7 +597,7 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
|
568
597
|
throw err;
|
|
569
598
|
}
|
|
570
599
|
}
|
|
571
|
-
|
|
600
|
+
recordTextCost(modelName, sumAgentResponseUsage(response), config);
|
|
572
601
|
const endTime = Date.now();
|
|
573
602
|
const duration = endTime - startTime;
|
|
574
603
|
logger_js_1.default.log(null, logger_js_1.default.levels.info, `Agent "${agent.options?.name}" completed in ${Math.round(duration / 1000)} seconds.`);
|
|
@@ -1001,8 +1030,16 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1001
1030
|
streamUsage = { input_tokens: 0, output_tokens: 0 };
|
|
1002
1031
|
try {
|
|
1003
1032
|
assertQuotaAvailable();
|
|
1004
|
-
|
|
1033
|
+
// Honour caller cancellation: passing the signal tears down the
|
|
1034
|
+
// upstream HTTP request so a cancelled call stops billing tokens.
|
|
1035
|
+
const abortSignal = (0, runtimeContext_js_1.getAbortSignal)();
|
|
1036
|
+
const stream = await llm.stream(activeMessages, {
|
|
1037
|
+
signal: abortSignal ?? undefined,
|
|
1038
|
+
});
|
|
1005
1039
|
for await (const chunk of stream) {
|
|
1040
|
+
if (abortSignal?.aborted) {
|
|
1041
|
+
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.BAD_REQUEST, "Request cancelled by caller");
|
|
1042
|
+
}
|
|
1006
1043
|
accumulateChunkUsage(streamUsage, chunk);
|
|
1007
1044
|
const content = chunk?.content || chunk;
|
|
1008
1045
|
if (typeof content === "string") {
|
|
@@ -1035,7 +1072,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1035
1072
|
throw err;
|
|
1036
1073
|
}
|
|
1037
1074
|
}
|
|
1038
|
-
|
|
1075
|
+
recordTextCost(modelName, streamUsage, config);
|
|
1039
1076
|
if (!rawContent) {
|
|
1040
1077
|
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
|
|
1041
1078
|
}
|
|
@@ -1135,7 +1172,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1135
1172
|
throw err;
|
|
1136
1173
|
}
|
|
1137
1174
|
}
|
|
1138
|
-
|
|
1175
|
+
recordTextCost(modelName, extractUsageFromInvoke(response), config);
|
|
1139
1176
|
const rawContent = response?.content || response;
|
|
1140
1177
|
// If not expecting JSON, return raw content directly
|
|
1141
1178
|
if (!expectsJsonResponse) {
|
|
@@ -1243,7 +1280,7 @@ const generateImageWithOpenAI = async (modelName, config, prompt, options) => {
|
|
|
1243
1280
|
}
|
|
1244
1281
|
assertQuotaAvailable();
|
|
1245
1282
|
const response = await openai.images.generate(requestParams);
|
|
1246
|
-
|
|
1283
|
+
recordImageCost(modelName, response.data?.length || n, config);
|
|
1247
1284
|
// Format response based on number of images
|
|
1248
1285
|
if (n === 1) {
|
|
1249
1286
|
const imageData = response.data[0];
|
|
@@ -1360,7 +1397,7 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
|
|
|
1360
1397
|
if (images.length === 0) {
|
|
1361
1398
|
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `No images were generated by ${modelName}`);
|
|
1362
1399
|
}
|
|
1363
|
-
|
|
1400
|
+
recordImageCost(modelName, images.length, config);
|
|
1364
1401
|
if (numberOfImages === 1 || images.length === 1) {
|
|
1365
1402
|
return images[0];
|
|
1366
1403
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-request execution context for an agent invocation.
|
|
3
|
+
*
|
|
4
|
+
* Agents read `config`, `params`, `onProgress`, `request`, and `event` as
|
|
5
|
+
* globals (the historical VM-host contract). When agents run as a long-lived
|
|
6
|
+
* gRPC server (see {@link "./serve"}), a single process handles many concurrent
|
|
7
|
+
* requests, so a shared mutable global would let requests clobber each other.
|
|
8
|
+
* This module stores the context per async call-chain via {@link AsyncLocalStorage}
|
|
9
|
+
* and exposes the same names as global getters, keeping the agent programming
|
|
10
|
+
* model intact while being concurrency-safe.
|
|
11
|
+
*/
|
|
12
|
+
export interface RequestContext {
|
|
13
|
+
config: any;
|
|
14
|
+
params: any;
|
|
15
|
+
request: any;
|
|
16
|
+
event: any;
|
|
17
|
+
onProgress: ((update: {
|
|
18
|
+
progress: number;
|
|
19
|
+
message: string;
|
|
20
|
+
}) => void) | null;
|
|
21
|
+
/** Aborted when the caller cancels (e.g. the frontend socket dropped). */
|
|
22
|
+
signal: AbortSignal | null;
|
|
23
|
+
/** Identity + tenant resolved from the request's auth token (see serve). */
|
|
24
|
+
session?: {
|
|
25
|
+
user: any;
|
|
26
|
+
tenant: any;
|
|
27
|
+
} | null;
|
|
28
|
+
}
|
|
29
|
+
/** Run `fn` with `ctx` bound as the active request context. */
|
|
30
|
+
export declare const runWithContext: <T>(ctx: RequestContext, fn: () => Promise<T>) => Promise<T>;
|
|
31
|
+
/** The active request context, or `undefined` outside a request. */
|
|
32
|
+
export declare const getContext: () => RequestContext | undefined;
|
|
33
|
+
/** The active request's abort signal, or `null`. */
|
|
34
|
+
export declare const getAbortSignal: () => AbortSignal | null;
|
|
35
|
+
/**
|
|
36
|
+
* Install global getters (`config`, `params`, `onProgress`, `request`, `event`)
|
|
37
|
+
* that resolve to the active {@link RequestContext}. Idempotent; call once at
|
|
38
|
+
* startup. After this, agent code referencing those globals transparently reads
|
|
39
|
+
* the current request's values even under concurrency.
|
|
40
|
+
*/
|
|
41
|
+
export declare const installGlobals: () => void;
|
|
42
|
+
//# sourceMappingURL=runtimeContext.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.installGlobals = exports.getAbortSignal = exports.getContext = exports.runWithContext = void 0;
|
|
4
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
5
|
+
const storage = new node_async_hooks_1.AsyncLocalStorage();
|
|
6
|
+
/** Run `fn` with `ctx` bound as the active request context. */
|
|
7
|
+
const runWithContext = (ctx, fn) => storage.run(ctx, fn);
|
|
8
|
+
exports.runWithContext = runWithContext;
|
|
9
|
+
/** The active request context, or `undefined` outside a request. */
|
|
10
|
+
const getContext = () => storage.getStore();
|
|
11
|
+
exports.getContext = getContext;
|
|
12
|
+
/** The active request's abort signal, or `null`. */
|
|
13
|
+
const getAbortSignal = () => storage.getStore()?.signal ?? null;
|
|
14
|
+
exports.getAbortSignal = getAbortSignal;
|
|
15
|
+
let installed = false;
|
|
16
|
+
/**
|
|
17
|
+
* Install global getters (`config`, `params`, `onProgress`, `request`, `event`)
|
|
18
|
+
* that resolve to the active {@link RequestContext}. Idempotent; call once at
|
|
19
|
+
* startup. After this, agent code referencing those globals transparently reads
|
|
20
|
+
* the current request's values even under concurrency.
|
|
21
|
+
*/
|
|
22
|
+
const installGlobals = () => {
|
|
23
|
+
if (installed)
|
|
24
|
+
return;
|
|
25
|
+
installed = true;
|
|
26
|
+
const define = (name, get) => Object.defineProperty(globalThis, name, { get, configurable: true });
|
|
27
|
+
define("config", () => storage.getStore()?.config);
|
|
28
|
+
define("params", () => storage.getStore()?.params);
|
|
29
|
+
define("request", () => storage.getStore()?.request);
|
|
30
|
+
define("event", () => storage.getStore()?.event);
|
|
31
|
+
define("onProgress", () => storage.getStore()?.onProgress);
|
|
32
|
+
};
|
|
33
|
+
exports.installGlobals = installGlobals;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as grpc from "@grpc/grpc-js";
|
|
2
|
+
/**
|
|
3
|
+
* The agent entry point. Reads `params` / `config` / `onProgress` from globals
|
|
4
|
+
* (populated per-request by {@link serve}) and returns the structured result.
|
|
5
|
+
* Receives the request's {@link AbortSignal} for cooperative cancellation; most
|
|
6
|
+
* agents can ignore it since the LLM layer honours it automatically.
|
|
7
|
+
*/
|
|
8
|
+
export type AgentMain = (signal?: AbortSignal) => Promise<any>;
|
|
9
|
+
/**
|
|
10
|
+
* Resolves the caller's identity + tenant from the request, or throws to refuse
|
|
11
|
+
* the call. The default validates `request.authToken` against the StackFactor
|
|
12
|
+
* API via `client-api`'s `session.getSession`.
|
|
13
|
+
*/
|
|
14
|
+
export type Authenticator = (request: any) => Promise<{
|
|
15
|
+
user: any;
|
|
16
|
+
tenant: any;
|
|
17
|
+
}>;
|
|
18
|
+
export interface ServeOptions {
|
|
19
|
+
/** Port to bind. Defaults to `process.env.PORT` then `8080` (Cloud Run). */
|
|
20
|
+
port?: number;
|
|
21
|
+
/** Host to bind. Defaults to `0.0.0.0`. */
|
|
22
|
+
host?: string;
|
|
23
|
+
/** Override the auth check (e.g. in tests). Defaults to {@link defaultAuthenticate}. */
|
|
24
|
+
authenticate?: Authenticator;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Start a gRPC server that exposes `main` as the fleet-standard `Agent.Execute`
|
|
28
|
+
* RPC. Per request it binds a {@link RequestContext} (so the agent's globals are
|
|
29
|
+
* concurrency-safe), streams each `onProgress` event as a `Progress` frame, and
|
|
30
|
+
* sends a final `Result` or `Error`. Caller cancellation (e.g. the frontend
|
|
31
|
+
* socket dropping) aborts the in-flight LLM call to stop billing.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* const { serve } = require("@stackfactor/agent-utils");
|
|
35
|
+
* serve(require("./src/main"));
|
|
36
|
+
*/
|
|
37
|
+
export declare const serve: (main: AgentMain, options?: ServeOptions) => grpc.Server;
|
|
38
|
+
//# sourceMappingURL=serve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AAStC;;;;;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,CAAC,OAAO,EAAE,GAAG,KAAK,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAElF,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;CAC9B;AAqGD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAAI,MAAM,SAAS,EAAE,UAAS,YAAiB,KAAG,IAAI,CAAC,MAsGxE,CAAC"}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.serve = void 0;
|
|
40
|
+
const grpc = __importStar(require("@grpc/grpc-js"));
|
|
41
|
+
const protoLoader = __importStar(require("@grpc/proto-loader"));
|
|
42
|
+
const client_api_1 = require("@stackfactor/client-api");
|
|
43
|
+
const node_fs_1 = require("node:fs");
|
|
44
|
+
const node_path_1 = require("node:path");
|
|
45
|
+
const node_os_1 = require("node:os");
|
|
46
|
+
const logger_js_1 = __importDefault(require("./logger.js"));
|
|
47
|
+
const runtimeContext_js_1 = require("./runtimeContext.js");
|
|
48
|
+
/** Marks an auth failure so the handler can map it to gRPC UNAUTHENTICATED. */
|
|
49
|
+
class UnauthenticatedError extends Error {
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Default authenticator: require a token on the request and resolve it to a
|
|
53
|
+
* session via the StackFactor API. Requires `BACKEND_URL` (or `REACT_APP_NODE_ENV`)
|
|
54
|
+
* to point client-api at the right backend. Throws {@link UnauthenticatedError}
|
|
55
|
+
* when the token is missing or rejected.
|
|
56
|
+
*/
|
|
57
|
+
const defaultAuthenticate = async (request) => {
|
|
58
|
+
const token = request?.authToken ?? request?.authorization;
|
|
59
|
+
if (!token) {
|
|
60
|
+
throw new UnauthenticatedError("Missing auth token");
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
return await client_api_1.session.getSession(token);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw new UnauthenticatedError(`Invalid auth token: ${error?.message ?? error}`);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* gRPC contract shared by the entire agent fleet. `data`/`config`/`request` are
|
|
71
|
+
* carried as JSON strings so arbitrary payloads need no proto schema churn.
|
|
72
|
+
* `Execute` is server-streaming: zero or more `Progress` frames (driven by the
|
|
73
|
+
* agent's `onProgress`) followed by exactly one `Result` or `Error`.
|
|
74
|
+
*/
|
|
75
|
+
const PROTO = `
|
|
76
|
+
syntax = "proto3";
|
|
77
|
+
package stackfactor.agent.v1;
|
|
78
|
+
|
|
79
|
+
service Agent {
|
|
80
|
+
rpc Execute(ExecuteRequest) returns (stream Update);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
message ExecuteRequest {
|
|
84
|
+
string content_type = 1;
|
|
85
|
+
string data_json = 2;
|
|
86
|
+
string config_json = 3;
|
|
87
|
+
string request_json = 4;
|
|
88
|
+
int32 event = 5;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
message Update {
|
|
92
|
+
oneof payload {
|
|
93
|
+
Progress progress = 1;
|
|
94
|
+
Result result = 2;
|
|
95
|
+
ErrorInfo error = 3;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
message Progress { int32 progress = 1; string message = 2; }
|
|
100
|
+
message Result { string result_json = 1; }
|
|
101
|
+
message ErrorInfo { int32 code = 1; string message = 2; }
|
|
102
|
+
`;
|
|
103
|
+
const loadAgentService = () => {
|
|
104
|
+
// proto-loader reads from a file; write the embedded schema to a temp path so
|
|
105
|
+
// the package stays self-contained across the cjs/esm dual build.
|
|
106
|
+
const file = (0, node_path_1.join)((0, node_os_1.tmpdir)(), "stackfactor-agent.v1.proto");
|
|
107
|
+
(0, node_fs_1.writeFileSync)(file, PROTO);
|
|
108
|
+
const packageDefinition = protoLoader.loadSync(file, {
|
|
109
|
+
keepCase: true,
|
|
110
|
+
longs: String,
|
|
111
|
+
enums: String,
|
|
112
|
+
defaults: true,
|
|
113
|
+
oneofs: true,
|
|
114
|
+
});
|
|
115
|
+
const loaded = grpc.loadPackageDefinition(packageDefinition);
|
|
116
|
+
return loaded.stackfactor.agent.v1.Agent.service;
|
|
117
|
+
};
|
|
118
|
+
const safeParse = (value, fallback) => {
|
|
119
|
+
if (!value)
|
|
120
|
+
return fallback;
|
|
121
|
+
try {
|
|
122
|
+
return JSON.parse(value);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return fallback;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Agents that fail throw `new Error(JSON.stringify({ code, message }))`. Unwrap
|
|
130
|
+
* that back into a structured gRPC error frame; fall back to a 500 otherwise.
|
|
131
|
+
*/
|
|
132
|
+
const toErrorFrame = (error) => {
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(error?.message);
|
|
135
|
+
if (parsed && typeof parsed.code === "number") {
|
|
136
|
+
return { code: parsed.code, message: String(parsed.message ?? "") };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// not a structured error
|
|
141
|
+
}
|
|
142
|
+
return { code: 500, message: String(error?.message ?? error) };
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Start a gRPC server that exposes `main` as the fleet-standard `Agent.Execute`
|
|
146
|
+
* RPC. Per request it binds a {@link RequestContext} (so the agent's globals are
|
|
147
|
+
* concurrency-safe), streams each `onProgress` event as a `Progress` frame, and
|
|
148
|
+
* sends a final `Result` or `Error`. Caller cancellation (e.g. the frontend
|
|
149
|
+
* socket dropping) aborts the in-flight LLM call to stop billing.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* const { serve } = require("@stackfactor/agent-utils");
|
|
153
|
+
* serve(require("./src/main"));
|
|
154
|
+
*/
|
|
155
|
+
const serve = (main, options = {}) => {
|
|
156
|
+
(0, runtimeContext_js_1.installGlobals)();
|
|
157
|
+
const port = options.port ?? (Number(process.env.PORT) || 8080);
|
|
158
|
+
const host = options.host ?? "0.0.0.0";
|
|
159
|
+
const authenticate = options.authenticate ?? defaultAuthenticate;
|
|
160
|
+
const execute = (call) => {
|
|
161
|
+
const abort = new AbortController();
|
|
162
|
+
call.on("cancelled", () => abort.abort());
|
|
163
|
+
call.on("error", () => abort.abort());
|
|
164
|
+
const req = call.request ?? {};
|
|
165
|
+
const request = safeParse(req.request_json, {});
|
|
166
|
+
const onProgress = (update) => {
|
|
167
|
+
if (abort.signal.aborted)
|
|
168
|
+
return;
|
|
169
|
+
call.write({
|
|
170
|
+
progress: {
|
|
171
|
+
progress: Math.round(update?.progress ?? 0),
|
|
172
|
+
message: update?.message ?? "",
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
const handle = async () => {
|
|
177
|
+
// Authenticate first — refuse the call when the token is missing/invalid.
|
|
178
|
+
let session;
|
|
179
|
+
try {
|
|
180
|
+
session = await authenticate(request);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
const message = error?.message ?? "Unauthenticated";
|
|
184
|
+
logger_js_1.default.log(null, logger_js_1.default.levels.warn, `agent Execute refused: ${message}`);
|
|
185
|
+
call.emit("error", {
|
|
186
|
+
code: grpc.status.UNAUTHENTICATED,
|
|
187
|
+
details: message,
|
|
188
|
+
});
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const context = {
|
|
192
|
+
config: safeParse(req.config_json, {}),
|
|
193
|
+
params: {
|
|
194
|
+
contentType: req.content_type,
|
|
195
|
+
data: safeParse(req.data_json, {}),
|
|
196
|
+
},
|
|
197
|
+
request: { ...request, session }, // expose identity/tenant to the agent
|
|
198
|
+
event: req.event ?? 0,
|
|
199
|
+
onProgress,
|
|
200
|
+
signal: abort.signal,
|
|
201
|
+
session,
|
|
202
|
+
};
|
|
203
|
+
try {
|
|
204
|
+
const result = await (0, runtimeContext_js_1.runWithContext)(context, () => main(abort.signal));
|
|
205
|
+
if (!abort.signal.aborted) {
|
|
206
|
+
call.write({ result: { result_json: JSON.stringify(result ?? null) } });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (!abort.signal.aborted) {
|
|
211
|
+
logger_js_1.default.log(null, logger_js_1.default.levels.error, `agent Execute failed: ${error?.message ?? error}`);
|
|
212
|
+
call.write({ error: toErrorFrame(error) });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
call.end();
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
void handle();
|
|
220
|
+
};
|
|
221
|
+
const server = new grpc.Server();
|
|
222
|
+
server.addService(loadAgentService(), { Execute: execute });
|
|
223
|
+
// Insecure: Cloud Run terminates TLS at the edge and forwards h2c to the
|
|
224
|
+
// container (requires the service to be deployed with --use-http2).
|
|
225
|
+
server.bindAsync(`${host}:${port}`, grpc.ServerCredentials.createInsecure(), (err, boundPort) => {
|
|
226
|
+
if (err) {
|
|
227
|
+
logger_js_1.default.log(null, logger_js_1.default.levels.error, `agent gRPC server failed to bind ${host}:${port}: ${err.message}`);
|
|
228
|
+
throw err;
|
|
229
|
+
}
|
|
230
|
+
logger_js_1.default.log(null, logger_js_1.default.levels.info, `agent gRPC server listening on ${host}:${boundPort}`);
|
|
231
|
+
});
|
|
232
|
+
return server;
|
|
233
|
+
};
|
|
234
|
+
exports.serve = serve;
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -2,8 +2,12 @@ import constants from "./const.js";
|
|
|
2
2
|
import errorHandling from "./errorHandling.js";
|
|
3
3
|
import langChain from "./langChain.js";
|
|
4
4
|
import logger from "./logger.js";
|
|
5
|
+
import { serve } from "./serve.js";
|
|
6
|
+
import * as runtimeContext from "./runtimeContext.js";
|
|
5
7
|
export { constants };
|
|
6
8
|
export { errorHandling };
|
|
7
9
|
export { langChain };
|
|
8
10
|
export { logger };
|
|
11
|
+
export { serve };
|
|
12
|
+
export { runtimeContext };
|
|
9
13
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/esm/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,aAAa,MAAM,oBAAoB,CAAC;AAC/C,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,MAAM,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,aAAa,MAAM,oBAAoB,CAAC;AAC/C,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,aAAa,EAAE,CAAC;AAEzB,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,MAAM,EAAE,CAAC;AAElB,OAAO,EAAE,KAAK,EAAE,CAAC;AAEjB,OAAO,EAAE,cAAc,EAAE,CAAC"}
|
package/dist/esm/index.js
CHANGED
|
@@ -2,7 +2,11 @@ import constants from "./const.js";
|
|
|
2
2
|
import errorHandling from "./errorHandling.js";
|
|
3
3
|
import langChain from "./langChain.js";
|
|
4
4
|
import logger from "./logger.js";
|
|
5
|
+
import { serve } from "./serve.js";
|
|
6
|
+
import * as runtimeContext from "./runtimeContext.js";
|
|
5
7
|
export { constants };
|
|
6
8
|
export { errorHandling };
|
|
7
9
|
export { langChain };
|
|
8
10
|
export { logger };
|
|
11
|
+
export { serve };
|
|
12
|
+
export { runtimeContext };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":";0CAiD6C,GAAG,KAAG,IAAI;wBAqgB/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBAyBG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,KAC1B,OAAO,CAAC,GAAG,CAAC;oCAqXF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,KACX,OAAO,CAAC,GAAG,CAAC;sDAosBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,KACX,OAAO,CAAC,GAAG,CAAC;0CAn+B8B,GAAG,KAAG,MAAM;mCAoBxC,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AAwgCT,wBAQE"}
|
package/dist/esm/langChain.js
CHANGED
|
@@ -8,6 +8,7 @@ import { createAgent as createLangChainAgent } from "langchain";
|
|
|
8
8
|
import errorHandlingHelper from "./errorHandling.js";
|
|
9
9
|
import logger from "./logger.js";
|
|
10
10
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
11
|
+
import { getAbortSignal } from "./runtimeContext.js";
|
|
11
12
|
const JSON_ESCAPE_INSTRUCTION = `
|
|
12
13
|
CRITICAL - Your response must be valid JSON. Escape ALL special characters in string values:
|
|
13
14
|
- Newlines → \\n
|
|
@@ -62,40 +63,68 @@ const recordCost = (cost) => {
|
|
|
62
63
|
}
|
|
63
64
|
};
|
|
64
65
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
66
|
+
* Reads a per-model rate from the flat cost constants the integration defines
|
|
67
|
+
* in its `config.yaml` (exposed on the config object), e.g.
|
|
68
|
+
* `claude-opus-4-7-input-token-costs`. Returns `null` when the constant is
|
|
69
|
+
* absent or not numeric, in which case cost recording becomes a no-op (the
|
|
70
|
+
* call still succeeds — pricing is the integration's configuration concern).
|
|
69
71
|
*/
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
if (!pricing)
|
|
72
|
+
const getModelRate = (modelName, config, kind) => {
|
|
73
|
+
if (!config || !modelName)
|
|
73
74
|
return null;
|
|
74
|
-
|
|
75
|
+
const rate = Number(config[`${modelName}-${kind}-costs`]);
|
|
76
|
+
return Number.isFinite(rate) ? rate : null;
|
|
75
77
|
};
|
|
76
78
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
+
* Accumulates the per-model cost breakdown on `global.quota.byModel` so the
|
|
80
|
+
* agent can report input vs output spend per model to the UI. No-ops when the
|
|
81
|
+
* quota global is absent.
|
|
79
82
|
*/
|
|
80
|
-
const
|
|
83
|
+
const recordModelBreakdown = (modelName, inputCost, outputCost, imageCost) => {
|
|
84
|
+
const quota = globalThis.quota;
|
|
85
|
+
if (!quota || !modelName)
|
|
86
|
+
return;
|
|
87
|
+
if (!quota.byModel || typeof quota.byModel !== "object")
|
|
88
|
+
quota.byModel = {};
|
|
89
|
+
const entry = (quota.byModel[modelName] ||= {
|
|
90
|
+
inputCost: 0,
|
|
91
|
+
outputCost: 0,
|
|
92
|
+
imageCost: 0,
|
|
93
|
+
});
|
|
94
|
+
if (Number.isFinite(inputCost) && inputCost > 0)
|
|
95
|
+
entry.inputCost += inputCost;
|
|
96
|
+
if (Number.isFinite(outputCost) && outputCost > 0)
|
|
97
|
+
entry.outputCost += outputCost;
|
|
98
|
+
if (Number.isFinite(imageCost) && imageCost > 0)
|
|
99
|
+
entry.imageCost += imageCost;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Computes and records the USD cost of a text LLM call from the
|
|
103
|
+
* `<model>-input-token-costs` / `<model>-output-token-costs` constants
|
|
104
|
+
* (dollars per million tokens). Updates both the session total
|
|
105
|
+
* (`quota.usedThisSession` / `quota.remaining`) and the per-model breakdown
|
|
106
|
+
* (`quota.byModel`).
|
|
107
|
+
*/
|
|
108
|
+
const recordTextCost = (modelName, usage, config) => {
|
|
81
109
|
if (!usage)
|
|
82
|
-
return
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
110
|
+
return;
|
|
111
|
+
const inputRate = getModelRate(modelName, config, "input-token") || 0;
|
|
112
|
+
const outputRate = getModelRate(modelName, config, "output-token") || 0;
|
|
113
|
+
const inputCost = ((usage.input_tokens || 0) / 1_000_000) * inputRate;
|
|
114
|
+
const outputCost = ((usage.output_tokens || 0) / 1_000_000) * outputRate;
|
|
115
|
+
recordCost(inputCost + outputCost);
|
|
116
|
+
recordModelBreakdown(modelName, inputCost, outputCost, 0);
|
|
89
117
|
};
|
|
90
118
|
/**
|
|
91
|
-
* Computes USD cost
|
|
92
|
-
*
|
|
119
|
+
* Computes and records the USD cost of an image generation call from the
|
|
120
|
+
* `<model>-image-costs` constant (dollars per generated image). Updates both
|
|
121
|
+
* the session total and the per-model breakdown.
|
|
93
122
|
*/
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
123
|
+
const recordImageCost = (modelName, numImages, config) => {
|
|
124
|
+
const rate = getModelRate(modelName, config, "image") || 0;
|
|
125
|
+
const imageCost = (numImages || 0) * rate;
|
|
126
|
+
recordCost(imageCost);
|
|
127
|
+
recordModelBreakdown(modelName, 0, 0, imageCost);
|
|
99
128
|
};
|
|
100
129
|
/**
|
|
101
130
|
* Extracts a normalized token-usage object from a single LangChain `invoke()`
|
|
@@ -563,7 +592,7 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
|
563
592
|
throw err;
|
|
564
593
|
}
|
|
565
594
|
}
|
|
566
|
-
|
|
595
|
+
recordTextCost(modelName, sumAgentResponseUsage(response), config);
|
|
567
596
|
const endTime = Date.now();
|
|
568
597
|
const duration = endTime - startTime;
|
|
569
598
|
logger.log(null, logger.levels.info, `Agent "${agent.options?.name}" completed in ${Math.round(duration / 1000)} seconds.`);
|
|
@@ -996,8 +1025,16 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
996
1025
|
streamUsage = { input_tokens: 0, output_tokens: 0 };
|
|
997
1026
|
try {
|
|
998
1027
|
assertQuotaAvailable();
|
|
999
|
-
|
|
1028
|
+
// Honour caller cancellation: passing the signal tears down the
|
|
1029
|
+
// upstream HTTP request so a cancelled call stops billing tokens.
|
|
1030
|
+
const abortSignal = getAbortSignal();
|
|
1031
|
+
const stream = await llm.stream(activeMessages, {
|
|
1032
|
+
signal: abortSignal ?? undefined,
|
|
1033
|
+
});
|
|
1000
1034
|
for await (const chunk of stream) {
|
|
1035
|
+
if (abortSignal?.aborted) {
|
|
1036
|
+
throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, "Request cancelled by caller");
|
|
1037
|
+
}
|
|
1001
1038
|
accumulateChunkUsage(streamUsage, chunk);
|
|
1002
1039
|
const content = chunk?.content || chunk;
|
|
1003
1040
|
if (typeof content === "string") {
|
|
@@ -1030,7 +1067,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1030
1067
|
throw err;
|
|
1031
1068
|
}
|
|
1032
1069
|
}
|
|
1033
|
-
|
|
1070
|
+
recordTextCost(modelName, streamUsage, config);
|
|
1034
1071
|
if (!rawContent) {
|
|
1035
1072
|
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
|
|
1036
1073
|
}
|
|
@@ -1130,7 +1167,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1130
1167
|
throw err;
|
|
1131
1168
|
}
|
|
1132
1169
|
}
|
|
1133
|
-
|
|
1170
|
+
recordTextCost(modelName, extractUsageFromInvoke(response), config);
|
|
1134
1171
|
const rawContent = response?.content || response;
|
|
1135
1172
|
// If not expecting JSON, return raw content directly
|
|
1136
1173
|
if (!expectsJsonResponse) {
|
|
@@ -1238,7 +1275,7 @@ const generateImageWithOpenAI = async (modelName, config, prompt, options) => {
|
|
|
1238
1275
|
}
|
|
1239
1276
|
assertQuotaAvailable();
|
|
1240
1277
|
const response = await openai.images.generate(requestParams);
|
|
1241
|
-
|
|
1278
|
+
recordImageCost(modelName, response.data?.length || n, config);
|
|
1242
1279
|
// Format response based on number of images
|
|
1243
1280
|
if (n === 1) {
|
|
1244
1281
|
const imageData = response.data[0];
|
|
@@ -1355,7 +1392,7 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
|
|
|
1355
1392
|
if (images.length === 0) {
|
|
1356
1393
|
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `No images were generated by ${modelName}`);
|
|
1357
1394
|
}
|
|
1358
|
-
|
|
1395
|
+
recordImageCost(modelName, images.length, config);
|
|
1359
1396
|
if (numberOfImages === 1 || images.length === 1) {
|
|
1360
1397
|
return images[0];
|
|
1361
1398
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-request execution context for an agent invocation.
|
|
3
|
+
*
|
|
4
|
+
* Agents read `config`, `params`, `onProgress`, `request`, and `event` as
|
|
5
|
+
* globals (the historical VM-host contract). When agents run as a long-lived
|
|
6
|
+
* gRPC server (see {@link "./serve"}), a single process handles many concurrent
|
|
7
|
+
* requests, so a shared mutable global would let requests clobber each other.
|
|
8
|
+
* This module stores the context per async call-chain via {@link AsyncLocalStorage}
|
|
9
|
+
* and exposes the same names as global getters, keeping the agent programming
|
|
10
|
+
* model intact while being concurrency-safe.
|
|
11
|
+
*/
|
|
12
|
+
export interface RequestContext {
|
|
13
|
+
config: any;
|
|
14
|
+
params: any;
|
|
15
|
+
request: any;
|
|
16
|
+
event: any;
|
|
17
|
+
onProgress: ((update: {
|
|
18
|
+
progress: number;
|
|
19
|
+
message: string;
|
|
20
|
+
}) => void) | null;
|
|
21
|
+
/** Aborted when the caller cancels (e.g. the frontend socket dropped). */
|
|
22
|
+
signal: AbortSignal | null;
|
|
23
|
+
/** Identity + tenant resolved from the request's auth token (see serve). */
|
|
24
|
+
session?: {
|
|
25
|
+
user: any;
|
|
26
|
+
tenant: any;
|
|
27
|
+
} | null;
|
|
28
|
+
}
|
|
29
|
+
/** Run `fn` with `ctx` bound as the active request context. */
|
|
30
|
+
export declare const runWithContext: <T>(ctx: RequestContext, fn: () => Promise<T>) => Promise<T>;
|
|
31
|
+
/** The active request context, or `undefined` outside a request. */
|
|
32
|
+
export declare const getContext: () => RequestContext | undefined;
|
|
33
|
+
/** The active request's abort signal, or `null`. */
|
|
34
|
+
export declare const getAbortSignal: () => AbortSignal | null;
|
|
35
|
+
/**
|
|
36
|
+
* Install global getters (`config`, `params`, `onProgress`, `request`, `event`)
|
|
37
|
+
* that resolve to the active {@link RequestContext}. Idempotent; call once at
|
|
38
|
+
* startup. After this, agent code referencing those globals transparently reads
|
|
39
|
+
* the current request's values even under concurrency.
|
|
40
|
+
*/
|
|
41
|
+
export declare const installGlobals: () => void;
|
|
42
|
+
//# sourceMappingURL=runtimeContext.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
const storage = new AsyncLocalStorage();
|
|
3
|
+
/** Run `fn` with `ctx` bound as the active request context. */
|
|
4
|
+
export const runWithContext = (ctx, fn) => storage.run(ctx, fn);
|
|
5
|
+
/** The active request context, or `undefined` outside a request. */
|
|
6
|
+
export const getContext = () => storage.getStore();
|
|
7
|
+
/** The active request's abort signal, or `null`. */
|
|
8
|
+
export const getAbortSignal = () => storage.getStore()?.signal ?? null;
|
|
9
|
+
let installed = false;
|
|
10
|
+
/**
|
|
11
|
+
* Install global getters (`config`, `params`, `onProgress`, `request`, `event`)
|
|
12
|
+
* that resolve to the active {@link RequestContext}. Idempotent; call once at
|
|
13
|
+
* startup. After this, agent code referencing those globals transparently reads
|
|
14
|
+
* the current request's values even under concurrency.
|
|
15
|
+
*/
|
|
16
|
+
export const installGlobals = () => {
|
|
17
|
+
if (installed)
|
|
18
|
+
return;
|
|
19
|
+
installed = true;
|
|
20
|
+
const define = (name, get) => Object.defineProperty(globalThis, name, { get, configurable: true });
|
|
21
|
+
define("config", () => storage.getStore()?.config);
|
|
22
|
+
define("params", () => storage.getStore()?.params);
|
|
23
|
+
define("request", () => storage.getStore()?.request);
|
|
24
|
+
define("event", () => storage.getStore()?.event);
|
|
25
|
+
define("onProgress", () => storage.getStore()?.onProgress);
|
|
26
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as grpc from "@grpc/grpc-js";
|
|
2
|
+
/**
|
|
3
|
+
* The agent entry point. Reads `params` / `config` / `onProgress` from globals
|
|
4
|
+
* (populated per-request by {@link serve}) and returns the structured result.
|
|
5
|
+
* Receives the request's {@link AbortSignal} for cooperative cancellation; most
|
|
6
|
+
* agents can ignore it since the LLM layer honours it automatically.
|
|
7
|
+
*/
|
|
8
|
+
export type AgentMain = (signal?: AbortSignal) => Promise<any>;
|
|
9
|
+
/**
|
|
10
|
+
* Resolves the caller's identity + tenant from the request, or throws to refuse
|
|
11
|
+
* the call. The default validates `request.authToken` against the StackFactor
|
|
12
|
+
* API via `client-api`'s `session.getSession`.
|
|
13
|
+
*/
|
|
14
|
+
export type Authenticator = (request: any) => Promise<{
|
|
15
|
+
user: any;
|
|
16
|
+
tenant: any;
|
|
17
|
+
}>;
|
|
18
|
+
export interface ServeOptions {
|
|
19
|
+
/** Port to bind. Defaults to `process.env.PORT` then `8080` (Cloud Run). */
|
|
20
|
+
port?: number;
|
|
21
|
+
/** Host to bind. Defaults to `0.0.0.0`. */
|
|
22
|
+
host?: string;
|
|
23
|
+
/** Override the auth check (e.g. in tests). Defaults to {@link defaultAuthenticate}. */
|
|
24
|
+
authenticate?: Authenticator;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Start a gRPC server that exposes `main` as the fleet-standard `Agent.Execute`
|
|
28
|
+
* RPC. Per request it binds a {@link RequestContext} (so the agent's globals are
|
|
29
|
+
* concurrency-safe), streams each `onProgress` event as a `Progress` frame, and
|
|
30
|
+
* sends a final `Result` or `Error`. Caller cancellation (e.g. the frontend
|
|
31
|
+
* socket dropping) aborts the in-flight LLM call to stop billing.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* const { serve } = require("@stackfactor/agent-utils");
|
|
35
|
+
* serve(require("./src/main"));
|
|
36
|
+
*/
|
|
37
|
+
export declare const serve: (main: AgentMain, options?: ServeOptions) => grpc.Server;
|
|
38
|
+
//# sourceMappingURL=serve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AAStC;;;;;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,CAAC,OAAO,EAAE,GAAG,KAAK,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAElF,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;CAC9B;AAqGD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAAI,MAAM,SAAS,EAAE,UAAS,YAAiB,KAAG,IAAI,CAAC,MAsGxE,CAAC"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import * as grpc from "@grpc/grpc-js";
|
|
2
|
+
import * as protoLoader from "@grpc/proto-loader";
|
|
3
|
+
import { session as clientSession } from "@stackfactor/client-api";
|
|
4
|
+
import { writeFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import logger from "./logger.js";
|
|
8
|
+
import { runWithContext, installGlobals } from "./runtimeContext.js";
|
|
9
|
+
/** Marks an auth failure so the handler can map it to gRPC UNAUTHENTICATED. */
|
|
10
|
+
class UnauthenticatedError extends Error {
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Default authenticator: require a token on the request and resolve it to a
|
|
14
|
+
* session via the StackFactor API. Requires `BACKEND_URL` (or `REACT_APP_NODE_ENV`)
|
|
15
|
+
* to point client-api at the right backend. Throws {@link UnauthenticatedError}
|
|
16
|
+
* when the token is missing or rejected.
|
|
17
|
+
*/
|
|
18
|
+
const defaultAuthenticate = async (request) => {
|
|
19
|
+
const token = request?.authToken ?? request?.authorization;
|
|
20
|
+
if (!token) {
|
|
21
|
+
throw new UnauthenticatedError("Missing auth token");
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return await clientSession.getSession(token);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
throw new UnauthenticatedError(`Invalid auth token: ${error?.message ?? error}`);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* gRPC contract shared by the entire agent fleet. `data`/`config`/`request` are
|
|
32
|
+
* carried as JSON strings so arbitrary payloads need no proto schema churn.
|
|
33
|
+
* `Execute` is server-streaming: zero or more `Progress` frames (driven by the
|
|
34
|
+
* agent's `onProgress`) followed by exactly one `Result` or `Error`.
|
|
35
|
+
*/
|
|
36
|
+
const PROTO = `
|
|
37
|
+
syntax = "proto3";
|
|
38
|
+
package stackfactor.agent.v1;
|
|
39
|
+
|
|
40
|
+
service Agent {
|
|
41
|
+
rpc Execute(ExecuteRequest) returns (stream Update);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
message ExecuteRequest {
|
|
45
|
+
string content_type = 1;
|
|
46
|
+
string data_json = 2;
|
|
47
|
+
string config_json = 3;
|
|
48
|
+
string request_json = 4;
|
|
49
|
+
int32 event = 5;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
message Update {
|
|
53
|
+
oneof payload {
|
|
54
|
+
Progress progress = 1;
|
|
55
|
+
Result result = 2;
|
|
56
|
+
ErrorInfo error = 3;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
message Progress { int32 progress = 1; string message = 2; }
|
|
61
|
+
message Result { string result_json = 1; }
|
|
62
|
+
message ErrorInfo { int32 code = 1; string message = 2; }
|
|
63
|
+
`;
|
|
64
|
+
const loadAgentService = () => {
|
|
65
|
+
// proto-loader reads from a file; write the embedded schema to a temp path so
|
|
66
|
+
// the package stays self-contained across the cjs/esm dual build.
|
|
67
|
+
const file = join(tmpdir(), "stackfactor-agent.v1.proto");
|
|
68
|
+
writeFileSync(file, PROTO);
|
|
69
|
+
const packageDefinition = protoLoader.loadSync(file, {
|
|
70
|
+
keepCase: true,
|
|
71
|
+
longs: String,
|
|
72
|
+
enums: String,
|
|
73
|
+
defaults: true,
|
|
74
|
+
oneofs: true,
|
|
75
|
+
});
|
|
76
|
+
const loaded = grpc.loadPackageDefinition(packageDefinition);
|
|
77
|
+
return loaded.stackfactor.agent.v1.Agent.service;
|
|
78
|
+
};
|
|
79
|
+
const safeParse = (value, fallback) => {
|
|
80
|
+
if (!value)
|
|
81
|
+
return fallback;
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(value);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return fallback;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Agents that fail throw `new Error(JSON.stringify({ code, message }))`. Unwrap
|
|
91
|
+
* that back into a structured gRPC error frame; fall back to a 500 otherwise.
|
|
92
|
+
*/
|
|
93
|
+
const toErrorFrame = (error) => {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(error?.message);
|
|
96
|
+
if (parsed && typeof parsed.code === "number") {
|
|
97
|
+
return { code: parsed.code, message: String(parsed.message ?? "") };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// not a structured error
|
|
102
|
+
}
|
|
103
|
+
return { code: 500, message: String(error?.message ?? error) };
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Start a gRPC server that exposes `main` as the fleet-standard `Agent.Execute`
|
|
107
|
+
* RPC. Per request it binds a {@link RequestContext} (so the agent's globals are
|
|
108
|
+
* concurrency-safe), streams each `onProgress` event as a `Progress` frame, and
|
|
109
|
+
* sends a final `Result` or `Error`. Caller cancellation (e.g. the frontend
|
|
110
|
+
* socket dropping) aborts the in-flight LLM call to stop billing.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* const { serve } = require("@stackfactor/agent-utils");
|
|
114
|
+
* serve(require("./src/main"));
|
|
115
|
+
*/
|
|
116
|
+
export const serve = (main, options = {}) => {
|
|
117
|
+
installGlobals();
|
|
118
|
+
const port = options.port ?? (Number(process.env.PORT) || 8080);
|
|
119
|
+
const host = options.host ?? "0.0.0.0";
|
|
120
|
+
const authenticate = options.authenticate ?? defaultAuthenticate;
|
|
121
|
+
const execute = (call) => {
|
|
122
|
+
const abort = new AbortController();
|
|
123
|
+
call.on("cancelled", () => abort.abort());
|
|
124
|
+
call.on("error", () => abort.abort());
|
|
125
|
+
const req = call.request ?? {};
|
|
126
|
+
const request = safeParse(req.request_json, {});
|
|
127
|
+
const onProgress = (update) => {
|
|
128
|
+
if (abort.signal.aborted)
|
|
129
|
+
return;
|
|
130
|
+
call.write({
|
|
131
|
+
progress: {
|
|
132
|
+
progress: Math.round(update?.progress ?? 0),
|
|
133
|
+
message: update?.message ?? "",
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
const handle = async () => {
|
|
138
|
+
// Authenticate first — refuse the call when the token is missing/invalid.
|
|
139
|
+
let session;
|
|
140
|
+
try {
|
|
141
|
+
session = await authenticate(request);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
const message = error?.message ?? "Unauthenticated";
|
|
145
|
+
logger.log(null, logger.levels.warn, `agent Execute refused: ${message}`);
|
|
146
|
+
call.emit("error", {
|
|
147
|
+
code: grpc.status.UNAUTHENTICATED,
|
|
148
|
+
details: message,
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const context = {
|
|
153
|
+
config: safeParse(req.config_json, {}),
|
|
154
|
+
params: {
|
|
155
|
+
contentType: req.content_type,
|
|
156
|
+
data: safeParse(req.data_json, {}),
|
|
157
|
+
},
|
|
158
|
+
request: { ...request, session }, // expose identity/tenant to the agent
|
|
159
|
+
event: req.event ?? 0,
|
|
160
|
+
onProgress,
|
|
161
|
+
signal: abort.signal,
|
|
162
|
+
session,
|
|
163
|
+
};
|
|
164
|
+
try {
|
|
165
|
+
const result = await runWithContext(context, () => main(abort.signal));
|
|
166
|
+
if (!abort.signal.aborted) {
|
|
167
|
+
call.write({ result: { result_json: JSON.stringify(result ?? null) } });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (!abort.signal.aborted) {
|
|
172
|
+
logger.log(null, logger.levels.error, `agent Execute failed: ${error?.message ?? error}`);
|
|
173
|
+
call.write({ error: toErrorFrame(error) });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
call.end();
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
void handle();
|
|
181
|
+
};
|
|
182
|
+
const server = new grpc.Server();
|
|
183
|
+
server.addService(loadAgentService(), { Execute: execute });
|
|
184
|
+
// Insecure: Cloud Run terminates TLS at the edge and forwards h2c to the
|
|
185
|
+
// container (requires the service to be deployed with --use-http2).
|
|
186
|
+
server.bindAsync(`${host}:${port}`, grpc.ServerCredentials.createInsecure(), (err, boundPort) => {
|
|
187
|
+
if (err) {
|
|
188
|
+
logger.log(null, logger.levels.error, `agent gRPC server failed to bind ${host}:${port}: ${err.message}`);
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
logger.log(null, logger.levels.info, `agent gRPC server listening on ${host}:${boundPort}`);
|
|
192
|
+
});
|
|
193
|
+
return server;
|
|
194
|
+
};
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "restricted"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.1.1",
|
|
7
7
|
"description": "",
|
|
8
8
|
"main": "dist/cjs/index.js",
|
|
9
9
|
"module": "dist/esm/index.js",
|
|
@@ -40,10 +40,13 @@
|
|
|
40
40
|
"homepage": "https://github.com/StackFactor/stackfactor-agent-utils#readme",
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@google/genai": "^1.46.0",
|
|
43
|
+
"@grpc/grpc-js": "^1.14.4",
|
|
44
|
+
"@grpc/proto-loader": "^0.7.15",
|
|
43
45
|
"@langchain/anthropic": "^1.3.25",
|
|
44
46
|
"@langchain/google-genai": "^2.1.26",
|
|
45
47
|
"@langchain/openai": "^1.3.0",
|
|
46
48
|
"@opentelemetry/api": "^1.9.0",
|
|
49
|
+
"@stackfactor/client-api": "^1.1.239",
|
|
47
50
|
"langchain": "^1.2.35",
|
|
48
51
|
"mongoose": "^9.3.1",
|
|
49
52
|
"openai": "^6.32.0",
|