@stackfactor/agent-utils 1.1.0 → 1.1.2

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.
@@ -0,0 +1,19 @@
1
+ import * as grpc from "@grpc/grpc-js";
2
+ /**
3
+ * gRPC contract shared by the entire agent fleet — the `serve()` server AND the
4
+ * relay client. `data`/`config`/`request` are carried as JSON strings so
5
+ * arbitrary payloads need no proto schema churn. `Execute` is server-streaming:
6
+ * zero or more `Progress` frames (driven by the agent's `onProgress`) followed
7
+ * by exactly one `Result` or `Error`.
8
+ */
9
+ export declare const PROTO = "\nsyntax = \"proto3\";\npackage stackfactor.agent.v1;\n\nservice Agent {\n rpc Execute(ExecuteRequest) returns (stream Update);\n}\n\nmessage ExecuteRequest {\n string content_type = 1;\n string data_json = 2;\n string config_json = 3;\n string request_json = 4;\n int32 event = 5;\n}\n\nmessage Update {\n oneof payload {\n Progress progress = 1;\n Result result = 2;\n ErrorInfo error = 3;\n }\n}\n\nmessage Progress { int32 progress = 1; string message = 2; }\nmessage Result { string result_json = 1; }\nmessage ErrorInfo { int32 code = 1; string message = 2; }\n";
10
+ /**
11
+ * Loads the Agent proto package. proto-loader reads from a file, so the embedded
12
+ * schema is written to a temp path — keeps the package self-contained across the
13
+ * cjs/esm dual build. Returns the loaded package (server uses
14
+ * `.stackfactor.agent.v1.Agent.service`; client uses the `Agent` constructor).
15
+ */
16
+ export declare const loadAgentPackage: () => any;
17
+ /** The Agent gRPC service definition (for the `serve()` server). */
18
+ export declare const loadAgentService: () => grpc.ServiceDefinition;
19
+ //# sourceMappingURL=agentProto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentProto.d.ts","sourceRoot":"","sources":["../../src/agentProto.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AAMtC;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,smBA2BjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAAO,GAWnC,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,gBAAgB,QAAO,IAAI,CAAC,iBACc,CAAC"}
@@ -0,0 +1,98 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.loadAgentService = exports.loadAgentPackage = exports.PROTO = void 0;
37
+ const grpc = __importStar(require("@grpc/grpc-js"));
38
+ const protoLoader = __importStar(require("@grpc/proto-loader"));
39
+ const node_fs_1 = require("node:fs");
40
+ const node_path_1 = require("node:path");
41
+ const node_os_1 = require("node:os");
42
+ /**
43
+ * gRPC contract shared by the entire agent fleet — the `serve()` server AND the
44
+ * relay client. `data`/`config`/`request` are carried as JSON strings so
45
+ * arbitrary payloads need no proto schema churn. `Execute` is server-streaming:
46
+ * zero or more `Progress` frames (driven by the agent's `onProgress`) followed
47
+ * by exactly one `Result` or `Error`.
48
+ */
49
+ exports.PROTO = `
50
+ syntax = "proto3";
51
+ package stackfactor.agent.v1;
52
+
53
+ service Agent {
54
+ rpc Execute(ExecuteRequest) returns (stream Update);
55
+ }
56
+
57
+ message ExecuteRequest {
58
+ string content_type = 1;
59
+ string data_json = 2;
60
+ string config_json = 3;
61
+ string request_json = 4;
62
+ int32 event = 5;
63
+ }
64
+
65
+ message Update {
66
+ oneof payload {
67
+ Progress progress = 1;
68
+ Result result = 2;
69
+ ErrorInfo error = 3;
70
+ }
71
+ }
72
+
73
+ message Progress { int32 progress = 1; string message = 2; }
74
+ message Result { string result_json = 1; }
75
+ message ErrorInfo { int32 code = 1; string message = 2; }
76
+ `;
77
+ /**
78
+ * Loads the Agent proto package. proto-loader reads from a file, so the embedded
79
+ * schema is written to a temp path — keeps the package self-contained across the
80
+ * cjs/esm dual build. Returns the loaded package (server uses
81
+ * `.stackfactor.agent.v1.Agent.service`; client uses the `Agent` constructor).
82
+ */
83
+ const loadAgentPackage = () => {
84
+ const file = (0, node_path_1.join)((0, node_os_1.tmpdir)(), "stackfactor-agent.v1.proto");
85
+ (0, node_fs_1.writeFileSync)(file, exports.PROTO);
86
+ const packageDefinition = protoLoader.loadSync(file, {
87
+ keepCase: true,
88
+ longs: String,
89
+ enums: String,
90
+ defaults: true,
91
+ oneofs: true,
92
+ });
93
+ return grpc.loadPackageDefinition(packageDefinition);
94
+ };
95
+ exports.loadAgentPackage = loadAgentPackage;
96
+ /** The Agent gRPC service definition (for the `serve()` server). */
97
+ const loadAgentService = () => (0, exports.loadAgentPackage)().stackfactor.agent.v1.Agent.service;
98
+ exports.loadAgentService = loadAgentService;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The fleet `Agent.Execute` request. `*_json` fields are JSON strings.
3
+ * `request_json` MUST include the caller's StackFactor session token as
4
+ * `authToken` — the agent validates it (via `serve()`'s auth gate) before
5
+ * running. This is distinct from the Cloud Run ID token (see `CallAgentOptions`).
6
+ */
7
+ export interface AgentExecuteRequest {
8
+ content_type: string;
9
+ data_json: string;
10
+ config_json: string;
11
+ request_json: string;
12
+ event: number;
13
+ }
14
+ export interface CallAgentOptions {
15
+ /**
16
+ * Google ID token (audience = the agent's Cloud Run URL) to get past Cloud
17
+ * Run's `--no-allow-unauthenticated` ingress. Attached as `authorization:
18
+ * Bearer` metadata. The agent never sees it; ingress consumes it.
19
+ */
20
+ idToken?: string;
21
+ /** Called for each streamed `Progress` frame (e.g. to forward to a socket). */
22
+ onProgress?: (update: {
23
+ progress: number;
24
+ message: string;
25
+ }) => void;
26
+ /** Abort the RPC (e.g. the frontend socket dropped) → stops the agent's LLM. */
27
+ signal?: AbortSignal;
28
+ /** Call deadline; defaults to 900s to match Cloud Run's max request timeout. */
29
+ deadlineMs?: number;
30
+ }
31
+ /**
32
+ * Calls a deployed agent's `Agent.Execute` over gRPC server-streaming. Forwards
33
+ * each `Progress` frame to `onProgress`, resolves with the parsed `result_json`
34
+ * on `Result`, and rejects (with `error.code`) on `ErrorInfo`. Caller-cancel via
35
+ * `options.signal` cancels the RPC.
36
+ *
37
+ * @param agentUrl - The agent's Cloud Run URL (https://…run.app).
38
+ * @param request - The `ExecuteRequest` (its `request_json` carries `authToken`).
39
+ */
40
+ export declare const callAgent: (agentUrl: string, request: AgentExecuteRequest, options?: CallAgentOptions) => Promise<any>;
41
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACrE,gFAAgF;IAChF,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAqCD;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,GACpB,UAAU,MAAM,EAChB,SAAS,mBAAmB,EAC5B,UAAS,gBAAqB,KAC7B,OAAO,CAAC,GAAG,CAyDV,CAAC"}
@@ -0,0 +1,131 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.callAgent = void 0;
37
+ const grpc = __importStar(require("@grpc/grpc-js"));
38
+ const agentProto_js_1 = require("./agentProto.js");
39
+ const DEFAULT_DEADLINE_MS = 900_000;
40
+ const safeParse = (value, fallback) => {
41
+ if (!value)
42
+ return fallback;
43
+ try {
44
+ return JSON.parse(value);
45
+ }
46
+ catch {
47
+ return fallback;
48
+ }
49
+ };
50
+ /** Turns an `ErrorInfo` frame into an Error carrying the agent's `code`. */
51
+ const agentError = (frame) => {
52
+ const error = new Error(frame?.message || "Agent error");
53
+ error.code = frame?.code;
54
+ return error;
55
+ };
56
+ /**
57
+ * Channel credentials for an authenticated Cloud Run agent: TLS (Cloud Run
58
+ * terminates at the edge) plus per-call metadata carrying the ID token.
59
+ */
60
+ const buildCredentials = (idToken) => {
61
+ const ssl = grpc.credentials.createSsl();
62
+ if (!idToken) {
63
+ return ssl;
64
+ }
65
+ const callCreds = grpc.credentials.createFromMetadataGenerator((_ctx, cb) => {
66
+ const metadata = new grpc.Metadata();
67
+ metadata.add("authorization", `Bearer ${idToken}`);
68
+ cb(null, metadata);
69
+ });
70
+ return grpc.credentials.combineChannelCredentials(ssl, callCreds);
71
+ };
72
+ /**
73
+ * Calls a deployed agent's `Agent.Execute` over gRPC server-streaming. Forwards
74
+ * each `Progress` frame to `onProgress`, resolves with the parsed `result_json`
75
+ * on `Result`, and rejects (with `error.code`) on `ErrorInfo`. Caller-cancel via
76
+ * `options.signal` cancels the RPC.
77
+ *
78
+ * @param agentUrl - The agent's Cloud Run URL (https://…run.app).
79
+ * @param request - The `ExecuteRequest` (its `request_json` carries `authToken`).
80
+ */
81
+ const callAgent = (agentUrl, request, options = {}) => new Promise((resolve, reject) => {
82
+ const loaded = (0, agentProto_js_1.loadAgentPackage)();
83
+ const url = new URL(agentUrl);
84
+ const address = `${url.hostname}:${url.port || "443"}`;
85
+ const client = new loaded.stackfactor.agent.v1.Agent(address, buildCredentials(options.idToken));
86
+ const stream = client.Execute(request, {
87
+ deadline: Date.now() + (options.deadlineMs ?? DEFAULT_DEADLINE_MS),
88
+ });
89
+ let settled = false;
90
+ const settle = (run) => {
91
+ if (settled) {
92
+ return;
93
+ }
94
+ settled = true;
95
+ try {
96
+ client.close();
97
+ }
98
+ catch {
99
+ // ignore close errors
100
+ }
101
+ run();
102
+ };
103
+ if (options.signal) {
104
+ if (options.signal.aborted) {
105
+ stream.cancel();
106
+ }
107
+ else {
108
+ options.signal.addEventListener("abort", () => stream.cancel(), {
109
+ once: true,
110
+ });
111
+ }
112
+ }
113
+ stream.on("data", (update) => {
114
+ if (update.progress) {
115
+ options.onProgress?.({
116
+ progress: update.progress.progress ?? 0,
117
+ message: update.progress.message ?? "",
118
+ });
119
+ }
120
+ else if (update.result) {
121
+ const result = safeParse(update.result.result_json, null);
122
+ settle(() => resolve(result));
123
+ }
124
+ else if (update.error) {
125
+ settle(() => reject(agentError(update.error)));
126
+ }
127
+ });
128
+ stream.on("error", (error) => settle(() => reject(error)));
129
+ stream.on("end", () => settle(() => reject(new Error("Agent stream ended without a result or error"))));
130
+ });
131
+ exports.callAgent = callAgent;
@@ -3,11 +3,14 @@ import errorHandling from "./errorHandling.js";
3
3
  import langChain from "./langChain.js";
4
4
  import logger from "./logger.js";
5
5
  import { serve } from "./serve.js";
6
+ import { callAgent } from "./client.js";
6
7
  import * as runtimeContext from "./runtimeContext.js";
7
8
  export { constants };
8
9
  export { errorHandling };
9
10
  export { langChain };
10
11
  export { logger };
11
12
  export { serve };
13
+ export { callAgent };
14
+ export type { AgentExecuteRequest, CallAgentOptions } from "./client.js";
12
15
  export { runtimeContext };
13
16
  //# sourceMappingURL=index.d.ts.map
@@ -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;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"}
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,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,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,SAAS,EAAE,CAAC;AAErB,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEzE,OAAO,EAAE,cAAc,EAAE,CAAC"}
package/dist/cjs/index.js CHANGED
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.runtimeContext = exports.serve = exports.logger = exports.langChain = exports.errorHandling = exports.constants = void 0;
39
+ exports.runtimeContext = exports.callAgent = exports.serve = exports.logger = exports.langChain = exports.errorHandling = exports.constants = void 0;
40
40
  const const_js_1 = __importDefault(require("./const.js"));
41
41
  exports.constants = const_js_1.default;
42
42
  const errorHandling_js_1 = __importDefault(require("./errorHandling.js"));
@@ -47,5 +47,7 @@ const logger_js_1 = __importDefault(require("./logger.js"));
47
47
  exports.logger = logger_js_1.default;
48
48
  const serve_js_1 = require("./serve.js");
49
49
  Object.defineProperty(exports, "serve", { enumerable: true, get: function () { return serve_js_1.serve; } });
50
+ const client_js_1 = require("./client.js");
51
+ Object.defineProperty(exports, "callAgent", { enumerable: true, get: function () { return client_js_1.callAgent; } });
50
52
  const runtimeContext = __importStar(require("./runtimeContext.js"));
51
53
  exports.runtimeContext = runtimeContext;
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":";0CAiD6C,GAAG,KAAG,IAAI;wBAyhB/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"}
@@ -68,40 +68,81 @@ const recordCost = (cost) => {
68
68
  }
69
69
  };
70
70
  /**
71
- * Looks up the per-model pricing entry from `config.modelPricing`. Returns `null`
72
- * when pricing is not configured for the model, in which case cost recording
73
- * becomes a no-op (the call still succeeds pricing data is the host's
74
- * responsibility, not the agent's).
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).
75
76
  */
76
- const getModelPrice = (modelName, config) => {
77
- const pricing = config?.modelPricing;
78
- if (!pricing)
77
+ const getModelRate = (modelName, config, kind) => {
78
+ if (!config || !modelName)
79
79
  return null;
80
- return pricing[modelName] || null;
80
+ const rate = Number(config[`${modelName}-${kind}-costs`]);
81
+ return Number.isFinite(rate) ? rate : null;
81
82
  };
82
83
  /**
83
- * Computes USD cost for a text LLM call. `config.modelPricing[modelName]` is
84
- * expected to provide `input` and `output` rates in dollars per million tokens.
84
+ * Accumulates the per-model usage breakdown on `global.quota.byModel` so the
85
+ * agent can report input/output token counts and image spend per model to the
86
+ * UI. No-ops when the quota global is absent.
85
87
  */
86
- const calculateTextCost = (modelName, usage, config) => {
88
+ const recordModelBreakdown = (modelName, inputCost, outputCost, imageCost, inputTokens = 0, outputTokens = 0) => {
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
+ inputTokens: 0,
99
+ outputTokens: 0,
100
+ });
101
+ // Entries created by older versions of this module lack the token counters.
102
+ if (typeof entry.inputTokens !== "number")
103
+ entry.inputTokens = 0;
104
+ if (typeof entry.outputTokens !== "number")
105
+ entry.outputTokens = 0;
106
+ if (Number.isFinite(inputCost) && inputCost > 0)
107
+ entry.inputCost += inputCost;
108
+ if (Number.isFinite(outputCost) && outputCost > 0)
109
+ entry.outputCost += outputCost;
110
+ if (Number.isFinite(imageCost) && imageCost > 0)
111
+ entry.imageCost += imageCost;
112
+ if (Number.isFinite(inputTokens) && inputTokens > 0)
113
+ entry.inputTokens += inputTokens;
114
+ if (Number.isFinite(outputTokens) && outputTokens > 0)
115
+ entry.outputTokens += outputTokens;
116
+ };
117
+ /**
118
+ * Computes and records the USD cost of a text LLM call from the
119
+ * `<model>-input-token-costs` / `<model>-output-token-costs` constants
120
+ * (dollars per million tokens). Updates both the session total
121
+ * (`quota.usedThisSession` / `quota.remaining`) and the per-model breakdown
122
+ * (`quota.byModel`).
123
+ */
124
+ const recordTextCost = (modelName, usage, config) => {
87
125
  if (!usage)
88
- return 0;
89
- const price = getModelPrice(modelName, config);
90
- if (!price)
91
- return 0;
92
- const inputCost = ((usage.input_tokens || 0) / 1_000_000) * (price.input || 0);
93
- const outputCost = ((usage.output_tokens || 0) / 1_000_000) * (price.output || 0);
94
- return inputCost + outputCost;
126
+ return;
127
+ const inputRate = getModelRate(modelName, config, "input-token") || 0;
128
+ const outputRate = getModelRate(modelName, config, "output-token") || 0;
129
+ const inputTokens = usage.input_tokens || 0;
130
+ const outputTokens = usage.output_tokens || 0;
131
+ const inputCost = (inputTokens / 1_000_000) * inputRate;
132
+ const outputCost = (outputTokens / 1_000_000) * outputRate;
133
+ recordCost(inputCost + outputCost);
134
+ recordModelBreakdown(modelName, inputCost, outputCost, 0, inputTokens, outputTokens);
95
135
  };
96
136
  /**
97
- * Computes USD cost for an image generation call. `config.modelPricing[modelName]`
98
- * is expected to provide a `perImage` rate in dollars.
137
+ * Computes and records the USD cost of an image generation call from the
138
+ * `<model>-image-costs` constant (dollars per generated image). Updates both
139
+ * the session total and the per-model breakdown.
99
140
  */
100
- const calculateImageCost = (modelName, numImages, config) => {
101
- const price = getModelPrice(modelName, config);
102
- if (!price)
103
- return 0;
104
- return (numImages || 0) * (price.perImage || 0);
141
+ const recordImageCost = (modelName, numImages, config) => {
142
+ const rate = getModelRate(modelName, config, "image") || 0;
143
+ const imageCost = (numImages || 0) * rate;
144
+ recordCost(imageCost);
145
+ recordModelBreakdown(modelName, 0, 0, imageCost);
105
146
  };
106
147
  /**
107
148
  * Extracts a normalized token-usage object from a single LangChain `invoke()`
@@ -569,7 +610,7 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
569
610
  throw err;
570
611
  }
571
612
  }
572
- recordCost(calculateTextCost(modelName, sumAgentResponseUsage(response), config));
613
+ recordTextCost(modelName, sumAgentResponseUsage(response), config);
573
614
  const endTime = Date.now();
574
615
  const duration = endTime - startTime;
575
616
  logger_js_1.default.log(null, logger_js_1.default.levels.info, `Agent "${agent.options?.name}" completed in ${Math.round(duration / 1000)} seconds.`);
@@ -1044,7 +1085,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1044
1085
  throw err;
1045
1086
  }
1046
1087
  }
1047
- recordCost(calculateTextCost(modelName, streamUsage, config));
1088
+ recordTextCost(modelName, streamUsage, config);
1048
1089
  if (!rawContent) {
1049
1090
  throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
1050
1091
  }
@@ -1144,7 +1185,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1144
1185
  throw err;
1145
1186
  }
1146
1187
  }
1147
- recordCost(calculateTextCost(modelName, extractUsageFromInvoke(response), config));
1188
+ recordTextCost(modelName, extractUsageFromInvoke(response), config);
1148
1189
  const rawContent = response?.content || response;
1149
1190
  // If not expecting JSON, return raw content directly
1150
1191
  if (!expectsJsonResponse) {
@@ -1252,7 +1293,7 @@ const generateImageWithOpenAI = async (modelName, config, prompt, options) => {
1252
1293
  }
1253
1294
  assertQuotaAvailable();
1254
1295
  const response = await openai.images.generate(requestParams);
1255
- recordCost(calculateImageCost(modelName, response.data?.length || n, config));
1296
+ recordImageCost(modelName, response.data?.length || n, config);
1256
1297
  // Format response based on number of images
1257
1298
  if (n === 1) {
1258
1299
  const imageData = response.data[0];
@@ -1369,7 +1410,7 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
1369
1410
  if (images.length === 0) {
1370
1411
  throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `No images were generated by ${modelName}`);
1371
1412
  }
1372
- recordCost(calculateImageCost(modelName, images.length, config));
1413
+ recordImageCost(modelName, images.length, config);
1373
1414
  if (numberOfImages === 1 || images.length === 1) {
1374
1415
  return images[0];
1375
1416
  }
@@ -1 +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"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AAMtC;;;;;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;AAkDD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAAI,MAAM,SAAS,EAAE,UAAS,YAAiB,KAAG,IAAI,CAAC,MAsGxE,CAAC"}
package/dist/cjs/serve.js CHANGED
@@ -38,12 +38,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.serve = void 0;
40
40
  const grpc = __importStar(require("@grpc/grpc-js"));
41
- const protoLoader = __importStar(require("@grpc/proto-loader"));
42
41
  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
42
  const logger_js_1 = __importDefault(require("./logger.js"));
43
+ const agentProto_js_1 = require("./agentProto.js");
47
44
  const runtimeContext_js_1 = require("./runtimeContext.js");
48
45
  /** Marks an auth failure so the handler can map it to gRPC UNAUTHENTICATED. */
49
46
  class UnauthenticatedError extends Error {
@@ -66,55 +63,6 @@ const defaultAuthenticate = async (request) => {
66
63
  throw new UnauthenticatedError(`Invalid auth token: ${error?.message ?? error}`);
67
64
  }
68
65
  };
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
66
  const safeParse = (value, fallback) => {
119
67
  if (!value)
120
68
  return fallback;
@@ -219,7 +167,7 @@ const serve = (main, options = {}) => {
219
167
  void handle();
220
168
  };
221
169
  const server = new grpc.Server();
222
- server.addService(loadAgentService(), { Execute: execute });
170
+ server.addService((0, agentProto_js_1.loadAgentService)(), { Execute: execute });
223
171
  // Insecure: Cloud Run terminates TLS at the edge and forwards h2c to the
224
172
  // container (requires the service to be deployed with --use-http2).
225
173
  server.bindAsync(`${host}:${port}`, grpc.ServerCredentials.createInsecure(), (err, boundPort) => {
@@ -0,0 +1,19 @@
1
+ import * as grpc from "@grpc/grpc-js";
2
+ /**
3
+ * gRPC contract shared by the entire agent fleet — the `serve()` server AND the
4
+ * relay client. `data`/`config`/`request` are carried as JSON strings so
5
+ * arbitrary payloads need no proto schema churn. `Execute` is server-streaming:
6
+ * zero or more `Progress` frames (driven by the agent's `onProgress`) followed
7
+ * by exactly one `Result` or `Error`.
8
+ */
9
+ export declare const PROTO = "\nsyntax = \"proto3\";\npackage stackfactor.agent.v1;\n\nservice Agent {\n rpc Execute(ExecuteRequest) returns (stream Update);\n}\n\nmessage ExecuteRequest {\n string content_type = 1;\n string data_json = 2;\n string config_json = 3;\n string request_json = 4;\n int32 event = 5;\n}\n\nmessage Update {\n oneof payload {\n Progress progress = 1;\n Result result = 2;\n ErrorInfo error = 3;\n }\n}\n\nmessage Progress { int32 progress = 1; string message = 2; }\nmessage Result { string result_json = 1; }\nmessage ErrorInfo { int32 code = 1; string message = 2; }\n";
10
+ /**
11
+ * Loads the Agent proto package. proto-loader reads from a file, so the embedded
12
+ * schema is written to a temp path — keeps the package self-contained across the
13
+ * cjs/esm dual build. Returns the loaded package (server uses
14
+ * `.stackfactor.agent.v1.Agent.service`; client uses the `Agent` constructor).
15
+ */
16
+ export declare const loadAgentPackage: () => any;
17
+ /** The Agent gRPC service definition (for the `serve()` server). */
18
+ export declare const loadAgentService: () => grpc.ServiceDefinition;
19
+ //# sourceMappingURL=agentProto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentProto.d.ts","sourceRoot":"","sources":["../../src/agentProto.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AAMtC;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,smBA2BjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAAO,GAWnC,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,gBAAgB,QAAO,IAAI,CAAC,iBACc,CAAC"}
@@ -0,0 +1,60 @@
1
+ import * as grpc from "@grpc/grpc-js";
2
+ import * as protoLoader from "@grpc/proto-loader";
3
+ import { writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ /**
7
+ * gRPC contract shared by the entire agent fleet — the `serve()` server AND the
8
+ * relay client. `data`/`config`/`request` are carried as JSON strings so
9
+ * arbitrary payloads need no proto schema churn. `Execute` is server-streaming:
10
+ * zero or more `Progress` frames (driven by the agent's `onProgress`) followed
11
+ * by exactly one `Result` or `Error`.
12
+ */
13
+ export const PROTO = `
14
+ syntax = "proto3";
15
+ package stackfactor.agent.v1;
16
+
17
+ service Agent {
18
+ rpc Execute(ExecuteRequest) returns (stream Update);
19
+ }
20
+
21
+ message ExecuteRequest {
22
+ string content_type = 1;
23
+ string data_json = 2;
24
+ string config_json = 3;
25
+ string request_json = 4;
26
+ int32 event = 5;
27
+ }
28
+
29
+ message Update {
30
+ oneof payload {
31
+ Progress progress = 1;
32
+ Result result = 2;
33
+ ErrorInfo error = 3;
34
+ }
35
+ }
36
+
37
+ message Progress { int32 progress = 1; string message = 2; }
38
+ message Result { string result_json = 1; }
39
+ message ErrorInfo { int32 code = 1; string message = 2; }
40
+ `;
41
+ /**
42
+ * Loads the Agent proto package. proto-loader reads from a file, so the embedded
43
+ * schema is written to a temp path — keeps the package self-contained across the
44
+ * cjs/esm dual build. Returns the loaded package (server uses
45
+ * `.stackfactor.agent.v1.Agent.service`; client uses the `Agent` constructor).
46
+ */
47
+ export const loadAgentPackage = () => {
48
+ const file = join(tmpdir(), "stackfactor-agent.v1.proto");
49
+ writeFileSync(file, PROTO);
50
+ const packageDefinition = protoLoader.loadSync(file, {
51
+ keepCase: true,
52
+ longs: String,
53
+ enums: String,
54
+ defaults: true,
55
+ oneofs: true,
56
+ });
57
+ return grpc.loadPackageDefinition(packageDefinition);
58
+ };
59
+ /** The Agent gRPC service definition (for the `serve()` server). */
60
+ export const loadAgentService = () => loadAgentPackage().stackfactor.agent.v1.Agent.service;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The fleet `Agent.Execute` request. `*_json` fields are JSON strings.
3
+ * `request_json` MUST include the caller's StackFactor session token as
4
+ * `authToken` — the agent validates it (via `serve()`'s auth gate) before
5
+ * running. This is distinct from the Cloud Run ID token (see `CallAgentOptions`).
6
+ */
7
+ export interface AgentExecuteRequest {
8
+ content_type: string;
9
+ data_json: string;
10
+ config_json: string;
11
+ request_json: string;
12
+ event: number;
13
+ }
14
+ export interface CallAgentOptions {
15
+ /**
16
+ * Google ID token (audience = the agent's Cloud Run URL) to get past Cloud
17
+ * Run's `--no-allow-unauthenticated` ingress. Attached as `authorization:
18
+ * Bearer` metadata. The agent never sees it; ingress consumes it.
19
+ */
20
+ idToken?: string;
21
+ /** Called for each streamed `Progress` frame (e.g. to forward to a socket). */
22
+ onProgress?: (update: {
23
+ progress: number;
24
+ message: string;
25
+ }) => void;
26
+ /** Abort the RPC (e.g. the frontend socket dropped) → stops the agent's LLM. */
27
+ signal?: AbortSignal;
28
+ /** Call deadline; defaults to 900s to match Cloud Run's max request timeout. */
29
+ deadlineMs?: number;
30
+ }
31
+ /**
32
+ * Calls a deployed agent's `Agent.Execute` over gRPC server-streaming. Forwards
33
+ * each `Progress` frame to `onProgress`, resolves with the parsed `result_json`
34
+ * on `Result`, and rejects (with `error.code`) on `ErrorInfo`. Caller-cancel via
35
+ * `options.signal` cancels the RPC.
36
+ *
37
+ * @param agentUrl - The agent's Cloud Run URL (https://…run.app).
38
+ * @param request - The `ExecuteRequest` (its `request_json` carries `authToken`).
39
+ */
40
+ export declare const callAgent: (agentUrl: string, request: AgentExecuteRequest, options?: CallAgentOptions) => Promise<any>;
41
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACrE,gFAAgF;IAChF,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAqCD;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,GACpB,UAAU,MAAM,EAChB,SAAS,mBAAmB,EAC5B,UAAS,gBAAqB,KAC7B,OAAO,CAAC,GAAG,CAyDV,CAAC"}
@@ -0,0 +1,94 @@
1
+ import * as grpc from "@grpc/grpc-js";
2
+ import { loadAgentPackage } from "./agentProto.js";
3
+ const DEFAULT_DEADLINE_MS = 900_000;
4
+ const safeParse = (value, fallback) => {
5
+ if (!value)
6
+ return fallback;
7
+ try {
8
+ return JSON.parse(value);
9
+ }
10
+ catch {
11
+ return fallback;
12
+ }
13
+ };
14
+ /** Turns an `ErrorInfo` frame into an Error carrying the agent's `code`. */
15
+ const agentError = (frame) => {
16
+ const error = new Error(frame?.message || "Agent error");
17
+ error.code = frame?.code;
18
+ return error;
19
+ };
20
+ /**
21
+ * Channel credentials for an authenticated Cloud Run agent: TLS (Cloud Run
22
+ * terminates at the edge) plus per-call metadata carrying the ID token.
23
+ */
24
+ const buildCredentials = (idToken) => {
25
+ const ssl = grpc.credentials.createSsl();
26
+ if (!idToken) {
27
+ return ssl;
28
+ }
29
+ const callCreds = grpc.credentials.createFromMetadataGenerator((_ctx, cb) => {
30
+ const metadata = new grpc.Metadata();
31
+ metadata.add("authorization", `Bearer ${idToken}`);
32
+ cb(null, metadata);
33
+ });
34
+ return grpc.credentials.combineChannelCredentials(ssl, callCreds);
35
+ };
36
+ /**
37
+ * Calls a deployed agent's `Agent.Execute` over gRPC server-streaming. Forwards
38
+ * each `Progress` frame to `onProgress`, resolves with the parsed `result_json`
39
+ * on `Result`, and rejects (with `error.code`) on `ErrorInfo`. Caller-cancel via
40
+ * `options.signal` cancels the RPC.
41
+ *
42
+ * @param agentUrl - The agent's Cloud Run URL (https://…run.app).
43
+ * @param request - The `ExecuteRequest` (its `request_json` carries `authToken`).
44
+ */
45
+ export const callAgent = (agentUrl, request, options = {}) => new Promise((resolve, reject) => {
46
+ const loaded = loadAgentPackage();
47
+ const url = new URL(agentUrl);
48
+ const address = `${url.hostname}:${url.port || "443"}`;
49
+ const client = new loaded.stackfactor.agent.v1.Agent(address, buildCredentials(options.idToken));
50
+ const stream = client.Execute(request, {
51
+ deadline: Date.now() + (options.deadlineMs ?? DEFAULT_DEADLINE_MS),
52
+ });
53
+ let settled = false;
54
+ const settle = (run) => {
55
+ if (settled) {
56
+ return;
57
+ }
58
+ settled = true;
59
+ try {
60
+ client.close();
61
+ }
62
+ catch {
63
+ // ignore close errors
64
+ }
65
+ run();
66
+ };
67
+ if (options.signal) {
68
+ if (options.signal.aborted) {
69
+ stream.cancel();
70
+ }
71
+ else {
72
+ options.signal.addEventListener("abort", () => stream.cancel(), {
73
+ once: true,
74
+ });
75
+ }
76
+ }
77
+ stream.on("data", (update) => {
78
+ if (update.progress) {
79
+ options.onProgress?.({
80
+ progress: update.progress.progress ?? 0,
81
+ message: update.progress.message ?? "",
82
+ });
83
+ }
84
+ else if (update.result) {
85
+ const result = safeParse(update.result.result_json, null);
86
+ settle(() => resolve(result));
87
+ }
88
+ else if (update.error) {
89
+ settle(() => reject(agentError(update.error)));
90
+ }
91
+ });
92
+ stream.on("error", (error) => settle(() => reject(error)));
93
+ stream.on("end", () => settle(() => reject(new Error("Agent stream ended without a result or error"))));
94
+ });
@@ -3,11 +3,14 @@ import errorHandling from "./errorHandling.js";
3
3
  import langChain from "./langChain.js";
4
4
  import logger from "./logger.js";
5
5
  import { serve } from "./serve.js";
6
+ import { callAgent } from "./client.js";
6
7
  import * as runtimeContext from "./runtimeContext.js";
7
8
  export { constants };
8
9
  export { errorHandling };
9
10
  export { langChain };
10
11
  export { logger };
11
12
  export { serve };
13
+ export { callAgent };
14
+ export type { AgentExecuteRequest, CallAgentOptions } from "./client.js";
12
15
  export { runtimeContext };
13
16
  //# sourceMappingURL=index.d.ts.map
@@ -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;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"}
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,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,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,SAAS,EAAE,CAAC;AAErB,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEzE,OAAO,EAAE,cAAc,EAAE,CAAC"}
package/dist/esm/index.js CHANGED
@@ -3,10 +3,12 @@ import errorHandling from "./errorHandling.js";
3
3
  import langChain from "./langChain.js";
4
4
  import logger from "./logger.js";
5
5
  import { serve } from "./serve.js";
6
+ import { callAgent } from "./client.js";
6
7
  import * as runtimeContext from "./runtimeContext.js";
7
8
  export { constants };
8
9
  export { errorHandling };
9
10
  export { langChain };
10
11
  export { logger };
11
12
  export { serve };
13
+ export { callAgent };
12
14
  export { runtimeContext };
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":";0CAiD6C,GAAG,KAAG,IAAI;wBAyhB/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"}
@@ -63,40 +63,81 @@ const recordCost = (cost) => {
63
63
  }
64
64
  };
65
65
  /**
66
- * Looks up the per-model pricing entry from `config.modelPricing`. Returns `null`
67
- * when pricing is not configured for the model, in which case cost recording
68
- * becomes a no-op (the call still succeeds pricing data is the host's
69
- * responsibility, not the agent's).
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).
70
71
  */
71
- const getModelPrice = (modelName, config) => {
72
- const pricing = config?.modelPricing;
73
- if (!pricing)
72
+ const getModelRate = (modelName, config, kind) => {
73
+ if (!config || !modelName)
74
74
  return null;
75
- return pricing[modelName] || null;
75
+ const rate = Number(config[`${modelName}-${kind}-costs`]);
76
+ return Number.isFinite(rate) ? rate : null;
76
77
  };
77
78
  /**
78
- * Computes USD cost for a text LLM call. `config.modelPricing[modelName]` is
79
- * expected to provide `input` and `output` rates in dollars per million tokens.
79
+ * Accumulates the per-model usage breakdown on `global.quota.byModel` so the
80
+ * agent can report input/output token counts and image spend per model to the
81
+ * UI. No-ops when the quota global is absent.
80
82
  */
81
- const calculateTextCost = (modelName, usage, config) => {
83
+ const recordModelBreakdown = (modelName, inputCost, outputCost, imageCost, inputTokens = 0, outputTokens = 0) => {
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
+ inputTokens: 0,
94
+ outputTokens: 0,
95
+ });
96
+ // Entries created by older versions of this module lack the token counters.
97
+ if (typeof entry.inputTokens !== "number")
98
+ entry.inputTokens = 0;
99
+ if (typeof entry.outputTokens !== "number")
100
+ entry.outputTokens = 0;
101
+ if (Number.isFinite(inputCost) && inputCost > 0)
102
+ entry.inputCost += inputCost;
103
+ if (Number.isFinite(outputCost) && outputCost > 0)
104
+ entry.outputCost += outputCost;
105
+ if (Number.isFinite(imageCost) && imageCost > 0)
106
+ entry.imageCost += imageCost;
107
+ if (Number.isFinite(inputTokens) && inputTokens > 0)
108
+ entry.inputTokens += inputTokens;
109
+ if (Number.isFinite(outputTokens) && outputTokens > 0)
110
+ entry.outputTokens += outputTokens;
111
+ };
112
+ /**
113
+ * Computes and records the USD cost of a text LLM call from the
114
+ * `<model>-input-token-costs` / `<model>-output-token-costs` constants
115
+ * (dollars per million tokens). Updates both the session total
116
+ * (`quota.usedThisSession` / `quota.remaining`) and the per-model breakdown
117
+ * (`quota.byModel`).
118
+ */
119
+ const recordTextCost = (modelName, usage, config) => {
82
120
  if (!usage)
83
- return 0;
84
- const price = getModelPrice(modelName, config);
85
- if (!price)
86
- return 0;
87
- const inputCost = ((usage.input_tokens || 0) / 1_000_000) * (price.input || 0);
88
- const outputCost = ((usage.output_tokens || 0) / 1_000_000) * (price.output || 0);
89
- return inputCost + outputCost;
121
+ return;
122
+ const inputRate = getModelRate(modelName, config, "input-token") || 0;
123
+ const outputRate = getModelRate(modelName, config, "output-token") || 0;
124
+ const inputTokens = usage.input_tokens || 0;
125
+ const outputTokens = usage.output_tokens || 0;
126
+ const inputCost = (inputTokens / 1_000_000) * inputRate;
127
+ const outputCost = (outputTokens / 1_000_000) * outputRate;
128
+ recordCost(inputCost + outputCost);
129
+ recordModelBreakdown(modelName, inputCost, outputCost, 0, inputTokens, outputTokens);
90
130
  };
91
131
  /**
92
- * Computes USD cost for an image generation call. `config.modelPricing[modelName]`
93
- * is expected to provide a `perImage` rate in dollars.
132
+ * Computes and records the USD cost of an image generation call from the
133
+ * `<model>-image-costs` constant (dollars per generated image). Updates both
134
+ * the session total and the per-model breakdown.
94
135
  */
95
- const calculateImageCost = (modelName, numImages, config) => {
96
- const price = getModelPrice(modelName, config);
97
- if (!price)
98
- return 0;
99
- return (numImages || 0) * (price.perImage || 0);
136
+ const recordImageCost = (modelName, numImages, config) => {
137
+ const rate = getModelRate(modelName, config, "image") || 0;
138
+ const imageCost = (numImages || 0) * rate;
139
+ recordCost(imageCost);
140
+ recordModelBreakdown(modelName, 0, 0, imageCost);
100
141
  };
101
142
  /**
102
143
  * Extracts a normalized token-usage object from a single LangChain `invoke()`
@@ -564,7 +605,7 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
564
605
  throw err;
565
606
  }
566
607
  }
567
- recordCost(calculateTextCost(modelName, sumAgentResponseUsage(response), config));
608
+ recordTextCost(modelName, sumAgentResponseUsage(response), config);
568
609
  const endTime = Date.now();
569
610
  const duration = endTime - startTime;
570
611
  logger.log(null, logger.levels.info, `Agent "${agent.options?.name}" completed in ${Math.round(duration / 1000)} seconds.`);
@@ -1039,7 +1080,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1039
1080
  throw err;
1040
1081
  }
1041
1082
  }
1042
- recordCost(calculateTextCost(modelName, streamUsage, config));
1083
+ recordTextCost(modelName, streamUsage, config);
1043
1084
  if (!rawContent) {
1044
1085
  throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
1045
1086
  }
@@ -1139,7 +1180,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1139
1180
  throw err;
1140
1181
  }
1141
1182
  }
1142
- recordCost(calculateTextCost(modelName, extractUsageFromInvoke(response), config));
1183
+ recordTextCost(modelName, extractUsageFromInvoke(response), config);
1143
1184
  const rawContent = response?.content || response;
1144
1185
  // If not expecting JSON, return raw content directly
1145
1186
  if (!expectsJsonResponse) {
@@ -1247,7 +1288,7 @@ const generateImageWithOpenAI = async (modelName, config, prompt, options) => {
1247
1288
  }
1248
1289
  assertQuotaAvailable();
1249
1290
  const response = await openai.images.generate(requestParams);
1250
- recordCost(calculateImageCost(modelName, response.data?.length || n, config));
1291
+ recordImageCost(modelName, response.data?.length || n, config);
1251
1292
  // Format response based on number of images
1252
1293
  if (n === 1) {
1253
1294
  const imageData = response.data[0];
@@ -1364,7 +1405,7 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
1364
1405
  if (images.length === 0) {
1365
1406
  throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `No images were generated by ${modelName}`);
1366
1407
  }
1367
- recordCost(calculateImageCost(modelName, images.length, config));
1408
+ recordImageCost(modelName, images.length, config);
1368
1409
  if (numberOfImages === 1 || images.length === 1) {
1369
1410
  return images[0];
1370
1411
  }
@@ -1 +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"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AAMtC;;;;;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;AAkDD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAAI,MAAM,SAAS,EAAE,UAAS,YAAiB,KAAG,IAAI,CAAC,MAsGxE,CAAC"}
package/dist/esm/serve.js CHANGED
@@ -1,10 +1,7 @@
1
1
  import * as grpc from "@grpc/grpc-js";
2
- import * as protoLoader from "@grpc/proto-loader";
3
2
  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
3
  import logger from "./logger.js";
4
+ import { loadAgentService } from "./agentProto.js";
8
5
  import { runWithContext, installGlobals } from "./runtimeContext.js";
9
6
  /** Marks an auth failure so the handler can map it to gRPC UNAUTHENTICATED. */
10
7
  class UnauthenticatedError extends Error {
@@ -27,55 +24,6 @@ const defaultAuthenticate = async (request) => {
27
24
  throw new UnauthenticatedError(`Invalid auth token: ${error?.message ?? error}`);
28
25
  }
29
26
  };
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
27
  const safeParse = (value, fallback) => {
80
28
  if (!value)
81
29
  return fallback;
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@stackfactor/agent-utils",
3
3
  "publishConfig": {
4
- "access": "restricted"
4
+ "access": "public"
5
5
  },
6
- "version": "1.1.0",
6
+ "version": "1.1.2",
7
7
  "description": "",
8
8
  "main": "dist/cjs/index.js",
9
9
  "module": "dist/esm/index.js",