@stackfactor/agent-utils 1.0.24 → 1.1.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/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 +10 -1
- 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 +10 -1
- 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;wBAke/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;oCAuXF,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;sDAwsBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,KACX,OAAO,CAAC,GAAG,CAAC;0CAv+B8B,GAAG,KAAG,MAAM;mCAoBxC,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AA4gCT,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
|
|
@@ -1001,8 +1002,16 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1001
1002
|
streamUsage = { input_tokens: 0, output_tokens: 0 };
|
|
1002
1003
|
try {
|
|
1003
1004
|
assertQuotaAvailable();
|
|
1004
|
-
|
|
1005
|
+
// Honour caller cancellation: passing the signal tears down the
|
|
1006
|
+
// upstream HTTP request so a cancelled call stops billing tokens.
|
|
1007
|
+
const abortSignal = (0, runtimeContext_js_1.getAbortSignal)();
|
|
1008
|
+
const stream = await llm.stream(activeMessages, {
|
|
1009
|
+
signal: abortSignal ?? undefined,
|
|
1010
|
+
});
|
|
1005
1011
|
for await (const chunk of stream) {
|
|
1012
|
+
if (abortSignal?.aborted) {
|
|
1013
|
+
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.BAD_REQUEST, "Request cancelled by caller");
|
|
1014
|
+
}
|
|
1006
1015
|
accumulateChunkUsage(streamUsage, chunk);
|
|
1007
1016
|
const content = chunk?.content || chunk;
|
|
1008
1017
|
if (typeof content === "string") {
|
|
@@ -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;wBAke/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;oCAuXF,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;sDAwsBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,KACX,OAAO,CAAC,GAAG,CAAC;0CAv+B8B,GAAG,KAAG,MAAM;mCAoBxC,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AA4gCT,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
|
|
@@ -996,8 +997,16 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
996
997
|
streamUsage = { input_tokens: 0, output_tokens: 0 };
|
|
997
998
|
try {
|
|
998
999
|
assertQuotaAvailable();
|
|
999
|
-
|
|
1000
|
+
// Honour caller cancellation: passing the signal tears down the
|
|
1001
|
+
// upstream HTTP request so a cancelled call stops billing tokens.
|
|
1002
|
+
const abortSignal = getAbortSignal();
|
|
1003
|
+
const stream = await llm.stream(activeMessages, {
|
|
1004
|
+
signal: abortSignal ?? undefined,
|
|
1005
|
+
});
|
|
1000
1006
|
for await (const chunk of stream) {
|
|
1007
|
+
if (abortSignal?.aborted) {
|
|
1008
|
+
throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, "Request cancelled by caller");
|
|
1009
|
+
}
|
|
1001
1010
|
accumulateChunkUsage(streamUsage, chunk);
|
|
1002
1011
|
const content = chunk?.content || chunk;
|
|
1003
1012
|
if (typeof content === "string") {
|
|
@@ -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.0
|
|
6
|
+
"version": "1.1.0",
|
|
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",
|