@stackfactor/agent-utils 1.4.3 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -6
- package/dist/cjs/index.d.ts +5 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +7 -1
- package/dist/cjs/langChain.d.ts +24 -4
- package/dist/cjs/langChain.d.ts.map +1 -1
- package/dist/cjs/langChain.js +218 -172
- package/dist/cjs/runtimeContext.d.ts +3 -0
- package/dist/cjs/runtimeContext.d.ts.map +1 -1
- package/dist/cjs/serve.d.ts +88 -0
- package/dist/cjs/serve.d.ts.map +1 -1
- package/dist/cjs/serve.js +230 -3
- package/dist/cjs/tavily.d.ts +4 -0
- package/dist/cjs/tavily.d.ts.map +1 -1
- package/dist/cjs/tavily.js +17 -18
- package/dist/cjs/usage.d.ts +74 -0
- package/dist/cjs/usage.d.ts.map +1 -0
- package/dist/cjs/usage.js +160 -0
- package/dist/esm/index.d.ts +5 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +4 -2
- package/dist/esm/langChain.d.ts +24 -4
- package/dist/esm/langChain.d.ts.map +1 -1
- package/dist/esm/langChain.js +218 -172
- package/dist/esm/runtimeContext.d.ts +3 -0
- package/dist/esm/runtimeContext.d.ts.map +1 -1
- package/dist/esm/serve.d.ts +88 -0
- package/dist/esm/serve.d.ts.map +1 -1
- package/dist/esm/serve.js +226 -3
- package/dist/esm/tavily.d.ts +4 -0
- package/dist/esm/tavily.d.ts.map +1 -1
- package/dist/esm/tavily.js +17 -18
- package/dist/esm/usage.d.ts +74 -0
- package/dist/esm/usage.d.ts.map +1 -0
- package/dist/esm/usage.js +150 -0
- package/package.json +9 -4
package/dist/esm/serve.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as grpc from "@grpc/grpc-js";
|
|
2
|
+
import { type ReportUsage } from "./usage.js";
|
|
2
3
|
/**
|
|
3
4
|
* The agent entry point. Reads `params` / `config` / `onProgress` from globals
|
|
4
5
|
* (populated per-request by {@link serve}) and returns the structured result.
|
|
@@ -22,7 +23,94 @@ export interface ServeOptions {
|
|
|
22
23
|
host?: string;
|
|
23
24
|
/** Override the auth check (e.g. in tests). Defaults to {@link defaultAuthenticate}. */
|
|
24
25
|
authenticate?: Authenticator;
|
|
26
|
+
/** Run-once mode only: override the envelope fetch (e.g. in tests). */
|
|
27
|
+
fetchEnvelope?: (batchJobId: string, token: string) => Promise<any>;
|
|
28
|
+
/** Run-once mode only: override the run lifecycle (e.g. in tests). */
|
|
29
|
+
lifecycle?: RunLifecycle;
|
|
30
|
+
/** Run-once mode only: override process exit (e.g. in tests). */
|
|
31
|
+
exit?: (code: number) => void;
|
|
32
|
+
/** Override how what the work used is reported to the platform (e.g. in tests). */
|
|
33
|
+
reportUsage?: ReportUsage;
|
|
34
|
+
/** Override how often running work reports what it used so far (e.g. in tests). */
|
|
35
|
+
usageReportIntervalMs?: number;
|
|
25
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* How an execution reports its own run to the platform: it claims the run
|
|
39
|
+
* before doing the work, says it is alive while it does, and settles it with
|
|
40
|
+
* the outcome. Every call carries the execution's AGENT token, which the
|
|
41
|
+
* platform minted for this run and which names it — which run is never the
|
|
42
|
+
* execution's to assert.
|
|
43
|
+
*/
|
|
44
|
+
export interface RunLifecycle {
|
|
45
|
+
claim: (token: string) => Promise<{
|
|
46
|
+
claimed: boolean;
|
|
47
|
+
}>;
|
|
48
|
+
heartbeat: (token: string) => Promise<void>;
|
|
49
|
+
settle: (status: number, outcome: unknown, token: string) => Promise<{
|
|
50
|
+
settled: boolean;
|
|
51
|
+
}>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Environment a Cloud Run JOB execution is started with by the platform
|
|
55
|
+
* (stackfactor-batch-jobs, agentJobExecutions.ts): references only — the
|
|
56
|
+
* event record, the integration, the tenant, and a token minted for this
|
|
57
|
+
* execution alone. Their presence is what switches {@link serve} into run-once
|
|
58
|
+
* mode; the payload and the tenant's configuration are fetched, never passed.
|
|
59
|
+
*/
|
|
60
|
+
export declare const EXECUTION_ENV: {
|
|
61
|
+
readonly BATCH_JOB_ID: "SF_EXECUTION_BATCH_JOB_ID";
|
|
62
|
+
readonly INTEGRATION_ID: "SF_EXECUTION_INTEGRATION_ID";
|
|
63
|
+
readonly TENANT_ID: "SF_EXECUTION_TENANT_ID";
|
|
64
|
+
readonly TOKEN: "SF_EXECUTION_TOKEN";
|
|
65
|
+
};
|
|
66
|
+
/** True when this process was started as a job execution rather than a service. */
|
|
67
|
+
export declare const isRunOnceExecution: (env?: NodeJS.ProcessEnv) => boolean;
|
|
68
|
+
/**
|
|
69
|
+
* True for the one kind of run that is ALLOWED to carry no StackFactor token: a
|
|
70
|
+
* delivery to a webhook declared `requireToken: false`. There the platform
|
|
71
|
+
* authenticates nobody and passes the provider's request to the integration as
|
|
72
|
+
* it arrived, so there is no token to validate and no session to build — the
|
|
73
|
+
* handler verifies its provider itself, and brings its own API key (from its
|
|
74
|
+
* configuration) if it has to act on the platform.
|
|
75
|
+
*
|
|
76
|
+
* This does not open the agent up. The Execute RPC is reachable only by callers
|
|
77
|
+
* holding Cloud Run's invoker role — the backend and the batch worker — so a
|
|
78
|
+
* request with no token means the platform CHOSE to send none, and it only ever
|
|
79
|
+
* does that for such a webhook. A token that IS present is always validated,
|
|
80
|
+
* whatever the event, and every other event still requires one.
|
|
81
|
+
*/
|
|
82
|
+
export declare const isAnonymousWebhookRun: (event: any, request: any) => boolean;
|
|
83
|
+
/**
|
|
84
|
+
* True for the runs that are ALLOWED to carry no StackFactor token, because
|
|
85
|
+
* the platform starts them with no identity on purpose:
|
|
86
|
+
* - a delivery to a `requireToken: false` webhook (isAnonymousWebhookRun);
|
|
87
|
+
* - a scheduled run: the clock is nobody.
|
|
88
|
+
* Both run with `session: null`. Since backend-core's decision record §11 the
|
|
89
|
+
* platform starts both with a token of their own (the integration's managed
|
|
90
|
+
* account in the tenant), so against a current platform neither is ever
|
|
91
|
+
* tokenless and this answers false; it stays for a platform that predates
|
|
92
|
+
* that, and can go once none does. The reasoning of isAnonymousWebhookRun holds
|
|
93
|
+
* for both: only the platform can reach Execute or start an execution, so no
|
|
94
|
+
* token means the platform chose to send none. A token that IS present is
|
|
95
|
+
* always validated, and every other event still requires one.
|
|
96
|
+
*/
|
|
97
|
+
export declare const isAnonymousRun: (event: any, request: any) => boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Run-once mode: the same agent, started as one Cloud Run JOB execution for
|
|
100
|
+
* one platform event (`runAs: job`). Fetches the execution's envelope from the
|
|
101
|
+
* backend with the execution's token — exactly what a service call would have
|
|
102
|
+
* received in its Execute request — binds the same {@link RequestContext} the
|
|
103
|
+
* gRPC path binds, runs the handler once, and exits: 0 on success, 1 on any
|
|
104
|
+
* failure so Cloud Run retries the task. SIGTERM (Cloud Run cancelling the
|
|
105
|
+
* execution or its task timeout) aborts the in-flight work.
|
|
106
|
+
*
|
|
107
|
+
* The execution reports its own run: it claims it before doing the work (an
|
|
108
|
+
* execution that loses the claim exits 0 without repeating it), says it is
|
|
109
|
+
* alive while it runs, and settles it with the outcome. The handler is
|
|
110
|
+
* written once and does not know which runtime it is on, nor that any of this
|
|
111
|
+
* is happening.
|
|
112
|
+
*/
|
|
113
|
+
export declare const runOnce: (main: AgentMain, options?: ServeOptions) => Promise<void>;
|
|
26
114
|
/**
|
|
27
115
|
* Start a gRPC server that exposes `main` as the fleet-standard `Agent.Execute`
|
|
28
116
|
* RPC. Per request it binds a {@link RequestContext} (so the agent's globals are
|
package/dist/esm/serve.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AAWtC,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,YAAY,CAAC;AAwPpB;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,GAAG,KACT,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,uEAAuE;IACvE,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IACpE,sEAAsE;IACtE,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB,iEAAiE;IACjE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,mFAAmF;IACnF,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,mFAAmF;IACnF,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACxD,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,MAAM,EAAE,CACN,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,KACV,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACpC;AAKD;;;;;;GAMG;AACH,eAAO,MAAM,aAAa;;;;;CAKhB,CAAC;AAEX,mFAAmF;AACnF,eAAO,MAAM,kBAAkB,GAAI,MAAK,MAAM,CAAC,UAAwB,KAAG,OACJ,CAAC;AAKvE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO,GAAG,EAAE,SAAS,GAAG,KAAG,OAEhB,CAAC;AAKlD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,GAAG,EAAE,SAAS,GAAG,KAAG,OAGN,CAAC;AA4JrD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,OAAO,GAClB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,OAAO,CAAC,IAAI,CA8Id,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,IAAI,CAAC,MAyOP,CAAC"}
|
package/dist/esm/serve.js
CHANGED
|
@@ -2,10 +2,11 @@ import * as grpc from "@grpc/grpc-js";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { resolve, relative, isAbsolute } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
import { session as clientSession } from "@stackfactor/client-api";
|
|
5
|
+
import { integration as clientIntegration, session as clientSession, } from "@stackfactor/client-api";
|
|
6
6
|
import logger from "./logger.js";
|
|
7
7
|
import { loadAgentService } from "./agentProto.js";
|
|
8
8
|
import { runWithContext, installGlobals } from "./runtimeContext.js";
|
|
9
|
+
import { UsageLedger, reportUsage, reportUsageWhileRunning, } from "./usage.js";
|
|
9
10
|
// Genuine dynamic `import()` that survives TypeScript's CJS downlevelling (which
|
|
10
11
|
// would otherwise turn `import()` into `require()` and break file:// URLs and
|
|
11
12
|
// ESM check modules). Built once; see the call site in runSelfCheck.
|
|
@@ -195,6 +196,60 @@ const runSelfCheck = async (checkCode, config, session) => {
|
|
|
195
196
|
clearTimeout(timer);
|
|
196
197
|
}
|
|
197
198
|
};
|
|
199
|
+
/** How often a running execution says it is still alive. */
|
|
200
|
+
const HEARTBEAT_MS = 30_000;
|
|
201
|
+
/**
|
|
202
|
+
* Environment a Cloud Run JOB execution is started with by the platform
|
|
203
|
+
* (stackfactor-batch-jobs, agentJobExecutions.ts): references only — the
|
|
204
|
+
* event record, the integration, the tenant, and a token minted for this
|
|
205
|
+
* execution alone. Their presence is what switches {@link serve} into run-once
|
|
206
|
+
* mode; the payload and the tenant's configuration are fetched, never passed.
|
|
207
|
+
*/
|
|
208
|
+
export const EXECUTION_ENV = {
|
|
209
|
+
BATCH_JOB_ID: "SF_EXECUTION_BATCH_JOB_ID",
|
|
210
|
+
INTEGRATION_ID: "SF_EXECUTION_INTEGRATION_ID",
|
|
211
|
+
TENANT_ID: "SF_EXECUTION_TENANT_ID",
|
|
212
|
+
TOKEN: "SF_EXECUTION_TOKEN",
|
|
213
|
+
};
|
|
214
|
+
/** True when this process was started as a job execution rather than a service. */
|
|
215
|
+
export const isRunOnceExecution = (env = process.env) => Boolean(env[EXECUTION_ENV.BATCH_JOB_ID] && env[EXECUTION_ENV.TOKEN]);
|
|
216
|
+
/** `ExecuteRequest.event` of an inbound webhook delivery (backend-core EVENT_TYPE.WEBHOOK_RECEIVED). */
|
|
217
|
+
const WEBHOOK_RECEIVED = 350;
|
|
218
|
+
/**
|
|
219
|
+
* True for the one kind of run that is ALLOWED to carry no StackFactor token: a
|
|
220
|
+
* delivery to a webhook declared `requireToken: false`. There the platform
|
|
221
|
+
* authenticates nobody and passes the provider's request to the integration as
|
|
222
|
+
* it arrived, so there is no token to validate and no session to build — the
|
|
223
|
+
* handler verifies its provider itself, and brings its own API key (from its
|
|
224
|
+
* configuration) if it has to act on the platform.
|
|
225
|
+
*
|
|
226
|
+
* This does not open the agent up. The Execute RPC is reachable only by callers
|
|
227
|
+
* holding Cloud Run's invoker role — the backend and the batch worker — so a
|
|
228
|
+
* request with no token means the platform CHOSE to send none, and it only ever
|
|
229
|
+
* does that for such a webhook. A token that IS present is always validated,
|
|
230
|
+
* whatever the event, and every other event still requires one.
|
|
231
|
+
*/
|
|
232
|
+
export const isAnonymousWebhookRun = (event, request) => Number(event) === WEBHOOK_RECEIVED &&
|
|
233
|
+
!(request?.authToken ?? request?.authorization);
|
|
234
|
+
/** `ExecuteRequest.event` of a scheduled run (backend-core EVENT_TYPE.SCHEDULED_RUN). */
|
|
235
|
+
const SCHEDULED_RUN = 510;
|
|
236
|
+
/**
|
|
237
|
+
* True for the runs that are ALLOWED to carry no StackFactor token, because
|
|
238
|
+
* the platform starts them with no identity on purpose:
|
|
239
|
+
* - a delivery to a `requireToken: false` webhook (isAnonymousWebhookRun);
|
|
240
|
+
* - a scheduled run: the clock is nobody.
|
|
241
|
+
* Both run with `session: null`. Since backend-core's decision record §11 the
|
|
242
|
+
* platform starts both with a token of their own (the integration's managed
|
|
243
|
+
* account in the tenant), so against a current platform neither is ever
|
|
244
|
+
* tokenless and this answers false; it stays for a platform that predates
|
|
245
|
+
* that, and can go once none does. The reasoning of isAnonymousWebhookRun holds
|
|
246
|
+
* for both: only the platform can reach Execute or start an execution, so no
|
|
247
|
+
* token means the platform chose to send none. A token that IS present is
|
|
248
|
+
* always validated, and every other event still requires one.
|
|
249
|
+
*/
|
|
250
|
+
export const isAnonymousRun = (event, request) => isAnonymousWebhookRun(event, request) ||
|
|
251
|
+
(Number(event) === SCHEDULED_RUN &&
|
|
252
|
+
!(request?.authToken ?? request?.authorization));
|
|
198
253
|
/** Marks an auth failure so the handler can map it to gRPC UNAUTHENTICATED. */
|
|
199
254
|
class UnauthenticatedError extends Error {
|
|
200
255
|
}
|
|
@@ -297,6 +352,149 @@ const toErrorFrame = (error) => {
|
|
|
297
352
|
}
|
|
298
353
|
return { code: 500, message: String(error?.message ?? error) };
|
|
299
354
|
};
|
|
355
|
+
/** How the platform records a settled run (backend-core BATCH_JOB.STATUS). */
|
|
356
|
+
const RUN_STATUS = { COMPLETED: 2, FAILED: 3 };
|
|
357
|
+
/**
|
|
358
|
+
* Settles the run this execution claimed, if it claimed one. Never throws and
|
|
359
|
+
* never changes the execution's own outcome: the work either happened or it
|
|
360
|
+
* did not, and a platform that cannot be told so right now will time the run
|
|
361
|
+
* out on its own. `settled: false` means it already did.
|
|
362
|
+
*/
|
|
363
|
+
const settleRun = async (claimed, lifecycle, status, outcome, batchJobId) => {
|
|
364
|
+
if (!claimed)
|
|
365
|
+
return;
|
|
366
|
+
try {
|
|
367
|
+
const answer = await lifecycle.settle(status, outcome ?? null, claimed.token);
|
|
368
|
+
if (!answer?.settled) {
|
|
369
|
+
logger.log(null, logger.levels.warn, `run-once execution for event record ${batchJobId}: the run was no longer running, so its outcome was not recorded`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
logger.log(null, logger.levels.warn, `run-once execution for event record ${batchJobId}: settling the run failed: ${error?.message ?? error}`);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
/**
|
|
377
|
+
* Run-once mode: the same agent, started as one Cloud Run JOB execution for
|
|
378
|
+
* one platform event (`runAs: job`). Fetches the execution's envelope from the
|
|
379
|
+
* backend with the execution's token — exactly what a service call would have
|
|
380
|
+
* received in its Execute request — binds the same {@link RequestContext} the
|
|
381
|
+
* gRPC path binds, runs the handler once, and exits: 0 on success, 1 on any
|
|
382
|
+
* failure so Cloud Run retries the task. SIGTERM (Cloud Run cancelling the
|
|
383
|
+
* execution or its task timeout) aborts the in-flight work.
|
|
384
|
+
*
|
|
385
|
+
* The execution reports its own run: it claims it before doing the work (an
|
|
386
|
+
* execution that loses the claim exits 0 without repeating it), says it is
|
|
387
|
+
* alive while it runs, and settles it with the outcome. The handler is
|
|
388
|
+
* written once and does not know which runtime it is on, nor that any of this
|
|
389
|
+
* is happening.
|
|
390
|
+
*/
|
|
391
|
+
export const runOnce = async (main, options = {}) => {
|
|
392
|
+
installGlobals();
|
|
393
|
+
const exit = options.exit ?? ((code) => process.exit(code));
|
|
394
|
+
const batchJobId = process.env[EXECUTION_ENV.BATCH_JOB_ID] ?? "";
|
|
395
|
+
const token = process.env[EXECUTION_ENV.TOKEN] ?? "";
|
|
396
|
+
const fetchEnvelope = options.fetchEnvelope ??
|
|
397
|
+
((id, tok) => clientIntegration.getExecutionEnvelope(id, tok));
|
|
398
|
+
const authenticate = options.authenticate ?? defaultAuthenticate;
|
|
399
|
+
const abort = new AbortController();
|
|
400
|
+
const onSignal = () => abort.abort();
|
|
401
|
+
process.once("SIGTERM", onSignal);
|
|
402
|
+
process.once("SIGINT", onSignal);
|
|
403
|
+
const lifecycle = options.lifecycle ?? {
|
|
404
|
+
claim: clientIntegration.claimExecution,
|
|
405
|
+
heartbeat: clientIntegration.heartbeatExecution,
|
|
406
|
+
settle: clientIntegration.settleExecution,
|
|
407
|
+
};
|
|
408
|
+
// Set once the run is ours: only an execution that claimed a run settles it.
|
|
409
|
+
let claimed = null;
|
|
410
|
+
let heartbeat = null;
|
|
411
|
+
// What the run used is reported before it is settled, however it ended:
|
|
412
|
+
// settling revokes the token the report is made with.
|
|
413
|
+
let reportWhatWasUsed = async () => { };
|
|
414
|
+
let stopReporting = () => { };
|
|
415
|
+
try {
|
|
416
|
+
const req = await fetchEnvelope(batchJobId, token);
|
|
417
|
+
const request = safeParse(req?.request_json, {});
|
|
418
|
+
// A scheduled run carries no identity by design: see isAnonymousRun.
|
|
419
|
+
const session = isAnonymousRun(req?.event, request)
|
|
420
|
+
? null
|
|
421
|
+
: await authenticate(request);
|
|
422
|
+
const context = {
|
|
423
|
+
config: safeParse(req?.config_json, {}),
|
|
424
|
+
params: {
|
|
425
|
+
contentType: req?.content_type,
|
|
426
|
+
data: safeParse(req?.data_json, {}),
|
|
427
|
+
},
|
|
428
|
+
request: { ...request, session },
|
|
429
|
+
event: req?.event ?? 0,
|
|
430
|
+
onProgress: (update) => {
|
|
431
|
+
logger.log(null, logger.levels.info, `progress ${Math.round(update?.progress ?? 0)}%: ${update?.message ?? ""}`);
|
|
432
|
+
},
|
|
433
|
+
signal: abort.signal,
|
|
434
|
+
session,
|
|
435
|
+
usage: new UsageLedger(),
|
|
436
|
+
};
|
|
437
|
+
reportWhatWasUsed = () => reportUsage(context, { report: options.reportUsage, last: true });
|
|
438
|
+
let entry = main;
|
|
439
|
+
const entryPath = String(req?.code ?? "").trim();
|
|
440
|
+
if (entryPath) {
|
|
441
|
+
const loaded = await loadAgentModule(entryPath, "handler");
|
|
442
|
+
if (loaded.reason) {
|
|
443
|
+
throw new Error(`entry module "${entryPath}" could not be loaded (${loaded.reason}): ${loaded.detail}`);
|
|
444
|
+
}
|
|
445
|
+
entry = loaded.fn;
|
|
446
|
+
}
|
|
447
|
+
// Claim before doing anything. Of however many executions the platform may
|
|
448
|
+
// start for one run — a retried task, a redelivered message — exactly one
|
|
449
|
+
// wins; the others exit 0 without repeating the work, which is not a
|
|
450
|
+
// failure and must not be retried. The execution's token names its run
|
|
451
|
+
// (every run, scheduled ones included, is started with one), so nothing
|
|
452
|
+
// else is sent.
|
|
453
|
+
const answer = await lifecycle.claim(token);
|
|
454
|
+
if (!answer?.claimed) {
|
|
455
|
+
logger.log(null, logger.levels.info, `run-once execution for event record ${batchJobId}: another execution already claimed this run, nothing to do`);
|
|
456
|
+
exit(0);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
claimed = { token };
|
|
460
|
+
heartbeat = setInterval(() => {
|
|
461
|
+
void lifecycle
|
|
462
|
+
.heartbeat(token)
|
|
463
|
+
.catch((error) => logger.log(null, logger.levels.warn, `run-once execution for event record ${batchJobId}: heartbeat failed: ${error?.message ?? error}`));
|
|
464
|
+
}, HEARTBEAT_MS);
|
|
465
|
+
heartbeat.unref?.();
|
|
466
|
+
stopReporting = reportUsageWhileRunning(context, {
|
|
467
|
+
report: options.reportUsage,
|
|
468
|
+
intervalMs: options.usageReportIntervalMs,
|
|
469
|
+
});
|
|
470
|
+
logger.log(null, logger.levels.info, `run-once execution for event record ${batchJobId} (${req?.content_type ?? ""}) starting`);
|
|
471
|
+
const result = await runWithContext(context, () => entry(abort.signal));
|
|
472
|
+
logger.log(null, logger.levels.info, `run-once execution for event record ${batchJobId} completed: ${JSON.stringify(result ?? null).slice(0, 500)}`);
|
|
473
|
+
stopReporting();
|
|
474
|
+
await reportWhatWasUsed();
|
|
475
|
+
await settleRun(claimed, lifecycle, RUN_STATUS.COMPLETED, result, batchJobId);
|
|
476
|
+
exit(0);
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
const frame = toErrorFrame(error);
|
|
480
|
+
logger.log(null, logger.levels.error, `run-once execution for event record ${batchJobId} failed${abort.signal.aborted ? " (aborted)" : ""}: ${frame.code} ${frame.message}`);
|
|
481
|
+
// Work that failed still used what it used.
|
|
482
|
+
stopReporting();
|
|
483
|
+
await reportWhatWasUsed();
|
|
484
|
+
// The run is ours to close whether it went well or not: a run left
|
|
485
|
+
// running is one the platform has to time out, and its credential lives
|
|
486
|
+
// until it does.
|
|
487
|
+
await settleRun(claimed, lifecycle, RUN_STATUS.FAILED, frame, batchJobId);
|
|
488
|
+
exit(1);
|
|
489
|
+
}
|
|
490
|
+
finally {
|
|
491
|
+
stopReporting();
|
|
492
|
+
if (heartbeat)
|
|
493
|
+
clearInterval(heartbeat);
|
|
494
|
+
process.off("SIGTERM", onSignal);
|
|
495
|
+
process.off("SIGINT", onSignal);
|
|
496
|
+
}
|
|
497
|
+
};
|
|
300
498
|
/**
|
|
301
499
|
* Start a gRPC server that exposes `main` as the fleet-standard `Agent.Execute`
|
|
302
500
|
* RPC. Per request it binds a {@link RequestContext} (so the agent's globals are
|
|
@@ -309,6 +507,12 @@ const toErrorFrame = (error) => {
|
|
|
309
507
|
* serve(require("./src/main"));
|
|
310
508
|
*/
|
|
311
509
|
export const serve = (main, options = {}) => {
|
|
510
|
+
// Started as a job execution: run the handler once and exit instead of
|
|
511
|
+
// serving. Same image, same entrypoint; the environment decides.
|
|
512
|
+
if (isRunOnceExecution()) {
|
|
513
|
+
void runOnce(main, options);
|
|
514
|
+
return new grpc.Server();
|
|
515
|
+
}
|
|
312
516
|
installGlobals();
|
|
313
517
|
const port = options.port ?? (Number(process.env.PORT) || 8080);
|
|
314
518
|
const host = options.host ?? "0.0.0.0";
|
|
@@ -331,9 +535,13 @@ export const serve = (main, options = {}) => {
|
|
|
331
535
|
};
|
|
332
536
|
const handle = async () => {
|
|
333
537
|
// Authenticate first — refuse the call when the token is missing/invalid.
|
|
334
|
-
|
|
538
|
+
// The exceptions carry no token by design and run with no session: see
|
|
539
|
+
// isAnonymousRun.
|
|
540
|
+
let session = null;
|
|
335
541
|
try {
|
|
336
|
-
|
|
542
|
+
if (!isAnonymousRun(req.event, request)) {
|
|
543
|
+
session = await authenticate(request);
|
|
544
|
+
}
|
|
337
545
|
}
|
|
338
546
|
catch (error) {
|
|
339
547
|
const message = error?.message ?? "Unauthenticated";
|
|
@@ -358,6 +566,7 @@ export const serve = (main, options = {}) => {
|
|
|
358
566
|
onProgress,
|
|
359
567
|
signal: abort.signal,
|
|
360
568
|
session,
|
|
569
|
+
usage: new UsageLedger(),
|
|
361
570
|
};
|
|
362
571
|
// Which function actually runs. Default is the agent's own `main` — the
|
|
363
572
|
// path every caller took before `code` existed, and still the path taken
|
|
@@ -388,8 +597,21 @@ export const serve = (main, options = {}) => {
|
|
|
388
597
|
}
|
|
389
598
|
entry = loaded.fn;
|
|
390
599
|
}
|
|
600
|
+
// What the call used is reported before it is answered, however it
|
|
601
|
+
// ended — a call that failed, or that its caller gave up on, still used
|
|
602
|
+
// what it used — because the platform revokes the call's token as soon
|
|
603
|
+
// as it has the answer.
|
|
604
|
+
const stopReporting = reportUsageWhileRunning(context, {
|
|
605
|
+
report: options.reportUsage,
|
|
606
|
+
intervalMs: options.usageReportIntervalMs,
|
|
607
|
+
});
|
|
608
|
+
const reportWhatWasUsed = async () => {
|
|
609
|
+
stopReporting();
|
|
610
|
+
await reportUsage(context, { report: options.reportUsage, last: true });
|
|
611
|
+
};
|
|
391
612
|
try {
|
|
392
613
|
const result = await runWithContext(context, () => entry(abort.signal));
|
|
614
|
+
await reportWhatWasUsed();
|
|
393
615
|
if (!abort.signal.aborted) {
|
|
394
616
|
call.write({
|
|
395
617
|
result: { result_json: JSON.stringify(result ?? null) },
|
|
@@ -397,6 +619,7 @@ export const serve = (main, options = {}) => {
|
|
|
397
619
|
}
|
|
398
620
|
}
|
|
399
621
|
catch (error) {
|
|
622
|
+
await reportWhatWasUsed();
|
|
400
623
|
if (!abort.signal.aborted) {
|
|
401
624
|
logger.log(null, logger.levels.error, `agent Execute failed: ${error?.message ?? error}`);
|
|
402
625
|
call.write({ error: toErrorFrame(error) });
|
package/dist/esm/tavily.d.ts
CHANGED
|
@@ -86,6 +86,10 @@ declare const _default: {
|
|
|
86
86
|
assertTavilyUsable: (config: any, options: TavilyConfig) => void;
|
|
87
87
|
buildTavilyTools: (config: any, options: TavilyConfig, usageTracker?: UsageTracker | null) => any[];
|
|
88
88
|
getTavilyOptions: (config: any) => TavilyConfig | null;
|
|
89
|
+
recordTavilyUsage: (tracker: UsageTracker | null | undefined, _config: any, credits: number, sources: {
|
|
90
|
+
url: string;
|
|
91
|
+
title?: string;
|
|
92
|
+
}[]) => void;
|
|
89
93
|
};
|
|
90
94
|
export default _default;
|
|
91
95
|
//# sourceMappingURL=tavily.d.ts.map
|
package/dist/esm/tavily.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tavily.d.ts","sourceRoot":"","sources":["../../src/tavily.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"tavily.d.ts","sourceRoot":"","sources":["../../src/tavily.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AA0BnD,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,KAAK,CAAC;AAEpE;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;IACzB,iEAAiE;IACjE,WAAW,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,YAAY,CAAC;IAC3D,kFAAkF;IAClF,YAAY,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IACpC,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4CAA4C;IAC5C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,6CAA6C;IAC7C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;CAC9B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,QAAQ,GAAG,KAAG,YAAY,GAAG,IAI7D,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,SAAS,YAAY,KAAG,IAsBvE,CAAC;AAsLF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,gBAAgB,GAC3B,QAAQ,GAAG,EACX,SAAS,YAAY,EACrB,eAAc,YAAY,GAAG,IAAW,KACvC,GAAG,EA2PL,CAAC;;iCA5dyC,GAAG,WAAW,YAAY,KAAG,IAAI;+BA8NlE,GAAG,WACF,YAAY,iBACP,YAAY,GAAG,IAAI,KAChC,GAAG,EAAE;+BAhPiC,GAAG,KAAG,YAAY,GAAG,IAAI;iCAsIvD,YAAY,GAAG,IAAI,GAAG,SAAS,WAC/B,GAAG,WACH,MAAM,WACN;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,KACzC,IAAI;;AAmWP,wBAKE"}
|
package/dist/esm/tavily.js
CHANGED
|
@@ -4,9 +4,8 @@ import constants from "./const.js";
|
|
|
4
4
|
import errorHandlingHelper from "./errorHandling.js";
|
|
5
5
|
import logger from "./logger.js";
|
|
6
6
|
import { getAbortSignal } from "./runtimeContext.js";
|
|
7
|
+
import { recordUsage } from "./usage.js";
|
|
7
8
|
const TAVILY_API_BASE = "https://api.tavily.com";
|
|
8
|
-
/** Tavily list price per credit, used when the integration defines no rate. */
|
|
9
|
-
const DEFAULT_CREDIT_COST_USD = 0.008;
|
|
10
9
|
/**
|
|
11
10
|
* Context budgets, in characters, applied to everything a tool hands back to
|
|
12
11
|
* the model. Tavily returns whatever the page holds, and unlike a provider's
|
|
@@ -121,19 +120,25 @@ const callTavily = async (endpoint, body, apiKey, timeoutSeconds) => {
|
|
|
121
120
|
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `Tavily ${endpoint} returned ${response.status}: ${detail.slice(0, 500)}`);
|
|
122
121
|
}
|
|
123
122
|
};
|
|
123
|
+
/** The name Tavily's credits are counted and priced under. */
|
|
124
|
+
const TAVILY_MODEL = "tavily";
|
|
124
125
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* `tavilyCredits`. Sources land on
|
|
129
|
-
* URL across the whole run, so
|
|
130
|
-
* require when this output is
|
|
131
|
-
*
|
|
132
|
-
* @param
|
|
126
|
+
* Counts one Tavily call's credits and records the URLs it returned. Credits
|
|
127
|
+
* come from the response's own `usage` block; they are counted into the
|
|
128
|
+
* request's usage ledger, which is reported to the platform, and into the
|
|
129
|
+
* caller's tracker under `tavilyCredits`. Sources land on
|
|
130
|
+
* `tracker.webSearchSources`, deduplicated by URL across the whole run, so
|
|
131
|
+
* callers have the citation list Tavily's terms require when this output is
|
|
132
|
+
* shown to end users.
|
|
133
|
+
* @param tracker - The caller's usage accumulator, when it passed one
|
|
134
|
+
* @param _config - No longer read: it carried the rate, which is the platform's to say
|
|
133
135
|
* @param credits - Credits reported by the Tavily response
|
|
134
136
|
* @param sources - URLs (and titles where known) the call returned
|
|
135
137
|
*/
|
|
136
|
-
const recordTavilyUsage = (tracker,
|
|
138
|
+
const recordTavilyUsage = (tracker, _config, credits, sources) => {
|
|
139
|
+
if (Number.isFinite(credits) && credits > 0) {
|
|
140
|
+
recordUsage(TAVILY_MODEL, "credits", credits);
|
|
141
|
+
}
|
|
137
142
|
if (!tracker)
|
|
138
143
|
return;
|
|
139
144
|
if (typeof tracker.cost !== "number")
|
|
@@ -141,13 +146,6 @@ const recordTavilyUsage = (tracker, config, credits, sources) => {
|
|
|
141
146
|
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
142
147
|
tracker.tokens = {};
|
|
143
148
|
if (Number.isFinite(credits) && credits > 0) {
|
|
144
|
-
const configured = Number(config?.["tavily-credit-costs"]);
|
|
145
|
-
const rate = Number.isFinite(configured)
|
|
146
|
-
? configured
|
|
147
|
-
: DEFAULT_CREDIT_COST_USD;
|
|
148
|
-
const addedCost = credits * rate;
|
|
149
|
-
if (Number.isFinite(addedCost) && addedCost > 0)
|
|
150
|
-
tracker.cost += addedCost;
|
|
151
149
|
tracker.tokens.tavilyCredits =
|
|
152
150
|
(tracker.tokens.tavilyCredits || 0) + credits;
|
|
153
151
|
}
|
|
@@ -392,4 +390,5 @@ export default {
|
|
|
392
390
|
assertTavilyUsable,
|
|
393
391
|
buildTavilyTools,
|
|
394
392
|
getTavilyOptions,
|
|
393
|
+
recordTavilyUsage,
|
|
395
394
|
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type RequestContext } from "./runtimeContext.js";
|
|
2
|
+
/**
|
|
3
|
+
* What an agent used, and how the platform gets to know.
|
|
4
|
+
*
|
|
5
|
+
* An agent says WHAT it used — a model, a unit, an amount — and the platform
|
|
6
|
+
* says what that costs, who is charged and what it was for. Everything that
|
|
7
|
+
* calls a model through this package counts what the call used into the
|
|
8
|
+
* request's {@link UsageLedger}; {@link serve} and {@link runOnce} report the
|
|
9
|
+
* ledger to the platform while the work runs and when it ends, whether it
|
|
10
|
+
* ended well or not. An agent does nothing for this to happen.
|
|
11
|
+
*
|
|
12
|
+
* An agent that calls a provider itself — its own SDK, an HTTP request —
|
|
13
|
+
* counts what that used with {@link recordUsage}.
|
|
14
|
+
*/
|
|
15
|
+
/** What usage is counted in; the units the platform has rates for. */
|
|
16
|
+
export declare const USAGE_UNITS: readonly ["inputTokens", "outputTokens", "imageInputTokens", "imageOutputTokens", "characters", "seconds", "credits"];
|
|
17
|
+
export type UsageUnit = (typeof USAGE_UNITS)[number];
|
|
18
|
+
export interface UsageEntry {
|
|
19
|
+
model: string;
|
|
20
|
+
unit: UsageUnit;
|
|
21
|
+
amount: number;
|
|
22
|
+
}
|
|
23
|
+
/** Sends one report to the platform. Rejects when the platform did not take it. */
|
|
24
|
+
export type ReportUsage = (usage: UsageEntry[], reportId: string, token: string) => Promise<unknown>;
|
|
25
|
+
/** How often work that is still running reports what it used so far. */
|
|
26
|
+
export declare const USAGE_REPORT_INTERVAL_MS = 60000;
|
|
27
|
+
/**
|
|
28
|
+
* What one request used and has not reported yet, summed by model and unit.
|
|
29
|
+
*/
|
|
30
|
+
export declare class UsageLedger {
|
|
31
|
+
private pending;
|
|
32
|
+
/** Adds to what is pending. Anything that is not a positive amount of a known unit is ignored. */
|
|
33
|
+
add(model: string, unit: UsageUnit, amount: number): void;
|
|
34
|
+
/** What is pending, which then no longer is. */
|
|
35
|
+
take(): UsageEntry[];
|
|
36
|
+
/** True when there is nothing to report. */
|
|
37
|
+
get isEmpty(): boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Counts what the running request used. For an agent that calls a provider
|
|
41
|
+
* itself; what is called through `langChain` is counted already. Outside a
|
|
42
|
+
* request there is nothing to count into, and nothing happens.
|
|
43
|
+
* @param model - The model's id, as its provider names it
|
|
44
|
+
* @param unit - What the amount is counted in
|
|
45
|
+
* @param amount - How much
|
|
46
|
+
*/
|
|
47
|
+
export declare const recordUsage: (model: string, unit: UsageUnit, amount: number) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Reports what the request used and has not reported yet. Never throws, and
|
|
50
|
+
* never changes what the work answers: the work happened, and a platform that
|
|
51
|
+
* cannot be told what it used right now is told by the next report, or not at
|
|
52
|
+
* all — which is logged as an error, with what was used.
|
|
53
|
+
*
|
|
54
|
+
* @param context - The request whose ledger is reported
|
|
55
|
+
* @param options - `report` sends a report (default: the platform's route);
|
|
56
|
+
* `last` says no other report follows, so what cannot be sent is given up on
|
|
57
|
+
* rather than kept for the next one
|
|
58
|
+
*/
|
|
59
|
+
export declare const reportUsage: (context: Pick<RequestContext, "usage" | "request"> | undefined, options?: {
|
|
60
|
+
report?: ReportUsage;
|
|
61
|
+
last?: boolean;
|
|
62
|
+
retryBaseMs?: number;
|
|
63
|
+
}) => Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Reports what the request used so far every {@link USAGE_REPORT_INTERVAL_MS}
|
|
66
|
+
* while the work runs, so work that dies has reported most of what it used.
|
|
67
|
+
* @returns Stops it
|
|
68
|
+
*/
|
|
69
|
+
export declare const reportUsageWhileRunning: (context: Pick<RequestContext, "usage" | "request">, options?: {
|
|
70
|
+
report?: ReportUsage;
|
|
71
|
+
intervalMs?: number;
|
|
72
|
+
retryBaseMs?: number;
|
|
73
|
+
}) => (() => void);
|
|
74
|
+
//# sourceMappingURL=usage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../../src/usage.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAEtE;;;;;;;;;;;;GAYG;AAEH,sEAAsE;AACtE,eAAO,MAAM,WAAW,uHAQd,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,mFAAmF;AACnF,MAAM,MAAM,WAAW,GAAG,CACxB,KAAK,EAAE,UAAU,EAAE,EACnB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,KACV,OAAO,CAAC,OAAO,CAAC,CAAC;AAOtB,wEAAwE;AACxE,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAI/C;;GAEG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAiC;IAEhD,kGAAkG;IAClG,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAkBzD,gDAAgD;IAChD,IAAI,IAAI,UAAU,EAAE;IAMpB,4CAA4C;IAC5C,IAAI,OAAO,IAAI,OAAO,CAErB;CACF;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,EAAE,MAAM,SAAS,EAAE,QAAQ,MAAM,KAAG,IAE5E,CAAC;AA8BF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,GACtB,SAAS,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,SAAS,CAAC,GAAG,SAAS,EAC9D,UAAS;IAAE,MAAM,CAAC,EAAE,WAAW,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,KAC3E,OAAO,CAAC,IAAI,CAuCd,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,GAClC,SAAS,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,SAAS,CAAC,EAClD,UAAS;IAAE,MAAM,CAAC,EAAE,WAAW,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,KAChF,CAAC,MAAM,IAAI,CAMb,CAAC"}
|