@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/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 path = typeof obj.path === "string" ? obj.path : "";
230
+ const path2 = typeof obj.path === "string" ? obj.path : "";
209
231
  const message = typeof obj.message === "string" ? obj.message : "";
210
- if (!code || !path || !message) continue;
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 <= 0) {
462
- throw new ConfigError("ttlSeconds must be positive when provided");
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 path = queryString ? `/auth/device/start?${queryString}` : "/auth/device/start";
568
- const apiResp = await this.http.json(path, {
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: apiResp.token_type,
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.2",
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: { access: "public" },
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 path = e.path.length > 0 ? `${e.path.join(".")}: ` : "";
1282
- return `${path}${e.message}`;
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 = [...path, ...issueData.path || []];
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, path, key) {
3578
+ constructor(parent, value, path2, key) {
3524
3579
  this._cachedPath = [];
3525
3580
  this.parent = parent;
3526
3581
  this.data = value;
3527
- this._path = 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 path = runByIdPath(runId);
7422
- const out = await this.http.json(path, {
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: options.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 path = params.toString() ? `${basePath}?${params}` : basePath;
7521
+ const path2 = params.toString() ? `${basePath}?${params}` : basePath;
7458
7522
  const headers = { ...options.headers || {} };
7459
- const resp = await this.http.request(path, {
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 path = runToolResultsPath(runId);
7511
- const out = await this.http.json(path, {
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: options.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 path = runPendingToolsPath(runId);
7532
- const out = await this.http.json(path, {
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: options.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: options.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 an end-user identity (provider + subject) to a customer found by email.
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
- const response = await this.http.json("/customers/claim", {
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 createCheckoutSession(customerId, request) {
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}/checkout`,
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 status for a customer.
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
- return await this.http.json(
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 path = qs.toString() ? `/models?${qs.toString()}` : "/models";
7973
- const resp = await this.http.json(path, { method: "GET" });
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/http.ts
7979
- var HTTPClient = class {
7980
- constructor(cfg) {
7981
- const resolvedBase = normalizeBaseUrl(cfg.baseUrl || DEFAULT_BASE_URL);
7982
- if (!isValidHttpUrl(resolvedBase)) {
7983
- throw new ConfigError(
7984
- "baseUrl must start with http:// or https://"
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
- this.baseUrl = resolvedBase;
7988
- this.apiKey = cfg.apiKey ? parseApiKey(cfg.apiKey) : void 0;
7989
- this.accessToken = cfg.accessToken?.trim();
7990
- this.fetchImpl = cfg.fetchImpl;
7991
- this.clientHeader = cfg.clientHeader?.trim() || DEFAULT_CLIENT_HEADER;
7992
- this.defaultConnectTimeoutMs = cfg.connectTimeoutMs === void 0 ? DEFAULT_CONNECT_TIMEOUT_MS : Math.max(0, cfg.connectTimeoutMs);
7993
- this.defaultTimeoutMs = cfg.timeoutMs === void 0 ? DEFAULT_REQUEST_TIMEOUT_MS : Math.max(0, cfg.timeoutMs);
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
- async request(path, options = {}) {
8000
- const fetchFn = this.fetchImpl ?? globalThis.fetch;
8001
- if (!fetchFn) {
8002
- throw new ConfigError(
8003
- "fetch is not available; provide a fetch implementation"
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
- const method = options.method || "GET";
8007
- const url = buildUrl(this.baseUrl, path);
8008
- const metrics = mergeMetrics(this.metrics, options.metrics);
8009
- const trace = mergeTrace(this.trace, options.trace);
8010
- const context = {
8011
- method,
8012
- path,
8013
- ...options.context || {}
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
- trace?.requestStart?.(context);
8016
- const start = metrics?.httpRequest || trace?.requestFinish ? Date.now() : 0;
8017
- const headers = new Headers({
8018
- ...this.defaultHeaders,
8019
- ...options.headers || {}
8020
- });
8021
- const accepts = options.accept || (options.raw ? void 0 : "application/json");
8022
- if (accepts && !headers.has("Accept")) {
8023
- headers.set("Accept", accepts);
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
- const body = options.body;
8026
- const shouldEncodeJSON = body !== void 0 && body !== null && typeof body === "object" && !(body instanceof FormData) && !(body instanceof Blob);
8027
- const payload = shouldEncodeJSON ? JSON.stringify(body) : body;
8028
- if (shouldEncodeJSON && !headers.has("Content-Type")) {
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
- const accessToken = options.accessToken ?? this.accessToken;
8032
- if (accessToken) {
8033
- const bearer = accessToken.toLowerCase().startsWith("bearer ") ? accessToken : `Bearer ${accessToken}`;
8034
- headers.set("Authorization", bearer);
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 apiKey = options.apiKey ?? this.apiKey;
8037
- if (apiKey) {
8038
- headers.set("X-ModelRelay-Api-Key", apiKey);
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
- if (this.clientHeader && !headers.has("X-ModelRelay-Client")) {
8041
- headers.set("X-ModelRelay-Client", this.clientHeader);
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 timeoutMs = options.useDefaultTimeout === false ? options.timeoutMs : options.timeoutMs ?? this.defaultTimeoutMs;
8044
- const connectTimeoutMs = options.useDefaultConnectTimeout === false ? options.connectTimeoutMs : options.connectTimeoutMs ?? this.defaultConnectTimeoutMs;
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 response = await fetchFn(url, {
8075
- method,
8076
- headers,
8077
- body: payload,
8078
- signal
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
- return response;
8421
+ results.push(result);
8108
8422
  } catch (err) {
8109
- if (options.signal?.aborted) {
8110
- throw err;
8111
- }
8112
- if (err instanceof ModelRelayError) {
8113
- recordHttpMetrics(metrics, trace, start, void 0, {
8114
- error: err,
8115
- context
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
- throw lastError instanceof Error ? lastError : new TransportError("request failed", {
8149
- kind: "other",
8150
- retries: buildRetryMetadata(attempts, lastStatus)
8151
- });
8152
- }
8153
- async json(path, options = {}) {
8154
- const response = await this.request(path, {
8155
- ...options,
8156
- raw: true,
8157
- accept: options.accept || "application/json"
8158
- });
8159
- if (!response.ok) {
8160
- throw await parseErrorResponse(response);
8161
- }
8162
- if (response.status === 204) {
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 buildUrl(baseUrl, path) {
8176
- if (/^https?:\/\//i.test(path)) {
8177
- return path;
8178
- }
8179
- if (!path.startsWith("/")) {
8180
- path = `/${path}`;
8181
- }
8182
- return `${baseUrl}${path}`;
8183
- }
8184
- function normalizeBaseUrl(value) {
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 normalizeRetryConfig(retry) {
8195
- if (retry === false) return void 0;
8196
- const cfg = retry || {};
8197
- return {
8198
- maxAttempts: Math.max(1, cfg.maxAttempts ?? 3),
8199
- baseBackoffMs: Math.max(0, cfg.baseBackoffMs ?? 300),
8200
- maxBackoffMs: Math.max(0, cfg.maxBackoffMs ?? 5e3),
8201
- retryPost: cfg.retryPost ?? true
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
- return false;
8212
- }
8213
- function isRetryableError(err, kind) {
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 controller.signal;
8476
+ return Array.from(merged.values());
8244
8477
  }
8245
- function normalizeHeaders(headers) {
8246
- if (!headers) return {};
8247
- const normalized = {};
8248
- for (const [key, value] of Object.entries(headers)) {
8249
- if (!key || !value) continue;
8250
- const k = key.trim();
8251
- const v = value.trim();
8252
- if (k && v) {
8253
- normalized[k] = v;
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 normalized;
8498
+ return void 0;
8257
8499
  }
8258
- function buildRetryMetadata(attempt, lastStatus, lastError) {
8259
- if (!attempt || attempt <= 1) return void 0;
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
- function classifyTransportErrorKind(err, connectTimedOut, requestTimedOut) {
8267
- if (connectTimedOut) return "connect";
8268
- if (requestTimedOut) return "timeout";
8269
- if (err instanceof DOMException && err.name === "AbortError") {
8270
- return requestTimedOut ? "timeout" : "request";
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
- if (err instanceof TypeError) return "request";
8273
- return "other";
8274
- }
8275
- function toTransportError(err, kind, retries) {
8276
- const message = err instanceof Error ? err.message : typeof err === "string" ? err : "request failed";
8277
- return new TransportError(message, { kind, retries, cause: err });
8278
- }
8279
- function recordHttpMetrics(metrics, trace, start, retries, info) {
8280
- if (!metrics?.httpRequest && !trace?.requestFinish) return;
8281
- const latencyMs = start ? Date.now() - start : 0;
8282
- if (metrics?.httpRequest) {
8283
- metrics.httpRequest({
8284
- latencyMs,
8285
- status: info.status,
8286
- error: info.error ? String(info.error) : void 0,
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
- trace?.requestFinish?.({
8292
- context: info.context,
8293
- status: info.status,
8294
- error: info.error,
8295
- retries,
8296
- latencyMs
8297
- });
8298
- }
8299
- function withRequestId(context, headers) {
8300
- const requestId = headers.get("X-ModelRelay-Request-Id") || headers.get("X-Request-Id") || context.requestId;
8301
- if (!requestId) return context;
8302
- return { ...context, requestId };
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
- return { ...options, customerId };
8321
- }
8322
- var CustomerResponsesClient = class {
8323
- constructor(base, customerId) {
8324
- this.customerId = normalizeCustomerId(customerId);
8325
- this.base = base;
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
- get id() {
8328
- return this.customerId;
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
- new() {
8331
- return this.base.new().customerId(this.customerId);
8620
+ // ============================================================================
8621
+ // Session Interface Implementation
8622
+ // ============================================================================
8623
+ /**
8624
+ * Full conversation history (read-only).
8625
+ */
8626
+ get history() {
8627
+ return this.messages;
8332
8628
  }
8333
- ensureRequestCustomer(request) {
8334
- const req = asInternal(request);
8335
- const reqCustomer = req.options.customerId;
8336
- if (reqCustomer && reqCustomer !== this.customerId) {
8337
- throw new ConfigError("customerId mismatch", {
8338
- expected: this.customerId,
8339
- received: reqCustomer
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
- async create(request, options = {}) {
8344
- this.ensureRequestCustomer(request);
8345
- return this.base.create(request, mergeCustomerOptions(this.customerId, options));
8346
- }
8347
- async stream(request, options = {}) {
8348
- this.ensureRequestCustomer(request);
8349
- return this.base.stream(request, mergeCustomerOptions(this.customerId, options));
8350
- }
8351
- async streamJSON(request, options = {}) {
8352
- this.ensureRequestCustomer(request);
8353
- return this.base.streamJSON(
8354
- request,
8355
- mergeCustomerOptions(this.customerId, options)
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
- async text(system, user, options = {}) {
8359
- return this.base.textForCustomer(
8360
- this.customerId,
8361
- system,
8362
- user,
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
- async streamTextDeltas(system, user, options = {}) {
8367
- return this.base.streamTextDeltasForCustomer(
8368
- this.customerId,
8369
- system,
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
- var CustomerScopedModelRelay = class {
8376
- constructor(responses, customerId, baseUrl) {
8377
- const normalized = normalizeCustomerId(customerId);
8378
- this.responses = new CustomerResponsesClient(responses, normalized);
8379
- this.customerId = normalized;
8380
- this.baseUrl = baseUrl;
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
- // src/token_providers.ts
8385
- function isReusable(token) {
8386
- if (!token.token) {
8387
- return false;
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
- return token.expiresAt.getTime() - Date.now() > 6e4;
8390
- }
8391
- var FrontendTokenProvider = class {
8392
- constructor(cfg) {
8393
- const publishableKey = parsePublishableKey(cfg.publishableKey);
8394
- const http = new HTTPClient({
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
- this.publishableKey = publishableKey;
8401
- this.customer = cfg.customer;
8402
- this.auth = new AuthClient(http, { apiKey: publishableKey, customer: cfg.customer });
8403
- }
8404
- async getToken() {
8405
- if (!this.customer?.provider || !this.customer?.subject) {
8406
- throw new ConfigError("customer.provider and customer.subject are required");
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 reqBase = {
8409
- publishableKey: this.publishableKey,
8410
- identityProvider: this.customer.provider,
8411
- identitySubject: this.customer.subject,
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 (!token.token) {
8427
- throw new ConfigError("frontend token exchange returned an empty token");
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 token.token;
8430
- }
8431
- };
8432
- var CustomerTokenProvider = class {
8433
- constructor(cfg) {
8434
- const key = parseSecretKey(cfg.secretKey);
8435
- const http = new HTTPClient({
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 getToken() {
8445
- if (this.cached && isReusable(this.cached)) {
8446
- return this.cached.token;
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 token = await this.auth.customerToken(this.req);
8449
- this.cached = token;
8450
- return token.token;
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
- var OIDCExchangeTokenProvider = class {
8454
- constructor(cfg) {
8455
- const apiKey = parseApiKey(cfg.apiKey);
8456
- const http = new HTTPClient({
8457
- baseUrl: cfg.baseUrl || DEFAULT_BASE_URL,
8458
- fetchImpl: cfg.fetch,
8459
- clientHeader: cfg.clientHeader || DEFAULT_CLIENT_HEADER,
8460
- apiKey
8461
- });
8462
- this.auth = new AuthClient(http, { apiKey });
8463
- this.idTokenProvider = cfg.idTokenProvider;
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
- const idToken = (await this.idTokenProvider())?.trim();
8471
- if (!idToken) {
8472
- throw new ConfigError("idTokenProvider returned an empty id_token");
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/device_flow.ts
8481
- async function startOAuthDeviceAuthorization(req) {
8482
- const deviceAuthorizationEndpoint = req.deviceAuthorizationEndpoint?.trim();
8483
- if (!deviceAuthorizationEndpoint) {
8484
- throw new ConfigError("deviceAuthorizationEndpoint is required");
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
- const clientId = req.clientId?.trim();
8487
- if (!clientId) {
8488
- throw new ConfigError("clientId is required");
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
- const form = new URLSearchParams();
8491
- form.set("client_id", clientId);
8492
- if (req.scope?.trim()) {
8493
- form.set("scope", req.scope.trim());
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
- if (req.audience?.trim()) {
8496
- form.set("audience", req.audience.trim());
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
- const payload = await postOAuthForm(deviceAuthorizationEndpoint, form, {
8499
- fetch: req.fetch,
8500
- signal: req.signal
8501
- });
8502
- const deviceCode = String(payload.device_code || "").trim();
8503
- const userCode = String(payload.user_code || "").trim();
8504
- const verificationUri = String(payload.verification_uri || payload.verification_uri_complete || "").trim();
8505
- const verificationUriComplete = String(payload.verification_uri_complete || "").trim() || void 0;
8506
- const expiresIn = Number(payload.expires_in || 0);
8507
- const intervalSeconds = Math.max(1, Number(payload.interval || 5));
8508
- if (!deviceCode || !userCode || !verificationUri || !expiresIn) {
8509
- throw new TransportError("oauth device authorization returned an invalid response", {
8510
- kind: "request",
8511
- cause: payload
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
- return {
8515
- deviceCode,
8516
- userCode,
8517
- verificationUri,
8518
- verificationUriComplete,
8519
- expiresAt: new Date(Date.now() + expiresIn * 1e3),
8520
- intervalSeconds
8521
- };
8522
- }
8523
- async function pollOAuthDeviceToken(req) {
8524
- const tokenEndpoint = req.tokenEndpoint?.trim();
8525
- if (!tokenEndpoint) {
8526
- throw new ConfigError("tokenEndpoint is required");
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
- const deviceCode = req.deviceCode?.trim();
8533
- if (!deviceCode) {
8534
- throw new ConfigError("deviceCode is required");
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
- const deadline = req.deadline ?? new Date(Date.now() + 10 * 60 * 1e3);
8537
- let intervalMs = Math.max(1, req.intervalSeconds ?? 5) * 1e3;
8538
- while (true) {
8539
- if (Date.now() >= deadline.getTime()) {
8540
- throw new TransportError("oauth device flow timed out", { kind: "timeout" });
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 form = new URLSearchParams();
8543
- form.set("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
8544
- form.set("device_code", deviceCode);
8545
- form.set("client_id", clientId);
8546
- const payload = await postOAuthForm(tokenEndpoint, form, {
8547
- fetch: req.fetch,
8548
- signal: req.signal,
8549
- allowErrorPayload: true
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 err = String(payload.error || "").trim();
8552
- if (err) {
8553
- switch (err) {
8554
- case "authorization_pending":
8555
- await sleep(intervalMs, req.signal);
8556
- continue;
8557
- case "slow_down":
8558
- intervalMs += 5e3;
8559
- await sleep(intervalMs, req.signal);
8560
- continue;
8561
- case "expired_token":
8562
- case "access_denied":
8563
- case "invalid_grant":
8564
- throw new TransportError(`oauth device flow failed: ${err}`, {
8565
- kind: "request",
8566
- cause: payload
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
- default:
8569
- throw new TransportError(`oauth device flow error: ${err}`, {
8570
- kind: "request",
8571
- cause: payload
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
- const accessToken = String(payload.access_token || "").trim() || void 0;
8576
- const idToken = String(payload.id_token || "").trim() || void 0;
8577
- const refreshToken = String(payload.refresh_token || "").trim() || void 0;
8578
- const tokenType = String(payload.token_type || "").trim() || void 0;
8579
- const scope = String(payload.scope || "").trim() || void 0;
8580
- const expiresIn = payload.expires_in !== void 0 ? Number(payload.expires_in) : void 0;
8581
- const expiresAt = typeof expiresIn === "number" && Number.isFinite(expiresIn) && expiresIn > 0 ? new Date(Date.now() + expiresIn * 1e3) : void 0;
8582
- if (!accessToken && !idToken) {
8583
- throw new TransportError("oauth device flow returned an invalid token response", {
8584
- kind: "request",
8585
- cause: payload
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
- async function runOAuthDeviceFlowForIDToken(cfg) {
8592
- const auth = await startOAuthDeviceAuthorization({
8593
- deviceAuthorizationEndpoint: cfg.deviceAuthorizationEndpoint,
8594
- clientId: cfg.clientId,
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 token.idToken;
9278
+ return trimmed;
8617
9279
  }
8618
- async function postOAuthForm(url, form, opts) {
8619
- const fetchFn = opts.fetch ?? globalThis.fetch;
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
- async function sleep(ms, signal) {
8649
- if (!ms || ms <= 0) {
8650
- return;
8651
- }
8652
- if (!signal) {
8653
- await new Promise((resolve) => setTimeout(resolve, ms));
8654
- return;
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 (signal.aborted) {
8657
- throw new TransportError("oauth device flow aborted", { kind: "request" });
9297
+ if (status >= 500 && status < 600) {
9298
+ return method !== "POST" || retryPost;
8658
9299
  }
8659
- await new Promise((resolve, reject) => {
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 transformJSONObject(object) {
8677
- return { object };
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 transformJSONMerge(merge) {
8680
- return { merge: merge.slice() };
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 wireRequest(req) {
8683
- const raw = req;
8684
- if (raw && typeof raw === "object") {
8685
- if ("input" in raw) {
8686
- return req;
8687
- }
8688
- if ("body" in raw) {
8689
- return raw.body ?? {};
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 asInternal(req).body;
9332
+ return controller.signal;
8693
9333
  }
8694
- var WorkflowBuilderV0 = class _WorkflowBuilderV0 {
8695
- constructor(state = { nodes: [], edges: [], outputs: [] }) {
8696
- this.state = state;
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
- static new() {
8699
- return new _WorkflowBuilderV0();
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
- with(patch) {
8702
- return new _WorkflowBuilderV0({
8703
- ...this.state,
8704
- ...patch
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
- name(name) {
8708
- return this.with({ name: name.trim() || void 0 });
8709
- }
8710
- execution(execution) {
8711
- return this.with({ execution });
8712
- }
8713
- node(node) {
8714
- return this.with({ nodes: [...this.state.nodes, node] });
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
- llmResponses(id, request, options = {}) {
8717
- const input = {
8718
- request: wireRequest(request),
8719
- ...options.stream === void 0 ? {} : { stream: options.stream },
8720
- ...options.toolExecution === void 0 ? {} : { tool_execution: { mode: options.toolExecution } },
8721
- ...options.toolLimits === void 0 ? {} : { tool_limits: { ...options.toolLimits } },
8722
- ...options.bindings === void 0 ? {} : { bindings: options.bindings.slice() }
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
- joinAll(id) {
8731
- return this.node({ id, type: WorkflowNodeTypes.JoinAll });
8732
- }
8733
- transformJSON(id, input) {
8734
- return this.node({ id, type: WorkflowNodeTypes.TransformJSON, input });
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
- edge(from, to) {
8737
- return this.with({ edges: [...this.state.edges, { from, to }] });
9416
+ get id() {
9417
+ return this.customerId;
8738
9418
  }
8739
- output(name, from, pointer) {
8740
- return this.with({
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
- build() {
8748
- const edges = this.state.edges.slice().sort((a, b) => {
8749
- const af = String(a.from);
8750
- const bf = String(b.from);
8751
- if (af < bf) return -1;
8752
- if (af > bf) return 1;
8753
- const at = String(a.to);
8754
- const bt = String(b.to);
8755
- if (at < bt) return -1;
8756
- if (at > bt) return 1;
8757
- return 0;
8758
- });
8759
- const outputs = this.state.outputs.slice().sort((a, b) => {
8760
- const an = String(a.name);
8761
- const bn = String(b.name);
8762
- if (an < bn) return -1;
8763
- if (an > bn) return 1;
8764
- const af = String(a.from);
8765
- const bf = String(b.from);
8766
- if (af < bf) return -1;
8767
- if (af > bf) return 1;
8768
- const ap = a.pointer ?? "";
8769
- const bp = b.pointer ?? "";
8770
- if (ap < bp) return -1;
8771
- if (ap > bp) return 1;
8772
- return 0;
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
- return {
8775
- kind: WorkflowKinds.WorkflowV0,
8776
- ...this.state.name ? { name: this.state.name } : {},
8777
- ...this.state.execution ? { execution: this.state.execution } : {},
8778
- nodes: this.state.nodes.slice(),
8779
- ...edges.length ? { edges } : {},
8780
- outputs
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/workflow_v0.schema.json
8789
- var workflow_v0_schema_default = {
8790
- $id: "https://modelrelay.ai/schemas/workflow_v0.schema.json",
8791
- $schema: "http://json-schema.org/draft-07/schema#",
8792
- additionalProperties: false,
8793
- definitions: {
8794
- edge: {
8795
- additionalProperties: false,
8796
- properties: {
8797
- from: {
8798
- minLength: 1,
8799
- type: "string"
8800
- },
8801
- to: {
8802
- minLength: 1,
8803
- type: "string"
8804
- }
8805
- },
8806
- required: [
8807
- "from",
8808
- "to"
8809
- ],
8810
- type: "object"
8811
- },
8812
- llmResponsesBinding: {
8813
- additionalProperties: false,
8814
- properties: {
8815
- encoding: {
8816
- enum: [
8817
- "json",
8818
- "json_string"
8819
- ],
8820
- type: "string"
8821
- },
8822
- from: {
8823
- minLength: 1,
8824
- type: "string"
8825
- },
8826
- pointer: {
8827
- pattern: "^(/.*)?$",
8828
- type: "string"
8829
- },
8830
- to: {
8831
- pattern: "^/.*$",
8832
- type: "string"
8833
- }
8834
- },
8835
- required: [
8836
- "from",
8837
- "to"
8838
- ],
8839
- type: "object"
8840
- },
8841
- node: {
8842
- additionalProperties: false,
8843
- oneOf: [
8844
- {
8845
- allOf: [
8846
- {
8847
- properties: {
8848
- input: {
8849
- properties: {
8850
- bindings: {
8851
- items: {
8852
- $ref: "#/definitions/llmResponsesBinding"
8853
- },
8854
- type: "array"
8855
- },
8856
- request: {
8857
- type: "object"
8858
- },
8859
- stream: {
8860
- type: "boolean"
8861
- },
8862
- tool_execution: {
8863
- additionalProperties: false,
8864
- properties: {
8865
- mode: {
8866
- default: "server",
8867
- enum: [
8868
- "server",
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
- properties: {
8988
- id: {
8989
- minLength: 1,
8990
- type: "string"
8991
- },
8992
- input: {},
8993
- type: {
8994
- enum: [
8995
- "llm.responses",
8996
- "join.all",
8997
- "transform.json"
8998
- ],
8999
- type: "string"
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
- required: [
9003
- "id",
9004
- "type"
9005
- ],
9006
- type: "object"
9007
- },
9008
- output: {
9009
- additionalProperties: false,
9010
- properties: {
9011
- from: {
9012
- minLength: 1,
9013
- type: "string"
9014
- },
9015
- name: {
9016
- minLength: 1,
9017
- type: "string"
9018
- },
9019
- pointer: {
9020
- pattern: "^(/.*)?$",
9021
- type: "string"
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
- required: [
9025
- "name",
9026
- "from"
9027
- ],
9028
- type: "object"
9029
- },
9030
- transformValue: {
9031
- additionalProperties: false,
9032
- properties: {
9033
- from: {
9034
- minLength: 1,
9035
- type: "string"
9036
- },
9037
- pointer: {
9038
- pattern: "^(/.*)?$",
9039
- type: "string"
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
- required: [
9043
- "from"
9044
- ],
9045
- type: "object"
9046
- }
9047
- },
9048
- properties: {
9049
- edges: {
9050
- items: {
9051
- $ref: "#/definitions/edge"
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
- type: "array"
9054
- },
9055
- execution: {
9056
- additionalProperties: false,
9057
- properties: {
9058
- max_parallelism: {
9059
- minimum: 1,
9060
- type: "integer"
9061
- },
9062
- node_timeout_ms: {
9063
- minimum: 1,
9064
- type: "integer"
9065
- },
9066
- run_timeout_ms: {
9067
- minimum: 1,
9068
- type: "integer"
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
- type: "object"
9072
- },
9073
- kind: {
9074
- const: "workflow.v0",
9075
- type: "string"
9076
- },
9077
- name: {
9078
- type: "string"
9079
- },
9080
- nodes: {
9081
- items: {
9082
- $ref: "#/definitions/node"
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
- minItems: 1,
9085
- type: "array"
9086
- },
9087
- outputs: {
9088
- items: {
9089
- $ref: "#/definitions/output"
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
- minItems: 1,
9092
- type: "array"
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
- required: [
9096
- "kind",
9097
- "nodes",
9098
- "outputs"
9099
- ],
9100
- title: "ModelRelay workflow.v0",
9101
- type: "object"
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,