@modelrelay/sdk 1.3.2 → 1.10.3
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 +57 -8
- package/dist/index.cjs +3476 -1008
- package/dist/index.d.cts +2199 -321
- package/dist/index.d.ts +2199 -321
- package/dist/index.js +3441 -1008
- package/package.json +15 -3
package/dist/index.js
CHANGED
|
@@ -159,6 +159,28 @@ var WorkflowValidationError = class extends ModelRelayError {
|
|
|
159
159
|
this.issues = opts.issues;
|
|
160
160
|
}
|
|
161
161
|
};
|
|
162
|
+
var ToolArgumentError = class extends ModelRelayError {
|
|
163
|
+
constructor(opts) {
|
|
164
|
+
super(opts.message, {
|
|
165
|
+
category: "config",
|
|
166
|
+
status: 400,
|
|
167
|
+
cause: opts.cause
|
|
168
|
+
});
|
|
169
|
+
this.toolCallId = opts.toolCallId;
|
|
170
|
+
this.toolName = opts.toolName;
|
|
171
|
+
this.rawArguments = opts.rawArguments;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
var PathEscapeError = class extends ModelRelayError {
|
|
175
|
+
constructor(opts) {
|
|
176
|
+
super(`path escapes sandbox: ${opts.requestedPath}`, {
|
|
177
|
+
category: "config",
|
|
178
|
+
status: 403
|
|
179
|
+
});
|
|
180
|
+
this.requestedPath = opts.requestedPath;
|
|
181
|
+
this.resolvedPath = opts.resolvedPath;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
162
184
|
function isEmailRequired(err) {
|
|
163
185
|
return err instanceof APIError && err.isEmailRequired();
|
|
164
186
|
}
|
|
@@ -205,10 +227,10 @@ async function parseErrorResponse(response, retries) {
|
|
|
205
227
|
if (!raw || typeof raw !== "object") continue;
|
|
206
228
|
const obj = raw;
|
|
207
229
|
const code = typeof obj.code === "string" ? obj.code : "";
|
|
208
|
-
const
|
|
230
|
+
const path2 = typeof obj.path === "string" ? obj.path : "";
|
|
209
231
|
const message = typeof obj.message === "string" ? obj.message : "";
|
|
210
|
-
if (!code || !
|
|
211
|
-
normalized.push({ code, path, message });
|
|
232
|
+
if (!code || !path2 || !message) continue;
|
|
233
|
+
normalized.push({ code, path: path2, message });
|
|
212
234
|
}
|
|
213
235
|
if (normalized.length > 0) {
|
|
214
236
|
return new WorkflowValidationError({
|
|
@@ -449,24 +471,18 @@ var AuthClient = class {
|
|
|
449
471
|
* Mint a customer-scoped bearer token (requires a secret key).
|
|
450
472
|
*/
|
|
451
473
|
async customerToken(request) {
|
|
452
|
-
const projectId = request.projectId?.trim();
|
|
453
|
-
if (!projectId) {
|
|
454
|
-
throw new ConfigError("projectId is required");
|
|
455
|
-
}
|
|
456
474
|
const customerId = request.customerId?.trim();
|
|
457
475
|
const customerExternalId = request.customerExternalId?.trim();
|
|
458
476
|
if (!!customerId && !!customerExternalId || !customerId && !customerExternalId) {
|
|
459
477
|
throw new ConfigError("Provide exactly one of customerId or customerExternalId");
|
|
460
478
|
}
|
|
461
|
-
if (request.ttlSeconds !== void 0 && request.ttlSeconds
|
|
462
|
-
throw new ConfigError("ttlSeconds must be
|
|
479
|
+
if (request.ttlSeconds !== void 0 && request.ttlSeconds < 0) {
|
|
480
|
+
throw new ConfigError("ttlSeconds must be non-negative when provided");
|
|
463
481
|
}
|
|
464
482
|
if (!this.apiKey || this.apiKeyIsPublishable) {
|
|
465
483
|
throw new ConfigError("Secret API key is required to mint customer tokens");
|
|
466
484
|
}
|
|
467
|
-
const payload = {
|
|
468
|
-
project_id: projectId
|
|
469
|
-
};
|
|
485
|
+
const payload = {};
|
|
470
486
|
if (customerId) {
|
|
471
487
|
payload.customer_id = customerId;
|
|
472
488
|
}
|
|
@@ -564,8 +580,8 @@ var AuthClient = class {
|
|
|
564
580
|
params.set("provider", request.provider);
|
|
565
581
|
}
|
|
566
582
|
const queryString = params.toString();
|
|
567
|
-
const
|
|
568
|
-
const apiResp = await this.http.json(
|
|
583
|
+
const path2 = queryString ? `/auth/device/start?${queryString}` : "/auth/device/start";
|
|
584
|
+
const apiResp = await this.http.json(path2, {
|
|
569
585
|
method: "POST",
|
|
570
586
|
apiKey: this.apiKey
|
|
571
587
|
});
|
|
@@ -629,7 +645,7 @@ var AuthClient = class {
|
|
|
629
645
|
token: apiResp.token,
|
|
630
646
|
expiresAt: new Date(apiResp.expires_at),
|
|
631
647
|
expiresIn: apiResp.expires_in,
|
|
632
|
-
tokenType:
|
|
648
|
+
tokenType: "Bearer",
|
|
633
649
|
projectId: apiResp.project_id,
|
|
634
650
|
customerId: apiResp.customer_id,
|
|
635
651
|
customerExternalId: apiResp.customer_external_id,
|
|
@@ -688,7 +704,7 @@ function isTokenReusable(token) {
|
|
|
688
704
|
// package.json
|
|
689
705
|
var package_default = {
|
|
690
706
|
name: "@modelrelay/sdk",
|
|
691
|
-
version: "1.3
|
|
707
|
+
version: "1.10.3",
|
|
692
708
|
description: "TypeScript SDK for the ModelRelay API",
|
|
693
709
|
type: "module",
|
|
694
710
|
main: "dist/index.cjs",
|
|
@@ -701,12 +717,14 @@ var package_default = {
|
|
|
701
717
|
require: "./dist/index.cjs"
|
|
702
718
|
}
|
|
703
719
|
},
|
|
704
|
-
publishConfig: {
|
|
720
|
+
publishConfig: {
|
|
721
|
+
access: "public"
|
|
722
|
+
},
|
|
705
723
|
files: [
|
|
706
724
|
"dist"
|
|
707
725
|
],
|
|
708
726
|
scripts: {
|
|
709
|
-
build: "tsup src/index.ts --format esm,cjs --dts",
|
|
727
|
+
build: "tsup src/index.ts --format esm,cjs --dts --external playwright",
|
|
710
728
|
dev: "tsup src/index.ts --format esm,cjs --dts --watch",
|
|
711
729
|
lint: "tsc --noEmit --project tsconfig.lint.json",
|
|
712
730
|
test: "vitest run",
|
|
@@ -723,8 +741,18 @@ var package_default = {
|
|
|
723
741
|
dependencies: {
|
|
724
742
|
"fast-json-patch": "^3.1.1"
|
|
725
743
|
},
|
|
744
|
+
peerDependencies: {
|
|
745
|
+
playwright: ">=1.40.0"
|
|
746
|
+
},
|
|
747
|
+
peerDependenciesMeta: {
|
|
748
|
+
playwright: {
|
|
749
|
+
optional: true
|
|
750
|
+
}
|
|
751
|
+
},
|
|
726
752
|
devDependencies: {
|
|
753
|
+
"@types/node": "^25.0.3",
|
|
727
754
|
"openapi-typescript": "^7.4.4",
|
|
755
|
+
playwright: "^1.49.0",
|
|
728
756
|
tsup: "^8.2.4",
|
|
729
757
|
typescript: "^5.6.3",
|
|
730
758
|
vitest: "^2.1.4",
|
|
@@ -761,6 +789,22 @@ function asModelId(value) {
|
|
|
761
789
|
function asTierCode(value) {
|
|
762
790
|
return value;
|
|
763
791
|
}
|
|
792
|
+
var SubscriptionStatuses = {
|
|
793
|
+
Active: "active",
|
|
794
|
+
Trialing: "trialing",
|
|
795
|
+
PastDue: "past_due",
|
|
796
|
+
Canceled: "canceled",
|
|
797
|
+
Unpaid: "unpaid",
|
|
798
|
+
Incomplete: "incomplete",
|
|
799
|
+
IncompleteExpired: "incomplete_expired",
|
|
800
|
+
Paused: "paused"
|
|
801
|
+
};
|
|
802
|
+
var BillingProviders = {
|
|
803
|
+
Stripe: "stripe",
|
|
804
|
+
Crypto: "crypto",
|
|
805
|
+
AppStore: "app_store",
|
|
806
|
+
External: "external"
|
|
807
|
+
};
|
|
764
808
|
function createUsage(inputTokens, outputTokens, totalTokens) {
|
|
765
809
|
return {
|
|
766
810
|
inputTokens,
|
|
@@ -1278,8 +1322,8 @@ function parseToolArgs(call, schema) {
|
|
|
1278
1322
|
const zodErr = err;
|
|
1279
1323
|
if (zodErr.errors && Array.isArray(zodErr.errors)) {
|
|
1280
1324
|
const issues = zodErr.errors.map((e) => {
|
|
1281
|
-
const
|
|
1282
|
-
return `${
|
|
1325
|
+
const path2 = e.path.length > 0 ? `${e.path.join(".")}: ` : "";
|
|
1326
|
+
return `${path2}${e.message}`;
|
|
1283
1327
|
}).join("; ");
|
|
1284
1328
|
message = issues;
|
|
1285
1329
|
} else {
|
|
@@ -1403,7 +1447,7 @@ var ToolRegistry = class {
|
|
|
1403
1447
|
result
|
|
1404
1448
|
};
|
|
1405
1449
|
} catch (err) {
|
|
1406
|
-
const isRetryable = err instanceof ToolArgsError;
|
|
1450
|
+
const isRetryable = err instanceof ToolArgsError || err instanceof ToolArgumentError;
|
|
1407
1451
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1408
1452
|
return {
|
|
1409
1453
|
toolCallId: call.id,
|
|
@@ -1923,6 +1967,7 @@ function mapNDJSONResponseEvent(line, requestId) {
|
|
|
1923
1967
|
}
|
|
1924
1968
|
const toolCallDelta = extractToolCallDelta(parsed, type);
|
|
1925
1969
|
const toolCalls = extractToolCalls(parsed, type);
|
|
1970
|
+
const toolResult = extractToolResult(parsed, type);
|
|
1926
1971
|
return {
|
|
1927
1972
|
type,
|
|
1928
1973
|
event: recordType2,
|
|
@@ -1930,6 +1975,7 @@ function mapNDJSONResponseEvent(line, requestId) {
|
|
|
1930
1975
|
textDelta,
|
|
1931
1976
|
toolCallDelta,
|
|
1932
1977
|
toolCalls,
|
|
1978
|
+
toolResult,
|
|
1933
1979
|
responseId,
|
|
1934
1980
|
model,
|
|
1935
1981
|
stopReason,
|
|
@@ -1993,6 +2039,15 @@ function extractToolCalls(payload, type) {
|
|
|
1993
2039
|
}
|
|
1994
2040
|
return void 0;
|
|
1995
2041
|
}
|
|
2042
|
+
function extractToolResult(payload, type) {
|
|
2043
|
+
if (type !== "tool_use_stop") {
|
|
2044
|
+
return void 0;
|
|
2045
|
+
}
|
|
2046
|
+
if ("tool_result" in payload) {
|
|
2047
|
+
return payload.tool_result;
|
|
2048
|
+
}
|
|
2049
|
+
return void 0;
|
|
2050
|
+
}
|
|
1996
2051
|
function normalizeToolCalls(toolCalls) {
|
|
1997
2052
|
const validToolTypes = /* @__PURE__ */ new Set([
|
|
1998
2053
|
"function",
|
|
@@ -3403,8 +3458,8 @@ function getErrorMap() {
|
|
|
3403
3458
|
|
|
3404
3459
|
// node_modules/zod/v3/helpers/parseUtil.js
|
|
3405
3460
|
var makeIssue = (params) => {
|
|
3406
|
-
const { data, path, errorMaps, issueData } = params;
|
|
3407
|
-
const fullPath = [...
|
|
3461
|
+
const { data, path: path2, errorMaps, issueData } = params;
|
|
3462
|
+
const fullPath = [...path2, ...issueData.path || []];
|
|
3408
3463
|
const fullIssue = {
|
|
3409
3464
|
...issueData,
|
|
3410
3465
|
path: fullPath
|
|
@@ -3520,11 +3575,11 @@ var errorUtil;
|
|
|
3520
3575
|
|
|
3521
3576
|
// node_modules/zod/v3/types.js
|
|
3522
3577
|
var ParseInputLazyPath = class {
|
|
3523
|
-
constructor(parent, value,
|
|
3578
|
+
constructor(parent, value, path2, key) {
|
|
3524
3579
|
this._cachedPath = [];
|
|
3525
3580
|
this.parent = parent;
|
|
3526
3581
|
this.data = value;
|
|
3527
|
-
this._path =
|
|
3582
|
+
this._path = path2;
|
|
3528
3583
|
this._key = key;
|
|
3529
3584
|
}
|
|
3530
3585
|
get path() {
|
|
@@ -7357,11 +7412,18 @@ var RunsClient = class {
|
|
|
7357
7412
|
this.metrics = cfg.metrics;
|
|
7358
7413
|
this.trace = cfg.trace;
|
|
7359
7414
|
}
|
|
7415
|
+
applyCustomerHeader(headers, customerId) {
|
|
7416
|
+
const trimmed = customerId?.trim();
|
|
7417
|
+
if (trimmed) {
|
|
7418
|
+
headers[CUSTOMER_ID_HEADER] = trimmed;
|
|
7419
|
+
}
|
|
7420
|
+
}
|
|
7360
7421
|
async create(spec, options = {}) {
|
|
7361
7422
|
const metrics = mergeMetrics(this.metrics, options.metrics);
|
|
7362
7423
|
const trace = mergeTrace(this.trace, options.trace);
|
|
7363
7424
|
const authHeaders = await this.auth.authForResponses();
|
|
7364
7425
|
const headers = { ...options.headers || {} };
|
|
7426
|
+
this.applyCustomerHeader(headers, options.customerId);
|
|
7365
7427
|
const payload = { spec };
|
|
7366
7428
|
if (options.idempotencyKey?.trim()) {
|
|
7367
7429
|
payload.options = { idempotency_key: options.idempotencyKey.trim() };
|
|
@@ -7418,10 +7480,12 @@ var RunsClient = class {
|
|
|
7418
7480
|
const metrics = mergeMetrics(this.metrics, options.metrics);
|
|
7419
7481
|
const trace = mergeTrace(this.trace, options.trace);
|
|
7420
7482
|
const authHeaders = await this.auth.authForResponses();
|
|
7421
|
-
const
|
|
7422
|
-
const
|
|
7483
|
+
const path2 = runByIdPath(runId);
|
|
7484
|
+
const headers = { ...options.headers || {} };
|
|
7485
|
+
this.applyCustomerHeader(headers, options.customerId);
|
|
7486
|
+
const out = await this.http.json(path2, {
|
|
7423
7487
|
method: "GET",
|
|
7424
|
-
headers
|
|
7488
|
+
headers,
|
|
7425
7489
|
signal: options.signal,
|
|
7426
7490
|
apiKey: authHeaders.apiKey,
|
|
7427
7491
|
accessToken: authHeaders.accessToken,
|
|
@@ -7430,7 +7494,7 @@ var RunsClient = class {
|
|
|
7430
7494
|
retry: options.retry,
|
|
7431
7495
|
metrics,
|
|
7432
7496
|
trace,
|
|
7433
|
-
context: { method: "GET", path }
|
|
7497
|
+
context: { method: "GET", path: path2 }
|
|
7434
7498
|
});
|
|
7435
7499
|
return {
|
|
7436
7500
|
...out,
|
|
@@ -7454,9 +7518,10 @@ var RunsClient = class {
|
|
|
7454
7518
|
if (options.wait === false) {
|
|
7455
7519
|
params.set("wait", "0");
|
|
7456
7520
|
}
|
|
7457
|
-
const
|
|
7521
|
+
const path2 = params.toString() ? `${basePath}?${params}` : basePath;
|
|
7458
7522
|
const headers = { ...options.headers || {} };
|
|
7459
|
-
|
|
7523
|
+
this.applyCustomerHeader(headers, options.customerId);
|
|
7524
|
+
const resp = await this.http.request(path2, {
|
|
7460
7525
|
method: "GET",
|
|
7461
7526
|
headers,
|
|
7462
7527
|
signal: options.signal,
|
|
@@ -7507,10 +7572,12 @@ var RunsClient = class {
|
|
|
7507
7572
|
const metrics = mergeMetrics(this.metrics, options.metrics);
|
|
7508
7573
|
const trace = mergeTrace(this.trace, options.trace);
|
|
7509
7574
|
const authHeaders = await this.auth.authForResponses();
|
|
7510
|
-
const
|
|
7511
|
-
const
|
|
7575
|
+
const path2 = runToolResultsPath(runId);
|
|
7576
|
+
const headers = { ...options.headers || {} };
|
|
7577
|
+
this.applyCustomerHeader(headers, options.customerId);
|
|
7578
|
+
const out = await this.http.json(path2, {
|
|
7512
7579
|
method: "POST",
|
|
7513
|
-
headers
|
|
7580
|
+
headers,
|
|
7514
7581
|
body: req,
|
|
7515
7582
|
signal: options.signal,
|
|
7516
7583
|
apiKey: authHeaders.apiKey,
|
|
@@ -7520,7 +7587,7 @@ var RunsClient = class {
|
|
|
7520
7587
|
retry: options.retry,
|
|
7521
7588
|
metrics,
|
|
7522
7589
|
trace,
|
|
7523
|
-
context: { method: "POST", path }
|
|
7590
|
+
context: { method: "POST", path: path2 }
|
|
7524
7591
|
});
|
|
7525
7592
|
return out;
|
|
7526
7593
|
}
|
|
@@ -7528,10 +7595,12 @@ var RunsClient = class {
|
|
|
7528
7595
|
const metrics = mergeMetrics(this.metrics, options.metrics);
|
|
7529
7596
|
const trace = mergeTrace(this.trace, options.trace);
|
|
7530
7597
|
const authHeaders = await this.auth.authForResponses();
|
|
7531
|
-
const
|
|
7532
|
-
const
|
|
7598
|
+
const path2 = runPendingToolsPath(runId);
|
|
7599
|
+
const headers = { ...options.headers || {} };
|
|
7600
|
+
this.applyCustomerHeader(headers, options.customerId);
|
|
7601
|
+
const out = await this.http.json(path2, {
|
|
7533
7602
|
method: "GET",
|
|
7534
|
-
headers
|
|
7603
|
+
headers,
|
|
7535
7604
|
signal: options.signal,
|
|
7536
7605
|
apiKey: authHeaders.apiKey,
|
|
7537
7606
|
accessToken: authHeaders.accessToken,
|
|
@@ -7540,7 +7609,7 @@ var RunsClient = class {
|
|
|
7540
7609
|
retry: options.retry,
|
|
7541
7610
|
metrics,
|
|
7542
7611
|
trace,
|
|
7543
|
-
context: { method: "GET", path }
|
|
7612
|
+
context: { method: "GET", path: path2 }
|
|
7544
7613
|
});
|
|
7545
7614
|
return {
|
|
7546
7615
|
...out,
|
|
@@ -7569,12 +7638,17 @@ var WorkflowsClient = class {
|
|
|
7569
7638
|
const metrics = mergeMetrics(this.metrics, options.metrics);
|
|
7570
7639
|
const trace = mergeTrace(this.trace, options.trace);
|
|
7571
7640
|
const authHeaders = await this.auth.authForResponses();
|
|
7641
|
+
const headers = { ...options.headers || {} };
|
|
7642
|
+
const customerId = options.customerId?.trim();
|
|
7643
|
+
if (customerId) {
|
|
7644
|
+
headers[CUSTOMER_ID_HEADER] = customerId;
|
|
7645
|
+
}
|
|
7572
7646
|
try {
|
|
7573
7647
|
const out = await this.http.json(
|
|
7574
7648
|
WORKFLOWS_COMPILE_PATH,
|
|
7575
7649
|
{
|
|
7576
7650
|
method: "POST",
|
|
7577
|
-
headers
|
|
7651
|
+
headers,
|
|
7578
7652
|
body: spec,
|
|
7579
7653
|
signal: options.signal,
|
|
7580
7654
|
apiKey: authHeaders.apiKey,
|
|
@@ -7725,9 +7799,6 @@ var CustomersClient = class {
|
|
|
7725
7799
|
*/
|
|
7726
7800
|
async create(request) {
|
|
7727
7801
|
this.ensureSecretKey();
|
|
7728
|
-
if (!request.tier_id?.trim()) {
|
|
7729
|
-
throw new ConfigError("tier_id is required");
|
|
7730
|
-
}
|
|
7731
7802
|
if (!request.external_id?.trim()) {
|
|
7732
7803
|
throw new ConfigError("external_id is required");
|
|
7733
7804
|
}
|
|
@@ -7768,9 +7839,6 @@ var CustomersClient = class {
|
|
|
7768
7839
|
*/
|
|
7769
7840
|
async upsert(request) {
|
|
7770
7841
|
this.ensureSecretKey();
|
|
7771
|
-
if (!request.tier_id?.trim()) {
|
|
7772
|
-
throw new ConfigError("tier_id is required");
|
|
7773
|
-
}
|
|
7774
7842
|
if (!request.external_id?.trim()) {
|
|
7775
7843
|
throw new ConfigError("external_id is required");
|
|
7776
7844
|
}
|
|
@@ -7788,7 +7856,7 @@ var CustomersClient = class {
|
|
|
7788
7856
|
return response.customer;
|
|
7789
7857
|
}
|
|
7790
7858
|
/**
|
|
7791
|
-
* Link
|
|
7859
|
+
* Link a customer identity (provider + subject) to a customer found by email.
|
|
7792
7860
|
* Used when a customer subscribes via Stripe Checkout (email only) and later authenticates to the app.
|
|
7793
7861
|
*
|
|
7794
7862
|
* This is a user self-service operation that works with publishable keys,
|
|
@@ -7813,12 +7881,11 @@ var CustomersClient = class {
|
|
|
7813
7881
|
if (!request.subject?.trim()) {
|
|
7814
7882
|
throw new ConfigError("subject is required");
|
|
7815
7883
|
}
|
|
7816
|
-
|
|
7884
|
+
await this.http.request("/customers/claim", {
|
|
7817
7885
|
method: "POST",
|
|
7818
7886
|
body: request,
|
|
7819
7887
|
apiKey: this.apiKey
|
|
7820
7888
|
});
|
|
7821
|
-
return response.customer;
|
|
7822
7889
|
}
|
|
7823
7890
|
/**
|
|
7824
7891
|
* Delete a customer by ID.
|
|
@@ -7834,18 +7901,21 @@ var CustomersClient = class {
|
|
|
7834
7901
|
});
|
|
7835
7902
|
}
|
|
7836
7903
|
/**
|
|
7837
|
-
* Create a Stripe checkout session for a customer.
|
|
7904
|
+
* Create a Stripe checkout session for a customer subscription.
|
|
7838
7905
|
*/
|
|
7839
|
-
async
|
|
7906
|
+
async subscribe(customerId, request) {
|
|
7840
7907
|
this.ensureSecretKey();
|
|
7841
7908
|
if (!customerId?.trim()) {
|
|
7842
7909
|
throw new ConfigError("customerId is required");
|
|
7843
7910
|
}
|
|
7911
|
+
if (!request.tier_id?.trim()) {
|
|
7912
|
+
throw new ConfigError("tier_id is required");
|
|
7913
|
+
}
|
|
7844
7914
|
if (!request.success_url?.trim() || !request.cancel_url?.trim()) {
|
|
7845
7915
|
throw new ConfigError("success_url and cancel_url are required");
|
|
7846
7916
|
}
|
|
7847
7917
|
return await this.http.json(
|
|
7848
|
-
`/customers/${customerId}/
|
|
7918
|
+
`/customers/${customerId}/subscribe`,
|
|
7849
7919
|
{
|
|
7850
7920
|
method: "POST",
|
|
7851
7921
|
body: request,
|
|
@@ -7854,20 +7924,34 @@ var CustomersClient = class {
|
|
|
7854
7924
|
);
|
|
7855
7925
|
}
|
|
7856
7926
|
/**
|
|
7857
|
-
* Get the subscription
|
|
7927
|
+
* Get the subscription details for a customer.
|
|
7858
7928
|
*/
|
|
7859
7929
|
async getSubscription(customerId) {
|
|
7860
7930
|
this.ensureSecretKey();
|
|
7861
7931
|
if (!customerId?.trim()) {
|
|
7862
7932
|
throw new ConfigError("customerId is required");
|
|
7863
7933
|
}
|
|
7864
|
-
|
|
7934
|
+
const response = await this.http.json(
|
|
7865
7935
|
`/customers/${customerId}/subscription`,
|
|
7866
7936
|
{
|
|
7867
7937
|
method: "GET",
|
|
7868
7938
|
apiKey: this.apiKey
|
|
7869
7939
|
}
|
|
7870
7940
|
);
|
|
7941
|
+
return response.subscription;
|
|
7942
|
+
}
|
|
7943
|
+
/**
|
|
7944
|
+
* Cancel a customer's subscription at period end.
|
|
7945
|
+
*/
|
|
7946
|
+
async unsubscribe(customerId) {
|
|
7947
|
+
this.ensureSecretKey();
|
|
7948
|
+
if (!customerId?.trim()) {
|
|
7949
|
+
throw new ConfigError("customerId is required");
|
|
7950
|
+
}
|
|
7951
|
+
await this.http.request(`/customers/${customerId}/subscription`, {
|
|
7952
|
+
method: "DELETE",
|
|
7953
|
+
apiKey: this.apiKey
|
|
7954
|
+
});
|
|
7871
7955
|
}
|
|
7872
7956
|
};
|
|
7873
7957
|
|
|
@@ -7969,903 +8053,1908 @@ var ModelsClient = class {
|
|
|
7969
8053
|
if (params.capability) {
|
|
7970
8054
|
qs.set("capability", params.capability);
|
|
7971
8055
|
}
|
|
7972
|
-
const
|
|
7973
|
-
const resp = await this.http.json(
|
|
8056
|
+
const path2 = qs.toString() ? `/models?${qs.toString()}` : "/models";
|
|
8057
|
+
const resp = await this.http.json(path2, { method: "GET" });
|
|
7974
8058
|
return resp.models;
|
|
7975
8059
|
}
|
|
7976
8060
|
};
|
|
7977
8061
|
|
|
7978
|
-
// src/
|
|
7979
|
-
var
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
|
|
8062
|
+
// src/images.ts
|
|
8063
|
+
var IMAGES_PATH = "/images/generate";
|
|
8064
|
+
var ImagesClient = class {
|
|
8065
|
+
constructor(http, auth) {
|
|
8066
|
+
this.http = http;
|
|
8067
|
+
this.auth = auth;
|
|
8068
|
+
}
|
|
8069
|
+
/**
|
|
8070
|
+
* Generate images from a text prompt.
|
|
8071
|
+
*
|
|
8072
|
+
* By default, returns URLs (requires storage configuration).
|
|
8073
|
+
* Use response_format: "b64_json" for testing without storage.
|
|
8074
|
+
*
|
|
8075
|
+
* @param request - Image generation request (model optional if tier defines default)
|
|
8076
|
+
* @returns Generated images with URLs or base64 data
|
|
8077
|
+
* @throws {Error} If prompt is empty
|
|
8078
|
+
*/
|
|
8079
|
+
async generate(request) {
|
|
8080
|
+
if (!request.prompt?.trim()) {
|
|
8081
|
+
throw new Error("prompt is required");
|
|
7986
8082
|
}
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
this.retry = normalizeRetryConfig(cfg.retry);
|
|
7995
|
-
this.defaultHeaders = normalizeHeaders(cfg.defaultHeaders);
|
|
7996
|
-
this.metrics = cfg.metrics;
|
|
7997
|
-
this.trace = cfg.trace;
|
|
8083
|
+
const auth = await this.auth.authForResponses();
|
|
8084
|
+
return await this.http.json(IMAGES_PATH, {
|
|
8085
|
+
method: "POST",
|
|
8086
|
+
body: request,
|
|
8087
|
+
apiKey: auth.apiKey,
|
|
8088
|
+
accessToken: auth.accessToken
|
|
8089
|
+
});
|
|
7998
8090
|
}
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8091
|
+
};
|
|
8092
|
+
|
|
8093
|
+
// src/sessions/types.ts
|
|
8094
|
+
function asSessionId(value) {
|
|
8095
|
+
return value;
|
|
8096
|
+
}
|
|
8097
|
+
function generateSessionId() {
|
|
8098
|
+
return crypto.randomUUID();
|
|
8099
|
+
}
|
|
8100
|
+
|
|
8101
|
+
// src/sessions/stores/memory_store.ts
|
|
8102
|
+
var MemorySessionStore = class {
|
|
8103
|
+
constructor() {
|
|
8104
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
8105
|
+
}
|
|
8106
|
+
async load(id) {
|
|
8107
|
+
const state = this.sessions.get(id);
|
|
8108
|
+
if (!state) return null;
|
|
8109
|
+
return structuredClone(state);
|
|
8110
|
+
}
|
|
8111
|
+
async save(state) {
|
|
8112
|
+
this.sessions.set(state.id, structuredClone(state));
|
|
8113
|
+
}
|
|
8114
|
+
async delete(id) {
|
|
8115
|
+
this.sessions.delete(id);
|
|
8116
|
+
}
|
|
8117
|
+
async list() {
|
|
8118
|
+
return Array.from(this.sessions.keys());
|
|
8119
|
+
}
|
|
8120
|
+
async close() {
|
|
8121
|
+
this.sessions.clear();
|
|
8122
|
+
}
|
|
8123
|
+
/**
|
|
8124
|
+
* Get the number of sessions in the store.
|
|
8125
|
+
* Useful for testing.
|
|
8126
|
+
*/
|
|
8127
|
+
get size() {
|
|
8128
|
+
return this.sessions.size;
|
|
8129
|
+
}
|
|
8130
|
+
};
|
|
8131
|
+
function createMemorySessionStore() {
|
|
8132
|
+
return new MemorySessionStore();
|
|
8133
|
+
}
|
|
8134
|
+
|
|
8135
|
+
// src/sessions/local_session.ts
|
|
8136
|
+
var LocalSession = class _LocalSession {
|
|
8137
|
+
constructor(client, store, options, existingState) {
|
|
8138
|
+
this.type = "local";
|
|
8139
|
+
this.messages = [];
|
|
8140
|
+
this.artifacts = /* @__PURE__ */ new Map();
|
|
8141
|
+
this.nextSeq = 1;
|
|
8142
|
+
this.currentEvents = [];
|
|
8143
|
+
this.currentUsage = {
|
|
8144
|
+
inputTokens: 0,
|
|
8145
|
+
outputTokens: 0,
|
|
8146
|
+
totalTokens: 0,
|
|
8147
|
+
llmCalls: 0,
|
|
8148
|
+
toolCalls: 0
|
|
8149
|
+
};
|
|
8150
|
+
this.client = client;
|
|
8151
|
+
this.store = store;
|
|
8152
|
+
this.toolRegistry = options.toolRegistry;
|
|
8153
|
+
this.defaultModel = options.defaultModel;
|
|
8154
|
+
this.defaultProvider = options.defaultProvider;
|
|
8155
|
+
this.defaultTools = options.defaultTools;
|
|
8156
|
+
this.metadata = options.metadata || {};
|
|
8157
|
+
if (existingState) {
|
|
8158
|
+
this.id = existingState.id;
|
|
8159
|
+
this.messages = existingState.messages.map((m) => ({
|
|
8160
|
+
...m,
|
|
8161
|
+
createdAt: new Date(m.createdAt)
|
|
8162
|
+
}));
|
|
8163
|
+
this.artifacts = new Map(Object.entries(existingState.artifacts));
|
|
8164
|
+
this.nextSeq = this.messages.length + 1;
|
|
8165
|
+
this.createdAt = new Date(existingState.createdAt);
|
|
8166
|
+
this.updatedAt = new Date(existingState.updatedAt);
|
|
8167
|
+
} else {
|
|
8168
|
+
this.id = options.sessionId || generateSessionId();
|
|
8169
|
+
this.createdAt = /* @__PURE__ */ new Date();
|
|
8170
|
+
this.updatedAt = /* @__PURE__ */ new Date();
|
|
8005
8171
|
}
|
|
8006
|
-
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8172
|
+
}
|
|
8173
|
+
/**
|
|
8174
|
+
* Create a new local session.
|
|
8175
|
+
*
|
|
8176
|
+
* @param client - ModelRelay client
|
|
8177
|
+
* @param options - Session configuration
|
|
8178
|
+
* @returns A new LocalSession instance
|
|
8179
|
+
*/
|
|
8180
|
+
static create(client, options = {}) {
|
|
8181
|
+
const store = createStore(options.persistence || "memory", options.storagePath);
|
|
8182
|
+
return new _LocalSession(client, store, options);
|
|
8183
|
+
}
|
|
8184
|
+
/**
|
|
8185
|
+
* Resume an existing session from storage.
|
|
8186
|
+
*
|
|
8187
|
+
* @param client - ModelRelay client
|
|
8188
|
+
* @param sessionId - ID of the session to resume
|
|
8189
|
+
* @param options - Session configuration (must match original persistence settings)
|
|
8190
|
+
* @returns The resumed LocalSession, or null if not found
|
|
8191
|
+
*/
|
|
8192
|
+
static async resume(client, sessionId, options = {}) {
|
|
8193
|
+
const id = typeof sessionId === "string" ? asSessionId(sessionId) : sessionId;
|
|
8194
|
+
const store = createStore(options.persistence || "memory", options.storagePath);
|
|
8195
|
+
const state = await store.load(id);
|
|
8196
|
+
if (!state) {
|
|
8197
|
+
await store.close();
|
|
8198
|
+
return null;
|
|
8199
|
+
}
|
|
8200
|
+
return new _LocalSession(client, store, options, state);
|
|
8201
|
+
}
|
|
8202
|
+
get history() {
|
|
8203
|
+
return this.messages;
|
|
8204
|
+
}
|
|
8205
|
+
async run(prompt, options = {}) {
|
|
8206
|
+
const userMessage = this.addMessage({
|
|
8207
|
+
type: "message",
|
|
8208
|
+
role: "user",
|
|
8209
|
+
content: [{ type: "text", text: prompt }]
|
|
8210
|
+
});
|
|
8211
|
+
this.currentEvents = [];
|
|
8212
|
+
this.currentUsage = {
|
|
8213
|
+
inputTokens: 0,
|
|
8214
|
+
outputTokens: 0,
|
|
8215
|
+
totalTokens: 0,
|
|
8216
|
+
llmCalls: 0,
|
|
8217
|
+
toolCalls: 0
|
|
8014
8218
|
};
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8219
|
+
this.currentRunId = void 0;
|
|
8220
|
+
this.currentNodeId = void 0;
|
|
8221
|
+
this.currentWaiting = void 0;
|
|
8222
|
+
try {
|
|
8223
|
+
const input = this.buildInput();
|
|
8224
|
+
const tools = mergeTools(this.defaultTools, options.tools);
|
|
8225
|
+
const spec = {
|
|
8226
|
+
kind: "workflow.v0",
|
|
8227
|
+
name: `session-${this.id}-turn-${this.nextSeq}`,
|
|
8228
|
+
nodes: [
|
|
8229
|
+
{
|
|
8230
|
+
id: "main",
|
|
8231
|
+
type: "llm.responses",
|
|
8232
|
+
input: {
|
|
8233
|
+
request: {
|
|
8234
|
+
provider: options.provider || this.defaultProvider,
|
|
8235
|
+
model: options.model || this.defaultModel,
|
|
8236
|
+
input,
|
|
8237
|
+
tools
|
|
8238
|
+
},
|
|
8239
|
+
tool_execution: this.toolRegistry ? { mode: "client" } : void 0
|
|
8240
|
+
}
|
|
8241
|
+
}
|
|
8242
|
+
],
|
|
8243
|
+
outputs: [{ name: "result", from: "main" }]
|
|
8244
|
+
};
|
|
8245
|
+
const run = await this.client.runs.create(spec, {
|
|
8246
|
+
customerId: options.customerId
|
|
8247
|
+
});
|
|
8248
|
+
this.currentRunId = run.run_id;
|
|
8249
|
+
return await this.processRunEvents(options.signal);
|
|
8250
|
+
} catch (err) {
|
|
8251
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
8252
|
+
return {
|
|
8253
|
+
status: "error",
|
|
8254
|
+
error: error.message,
|
|
8255
|
+
runId: this.currentRunId || parseRunId("unknown"),
|
|
8256
|
+
usage: this.currentUsage,
|
|
8257
|
+
events: this.currentEvents
|
|
8258
|
+
};
|
|
8024
8259
|
}
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
headers.set("Content-Type", "application/json");
|
|
8260
|
+
}
|
|
8261
|
+
async submitToolResults(results) {
|
|
8262
|
+
if (!this.currentRunId || !this.currentNodeId || !this.currentWaiting) {
|
|
8263
|
+
throw new Error("No pending tool calls to submit results for");
|
|
8030
8264
|
}
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8265
|
+
await this.client.runs.submitToolResults(this.currentRunId, {
|
|
8266
|
+
node_id: this.currentNodeId,
|
|
8267
|
+
step: this.currentWaiting.step,
|
|
8268
|
+
request_id: this.currentWaiting.request_id,
|
|
8269
|
+
results: results.map((r) => ({
|
|
8270
|
+
tool_call_id: r.toolCallId,
|
|
8271
|
+
name: r.toolName,
|
|
8272
|
+
output: r.error ? `Error: ${r.error}` : typeof r.result === "string" ? r.result : JSON.stringify(r.result)
|
|
8273
|
+
}))
|
|
8274
|
+
});
|
|
8275
|
+
this.currentWaiting = void 0;
|
|
8276
|
+
return await this.processRunEvents();
|
|
8277
|
+
}
|
|
8278
|
+
getArtifacts() {
|
|
8279
|
+
return new Map(this.artifacts);
|
|
8280
|
+
}
|
|
8281
|
+
async close() {
|
|
8282
|
+
await this.persist();
|
|
8283
|
+
await this.store.close();
|
|
8284
|
+
}
|
|
8285
|
+
// ============================================================================
|
|
8286
|
+
// Private Methods
|
|
8287
|
+
// ============================================================================
|
|
8288
|
+
addMessage(input, runId) {
|
|
8289
|
+
const message = {
|
|
8290
|
+
...input,
|
|
8291
|
+
seq: this.nextSeq++,
|
|
8292
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
8293
|
+
runId
|
|
8294
|
+
};
|
|
8295
|
+
this.messages.push(message);
|
|
8296
|
+
this.updatedAt = /* @__PURE__ */ new Date();
|
|
8297
|
+
return message;
|
|
8298
|
+
}
|
|
8299
|
+
buildInput() {
|
|
8300
|
+
return this.messages.map((m) => ({
|
|
8301
|
+
type: m.type,
|
|
8302
|
+
role: m.role,
|
|
8303
|
+
content: m.content,
|
|
8304
|
+
toolCalls: m.toolCalls,
|
|
8305
|
+
toolCallId: m.toolCallId
|
|
8306
|
+
}));
|
|
8307
|
+
}
|
|
8308
|
+
async processRunEvents(signal) {
|
|
8309
|
+
if (!this.currentRunId) {
|
|
8310
|
+
throw new Error("No current run");
|
|
8035
8311
|
}
|
|
8036
|
-
const
|
|
8037
|
-
|
|
8038
|
-
|
|
8312
|
+
const eventStream = await this.client.runs.events(this.currentRunId, {
|
|
8313
|
+
afterSeq: this.currentEvents.length
|
|
8314
|
+
});
|
|
8315
|
+
for await (const event of eventStream) {
|
|
8316
|
+
if (signal?.aborted) {
|
|
8317
|
+
return {
|
|
8318
|
+
status: "canceled",
|
|
8319
|
+
runId: this.currentRunId,
|
|
8320
|
+
usage: this.currentUsage,
|
|
8321
|
+
events: this.currentEvents
|
|
8322
|
+
};
|
|
8323
|
+
}
|
|
8324
|
+
this.currentEvents.push(event);
|
|
8325
|
+
switch (event.type) {
|
|
8326
|
+
case "node_llm_call":
|
|
8327
|
+
this.currentUsage = {
|
|
8328
|
+
...this.currentUsage,
|
|
8329
|
+
llmCalls: this.currentUsage.llmCalls + 1,
|
|
8330
|
+
inputTokens: this.currentUsage.inputTokens + (event.llm_call.usage?.input_tokens || 0),
|
|
8331
|
+
outputTokens: this.currentUsage.outputTokens + (event.llm_call.usage?.output_tokens || 0),
|
|
8332
|
+
totalTokens: this.currentUsage.totalTokens + (event.llm_call.usage?.total_tokens || 0)
|
|
8333
|
+
};
|
|
8334
|
+
break;
|
|
8335
|
+
case "node_tool_call":
|
|
8336
|
+
this.currentUsage = {
|
|
8337
|
+
...this.currentUsage,
|
|
8338
|
+
toolCalls: this.currentUsage.toolCalls + 1
|
|
8339
|
+
};
|
|
8340
|
+
break;
|
|
8341
|
+
case "node_waiting":
|
|
8342
|
+
this.currentNodeId = event.node_id;
|
|
8343
|
+
this.currentWaiting = event.waiting;
|
|
8344
|
+
if (this.toolRegistry) {
|
|
8345
|
+
const results = await this.executeTools(event.waiting.pending_tool_calls);
|
|
8346
|
+
return await this.submitToolResults(results);
|
|
8347
|
+
}
|
|
8348
|
+
return {
|
|
8349
|
+
status: "waiting_for_tools",
|
|
8350
|
+
pendingTools: event.waiting.pending_tool_calls.map((tc) => ({
|
|
8351
|
+
toolCallId: tc.tool_call_id,
|
|
8352
|
+
name: tc.name,
|
|
8353
|
+
arguments: tc.arguments
|
|
8354
|
+
})),
|
|
8355
|
+
runId: this.currentRunId,
|
|
8356
|
+
usage: this.currentUsage,
|
|
8357
|
+
events: this.currentEvents
|
|
8358
|
+
};
|
|
8359
|
+
case "run_completed":
|
|
8360
|
+
const runState = await this.client.runs.get(this.currentRunId);
|
|
8361
|
+
const output = extractTextOutput(runState.outputs || {});
|
|
8362
|
+
if (output) {
|
|
8363
|
+
this.addMessage(
|
|
8364
|
+
{
|
|
8365
|
+
type: "message",
|
|
8366
|
+
role: "assistant",
|
|
8367
|
+
content: [{ type: "text", text: output }]
|
|
8368
|
+
},
|
|
8369
|
+
this.currentRunId
|
|
8370
|
+
);
|
|
8371
|
+
}
|
|
8372
|
+
await this.persist();
|
|
8373
|
+
return {
|
|
8374
|
+
status: "complete",
|
|
8375
|
+
output,
|
|
8376
|
+
runId: this.currentRunId,
|
|
8377
|
+
usage: this.currentUsage,
|
|
8378
|
+
events: this.currentEvents
|
|
8379
|
+
};
|
|
8380
|
+
case "run_failed":
|
|
8381
|
+
return {
|
|
8382
|
+
status: "error",
|
|
8383
|
+
error: event.error.message,
|
|
8384
|
+
runId: this.currentRunId,
|
|
8385
|
+
usage: this.currentUsage,
|
|
8386
|
+
events: this.currentEvents
|
|
8387
|
+
};
|
|
8388
|
+
case "run_canceled":
|
|
8389
|
+
return {
|
|
8390
|
+
status: "canceled",
|
|
8391
|
+
error: event.error.message,
|
|
8392
|
+
runId: this.currentRunId,
|
|
8393
|
+
usage: this.currentUsage,
|
|
8394
|
+
events: this.currentEvents
|
|
8395
|
+
};
|
|
8396
|
+
}
|
|
8039
8397
|
}
|
|
8040
|
-
|
|
8041
|
-
|
|
8398
|
+
return {
|
|
8399
|
+
status: "error",
|
|
8400
|
+
error: "Run event stream ended unexpectedly",
|
|
8401
|
+
runId: this.currentRunId,
|
|
8402
|
+
usage: this.currentUsage,
|
|
8403
|
+
events: this.currentEvents
|
|
8404
|
+
};
|
|
8405
|
+
}
|
|
8406
|
+
async executeTools(pendingTools) {
|
|
8407
|
+
if (!this.toolRegistry) {
|
|
8408
|
+
throw new Error("No tool registry configured");
|
|
8042
8409
|
}
|
|
8043
|
-
const
|
|
8044
|
-
const
|
|
8045
|
-
const retryCfg = normalizeRetryConfig(
|
|
8046
|
-
options.retry === void 0 ? this.retry : options.retry
|
|
8047
|
-
);
|
|
8048
|
-
const attempts = retryCfg ? Math.max(1, retryCfg.maxAttempts) : 1;
|
|
8049
|
-
let lastError;
|
|
8050
|
-
let lastStatus;
|
|
8051
|
-
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
8052
|
-
let connectTimedOut = false;
|
|
8053
|
-
let requestTimedOut = false;
|
|
8054
|
-
const connectController = connectTimeoutMs && connectTimeoutMs > 0 ? new AbortController() : void 0;
|
|
8055
|
-
const requestController = timeoutMs && timeoutMs > 0 ? new AbortController() : void 0;
|
|
8056
|
-
const signal = mergeSignals(
|
|
8057
|
-
options.signal,
|
|
8058
|
-
connectController?.signal,
|
|
8059
|
-
requestController?.signal
|
|
8060
|
-
);
|
|
8061
|
-
const connectTimer = connectController && setTimeout(() => {
|
|
8062
|
-
connectTimedOut = true;
|
|
8063
|
-
connectController.abort(
|
|
8064
|
-
new DOMException("connect timeout", "AbortError")
|
|
8065
|
-
);
|
|
8066
|
-
}, connectTimeoutMs);
|
|
8067
|
-
const requestTimer = requestController && setTimeout(() => {
|
|
8068
|
-
requestTimedOut = true;
|
|
8069
|
-
requestController.abort(
|
|
8070
|
-
new DOMException("timeout", "AbortError")
|
|
8071
|
-
);
|
|
8072
|
-
}, timeoutMs);
|
|
8410
|
+
const results = [];
|
|
8411
|
+
for (const pending of pendingTools) {
|
|
8073
8412
|
try {
|
|
8074
|
-
const
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
if (connectTimer) {
|
|
8081
|
-
clearTimeout(connectTimer);
|
|
8082
|
-
}
|
|
8083
|
-
if (!response.ok) {
|
|
8084
|
-
const shouldRetry = retryCfg && shouldRetryStatus(
|
|
8085
|
-
response.status,
|
|
8086
|
-
method,
|
|
8087
|
-
retryCfg.retryPost
|
|
8088
|
-
) && attempt < attempts;
|
|
8089
|
-
if (shouldRetry) {
|
|
8090
|
-
lastStatus = response.status;
|
|
8091
|
-
await backoff(attempt, retryCfg);
|
|
8092
|
-
continue;
|
|
8413
|
+
const result = await this.toolRegistry.execute({
|
|
8414
|
+
id: pending.tool_call_id,
|
|
8415
|
+
type: "function",
|
|
8416
|
+
function: {
|
|
8417
|
+
name: pending.name,
|
|
8418
|
+
arguments: pending.arguments
|
|
8093
8419
|
}
|
|
8094
|
-
const retries = buildRetryMetadata(attempt, response.status, lastError);
|
|
8095
|
-
const finishedCtx2 = withRequestId(context, response.headers);
|
|
8096
|
-
recordHttpMetrics(metrics, trace, start, retries, {
|
|
8097
|
-
status: response.status,
|
|
8098
|
-
context: finishedCtx2
|
|
8099
|
-
});
|
|
8100
|
-
throw options.raw ? await parseErrorResponse(response, retries) : await parseErrorResponse(response, retries);
|
|
8101
|
-
}
|
|
8102
|
-
const finishedCtx = withRequestId(context, response.headers);
|
|
8103
|
-
recordHttpMetrics(metrics, trace, start, void 0, {
|
|
8104
|
-
status: response.status,
|
|
8105
|
-
context: finishedCtx
|
|
8106
8420
|
});
|
|
8107
|
-
|
|
8421
|
+
results.push(result);
|
|
8108
8422
|
} catch (err) {
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
});
|
|
8117
|
-
throw err;
|
|
8118
|
-
}
|
|
8119
|
-
const transportKind = classifyTransportErrorKind(
|
|
8120
|
-
err,
|
|
8121
|
-
connectTimedOut,
|
|
8122
|
-
requestTimedOut
|
|
8123
|
-
);
|
|
8124
|
-
const shouldRetry = retryCfg && isRetryableError(err, transportKind) && (method !== "POST" || retryCfg.retryPost) && attempt < attempts;
|
|
8125
|
-
if (!shouldRetry) {
|
|
8126
|
-
const retries = buildRetryMetadata(
|
|
8127
|
-
attempt,
|
|
8128
|
-
lastStatus,
|
|
8129
|
-
err instanceof Error ? err.message : String(err)
|
|
8130
|
-
);
|
|
8131
|
-
recordHttpMetrics(metrics, trace, start, retries, {
|
|
8132
|
-
error: err,
|
|
8133
|
-
context
|
|
8134
|
-
});
|
|
8135
|
-
throw toTransportError(err, transportKind, retries);
|
|
8136
|
-
}
|
|
8137
|
-
lastError = err;
|
|
8138
|
-
await backoff(attempt, retryCfg);
|
|
8139
|
-
} finally {
|
|
8140
|
-
if (connectTimer) {
|
|
8141
|
-
clearTimeout(connectTimer);
|
|
8142
|
-
}
|
|
8143
|
-
if (requestTimer) {
|
|
8144
|
-
clearTimeout(requestTimer);
|
|
8145
|
-
}
|
|
8423
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
8424
|
+
results.push({
|
|
8425
|
+
toolCallId: pending.tool_call_id,
|
|
8426
|
+
toolName: pending.name,
|
|
8427
|
+
result: null,
|
|
8428
|
+
error: error.message
|
|
8429
|
+
});
|
|
8146
8430
|
}
|
|
8147
8431
|
}
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
}
|
|
8162
|
-
|
|
8163
|
-
return void 0;
|
|
8164
|
-
}
|
|
8165
|
-
try {
|
|
8166
|
-
return await response.json();
|
|
8167
|
-
} catch (err) {
|
|
8168
|
-
throw new APIError("failed to parse response JSON", {
|
|
8169
|
-
status: response.status,
|
|
8170
|
-
data: err
|
|
8171
|
-
});
|
|
8172
|
-
}
|
|
8432
|
+
return results;
|
|
8433
|
+
}
|
|
8434
|
+
async persist() {
|
|
8435
|
+
const state = {
|
|
8436
|
+
id: this.id,
|
|
8437
|
+
messages: this.messages.map((m) => ({
|
|
8438
|
+
...m,
|
|
8439
|
+
createdAt: m.createdAt
|
|
8440
|
+
})),
|
|
8441
|
+
artifacts: Object.fromEntries(this.artifacts),
|
|
8442
|
+
metadata: this.metadata,
|
|
8443
|
+
createdAt: this.createdAt.toISOString(),
|
|
8444
|
+
updatedAt: this.updatedAt.toISOString()
|
|
8445
|
+
};
|
|
8446
|
+
await this.store.save(state);
|
|
8173
8447
|
}
|
|
8174
8448
|
};
|
|
8175
|
-
function
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
const trimmed = value.trim();
|
|
8186
|
-
if (trimmed.endsWith("/")) {
|
|
8187
|
-
return trimmed.slice(0, -1);
|
|
8449
|
+
function createStore(persistence, storagePath) {
|
|
8450
|
+
switch (persistence) {
|
|
8451
|
+
case "memory":
|
|
8452
|
+
return createMemorySessionStore();
|
|
8453
|
+
case "file":
|
|
8454
|
+
throw new Error("File persistence not yet implemented");
|
|
8455
|
+
case "sqlite":
|
|
8456
|
+
throw new Error("SQLite persistence not yet implemented");
|
|
8457
|
+
default:
|
|
8458
|
+
throw new Error(`Unknown persistence mode: ${persistence}`);
|
|
8188
8459
|
}
|
|
8189
|
-
return trimmed;
|
|
8190
|
-
}
|
|
8191
|
-
function isValidHttpUrl(value) {
|
|
8192
|
-
return /^https?:\/\//i.test(value);
|
|
8193
8460
|
}
|
|
8194
|
-
function
|
|
8195
|
-
if (
|
|
8196
|
-
|
|
8197
|
-
return
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
}
|
|
8204
|
-
function shouldRetryStatus(status, method, retryPost) {
|
|
8205
|
-
if (status === 408 || status === 429) {
|
|
8206
|
-
return method !== "POST" || retryPost;
|
|
8207
|
-
}
|
|
8208
|
-
if (status >= 500 && status < 600) {
|
|
8209
|
-
return method !== "POST" || retryPost;
|
|
8461
|
+
function mergeTools(defaults, overrides) {
|
|
8462
|
+
if (!defaults && !overrides) return void 0;
|
|
8463
|
+
if (!defaults) return overrides;
|
|
8464
|
+
if (!overrides) return defaults;
|
|
8465
|
+
const merged = /* @__PURE__ */ new Map();
|
|
8466
|
+
for (const tool of defaults) {
|
|
8467
|
+
if (tool.type === "function" && tool.function) {
|
|
8468
|
+
merged.set(tool.function.name, tool);
|
|
8469
|
+
}
|
|
8210
8470
|
}
|
|
8211
|
-
|
|
8212
|
-
|
|
8213
|
-
function
|
|
8214
|
-
if (!err) return false;
|
|
8215
|
-
if (kind === "timeout" || kind === "connect") return true;
|
|
8216
|
-
return err instanceof DOMException || err instanceof TypeError;
|
|
8217
|
-
}
|
|
8218
|
-
function backoff(attempt, cfg) {
|
|
8219
|
-
const exp = Math.max(0, attempt - 1);
|
|
8220
|
-
const base = cfg.baseBackoffMs * Math.pow(2, Math.min(exp, 10));
|
|
8221
|
-
const capped = Math.min(base, cfg.maxBackoffMs);
|
|
8222
|
-
const jitter = 0.5 + Math.random();
|
|
8223
|
-
const delay = Math.min(cfg.maxBackoffMs, capped * jitter);
|
|
8224
|
-
if (delay <= 0) return Promise.resolve();
|
|
8225
|
-
return new Promise((resolve) => setTimeout(resolve, delay));
|
|
8226
|
-
}
|
|
8227
|
-
function mergeSignals(...signals) {
|
|
8228
|
-
const active = signals.filter(Boolean);
|
|
8229
|
-
if (active.length === 0) return void 0;
|
|
8230
|
-
if (active.length === 1) return active[0];
|
|
8231
|
-
const controller = new AbortController();
|
|
8232
|
-
for (const src of active) {
|
|
8233
|
-
if (src.aborted) {
|
|
8234
|
-
controller.abort(src.reason);
|
|
8235
|
-
break;
|
|
8471
|
+
for (const tool of overrides) {
|
|
8472
|
+
if (tool.type === "function" && tool.function) {
|
|
8473
|
+
merged.set(tool.function.name, tool);
|
|
8236
8474
|
}
|
|
8237
|
-
src.addEventListener(
|
|
8238
|
-
"abort",
|
|
8239
|
-
() => controller.abort(src.reason),
|
|
8240
|
-
{ once: true }
|
|
8241
|
-
);
|
|
8242
8475
|
}
|
|
8243
|
-
return
|
|
8476
|
+
return Array.from(merged.values());
|
|
8244
8477
|
}
|
|
8245
|
-
function
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8478
|
+
function extractTextOutput(outputs) {
|
|
8479
|
+
const result = outputs.result;
|
|
8480
|
+
if (typeof result === "string") return result;
|
|
8481
|
+
if (result && typeof result === "object") {
|
|
8482
|
+
const resp = result;
|
|
8483
|
+
if (Array.isArray(resp.output)) {
|
|
8484
|
+
const textParts = resp.output.filter((item) => item?.type === "message" && item?.role === "assistant").flatMap(
|
|
8485
|
+
(item) => (item.content || []).filter((c) => c?.type === "text").map((c) => c.text)
|
|
8486
|
+
);
|
|
8487
|
+
if (textParts.length > 0) {
|
|
8488
|
+
return textParts.join("\n");
|
|
8489
|
+
}
|
|
8490
|
+
}
|
|
8491
|
+
if (Array.isArray(resp.content)) {
|
|
8492
|
+
const textParts = resp.content.filter((c) => c?.type === "text").map((c) => c.text);
|
|
8493
|
+
if (textParts.length > 0) {
|
|
8494
|
+
return textParts.join("\n");
|
|
8495
|
+
}
|
|
8254
8496
|
}
|
|
8255
8497
|
}
|
|
8256
|
-
return
|
|
8498
|
+
return void 0;
|
|
8257
8499
|
}
|
|
8258
|
-
function
|
|
8259
|
-
|
|
8260
|
-
return {
|
|
8261
|
-
attempts: attempt,
|
|
8262
|
-
lastStatus,
|
|
8263
|
-
lastError: typeof lastError === "string" ? lastError : lastError instanceof Error ? lastError.message : lastError ? String(lastError) : void 0
|
|
8264
|
-
};
|
|
8500
|
+
function createLocalSession(client, options = {}) {
|
|
8501
|
+
return LocalSession.create(client, options);
|
|
8265
8502
|
}
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8503
|
+
|
|
8504
|
+
// src/sessions/remote_session.ts
|
|
8505
|
+
var RemoteSession = class _RemoteSession {
|
|
8506
|
+
constructor(client, http, sessionData, options = {}) {
|
|
8507
|
+
this.type = "remote";
|
|
8508
|
+
this.messages = [];
|
|
8509
|
+
this.artifacts = /* @__PURE__ */ new Map();
|
|
8510
|
+
this.nextSeq = 1;
|
|
8511
|
+
this.currentEvents = [];
|
|
8512
|
+
this.currentUsage = {
|
|
8513
|
+
inputTokens: 0,
|
|
8514
|
+
outputTokens: 0,
|
|
8515
|
+
totalTokens: 0,
|
|
8516
|
+
llmCalls: 0,
|
|
8517
|
+
toolCalls: 0
|
|
8518
|
+
};
|
|
8519
|
+
this.client = client;
|
|
8520
|
+
this.http = http;
|
|
8521
|
+
this.id = asSessionId(sessionData.id);
|
|
8522
|
+
this.metadata = sessionData.metadata;
|
|
8523
|
+
this.endUserId = sessionData.end_user_id || options.endUserId;
|
|
8524
|
+
this.createdAt = new Date(sessionData.created_at);
|
|
8525
|
+
this.updatedAt = new Date(sessionData.updated_at);
|
|
8526
|
+
this.toolRegistry = options.toolRegistry;
|
|
8527
|
+
this.defaultModel = options.defaultModel;
|
|
8528
|
+
this.defaultProvider = options.defaultProvider;
|
|
8529
|
+
this.defaultTools = options.defaultTools;
|
|
8530
|
+
if ("messages" in sessionData && sessionData.messages) {
|
|
8531
|
+
this.messages = sessionData.messages.map((m) => ({
|
|
8532
|
+
type: "message",
|
|
8533
|
+
role: m.role,
|
|
8534
|
+
content: m.content,
|
|
8535
|
+
seq: m.seq,
|
|
8536
|
+
createdAt: new Date(m.created_at),
|
|
8537
|
+
runId: m.run_id ? parseRunId(m.run_id) : void 0
|
|
8538
|
+
}));
|
|
8539
|
+
this.nextSeq = this.messages.length + 1;
|
|
8540
|
+
}
|
|
8271
8541
|
}
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
retries,
|
|
8288
|
-
context: info.context
|
|
8542
|
+
/**
|
|
8543
|
+
* Create a new remote session on the server.
|
|
8544
|
+
*
|
|
8545
|
+
* @param client - ModelRelay client
|
|
8546
|
+
* @param options - Session configuration
|
|
8547
|
+
* @returns A new RemoteSession instance
|
|
8548
|
+
*/
|
|
8549
|
+
static async create(client, options = {}) {
|
|
8550
|
+
const http = getHTTPClient(client);
|
|
8551
|
+
const response = await http.request("/sessions", {
|
|
8552
|
+
method: "POST",
|
|
8553
|
+
body: {
|
|
8554
|
+
end_user_id: options.endUserId,
|
|
8555
|
+
metadata: options.metadata || {}
|
|
8556
|
+
}
|
|
8289
8557
|
});
|
|
8558
|
+
const data = await response.json();
|
|
8559
|
+
return new _RemoteSession(client, http, data, options);
|
|
8290
8560
|
}
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
// src/customer_scoped.ts
|
|
8306
|
-
function normalizeCustomerId(customerId) {
|
|
8307
|
-
const trimmed = customerId?.trim?.() ? customerId.trim() : "";
|
|
8308
|
-
if (!trimmed) {
|
|
8309
|
-
throw new ConfigError("customerId is required");
|
|
8310
|
-
}
|
|
8311
|
-
return trimmed;
|
|
8312
|
-
}
|
|
8313
|
-
function mergeCustomerOptions(customerId, options = {}) {
|
|
8314
|
-
if (options.customerId && options.customerId !== customerId) {
|
|
8315
|
-
throw new ConfigError("customerId mismatch", {
|
|
8316
|
-
expected: customerId,
|
|
8317
|
-
received: options.customerId
|
|
8561
|
+
/**
|
|
8562
|
+
* Get an existing remote session by ID.
|
|
8563
|
+
*
|
|
8564
|
+
* @param client - ModelRelay client
|
|
8565
|
+
* @param sessionId - ID of the session to retrieve
|
|
8566
|
+
* @param options - Optional configuration (toolRegistry, defaults)
|
|
8567
|
+
* @returns The RemoteSession instance
|
|
8568
|
+
*/
|
|
8569
|
+
static async get(client, sessionId, options = {}) {
|
|
8570
|
+
const http = getHTTPClient(client);
|
|
8571
|
+
const id = typeof sessionId === "string" ? sessionId : String(sessionId);
|
|
8572
|
+
const response = await http.request(`/sessions/${id}`, {
|
|
8573
|
+
method: "GET"
|
|
8318
8574
|
});
|
|
8575
|
+
const data = await response.json();
|
|
8576
|
+
return new _RemoteSession(client, http, data, options);
|
|
8319
8577
|
}
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8578
|
+
/**
|
|
8579
|
+
* List remote sessions.
|
|
8580
|
+
*
|
|
8581
|
+
* @param client - ModelRelay client
|
|
8582
|
+
* @param options - List options
|
|
8583
|
+
* @returns Paginated list of session info
|
|
8584
|
+
*/
|
|
8585
|
+
static async list(client, options = {}) {
|
|
8586
|
+
const http = getHTTPClient(client);
|
|
8587
|
+
const params = new URLSearchParams();
|
|
8588
|
+
if (options.limit) params.set("limit", String(options.limit));
|
|
8589
|
+
if (options.offset) params.set("offset", String(options.offset));
|
|
8590
|
+
if (options.endUserId) params.set("end_user_id", options.endUserId);
|
|
8591
|
+
const response = await http.request(
|
|
8592
|
+
`/sessions${params.toString() ? `?${params.toString()}` : ""}`,
|
|
8593
|
+
{ method: "GET" }
|
|
8594
|
+
);
|
|
8595
|
+
const data = await response.json();
|
|
8596
|
+
return {
|
|
8597
|
+
sessions: data.sessions.map((s) => ({
|
|
8598
|
+
id: asSessionId(s.id),
|
|
8599
|
+
messageCount: s.message_count,
|
|
8600
|
+
metadata: s.metadata,
|
|
8601
|
+
createdAt: new Date(s.created_at),
|
|
8602
|
+
updatedAt: new Date(s.updated_at)
|
|
8603
|
+
})),
|
|
8604
|
+
nextCursor: data.next_cursor
|
|
8605
|
+
};
|
|
8326
8606
|
}
|
|
8327
|
-
|
|
8328
|
-
|
|
8607
|
+
/**
|
|
8608
|
+
* Delete a remote session.
|
|
8609
|
+
*
|
|
8610
|
+
* @param client - ModelRelay client
|
|
8611
|
+
* @param sessionId - ID of the session to delete
|
|
8612
|
+
*/
|
|
8613
|
+
static async delete(client, sessionId) {
|
|
8614
|
+
const http = getHTTPClient(client);
|
|
8615
|
+
const id = typeof sessionId === "string" ? sessionId : String(sessionId);
|
|
8616
|
+
await http.request(`/sessions/${id}`, {
|
|
8617
|
+
method: "DELETE"
|
|
8618
|
+
});
|
|
8329
8619
|
}
|
|
8330
|
-
|
|
8331
|
-
|
|
8620
|
+
// ============================================================================
|
|
8621
|
+
// Session Interface Implementation
|
|
8622
|
+
// ============================================================================
|
|
8623
|
+
/**
|
|
8624
|
+
* Full conversation history (read-only).
|
|
8625
|
+
*/
|
|
8626
|
+
get history() {
|
|
8627
|
+
return this.messages;
|
|
8332
8628
|
}
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8629
|
+
/**
|
|
8630
|
+
* Execute a prompt as a new turn in this session.
|
|
8631
|
+
*/
|
|
8632
|
+
async run(prompt, options = {}) {
|
|
8633
|
+
const userMessage = this.addMessage({
|
|
8634
|
+
type: "message",
|
|
8635
|
+
role: "user",
|
|
8636
|
+
content: [{ type: "text", text: prompt }]
|
|
8637
|
+
});
|
|
8638
|
+
this.resetRunState();
|
|
8639
|
+
try {
|
|
8640
|
+
const input = this.buildInput();
|
|
8641
|
+
const tools = mergeTools2(this.defaultTools, options.tools);
|
|
8642
|
+
const spec = {
|
|
8643
|
+
kind: "workflow.v0",
|
|
8644
|
+
name: `session-${this.id}-turn-${this.nextSeq}`,
|
|
8645
|
+
nodes: [
|
|
8646
|
+
{
|
|
8647
|
+
id: "main",
|
|
8648
|
+
type: "llm.responses",
|
|
8649
|
+
input: {
|
|
8650
|
+
request: {
|
|
8651
|
+
provider: options.provider || this.defaultProvider,
|
|
8652
|
+
model: options.model || this.defaultModel,
|
|
8653
|
+
input,
|
|
8654
|
+
tools
|
|
8655
|
+
},
|
|
8656
|
+
tool_execution: this.toolRegistry ? { mode: "client" } : void 0
|
|
8657
|
+
}
|
|
8658
|
+
}
|
|
8659
|
+
],
|
|
8660
|
+
outputs: [{ name: "result", from: "main" }]
|
|
8661
|
+
};
|
|
8662
|
+
const run = await this.client.runs.create(spec, {
|
|
8663
|
+
customerId: options.customerId || this.endUserId
|
|
8340
8664
|
});
|
|
8665
|
+
this.currentRunId = run.run_id;
|
|
8666
|
+
return await this.processRunEvents(options.signal);
|
|
8667
|
+
} catch (err) {
|
|
8668
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
8669
|
+
return {
|
|
8670
|
+
status: "error",
|
|
8671
|
+
error: error.message,
|
|
8672
|
+
runId: this.currentRunId || parseRunId("unknown"),
|
|
8673
|
+
usage: { ...this.currentUsage },
|
|
8674
|
+
events: [...this.currentEvents]
|
|
8675
|
+
};
|
|
8341
8676
|
}
|
|
8342
8677
|
}
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
|
|
8678
|
+
/**
|
|
8679
|
+
* Submit tool results for a waiting run.
|
|
8680
|
+
*/
|
|
8681
|
+
async submitToolResults(results) {
|
|
8682
|
+
if (!this.currentRunId || !this.currentNodeId || !this.currentWaiting) {
|
|
8683
|
+
throw new Error("No pending tool calls to submit results for");
|
|
8684
|
+
}
|
|
8685
|
+
await this.client.runs.submitToolResults(this.currentRunId, {
|
|
8686
|
+
node_id: this.currentNodeId,
|
|
8687
|
+
step: this.currentWaiting.step,
|
|
8688
|
+
request_id: this.currentWaiting.request_id,
|
|
8689
|
+
results: results.map((r) => ({
|
|
8690
|
+
tool_call_id: r.toolCallId,
|
|
8691
|
+
name: r.toolName,
|
|
8692
|
+
output: r.error ? `Error: ${r.error}` : typeof r.result === "string" ? r.result : JSON.stringify(r.result)
|
|
8693
|
+
}))
|
|
8694
|
+
});
|
|
8695
|
+
this.currentWaiting = void 0;
|
|
8696
|
+
return await this.processRunEvents();
|
|
8357
8697
|
}
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
mergeCustomerOptions(this.customerId, options)
|
|
8364
|
-
);
|
|
8698
|
+
/**
|
|
8699
|
+
* Get all artifacts produced during this session.
|
|
8700
|
+
*/
|
|
8701
|
+
getArtifacts() {
|
|
8702
|
+
return new Map(this.artifacts);
|
|
8365
8703
|
}
|
|
8366
|
-
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
user,
|
|
8371
|
-
mergeCustomerOptions(this.customerId, options)
|
|
8372
|
-
);
|
|
8704
|
+
/**
|
|
8705
|
+
* Close the session (no-op for remote sessions).
|
|
8706
|
+
*/
|
|
8707
|
+
async close() {
|
|
8373
8708
|
}
|
|
8374
|
-
|
|
8375
|
-
|
|
8376
|
-
|
|
8377
|
-
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
|
|
8709
|
+
/**
|
|
8710
|
+
* Refresh the session state from the server.
|
|
8711
|
+
*/
|
|
8712
|
+
async refresh() {
|
|
8713
|
+
const response = await this.http.request(`/sessions/${this.id}`, {
|
|
8714
|
+
method: "GET"
|
|
8715
|
+
});
|
|
8716
|
+
const data = await response.json();
|
|
8717
|
+
this.metadata = data.metadata;
|
|
8718
|
+
this.updatedAt = new Date(data.updated_at);
|
|
8719
|
+
if (data.messages) {
|
|
8720
|
+
this.messages = data.messages.map((m) => ({
|
|
8721
|
+
type: "message",
|
|
8722
|
+
role: m.role,
|
|
8723
|
+
content: m.content,
|
|
8724
|
+
seq: m.seq,
|
|
8725
|
+
createdAt: new Date(m.created_at),
|
|
8726
|
+
runId: m.run_id ? parseRunId(m.run_id) : void 0
|
|
8727
|
+
}));
|
|
8728
|
+
this.nextSeq = this.messages.length + 1;
|
|
8729
|
+
}
|
|
8730
|
+
}
|
|
8731
|
+
// ============================================================================
|
|
8732
|
+
// Private Methods
|
|
8733
|
+
// ============================================================================
|
|
8734
|
+
addMessage(input, runId) {
|
|
8735
|
+
const message = {
|
|
8736
|
+
...input,
|
|
8737
|
+
seq: this.nextSeq++,
|
|
8738
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
8739
|
+
runId
|
|
8740
|
+
};
|
|
8741
|
+
this.messages.push(message);
|
|
8742
|
+
this.updatedAt = /* @__PURE__ */ new Date();
|
|
8743
|
+
return message;
|
|
8744
|
+
}
|
|
8745
|
+
buildInput() {
|
|
8746
|
+
return this.messages.map((m) => ({
|
|
8747
|
+
type: m.type,
|
|
8748
|
+
role: m.role,
|
|
8749
|
+
content: m.content,
|
|
8750
|
+
toolCalls: m.toolCalls,
|
|
8751
|
+
toolCallId: m.toolCallId
|
|
8752
|
+
}));
|
|
8381
8753
|
}
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8754
|
+
resetRunState() {
|
|
8755
|
+
this.currentRunId = void 0;
|
|
8756
|
+
this.currentNodeId = void 0;
|
|
8757
|
+
this.currentWaiting = void 0;
|
|
8758
|
+
this.currentEvents = [];
|
|
8759
|
+
this.currentUsage = {
|
|
8760
|
+
inputTokens: 0,
|
|
8761
|
+
outputTokens: 0,
|
|
8762
|
+
totalTokens: 0,
|
|
8763
|
+
llmCalls: 0,
|
|
8764
|
+
toolCalls: 0
|
|
8765
|
+
};
|
|
8388
8766
|
}
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
const
|
|
8394
|
-
|
|
8395
|
-
baseUrl: cfg.baseUrl || DEFAULT_BASE_URL,
|
|
8396
|
-
fetchImpl: cfg.fetch,
|
|
8397
|
-
clientHeader: cfg.clientHeader || DEFAULT_CLIENT_HEADER,
|
|
8398
|
-
apiKey: publishableKey
|
|
8767
|
+
async processRunEvents(signal) {
|
|
8768
|
+
if (!this.currentRunId) {
|
|
8769
|
+
throw new Error("No current run");
|
|
8770
|
+
}
|
|
8771
|
+
const eventStream = await this.client.runs.events(this.currentRunId, {
|
|
8772
|
+
afterSeq: this.currentEvents.length
|
|
8399
8773
|
});
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8774
|
+
for await (const event of eventStream) {
|
|
8775
|
+
if (signal?.aborted) {
|
|
8776
|
+
return {
|
|
8777
|
+
status: "canceled",
|
|
8778
|
+
runId: this.currentRunId,
|
|
8779
|
+
usage: { ...this.currentUsage },
|
|
8780
|
+
events: [...this.currentEvents]
|
|
8781
|
+
};
|
|
8782
|
+
}
|
|
8783
|
+
this.currentEvents.push(event);
|
|
8784
|
+
switch (event.type) {
|
|
8785
|
+
case "node_llm_call":
|
|
8786
|
+
this.currentUsage = {
|
|
8787
|
+
...this.currentUsage,
|
|
8788
|
+
llmCalls: this.currentUsage.llmCalls + 1,
|
|
8789
|
+
inputTokens: this.currentUsage.inputTokens + (event.llm_call.usage?.input_tokens || 0),
|
|
8790
|
+
outputTokens: this.currentUsage.outputTokens + (event.llm_call.usage?.output_tokens || 0),
|
|
8791
|
+
totalTokens: this.currentUsage.totalTokens + (event.llm_call.usage?.total_tokens || 0)
|
|
8792
|
+
};
|
|
8793
|
+
break;
|
|
8794
|
+
case "node_tool_call":
|
|
8795
|
+
this.currentUsage = {
|
|
8796
|
+
...this.currentUsage,
|
|
8797
|
+
toolCalls: this.currentUsage.toolCalls + 1
|
|
8798
|
+
};
|
|
8799
|
+
break;
|
|
8800
|
+
case "node_waiting":
|
|
8801
|
+
this.currentNodeId = event.node_id;
|
|
8802
|
+
this.currentWaiting = event.waiting;
|
|
8803
|
+
if (this.toolRegistry && event.waiting.reason === "tool_results" && event.waiting.pending_tool_calls && event.waiting.pending_tool_calls.length > 0) {
|
|
8804
|
+
const results = await this.executeToolsLocally(
|
|
8805
|
+
event.waiting.pending_tool_calls
|
|
8806
|
+
);
|
|
8807
|
+
if (results) {
|
|
8808
|
+
return this.submitToolResults(results);
|
|
8809
|
+
}
|
|
8810
|
+
}
|
|
8811
|
+
if (event.waiting.reason === "tool_results" && event.waiting.pending_tool_calls) {
|
|
8812
|
+
return {
|
|
8813
|
+
status: "waiting_for_tools",
|
|
8814
|
+
pendingTools: event.waiting.pending_tool_calls.map(
|
|
8815
|
+
(tc) => ({
|
|
8816
|
+
toolCallId: tc.tool_call_id,
|
|
8817
|
+
name: tc.name,
|
|
8818
|
+
arguments: tc.arguments
|
|
8819
|
+
})
|
|
8820
|
+
),
|
|
8821
|
+
runId: this.currentRunId,
|
|
8822
|
+
usage: { ...this.currentUsage },
|
|
8823
|
+
events: [...this.currentEvents]
|
|
8824
|
+
};
|
|
8825
|
+
}
|
|
8826
|
+
break;
|
|
8827
|
+
case "run_completed": {
|
|
8828
|
+
const runState2 = await this.client.runs.get(this.currentRunId);
|
|
8829
|
+
let output2;
|
|
8830
|
+
if (runState2.outputs && Array.isArray(runState2.outputs)) {
|
|
8831
|
+
output2 = runState2.outputs.filter((o) => o.type === "text").map((o) => o.text || "").join("");
|
|
8832
|
+
}
|
|
8833
|
+
if (output2) {
|
|
8834
|
+
this.addMessage(
|
|
8835
|
+
{
|
|
8836
|
+
type: "message",
|
|
8837
|
+
role: "assistant",
|
|
8838
|
+
content: [{ type: "text", text: output2 }]
|
|
8839
|
+
},
|
|
8840
|
+
this.currentRunId
|
|
8841
|
+
);
|
|
8842
|
+
}
|
|
8843
|
+
return {
|
|
8844
|
+
status: "complete",
|
|
8845
|
+
output: output2,
|
|
8846
|
+
runId: this.currentRunId,
|
|
8847
|
+
usage: { ...this.currentUsage },
|
|
8848
|
+
events: [...this.currentEvents]
|
|
8849
|
+
};
|
|
8850
|
+
}
|
|
8851
|
+
case "run_failed":
|
|
8852
|
+
return {
|
|
8853
|
+
status: "error",
|
|
8854
|
+
error: "Run failed",
|
|
8855
|
+
runId: this.currentRunId,
|
|
8856
|
+
usage: { ...this.currentUsage },
|
|
8857
|
+
events: [...this.currentEvents]
|
|
8858
|
+
};
|
|
8859
|
+
case "run_canceled":
|
|
8860
|
+
return {
|
|
8861
|
+
status: "canceled",
|
|
8862
|
+
runId: this.currentRunId,
|
|
8863
|
+
usage: { ...this.currentUsage },
|
|
8864
|
+
events: [...this.currentEvents]
|
|
8865
|
+
};
|
|
8866
|
+
}
|
|
8407
8867
|
}
|
|
8408
|
-
const
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
deviceId: this.customer.deviceId,
|
|
8413
|
-
ttlSeconds: this.customer.ttlSeconds
|
|
8414
|
-
};
|
|
8415
|
-
let token;
|
|
8416
|
-
if (this.customer.email) {
|
|
8417
|
-
const req = {
|
|
8418
|
-
...reqBase,
|
|
8419
|
-
email: this.customer.email
|
|
8420
|
-
};
|
|
8421
|
-
token = await this.auth.frontendTokenAutoProvision(req);
|
|
8422
|
-
} else {
|
|
8423
|
-
const req = reqBase;
|
|
8424
|
-
token = await this.auth.frontendToken(req);
|
|
8868
|
+
const runState = await this.client.runs.get(this.currentRunId);
|
|
8869
|
+
let output;
|
|
8870
|
+
if (runState.outputs && Array.isArray(runState.outputs)) {
|
|
8871
|
+
output = runState.outputs.filter((o) => o.type === "text").map((o) => o.text || "").join("");
|
|
8425
8872
|
}
|
|
8426
|
-
if (
|
|
8427
|
-
|
|
8873
|
+
if (output) {
|
|
8874
|
+
this.addMessage(
|
|
8875
|
+
{
|
|
8876
|
+
type: "message",
|
|
8877
|
+
role: "assistant",
|
|
8878
|
+
content: [{ type: "text", text: output }]
|
|
8879
|
+
},
|
|
8880
|
+
this.currentRunId
|
|
8881
|
+
);
|
|
8428
8882
|
}
|
|
8429
|
-
return
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
baseUrl: cfg.baseUrl || DEFAULT_BASE_URL,
|
|
8437
|
-
fetchImpl: cfg.fetch,
|
|
8438
|
-
clientHeader: cfg.clientHeader || DEFAULT_CLIENT_HEADER,
|
|
8439
|
-
apiKey: key
|
|
8440
|
-
});
|
|
8441
|
-
this.auth = new AuthClient(http, { apiKey: key });
|
|
8442
|
-
this.req = cfg.request;
|
|
8883
|
+
return {
|
|
8884
|
+
status: "complete",
|
|
8885
|
+
output,
|
|
8886
|
+
runId: this.currentRunId,
|
|
8887
|
+
usage: { ...this.currentUsage },
|
|
8888
|
+
events: [...this.currentEvents]
|
|
8889
|
+
};
|
|
8443
8890
|
}
|
|
8444
|
-
async
|
|
8445
|
-
if (this.
|
|
8446
|
-
|
|
8891
|
+
async executeToolsLocally(toolCalls) {
|
|
8892
|
+
if (!this.toolRegistry) return null;
|
|
8893
|
+
for (const tc of toolCalls) {
|
|
8894
|
+
if (!this.toolRegistry.has(tc.name)) {
|
|
8895
|
+
return null;
|
|
8896
|
+
}
|
|
8447
8897
|
}
|
|
8448
|
-
const
|
|
8449
|
-
|
|
8450
|
-
|
|
8898
|
+
const results = [];
|
|
8899
|
+
for (const tc of toolCalls) {
|
|
8900
|
+
const toolCall = {
|
|
8901
|
+
id: tc.tool_call_id,
|
|
8902
|
+
type: "function",
|
|
8903
|
+
function: {
|
|
8904
|
+
name: tc.name,
|
|
8905
|
+
arguments: tc.arguments
|
|
8906
|
+
}
|
|
8907
|
+
};
|
|
8908
|
+
const result = await this.toolRegistry.execute(toolCall);
|
|
8909
|
+
results.push(result);
|
|
8910
|
+
}
|
|
8911
|
+
return results;
|
|
8451
8912
|
}
|
|
8452
8913
|
};
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
this.request = { idToken: "", projectId: cfg.projectId };
|
|
8465
|
-
}
|
|
8466
|
-
async getToken() {
|
|
8467
|
-
if (this.cached && isReusable(this.cached)) {
|
|
8468
|
-
return this.cached.token;
|
|
8914
|
+
function getHTTPClient(client) {
|
|
8915
|
+
return client.http;
|
|
8916
|
+
}
|
|
8917
|
+
function mergeTools2(defaults, overrides) {
|
|
8918
|
+
if (!defaults && !overrides) return void 0;
|
|
8919
|
+
if (!defaults) return overrides;
|
|
8920
|
+
if (!overrides) return defaults;
|
|
8921
|
+
const merged = /* @__PURE__ */ new Map();
|
|
8922
|
+
for (const tool of defaults) {
|
|
8923
|
+
if (tool.type === "function" && tool.function) {
|
|
8924
|
+
merged.set(tool.function.name, tool);
|
|
8469
8925
|
}
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8926
|
+
}
|
|
8927
|
+
for (const tool of overrides) {
|
|
8928
|
+
if (tool.type === "function" && tool.function) {
|
|
8929
|
+
merged.set(tool.function.name, tool);
|
|
8473
8930
|
}
|
|
8474
|
-
const token = await this.auth.oidcExchange({ ...this.request, idToken });
|
|
8475
|
-
this.cached = token;
|
|
8476
|
-
return token.token;
|
|
8477
8931
|
}
|
|
8478
|
-
|
|
8932
|
+
return Array.from(merged.values());
|
|
8933
|
+
}
|
|
8479
8934
|
|
|
8480
|
-
// src/
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8935
|
+
// src/sessions/client.ts
|
|
8936
|
+
var SessionsClient = class {
|
|
8937
|
+
constructor(modelRelay, http, auth) {
|
|
8938
|
+
this.modelRelay = modelRelay;
|
|
8939
|
+
this.http = http;
|
|
8940
|
+
this.auth = auth;
|
|
8485
8941
|
}
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
|
|
8942
|
+
// ============================================================================
|
|
8943
|
+
// Local Sessions (Client-Managed)
|
|
8944
|
+
// ============================================================================
|
|
8945
|
+
/**
|
|
8946
|
+
* Create a new local session.
|
|
8947
|
+
*
|
|
8948
|
+
* Local sessions keep history on the client side with optional persistence.
|
|
8949
|
+
* Use for privacy-sensitive workflows or offline-capable agents.
|
|
8950
|
+
*
|
|
8951
|
+
* @param options - Session configuration
|
|
8952
|
+
* @returns A new LocalSession instance
|
|
8953
|
+
*
|
|
8954
|
+
* @example
|
|
8955
|
+
* ```typescript
|
|
8956
|
+
* const session = client.sessions.createLocal({
|
|
8957
|
+
* toolRegistry: createLocalFSTools({ root: process.cwd() }),
|
|
8958
|
+
* persistence: "memory", // or "file", "sqlite"
|
|
8959
|
+
* });
|
|
8960
|
+
*
|
|
8961
|
+
* const result = await session.run("Create a hello world file");
|
|
8962
|
+
* ```
|
|
8963
|
+
*/
|
|
8964
|
+
createLocal(options = {}) {
|
|
8965
|
+
return createLocalSession(this.modelRelay, options);
|
|
8489
8966
|
}
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
8967
|
+
/**
|
|
8968
|
+
* Resume an existing local session from storage.
|
|
8969
|
+
*
|
|
8970
|
+
* @param sessionId - ID of the session to resume
|
|
8971
|
+
* @param options - Session configuration (must match original persistence settings)
|
|
8972
|
+
* @returns The resumed LocalSession, or null if not found
|
|
8973
|
+
*
|
|
8974
|
+
* @example
|
|
8975
|
+
* ```typescript
|
|
8976
|
+
* const session = await client.sessions.resumeLocal("session-id", {
|
|
8977
|
+
* persistence: "sqlite",
|
|
8978
|
+
* });
|
|
8979
|
+
*
|
|
8980
|
+
* if (session) {
|
|
8981
|
+
* console.log(`Resumed session with ${session.history.length} messages`);
|
|
8982
|
+
* const result = await session.run("Continue where we left off");
|
|
8983
|
+
* }
|
|
8984
|
+
* ```
|
|
8985
|
+
*/
|
|
8986
|
+
async resumeLocal(sessionId, options = {}) {
|
|
8987
|
+
return LocalSession.resume(this.modelRelay, sessionId, options);
|
|
8494
8988
|
}
|
|
8495
|
-
|
|
8496
|
-
|
|
8989
|
+
// ============================================================================
|
|
8990
|
+
// Remote Sessions (Server-Managed)
|
|
8991
|
+
// ============================================================================
|
|
8992
|
+
/**
|
|
8993
|
+
* Create a new remote session.
|
|
8994
|
+
*
|
|
8995
|
+
* Remote sessions store history on the server for cross-device continuity.
|
|
8996
|
+
* Use for browser-based agents or team collaboration.
|
|
8997
|
+
*
|
|
8998
|
+
* @param options - Session configuration
|
|
8999
|
+
* @returns A new RemoteSession instance
|
|
9000
|
+
*
|
|
9001
|
+
* @example
|
|
9002
|
+
* ```typescript
|
|
9003
|
+
* const session = await client.sessions.create({
|
|
9004
|
+
* metadata: { name: "Feature implementation" },
|
|
9005
|
+
* });
|
|
9006
|
+
*
|
|
9007
|
+
* const result = await session.run("Implement the login feature");
|
|
9008
|
+
* ```
|
|
9009
|
+
*/
|
|
9010
|
+
async create(options = {}) {
|
|
9011
|
+
return RemoteSession.create(this.modelRelay, options);
|
|
8497
9012
|
}
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
9013
|
+
/**
|
|
9014
|
+
* Get an existing remote session by ID.
|
|
9015
|
+
*
|
|
9016
|
+
* @param sessionId - ID of the session to retrieve
|
|
9017
|
+
* @param options - Optional configuration (toolRegistry, defaults)
|
|
9018
|
+
* @returns The RemoteSession instance
|
|
9019
|
+
*
|
|
9020
|
+
* @example
|
|
9021
|
+
* ```typescript
|
|
9022
|
+
* const session = await client.sessions.get("session-id");
|
|
9023
|
+
* console.log(`Session has ${session.history.length} messages`);
|
|
9024
|
+
* ```
|
|
9025
|
+
*/
|
|
9026
|
+
async get(sessionId, options = {}) {
|
|
9027
|
+
return RemoteSession.get(this.modelRelay, sessionId, options);
|
|
9028
|
+
}
|
|
9029
|
+
/**
|
|
9030
|
+
* List remote sessions.
|
|
9031
|
+
*
|
|
9032
|
+
* @param options - List options (limit, cursor, endUserId)
|
|
9033
|
+
* @returns Paginated list of session summaries
|
|
9034
|
+
*
|
|
9035
|
+
* @example
|
|
9036
|
+
* ```typescript
|
|
9037
|
+
* const { sessions, nextCursor } = await client.sessions.list({ limit: 10 });
|
|
9038
|
+
* for (const info of sessions) {
|
|
9039
|
+
* console.log(`Session ${info.id}: ${info.messageCount} messages`);
|
|
9040
|
+
* }
|
|
9041
|
+
* ```
|
|
9042
|
+
*/
|
|
9043
|
+
async list(options = {}) {
|
|
9044
|
+
return RemoteSession.list(this.modelRelay, {
|
|
9045
|
+
limit: options.limit,
|
|
9046
|
+
offset: options.cursor ? parseInt(options.cursor, 10) : void 0,
|
|
9047
|
+
endUserId: options.endUserId
|
|
8512
9048
|
});
|
|
8513
9049
|
}
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
const clientId = req.clientId?.trim();
|
|
8529
|
-
if (!clientId) {
|
|
8530
|
-
throw new ConfigError("clientId is required");
|
|
9050
|
+
/**
|
|
9051
|
+
* Delete a remote session.
|
|
9052
|
+
*
|
|
9053
|
+
* Requires a secret key (not publishable key).
|
|
9054
|
+
*
|
|
9055
|
+
* @param sessionId - ID of the session to delete
|
|
9056
|
+
*
|
|
9057
|
+
* @example
|
|
9058
|
+
* ```typescript
|
|
9059
|
+
* await client.sessions.delete("session-id");
|
|
9060
|
+
* ```
|
|
9061
|
+
*/
|
|
9062
|
+
async delete(sessionId) {
|
|
9063
|
+
return RemoteSession.delete(this.modelRelay, sessionId);
|
|
8531
9064
|
}
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
9065
|
+
};
|
|
9066
|
+
|
|
9067
|
+
// src/http.ts
|
|
9068
|
+
var HTTPClient = class {
|
|
9069
|
+
constructor(cfg) {
|
|
9070
|
+
const resolvedBase = normalizeBaseUrl(cfg.baseUrl || DEFAULT_BASE_URL);
|
|
9071
|
+
if (!isValidHttpUrl(resolvedBase)) {
|
|
9072
|
+
throw new ConfigError(
|
|
9073
|
+
"baseUrl must start with http:// or https://"
|
|
9074
|
+
);
|
|
9075
|
+
}
|
|
9076
|
+
this.baseUrl = resolvedBase;
|
|
9077
|
+
this.apiKey = cfg.apiKey ? parseApiKey(cfg.apiKey) : void 0;
|
|
9078
|
+
this.accessToken = cfg.accessToken?.trim();
|
|
9079
|
+
this.fetchImpl = cfg.fetchImpl;
|
|
9080
|
+
this.clientHeader = cfg.clientHeader?.trim() || DEFAULT_CLIENT_HEADER;
|
|
9081
|
+
this.defaultConnectTimeoutMs = cfg.connectTimeoutMs === void 0 ? DEFAULT_CONNECT_TIMEOUT_MS : Math.max(0, cfg.connectTimeoutMs);
|
|
9082
|
+
this.defaultTimeoutMs = cfg.timeoutMs === void 0 ? DEFAULT_REQUEST_TIMEOUT_MS : Math.max(0, cfg.timeoutMs);
|
|
9083
|
+
this.retry = normalizeRetryConfig(cfg.retry);
|
|
9084
|
+
this.defaultHeaders = normalizeHeaders(cfg.defaultHeaders);
|
|
9085
|
+
this.metrics = cfg.metrics;
|
|
9086
|
+
this.trace = cfg.trace;
|
|
8535
9087
|
}
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
9088
|
+
async request(path2, options = {}) {
|
|
9089
|
+
const fetchFn = this.fetchImpl ?? globalThis.fetch;
|
|
9090
|
+
if (!fetchFn) {
|
|
9091
|
+
throw new ConfigError(
|
|
9092
|
+
"fetch is not available; provide a fetch implementation"
|
|
9093
|
+
);
|
|
8541
9094
|
}
|
|
8542
|
-
const
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8546
|
-
const
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
9095
|
+
const method = options.method || "GET";
|
|
9096
|
+
const url = buildUrl(this.baseUrl, path2);
|
|
9097
|
+
const metrics = mergeMetrics(this.metrics, options.metrics);
|
|
9098
|
+
const trace = mergeTrace(this.trace, options.trace);
|
|
9099
|
+
const context = {
|
|
9100
|
+
method,
|
|
9101
|
+
path: path2,
|
|
9102
|
+
...options.context || {}
|
|
9103
|
+
};
|
|
9104
|
+
trace?.requestStart?.(context);
|
|
9105
|
+
const start = metrics?.httpRequest || trace?.requestFinish ? Date.now() : 0;
|
|
9106
|
+
const headers = new Headers({
|
|
9107
|
+
...this.defaultHeaders,
|
|
9108
|
+
...options.headers || {}
|
|
8550
9109
|
});
|
|
8551
|
-
const
|
|
8552
|
-
if (
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
9110
|
+
const accepts = options.accept || (options.raw ? void 0 : "application/json");
|
|
9111
|
+
if (accepts && !headers.has("Accept")) {
|
|
9112
|
+
headers.set("Accept", accepts);
|
|
9113
|
+
}
|
|
9114
|
+
const body = options.body;
|
|
9115
|
+
const shouldEncodeJSON = body !== void 0 && body !== null && typeof body === "object" && !(body instanceof FormData) && !(body instanceof Blob);
|
|
9116
|
+
const payload = shouldEncodeJSON ? JSON.stringify(body) : body;
|
|
9117
|
+
if (shouldEncodeJSON && !headers.has("Content-Type")) {
|
|
9118
|
+
headers.set("Content-Type", "application/json");
|
|
9119
|
+
}
|
|
9120
|
+
const accessToken = options.accessToken ?? this.accessToken;
|
|
9121
|
+
if (accessToken) {
|
|
9122
|
+
const bearer = accessToken.toLowerCase().startsWith("bearer ") ? accessToken : `Bearer ${accessToken}`;
|
|
9123
|
+
headers.set("Authorization", bearer);
|
|
9124
|
+
}
|
|
9125
|
+
const apiKey = options.apiKey ?? this.apiKey;
|
|
9126
|
+
if (apiKey) {
|
|
9127
|
+
headers.set("X-ModelRelay-Api-Key", apiKey);
|
|
9128
|
+
}
|
|
9129
|
+
if (this.clientHeader && !headers.has("X-ModelRelay-Client")) {
|
|
9130
|
+
headers.set("X-ModelRelay-Client", this.clientHeader);
|
|
9131
|
+
}
|
|
9132
|
+
const timeoutMs = options.useDefaultTimeout === false ? options.timeoutMs : options.timeoutMs ?? this.defaultTimeoutMs;
|
|
9133
|
+
const connectTimeoutMs = options.useDefaultConnectTimeout === false ? options.connectTimeoutMs : options.connectTimeoutMs ?? this.defaultConnectTimeoutMs;
|
|
9134
|
+
const retryCfg = normalizeRetryConfig(
|
|
9135
|
+
options.retry === void 0 ? this.retry : options.retry
|
|
9136
|
+
);
|
|
9137
|
+
const attempts = retryCfg ? Math.max(1, retryCfg.maxAttempts) : 1;
|
|
9138
|
+
let lastError;
|
|
9139
|
+
let lastStatus;
|
|
9140
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
9141
|
+
let connectTimedOut = false;
|
|
9142
|
+
let requestTimedOut = false;
|
|
9143
|
+
const connectController = connectTimeoutMs && connectTimeoutMs > 0 ? new AbortController() : void 0;
|
|
9144
|
+
const requestController = timeoutMs && timeoutMs > 0 ? new AbortController() : void 0;
|
|
9145
|
+
const signal = mergeSignals(
|
|
9146
|
+
options.signal,
|
|
9147
|
+
connectController?.signal,
|
|
9148
|
+
requestController?.signal
|
|
9149
|
+
);
|
|
9150
|
+
const connectTimer = connectController && setTimeout(() => {
|
|
9151
|
+
connectTimedOut = true;
|
|
9152
|
+
connectController.abort(
|
|
9153
|
+
new DOMException("connect timeout", "AbortError")
|
|
9154
|
+
);
|
|
9155
|
+
}, connectTimeoutMs);
|
|
9156
|
+
const requestTimer = requestController && setTimeout(() => {
|
|
9157
|
+
requestTimedOut = true;
|
|
9158
|
+
requestController.abort(
|
|
9159
|
+
new DOMException("timeout", "AbortError")
|
|
9160
|
+
);
|
|
9161
|
+
}, timeoutMs);
|
|
9162
|
+
try {
|
|
9163
|
+
const response = await fetchFn(url, {
|
|
9164
|
+
method,
|
|
9165
|
+
headers,
|
|
9166
|
+
body: payload,
|
|
9167
|
+
signal
|
|
9168
|
+
});
|
|
9169
|
+
if (connectTimer) {
|
|
9170
|
+
clearTimeout(connectTimer);
|
|
9171
|
+
}
|
|
9172
|
+
if (!response.ok) {
|
|
9173
|
+
const shouldRetry = retryCfg && shouldRetryStatus(
|
|
9174
|
+
response.status,
|
|
9175
|
+
method,
|
|
9176
|
+
retryCfg.retryPost
|
|
9177
|
+
) && attempt < attempts;
|
|
9178
|
+
if (shouldRetry) {
|
|
9179
|
+
lastStatus = response.status;
|
|
9180
|
+
await backoff(attempt, retryCfg);
|
|
9181
|
+
continue;
|
|
9182
|
+
}
|
|
9183
|
+
const retries = buildRetryMetadata(attempt, response.status, lastError);
|
|
9184
|
+
const finishedCtx2 = withRequestId(context, response.headers);
|
|
9185
|
+
recordHttpMetrics(metrics, trace, start, retries, {
|
|
9186
|
+
status: response.status,
|
|
9187
|
+
context: finishedCtx2
|
|
8567
9188
|
});
|
|
8568
|
-
|
|
8569
|
-
|
|
8570
|
-
|
|
8571
|
-
|
|
9189
|
+
throw options.raw ? await parseErrorResponse(response, retries) : await parseErrorResponse(response, retries);
|
|
9190
|
+
}
|
|
9191
|
+
const finishedCtx = withRequestId(context, response.headers);
|
|
9192
|
+
recordHttpMetrics(metrics, trace, start, void 0, {
|
|
9193
|
+
status: response.status,
|
|
9194
|
+
context: finishedCtx
|
|
9195
|
+
});
|
|
9196
|
+
return response;
|
|
9197
|
+
} catch (err) {
|
|
9198
|
+
if (options.signal?.aborted) {
|
|
9199
|
+
throw err;
|
|
9200
|
+
}
|
|
9201
|
+
if (err instanceof ModelRelayError) {
|
|
9202
|
+
recordHttpMetrics(metrics, trace, start, void 0, {
|
|
9203
|
+
error: err,
|
|
9204
|
+
context
|
|
9205
|
+
});
|
|
9206
|
+
throw err;
|
|
9207
|
+
}
|
|
9208
|
+
const transportKind = classifyTransportErrorKind(
|
|
9209
|
+
err,
|
|
9210
|
+
connectTimedOut,
|
|
9211
|
+
requestTimedOut
|
|
9212
|
+
);
|
|
9213
|
+
const shouldRetry = retryCfg && isRetryableError(err, transportKind) && (method !== "POST" || retryCfg.retryPost) && attempt < attempts;
|
|
9214
|
+
if (!shouldRetry) {
|
|
9215
|
+
const retries = buildRetryMetadata(
|
|
9216
|
+
attempt,
|
|
9217
|
+
lastStatus,
|
|
9218
|
+
err instanceof Error ? err.message : String(err)
|
|
9219
|
+
);
|
|
9220
|
+
recordHttpMetrics(metrics, trace, start, retries, {
|
|
9221
|
+
error: err,
|
|
9222
|
+
context
|
|
8572
9223
|
});
|
|
9224
|
+
throw toTransportError(err, transportKind, retries);
|
|
9225
|
+
}
|
|
9226
|
+
lastError = err;
|
|
9227
|
+
await backoff(attempt, retryCfg);
|
|
9228
|
+
} finally {
|
|
9229
|
+
if (connectTimer) {
|
|
9230
|
+
clearTimeout(connectTimer);
|
|
9231
|
+
}
|
|
9232
|
+
if (requestTimer) {
|
|
9233
|
+
clearTimeout(requestTimer);
|
|
9234
|
+
}
|
|
8573
9235
|
}
|
|
8574
9236
|
}
|
|
8575
|
-
|
|
8576
|
-
|
|
8577
|
-
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
8581
|
-
const
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
|
|
8585
|
-
|
|
9237
|
+
throw lastError instanceof Error ? lastError : new TransportError("request failed", {
|
|
9238
|
+
kind: "other",
|
|
9239
|
+
retries: buildRetryMetadata(attempts, lastStatus)
|
|
9240
|
+
});
|
|
9241
|
+
}
|
|
9242
|
+
async json(path2, options = {}) {
|
|
9243
|
+
const response = await this.request(path2, {
|
|
9244
|
+
...options,
|
|
9245
|
+
raw: true,
|
|
9246
|
+
accept: options.accept || "application/json"
|
|
9247
|
+
});
|
|
9248
|
+
if (!response.ok) {
|
|
9249
|
+
throw await parseErrorResponse(response);
|
|
9250
|
+
}
|
|
9251
|
+
if (response.status === 204) {
|
|
9252
|
+
return void 0;
|
|
9253
|
+
}
|
|
9254
|
+
try {
|
|
9255
|
+
return await response.json();
|
|
9256
|
+
} catch (err) {
|
|
9257
|
+
throw new APIError("failed to parse response JSON", {
|
|
9258
|
+
status: response.status,
|
|
9259
|
+
data: err
|
|
8586
9260
|
});
|
|
8587
9261
|
}
|
|
8588
|
-
return { accessToken, idToken, refreshToken, tokenType, scope, expiresAt };
|
|
8589
9262
|
}
|
|
9263
|
+
};
|
|
9264
|
+
function buildUrl(baseUrl, path2) {
|
|
9265
|
+
if (/^https?:\/\//i.test(path2)) {
|
|
9266
|
+
return path2;
|
|
9267
|
+
}
|
|
9268
|
+
if (!path2.startsWith("/")) {
|
|
9269
|
+
path2 = `/${path2}`;
|
|
9270
|
+
}
|
|
9271
|
+
return `${baseUrl}${path2}`;
|
|
8590
9272
|
}
|
|
8591
|
-
|
|
8592
|
-
const
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
scope: cfg.scope,
|
|
8596
|
-
audience: cfg.audience,
|
|
8597
|
-
fetch: cfg.fetch,
|
|
8598
|
-
signal: cfg.signal
|
|
8599
|
-
});
|
|
8600
|
-
await cfg.onUserCode(auth);
|
|
8601
|
-
const token = await pollOAuthDeviceToken({
|
|
8602
|
-
tokenEndpoint: cfg.tokenEndpoint,
|
|
8603
|
-
clientId: cfg.clientId,
|
|
8604
|
-
deviceCode: auth.deviceCode,
|
|
8605
|
-
intervalSeconds: auth.intervalSeconds,
|
|
8606
|
-
deadline: auth.expiresAt,
|
|
8607
|
-
fetch: cfg.fetch,
|
|
8608
|
-
signal: cfg.signal
|
|
8609
|
-
});
|
|
8610
|
-
if (!token.idToken) {
|
|
8611
|
-
throw new TransportError("oauth device flow did not return an id_token", {
|
|
8612
|
-
kind: "request",
|
|
8613
|
-
cause: token
|
|
8614
|
-
});
|
|
9273
|
+
function normalizeBaseUrl(value) {
|
|
9274
|
+
const trimmed = value.trim();
|
|
9275
|
+
if (trimmed.endsWith("/")) {
|
|
9276
|
+
return trimmed.slice(0, -1);
|
|
8615
9277
|
}
|
|
8616
|
-
return
|
|
9278
|
+
return trimmed;
|
|
8617
9279
|
}
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
if (!fetchFn) {
|
|
8621
|
-
throw new ConfigError("fetch is not available; provide a fetch implementation");
|
|
8622
|
-
}
|
|
8623
|
-
let resp;
|
|
8624
|
-
try {
|
|
8625
|
-
resp = await fetchFn(url, {
|
|
8626
|
-
method: "POST",
|
|
8627
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
8628
|
-
body: form.toString(),
|
|
8629
|
-
signal: opts.signal
|
|
8630
|
-
});
|
|
8631
|
-
} catch (cause) {
|
|
8632
|
-
throw new TransportError("oauth request failed", { kind: "request", cause });
|
|
8633
|
-
}
|
|
8634
|
-
let json;
|
|
8635
|
-
try {
|
|
8636
|
-
json = await resp.json();
|
|
8637
|
-
} catch (cause) {
|
|
8638
|
-
throw new TransportError("oauth response was not valid JSON", { kind: "request", cause });
|
|
8639
|
-
}
|
|
8640
|
-
if (!resp.ok && !opts.allowErrorPayload) {
|
|
8641
|
-
throw new TransportError(`oauth request failed (${resp.status})`, {
|
|
8642
|
-
kind: "request",
|
|
8643
|
-
cause: json
|
|
8644
|
-
});
|
|
8645
|
-
}
|
|
8646
|
-
return json || {};
|
|
9280
|
+
function isValidHttpUrl(value) {
|
|
9281
|
+
return /^https?:\/\//i.test(value);
|
|
8647
9282
|
}
|
|
8648
|
-
|
|
8649
|
-
if (
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
9283
|
+
function normalizeRetryConfig(retry) {
|
|
9284
|
+
if (retry === false) return void 0;
|
|
9285
|
+
const cfg = retry || {};
|
|
9286
|
+
return {
|
|
9287
|
+
maxAttempts: Math.max(1, cfg.maxAttempts ?? 3),
|
|
9288
|
+
baseBackoffMs: Math.max(0, cfg.baseBackoffMs ?? 300),
|
|
9289
|
+
maxBackoffMs: Math.max(0, cfg.maxBackoffMs ?? 5e3),
|
|
9290
|
+
retryPost: cfg.retryPost ?? true
|
|
9291
|
+
};
|
|
9292
|
+
}
|
|
9293
|
+
function shouldRetryStatus(status, method, retryPost) {
|
|
9294
|
+
if (status === 408 || status === 429) {
|
|
9295
|
+
return method !== "POST" || retryPost;
|
|
8655
9296
|
}
|
|
8656
|
-
if (
|
|
8657
|
-
|
|
9297
|
+
if (status >= 500 && status < 600) {
|
|
9298
|
+
return method !== "POST" || retryPost;
|
|
8658
9299
|
}
|
|
8659
|
-
|
|
8660
|
-
const onAbort = () => {
|
|
8661
|
-
signal.removeEventListener("abort", onAbort);
|
|
8662
|
-
reject(new TransportError("oauth device flow aborted", { kind: "request" }));
|
|
8663
|
-
};
|
|
8664
|
-
signal.addEventListener("abort", onAbort);
|
|
8665
|
-
setTimeout(() => {
|
|
8666
|
-
signal.removeEventListener("abort", onAbort);
|
|
8667
|
-
resolve();
|
|
8668
|
-
}, ms);
|
|
8669
|
-
});
|
|
8670
|
-
}
|
|
8671
|
-
|
|
8672
|
-
// src/workflow_builder.ts
|
|
8673
|
-
function transformJSONValue(from, pointer) {
|
|
8674
|
-
return pointer ? { from, pointer } : { from };
|
|
9300
|
+
return false;
|
|
8675
9301
|
}
|
|
8676
|
-
function
|
|
8677
|
-
|
|
9302
|
+
function isRetryableError(err, kind) {
|
|
9303
|
+
if (!err) return false;
|
|
9304
|
+
if (kind === "timeout" || kind === "connect") return true;
|
|
9305
|
+
return err instanceof DOMException || err instanceof TypeError;
|
|
8678
9306
|
}
|
|
8679
|
-
function
|
|
8680
|
-
|
|
9307
|
+
function backoff(attempt, cfg) {
|
|
9308
|
+
const exp = Math.max(0, attempt - 1);
|
|
9309
|
+
const base = cfg.baseBackoffMs * Math.pow(2, Math.min(exp, 10));
|
|
9310
|
+
const capped = Math.min(base, cfg.maxBackoffMs);
|
|
9311
|
+
const jitter = 0.5 + Math.random();
|
|
9312
|
+
const delay = Math.min(cfg.maxBackoffMs, capped * jitter);
|
|
9313
|
+
if (delay <= 0) return Promise.resolve();
|
|
9314
|
+
return new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
8681
9315
|
}
|
|
8682
|
-
function
|
|
8683
|
-
const
|
|
8684
|
-
if (
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
if (
|
|
8689
|
-
|
|
9316
|
+
function mergeSignals(...signals) {
|
|
9317
|
+
const active = signals.filter(Boolean);
|
|
9318
|
+
if (active.length === 0) return void 0;
|
|
9319
|
+
if (active.length === 1) return active[0];
|
|
9320
|
+
const controller = new AbortController();
|
|
9321
|
+
for (const src of active) {
|
|
9322
|
+
if (src.aborted) {
|
|
9323
|
+
controller.abort(src.reason);
|
|
9324
|
+
break;
|
|
8690
9325
|
}
|
|
9326
|
+
src.addEventListener(
|
|
9327
|
+
"abort",
|
|
9328
|
+
() => controller.abort(src.reason),
|
|
9329
|
+
{ once: true }
|
|
9330
|
+
);
|
|
8691
9331
|
}
|
|
8692
|
-
return
|
|
9332
|
+
return controller.signal;
|
|
8693
9333
|
}
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
9334
|
+
function normalizeHeaders(headers) {
|
|
9335
|
+
if (!headers) return {};
|
|
9336
|
+
const normalized = {};
|
|
9337
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
9338
|
+
if (!key || !value) continue;
|
|
9339
|
+
const k = key.trim();
|
|
9340
|
+
const v = value.trim();
|
|
9341
|
+
if (k && v) {
|
|
9342
|
+
normalized[k] = v;
|
|
9343
|
+
}
|
|
8697
9344
|
}
|
|
8698
|
-
|
|
8699
|
-
|
|
9345
|
+
return normalized;
|
|
9346
|
+
}
|
|
9347
|
+
function buildRetryMetadata(attempt, lastStatus, lastError) {
|
|
9348
|
+
if (!attempt || attempt <= 1) return void 0;
|
|
9349
|
+
return {
|
|
9350
|
+
attempts: attempt,
|
|
9351
|
+
lastStatus,
|
|
9352
|
+
lastError: typeof lastError === "string" ? lastError : lastError instanceof Error ? lastError.message : lastError ? String(lastError) : void 0
|
|
9353
|
+
};
|
|
9354
|
+
}
|
|
9355
|
+
function classifyTransportErrorKind(err, connectTimedOut, requestTimedOut) {
|
|
9356
|
+
if (connectTimedOut) return "connect";
|
|
9357
|
+
if (requestTimedOut) return "timeout";
|
|
9358
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
9359
|
+
return requestTimedOut ? "timeout" : "request";
|
|
8700
9360
|
}
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
9361
|
+
if (err instanceof TypeError) return "request";
|
|
9362
|
+
return "other";
|
|
9363
|
+
}
|
|
9364
|
+
function toTransportError(err, kind, retries) {
|
|
9365
|
+
const message = err instanceof Error ? err.message : typeof err === "string" ? err : "request failed";
|
|
9366
|
+
return new TransportError(message, { kind, retries, cause: err });
|
|
9367
|
+
}
|
|
9368
|
+
function recordHttpMetrics(metrics, trace, start, retries, info) {
|
|
9369
|
+
if (!metrics?.httpRequest && !trace?.requestFinish) return;
|
|
9370
|
+
const latencyMs = start ? Date.now() - start : 0;
|
|
9371
|
+
if (metrics?.httpRequest) {
|
|
9372
|
+
metrics.httpRequest({
|
|
9373
|
+
latencyMs,
|
|
9374
|
+
status: info.status,
|
|
9375
|
+
error: info.error ? String(info.error) : void 0,
|
|
9376
|
+
retries,
|
|
9377
|
+
context: info.context
|
|
8705
9378
|
});
|
|
8706
9379
|
}
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
9380
|
+
trace?.requestFinish?.({
|
|
9381
|
+
context: info.context,
|
|
9382
|
+
status: info.status,
|
|
9383
|
+
error: info.error,
|
|
9384
|
+
retries,
|
|
9385
|
+
latencyMs
|
|
9386
|
+
});
|
|
9387
|
+
}
|
|
9388
|
+
function withRequestId(context, headers) {
|
|
9389
|
+
const requestId = headers.get("X-ModelRelay-Request-Id") || headers.get("X-Request-Id") || context.requestId;
|
|
9390
|
+
if (!requestId) return context;
|
|
9391
|
+
return { ...context, requestId };
|
|
9392
|
+
}
|
|
9393
|
+
|
|
9394
|
+
// src/customer_scoped.ts
|
|
9395
|
+
function normalizeCustomerId(customerId) {
|
|
9396
|
+
const trimmed = customerId?.trim?.() ? customerId.trim() : "";
|
|
9397
|
+
if (!trimmed) {
|
|
9398
|
+
throw new ConfigError("customerId is required");
|
|
8715
9399
|
}
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
};
|
|
8724
|
-
return this.node({
|
|
8725
|
-
id,
|
|
8726
|
-
type: WorkflowNodeTypes.LLMResponses,
|
|
8727
|
-
input
|
|
9400
|
+
return trimmed;
|
|
9401
|
+
}
|
|
9402
|
+
function mergeCustomerOptions(customerId, options = {}) {
|
|
9403
|
+
if (options.customerId && options.customerId !== customerId) {
|
|
9404
|
+
throw new ConfigError("customerId mismatch", {
|
|
9405
|
+
expected: customerId,
|
|
9406
|
+
received: options.customerId
|
|
8728
9407
|
});
|
|
8729
9408
|
}
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
9409
|
+
return { ...options, customerId };
|
|
9410
|
+
}
|
|
9411
|
+
var CustomerResponsesClient = class {
|
|
9412
|
+
constructor(base, customerId) {
|
|
9413
|
+
this.customerId = normalizeCustomerId(customerId);
|
|
9414
|
+
this.base = base;
|
|
8735
9415
|
}
|
|
8736
|
-
|
|
8737
|
-
return this.
|
|
9416
|
+
get id() {
|
|
9417
|
+
return this.customerId;
|
|
8738
9418
|
}
|
|
8739
|
-
|
|
8740
|
-
return this.
|
|
8741
|
-
outputs: [
|
|
8742
|
-
...this.state.outputs,
|
|
8743
|
-
{ name, from, ...pointer ? { pointer } : {} }
|
|
8744
|
-
]
|
|
8745
|
-
});
|
|
9419
|
+
new() {
|
|
9420
|
+
return this.base.new().customerId(this.customerId);
|
|
8746
9421
|
}
|
|
8747
|
-
|
|
8748
|
-
const
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
9422
|
+
ensureRequestCustomer(request) {
|
|
9423
|
+
const req = asInternal(request);
|
|
9424
|
+
const reqCustomer = req.options.customerId;
|
|
9425
|
+
if (reqCustomer && reqCustomer !== this.customerId) {
|
|
9426
|
+
throw new ConfigError("customerId mismatch", {
|
|
9427
|
+
expected: this.customerId,
|
|
9428
|
+
received: reqCustomer
|
|
9429
|
+
});
|
|
9430
|
+
}
|
|
9431
|
+
}
|
|
9432
|
+
async create(request, options = {}) {
|
|
9433
|
+
this.ensureRequestCustomer(request);
|
|
9434
|
+
return this.base.create(request, mergeCustomerOptions(this.customerId, options));
|
|
9435
|
+
}
|
|
9436
|
+
async stream(request, options = {}) {
|
|
9437
|
+
this.ensureRequestCustomer(request);
|
|
9438
|
+
return this.base.stream(request, mergeCustomerOptions(this.customerId, options));
|
|
9439
|
+
}
|
|
9440
|
+
async streamJSON(request, options = {}) {
|
|
9441
|
+
this.ensureRequestCustomer(request);
|
|
9442
|
+
return this.base.streamJSON(
|
|
9443
|
+
request,
|
|
9444
|
+
mergeCustomerOptions(this.customerId, options)
|
|
9445
|
+
);
|
|
9446
|
+
}
|
|
9447
|
+
async text(system, user, options = {}) {
|
|
9448
|
+
return this.base.textForCustomer(
|
|
9449
|
+
this.customerId,
|
|
9450
|
+
system,
|
|
9451
|
+
user,
|
|
9452
|
+
mergeCustomerOptions(this.customerId, options)
|
|
9453
|
+
);
|
|
9454
|
+
}
|
|
9455
|
+
async streamTextDeltas(system, user, options = {}) {
|
|
9456
|
+
return this.base.streamTextDeltasForCustomer(
|
|
9457
|
+
this.customerId,
|
|
9458
|
+
system,
|
|
9459
|
+
user,
|
|
9460
|
+
mergeCustomerOptions(this.customerId, options)
|
|
9461
|
+
);
|
|
9462
|
+
}
|
|
9463
|
+
};
|
|
9464
|
+
var CustomerScopedModelRelay = class {
|
|
9465
|
+
constructor(responses, customerId, baseUrl) {
|
|
9466
|
+
const normalized = normalizeCustomerId(customerId);
|
|
9467
|
+
this.responses = new CustomerResponsesClient(responses, normalized);
|
|
9468
|
+
this.customerId = normalized;
|
|
9469
|
+
this.baseUrl = baseUrl;
|
|
9470
|
+
}
|
|
9471
|
+
};
|
|
9472
|
+
|
|
9473
|
+
// src/token_providers.ts
|
|
9474
|
+
function isReusable(token) {
|
|
9475
|
+
if (!token.token) {
|
|
9476
|
+
return false;
|
|
9477
|
+
}
|
|
9478
|
+
return token.expiresAt.getTime() - Date.now() > 6e4;
|
|
9479
|
+
}
|
|
9480
|
+
var FrontendTokenProvider = class {
|
|
9481
|
+
constructor(cfg) {
|
|
9482
|
+
const publishableKey = parsePublishableKey(cfg.publishableKey);
|
|
9483
|
+
const http = new HTTPClient({
|
|
9484
|
+
baseUrl: cfg.baseUrl || DEFAULT_BASE_URL,
|
|
9485
|
+
fetchImpl: cfg.fetch,
|
|
9486
|
+
clientHeader: cfg.clientHeader || DEFAULT_CLIENT_HEADER,
|
|
9487
|
+
apiKey: publishableKey
|
|
8773
9488
|
});
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
|
|
9489
|
+
this.publishableKey = publishableKey;
|
|
9490
|
+
this.customer = cfg.customer;
|
|
9491
|
+
this.auth = new AuthClient(http, { apiKey: publishableKey, customer: cfg.customer });
|
|
9492
|
+
}
|
|
9493
|
+
async getToken() {
|
|
9494
|
+
if (!this.customer?.provider || !this.customer?.subject) {
|
|
9495
|
+
throw new ConfigError("customer.provider and customer.subject are required");
|
|
9496
|
+
}
|
|
9497
|
+
const reqBase = {
|
|
9498
|
+
publishableKey: this.publishableKey,
|
|
9499
|
+
identityProvider: this.customer.provider,
|
|
9500
|
+
identitySubject: this.customer.subject,
|
|
9501
|
+
deviceId: this.customer.deviceId,
|
|
9502
|
+
ttlSeconds: this.customer.ttlSeconds
|
|
8781
9503
|
};
|
|
9504
|
+
let token;
|
|
9505
|
+
if (this.customer.email) {
|
|
9506
|
+
const req = {
|
|
9507
|
+
...reqBase,
|
|
9508
|
+
email: this.customer.email
|
|
9509
|
+
};
|
|
9510
|
+
token = await this.auth.frontendTokenAutoProvision(req);
|
|
9511
|
+
} else {
|
|
9512
|
+
const req = reqBase;
|
|
9513
|
+
token = await this.auth.frontendToken(req);
|
|
9514
|
+
}
|
|
9515
|
+
if (!token.token) {
|
|
9516
|
+
throw new ConfigError("frontend token exchange returned an empty token");
|
|
9517
|
+
}
|
|
9518
|
+
return token.token;
|
|
9519
|
+
}
|
|
9520
|
+
};
|
|
9521
|
+
var CustomerTokenProvider = class {
|
|
9522
|
+
constructor(cfg) {
|
|
9523
|
+
const key = parseSecretKey(cfg.secretKey);
|
|
9524
|
+
const http = new HTTPClient({
|
|
9525
|
+
baseUrl: cfg.baseUrl || DEFAULT_BASE_URL,
|
|
9526
|
+
fetchImpl: cfg.fetch,
|
|
9527
|
+
clientHeader: cfg.clientHeader || DEFAULT_CLIENT_HEADER,
|
|
9528
|
+
apiKey: key
|
|
9529
|
+
});
|
|
9530
|
+
this.auth = new AuthClient(http, { apiKey: key });
|
|
9531
|
+
this.req = cfg.request;
|
|
9532
|
+
}
|
|
9533
|
+
async getToken() {
|
|
9534
|
+
if (this.cached && isReusable(this.cached)) {
|
|
9535
|
+
return this.cached.token;
|
|
9536
|
+
}
|
|
9537
|
+
const token = await this.auth.customerToken(this.req);
|
|
9538
|
+
this.cached = token;
|
|
9539
|
+
return token.token;
|
|
9540
|
+
}
|
|
9541
|
+
};
|
|
9542
|
+
var OIDCExchangeTokenProvider = class {
|
|
9543
|
+
constructor(cfg) {
|
|
9544
|
+
const apiKey = parseApiKey(cfg.apiKey);
|
|
9545
|
+
const http = new HTTPClient({
|
|
9546
|
+
baseUrl: cfg.baseUrl || DEFAULT_BASE_URL,
|
|
9547
|
+
fetchImpl: cfg.fetch,
|
|
9548
|
+
clientHeader: cfg.clientHeader || DEFAULT_CLIENT_HEADER,
|
|
9549
|
+
apiKey
|
|
9550
|
+
});
|
|
9551
|
+
this.auth = new AuthClient(http, { apiKey });
|
|
9552
|
+
this.idTokenProvider = cfg.idTokenProvider;
|
|
9553
|
+
this.request = { idToken: "", projectId: cfg.projectId };
|
|
9554
|
+
}
|
|
9555
|
+
async getToken() {
|
|
9556
|
+
if (this.cached && isReusable(this.cached)) {
|
|
9557
|
+
return this.cached.token;
|
|
9558
|
+
}
|
|
9559
|
+
const idToken = (await this.idTokenProvider())?.trim();
|
|
9560
|
+
if (!idToken) {
|
|
9561
|
+
throw new ConfigError("idTokenProvider returned an empty id_token");
|
|
9562
|
+
}
|
|
9563
|
+
const token = await this.auth.oidcExchange({ ...this.request, idToken });
|
|
9564
|
+
this.cached = token;
|
|
9565
|
+
return token.token;
|
|
8782
9566
|
}
|
|
8783
9567
|
};
|
|
8784
|
-
function workflowV0() {
|
|
8785
|
-
return WorkflowBuilderV0.new();
|
|
8786
|
-
}
|
|
8787
9568
|
|
|
8788
|
-
// src/
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
9569
|
+
// src/device_flow.ts
|
|
9570
|
+
async function startOAuthDeviceAuthorization(req) {
|
|
9571
|
+
const deviceAuthorizationEndpoint = req.deviceAuthorizationEndpoint?.trim();
|
|
9572
|
+
if (!deviceAuthorizationEndpoint) {
|
|
9573
|
+
throw new ConfigError("deviceAuthorizationEndpoint is required");
|
|
9574
|
+
}
|
|
9575
|
+
const clientId = req.clientId?.trim();
|
|
9576
|
+
if (!clientId) {
|
|
9577
|
+
throw new ConfigError("clientId is required");
|
|
9578
|
+
}
|
|
9579
|
+
const form = new URLSearchParams();
|
|
9580
|
+
form.set("client_id", clientId);
|
|
9581
|
+
if (req.scope?.trim()) {
|
|
9582
|
+
form.set("scope", req.scope.trim());
|
|
9583
|
+
}
|
|
9584
|
+
if (req.audience?.trim()) {
|
|
9585
|
+
form.set("audience", req.audience.trim());
|
|
9586
|
+
}
|
|
9587
|
+
const payload = await postOAuthForm(deviceAuthorizationEndpoint, form, {
|
|
9588
|
+
fetch: req.fetch,
|
|
9589
|
+
signal: req.signal
|
|
9590
|
+
});
|
|
9591
|
+
const deviceCode = String(payload.device_code || "").trim();
|
|
9592
|
+
const userCode = String(payload.user_code || "").trim();
|
|
9593
|
+
const verificationUri = String(payload.verification_uri || payload.verification_uri_complete || "").trim();
|
|
9594
|
+
const verificationUriComplete = String(payload.verification_uri_complete || "").trim() || void 0;
|
|
9595
|
+
const expiresIn = Number(payload.expires_in || 0);
|
|
9596
|
+
const intervalSeconds = Math.max(1, Number(payload.interval || 5));
|
|
9597
|
+
if (!deviceCode || !userCode || !verificationUri || !expiresIn) {
|
|
9598
|
+
throw new TransportError("oauth device authorization returned an invalid response", {
|
|
9599
|
+
kind: "request",
|
|
9600
|
+
cause: payload
|
|
9601
|
+
});
|
|
9602
|
+
}
|
|
9603
|
+
return {
|
|
9604
|
+
deviceCode,
|
|
9605
|
+
userCode,
|
|
9606
|
+
verificationUri,
|
|
9607
|
+
verificationUriComplete,
|
|
9608
|
+
expiresAt: new Date(Date.now() + expiresIn * 1e3),
|
|
9609
|
+
intervalSeconds
|
|
9610
|
+
};
|
|
9611
|
+
}
|
|
9612
|
+
async function pollOAuthDeviceToken(req) {
|
|
9613
|
+
const tokenEndpoint = req.tokenEndpoint?.trim();
|
|
9614
|
+
if (!tokenEndpoint) {
|
|
9615
|
+
throw new ConfigError("tokenEndpoint is required");
|
|
9616
|
+
}
|
|
9617
|
+
const clientId = req.clientId?.trim();
|
|
9618
|
+
if (!clientId) {
|
|
9619
|
+
throw new ConfigError("clientId is required");
|
|
9620
|
+
}
|
|
9621
|
+
const deviceCode = req.deviceCode?.trim();
|
|
9622
|
+
if (!deviceCode) {
|
|
9623
|
+
throw new ConfigError("deviceCode is required");
|
|
9624
|
+
}
|
|
9625
|
+
const deadline = req.deadline ?? new Date(Date.now() + 10 * 60 * 1e3);
|
|
9626
|
+
let intervalMs = Math.max(1, req.intervalSeconds ?? 5) * 1e3;
|
|
9627
|
+
while (true) {
|
|
9628
|
+
if (Date.now() >= deadline.getTime()) {
|
|
9629
|
+
throw new TransportError("oauth device flow timed out", { kind: "timeout" });
|
|
9630
|
+
}
|
|
9631
|
+
const form = new URLSearchParams();
|
|
9632
|
+
form.set("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
|
|
9633
|
+
form.set("device_code", deviceCode);
|
|
9634
|
+
form.set("client_id", clientId);
|
|
9635
|
+
const payload = await postOAuthForm(tokenEndpoint, form, {
|
|
9636
|
+
fetch: req.fetch,
|
|
9637
|
+
signal: req.signal,
|
|
9638
|
+
allowErrorPayload: true
|
|
9639
|
+
});
|
|
9640
|
+
const err = String(payload.error || "").trim();
|
|
9641
|
+
if (err) {
|
|
9642
|
+
switch (err) {
|
|
9643
|
+
case "authorization_pending":
|
|
9644
|
+
await sleep(intervalMs, req.signal);
|
|
9645
|
+
continue;
|
|
9646
|
+
case "slow_down":
|
|
9647
|
+
intervalMs += 5e3;
|
|
9648
|
+
await sleep(intervalMs, req.signal);
|
|
9649
|
+
continue;
|
|
9650
|
+
case "expired_token":
|
|
9651
|
+
case "access_denied":
|
|
9652
|
+
case "invalid_grant":
|
|
9653
|
+
throw new TransportError(`oauth device flow failed: ${err}`, {
|
|
9654
|
+
kind: "request",
|
|
9655
|
+
cause: payload
|
|
9656
|
+
});
|
|
9657
|
+
default:
|
|
9658
|
+
throw new TransportError(`oauth device flow error: ${err}`, {
|
|
9659
|
+
kind: "request",
|
|
9660
|
+
cause: payload
|
|
9661
|
+
});
|
|
9662
|
+
}
|
|
9663
|
+
}
|
|
9664
|
+
const accessToken = String(payload.access_token || "").trim() || void 0;
|
|
9665
|
+
const idToken = String(payload.id_token || "").trim() || void 0;
|
|
9666
|
+
const refreshToken = String(payload.refresh_token || "").trim() || void 0;
|
|
9667
|
+
const tokenType = String(payload.token_type || "").trim() || void 0;
|
|
9668
|
+
const scope = String(payload.scope || "").trim() || void 0;
|
|
9669
|
+
const expiresIn = payload.expires_in !== void 0 ? Number(payload.expires_in) : void 0;
|
|
9670
|
+
const expiresAt = typeof expiresIn === "number" && Number.isFinite(expiresIn) && expiresIn > 0 ? new Date(Date.now() + expiresIn * 1e3) : void 0;
|
|
9671
|
+
if (!accessToken && !idToken) {
|
|
9672
|
+
throw new TransportError("oauth device flow returned an invalid token response", {
|
|
9673
|
+
kind: "request",
|
|
9674
|
+
cause: payload
|
|
9675
|
+
});
|
|
9676
|
+
}
|
|
9677
|
+
return { accessToken, idToken, refreshToken, tokenType, scope, expiresAt };
|
|
9678
|
+
}
|
|
9679
|
+
}
|
|
9680
|
+
async function runOAuthDeviceFlowForIDToken(cfg) {
|
|
9681
|
+
const auth = await startOAuthDeviceAuthorization({
|
|
9682
|
+
deviceAuthorizationEndpoint: cfg.deviceAuthorizationEndpoint,
|
|
9683
|
+
clientId: cfg.clientId,
|
|
9684
|
+
scope: cfg.scope,
|
|
9685
|
+
audience: cfg.audience,
|
|
9686
|
+
fetch: cfg.fetch,
|
|
9687
|
+
signal: cfg.signal
|
|
9688
|
+
});
|
|
9689
|
+
await cfg.onUserCode(auth);
|
|
9690
|
+
const token = await pollOAuthDeviceToken({
|
|
9691
|
+
tokenEndpoint: cfg.tokenEndpoint,
|
|
9692
|
+
clientId: cfg.clientId,
|
|
9693
|
+
deviceCode: auth.deviceCode,
|
|
9694
|
+
intervalSeconds: auth.intervalSeconds,
|
|
9695
|
+
deadline: auth.expiresAt,
|
|
9696
|
+
fetch: cfg.fetch,
|
|
9697
|
+
signal: cfg.signal
|
|
9698
|
+
});
|
|
9699
|
+
if (!token.idToken) {
|
|
9700
|
+
throw new TransportError("oauth device flow did not return an id_token", {
|
|
9701
|
+
kind: "request",
|
|
9702
|
+
cause: token
|
|
9703
|
+
});
|
|
9704
|
+
}
|
|
9705
|
+
return token.idToken;
|
|
9706
|
+
}
|
|
9707
|
+
async function postOAuthForm(url, form, opts) {
|
|
9708
|
+
const fetchFn = opts.fetch ?? globalThis.fetch;
|
|
9709
|
+
if (!fetchFn) {
|
|
9710
|
+
throw new ConfigError("fetch is not available; provide a fetch implementation");
|
|
9711
|
+
}
|
|
9712
|
+
let resp;
|
|
9713
|
+
try {
|
|
9714
|
+
resp = await fetchFn(url, {
|
|
9715
|
+
method: "POST",
|
|
9716
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
9717
|
+
body: form.toString(),
|
|
9718
|
+
signal: opts.signal
|
|
9719
|
+
});
|
|
9720
|
+
} catch (cause) {
|
|
9721
|
+
throw new TransportError("oauth request failed", { kind: "request", cause });
|
|
9722
|
+
}
|
|
9723
|
+
let json;
|
|
9724
|
+
try {
|
|
9725
|
+
json = await resp.json();
|
|
9726
|
+
} catch (cause) {
|
|
9727
|
+
throw new TransportError("oauth response was not valid JSON", { kind: "request", cause });
|
|
9728
|
+
}
|
|
9729
|
+
if (!resp.ok && !opts.allowErrorPayload) {
|
|
9730
|
+
throw new TransportError(`oauth request failed (${resp.status})`, {
|
|
9731
|
+
kind: "request",
|
|
9732
|
+
cause: json
|
|
9733
|
+
});
|
|
9734
|
+
}
|
|
9735
|
+
return json || {};
|
|
9736
|
+
}
|
|
9737
|
+
async function sleep(ms, signal) {
|
|
9738
|
+
if (!ms || ms <= 0) {
|
|
9739
|
+
return;
|
|
9740
|
+
}
|
|
9741
|
+
if (!signal) {
|
|
9742
|
+
await new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
9743
|
+
return;
|
|
9744
|
+
}
|
|
9745
|
+
if (signal.aborted) {
|
|
9746
|
+
throw new TransportError("oauth device flow aborted", { kind: "request" });
|
|
9747
|
+
}
|
|
9748
|
+
await new Promise((resolve2, reject) => {
|
|
9749
|
+
const onAbort = () => {
|
|
9750
|
+
signal.removeEventListener("abort", onAbort);
|
|
9751
|
+
reject(new TransportError("oauth device flow aborted", { kind: "request" }));
|
|
9752
|
+
};
|
|
9753
|
+
signal.addEventListener("abort", onAbort);
|
|
9754
|
+
setTimeout(() => {
|
|
9755
|
+
signal.removeEventListener("abort", onAbort);
|
|
9756
|
+
resolve2();
|
|
9757
|
+
}, ms);
|
|
9758
|
+
});
|
|
9759
|
+
}
|
|
9760
|
+
|
|
9761
|
+
// src/workflow_builder.ts
|
|
9762
|
+
function transformJSONValue(from, pointer) {
|
|
9763
|
+
return pointer ? { from, pointer } : { from };
|
|
9764
|
+
}
|
|
9765
|
+
function transformJSONObject(object) {
|
|
9766
|
+
return { object };
|
|
9767
|
+
}
|
|
9768
|
+
function transformJSONMerge(merge) {
|
|
9769
|
+
return { merge: merge.slice() };
|
|
9770
|
+
}
|
|
9771
|
+
function wireRequest(req) {
|
|
9772
|
+
const raw = req;
|
|
9773
|
+
if (raw && typeof raw === "object") {
|
|
9774
|
+
if ("input" in raw) {
|
|
9775
|
+
return req;
|
|
9776
|
+
}
|
|
9777
|
+
if ("body" in raw) {
|
|
9778
|
+
return raw.body ?? {};
|
|
9779
|
+
}
|
|
9780
|
+
}
|
|
9781
|
+
return asInternal(req).body;
|
|
9782
|
+
}
|
|
9783
|
+
var WorkflowBuilderV0 = class _WorkflowBuilderV0 {
|
|
9784
|
+
constructor(state = { nodes: [], edges: [], outputs: [] }) {
|
|
9785
|
+
this.state = state;
|
|
9786
|
+
}
|
|
9787
|
+
static new() {
|
|
9788
|
+
return new _WorkflowBuilderV0();
|
|
9789
|
+
}
|
|
9790
|
+
with(patch) {
|
|
9791
|
+
return new _WorkflowBuilderV0({
|
|
9792
|
+
...this.state,
|
|
9793
|
+
...patch
|
|
9794
|
+
});
|
|
9795
|
+
}
|
|
9796
|
+
name(name) {
|
|
9797
|
+
return this.with({ name: name.trim() || void 0 });
|
|
9798
|
+
}
|
|
9799
|
+
execution(execution) {
|
|
9800
|
+
return this.with({ execution });
|
|
9801
|
+
}
|
|
9802
|
+
node(node) {
|
|
9803
|
+
return this.with({ nodes: [...this.state.nodes, node] });
|
|
9804
|
+
}
|
|
9805
|
+
llmResponses(id, request, options = {}) {
|
|
9806
|
+
const input = {
|
|
9807
|
+
request: wireRequest(request),
|
|
9808
|
+
...options.stream === void 0 ? {} : { stream: options.stream },
|
|
9809
|
+
...options.toolExecution === void 0 ? {} : { tool_execution: { mode: options.toolExecution } },
|
|
9810
|
+
...options.toolLimits === void 0 ? {} : { tool_limits: { ...options.toolLimits } },
|
|
9811
|
+
...options.bindings === void 0 ? {} : { bindings: options.bindings.slice() }
|
|
9812
|
+
};
|
|
9813
|
+
return this.node({
|
|
9814
|
+
id,
|
|
9815
|
+
type: WorkflowNodeTypes.LLMResponses,
|
|
9816
|
+
input
|
|
9817
|
+
});
|
|
9818
|
+
}
|
|
9819
|
+
joinAll(id) {
|
|
9820
|
+
return this.node({ id, type: WorkflowNodeTypes.JoinAll });
|
|
9821
|
+
}
|
|
9822
|
+
transformJSON(id, input) {
|
|
9823
|
+
return this.node({ id, type: WorkflowNodeTypes.TransformJSON, input });
|
|
9824
|
+
}
|
|
9825
|
+
edge(from, to) {
|
|
9826
|
+
return this.with({ edges: [...this.state.edges, { from, to }] });
|
|
9827
|
+
}
|
|
9828
|
+
output(name, from, pointer) {
|
|
9829
|
+
return this.with({
|
|
9830
|
+
outputs: [
|
|
9831
|
+
...this.state.outputs,
|
|
9832
|
+
{ name, from, ...pointer ? { pointer } : {} }
|
|
9833
|
+
]
|
|
9834
|
+
});
|
|
9835
|
+
}
|
|
9836
|
+
build() {
|
|
9837
|
+
const edges = this.state.edges.slice().sort((a, b) => {
|
|
9838
|
+
const af = String(a.from);
|
|
9839
|
+
const bf = String(b.from);
|
|
9840
|
+
if (af < bf) return -1;
|
|
9841
|
+
if (af > bf) return 1;
|
|
9842
|
+
const at = String(a.to);
|
|
9843
|
+
const bt = String(b.to);
|
|
9844
|
+
if (at < bt) return -1;
|
|
9845
|
+
if (at > bt) return 1;
|
|
9846
|
+
return 0;
|
|
9847
|
+
});
|
|
9848
|
+
const outputs = this.state.outputs.slice().sort((a, b) => {
|
|
9849
|
+
const an = String(a.name);
|
|
9850
|
+
const bn = String(b.name);
|
|
9851
|
+
if (an < bn) return -1;
|
|
9852
|
+
if (an > bn) return 1;
|
|
9853
|
+
const af = String(a.from);
|
|
9854
|
+
const bf = String(b.from);
|
|
9855
|
+
if (af < bf) return -1;
|
|
9856
|
+
if (af > bf) return 1;
|
|
9857
|
+
const ap = a.pointer ?? "";
|
|
9858
|
+
const bp = b.pointer ?? "";
|
|
9859
|
+
if (ap < bp) return -1;
|
|
9860
|
+
if (ap > bp) return 1;
|
|
9861
|
+
return 0;
|
|
9862
|
+
});
|
|
9863
|
+
return {
|
|
9864
|
+
kind: WorkflowKinds.WorkflowV0,
|
|
9865
|
+
...this.state.name ? { name: this.state.name } : {},
|
|
9866
|
+
...this.state.execution ? { execution: this.state.execution } : {},
|
|
9867
|
+
nodes: this.state.nodes.slice(),
|
|
9868
|
+
...edges.length ? { edges } : {},
|
|
9869
|
+
outputs
|
|
9870
|
+
};
|
|
9871
|
+
}
|
|
9872
|
+
};
|
|
9873
|
+
function workflowV0() {
|
|
9874
|
+
return WorkflowBuilderV0.new();
|
|
9875
|
+
}
|
|
9876
|
+
|
|
9877
|
+
// src/workflow_v0.schema.json
|
|
9878
|
+
var workflow_v0_schema_default = {
|
|
9879
|
+
$id: "https://modelrelay.ai/schemas/workflow_v0.schema.json",
|
|
9880
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
9881
|
+
additionalProperties: false,
|
|
9882
|
+
definitions: {
|
|
9883
|
+
edge: {
|
|
9884
|
+
additionalProperties: false,
|
|
9885
|
+
properties: {
|
|
9886
|
+
from: {
|
|
9887
|
+
minLength: 1,
|
|
9888
|
+
type: "string"
|
|
9889
|
+
},
|
|
9890
|
+
to: {
|
|
9891
|
+
minLength: 1,
|
|
9892
|
+
type: "string"
|
|
9893
|
+
}
|
|
9894
|
+
},
|
|
9895
|
+
required: [
|
|
9896
|
+
"from",
|
|
9897
|
+
"to"
|
|
9898
|
+
],
|
|
9899
|
+
type: "object"
|
|
9900
|
+
},
|
|
9901
|
+
llmResponsesBinding: {
|
|
9902
|
+
additionalProperties: false,
|
|
9903
|
+
properties: {
|
|
9904
|
+
encoding: {
|
|
9905
|
+
enum: [
|
|
9906
|
+
"json",
|
|
9907
|
+
"json_string"
|
|
9908
|
+
],
|
|
9909
|
+
type: "string"
|
|
9910
|
+
},
|
|
9911
|
+
from: {
|
|
9912
|
+
minLength: 1,
|
|
9913
|
+
type: "string"
|
|
9914
|
+
},
|
|
9915
|
+
pointer: {
|
|
9916
|
+
pattern: "^(/.*)?$",
|
|
9917
|
+
type: "string"
|
|
9918
|
+
},
|
|
9919
|
+
to: {
|
|
9920
|
+
pattern: "^/.*$",
|
|
9921
|
+
type: "string"
|
|
9922
|
+
}
|
|
9923
|
+
},
|
|
9924
|
+
required: [
|
|
9925
|
+
"from",
|
|
9926
|
+
"to"
|
|
9927
|
+
],
|
|
9928
|
+
type: "object"
|
|
9929
|
+
},
|
|
9930
|
+
node: {
|
|
9931
|
+
additionalProperties: false,
|
|
9932
|
+
oneOf: [
|
|
9933
|
+
{
|
|
9934
|
+
allOf: [
|
|
9935
|
+
{
|
|
9936
|
+
properties: {
|
|
9937
|
+
input: {
|
|
9938
|
+
properties: {
|
|
9939
|
+
bindings: {
|
|
9940
|
+
items: {
|
|
9941
|
+
$ref: "#/definitions/llmResponsesBinding"
|
|
9942
|
+
},
|
|
9943
|
+
type: "array"
|
|
9944
|
+
},
|
|
9945
|
+
request: {
|
|
9946
|
+
type: "object"
|
|
9947
|
+
},
|
|
9948
|
+
stream: {
|
|
9949
|
+
type: "boolean"
|
|
9950
|
+
},
|
|
9951
|
+
tool_execution: {
|
|
9952
|
+
additionalProperties: false,
|
|
9953
|
+
properties: {
|
|
9954
|
+
mode: {
|
|
9955
|
+
default: "server",
|
|
9956
|
+
enum: [
|
|
9957
|
+
"server",
|
|
8869
9958
|
"client"
|
|
8870
9959
|
],
|
|
8871
9960
|
type: "string"
|
|
@@ -8924,182 +10013,1499 @@ var workflow_v0_schema_default = {
|
|
|
8924
10013
|
const: "join.all"
|
|
8925
10014
|
}
|
|
8926
10015
|
}
|
|
8927
|
-
},
|
|
8928
|
-
{
|
|
8929
|
-
allOf: [
|
|
8930
|
-
{
|
|
8931
|
-
properties: {
|
|
8932
|
-
input: {
|
|
8933
|
-
additionalProperties: false,
|
|
8934
|
-
oneOf: [
|
|
8935
|
-
{
|
|
8936
|
-
not: {
|
|
8937
|
-
required: [
|
|
8938
|
-
"merge"
|
|
8939
|
-
]
|
|
8940
|
-
},
|
|
8941
|
-
required: [
|
|
8942
|
-
"object"
|
|
8943
|
-
]
|
|
8944
|
-
},
|
|
8945
|
-
{
|
|
8946
|
-
not: {
|
|
8947
|
-
required: [
|
|
8948
|
-
"object"
|
|
8949
|
-
]
|
|
8950
|
-
},
|
|
8951
|
-
required: [
|
|
8952
|
-
"merge"
|
|
8953
|
-
]
|
|
8954
|
-
}
|
|
8955
|
-
],
|
|
8956
|
-
properties: {
|
|
8957
|
-
merge: {
|
|
8958
|
-
items: {
|
|
8959
|
-
$ref: "#/definitions/transformValue"
|
|
8960
|
-
},
|
|
8961
|
-
minItems: 1,
|
|
8962
|
-
type: "array"
|
|
8963
|
-
},
|
|
8964
|
-
object: {
|
|
8965
|
-
additionalProperties: {
|
|
8966
|
-
$ref: "#/definitions/transformValue"
|
|
8967
|
-
},
|
|
8968
|
-
minProperties: 1,
|
|
8969
|
-
type: "object"
|
|
8970
|
-
}
|
|
8971
|
-
},
|
|
8972
|
-
type: "object"
|
|
8973
|
-
}
|
|
10016
|
+
},
|
|
10017
|
+
{
|
|
10018
|
+
allOf: [
|
|
10019
|
+
{
|
|
10020
|
+
properties: {
|
|
10021
|
+
input: {
|
|
10022
|
+
additionalProperties: false,
|
|
10023
|
+
oneOf: [
|
|
10024
|
+
{
|
|
10025
|
+
not: {
|
|
10026
|
+
required: [
|
|
10027
|
+
"merge"
|
|
10028
|
+
]
|
|
10029
|
+
},
|
|
10030
|
+
required: [
|
|
10031
|
+
"object"
|
|
10032
|
+
]
|
|
10033
|
+
},
|
|
10034
|
+
{
|
|
10035
|
+
not: {
|
|
10036
|
+
required: [
|
|
10037
|
+
"object"
|
|
10038
|
+
]
|
|
10039
|
+
},
|
|
10040
|
+
required: [
|
|
10041
|
+
"merge"
|
|
10042
|
+
]
|
|
10043
|
+
}
|
|
10044
|
+
],
|
|
10045
|
+
properties: {
|
|
10046
|
+
merge: {
|
|
10047
|
+
items: {
|
|
10048
|
+
$ref: "#/definitions/transformValue"
|
|
10049
|
+
},
|
|
10050
|
+
minItems: 1,
|
|
10051
|
+
type: "array"
|
|
10052
|
+
},
|
|
10053
|
+
object: {
|
|
10054
|
+
additionalProperties: {
|
|
10055
|
+
$ref: "#/definitions/transformValue"
|
|
10056
|
+
},
|
|
10057
|
+
minProperties: 1,
|
|
10058
|
+
type: "object"
|
|
10059
|
+
}
|
|
10060
|
+
},
|
|
10061
|
+
type: "object"
|
|
10062
|
+
}
|
|
10063
|
+
}
|
|
10064
|
+
}
|
|
10065
|
+
],
|
|
10066
|
+
properties: {
|
|
10067
|
+
type: {
|
|
10068
|
+
const: "transform.json"
|
|
10069
|
+
}
|
|
10070
|
+
},
|
|
10071
|
+
required: [
|
|
10072
|
+
"input"
|
|
10073
|
+
]
|
|
10074
|
+
}
|
|
10075
|
+
],
|
|
10076
|
+
properties: {
|
|
10077
|
+
id: {
|
|
10078
|
+
minLength: 1,
|
|
10079
|
+
type: "string"
|
|
10080
|
+
},
|
|
10081
|
+
input: {},
|
|
10082
|
+
type: {
|
|
10083
|
+
enum: [
|
|
10084
|
+
"llm.responses",
|
|
10085
|
+
"join.all",
|
|
10086
|
+
"transform.json"
|
|
10087
|
+
],
|
|
10088
|
+
type: "string"
|
|
10089
|
+
}
|
|
10090
|
+
},
|
|
10091
|
+
required: [
|
|
10092
|
+
"id",
|
|
10093
|
+
"type"
|
|
10094
|
+
],
|
|
10095
|
+
type: "object"
|
|
10096
|
+
},
|
|
10097
|
+
output: {
|
|
10098
|
+
additionalProperties: false,
|
|
10099
|
+
properties: {
|
|
10100
|
+
from: {
|
|
10101
|
+
minLength: 1,
|
|
10102
|
+
type: "string"
|
|
10103
|
+
},
|
|
10104
|
+
name: {
|
|
10105
|
+
minLength: 1,
|
|
10106
|
+
type: "string"
|
|
10107
|
+
},
|
|
10108
|
+
pointer: {
|
|
10109
|
+
pattern: "^(/.*)?$",
|
|
10110
|
+
type: "string"
|
|
10111
|
+
}
|
|
10112
|
+
},
|
|
10113
|
+
required: [
|
|
10114
|
+
"name",
|
|
10115
|
+
"from"
|
|
10116
|
+
],
|
|
10117
|
+
type: "object"
|
|
10118
|
+
},
|
|
10119
|
+
transformValue: {
|
|
10120
|
+
additionalProperties: false,
|
|
10121
|
+
properties: {
|
|
10122
|
+
from: {
|
|
10123
|
+
minLength: 1,
|
|
10124
|
+
type: "string"
|
|
10125
|
+
},
|
|
10126
|
+
pointer: {
|
|
10127
|
+
pattern: "^(/.*)?$",
|
|
10128
|
+
type: "string"
|
|
10129
|
+
}
|
|
10130
|
+
},
|
|
10131
|
+
required: [
|
|
10132
|
+
"from"
|
|
10133
|
+
],
|
|
10134
|
+
type: "object"
|
|
10135
|
+
}
|
|
10136
|
+
},
|
|
10137
|
+
properties: {
|
|
10138
|
+
edges: {
|
|
10139
|
+
items: {
|
|
10140
|
+
$ref: "#/definitions/edge"
|
|
10141
|
+
},
|
|
10142
|
+
type: "array"
|
|
10143
|
+
},
|
|
10144
|
+
execution: {
|
|
10145
|
+
additionalProperties: false,
|
|
10146
|
+
properties: {
|
|
10147
|
+
max_parallelism: {
|
|
10148
|
+
minimum: 1,
|
|
10149
|
+
type: "integer"
|
|
10150
|
+
},
|
|
10151
|
+
node_timeout_ms: {
|
|
10152
|
+
minimum: 1,
|
|
10153
|
+
type: "integer"
|
|
10154
|
+
},
|
|
10155
|
+
run_timeout_ms: {
|
|
10156
|
+
minimum: 1,
|
|
10157
|
+
type: "integer"
|
|
10158
|
+
}
|
|
10159
|
+
},
|
|
10160
|
+
type: "object"
|
|
10161
|
+
},
|
|
10162
|
+
kind: {
|
|
10163
|
+
const: "workflow.v0",
|
|
10164
|
+
type: "string"
|
|
10165
|
+
},
|
|
10166
|
+
name: {
|
|
10167
|
+
type: "string"
|
|
10168
|
+
},
|
|
10169
|
+
nodes: {
|
|
10170
|
+
items: {
|
|
10171
|
+
$ref: "#/definitions/node"
|
|
10172
|
+
},
|
|
10173
|
+
minItems: 1,
|
|
10174
|
+
type: "array"
|
|
10175
|
+
},
|
|
10176
|
+
outputs: {
|
|
10177
|
+
items: {
|
|
10178
|
+
$ref: "#/definitions/output"
|
|
10179
|
+
},
|
|
10180
|
+
minItems: 1,
|
|
10181
|
+
type: "array"
|
|
10182
|
+
}
|
|
10183
|
+
},
|
|
10184
|
+
required: [
|
|
10185
|
+
"kind",
|
|
10186
|
+
"nodes",
|
|
10187
|
+
"outputs"
|
|
10188
|
+
],
|
|
10189
|
+
title: "ModelRelay workflow.v0",
|
|
10190
|
+
type: "object"
|
|
10191
|
+
};
|
|
10192
|
+
|
|
10193
|
+
// src/tools_local_fs.ts
|
|
10194
|
+
import { promises as fs } from "fs";
|
|
10195
|
+
import * as path from "path";
|
|
10196
|
+
import { spawn } from "child_process";
|
|
10197
|
+
var ToolNames = {
|
|
10198
|
+
FS_READ_FILE: "fs.read_file",
|
|
10199
|
+
FS_LIST_FILES: "fs.list_files",
|
|
10200
|
+
FS_SEARCH: "fs.search"
|
|
10201
|
+
};
|
|
10202
|
+
var FSDefaults = {
|
|
10203
|
+
MAX_READ_BYTES: 64e3,
|
|
10204
|
+
HARD_MAX_READ_BYTES: 1e6,
|
|
10205
|
+
MAX_LIST_ENTRIES: 2e3,
|
|
10206
|
+
HARD_MAX_LIST_ENTRIES: 2e4,
|
|
10207
|
+
MAX_SEARCH_MATCHES: 100,
|
|
10208
|
+
HARD_MAX_SEARCH_MATCHES: 2e3,
|
|
10209
|
+
SEARCH_TIMEOUT_MS: 5e3,
|
|
10210
|
+
MAX_SEARCH_BYTES_PER_FILE: 1e6
|
|
10211
|
+
};
|
|
10212
|
+
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
10213
|
+
".git",
|
|
10214
|
+
"node_modules",
|
|
10215
|
+
"vendor",
|
|
10216
|
+
"dist",
|
|
10217
|
+
"build",
|
|
10218
|
+
".next",
|
|
10219
|
+
"target",
|
|
10220
|
+
".idea",
|
|
10221
|
+
".vscode",
|
|
10222
|
+
"__pycache__",
|
|
10223
|
+
".pytest_cache",
|
|
10224
|
+
"coverage"
|
|
10225
|
+
]);
|
|
10226
|
+
var LocalFSToolPack = class {
|
|
10227
|
+
constructor(options) {
|
|
10228
|
+
this.rgPath = null;
|
|
10229
|
+
this.rgChecked = false;
|
|
10230
|
+
const root = options.root?.trim();
|
|
10231
|
+
if (!root) {
|
|
10232
|
+
throw new Error("LocalFSToolPack: root directory required");
|
|
10233
|
+
}
|
|
10234
|
+
this.rootAbs = path.resolve(root);
|
|
10235
|
+
this.cfg = {
|
|
10236
|
+
ignoreDirs: options.ignoreDirs ?? new Set(DEFAULT_IGNORE_DIRS),
|
|
10237
|
+
maxReadBytes: options.maxReadBytes ?? FSDefaults.MAX_READ_BYTES,
|
|
10238
|
+
hardMaxReadBytes: options.hardMaxReadBytes ?? FSDefaults.HARD_MAX_READ_BYTES,
|
|
10239
|
+
maxListEntries: options.maxListEntries ?? FSDefaults.MAX_LIST_ENTRIES,
|
|
10240
|
+
hardMaxListEntries: options.hardMaxListEntries ?? FSDefaults.HARD_MAX_LIST_ENTRIES,
|
|
10241
|
+
maxSearchMatches: options.maxSearchMatches ?? FSDefaults.MAX_SEARCH_MATCHES,
|
|
10242
|
+
hardMaxSearchMatches: options.hardMaxSearchMatches ?? FSDefaults.HARD_MAX_SEARCH_MATCHES,
|
|
10243
|
+
searchTimeoutMs: options.searchTimeoutMs ?? FSDefaults.SEARCH_TIMEOUT_MS,
|
|
10244
|
+
maxSearchBytesPerFile: options.maxSearchBytesPerFile ?? FSDefaults.MAX_SEARCH_BYTES_PER_FILE
|
|
10245
|
+
};
|
|
10246
|
+
}
|
|
10247
|
+
/**
|
|
10248
|
+
* Returns the tool definitions for LLM requests.
|
|
10249
|
+
* Use these when constructing the tools array for /responses requests.
|
|
10250
|
+
*/
|
|
10251
|
+
getToolDefinitions() {
|
|
10252
|
+
return [
|
|
10253
|
+
{
|
|
10254
|
+
type: "function",
|
|
10255
|
+
function: {
|
|
10256
|
+
name: ToolNames.FS_READ_FILE,
|
|
10257
|
+
description: "Read the contents of a file. Returns the file contents as UTF-8 text.",
|
|
10258
|
+
parameters: {
|
|
10259
|
+
type: "object",
|
|
10260
|
+
properties: {
|
|
10261
|
+
path: {
|
|
10262
|
+
type: "string",
|
|
10263
|
+
description: "Workspace-relative path to the file (e.g., 'src/index.ts')"
|
|
10264
|
+
},
|
|
10265
|
+
max_bytes: {
|
|
10266
|
+
type: "integer",
|
|
10267
|
+
description: `Maximum bytes to read. Default: ${this.cfg.maxReadBytes}, max: ${this.cfg.hardMaxReadBytes}`
|
|
10268
|
+
}
|
|
10269
|
+
},
|
|
10270
|
+
required: ["path"]
|
|
10271
|
+
}
|
|
10272
|
+
}
|
|
10273
|
+
},
|
|
10274
|
+
{
|
|
10275
|
+
type: "function",
|
|
10276
|
+
function: {
|
|
10277
|
+
name: ToolNames.FS_LIST_FILES,
|
|
10278
|
+
description: "List files recursively in a directory. Returns newline-separated workspace-relative paths.",
|
|
10279
|
+
parameters: {
|
|
10280
|
+
type: "object",
|
|
10281
|
+
properties: {
|
|
10282
|
+
path: {
|
|
10283
|
+
type: "string",
|
|
10284
|
+
description: "Workspace-relative directory path. Default: '.' (workspace root)"
|
|
10285
|
+
},
|
|
10286
|
+
max_entries: {
|
|
10287
|
+
type: "integer",
|
|
10288
|
+
description: `Maximum files to list. Default: ${this.cfg.maxListEntries}, max: ${this.cfg.hardMaxListEntries}`
|
|
8974
10289
|
}
|
|
8975
10290
|
}
|
|
8976
|
-
|
|
8977
|
-
properties: {
|
|
8978
|
-
type: {
|
|
8979
|
-
const: "transform.json"
|
|
8980
|
-
}
|
|
8981
|
-
},
|
|
8982
|
-
required: [
|
|
8983
|
-
"input"
|
|
8984
|
-
]
|
|
10291
|
+
}
|
|
8985
10292
|
}
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
10293
|
+
},
|
|
10294
|
+
{
|
|
10295
|
+
type: "function",
|
|
10296
|
+
function: {
|
|
10297
|
+
name: ToolNames.FS_SEARCH,
|
|
10298
|
+
description: "Search for text matching a regex pattern. Returns matches as 'path:line:content' format.",
|
|
10299
|
+
parameters: {
|
|
10300
|
+
type: "object",
|
|
10301
|
+
properties: {
|
|
10302
|
+
query: {
|
|
10303
|
+
type: "string",
|
|
10304
|
+
description: "Regex pattern to search for"
|
|
10305
|
+
},
|
|
10306
|
+
path: {
|
|
10307
|
+
type: "string",
|
|
10308
|
+
description: "Workspace-relative directory to search. Default: '.' (workspace root)"
|
|
10309
|
+
},
|
|
10310
|
+
max_matches: {
|
|
10311
|
+
type: "integer",
|
|
10312
|
+
description: `Maximum matches to return. Default: ${this.cfg.maxSearchMatches}, max: ${this.cfg.hardMaxSearchMatches}`
|
|
10313
|
+
}
|
|
10314
|
+
},
|
|
10315
|
+
required: ["query"]
|
|
10316
|
+
}
|
|
10317
|
+
}
|
|
10318
|
+
}
|
|
10319
|
+
];
|
|
10320
|
+
}
|
|
10321
|
+
/**
|
|
10322
|
+
* Registers handlers into an existing ToolRegistry.
|
|
10323
|
+
* @param registry - The registry to register into
|
|
10324
|
+
* @returns The registry for chaining
|
|
10325
|
+
*/
|
|
10326
|
+
registerInto(registry) {
|
|
10327
|
+
registry.register(ToolNames.FS_READ_FILE, this.readFile.bind(this));
|
|
10328
|
+
registry.register(ToolNames.FS_LIST_FILES, this.listFiles.bind(this));
|
|
10329
|
+
registry.register(ToolNames.FS_SEARCH, this.search.bind(this));
|
|
10330
|
+
return registry;
|
|
10331
|
+
}
|
|
10332
|
+
/**
|
|
10333
|
+
* Creates a new ToolRegistry with fs.* tools pre-registered.
|
|
10334
|
+
*/
|
|
10335
|
+
toRegistry() {
|
|
10336
|
+
return this.registerInto(new ToolRegistry());
|
|
10337
|
+
}
|
|
10338
|
+
// ========================================================================
|
|
10339
|
+
// Tool Handlers
|
|
10340
|
+
// ========================================================================
|
|
10341
|
+
async readFile(_args, call) {
|
|
10342
|
+
const args = this.parseArgs(call, ["path"]);
|
|
10343
|
+
const func = call.function;
|
|
10344
|
+
const relPath = this.requireString(args, "path", call);
|
|
10345
|
+
const requestedMax = this.optionalPositiveInt(args, "max_bytes", call);
|
|
10346
|
+
let maxBytes = this.cfg.maxReadBytes;
|
|
10347
|
+
if (requestedMax !== void 0) {
|
|
10348
|
+
if (requestedMax > this.cfg.hardMaxReadBytes) {
|
|
10349
|
+
throw new ToolArgumentError({
|
|
10350
|
+
message: `max_bytes exceeds hard cap (${this.cfg.hardMaxReadBytes})`,
|
|
10351
|
+
toolCallId: call.id,
|
|
10352
|
+
toolName: func.name,
|
|
10353
|
+
rawArguments: func.arguments
|
|
10354
|
+
});
|
|
10355
|
+
}
|
|
10356
|
+
maxBytes = requestedMax;
|
|
10357
|
+
}
|
|
10358
|
+
const absPath = await this.resolveAndValidatePath(relPath, call);
|
|
10359
|
+
const stat = await fs.stat(absPath);
|
|
10360
|
+
if (stat.isDirectory()) {
|
|
10361
|
+
throw new Error(`fs.read_file: path is a directory: ${relPath}`);
|
|
10362
|
+
}
|
|
10363
|
+
if (stat.size > maxBytes) {
|
|
10364
|
+
throw new Error(`fs.read_file: file exceeds max_bytes (${maxBytes})`);
|
|
10365
|
+
}
|
|
10366
|
+
const data = await fs.readFile(absPath);
|
|
10367
|
+
if (!this.isValidUtf8(data)) {
|
|
10368
|
+
throw new Error(`fs.read_file: file is not valid UTF-8: ${relPath}`);
|
|
10369
|
+
}
|
|
10370
|
+
return data.toString("utf-8");
|
|
10371
|
+
}
|
|
10372
|
+
async listFiles(_args, call) {
|
|
10373
|
+
const args = this.parseArgs(call, []);
|
|
10374
|
+
const func = call.function;
|
|
10375
|
+
const startPath = this.optionalString(args, "path", call)?.trim() || ".";
|
|
10376
|
+
let maxEntries = this.cfg.maxListEntries;
|
|
10377
|
+
const requestedMax = this.optionalPositiveInt(args, "max_entries", call);
|
|
10378
|
+
if (requestedMax !== void 0) {
|
|
10379
|
+
if (requestedMax > this.cfg.hardMaxListEntries) {
|
|
10380
|
+
throw new ToolArgumentError({
|
|
10381
|
+
message: `max_entries exceeds hard cap (${this.cfg.hardMaxListEntries})`,
|
|
10382
|
+
toolCallId: call.id,
|
|
10383
|
+
toolName: func.name,
|
|
10384
|
+
rawArguments: func.arguments
|
|
10385
|
+
});
|
|
10386
|
+
}
|
|
10387
|
+
maxEntries = requestedMax;
|
|
10388
|
+
}
|
|
10389
|
+
const absPath = await this.resolveAndValidatePath(startPath, call);
|
|
10390
|
+
let rootReal;
|
|
10391
|
+
try {
|
|
10392
|
+
rootReal = await fs.realpath(this.rootAbs);
|
|
10393
|
+
} catch {
|
|
10394
|
+
rootReal = this.rootAbs;
|
|
10395
|
+
}
|
|
10396
|
+
const stat = await fs.stat(absPath);
|
|
10397
|
+
if (!stat.isDirectory()) {
|
|
10398
|
+
throw new Error(`fs.list_files: path is not a directory: ${startPath}`);
|
|
10399
|
+
}
|
|
10400
|
+
const files = [];
|
|
10401
|
+
await this.walkDir(absPath, async (filePath, dirent) => {
|
|
10402
|
+
if (files.length >= maxEntries) {
|
|
10403
|
+
return false;
|
|
10404
|
+
}
|
|
10405
|
+
if (dirent.isDirectory()) {
|
|
10406
|
+
if (this.cfg.ignoreDirs.has(dirent.name)) {
|
|
10407
|
+
return false;
|
|
10408
|
+
}
|
|
10409
|
+
return true;
|
|
10410
|
+
}
|
|
10411
|
+
if (dirent.isFile()) {
|
|
10412
|
+
const relPath = path.relative(rootReal, filePath);
|
|
10413
|
+
files.push(relPath.split(path.sep).join("/"));
|
|
10414
|
+
}
|
|
10415
|
+
return true;
|
|
10416
|
+
});
|
|
10417
|
+
return files.join("\n");
|
|
10418
|
+
}
|
|
10419
|
+
async search(_args, call) {
|
|
10420
|
+
const args = this.parseArgs(call, ["query"]);
|
|
10421
|
+
const func = call.function;
|
|
10422
|
+
const query = this.requireString(args, "query", call);
|
|
10423
|
+
const startPath = this.optionalString(args, "path", call)?.trim() || ".";
|
|
10424
|
+
let maxMatches = this.cfg.maxSearchMatches;
|
|
10425
|
+
const requestedMax = this.optionalPositiveInt(args, "max_matches", call);
|
|
10426
|
+
if (requestedMax !== void 0) {
|
|
10427
|
+
if (requestedMax > this.cfg.hardMaxSearchMatches) {
|
|
10428
|
+
throw new ToolArgumentError({
|
|
10429
|
+
message: `max_matches exceeds hard cap (${this.cfg.hardMaxSearchMatches})`,
|
|
10430
|
+
toolCallId: call.id,
|
|
10431
|
+
toolName: func.name,
|
|
10432
|
+
rawArguments: func.arguments
|
|
10433
|
+
});
|
|
10434
|
+
}
|
|
10435
|
+
maxMatches = requestedMax;
|
|
10436
|
+
}
|
|
10437
|
+
const absPath = await this.resolveAndValidatePath(startPath, call);
|
|
10438
|
+
const rgPath = await this.detectRipgrep();
|
|
10439
|
+
if (rgPath) {
|
|
10440
|
+
return this.searchWithRipgrep(
|
|
10441
|
+
rgPath,
|
|
10442
|
+
query,
|
|
10443
|
+
absPath,
|
|
10444
|
+
maxMatches
|
|
10445
|
+
);
|
|
10446
|
+
}
|
|
10447
|
+
return this.searchWithJS(query, absPath, maxMatches, call);
|
|
10448
|
+
}
|
|
10449
|
+
// ========================================================================
|
|
10450
|
+
// Path Safety
|
|
10451
|
+
// ========================================================================
|
|
10452
|
+
/**
|
|
10453
|
+
* Resolves a workspace-relative path and validates it stays within the sandbox.
|
|
10454
|
+
* @throws {ToolArgumentError} if path is invalid
|
|
10455
|
+
* @throws {PathEscapeError} if resolved path escapes root
|
|
10456
|
+
*/
|
|
10457
|
+
async resolveAndValidatePath(relPath, call) {
|
|
10458
|
+
const func = call.function;
|
|
10459
|
+
const cleanRel = relPath.trim();
|
|
10460
|
+
if (!cleanRel) {
|
|
10461
|
+
throw new ToolArgumentError({
|
|
10462
|
+
message: "path cannot be empty",
|
|
10463
|
+
toolCallId: call.id,
|
|
10464
|
+
toolName: func.name,
|
|
10465
|
+
rawArguments: func.arguments
|
|
10466
|
+
});
|
|
10467
|
+
}
|
|
10468
|
+
if (path.isAbsolute(cleanRel)) {
|
|
10469
|
+
throw new ToolArgumentError({
|
|
10470
|
+
message: "path must be workspace-relative (not absolute)",
|
|
10471
|
+
toolCallId: call.id,
|
|
10472
|
+
toolName: func.name,
|
|
10473
|
+
rawArguments: func.arguments
|
|
10474
|
+
});
|
|
10475
|
+
}
|
|
10476
|
+
const normalized = path.normalize(cleanRel);
|
|
10477
|
+
if (normalized.startsWith("..") || normalized.startsWith(`.${path.sep}..`)) {
|
|
10478
|
+
throw new ToolArgumentError({
|
|
10479
|
+
message: "path must not escape the workspace root",
|
|
10480
|
+
toolCallId: call.id,
|
|
10481
|
+
toolName: func.name,
|
|
10482
|
+
rawArguments: func.arguments
|
|
10483
|
+
});
|
|
10484
|
+
}
|
|
10485
|
+
const target = path.join(this.rootAbs, normalized);
|
|
10486
|
+
let rootReal;
|
|
10487
|
+
try {
|
|
10488
|
+
rootReal = await fs.realpath(this.rootAbs);
|
|
10489
|
+
} catch {
|
|
10490
|
+
rootReal = this.rootAbs;
|
|
10491
|
+
}
|
|
10492
|
+
let resolved;
|
|
10493
|
+
try {
|
|
10494
|
+
resolved = await fs.realpath(target);
|
|
10495
|
+
} catch (err) {
|
|
10496
|
+
resolved = path.join(rootReal, normalized);
|
|
10497
|
+
}
|
|
10498
|
+
const relFromRoot = path.relative(rootReal, resolved);
|
|
10499
|
+
if (relFromRoot.startsWith("..") || relFromRoot.startsWith(`.${path.sep}..`) || path.isAbsolute(relFromRoot)) {
|
|
10500
|
+
throw new PathEscapeError({
|
|
10501
|
+
requestedPath: relPath,
|
|
10502
|
+
resolvedPath: resolved
|
|
10503
|
+
});
|
|
10504
|
+
}
|
|
10505
|
+
return resolved;
|
|
10506
|
+
}
|
|
10507
|
+
// ========================================================================
|
|
10508
|
+
// Ripgrep Search
|
|
10509
|
+
// ========================================================================
|
|
10510
|
+
async detectRipgrep() {
|
|
10511
|
+
if (this.rgChecked) {
|
|
10512
|
+
return this.rgPath;
|
|
10513
|
+
}
|
|
10514
|
+
this.rgChecked = true;
|
|
10515
|
+
return new Promise((resolve2) => {
|
|
10516
|
+
const proc = spawn("rg", ["--version"], { stdio: "ignore" });
|
|
10517
|
+
proc.on("error", () => {
|
|
10518
|
+
this.rgPath = null;
|
|
10519
|
+
resolve2(null);
|
|
10520
|
+
});
|
|
10521
|
+
proc.on("close", (code) => {
|
|
10522
|
+
if (code === 0) {
|
|
10523
|
+
this.rgPath = "rg";
|
|
10524
|
+
resolve2("rg");
|
|
10525
|
+
} else {
|
|
10526
|
+
this.rgPath = null;
|
|
10527
|
+
resolve2(null);
|
|
10528
|
+
}
|
|
10529
|
+
});
|
|
10530
|
+
});
|
|
10531
|
+
}
|
|
10532
|
+
async searchWithRipgrep(rgPath, query, dirAbs, maxMatches) {
|
|
10533
|
+
return new Promise((resolve2, reject) => {
|
|
10534
|
+
const args = ["--line-number", "--no-heading", "--color=never"];
|
|
10535
|
+
for (const name of this.cfg.ignoreDirs) {
|
|
10536
|
+
args.push("--glob", `!**/${name}/**`);
|
|
10537
|
+
}
|
|
10538
|
+
args.push(query, dirAbs);
|
|
10539
|
+
const proc = spawn(rgPath, args, {
|
|
10540
|
+
timeout: this.cfg.searchTimeoutMs
|
|
10541
|
+
});
|
|
10542
|
+
const lines = [];
|
|
10543
|
+
let stderr = "";
|
|
10544
|
+
let killed = false;
|
|
10545
|
+
proc.stdout.on("data", (chunk) => {
|
|
10546
|
+
if (killed) return;
|
|
10547
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
10548
|
+
const newLines = text.split("\n").filter((l) => l.trim());
|
|
10549
|
+
for (const line of newLines) {
|
|
10550
|
+
lines.push(this.normalizeRipgrepLine(line));
|
|
10551
|
+
if (lines.length >= maxMatches) {
|
|
10552
|
+
killed = true;
|
|
10553
|
+
proc.kill();
|
|
10554
|
+
break;
|
|
10555
|
+
}
|
|
10556
|
+
}
|
|
10557
|
+
});
|
|
10558
|
+
proc.stderr.on("data", (chunk) => {
|
|
10559
|
+
stderr += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
10560
|
+
});
|
|
10561
|
+
proc.on("error", (err) => {
|
|
10562
|
+
reject(new Error(`fs.search: ripgrep error: ${err.message}`));
|
|
10563
|
+
});
|
|
10564
|
+
proc.on("close", (code) => {
|
|
10565
|
+
if (killed) {
|
|
10566
|
+
resolve2(lines.join("\n"));
|
|
10567
|
+
return;
|
|
10568
|
+
}
|
|
10569
|
+
if (code === 0 || code === 1) {
|
|
10570
|
+
resolve2(lines.join("\n"));
|
|
10571
|
+
} else if (code === 2 && stderr.toLowerCase().includes("regex")) {
|
|
10572
|
+
reject(
|
|
10573
|
+
new ToolArgumentError({
|
|
10574
|
+
message: `invalid query regex: ${stderr.trim()}`,
|
|
10575
|
+
toolCallId: "",
|
|
10576
|
+
toolName: ToolNames.FS_SEARCH,
|
|
10577
|
+
rawArguments: ""
|
|
10578
|
+
})
|
|
10579
|
+
);
|
|
10580
|
+
} else if (stderr) {
|
|
10581
|
+
reject(new Error(`fs.search: ripgrep failed: ${stderr.trim()}`));
|
|
10582
|
+
} else {
|
|
10583
|
+
resolve2(lines.join("\n"));
|
|
10584
|
+
}
|
|
10585
|
+
});
|
|
10586
|
+
});
|
|
10587
|
+
}
|
|
10588
|
+
normalizeRipgrepLine(line) {
|
|
10589
|
+
const trimmed = line.trim();
|
|
10590
|
+
if (!trimmed || !trimmed.includes(":")) {
|
|
10591
|
+
return trimmed;
|
|
10592
|
+
}
|
|
10593
|
+
const colonIdx = trimmed.indexOf(":");
|
|
10594
|
+
const filePath = trimmed.slice(0, colonIdx);
|
|
10595
|
+
const rest = trimmed.slice(colonIdx + 1);
|
|
10596
|
+
if (path.isAbsolute(filePath)) {
|
|
10597
|
+
const rel = path.relative(this.rootAbs, filePath);
|
|
10598
|
+
if (!rel.startsWith("..")) {
|
|
10599
|
+
return rel.split(path.sep).join("/") + ":" + rest;
|
|
10600
|
+
}
|
|
10601
|
+
}
|
|
10602
|
+
return filePath.split(path.sep).join("/") + ":" + rest;
|
|
10603
|
+
}
|
|
10604
|
+
// ========================================================================
|
|
10605
|
+
// JavaScript Fallback Search
|
|
10606
|
+
// ========================================================================
|
|
10607
|
+
async searchWithJS(query, dirAbs, maxMatches, call) {
|
|
10608
|
+
const func = call.function;
|
|
10609
|
+
let regex;
|
|
10610
|
+
try {
|
|
10611
|
+
regex = new RegExp(query);
|
|
10612
|
+
} catch (err) {
|
|
10613
|
+
throw new ToolArgumentError({
|
|
10614
|
+
message: `invalid query regex: ${err.message}`,
|
|
10615
|
+
toolCallId: call.id,
|
|
10616
|
+
toolName: func.name,
|
|
10617
|
+
rawArguments: func.arguments
|
|
10618
|
+
});
|
|
10619
|
+
}
|
|
10620
|
+
const matches = [];
|
|
10621
|
+
const deadline = Date.now() + this.cfg.searchTimeoutMs;
|
|
10622
|
+
await this.walkDir(dirAbs, async (filePath, dirent) => {
|
|
10623
|
+
if (Date.now() > deadline) {
|
|
10624
|
+
return false;
|
|
10625
|
+
}
|
|
10626
|
+
if (matches.length >= maxMatches) {
|
|
10627
|
+
return false;
|
|
10628
|
+
}
|
|
10629
|
+
if (dirent.isDirectory()) {
|
|
10630
|
+
if (this.cfg.ignoreDirs.has(dirent.name)) {
|
|
10631
|
+
return false;
|
|
10632
|
+
}
|
|
10633
|
+
return true;
|
|
10634
|
+
}
|
|
10635
|
+
if (!dirent.isFile()) {
|
|
10636
|
+
return true;
|
|
10637
|
+
}
|
|
10638
|
+
try {
|
|
10639
|
+
const stat = await fs.stat(filePath);
|
|
10640
|
+
if (stat.size > this.cfg.maxSearchBytesPerFile) {
|
|
10641
|
+
return true;
|
|
10642
|
+
}
|
|
10643
|
+
} catch {
|
|
10644
|
+
return true;
|
|
10645
|
+
}
|
|
10646
|
+
try {
|
|
10647
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
10648
|
+
const lines = content.split("\n");
|
|
10649
|
+
for (let i = 0; i < lines.length && matches.length < maxMatches; i++) {
|
|
10650
|
+
if (regex.test(lines[i])) {
|
|
10651
|
+
const relPath = path.relative(this.rootAbs, filePath);
|
|
10652
|
+
const normalizedPath = relPath.split(path.sep).join("/");
|
|
10653
|
+
matches.push(`${normalizedPath}:${i + 1}:${lines[i]}`);
|
|
10654
|
+
}
|
|
10655
|
+
}
|
|
10656
|
+
} catch {
|
|
10657
|
+
}
|
|
10658
|
+
return true;
|
|
10659
|
+
});
|
|
10660
|
+
return matches.join("\n");
|
|
10661
|
+
}
|
|
10662
|
+
// ========================================================================
|
|
10663
|
+
// Helpers
|
|
10664
|
+
// ========================================================================
|
|
10665
|
+
parseArgs(call, required) {
|
|
10666
|
+
const func = call.function;
|
|
10667
|
+
if (!func) {
|
|
10668
|
+
throw new ToolArgumentError({
|
|
10669
|
+
message: "tool call missing function",
|
|
10670
|
+
toolCallId: call.id,
|
|
10671
|
+
toolName: "",
|
|
10672
|
+
rawArguments: ""
|
|
10673
|
+
});
|
|
10674
|
+
}
|
|
10675
|
+
const rawArgs = func.arguments || "{}";
|
|
10676
|
+
let parsed;
|
|
10677
|
+
try {
|
|
10678
|
+
parsed = JSON.parse(rawArgs);
|
|
10679
|
+
} catch (err) {
|
|
10680
|
+
throw new ToolArgumentError({
|
|
10681
|
+
message: `invalid JSON arguments: ${err.message}`,
|
|
10682
|
+
toolCallId: call.id,
|
|
10683
|
+
toolName: func.name,
|
|
10684
|
+
rawArguments: rawArgs
|
|
10685
|
+
});
|
|
10686
|
+
}
|
|
10687
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
10688
|
+
throw new ToolArgumentError({
|
|
10689
|
+
message: "arguments must be an object",
|
|
10690
|
+
toolCallId: call.id,
|
|
10691
|
+
toolName: func.name,
|
|
10692
|
+
rawArguments: rawArgs
|
|
10693
|
+
});
|
|
10694
|
+
}
|
|
10695
|
+
const args = parsed;
|
|
10696
|
+
for (const key of required) {
|
|
10697
|
+
const value = args[key];
|
|
10698
|
+
if (value === void 0 || value === null || value === "") {
|
|
10699
|
+
throw new ToolArgumentError({
|
|
10700
|
+
message: `${key} is required`,
|
|
10701
|
+
toolCallId: call.id,
|
|
10702
|
+
toolName: func.name,
|
|
10703
|
+
rawArguments: rawArgs
|
|
10704
|
+
});
|
|
10705
|
+
}
|
|
10706
|
+
}
|
|
10707
|
+
return args;
|
|
10708
|
+
}
|
|
10709
|
+
toolArgumentError(call, message) {
|
|
10710
|
+
const func = call.function;
|
|
10711
|
+
throw new ToolArgumentError({
|
|
10712
|
+
message,
|
|
10713
|
+
toolCallId: call.id,
|
|
10714
|
+
toolName: func?.name ?? "",
|
|
10715
|
+
rawArguments: func?.arguments ?? ""
|
|
10716
|
+
});
|
|
10717
|
+
}
|
|
10718
|
+
requireString(args, key, call) {
|
|
10719
|
+
const value = args[key];
|
|
10720
|
+
if (typeof value !== "string") {
|
|
10721
|
+
this.toolArgumentError(call, `${key} must be a string`);
|
|
10722
|
+
}
|
|
10723
|
+
if (value.trim() === "") {
|
|
10724
|
+
this.toolArgumentError(call, `${key} is required`);
|
|
10725
|
+
}
|
|
10726
|
+
return value;
|
|
10727
|
+
}
|
|
10728
|
+
optionalString(args, key, call) {
|
|
10729
|
+
const value = args[key];
|
|
10730
|
+
if (value === void 0 || value === null) {
|
|
10731
|
+
return void 0;
|
|
10732
|
+
}
|
|
10733
|
+
if (typeof value !== "string") {
|
|
10734
|
+
this.toolArgumentError(call, `${key} must be a string`);
|
|
10735
|
+
}
|
|
10736
|
+
const trimmed = value.trim();
|
|
10737
|
+
if (trimmed === "") {
|
|
10738
|
+
return void 0;
|
|
10739
|
+
}
|
|
10740
|
+
return value;
|
|
10741
|
+
}
|
|
10742
|
+
optionalPositiveInt(args, key, call) {
|
|
10743
|
+
const value = args[key];
|
|
10744
|
+
if (value === void 0 || value === null) {
|
|
10745
|
+
return void 0;
|
|
10746
|
+
}
|
|
10747
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) {
|
|
10748
|
+
this.toolArgumentError(call, `${key} must be an integer`);
|
|
10749
|
+
}
|
|
10750
|
+
if (value <= 0) {
|
|
10751
|
+
this.toolArgumentError(call, `${key} must be > 0`);
|
|
10752
|
+
}
|
|
10753
|
+
return value;
|
|
10754
|
+
}
|
|
10755
|
+
isValidUtf8(buffer) {
|
|
10756
|
+
try {
|
|
10757
|
+
const text = buffer.toString("utf-8");
|
|
10758
|
+
return !text.includes("\uFFFD");
|
|
10759
|
+
} catch {
|
|
10760
|
+
return false;
|
|
10761
|
+
}
|
|
10762
|
+
}
|
|
10763
|
+
/**
|
|
10764
|
+
* Recursively walks a directory, calling visitor for each entry.
|
|
10765
|
+
* Visitor returns true to continue, false to skip (for dirs) or stop.
|
|
10766
|
+
*/
|
|
10767
|
+
async walkDir(dir, visitor) {
|
|
10768
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
10769
|
+
for (const entry of entries) {
|
|
10770
|
+
const fullPath = path.join(dir, entry.name);
|
|
10771
|
+
const shouldContinue = await visitor(fullPath, entry);
|
|
10772
|
+
if (!shouldContinue) {
|
|
10773
|
+
if (entry.isDirectory()) {
|
|
10774
|
+
continue;
|
|
10775
|
+
}
|
|
10776
|
+
return;
|
|
10777
|
+
}
|
|
10778
|
+
if (entry.isDirectory()) {
|
|
10779
|
+
await this.walkDir(fullPath, visitor);
|
|
10780
|
+
}
|
|
10781
|
+
}
|
|
10782
|
+
}
|
|
10783
|
+
};
|
|
10784
|
+
function createLocalFSToolPack(options) {
|
|
10785
|
+
return new LocalFSToolPack(options);
|
|
10786
|
+
}
|
|
10787
|
+
function createLocalFSTools(options) {
|
|
10788
|
+
return createLocalFSToolPack(options).toRegistry();
|
|
10789
|
+
}
|
|
10790
|
+
|
|
10791
|
+
// src/tools_browser.ts
|
|
10792
|
+
var BrowserToolNames = {
|
|
10793
|
+
/** Navigate to a URL and return accessibility tree */
|
|
10794
|
+
NAVIGATE: "browser.navigate",
|
|
10795
|
+
/** Click an element by accessible name/role */
|
|
10796
|
+
CLICK: "browser.click",
|
|
10797
|
+
/** Type text into an input field */
|
|
10798
|
+
TYPE: "browser.type",
|
|
10799
|
+
/** Get current accessibility tree */
|
|
10800
|
+
SNAPSHOT: "browser.snapshot",
|
|
10801
|
+
/** Scroll the page */
|
|
10802
|
+
SCROLL: "browser.scroll",
|
|
10803
|
+
/** Capture a screenshot */
|
|
10804
|
+
SCREENSHOT: "browser.screenshot",
|
|
10805
|
+
/** Extract data using CSS selectors */
|
|
10806
|
+
EXTRACT: "browser.extract"
|
|
10807
|
+
};
|
|
10808
|
+
var BrowserDefaults = {
|
|
10809
|
+
/** Navigation timeout in milliseconds */
|
|
10810
|
+
NAVIGATION_TIMEOUT_MS: 3e4,
|
|
10811
|
+
/** Action timeout in milliseconds */
|
|
10812
|
+
ACTION_TIMEOUT_MS: 5e3,
|
|
10813
|
+
/** Maximum nodes to include in accessibility tree output */
|
|
10814
|
+
MAX_SNAPSHOT_NODES: 500,
|
|
10815
|
+
/** Maximum screenshot size in bytes */
|
|
10816
|
+
MAX_SCREENSHOT_BYTES: 5e6
|
|
10817
|
+
};
|
|
10818
|
+
var BrowserToolPack = class {
|
|
10819
|
+
constructor(options = {}) {
|
|
10820
|
+
this.browser = null;
|
|
10821
|
+
this.context = null;
|
|
10822
|
+
this.page = null;
|
|
10823
|
+
this.cdpSession = null;
|
|
10824
|
+
this.ownsBrowser = false;
|
|
10825
|
+
this.ownsContext = false;
|
|
10826
|
+
this.cfg = {
|
|
10827
|
+
allowedDomains: options.allowedDomains ?? [],
|
|
10828
|
+
blockedDomains: options.blockedDomains ?? [],
|
|
10829
|
+
navigationTimeoutMs: options.navigationTimeoutMs ?? BrowserDefaults.NAVIGATION_TIMEOUT_MS,
|
|
10830
|
+
actionTimeoutMs: options.actionTimeoutMs ?? BrowserDefaults.ACTION_TIMEOUT_MS,
|
|
10831
|
+
maxSnapshotNodes: options.maxSnapshotNodes ?? BrowserDefaults.MAX_SNAPSHOT_NODES,
|
|
10832
|
+
headless: options.headless ?? true
|
|
10833
|
+
};
|
|
10834
|
+
if (options.browser) {
|
|
10835
|
+
this.browser = options.browser;
|
|
10836
|
+
this.ownsBrowser = false;
|
|
10837
|
+
}
|
|
10838
|
+
if (options.context) {
|
|
10839
|
+
this.context = options.context;
|
|
10840
|
+
this.ownsContext = false;
|
|
10841
|
+
}
|
|
10842
|
+
}
|
|
10843
|
+
/**
|
|
10844
|
+
* Initialize the browser. Must be called before using any tools.
|
|
10845
|
+
*/
|
|
10846
|
+
async initialize() {
|
|
10847
|
+
if (this.page) {
|
|
10848
|
+
return;
|
|
10849
|
+
}
|
|
10850
|
+
const { chromium } = await import("playwright");
|
|
10851
|
+
if (!this.browser) {
|
|
10852
|
+
this.browser = await chromium.launch({
|
|
10853
|
+
headless: this.cfg.headless
|
|
10854
|
+
});
|
|
10855
|
+
this.ownsBrowser = true;
|
|
10856
|
+
}
|
|
10857
|
+
if (!this.context) {
|
|
10858
|
+
this.context = await this.browser.newContext();
|
|
10859
|
+
this.ownsContext = true;
|
|
10860
|
+
}
|
|
10861
|
+
this.page = await this.context.newPage();
|
|
10862
|
+
}
|
|
10863
|
+
/**
|
|
10864
|
+
* Close the browser and clean up resources.
|
|
10865
|
+
*/
|
|
10866
|
+
async close() {
|
|
10867
|
+
if (this.cdpSession) {
|
|
10868
|
+
await this.cdpSession.detach().catch(() => {
|
|
10869
|
+
});
|
|
10870
|
+
this.cdpSession = null;
|
|
10871
|
+
}
|
|
10872
|
+
if (this.page) {
|
|
10873
|
+
await this.page.close().catch(() => {
|
|
10874
|
+
});
|
|
10875
|
+
this.page = null;
|
|
10876
|
+
}
|
|
10877
|
+
if (this.ownsContext && this.context) {
|
|
10878
|
+
await this.context.close().catch(() => {
|
|
10879
|
+
});
|
|
10880
|
+
this.context = null;
|
|
10881
|
+
}
|
|
10882
|
+
if (this.ownsBrowser && this.browser) {
|
|
10883
|
+
await this.browser.close().catch(() => {
|
|
10884
|
+
});
|
|
10885
|
+
this.browser = null;
|
|
10886
|
+
}
|
|
10887
|
+
}
|
|
10888
|
+
/**
|
|
10889
|
+
* Get tool definitions for use with LLM APIs.
|
|
10890
|
+
*/
|
|
10891
|
+
getToolDefinitions() {
|
|
10892
|
+
return [
|
|
10893
|
+
{
|
|
10894
|
+
type: ToolTypes.Function,
|
|
10895
|
+
function: {
|
|
10896
|
+
name: BrowserToolNames.NAVIGATE,
|
|
10897
|
+
description: "Navigate to a URL and return the page's accessibility tree. The tree shows interactive elements (buttons, links, inputs) with their accessible names.",
|
|
10898
|
+
parameters: {
|
|
10899
|
+
type: "object",
|
|
10900
|
+
properties: {
|
|
10901
|
+
url: {
|
|
10902
|
+
type: "string",
|
|
10903
|
+
description: "The URL to navigate to (must be http/https)"
|
|
10904
|
+
},
|
|
10905
|
+
waitUntil: {
|
|
10906
|
+
type: "string",
|
|
10907
|
+
enum: ["load", "domcontentloaded", "networkidle"],
|
|
10908
|
+
description: "When to consider navigation complete. Default: domcontentloaded"
|
|
10909
|
+
}
|
|
10910
|
+
},
|
|
10911
|
+
required: ["url"]
|
|
10912
|
+
}
|
|
9000
10913
|
}
|
|
9001
10914
|
},
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
10915
|
+
{
|
|
10916
|
+
type: ToolTypes.Function,
|
|
10917
|
+
function: {
|
|
10918
|
+
name: BrowserToolNames.CLICK,
|
|
10919
|
+
description: "Click an element by its accessible name. Returns updated accessibility tree.",
|
|
10920
|
+
parameters: {
|
|
10921
|
+
type: "object",
|
|
10922
|
+
properties: {
|
|
10923
|
+
name: {
|
|
10924
|
+
type: "string",
|
|
10925
|
+
description: "The accessible name of the element (from button text, aria-label, etc.)"
|
|
10926
|
+
},
|
|
10927
|
+
role: {
|
|
10928
|
+
type: "string",
|
|
10929
|
+
enum: [
|
|
10930
|
+
"button",
|
|
10931
|
+
"link",
|
|
10932
|
+
"menuitem",
|
|
10933
|
+
"checkbox",
|
|
10934
|
+
"radio",
|
|
10935
|
+
"tab"
|
|
10936
|
+
],
|
|
10937
|
+
description: "ARIA role to match. If omitted, searches buttons, links, and menuitems."
|
|
10938
|
+
}
|
|
10939
|
+
},
|
|
10940
|
+
required: ["name"]
|
|
10941
|
+
}
|
|
9022
10942
|
}
|
|
9023
10943
|
},
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
10944
|
+
{
|
|
10945
|
+
type: ToolTypes.Function,
|
|
10946
|
+
function: {
|
|
10947
|
+
name: BrowserToolNames.TYPE,
|
|
10948
|
+
description: "Type text into an input field identified by accessible name.",
|
|
10949
|
+
parameters: {
|
|
10950
|
+
type: "object",
|
|
10951
|
+
properties: {
|
|
10952
|
+
name: {
|
|
10953
|
+
type: "string",
|
|
10954
|
+
description: "The accessible name of the input (from label, aria-label, placeholder)"
|
|
10955
|
+
},
|
|
10956
|
+
text: {
|
|
10957
|
+
type: "string",
|
|
10958
|
+
description: "The text to type"
|
|
10959
|
+
},
|
|
10960
|
+
role: {
|
|
10961
|
+
type: "string",
|
|
10962
|
+
enum: ["textbox", "searchbox", "combobox"],
|
|
10963
|
+
description: "ARIA role. Default: textbox"
|
|
10964
|
+
}
|
|
10965
|
+
},
|
|
10966
|
+
required: ["name", "text"]
|
|
10967
|
+
}
|
|
9040
10968
|
}
|
|
9041
10969
|
},
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
10970
|
+
{
|
|
10971
|
+
type: ToolTypes.Function,
|
|
10972
|
+
function: {
|
|
10973
|
+
name: BrowserToolNames.SNAPSHOT,
|
|
10974
|
+
description: "Get the current page's accessibility tree without navigating.",
|
|
10975
|
+
parameters: {
|
|
10976
|
+
type: "object",
|
|
10977
|
+
properties: {}
|
|
10978
|
+
}
|
|
10979
|
+
}
|
|
9052
10980
|
},
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9058
|
-
|
|
9059
|
-
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
10981
|
+
{
|
|
10982
|
+
type: ToolTypes.Function,
|
|
10983
|
+
function: {
|
|
10984
|
+
name: BrowserToolNames.SCROLL,
|
|
10985
|
+
description: "Scroll the page in a given direction.",
|
|
10986
|
+
parameters: {
|
|
10987
|
+
type: "object",
|
|
10988
|
+
properties: {
|
|
10989
|
+
direction: {
|
|
10990
|
+
type: "string",
|
|
10991
|
+
enum: ["up", "down"],
|
|
10992
|
+
description: "Scroll direction"
|
|
10993
|
+
},
|
|
10994
|
+
amount: {
|
|
10995
|
+
type: "string",
|
|
10996
|
+
enum: ["page", "half", "toTop", "toBottom"],
|
|
10997
|
+
description: "How much to scroll. Default: page"
|
|
10998
|
+
}
|
|
10999
|
+
},
|
|
11000
|
+
required: ["direction"]
|
|
11001
|
+
}
|
|
9069
11002
|
}
|
|
9070
11003
|
},
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
11004
|
+
{
|
|
11005
|
+
type: ToolTypes.Function,
|
|
11006
|
+
function: {
|
|
11007
|
+
name: BrowserToolNames.SCREENSHOT,
|
|
11008
|
+
description: "Capture a PNG screenshot of the current page. Use sparingly - prefer accessibility tree for decisions.",
|
|
11009
|
+
parameters: {
|
|
11010
|
+
type: "object",
|
|
11011
|
+
properties: {
|
|
11012
|
+
fullPage: {
|
|
11013
|
+
type: "boolean",
|
|
11014
|
+
description: "Capture full scrollable page. Default: false (viewport only)"
|
|
11015
|
+
}
|
|
11016
|
+
}
|
|
11017
|
+
}
|
|
11018
|
+
}
|
|
9083
11019
|
},
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
11020
|
+
{
|
|
11021
|
+
type: ToolTypes.Function,
|
|
11022
|
+
function: {
|
|
11023
|
+
name: BrowserToolNames.EXTRACT,
|
|
11024
|
+
description: "Extract structured data from the page using CSS selectors.",
|
|
11025
|
+
parameters: {
|
|
11026
|
+
type: "object",
|
|
11027
|
+
properties: {
|
|
11028
|
+
selector: {
|
|
11029
|
+
type: "string",
|
|
11030
|
+
description: "CSS selector for elements to extract"
|
|
11031
|
+
},
|
|
11032
|
+
attribute: {
|
|
11033
|
+
type: "string",
|
|
11034
|
+
description: "Attribute to extract (textContent, href, src, etc.). Default: textContent"
|
|
11035
|
+
},
|
|
11036
|
+
multiple: {
|
|
11037
|
+
type: "boolean",
|
|
11038
|
+
description: "Return all matches as JSON array. Default: false (first match only)"
|
|
11039
|
+
}
|
|
11040
|
+
},
|
|
11041
|
+
required: ["selector"]
|
|
11042
|
+
}
|
|
11043
|
+
}
|
|
11044
|
+
}
|
|
11045
|
+
];
|
|
11046
|
+
}
|
|
11047
|
+
/**
|
|
11048
|
+
* Register tool handlers into an existing registry.
|
|
11049
|
+
*/
|
|
11050
|
+
registerInto(registry) {
|
|
11051
|
+
registry.register(BrowserToolNames.NAVIGATE, this.navigate.bind(this));
|
|
11052
|
+
registry.register(BrowserToolNames.CLICK, this.click.bind(this));
|
|
11053
|
+
registry.register(BrowserToolNames.TYPE, this.type.bind(this));
|
|
11054
|
+
registry.register(BrowserToolNames.SNAPSHOT, this.snapshot.bind(this));
|
|
11055
|
+
registry.register(BrowserToolNames.SCROLL, this.scroll.bind(this));
|
|
11056
|
+
registry.register(BrowserToolNames.SCREENSHOT, this.screenshot.bind(this));
|
|
11057
|
+
registry.register(BrowserToolNames.EXTRACT, this.extract.bind(this));
|
|
11058
|
+
return registry;
|
|
11059
|
+
}
|
|
11060
|
+
/**
|
|
11061
|
+
* Create a new registry with just this pack's tools.
|
|
11062
|
+
*/
|
|
11063
|
+
toRegistry() {
|
|
11064
|
+
return this.registerInto(new ToolRegistry());
|
|
11065
|
+
}
|
|
11066
|
+
// ========================================================================
|
|
11067
|
+
// Private: Helpers
|
|
11068
|
+
// ========================================================================
|
|
11069
|
+
ensureInitialized() {
|
|
11070
|
+
if (!this.page) {
|
|
11071
|
+
throw new Error(
|
|
11072
|
+
"BrowserToolPack not initialized. Call initialize() first."
|
|
11073
|
+
);
|
|
11074
|
+
}
|
|
11075
|
+
}
|
|
11076
|
+
parseArgs(call, required) {
|
|
11077
|
+
const func = call.function;
|
|
11078
|
+
if (!func) {
|
|
11079
|
+
throw new ToolArgumentError({
|
|
11080
|
+
message: "tool call missing function",
|
|
11081
|
+
toolCallId: call.id,
|
|
11082
|
+
toolName: "",
|
|
11083
|
+
rawArguments: ""
|
|
11084
|
+
});
|
|
11085
|
+
}
|
|
11086
|
+
const rawArgs = func.arguments || "{}";
|
|
11087
|
+
let parsed;
|
|
11088
|
+
try {
|
|
11089
|
+
parsed = JSON.parse(rawArgs);
|
|
11090
|
+
} catch (err) {
|
|
11091
|
+
throw new ToolArgumentError({
|
|
11092
|
+
message: `invalid JSON arguments: ${err.message}`,
|
|
11093
|
+
toolCallId: call.id,
|
|
11094
|
+
toolName: func.name,
|
|
11095
|
+
rawArguments: rawArgs
|
|
11096
|
+
});
|
|
11097
|
+
}
|
|
11098
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
11099
|
+
throw new ToolArgumentError({
|
|
11100
|
+
message: "arguments must be an object",
|
|
11101
|
+
toolCallId: call.id,
|
|
11102
|
+
toolName: func.name,
|
|
11103
|
+
rawArguments: rawArgs
|
|
11104
|
+
});
|
|
11105
|
+
}
|
|
11106
|
+
const args = parsed;
|
|
11107
|
+
for (const key of required) {
|
|
11108
|
+
const value = args[key];
|
|
11109
|
+
if (value === void 0 || value === null || value === "") {
|
|
11110
|
+
throw new ToolArgumentError({
|
|
11111
|
+
message: `${key} is required`,
|
|
11112
|
+
toolCallId: call.id,
|
|
11113
|
+
toolName: func.name,
|
|
11114
|
+
rawArguments: rawArgs
|
|
11115
|
+
});
|
|
11116
|
+
}
|
|
11117
|
+
}
|
|
11118
|
+
return args;
|
|
11119
|
+
}
|
|
11120
|
+
validateUrl(url, call) {
|
|
11121
|
+
let parsed;
|
|
11122
|
+
try {
|
|
11123
|
+
parsed = new URL(url);
|
|
11124
|
+
} catch {
|
|
11125
|
+
throw new ToolArgumentError({
|
|
11126
|
+
message: `Invalid URL: ${url}`,
|
|
11127
|
+
toolCallId: call.id,
|
|
11128
|
+
toolName: call.function?.name ?? "",
|
|
11129
|
+
rawArguments: call.function?.arguments ?? ""
|
|
11130
|
+
});
|
|
11131
|
+
}
|
|
11132
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
11133
|
+
throw new ToolArgumentError({
|
|
11134
|
+
message: `Invalid protocol: ${parsed.protocol}. Only http/https allowed.`,
|
|
11135
|
+
toolCallId: call.id,
|
|
11136
|
+
toolName: call.function?.name ?? "",
|
|
11137
|
+
rawArguments: call.function?.arguments ?? ""
|
|
11138
|
+
});
|
|
11139
|
+
}
|
|
11140
|
+
const domain = parsed.hostname;
|
|
11141
|
+
if (this.cfg.blockedDomains.some((d) => domain.endsWith(d))) {
|
|
11142
|
+
throw new ToolArgumentError({
|
|
11143
|
+
message: `Domain blocked: ${domain}`,
|
|
11144
|
+
toolCallId: call.id,
|
|
11145
|
+
toolName: call.function?.name ?? "",
|
|
11146
|
+
rawArguments: call.function?.arguments ?? ""
|
|
11147
|
+
});
|
|
11148
|
+
}
|
|
11149
|
+
if (this.cfg.allowedDomains.length > 0) {
|
|
11150
|
+
if (!this.cfg.allowedDomains.some((d) => domain.endsWith(d))) {
|
|
11151
|
+
throw new ToolArgumentError({
|
|
11152
|
+
message: `Domain not in allowlist: ${domain}`,
|
|
11153
|
+
toolCallId: call.id,
|
|
11154
|
+
toolName: call.function?.name ?? "",
|
|
11155
|
+
rawArguments: call.function?.arguments ?? ""
|
|
11156
|
+
});
|
|
11157
|
+
}
|
|
11158
|
+
}
|
|
11159
|
+
}
|
|
11160
|
+
/**
|
|
11161
|
+
* Validates the current page URL against allowlist/blocklist.
|
|
11162
|
+
* Called after navigation and before any action to catch redirects
|
|
11163
|
+
* and in-session navigation to blocked domains.
|
|
11164
|
+
*/
|
|
11165
|
+
ensureCurrentUrlAllowed() {
|
|
11166
|
+
if (!this.page) return;
|
|
11167
|
+
const currentUrl = this.page.url();
|
|
11168
|
+
if (currentUrl === "about:blank") return;
|
|
11169
|
+
let parsed;
|
|
11170
|
+
try {
|
|
11171
|
+
parsed = new URL(currentUrl);
|
|
11172
|
+
} catch {
|
|
11173
|
+
throw new Error(`Current page has invalid URL: ${currentUrl}`);
|
|
11174
|
+
}
|
|
11175
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
11176
|
+
throw new Error(
|
|
11177
|
+
`Current page protocol not allowed: ${parsed.protocol}. Only http/https allowed.`
|
|
11178
|
+
);
|
|
11179
|
+
}
|
|
11180
|
+
const domain = parsed.hostname;
|
|
11181
|
+
if (this.cfg.blockedDomains.some((d) => domain.endsWith(d))) {
|
|
11182
|
+
throw new Error(`Current page domain is blocked: ${domain}`);
|
|
11183
|
+
}
|
|
11184
|
+
if (this.cfg.allowedDomains.length > 0) {
|
|
11185
|
+
if (!this.cfg.allowedDomains.some((d) => domain.endsWith(d))) {
|
|
11186
|
+
throw new Error(`Current page domain not in allowlist: ${domain}`);
|
|
11187
|
+
}
|
|
11188
|
+
}
|
|
11189
|
+
}
|
|
11190
|
+
async getAccessibilityTree() {
|
|
11191
|
+
this.ensureInitialized();
|
|
11192
|
+
if (!this.cdpSession) {
|
|
11193
|
+
this.cdpSession = await this.page.context().newCDPSession(this.page);
|
|
11194
|
+
await this.cdpSession.send("Accessibility.enable");
|
|
11195
|
+
}
|
|
11196
|
+
const response = await this.cdpSession.send(
|
|
11197
|
+
"Accessibility.getFullAXTree"
|
|
11198
|
+
);
|
|
11199
|
+
return response.nodes;
|
|
11200
|
+
}
|
|
11201
|
+
formatAXTree(nodes) {
|
|
11202
|
+
const lines = [];
|
|
11203
|
+
let count = 0;
|
|
11204
|
+
for (const node of nodes) {
|
|
11205
|
+
if (count >= this.cfg.maxSnapshotNodes) {
|
|
11206
|
+
lines.push(`[truncated at ${this.cfg.maxSnapshotNodes} nodes]`);
|
|
11207
|
+
break;
|
|
11208
|
+
}
|
|
11209
|
+
if (node.ignored) {
|
|
11210
|
+
continue;
|
|
11211
|
+
}
|
|
11212
|
+
const role = node.role?.value || "unknown";
|
|
11213
|
+
const name = node.name?.value || "";
|
|
11214
|
+
if (!name && ["generic", "none", "text"].includes(role)) {
|
|
11215
|
+
continue;
|
|
11216
|
+
}
|
|
11217
|
+
const states = [];
|
|
11218
|
+
if (node.properties) {
|
|
11219
|
+
for (const prop of node.properties) {
|
|
11220
|
+
if (prop.value?.value === true) {
|
|
11221
|
+
const stateName = prop.name;
|
|
11222
|
+
if (["focused", "checked", "disabled", "expanded", "selected"].includes(
|
|
11223
|
+
stateName
|
|
11224
|
+
)) {
|
|
11225
|
+
states.push(stateName);
|
|
11226
|
+
}
|
|
11227
|
+
}
|
|
11228
|
+
}
|
|
11229
|
+
}
|
|
11230
|
+
const stateStr = states.length ? " " + states.join(" ") : "";
|
|
11231
|
+
const nameStr = name ? ` "${name}"` : "";
|
|
11232
|
+
lines.push(`[${role}${nameStr}${stateStr}]`);
|
|
11233
|
+
count++;
|
|
11234
|
+
}
|
|
11235
|
+
return lines.join("\n");
|
|
11236
|
+
}
|
|
11237
|
+
// ========================================================================
|
|
11238
|
+
// Private: Tool Handlers
|
|
11239
|
+
// ========================================================================
|
|
11240
|
+
async navigate(_args, call) {
|
|
11241
|
+
const args = this.parseArgs(call, ["url"]);
|
|
11242
|
+
this.validateUrl(args.url, call);
|
|
11243
|
+
this.ensureInitialized();
|
|
11244
|
+
const waitUntil = args.waitUntil ?? "domcontentloaded";
|
|
11245
|
+
await this.page.goto(args.url, {
|
|
11246
|
+
timeout: this.cfg.navigationTimeoutMs,
|
|
11247
|
+
waitUntil
|
|
11248
|
+
});
|
|
11249
|
+
this.ensureCurrentUrlAllowed();
|
|
11250
|
+
const tree = await this.getAccessibilityTree();
|
|
11251
|
+
return this.formatAXTree(tree);
|
|
11252
|
+
}
|
|
11253
|
+
async click(_args, call) {
|
|
11254
|
+
const args = this.parseArgs(call, ["name"]);
|
|
11255
|
+
this.ensureInitialized();
|
|
11256
|
+
this.ensureCurrentUrlAllowed();
|
|
11257
|
+
let locator;
|
|
11258
|
+
if (args.role) {
|
|
11259
|
+
locator = this.page.getByRole(args.role, {
|
|
11260
|
+
name: args.name
|
|
11261
|
+
});
|
|
11262
|
+
} else {
|
|
11263
|
+
locator = this.page.getByRole("button", { name: args.name }).or(this.page.getByRole("link", { name: args.name })).or(this.page.getByRole("menuitem", { name: args.name }));
|
|
11264
|
+
}
|
|
11265
|
+
await locator.click({ timeout: this.cfg.actionTimeoutMs });
|
|
11266
|
+
const tree = await this.getAccessibilityTree();
|
|
11267
|
+
return this.formatAXTree(tree);
|
|
11268
|
+
}
|
|
11269
|
+
async type(_args, call) {
|
|
11270
|
+
const args = this.parseArgs(call, ["name", "text"]);
|
|
11271
|
+
this.ensureInitialized();
|
|
11272
|
+
this.ensureCurrentUrlAllowed();
|
|
11273
|
+
const role = args.role ?? "textbox";
|
|
11274
|
+
const locator = this.page.getByRole(role, { name: args.name });
|
|
11275
|
+
await locator.fill(args.text, { timeout: this.cfg.actionTimeoutMs });
|
|
11276
|
+
return `Typed "${args.text}" into ${role} "${args.name}"`;
|
|
11277
|
+
}
|
|
11278
|
+
async snapshot(_args, _call) {
|
|
11279
|
+
this.ensureInitialized();
|
|
11280
|
+
this.ensureCurrentUrlAllowed();
|
|
11281
|
+
const tree = await this.getAccessibilityTree();
|
|
11282
|
+
return this.formatAXTree(tree);
|
|
11283
|
+
}
|
|
11284
|
+
async scroll(_args, call) {
|
|
11285
|
+
const args = this.parseArgs(call, ["direction"]);
|
|
11286
|
+
this.ensureInitialized();
|
|
11287
|
+
this.ensureCurrentUrlAllowed();
|
|
11288
|
+
const amount = args.amount ?? "page";
|
|
11289
|
+
if (amount === "toTop") {
|
|
11290
|
+
await this.page.evaluate(() => window.scrollTo(0, 0));
|
|
11291
|
+
} else if (amount === "toBottom") {
|
|
11292
|
+
await this.page.evaluate(
|
|
11293
|
+
() => window.scrollTo(0, document.body.scrollHeight)
|
|
11294
|
+
);
|
|
11295
|
+
} else {
|
|
11296
|
+
const viewport = this.page.viewportSize();
|
|
11297
|
+
const height = viewport?.height ?? 800;
|
|
11298
|
+
const scrollAmount = amount === "half" ? height / 2 : height;
|
|
11299
|
+
const delta = args.direction === "down" ? scrollAmount : -scrollAmount;
|
|
11300
|
+
await this.page.evaluate((d) => window.scrollBy(0, d), delta);
|
|
11301
|
+
}
|
|
11302
|
+
const tree = await this.getAccessibilityTree();
|
|
11303
|
+
return this.formatAXTree(tree);
|
|
11304
|
+
}
|
|
11305
|
+
async screenshot(_args, call) {
|
|
11306
|
+
const args = this.parseArgs(call, []);
|
|
11307
|
+
this.ensureInitialized();
|
|
11308
|
+
this.ensureCurrentUrlAllowed();
|
|
11309
|
+
const buffer = await this.page.screenshot({
|
|
11310
|
+
fullPage: args.fullPage ?? false,
|
|
11311
|
+
type: "png"
|
|
11312
|
+
});
|
|
11313
|
+
if (buffer.length > BrowserDefaults.MAX_SCREENSHOT_BYTES) {
|
|
11314
|
+
throw new Error(
|
|
11315
|
+
`Screenshot size (${buffer.length} bytes) exceeds maximum allowed (${BrowserDefaults.MAX_SCREENSHOT_BYTES} bytes). Try capturing viewport only.`
|
|
11316
|
+
);
|
|
11317
|
+
}
|
|
11318
|
+
const base64 = buffer.toString("base64");
|
|
11319
|
+
return `data:image/png;base64,${base64}`;
|
|
11320
|
+
}
|
|
11321
|
+
async extract(_args, call) {
|
|
11322
|
+
const args = this.parseArgs(call, ["selector"]);
|
|
11323
|
+
this.ensureInitialized();
|
|
11324
|
+
this.ensureCurrentUrlAllowed();
|
|
11325
|
+
const attribute = args.attribute ?? "textContent";
|
|
11326
|
+
const multiple = args.multiple ?? false;
|
|
11327
|
+
if (multiple) {
|
|
11328
|
+
const elements = this.page.locator(args.selector);
|
|
11329
|
+
const count = await elements.count();
|
|
11330
|
+
const results = [];
|
|
11331
|
+
for (let i = 0; i < count; i++) {
|
|
11332
|
+
const el = elements.nth(i);
|
|
11333
|
+
let value;
|
|
11334
|
+
if (attribute === "textContent") {
|
|
11335
|
+
value = await el.textContent();
|
|
11336
|
+
} else {
|
|
11337
|
+
value = await el.getAttribute(attribute);
|
|
11338
|
+
}
|
|
11339
|
+
if (value !== null) {
|
|
11340
|
+
results.push(value.trim());
|
|
11341
|
+
}
|
|
11342
|
+
}
|
|
11343
|
+
return JSON.stringify(results);
|
|
11344
|
+
} else {
|
|
11345
|
+
const el = this.page.locator(args.selector).first();
|
|
11346
|
+
let value;
|
|
11347
|
+
if (attribute === "textContent") {
|
|
11348
|
+
value = await el.textContent();
|
|
11349
|
+
} else {
|
|
11350
|
+
value = await el.getAttribute(attribute);
|
|
11351
|
+
}
|
|
11352
|
+
return value?.trim() ?? "";
|
|
11353
|
+
}
|
|
11354
|
+
}
|
|
11355
|
+
};
|
|
11356
|
+
function createBrowserToolPack(options = {}) {
|
|
11357
|
+
return new BrowserToolPack(options);
|
|
11358
|
+
}
|
|
11359
|
+
function createBrowserTools(options = {}) {
|
|
11360
|
+
const pack = new BrowserToolPack(options);
|
|
11361
|
+
return { pack, registry: pack.toRegistry() };
|
|
11362
|
+
}
|
|
11363
|
+
|
|
11364
|
+
// src/tools_runner.ts
|
|
11365
|
+
var ToolRunner = class {
|
|
11366
|
+
constructor(options) {
|
|
11367
|
+
this.registry = options.registry;
|
|
11368
|
+
this.runsClient = options.runsClient;
|
|
11369
|
+
this.customerId = options.customerId;
|
|
11370
|
+
this.onBeforeExecute = options.onBeforeExecute;
|
|
11371
|
+
this.onAfterExecute = options.onAfterExecute;
|
|
11372
|
+
this.onSubmitted = options.onSubmitted;
|
|
11373
|
+
this.onError = options.onError;
|
|
11374
|
+
}
|
|
11375
|
+
/**
|
|
11376
|
+
* Handles a node_waiting event by executing tools and submitting results.
|
|
11377
|
+
*
|
|
11378
|
+
* @param runId - The run ID
|
|
11379
|
+
* @param nodeId - The node ID that is waiting
|
|
11380
|
+
* @param waiting - The waiting state with pending tool calls
|
|
11381
|
+
* @returns The submission response with accepted count and new status
|
|
11382
|
+
*
|
|
11383
|
+
* @example
|
|
11384
|
+
* ```typescript
|
|
11385
|
+
* for await (const event of client.runs.events(runId)) {
|
|
11386
|
+
* if (event.type === "node_waiting") {
|
|
11387
|
+
* const result = await runner.handleNodeWaiting(
|
|
11388
|
+
* runId,
|
|
11389
|
+
* event.node_id,
|
|
11390
|
+
* event.waiting
|
|
11391
|
+
* );
|
|
11392
|
+
* console.log(`Submitted ${result.accepted} results, status: ${result.status}`);
|
|
11393
|
+
* }
|
|
11394
|
+
* }
|
|
11395
|
+
* ```
|
|
11396
|
+
*/
|
|
11397
|
+
async handleNodeWaiting(runId, nodeId, waiting) {
|
|
11398
|
+
const results = [];
|
|
11399
|
+
for (const pending of waiting.pending_tool_calls) {
|
|
11400
|
+
try {
|
|
11401
|
+
await this.onBeforeExecute?.(pending);
|
|
11402
|
+
const toolCall = createToolCall(
|
|
11403
|
+
pending.tool_call_id,
|
|
11404
|
+
pending.name,
|
|
11405
|
+
pending.arguments
|
|
11406
|
+
);
|
|
11407
|
+
const result = await this.registry.execute(toolCall);
|
|
11408
|
+
results.push(result);
|
|
11409
|
+
await this.onAfterExecute?.(result);
|
|
11410
|
+
} catch (err) {
|
|
11411
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
11412
|
+
await this.onError?.(error, pending);
|
|
11413
|
+
results.push({
|
|
11414
|
+
toolCallId: pending.tool_call_id,
|
|
11415
|
+
toolName: pending.name,
|
|
11416
|
+
result: null,
|
|
11417
|
+
error: error.message
|
|
11418
|
+
});
|
|
11419
|
+
}
|
|
11420
|
+
}
|
|
11421
|
+
const response = await this.runsClient.submitToolResults(
|
|
11422
|
+
runId,
|
|
11423
|
+
{
|
|
11424
|
+
node_id: nodeId,
|
|
11425
|
+
step: waiting.step,
|
|
11426
|
+
request_id: waiting.request_id,
|
|
11427
|
+
results: results.map((r) => ({
|
|
11428
|
+
tool_call_id: r.toolCallId,
|
|
11429
|
+
name: r.toolName,
|
|
11430
|
+
output: r.error ? `Error: ${r.error}` : typeof r.result === "string" ? r.result : JSON.stringify(r.result)
|
|
11431
|
+
}))
|
|
9090
11432
|
},
|
|
9091
|
-
|
|
9092
|
-
|
|
11433
|
+
{ customerId: this.customerId }
|
|
11434
|
+
);
|
|
11435
|
+
await this.onSubmitted?.(runId, response.accepted, response.status);
|
|
11436
|
+
return {
|
|
11437
|
+
accepted: response.accepted,
|
|
11438
|
+
status: response.status,
|
|
11439
|
+
results
|
|
11440
|
+
};
|
|
11441
|
+
}
|
|
11442
|
+
/**
|
|
11443
|
+
* Processes a stream of run events, automatically handling node_waiting events.
|
|
11444
|
+
*
|
|
11445
|
+
* This is the main entry point for running a workflow with client-side tools.
|
|
11446
|
+
* It yields all events through (including node_waiting after handling).
|
|
11447
|
+
*
|
|
11448
|
+
* @param runId - The run ID to process
|
|
11449
|
+
* @param events - AsyncIterable of run events (from RunsClient.events())
|
|
11450
|
+
* @yields All run events, with node_waiting events handled automatically
|
|
11451
|
+
*
|
|
11452
|
+
* @example
|
|
11453
|
+
* ```typescript
|
|
11454
|
+
* const run = await client.runs.create(workflowSpec);
|
|
11455
|
+
* const eventStream = client.runs.events(run.run_id);
|
|
11456
|
+
*
|
|
11457
|
+
* for await (const event of runner.processEvents(run.run_id, eventStream)) {
|
|
11458
|
+
* switch (event.type) {
|
|
11459
|
+
* case "node_started":
|
|
11460
|
+
* console.log(`Node ${event.node_id} started`);
|
|
11461
|
+
* break;
|
|
11462
|
+
* case "node_succeeded":
|
|
11463
|
+
* console.log(`Node ${event.node_id} succeeded`);
|
|
11464
|
+
* break;
|
|
11465
|
+
* case "run_succeeded":
|
|
11466
|
+
* console.log("Run completed!");
|
|
11467
|
+
* break;
|
|
11468
|
+
* }
|
|
11469
|
+
* }
|
|
11470
|
+
* ```
|
|
11471
|
+
*/
|
|
11472
|
+
async *processEvents(runId, events) {
|
|
11473
|
+
for await (const event of events) {
|
|
11474
|
+
if (event.type === "node_waiting") {
|
|
11475
|
+
const waitingEvent = event;
|
|
11476
|
+
try {
|
|
11477
|
+
await this.handleNodeWaiting(
|
|
11478
|
+
runId,
|
|
11479
|
+
waitingEvent.node_id,
|
|
11480
|
+
waitingEvent.waiting
|
|
11481
|
+
);
|
|
11482
|
+
} catch (err) {
|
|
11483
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
11484
|
+
await this.onError?.(error);
|
|
11485
|
+
throw error;
|
|
11486
|
+
}
|
|
11487
|
+
}
|
|
11488
|
+
yield event;
|
|
9093
11489
|
}
|
|
9094
|
-
}
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
11490
|
+
}
|
|
11491
|
+
/**
|
|
11492
|
+
* Checks if a run event is a node_waiting event.
|
|
11493
|
+
* Utility for filtering events when not using processEvents().
|
|
11494
|
+
*/
|
|
11495
|
+
static isNodeWaiting(event) {
|
|
11496
|
+
return event.type === "node_waiting";
|
|
11497
|
+
}
|
|
11498
|
+
/**
|
|
11499
|
+
* Checks if a run status is terminal (succeeded, failed, or canceled).
|
|
11500
|
+
* Utility for determining when to stop polling.
|
|
11501
|
+
*/
|
|
11502
|
+
static isTerminalStatus(status) {
|
|
11503
|
+
return status === "succeeded" || status === "failed" || status === "canceled";
|
|
11504
|
+
}
|
|
9102
11505
|
};
|
|
11506
|
+
function createToolRunner(options) {
|
|
11507
|
+
return new ToolRunner(options);
|
|
11508
|
+
}
|
|
9103
11509
|
|
|
9104
11510
|
// src/generated/index.ts
|
|
9105
11511
|
var generated_exports = {};
|
|
@@ -9156,9 +11562,11 @@ var ModelRelay = class _ModelRelay {
|
|
|
9156
11562
|
metrics: cfg.metrics,
|
|
9157
11563
|
trace: cfg.trace
|
|
9158
11564
|
});
|
|
11565
|
+
this.images = new ImagesClient(http, auth);
|
|
9159
11566
|
this.customers = new CustomersClient(http, { apiKey, accessToken, tokenProvider });
|
|
9160
11567
|
this.tiers = new TiersClient(http, { apiKey });
|
|
9161
11568
|
this.models = new ModelsClient(http);
|
|
11569
|
+
this.sessions = new SessionsClient(this, http, auth);
|
|
9162
11570
|
}
|
|
9163
11571
|
forCustomer(customerId) {
|
|
9164
11572
|
return new CustomerScopedModelRelay(this.responses, customerId, this.baseUrl);
|
|
@@ -9171,6 +11579,10 @@ function resolveBaseUrl(override) {
|
|
|
9171
11579
|
export {
|
|
9172
11580
|
APIError,
|
|
9173
11581
|
AuthClient,
|
|
11582
|
+
BillingProviders,
|
|
11583
|
+
BrowserDefaults,
|
|
11584
|
+
BrowserToolNames,
|
|
11585
|
+
BrowserToolPack,
|
|
9174
11586
|
ConfigError,
|
|
9175
11587
|
ContentPartTypes,
|
|
9176
11588
|
CustomerResponsesClient,
|
|
@@ -9180,10 +11592,17 @@ export {
|
|
|
9180
11592
|
DEFAULT_BASE_URL,
|
|
9181
11593
|
DEFAULT_CLIENT_HEADER,
|
|
9182
11594
|
DEFAULT_CONNECT_TIMEOUT_MS,
|
|
11595
|
+
DEFAULT_IGNORE_DIRS,
|
|
9183
11596
|
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
9184
11597
|
ErrorCodes,
|
|
11598
|
+
FSDefaults,
|
|
11599
|
+
ToolNames as FSToolNames,
|
|
9185
11600
|
FrontendTokenProvider,
|
|
11601
|
+
ImagesClient,
|
|
9186
11602
|
InputItemTypes,
|
|
11603
|
+
LocalFSToolPack,
|
|
11604
|
+
LocalSession,
|
|
11605
|
+
MemorySessionStore,
|
|
9187
11606
|
MessageRoles,
|
|
9188
11607
|
ModelRelay,
|
|
9189
11608
|
ModelRelayError,
|
|
@@ -9191,22 +11610,27 @@ export {
|
|
|
9191
11610
|
OIDCExchangeTokenProvider,
|
|
9192
11611
|
OutputFormatTypes,
|
|
9193
11612
|
OutputItemTypes,
|
|
11613
|
+
PathEscapeError,
|
|
9194
11614
|
ResponsesClient,
|
|
9195
11615
|
ResponsesStream,
|
|
9196
11616
|
RunsClient,
|
|
9197
11617
|
RunsEventStream,
|
|
9198
11618
|
SDK_VERSION,
|
|
11619
|
+
SessionsClient,
|
|
9199
11620
|
StopReasons,
|
|
9200
11621
|
StreamProtocolError,
|
|
9201
11622
|
StreamTimeoutError,
|
|
9202
11623
|
StructuredDecodeError,
|
|
9203
11624
|
StructuredExhaustedError,
|
|
9204
11625
|
StructuredJSONStream,
|
|
11626
|
+
SubscriptionStatuses,
|
|
9205
11627
|
TiersClient,
|
|
9206
11628
|
ToolArgsError,
|
|
11629
|
+
ToolArgumentError,
|
|
9207
11630
|
ToolCallAccumulator,
|
|
9208
11631
|
ToolChoiceTypes,
|
|
9209
11632
|
ToolRegistry,
|
|
11633
|
+
ToolRunner,
|
|
9210
11634
|
ToolTypes,
|
|
9211
11635
|
TransportError,
|
|
9212
11636
|
WORKFLOWS_COMPILE_PATH,
|
|
@@ -9218,17 +11642,25 @@ export {
|
|
|
9218
11642
|
WorkflowsClient,
|
|
9219
11643
|
asModelId,
|
|
9220
11644
|
asProviderId,
|
|
11645
|
+
asSessionId,
|
|
9221
11646
|
asTierCode,
|
|
9222
11647
|
assistantMessageWithToolCalls,
|
|
9223
11648
|
createAccessTokenAuth,
|
|
9224
11649
|
createApiKeyAuth,
|
|
9225
11650
|
createAssistantMessage,
|
|
11651
|
+
createBrowserToolPack,
|
|
11652
|
+
createBrowserTools,
|
|
9226
11653
|
createFunctionCall,
|
|
9227
11654
|
createFunctionTool,
|
|
9228
11655
|
createFunctionToolFromSchema,
|
|
11656
|
+
createLocalFSToolPack,
|
|
11657
|
+
createLocalFSTools,
|
|
11658
|
+
createLocalSession,
|
|
11659
|
+
createMemorySessionStore,
|
|
9229
11660
|
createRetryMessages,
|
|
9230
11661
|
createSystemMessage,
|
|
9231
11662
|
createToolCall,
|
|
11663
|
+
createToolRunner,
|
|
9232
11664
|
createUsage,
|
|
9233
11665
|
createUserMessage,
|
|
9234
11666
|
createWebTool,
|
|
@@ -9236,6 +11668,7 @@ export {
|
|
|
9236
11668
|
executeWithRetry,
|
|
9237
11669
|
firstToolCall,
|
|
9238
11670
|
formatToolErrorForModel,
|
|
11671
|
+
generateSessionId,
|
|
9239
11672
|
generated_exports as generated,
|
|
9240
11673
|
getRetryableErrors,
|
|
9241
11674
|
hasRetryableErrors,
|