@stackfactor/agent-utils 1.4.3 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +81 -6
  2. package/dist/cjs/errorHandling.d.ts.map +1 -1
  3. package/dist/cjs/errorHandling.js +10 -5
  4. package/dist/cjs/index.d.ts +8 -2
  5. package/dist/cjs/index.d.ts.map +1 -1
  6. package/dist/cjs/index.js +9 -1
  7. package/dist/cjs/langChain.d.ts +25 -5
  8. package/dist/cjs/langChain.d.ts.map +1 -1
  9. package/dist/cjs/langChain.js +244 -179
  10. package/dist/cjs/runtimeContext.d.ts +3 -0
  11. package/dist/cjs/runtimeContext.d.ts.map +1 -1
  12. package/dist/cjs/serve.d.ts +88 -0
  13. package/dist/cjs/serve.d.ts.map +1 -1
  14. package/dist/cjs/serve.js +230 -3
  15. package/dist/cjs/stackfactorKnowledge.d.ts +182 -0
  16. package/dist/cjs/stackfactorKnowledge.d.ts.map +1 -0
  17. package/dist/cjs/stackfactorKnowledge.js +274 -0
  18. package/dist/cjs/tavily.d.ts +4 -0
  19. package/dist/cjs/tavily.d.ts.map +1 -1
  20. package/dist/cjs/tavily.js +17 -18
  21. package/dist/cjs/usage.d.ts +74 -0
  22. package/dist/cjs/usage.d.ts.map +1 -0
  23. package/dist/cjs/usage.js +160 -0
  24. package/dist/esm/errorHandling.d.ts.map +1 -1
  25. package/dist/esm/errorHandling.js +10 -5
  26. package/dist/esm/index.d.ts +8 -2
  27. package/dist/esm/index.d.ts.map +1 -1
  28. package/dist/esm/index.js +6 -2
  29. package/dist/esm/langChain.d.ts +25 -5
  30. package/dist/esm/langChain.d.ts.map +1 -1
  31. package/dist/esm/langChain.js +244 -179
  32. package/dist/esm/runtimeContext.d.ts +3 -0
  33. package/dist/esm/runtimeContext.d.ts.map +1 -1
  34. package/dist/esm/serve.d.ts +88 -0
  35. package/dist/esm/serve.d.ts.map +1 -1
  36. package/dist/esm/serve.js +226 -3
  37. package/dist/esm/stackfactorKnowledge.d.ts +182 -0
  38. package/dist/esm/stackfactorKnowledge.d.ts.map +1 -0
  39. package/dist/esm/stackfactorKnowledge.js +266 -0
  40. package/dist/esm/tavily.d.ts +4 -0
  41. package/dist/esm/tavily.d.ts.map +1 -1
  42. package/dist/esm/tavily.js +17 -18
  43. package/dist/esm/usage.d.ts +74 -0
  44. package/dist/esm/usage.d.ts.map +1 -0
  45. package/dist/esm/usage.js +150 -0
  46. package/package.json +14 -8
@@ -1,4 +1,5 @@
1
1
  import * as grpc from "@grpc/grpc-js";
2
+ import { type ReportUsage } from "./usage.js";
2
3
  /**
3
4
  * The agent entry point. Reads `params` / `config` / `onProgress` from globals
4
5
  * (populated per-request by {@link serve}) and returns the structured result.
@@ -22,7 +23,94 @@ export interface ServeOptions {
22
23
  host?: string;
23
24
  /** Override the auth check (e.g. in tests). Defaults to {@link defaultAuthenticate}. */
24
25
  authenticate?: Authenticator;
26
+ /** Run-once mode only: override the envelope fetch (e.g. in tests). */
27
+ fetchEnvelope?: (batchJobId: string, token: string) => Promise<any>;
28
+ /** Run-once mode only: override the run lifecycle (e.g. in tests). */
29
+ lifecycle?: RunLifecycle;
30
+ /** Run-once mode only: override process exit (e.g. in tests). */
31
+ exit?: (code: number) => void;
32
+ /** Override how what the work used is reported to the platform (e.g. in tests). */
33
+ reportUsage?: ReportUsage;
34
+ /** Override how often running work reports what it used so far (e.g. in tests). */
35
+ usageReportIntervalMs?: number;
25
36
  }
37
+ /**
38
+ * How an execution reports its own run to the platform: it claims the run
39
+ * before doing the work, says it is alive while it does, and settles it with
40
+ * the outcome. Every call carries the execution's AGENT token, which the
41
+ * platform minted for this run and which names it — which run is never the
42
+ * execution's to assert.
43
+ */
44
+ export interface RunLifecycle {
45
+ claim: (token: string) => Promise<{
46
+ claimed: boolean;
47
+ }>;
48
+ heartbeat: (token: string) => Promise<void>;
49
+ settle: (status: number, outcome: unknown, token: string) => Promise<{
50
+ settled: boolean;
51
+ }>;
52
+ }
53
+ /**
54
+ * Environment a Cloud Run JOB execution is started with by the platform
55
+ * (stackfactor-batch-jobs, agentJobExecutions.ts): references only — the
56
+ * event record, the integration, the tenant, and a token minted for this
57
+ * execution alone. Their presence is what switches {@link serve} into run-once
58
+ * mode; the payload and the tenant's configuration are fetched, never passed.
59
+ */
60
+ export declare const EXECUTION_ENV: {
61
+ readonly BATCH_JOB_ID: "SF_EXECUTION_BATCH_JOB_ID";
62
+ readonly INTEGRATION_ID: "SF_EXECUTION_INTEGRATION_ID";
63
+ readonly TENANT_ID: "SF_EXECUTION_TENANT_ID";
64
+ readonly TOKEN: "SF_EXECUTION_TOKEN";
65
+ };
66
+ /** True when this process was started as a job execution rather than a service. */
67
+ export declare const isRunOnceExecution: (env?: NodeJS.ProcessEnv) => boolean;
68
+ /**
69
+ * True for the one kind of run that is ALLOWED to carry no StackFactor token: a
70
+ * delivery to a webhook declared `requireToken: false`. There the platform
71
+ * authenticates nobody and passes the provider's request to the integration as
72
+ * it arrived, so there is no token to validate and no session to build — the
73
+ * handler verifies its provider itself, and brings its own API key (from its
74
+ * configuration) if it has to act on the platform.
75
+ *
76
+ * This does not open the agent up. The Execute RPC is reachable only by callers
77
+ * holding Cloud Run's invoker role — the backend and the batch worker — so a
78
+ * request with no token means the platform CHOSE to send none, and it only ever
79
+ * does that for such a webhook. A token that IS present is always validated,
80
+ * whatever the event, and every other event still requires one.
81
+ */
82
+ export declare const isAnonymousWebhookRun: (event: any, request: any) => boolean;
83
+ /**
84
+ * True for the runs that are ALLOWED to carry no StackFactor token, because
85
+ * the platform starts them with no identity on purpose:
86
+ * - a delivery to a `requireToken: false` webhook (isAnonymousWebhookRun);
87
+ * - a scheduled run: the clock is nobody.
88
+ * Both run with `session: null`. Since backend-core's decision record §11 the
89
+ * platform starts both with a token of their own (the integration's managed
90
+ * account in the tenant), so against a current platform neither is ever
91
+ * tokenless and this answers false; it stays for a platform that predates
92
+ * that, and can go once none does. The reasoning of isAnonymousWebhookRun holds
93
+ * for both: only the platform can reach Execute or start an execution, so no
94
+ * token means the platform chose to send none. A token that IS present is
95
+ * always validated, and every other event still requires one.
96
+ */
97
+ export declare const isAnonymousRun: (event: any, request: any) => boolean;
98
+ /**
99
+ * Run-once mode: the same agent, started as one Cloud Run JOB execution for
100
+ * one platform event (`runAs: job`). Fetches the execution's envelope from the
101
+ * backend with the execution's token — exactly what a service call would have
102
+ * received in its Execute request — binds the same {@link RequestContext} the
103
+ * gRPC path binds, runs the handler once, and exits: 0 on success, 1 on any
104
+ * failure so Cloud Run retries the task. SIGTERM (Cloud Run cancelling the
105
+ * execution or its task timeout) aborts the in-flight work.
106
+ *
107
+ * The execution reports its own run: it claims it before doing the work (an
108
+ * execution that loses the claim exits 0 without repeating it), says it is
109
+ * alive while it runs, and settles it with the outcome. The handler is
110
+ * written once and does not know which runtime it is on, nor that any of this
111
+ * is happening.
112
+ */
113
+ export declare const runOnce: (main: AgentMain, options?: ServeOptions) => Promise<void>;
26
114
  /**
27
115
  * Start a gRPC server that exposes `main` as the fleet-standard `Agent.Execute`
28
116
  * RPC. Per request it binds a {@link RequestContext} (so the agent's globals are
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AA+PtC;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,GAAG,KACT,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B;AAyHD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,IAAI,CAAC,MA8MP,CAAC"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AAWtC,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,YAAY,CAAC;AAwPpB;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,GAAG,KACT,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,uEAAuE;IACvE,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IACpE,sEAAsE;IACtE,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB,iEAAiE;IACjE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,mFAAmF;IACnF,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,mFAAmF;IACnF,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACxD,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,MAAM,EAAE,CACN,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,KACV,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACpC;AAKD;;;;;;GAMG;AACH,eAAO,MAAM,aAAa;;;;;CAKhB,CAAC;AAEX,mFAAmF;AACnF,eAAO,MAAM,kBAAkB,GAAI,MAAK,MAAM,CAAC,UAAwB,KAAG,OACJ,CAAC;AAKvE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO,GAAG,EAAE,SAAS,GAAG,KAAG,OAEhB,CAAC;AAKlD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,GAAG,EAAE,SAAS,GAAG,KAAG,OAGN,CAAC;AA4JrD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,OAAO,GAClB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,OAAO,CAAC,IAAI,CA8Id,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,IAAI,CAAC,MAyOP,CAAC"}
package/dist/cjs/serve.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.serve = void 0;
39
+ exports.serve = exports.runOnce = exports.isAnonymousRun = exports.isAnonymousWebhookRun = exports.isRunOnceExecution = exports.EXECUTION_ENV = void 0;
40
40
  const grpc = __importStar(require("@grpc/grpc-js"));
41
41
  const node_fs_1 = require("node:fs");
42
42
  const node_path_1 = require("node:path");
@@ -45,6 +45,7 @@ const client_api_1 = require("@stackfactor/client-api");
45
45
  const logger_js_1 = __importDefault(require("./logger.js"));
46
46
  const agentProto_js_1 = require("./agentProto.js");
47
47
  const runtimeContext_js_1 = require("./runtimeContext.js");
48
+ const usage_js_1 = require("./usage.js");
48
49
  // Genuine dynamic `import()` that survives TypeScript's CJS downlevelling (which
49
50
  // would otherwise turn `import()` into `require()` and break file:// URLs and
50
51
  // ESM check modules). Built once; see the call site in runSelfCheck.
@@ -234,6 +235,63 @@ const runSelfCheck = async (checkCode, config, session) => {
234
235
  clearTimeout(timer);
235
236
  }
236
237
  };
238
+ /** How often a running execution says it is still alive. */
239
+ const HEARTBEAT_MS = 30_000;
240
+ /**
241
+ * Environment a Cloud Run JOB execution is started with by the platform
242
+ * (stackfactor-batch-jobs, agentJobExecutions.ts): references only — the
243
+ * event record, the integration, the tenant, and a token minted for this
244
+ * execution alone. Their presence is what switches {@link serve} into run-once
245
+ * mode; the payload and the tenant's configuration are fetched, never passed.
246
+ */
247
+ exports.EXECUTION_ENV = {
248
+ BATCH_JOB_ID: "SF_EXECUTION_BATCH_JOB_ID",
249
+ INTEGRATION_ID: "SF_EXECUTION_INTEGRATION_ID",
250
+ TENANT_ID: "SF_EXECUTION_TENANT_ID",
251
+ TOKEN: "SF_EXECUTION_TOKEN",
252
+ };
253
+ /** True when this process was started as a job execution rather than a service. */
254
+ const isRunOnceExecution = (env = process.env) => Boolean(env[exports.EXECUTION_ENV.BATCH_JOB_ID] && env[exports.EXECUTION_ENV.TOKEN]);
255
+ exports.isRunOnceExecution = isRunOnceExecution;
256
+ /** `ExecuteRequest.event` of an inbound webhook delivery (backend-core EVENT_TYPE.WEBHOOK_RECEIVED). */
257
+ const WEBHOOK_RECEIVED = 350;
258
+ /**
259
+ * True for the one kind of run that is ALLOWED to carry no StackFactor token: a
260
+ * delivery to a webhook declared `requireToken: false`. There the platform
261
+ * authenticates nobody and passes the provider's request to the integration as
262
+ * it arrived, so there is no token to validate and no session to build — the
263
+ * handler verifies its provider itself, and brings its own API key (from its
264
+ * configuration) if it has to act on the platform.
265
+ *
266
+ * This does not open the agent up. The Execute RPC is reachable only by callers
267
+ * holding Cloud Run's invoker role — the backend and the batch worker — so a
268
+ * request with no token means the platform CHOSE to send none, and it only ever
269
+ * does that for such a webhook. A token that IS present is always validated,
270
+ * whatever the event, and every other event still requires one.
271
+ */
272
+ const isAnonymousWebhookRun = (event, request) => Number(event) === WEBHOOK_RECEIVED &&
273
+ !(request?.authToken ?? request?.authorization);
274
+ exports.isAnonymousWebhookRun = isAnonymousWebhookRun;
275
+ /** `ExecuteRequest.event` of a scheduled run (backend-core EVENT_TYPE.SCHEDULED_RUN). */
276
+ const SCHEDULED_RUN = 510;
277
+ /**
278
+ * True for the runs that are ALLOWED to carry no StackFactor token, because
279
+ * the platform starts them with no identity on purpose:
280
+ * - a delivery to a `requireToken: false` webhook (isAnonymousWebhookRun);
281
+ * - a scheduled run: the clock is nobody.
282
+ * Both run with `session: null`. Since backend-core's decision record §11 the
283
+ * platform starts both with a token of their own (the integration's managed
284
+ * account in the tenant), so against a current platform neither is ever
285
+ * tokenless and this answers false; it stays for a platform that predates
286
+ * that, and can go once none does. The reasoning of isAnonymousWebhookRun holds
287
+ * for both: only the platform can reach Execute or start an execution, so no
288
+ * token means the platform chose to send none. A token that IS present is
289
+ * always validated, and every other event still requires one.
290
+ */
291
+ const isAnonymousRun = (event, request) => (0, exports.isAnonymousWebhookRun)(event, request) ||
292
+ (Number(event) === SCHEDULED_RUN &&
293
+ !(request?.authToken ?? request?.authorization));
294
+ exports.isAnonymousRun = isAnonymousRun;
237
295
  /** Marks an auth failure so the handler can map it to gRPC UNAUTHENTICATED. */
238
296
  class UnauthenticatedError extends Error {
239
297
  }
@@ -336,6 +394,150 @@ const toErrorFrame = (error) => {
336
394
  }
337
395
  return { code: 500, message: String(error?.message ?? error) };
338
396
  };
397
+ /** How the platform records a settled run (backend-core BATCH_JOB.STATUS). */
398
+ const RUN_STATUS = { COMPLETED: 2, FAILED: 3 };
399
+ /**
400
+ * Settles the run this execution claimed, if it claimed one. Never throws and
401
+ * never changes the execution's own outcome: the work either happened or it
402
+ * did not, and a platform that cannot be told so right now will time the run
403
+ * out on its own. `settled: false` means it already did.
404
+ */
405
+ const settleRun = async (claimed, lifecycle, status, outcome, batchJobId) => {
406
+ if (!claimed)
407
+ return;
408
+ try {
409
+ const answer = await lifecycle.settle(status, outcome ?? null, claimed.token);
410
+ if (!answer?.settled) {
411
+ logger_js_1.default.log(null, logger_js_1.default.levels.warn, `run-once execution for event record ${batchJobId}: the run was no longer running, so its outcome was not recorded`);
412
+ }
413
+ }
414
+ catch (error) {
415
+ logger_js_1.default.log(null, logger_js_1.default.levels.warn, `run-once execution for event record ${batchJobId}: settling the run failed: ${error?.message ?? error}`);
416
+ }
417
+ };
418
+ /**
419
+ * Run-once mode: the same agent, started as one Cloud Run JOB execution for
420
+ * one platform event (`runAs: job`). Fetches the execution's envelope from the
421
+ * backend with the execution's token — exactly what a service call would have
422
+ * received in its Execute request — binds the same {@link RequestContext} the
423
+ * gRPC path binds, runs the handler once, and exits: 0 on success, 1 on any
424
+ * failure so Cloud Run retries the task. SIGTERM (Cloud Run cancelling the
425
+ * execution or its task timeout) aborts the in-flight work.
426
+ *
427
+ * The execution reports its own run: it claims it before doing the work (an
428
+ * execution that loses the claim exits 0 without repeating it), says it is
429
+ * alive while it runs, and settles it with the outcome. The handler is
430
+ * written once and does not know which runtime it is on, nor that any of this
431
+ * is happening.
432
+ */
433
+ const runOnce = async (main, options = {}) => {
434
+ (0, runtimeContext_js_1.installGlobals)();
435
+ const exit = options.exit ?? ((code) => process.exit(code));
436
+ const batchJobId = process.env[exports.EXECUTION_ENV.BATCH_JOB_ID] ?? "";
437
+ const token = process.env[exports.EXECUTION_ENV.TOKEN] ?? "";
438
+ const fetchEnvelope = options.fetchEnvelope ??
439
+ ((id, tok) => client_api_1.integration.getExecutionEnvelope(id, tok));
440
+ const authenticate = options.authenticate ?? defaultAuthenticate;
441
+ const abort = new AbortController();
442
+ const onSignal = () => abort.abort();
443
+ process.once("SIGTERM", onSignal);
444
+ process.once("SIGINT", onSignal);
445
+ const lifecycle = options.lifecycle ?? {
446
+ claim: client_api_1.integration.claimExecution,
447
+ heartbeat: client_api_1.integration.heartbeatExecution,
448
+ settle: client_api_1.integration.settleExecution,
449
+ };
450
+ // Set once the run is ours: only an execution that claimed a run settles it.
451
+ let claimed = null;
452
+ let heartbeat = null;
453
+ // What the run used is reported before it is settled, however it ended:
454
+ // settling revokes the token the report is made with.
455
+ let reportWhatWasUsed = async () => { };
456
+ let stopReporting = () => { };
457
+ try {
458
+ const req = await fetchEnvelope(batchJobId, token);
459
+ const request = safeParse(req?.request_json, {});
460
+ // A scheduled run carries no identity by design: see isAnonymousRun.
461
+ const session = (0, exports.isAnonymousRun)(req?.event, request)
462
+ ? null
463
+ : await authenticate(request);
464
+ const context = {
465
+ config: safeParse(req?.config_json, {}),
466
+ params: {
467
+ contentType: req?.content_type,
468
+ data: safeParse(req?.data_json, {}),
469
+ },
470
+ request: { ...request, session },
471
+ event: req?.event ?? 0,
472
+ onProgress: (update) => {
473
+ logger_js_1.default.log(null, logger_js_1.default.levels.info, `progress ${Math.round(update?.progress ?? 0)}%: ${update?.message ?? ""}`);
474
+ },
475
+ signal: abort.signal,
476
+ session,
477
+ usage: new usage_js_1.UsageLedger(),
478
+ };
479
+ reportWhatWasUsed = () => (0, usage_js_1.reportUsage)(context, { report: options.reportUsage, last: true });
480
+ let entry = main;
481
+ const entryPath = String(req?.code ?? "").trim();
482
+ if (entryPath) {
483
+ const loaded = await loadAgentModule(entryPath, "handler");
484
+ if (loaded.reason) {
485
+ throw new Error(`entry module "${entryPath}" could not be loaded (${loaded.reason}): ${loaded.detail}`);
486
+ }
487
+ entry = loaded.fn;
488
+ }
489
+ // Claim before doing anything. Of however many executions the platform may
490
+ // start for one run — a retried task, a redelivered message — exactly one
491
+ // wins; the others exit 0 without repeating the work, which is not a
492
+ // failure and must not be retried. The execution's token names its run
493
+ // (every run, scheduled ones included, is started with one), so nothing
494
+ // else is sent.
495
+ const answer = await lifecycle.claim(token);
496
+ if (!answer?.claimed) {
497
+ logger_js_1.default.log(null, logger_js_1.default.levels.info, `run-once execution for event record ${batchJobId}: another execution already claimed this run, nothing to do`);
498
+ exit(0);
499
+ return;
500
+ }
501
+ claimed = { token };
502
+ heartbeat = setInterval(() => {
503
+ void lifecycle
504
+ .heartbeat(token)
505
+ .catch((error) => logger_js_1.default.log(null, logger_js_1.default.levels.warn, `run-once execution for event record ${batchJobId}: heartbeat failed: ${error?.message ?? error}`));
506
+ }, HEARTBEAT_MS);
507
+ heartbeat.unref?.();
508
+ stopReporting = (0, usage_js_1.reportUsageWhileRunning)(context, {
509
+ report: options.reportUsage,
510
+ intervalMs: options.usageReportIntervalMs,
511
+ });
512
+ logger_js_1.default.log(null, logger_js_1.default.levels.info, `run-once execution for event record ${batchJobId} (${req?.content_type ?? ""}) starting`);
513
+ const result = await (0, runtimeContext_js_1.runWithContext)(context, () => entry(abort.signal));
514
+ logger_js_1.default.log(null, logger_js_1.default.levels.info, `run-once execution for event record ${batchJobId} completed: ${JSON.stringify(result ?? null).slice(0, 500)}`);
515
+ stopReporting();
516
+ await reportWhatWasUsed();
517
+ await settleRun(claimed, lifecycle, RUN_STATUS.COMPLETED, result, batchJobId);
518
+ exit(0);
519
+ }
520
+ catch (error) {
521
+ const frame = toErrorFrame(error);
522
+ logger_js_1.default.log(null, logger_js_1.default.levels.error, `run-once execution for event record ${batchJobId} failed${abort.signal.aborted ? " (aborted)" : ""}: ${frame.code} ${frame.message}`);
523
+ // Work that failed still used what it used.
524
+ stopReporting();
525
+ await reportWhatWasUsed();
526
+ // The run is ours to close whether it went well or not: a run left
527
+ // running is one the platform has to time out, and its credential lives
528
+ // until it does.
529
+ await settleRun(claimed, lifecycle, RUN_STATUS.FAILED, frame, batchJobId);
530
+ exit(1);
531
+ }
532
+ finally {
533
+ stopReporting();
534
+ if (heartbeat)
535
+ clearInterval(heartbeat);
536
+ process.off("SIGTERM", onSignal);
537
+ process.off("SIGINT", onSignal);
538
+ }
539
+ };
540
+ exports.runOnce = runOnce;
339
541
  /**
340
542
  * Start a gRPC server that exposes `main` as the fleet-standard `Agent.Execute`
341
543
  * RPC. Per request it binds a {@link RequestContext} (so the agent's globals are
@@ -348,6 +550,12 @@ const toErrorFrame = (error) => {
348
550
  * serve(require("./src/main"));
349
551
  */
350
552
  const serve = (main, options = {}) => {
553
+ // Started as a job execution: run the handler once and exit instead of
554
+ // serving. Same image, same entrypoint; the environment decides.
555
+ if ((0, exports.isRunOnceExecution)()) {
556
+ void (0, exports.runOnce)(main, options);
557
+ return new grpc.Server();
558
+ }
351
559
  (0, runtimeContext_js_1.installGlobals)();
352
560
  const port = options.port ?? (Number(process.env.PORT) || 8080);
353
561
  const host = options.host ?? "0.0.0.0";
@@ -370,9 +578,13 @@ const serve = (main, options = {}) => {
370
578
  };
371
579
  const handle = async () => {
372
580
  // Authenticate first — refuse the call when the token is missing/invalid.
373
- let session;
581
+ // The exceptions carry no token by design and run with no session: see
582
+ // isAnonymousRun.
583
+ let session = null;
374
584
  try {
375
- session = await authenticate(request);
585
+ if (!(0, exports.isAnonymousRun)(req.event, request)) {
586
+ session = await authenticate(request);
587
+ }
376
588
  }
377
589
  catch (error) {
378
590
  const message = error?.message ?? "Unauthenticated";
@@ -397,6 +609,7 @@ const serve = (main, options = {}) => {
397
609
  onProgress,
398
610
  signal: abort.signal,
399
611
  session,
612
+ usage: new usage_js_1.UsageLedger(),
400
613
  };
401
614
  // Which function actually runs. Default is the agent's own `main` — the
402
615
  // path every caller took before `code` existed, and still the path taken
@@ -427,8 +640,21 @@ const serve = (main, options = {}) => {
427
640
  }
428
641
  entry = loaded.fn;
429
642
  }
643
+ // What the call used is reported before it is answered, however it
644
+ // ended — a call that failed, or that its caller gave up on, still used
645
+ // what it used — because the platform revokes the call's token as soon
646
+ // as it has the answer.
647
+ const stopReporting = (0, usage_js_1.reportUsageWhileRunning)(context, {
648
+ report: options.reportUsage,
649
+ intervalMs: options.usageReportIntervalMs,
650
+ });
651
+ const reportWhatWasUsed = async () => {
652
+ stopReporting();
653
+ await (0, usage_js_1.reportUsage)(context, { report: options.reportUsage, last: true });
654
+ };
430
655
  try {
431
656
  const result = await (0, runtimeContext_js_1.runWithContext)(context, () => entry(abort.signal));
657
+ await reportWhatWasUsed();
432
658
  if (!abort.signal.aborted) {
433
659
  call.write({
434
660
  result: { result_json: JSON.stringify(result ?? null) },
@@ -436,6 +662,7 @@ const serve = (main, options = {}) => {
436
662
  }
437
663
  }
438
664
  catch (error) {
665
+ await reportWhatWasUsed();
439
666
  if (!abort.signal.aborted) {
440
667
  logger_js_1.default.log(null, logger_js_1.default.levels.error, `agent Execute failed: ${error?.message ?? error}`);
441
668
  call.write({ error: toErrorFrame(error) });
@@ -0,0 +1,182 @@
1
+ /** What a STACKFACTOR reference to a knowledge base document carries (backend-core REFERENCE). */
2
+ export declare const STACKFACTOR_REFERENCE: Readonly<{
3
+ KIND: "STACKFACTOR";
4
+ TARGET_ELEMENT_TYPE: "KNOWLEDGE_BASE_DOCUMENT";
5
+ ORIGIN: "KB_EXTRACTION";
6
+ VISIBILITY: "EDIT";
7
+ }>;
8
+ /** How many documents one search may be kept to (the backend's bound); more are split. */
9
+ export declare const SEARCH_DOCUMENT_IDS_MAX = 500;
10
+ /** How long a query may be (the backend's bound); longer ones are cut. */
11
+ export declare const QUERY_MAX_LENGTH = 4000;
12
+ /** How many passages one search asks for when the caller does not say. */
13
+ export declare const DEFAULT_TOP_K = 8;
14
+ /** A chunk as the platform's search answers it (client-api `RetrievedChunk`). */
15
+ export interface KnowledgeChunk {
16
+ _id: string;
17
+ knowledgeBaseId?: string;
18
+ documentId: string;
19
+ documentName?: string | null;
20
+ sectionTitle?: string | null;
21
+ pageNumber?: number | null;
22
+ text?: string;
23
+ skills?: {
24
+ name: string | null;
25
+ microSkills?: string[];
26
+ }[];
27
+ score?: number | null;
28
+ [field: string]: unknown;
29
+ }
30
+ /** The platform's search, as client-api's `knowledgeBaseAgent.search` is called. */
31
+ export type KnowledgeSearch = (knowledgeBaseId: string, query: string, options: {
32
+ topK?: number;
33
+ documentIds?: string[];
34
+ }, token: string) => Promise<{
35
+ chunks: KnowledgeChunk[];
36
+ }>;
37
+ /** Where to search: a whole knowledge base, or only some of its documents. */
38
+ export interface KnowledgeSource {
39
+ knowledgeBaseId: string;
40
+ /** Only these documents; omitted, the whole knowledge base. */
41
+ documentIds?: string[];
42
+ }
43
+ /** A passage handed to the model, tagged so it can cite it. */
44
+ export interface GroundingPassage {
45
+ /** "k1", "k2"… in the order they rank. */
46
+ tag: string;
47
+ chunkId: string;
48
+ knowledgeBaseId: string;
49
+ documentId: string;
50
+ documentName: string | null;
51
+ sectionTitle: string | null;
52
+ pageNumber: number | null;
53
+ text: string;
54
+ /** The skills its passage teaches, as labelling named them. */
55
+ skills: {
56
+ name: string | null;
57
+ microSkills: string[];
58
+ }[];
59
+ score: number | null;
60
+ }
61
+ /** A knowledge base that could not be searched, and why. */
62
+ export interface SkippedSource {
63
+ knowledgeBaseId: string;
64
+ /** The HTTP status, when the platform answered one. */
65
+ status: number | null;
66
+ /** True when the person may not read it (or it is gone): expected, not a fault. */
67
+ noAccess: boolean;
68
+ message: string;
69
+ }
70
+ /** A STACKFACTOR reference to the knowledge base document a passage came from. */
71
+ export interface StackFactorReference {
72
+ kind: "STACKFACTOR";
73
+ title: string;
74
+ description: string | null;
75
+ target: {
76
+ elementType: "KNOWLEDGE_BASE_DOCUMENT";
77
+ elementId: string;
78
+ parentId: string;
79
+ };
80
+ knowledgeBase: {
81
+ knowledgeBaseId: string;
82
+ documentId: string;
83
+ chunkId: string;
84
+ sectionTitle: string | null;
85
+ };
86
+ origin: "KB_EXTRACTION";
87
+ visibility: "EDIT";
88
+ }
89
+ /**
90
+ * The knowledge bases, and the documents in them, that a list of references
91
+ * cites: its STACKFACTOR references to knowledge base documents, by their
92
+ * `knowledgeBase` ids, or by their `target` when only that is there. Any other
93
+ * reference is passed over. Each knowledge base once, its documents once each,
94
+ * in the order they first appear.
95
+ * @param references A skill's (or a microskill's) references
96
+ * @returns The sources to search
97
+ */
98
+ export declare const sourcesFromReferences: (references: unknown) => KnowledgeSource[];
99
+ /**
100
+ * The knowledge bases skill coverage found (client-api
101
+ * `knowledgeBase.getSkillCoverage`): every one the person can open that holds
102
+ * the skill, each searched whole.
103
+ * @param coverage The coverage answer, `{ knowledgeBases: [{ _id }] }`
104
+ * @returns The sources to search
105
+ */
106
+ export declare const sourcesFromCoverage: (coverage: unknown) => KnowledgeSource[];
107
+ /**
108
+ * Searches every source for every query, under the person's access, and hands
109
+ * back the passages found, best first, each once, tagged "k1", "k2"… for the
110
+ * model to cite. A source with documents is searched within them (in batches of
111
+ * SEARCH_DOCUMENT_IDS_MAX); one without, whole. A source the person may not
112
+ * read (401, 403, 404) is skipped: none of its passages are used and no more of
113
+ * its searches are made. A search that fails otherwise is reported and the
114
+ * passages its other searches found stand. Either way it is in `skipped`, once
115
+ * per knowledge base, and the passages of the others stand.
116
+ * @param options `sources`, `queries`, `search` (client-api
117
+ * `knowledgeBaseAgent.search`), `token` (the run's AGENT token), `topK`
118
+ * per search (default DEFAULT_TOP_K)
119
+ * @returns `{ passages, skipped }`
120
+ */
121
+ export declare const retrievePassages: (options: {
122
+ sources: KnowledgeSource[];
123
+ queries: string[];
124
+ search: KnowledgeSearch;
125
+ token: string;
126
+ topK?: number;
127
+ }) => Promise<{
128
+ passages: GroundingPassage[];
129
+ skipped: SkippedSource[];
130
+ }>;
131
+ /**
132
+ * The passages as a prompt shows them: tag, where each came from, and its text.
133
+ * @param passages From `retrievePassages`
134
+ * @returns `[{ tag, document, section, text }]`, for the model's input
135
+ */
136
+ export declare const passagesForPrompt: (passages: GroundingPassage[]) => {
137
+ tag: string;
138
+ document: string | null;
139
+ section: string | null;
140
+ text: string;
141
+ }[];
142
+ /**
143
+ * STACKFACTOR references for passages: one per passage, in their order, with no
144
+ * cap. With `tags`, only the passages the model cited by tag (any tag it made
145
+ * up is passed over); without, every passage.
146
+ * @param passages From `retrievePassages`
147
+ * @param tags The tags the model cited ("k3", "K3" and " k3 " alike)
148
+ * @returns The references, ready for a skill's, a microskill's or a question's `references`
149
+ */
150
+ export declare const referencesFor: (passages: GroundingPassage[], tags?: unknown[]) => StackFactorReference[];
151
+ declare const _default: {
152
+ DEFAULT_TOP_K: number;
153
+ QUERY_MAX_LENGTH: number;
154
+ SEARCH_DOCUMENT_IDS_MAX: number;
155
+ STACKFACTOR_REFERENCE: Readonly<{
156
+ KIND: "STACKFACTOR";
157
+ TARGET_ELEMENT_TYPE: "KNOWLEDGE_BASE_DOCUMENT";
158
+ ORIGIN: "KB_EXTRACTION";
159
+ VISIBILITY: "EDIT";
160
+ }>;
161
+ passagesForPrompt: (passages: GroundingPassage[]) => {
162
+ tag: string;
163
+ document: string | null;
164
+ section: string | null;
165
+ text: string;
166
+ }[];
167
+ referencesFor: (passages: GroundingPassage[], tags?: unknown[]) => StackFactorReference[];
168
+ retrievePassages: (options: {
169
+ sources: KnowledgeSource[];
170
+ queries: string[];
171
+ search: KnowledgeSearch;
172
+ token: string;
173
+ topK?: number;
174
+ }) => Promise<{
175
+ passages: GroundingPassage[];
176
+ skipped: SkippedSource[];
177
+ }>;
178
+ sourcesFromCoverage: (coverage: unknown) => KnowledgeSource[];
179
+ sourcesFromReferences: (references: unknown) => KnowledgeSource[];
180
+ };
181
+ export default _default;
182
+ //# sourceMappingURL=stackfactorKnowledge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stackfactorKnowledge.d.ts","sourceRoot":"","sources":["../../src/stackfactorKnowledge.ts"],"names":[],"mappings":"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"}