@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.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -22,6 +32,10 @@ var index_exports = {};
22
32
  __export(index_exports, {
23
33
  APIError: () => APIError,
24
34
  AuthClient: () => AuthClient,
35
+ BillingProviders: () => BillingProviders,
36
+ BrowserDefaults: () => BrowserDefaults,
37
+ BrowserToolNames: () => BrowserToolNames,
38
+ BrowserToolPack: () => BrowserToolPack,
25
39
  ConfigError: () => ConfigError,
26
40
  ContentPartTypes: () => ContentPartTypes,
27
41
  CustomerResponsesClient: () => CustomerResponsesClient,
@@ -31,10 +45,17 @@ __export(index_exports, {
31
45
  DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
32
46
  DEFAULT_CLIENT_HEADER: () => DEFAULT_CLIENT_HEADER,
33
47
  DEFAULT_CONNECT_TIMEOUT_MS: () => DEFAULT_CONNECT_TIMEOUT_MS,
48
+ DEFAULT_IGNORE_DIRS: () => DEFAULT_IGNORE_DIRS,
34
49
  DEFAULT_REQUEST_TIMEOUT_MS: () => DEFAULT_REQUEST_TIMEOUT_MS,
35
50
  ErrorCodes: () => ErrorCodes,
51
+ FSDefaults: () => FSDefaults,
52
+ FSToolNames: () => ToolNames,
36
53
  FrontendTokenProvider: () => FrontendTokenProvider,
54
+ ImagesClient: () => ImagesClient,
37
55
  InputItemTypes: () => InputItemTypes,
56
+ LocalFSToolPack: () => LocalFSToolPack,
57
+ LocalSession: () => LocalSession,
58
+ MemorySessionStore: () => MemorySessionStore,
38
59
  MessageRoles: () => MessageRoles,
39
60
  ModelRelay: () => ModelRelay,
40
61
  ModelRelayError: () => ModelRelayError,
@@ -42,22 +63,27 @@ __export(index_exports, {
42
63
  OIDCExchangeTokenProvider: () => OIDCExchangeTokenProvider,
43
64
  OutputFormatTypes: () => OutputFormatTypes,
44
65
  OutputItemTypes: () => OutputItemTypes,
66
+ PathEscapeError: () => PathEscapeError,
45
67
  ResponsesClient: () => ResponsesClient,
46
68
  ResponsesStream: () => ResponsesStream,
47
69
  RunsClient: () => RunsClient,
48
70
  RunsEventStream: () => RunsEventStream,
49
71
  SDK_VERSION: () => SDK_VERSION,
72
+ SessionsClient: () => SessionsClient,
50
73
  StopReasons: () => StopReasons,
51
74
  StreamProtocolError: () => StreamProtocolError,
52
75
  StreamTimeoutError: () => StreamTimeoutError,
53
76
  StructuredDecodeError: () => StructuredDecodeError,
54
77
  StructuredExhaustedError: () => StructuredExhaustedError,
55
78
  StructuredJSONStream: () => StructuredJSONStream,
79
+ SubscriptionStatuses: () => SubscriptionStatuses,
56
80
  TiersClient: () => TiersClient,
57
81
  ToolArgsError: () => ToolArgsError,
82
+ ToolArgumentError: () => ToolArgumentError,
58
83
  ToolCallAccumulator: () => ToolCallAccumulator,
59
84
  ToolChoiceTypes: () => ToolChoiceTypes,
60
85
  ToolRegistry: () => ToolRegistry,
86
+ ToolRunner: () => ToolRunner,
61
87
  ToolTypes: () => ToolTypes,
62
88
  TransportError: () => TransportError,
63
89
  WORKFLOWS_COMPILE_PATH: () => WORKFLOWS_COMPILE_PATH,
@@ -69,17 +95,25 @@ __export(index_exports, {
69
95
  WorkflowsClient: () => WorkflowsClient,
70
96
  asModelId: () => asModelId,
71
97
  asProviderId: () => asProviderId,
98
+ asSessionId: () => asSessionId,
72
99
  asTierCode: () => asTierCode,
73
100
  assistantMessageWithToolCalls: () => assistantMessageWithToolCalls,
74
101
  createAccessTokenAuth: () => createAccessTokenAuth,
75
102
  createApiKeyAuth: () => createApiKeyAuth,
76
103
  createAssistantMessage: () => createAssistantMessage,
104
+ createBrowserToolPack: () => createBrowserToolPack,
105
+ createBrowserTools: () => createBrowserTools,
77
106
  createFunctionCall: () => createFunctionCall,
78
107
  createFunctionTool: () => createFunctionTool,
79
108
  createFunctionToolFromSchema: () => createFunctionToolFromSchema,
109
+ createLocalFSToolPack: () => createLocalFSToolPack,
110
+ createLocalFSTools: () => createLocalFSTools,
111
+ createLocalSession: () => createLocalSession,
112
+ createMemorySessionStore: () => createMemorySessionStore,
80
113
  createRetryMessages: () => createRetryMessages,
81
114
  createSystemMessage: () => createSystemMessage,
82
115
  createToolCall: () => createToolCall,
116
+ createToolRunner: () => createToolRunner,
83
117
  createUsage: () => createUsage,
84
118
  createUserMessage: () => createUserMessage,
85
119
  createWebTool: () => createWebTool,
@@ -87,6 +121,7 @@ __export(index_exports, {
87
121
  executeWithRetry: () => executeWithRetry,
88
122
  firstToolCall: () => firstToolCall,
89
123
  formatToolErrorForModel: () => formatToolErrorForModel,
124
+ generateSessionId: () => generateSessionId,
90
125
  generated: () => generated_exports,
91
126
  getRetryableErrors: () => getRetryableErrors,
92
127
  hasRetryableErrors: () => hasRetryableErrors,
@@ -289,6 +324,28 @@ var WorkflowValidationError = class extends ModelRelayError {
289
324
  this.issues = opts.issues;
290
325
  }
291
326
  };
327
+ var ToolArgumentError = class extends ModelRelayError {
328
+ constructor(opts) {
329
+ super(opts.message, {
330
+ category: "config",
331
+ status: 400,
332
+ cause: opts.cause
333
+ });
334
+ this.toolCallId = opts.toolCallId;
335
+ this.toolName = opts.toolName;
336
+ this.rawArguments = opts.rawArguments;
337
+ }
338
+ };
339
+ var PathEscapeError = class extends ModelRelayError {
340
+ constructor(opts) {
341
+ super(`path escapes sandbox: ${opts.requestedPath}`, {
342
+ category: "config",
343
+ status: 403
344
+ });
345
+ this.requestedPath = opts.requestedPath;
346
+ this.resolvedPath = opts.resolvedPath;
347
+ }
348
+ };
292
349
  function isEmailRequired(err) {
293
350
  return err instanceof APIError && err.isEmailRequired();
294
351
  }
@@ -335,10 +392,10 @@ async function parseErrorResponse(response, retries) {
335
392
  if (!raw || typeof raw !== "object") continue;
336
393
  const obj = raw;
337
394
  const code = typeof obj.code === "string" ? obj.code : "";
338
- const path = typeof obj.path === "string" ? obj.path : "";
395
+ const path2 = typeof obj.path === "string" ? obj.path : "";
339
396
  const message = typeof obj.message === "string" ? obj.message : "";
340
- if (!code || !path || !message) continue;
341
- normalized.push({ code, path, message });
397
+ if (!code || !path2 || !message) continue;
398
+ normalized.push({ code, path: path2, message });
342
399
  }
343
400
  if (normalized.length > 0) {
344
401
  return new WorkflowValidationError({
@@ -579,24 +636,18 @@ var AuthClient = class {
579
636
  * Mint a customer-scoped bearer token (requires a secret key).
580
637
  */
581
638
  async customerToken(request) {
582
- const projectId = request.projectId?.trim();
583
- if (!projectId) {
584
- throw new ConfigError("projectId is required");
585
- }
586
639
  const customerId = request.customerId?.trim();
587
640
  const customerExternalId = request.customerExternalId?.trim();
588
641
  if (!!customerId && !!customerExternalId || !customerId && !customerExternalId) {
589
642
  throw new ConfigError("Provide exactly one of customerId or customerExternalId");
590
643
  }
591
- if (request.ttlSeconds !== void 0 && request.ttlSeconds <= 0) {
592
- throw new ConfigError("ttlSeconds must be positive when provided");
644
+ if (request.ttlSeconds !== void 0 && request.ttlSeconds < 0) {
645
+ throw new ConfigError("ttlSeconds must be non-negative when provided");
593
646
  }
594
647
  if (!this.apiKey || this.apiKeyIsPublishable) {
595
648
  throw new ConfigError("Secret API key is required to mint customer tokens");
596
649
  }
597
- const payload = {
598
- project_id: projectId
599
- };
650
+ const payload = {};
600
651
  if (customerId) {
601
652
  payload.customer_id = customerId;
602
653
  }
@@ -694,8 +745,8 @@ var AuthClient = class {
694
745
  params.set("provider", request.provider);
695
746
  }
696
747
  const queryString = params.toString();
697
- const path = queryString ? `/auth/device/start?${queryString}` : "/auth/device/start";
698
- const apiResp = await this.http.json(path, {
748
+ const path2 = queryString ? `/auth/device/start?${queryString}` : "/auth/device/start";
749
+ const apiResp = await this.http.json(path2, {
699
750
  method: "POST",
700
751
  apiKey: this.apiKey
701
752
  });
@@ -759,7 +810,7 @@ var AuthClient = class {
759
810
  token: apiResp.token,
760
811
  expiresAt: new Date(apiResp.expires_at),
761
812
  expiresIn: apiResp.expires_in,
762
- tokenType: apiResp.token_type,
813
+ tokenType: "Bearer",
763
814
  projectId: apiResp.project_id,
764
815
  customerId: apiResp.customer_id,
765
816
  customerExternalId: apiResp.customer_external_id,
@@ -818,7 +869,7 @@ function isTokenReusable(token) {
818
869
  // package.json
819
870
  var package_default = {
820
871
  name: "@modelrelay/sdk",
821
- version: "1.3.2",
872
+ version: "1.10.3",
822
873
  description: "TypeScript SDK for the ModelRelay API",
823
874
  type: "module",
824
875
  main: "dist/index.cjs",
@@ -831,12 +882,14 @@ var package_default = {
831
882
  require: "./dist/index.cjs"
832
883
  }
833
884
  },
834
- publishConfig: { access: "public" },
885
+ publishConfig: {
886
+ access: "public"
887
+ },
835
888
  files: [
836
889
  "dist"
837
890
  ],
838
891
  scripts: {
839
- build: "tsup src/index.ts --format esm,cjs --dts",
892
+ build: "tsup src/index.ts --format esm,cjs --dts --external playwright",
840
893
  dev: "tsup src/index.ts --format esm,cjs --dts --watch",
841
894
  lint: "tsc --noEmit --project tsconfig.lint.json",
842
895
  test: "vitest run",
@@ -853,8 +906,18 @@ var package_default = {
853
906
  dependencies: {
854
907
  "fast-json-patch": "^3.1.1"
855
908
  },
909
+ peerDependencies: {
910
+ playwright: ">=1.40.0"
911
+ },
912
+ peerDependenciesMeta: {
913
+ playwright: {
914
+ optional: true
915
+ }
916
+ },
856
917
  devDependencies: {
918
+ "@types/node": "^25.0.3",
857
919
  "openapi-typescript": "^7.4.4",
920
+ playwright: "^1.49.0",
858
921
  tsup: "^8.2.4",
859
922
  typescript: "^5.6.3",
860
923
  vitest: "^2.1.4",
@@ -891,6 +954,22 @@ function asModelId(value) {
891
954
  function asTierCode(value) {
892
955
  return value;
893
956
  }
957
+ var SubscriptionStatuses = {
958
+ Active: "active",
959
+ Trialing: "trialing",
960
+ PastDue: "past_due",
961
+ Canceled: "canceled",
962
+ Unpaid: "unpaid",
963
+ Incomplete: "incomplete",
964
+ IncompleteExpired: "incomplete_expired",
965
+ Paused: "paused"
966
+ };
967
+ var BillingProviders = {
968
+ Stripe: "stripe",
969
+ Crypto: "crypto",
970
+ AppStore: "app_store",
971
+ External: "external"
972
+ };
894
973
  function createUsage(inputTokens, outputTokens, totalTokens) {
895
974
  return {
896
975
  inputTokens,
@@ -1408,8 +1487,8 @@ function parseToolArgs(call, schema) {
1408
1487
  const zodErr = err;
1409
1488
  if (zodErr.errors && Array.isArray(zodErr.errors)) {
1410
1489
  const issues = zodErr.errors.map((e) => {
1411
- const path = e.path.length > 0 ? `${e.path.join(".")}: ` : "";
1412
- return `${path}${e.message}`;
1490
+ const path2 = e.path.length > 0 ? `${e.path.join(".")}: ` : "";
1491
+ return `${path2}${e.message}`;
1413
1492
  }).join("; ");
1414
1493
  message = issues;
1415
1494
  } else {
@@ -1533,7 +1612,7 @@ var ToolRegistry = class {
1533
1612
  result
1534
1613
  };
1535
1614
  } catch (err) {
1536
- const isRetryable = err instanceof ToolArgsError;
1615
+ const isRetryable = err instanceof ToolArgsError || err instanceof ToolArgumentError;
1537
1616
  const errorMessage = err instanceof Error ? err.message : String(err);
1538
1617
  return {
1539
1618
  toolCallId: call.id,
@@ -2053,6 +2132,7 @@ function mapNDJSONResponseEvent(line, requestId) {
2053
2132
  }
2054
2133
  const toolCallDelta = extractToolCallDelta(parsed, type);
2055
2134
  const toolCalls = extractToolCalls(parsed, type);
2135
+ const toolResult = extractToolResult(parsed, type);
2056
2136
  return {
2057
2137
  type,
2058
2138
  event: recordType2,
@@ -2060,6 +2140,7 @@ function mapNDJSONResponseEvent(line, requestId) {
2060
2140
  textDelta,
2061
2141
  toolCallDelta,
2062
2142
  toolCalls,
2143
+ toolResult,
2063
2144
  responseId,
2064
2145
  model,
2065
2146
  stopReason,
@@ -2123,6 +2204,15 @@ function extractToolCalls(payload, type) {
2123
2204
  }
2124
2205
  return void 0;
2125
2206
  }
2207
+ function extractToolResult(payload, type) {
2208
+ if (type !== "tool_use_stop") {
2209
+ return void 0;
2210
+ }
2211
+ if ("tool_result" in payload) {
2212
+ return payload.tool_result;
2213
+ }
2214
+ return void 0;
2215
+ }
2126
2216
  function normalizeToolCalls(toolCalls) {
2127
2217
  const validToolTypes = /* @__PURE__ */ new Set([
2128
2218
  "function",
@@ -3533,8 +3623,8 @@ function getErrorMap() {
3533
3623
 
3534
3624
  // node_modules/zod/v3/helpers/parseUtil.js
3535
3625
  var makeIssue = (params) => {
3536
- const { data, path, errorMaps, issueData } = params;
3537
- const fullPath = [...path, ...issueData.path || []];
3626
+ const { data, path: path2, errorMaps, issueData } = params;
3627
+ const fullPath = [...path2, ...issueData.path || []];
3538
3628
  const fullIssue = {
3539
3629
  ...issueData,
3540
3630
  path: fullPath
@@ -3650,11 +3740,11 @@ var errorUtil;
3650
3740
 
3651
3741
  // node_modules/zod/v3/types.js
3652
3742
  var ParseInputLazyPath = class {
3653
- constructor(parent, value, path, key) {
3743
+ constructor(parent, value, path2, key) {
3654
3744
  this._cachedPath = [];
3655
3745
  this.parent = parent;
3656
3746
  this.data = value;
3657
- this._path = path;
3747
+ this._path = path2;
3658
3748
  this._key = key;
3659
3749
  }
3660
3750
  get path() {
@@ -7487,11 +7577,18 @@ var RunsClient = class {
7487
7577
  this.metrics = cfg.metrics;
7488
7578
  this.trace = cfg.trace;
7489
7579
  }
7580
+ applyCustomerHeader(headers, customerId) {
7581
+ const trimmed = customerId?.trim();
7582
+ if (trimmed) {
7583
+ headers[CUSTOMER_ID_HEADER] = trimmed;
7584
+ }
7585
+ }
7490
7586
  async create(spec, options = {}) {
7491
7587
  const metrics = mergeMetrics(this.metrics, options.metrics);
7492
7588
  const trace = mergeTrace(this.trace, options.trace);
7493
7589
  const authHeaders = await this.auth.authForResponses();
7494
7590
  const headers = { ...options.headers || {} };
7591
+ this.applyCustomerHeader(headers, options.customerId);
7495
7592
  const payload = { spec };
7496
7593
  if (options.idempotencyKey?.trim()) {
7497
7594
  payload.options = { idempotency_key: options.idempotencyKey.trim() };
@@ -7548,10 +7645,12 @@ var RunsClient = class {
7548
7645
  const metrics = mergeMetrics(this.metrics, options.metrics);
7549
7646
  const trace = mergeTrace(this.trace, options.trace);
7550
7647
  const authHeaders = await this.auth.authForResponses();
7551
- const path = runByIdPath(runId);
7552
- const out = await this.http.json(path, {
7648
+ const path2 = runByIdPath(runId);
7649
+ const headers = { ...options.headers || {} };
7650
+ this.applyCustomerHeader(headers, options.customerId);
7651
+ const out = await this.http.json(path2, {
7553
7652
  method: "GET",
7554
- headers: options.headers,
7653
+ headers,
7555
7654
  signal: options.signal,
7556
7655
  apiKey: authHeaders.apiKey,
7557
7656
  accessToken: authHeaders.accessToken,
@@ -7560,7 +7659,7 @@ var RunsClient = class {
7560
7659
  retry: options.retry,
7561
7660
  metrics,
7562
7661
  trace,
7563
- context: { method: "GET", path }
7662
+ context: { method: "GET", path: path2 }
7564
7663
  });
7565
7664
  return {
7566
7665
  ...out,
@@ -7584,9 +7683,10 @@ var RunsClient = class {
7584
7683
  if (options.wait === false) {
7585
7684
  params.set("wait", "0");
7586
7685
  }
7587
- const path = params.toString() ? `${basePath}?${params}` : basePath;
7686
+ const path2 = params.toString() ? `${basePath}?${params}` : basePath;
7588
7687
  const headers = { ...options.headers || {} };
7589
- const resp = await this.http.request(path, {
7688
+ this.applyCustomerHeader(headers, options.customerId);
7689
+ const resp = await this.http.request(path2, {
7590
7690
  method: "GET",
7591
7691
  headers,
7592
7692
  signal: options.signal,
@@ -7637,10 +7737,12 @@ var RunsClient = class {
7637
7737
  const metrics = mergeMetrics(this.metrics, options.metrics);
7638
7738
  const trace = mergeTrace(this.trace, options.trace);
7639
7739
  const authHeaders = await this.auth.authForResponses();
7640
- const path = runToolResultsPath(runId);
7641
- const out = await this.http.json(path, {
7740
+ const path2 = runToolResultsPath(runId);
7741
+ const headers = { ...options.headers || {} };
7742
+ this.applyCustomerHeader(headers, options.customerId);
7743
+ const out = await this.http.json(path2, {
7642
7744
  method: "POST",
7643
- headers: options.headers,
7745
+ headers,
7644
7746
  body: req,
7645
7747
  signal: options.signal,
7646
7748
  apiKey: authHeaders.apiKey,
@@ -7650,7 +7752,7 @@ var RunsClient = class {
7650
7752
  retry: options.retry,
7651
7753
  metrics,
7652
7754
  trace,
7653
- context: { method: "POST", path }
7755
+ context: { method: "POST", path: path2 }
7654
7756
  });
7655
7757
  return out;
7656
7758
  }
@@ -7658,10 +7760,12 @@ var RunsClient = class {
7658
7760
  const metrics = mergeMetrics(this.metrics, options.metrics);
7659
7761
  const trace = mergeTrace(this.trace, options.trace);
7660
7762
  const authHeaders = await this.auth.authForResponses();
7661
- const path = runPendingToolsPath(runId);
7662
- const out = await this.http.json(path, {
7763
+ const path2 = runPendingToolsPath(runId);
7764
+ const headers = { ...options.headers || {} };
7765
+ this.applyCustomerHeader(headers, options.customerId);
7766
+ const out = await this.http.json(path2, {
7663
7767
  method: "GET",
7664
- headers: options.headers,
7768
+ headers,
7665
7769
  signal: options.signal,
7666
7770
  apiKey: authHeaders.apiKey,
7667
7771
  accessToken: authHeaders.accessToken,
@@ -7670,7 +7774,7 @@ var RunsClient = class {
7670
7774
  retry: options.retry,
7671
7775
  metrics,
7672
7776
  trace,
7673
- context: { method: "GET", path }
7777
+ context: { method: "GET", path: path2 }
7674
7778
  });
7675
7779
  return {
7676
7780
  ...out,
@@ -7699,12 +7803,17 @@ var WorkflowsClient = class {
7699
7803
  const metrics = mergeMetrics(this.metrics, options.metrics);
7700
7804
  const trace = mergeTrace(this.trace, options.trace);
7701
7805
  const authHeaders = await this.auth.authForResponses();
7806
+ const headers = { ...options.headers || {} };
7807
+ const customerId = options.customerId?.trim();
7808
+ if (customerId) {
7809
+ headers[CUSTOMER_ID_HEADER] = customerId;
7810
+ }
7702
7811
  try {
7703
7812
  const out = await this.http.json(
7704
7813
  WORKFLOWS_COMPILE_PATH,
7705
7814
  {
7706
7815
  method: "POST",
7707
- headers: options.headers,
7816
+ headers,
7708
7817
  body: spec,
7709
7818
  signal: options.signal,
7710
7819
  apiKey: authHeaders.apiKey,
@@ -7855,9 +7964,6 @@ var CustomersClient = class {
7855
7964
  */
7856
7965
  async create(request) {
7857
7966
  this.ensureSecretKey();
7858
- if (!request.tier_id?.trim()) {
7859
- throw new ConfigError("tier_id is required");
7860
- }
7861
7967
  if (!request.external_id?.trim()) {
7862
7968
  throw new ConfigError("external_id is required");
7863
7969
  }
@@ -7898,9 +8004,6 @@ var CustomersClient = class {
7898
8004
  */
7899
8005
  async upsert(request) {
7900
8006
  this.ensureSecretKey();
7901
- if (!request.tier_id?.trim()) {
7902
- throw new ConfigError("tier_id is required");
7903
- }
7904
8007
  if (!request.external_id?.trim()) {
7905
8008
  throw new ConfigError("external_id is required");
7906
8009
  }
@@ -7918,7 +8021,7 @@ var CustomersClient = class {
7918
8021
  return response.customer;
7919
8022
  }
7920
8023
  /**
7921
- * Link an end-user identity (provider + subject) to a customer found by email.
8024
+ * Link a customer identity (provider + subject) to a customer found by email.
7922
8025
  * Used when a customer subscribes via Stripe Checkout (email only) and later authenticates to the app.
7923
8026
  *
7924
8027
  * This is a user self-service operation that works with publishable keys,
@@ -7943,12 +8046,11 @@ var CustomersClient = class {
7943
8046
  if (!request.subject?.trim()) {
7944
8047
  throw new ConfigError("subject is required");
7945
8048
  }
7946
- const response = await this.http.json("/customers/claim", {
8049
+ await this.http.request("/customers/claim", {
7947
8050
  method: "POST",
7948
8051
  body: request,
7949
8052
  apiKey: this.apiKey
7950
8053
  });
7951
- return response.customer;
7952
8054
  }
7953
8055
  /**
7954
8056
  * Delete a customer by ID.
@@ -7964,18 +8066,21 @@ var CustomersClient = class {
7964
8066
  });
7965
8067
  }
7966
8068
  /**
7967
- * Create a Stripe checkout session for a customer.
8069
+ * Create a Stripe checkout session for a customer subscription.
7968
8070
  */
7969
- async createCheckoutSession(customerId, request) {
8071
+ async subscribe(customerId, request) {
7970
8072
  this.ensureSecretKey();
7971
8073
  if (!customerId?.trim()) {
7972
8074
  throw new ConfigError("customerId is required");
7973
8075
  }
8076
+ if (!request.tier_id?.trim()) {
8077
+ throw new ConfigError("tier_id is required");
8078
+ }
7974
8079
  if (!request.success_url?.trim() || !request.cancel_url?.trim()) {
7975
8080
  throw new ConfigError("success_url and cancel_url are required");
7976
8081
  }
7977
8082
  return await this.http.json(
7978
- `/customers/${customerId}/checkout`,
8083
+ `/customers/${customerId}/subscribe`,
7979
8084
  {
7980
8085
  method: "POST",
7981
8086
  body: request,
@@ -7984,20 +8089,34 @@ var CustomersClient = class {
7984
8089
  );
7985
8090
  }
7986
8091
  /**
7987
- * Get the subscription status for a customer.
8092
+ * Get the subscription details for a customer.
7988
8093
  */
7989
8094
  async getSubscription(customerId) {
7990
8095
  this.ensureSecretKey();
7991
8096
  if (!customerId?.trim()) {
7992
8097
  throw new ConfigError("customerId is required");
7993
8098
  }
7994
- return await this.http.json(
8099
+ const response = await this.http.json(
7995
8100
  `/customers/${customerId}/subscription`,
7996
8101
  {
7997
8102
  method: "GET",
7998
8103
  apiKey: this.apiKey
7999
8104
  }
8000
8105
  );
8106
+ return response.subscription;
8107
+ }
8108
+ /**
8109
+ * Cancel a customer's subscription at period end.
8110
+ */
8111
+ async unsubscribe(customerId) {
8112
+ this.ensureSecretKey();
8113
+ if (!customerId?.trim()) {
8114
+ throw new ConfigError("customerId is required");
8115
+ }
8116
+ await this.http.request(`/customers/${customerId}/subscription`, {
8117
+ method: "DELETE",
8118
+ apiKey: this.apiKey
8119
+ });
8001
8120
  }
8002
8121
  };
8003
8122
 
@@ -8099,903 +8218,1908 @@ var ModelsClient = class {
8099
8218
  if (params.capability) {
8100
8219
  qs.set("capability", params.capability);
8101
8220
  }
8102
- const path = qs.toString() ? `/models?${qs.toString()}` : "/models";
8103
- const resp = await this.http.json(path, { method: "GET" });
8221
+ const path2 = qs.toString() ? `/models?${qs.toString()}` : "/models";
8222
+ const resp = await this.http.json(path2, { method: "GET" });
8104
8223
  return resp.models;
8105
8224
  }
8106
8225
  };
8107
8226
 
8108
- // src/http.ts
8109
- var HTTPClient = class {
8110
- constructor(cfg) {
8111
- const resolvedBase = normalizeBaseUrl(cfg.baseUrl || DEFAULT_BASE_URL);
8112
- if (!isValidHttpUrl(resolvedBase)) {
8113
- throw new ConfigError(
8114
- "baseUrl must start with http:// or https://"
8115
- );
8227
+ // src/images.ts
8228
+ var IMAGES_PATH = "/images/generate";
8229
+ var ImagesClient = class {
8230
+ constructor(http, auth) {
8231
+ this.http = http;
8232
+ this.auth = auth;
8233
+ }
8234
+ /**
8235
+ * Generate images from a text prompt.
8236
+ *
8237
+ * By default, returns URLs (requires storage configuration).
8238
+ * Use response_format: "b64_json" for testing without storage.
8239
+ *
8240
+ * @param request - Image generation request (model optional if tier defines default)
8241
+ * @returns Generated images with URLs or base64 data
8242
+ * @throws {Error} If prompt is empty
8243
+ */
8244
+ async generate(request) {
8245
+ if (!request.prompt?.trim()) {
8246
+ throw new Error("prompt is required");
8116
8247
  }
8117
- this.baseUrl = resolvedBase;
8118
- this.apiKey = cfg.apiKey ? parseApiKey(cfg.apiKey) : void 0;
8119
- this.accessToken = cfg.accessToken?.trim();
8120
- this.fetchImpl = cfg.fetchImpl;
8121
- this.clientHeader = cfg.clientHeader?.trim() || DEFAULT_CLIENT_HEADER;
8122
- this.defaultConnectTimeoutMs = cfg.connectTimeoutMs === void 0 ? DEFAULT_CONNECT_TIMEOUT_MS : Math.max(0, cfg.connectTimeoutMs);
8123
- this.defaultTimeoutMs = cfg.timeoutMs === void 0 ? DEFAULT_REQUEST_TIMEOUT_MS : Math.max(0, cfg.timeoutMs);
8124
- this.retry = normalizeRetryConfig(cfg.retry);
8125
- this.defaultHeaders = normalizeHeaders(cfg.defaultHeaders);
8126
- this.metrics = cfg.metrics;
8127
- this.trace = cfg.trace;
8248
+ const auth = await this.auth.authForResponses();
8249
+ return await this.http.json(IMAGES_PATH, {
8250
+ method: "POST",
8251
+ body: request,
8252
+ apiKey: auth.apiKey,
8253
+ accessToken: auth.accessToken
8254
+ });
8128
8255
  }
8129
- async request(path, options = {}) {
8130
- const fetchFn = this.fetchImpl ?? globalThis.fetch;
8131
- if (!fetchFn) {
8132
- throw new ConfigError(
8133
- "fetch is not available; provide a fetch implementation"
8134
- );
8256
+ };
8257
+
8258
+ // src/sessions/types.ts
8259
+ function asSessionId(value) {
8260
+ return value;
8261
+ }
8262
+ function generateSessionId() {
8263
+ return crypto.randomUUID();
8264
+ }
8265
+
8266
+ // src/sessions/stores/memory_store.ts
8267
+ var MemorySessionStore = class {
8268
+ constructor() {
8269
+ this.sessions = /* @__PURE__ */ new Map();
8270
+ }
8271
+ async load(id) {
8272
+ const state = this.sessions.get(id);
8273
+ if (!state) return null;
8274
+ return structuredClone(state);
8275
+ }
8276
+ async save(state) {
8277
+ this.sessions.set(state.id, structuredClone(state));
8278
+ }
8279
+ async delete(id) {
8280
+ this.sessions.delete(id);
8281
+ }
8282
+ async list() {
8283
+ return Array.from(this.sessions.keys());
8284
+ }
8285
+ async close() {
8286
+ this.sessions.clear();
8287
+ }
8288
+ /**
8289
+ * Get the number of sessions in the store.
8290
+ * Useful for testing.
8291
+ */
8292
+ get size() {
8293
+ return this.sessions.size;
8294
+ }
8295
+ };
8296
+ function createMemorySessionStore() {
8297
+ return new MemorySessionStore();
8298
+ }
8299
+
8300
+ // src/sessions/local_session.ts
8301
+ var LocalSession = class _LocalSession {
8302
+ constructor(client, store, options, existingState) {
8303
+ this.type = "local";
8304
+ this.messages = [];
8305
+ this.artifacts = /* @__PURE__ */ new Map();
8306
+ this.nextSeq = 1;
8307
+ this.currentEvents = [];
8308
+ this.currentUsage = {
8309
+ inputTokens: 0,
8310
+ outputTokens: 0,
8311
+ totalTokens: 0,
8312
+ llmCalls: 0,
8313
+ toolCalls: 0
8314
+ };
8315
+ this.client = client;
8316
+ this.store = store;
8317
+ this.toolRegistry = options.toolRegistry;
8318
+ this.defaultModel = options.defaultModel;
8319
+ this.defaultProvider = options.defaultProvider;
8320
+ this.defaultTools = options.defaultTools;
8321
+ this.metadata = options.metadata || {};
8322
+ if (existingState) {
8323
+ this.id = existingState.id;
8324
+ this.messages = existingState.messages.map((m) => ({
8325
+ ...m,
8326
+ createdAt: new Date(m.createdAt)
8327
+ }));
8328
+ this.artifacts = new Map(Object.entries(existingState.artifacts));
8329
+ this.nextSeq = this.messages.length + 1;
8330
+ this.createdAt = new Date(existingState.createdAt);
8331
+ this.updatedAt = new Date(existingState.updatedAt);
8332
+ } else {
8333
+ this.id = options.sessionId || generateSessionId();
8334
+ this.createdAt = /* @__PURE__ */ new Date();
8335
+ this.updatedAt = /* @__PURE__ */ new Date();
8135
8336
  }
8136
- const method = options.method || "GET";
8137
- const url = buildUrl(this.baseUrl, path);
8138
- const metrics = mergeMetrics(this.metrics, options.metrics);
8139
- const trace = mergeTrace(this.trace, options.trace);
8140
- const context = {
8141
- method,
8142
- path,
8143
- ...options.context || {}
8337
+ }
8338
+ /**
8339
+ * Create a new local session.
8340
+ *
8341
+ * @param client - ModelRelay client
8342
+ * @param options - Session configuration
8343
+ * @returns A new LocalSession instance
8344
+ */
8345
+ static create(client, options = {}) {
8346
+ const store = createStore(options.persistence || "memory", options.storagePath);
8347
+ return new _LocalSession(client, store, options);
8348
+ }
8349
+ /**
8350
+ * Resume an existing session from storage.
8351
+ *
8352
+ * @param client - ModelRelay client
8353
+ * @param sessionId - ID of the session to resume
8354
+ * @param options - Session configuration (must match original persistence settings)
8355
+ * @returns The resumed LocalSession, or null if not found
8356
+ */
8357
+ static async resume(client, sessionId, options = {}) {
8358
+ const id = typeof sessionId === "string" ? asSessionId(sessionId) : sessionId;
8359
+ const store = createStore(options.persistence || "memory", options.storagePath);
8360
+ const state = await store.load(id);
8361
+ if (!state) {
8362
+ await store.close();
8363
+ return null;
8364
+ }
8365
+ return new _LocalSession(client, store, options, state);
8366
+ }
8367
+ get history() {
8368
+ return this.messages;
8369
+ }
8370
+ async run(prompt, options = {}) {
8371
+ const userMessage = this.addMessage({
8372
+ type: "message",
8373
+ role: "user",
8374
+ content: [{ type: "text", text: prompt }]
8375
+ });
8376
+ this.currentEvents = [];
8377
+ this.currentUsage = {
8378
+ inputTokens: 0,
8379
+ outputTokens: 0,
8380
+ totalTokens: 0,
8381
+ llmCalls: 0,
8382
+ toolCalls: 0
8144
8383
  };
8145
- trace?.requestStart?.(context);
8146
- const start = metrics?.httpRequest || trace?.requestFinish ? Date.now() : 0;
8147
- const headers = new Headers({
8148
- ...this.defaultHeaders,
8149
- ...options.headers || {}
8150
- });
8151
- const accepts = options.accept || (options.raw ? void 0 : "application/json");
8152
- if (accepts && !headers.has("Accept")) {
8153
- headers.set("Accept", accepts);
8384
+ this.currentRunId = void 0;
8385
+ this.currentNodeId = void 0;
8386
+ this.currentWaiting = void 0;
8387
+ try {
8388
+ const input = this.buildInput();
8389
+ const tools = mergeTools(this.defaultTools, options.tools);
8390
+ const spec = {
8391
+ kind: "workflow.v0",
8392
+ name: `session-${this.id}-turn-${this.nextSeq}`,
8393
+ nodes: [
8394
+ {
8395
+ id: "main",
8396
+ type: "llm.responses",
8397
+ input: {
8398
+ request: {
8399
+ provider: options.provider || this.defaultProvider,
8400
+ model: options.model || this.defaultModel,
8401
+ input,
8402
+ tools
8403
+ },
8404
+ tool_execution: this.toolRegistry ? { mode: "client" } : void 0
8405
+ }
8406
+ }
8407
+ ],
8408
+ outputs: [{ name: "result", from: "main" }]
8409
+ };
8410
+ const run = await this.client.runs.create(spec, {
8411
+ customerId: options.customerId
8412
+ });
8413
+ this.currentRunId = run.run_id;
8414
+ return await this.processRunEvents(options.signal);
8415
+ } catch (err) {
8416
+ const error = err instanceof Error ? err : new Error(String(err));
8417
+ return {
8418
+ status: "error",
8419
+ error: error.message,
8420
+ runId: this.currentRunId || parseRunId("unknown"),
8421
+ usage: this.currentUsage,
8422
+ events: this.currentEvents
8423
+ };
8154
8424
  }
8155
- const body = options.body;
8156
- const shouldEncodeJSON = body !== void 0 && body !== null && typeof body === "object" && !(body instanceof FormData) && !(body instanceof Blob);
8157
- const payload = shouldEncodeJSON ? JSON.stringify(body) : body;
8158
- if (shouldEncodeJSON && !headers.has("Content-Type")) {
8159
- headers.set("Content-Type", "application/json");
8425
+ }
8426
+ async submitToolResults(results) {
8427
+ if (!this.currentRunId || !this.currentNodeId || !this.currentWaiting) {
8428
+ throw new Error("No pending tool calls to submit results for");
8160
8429
  }
8161
- const accessToken = options.accessToken ?? this.accessToken;
8162
- if (accessToken) {
8163
- const bearer = accessToken.toLowerCase().startsWith("bearer ") ? accessToken : `Bearer ${accessToken}`;
8164
- headers.set("Authorization", bearer);
8430
+ await this.client.runs.submitToolResults(this.currentRunId, {
8431
+ node_id: this.currentNodeId,
8432
+ step: this.currentWaiting.step,
8433
+ request_id: this.currentWaiting.request_id,
8434
+ results: results.map((r) => ({
8435
+ tool_call_id: r.toolCallId,
8436
+ name: r.toolName,
8437
+ output: r.error ? `Error: ${r.error}` : typeof r.result === "string" ? r.result : JSON.stringify(r.result)
8438
+ }))
8439
+ });
8440
+ this.currentWaiting = void 0;
8441
+ return await this.processRunEvents();
8442
+ }
8443
+ getArtifacts() {
8444
+ return new Map(this.artifacts);
8445
+ }
8446
+ async close() {
8447
+ await this.persist();
8448
+ await this.store.close();
8449
+ }
8450
+ // ============================================================================
8451
+ // Private Methods
8452
+ // ============================================================================
8453
+ addMessage(input, runId) {
8454
+ const message = {
8455
+ ...input,
8456
+ seq: this.nextSeq++,
8457
+ createdAt: /* @__PURE__ */ new Date(),
8458
+ runId
8459
+ };
8460
+ this.messages.push(message);
8461
+ this.updatedAt = /* @__PURE__ */ new Date();
8462
+ return message;
8463
+ }
8464
+ buildInput() {
8465
+ return this.messages.map((m) => ({
8466
+ type: m.type,
8467
+ role: m.role,
8468
+ content: m.content,
8469
+ toolCalls: m.toolCalls,
8470
+ toolCallId: m.toolCallId
8471
+ }));
8472
+ }
8473
+ async processRunEvents(signal) {
8474
+ if (!this.currentRunId) {
8475
+ throw new Error("No current run");
8165
8476
  }
8166
- const apiKey = options.apiKey ?? this.apiKey;
8167
- if (apiKey) {
8168
- headers.set("X-ModelRelay-Api-Key", apiKey);
8477
+ const eventStream = await this.client.runs.events(this.currentRunId, {
8478
+ afterSeq: this.currentEvents.length
8479
+ });
8480
+ for await (const event of eventStream) {
8481
+ if (signal?.aborted) {
8482
+ return {
8483
+ status: "canceled",
8484
+ runId: this.currentRunId,
8485
+ usage: this.currentUsage,
8486
+ events: this.currentEvents
8487
+ };
8488
+ }
8489
+ this.currentEvents.push(event);
8490
+ switch (event.type) {
8491
+ case "node_llm_call":
8492
+ this.currentUsage = {
8493
+ ...this.currentUsage,
8494
+ llmCalls: this.currentUsage.llmCalls + 1,
8495
+ inputTokens: this.currentUsage.inputTokens + (event.llm_call.usage?.input_tokens || 0),
8496
+ outputTokens: this.currentUsage.outputTokens + (event.llm_call.usage?.output_tokens || 0),
8497
+ totalTokens: this.currentUsage.totalTokens + (event.llm_call.usage?.total_tokens || 0)
8498
+ };
8499
+ break;
8500
+ case "node_tool_call":
8501
+ this.currentUsage = {
8502
+ ...this.currentUsage,
8503
+ toolCalls: this.currentUsage.toolCalls + 1
8504
+ };
8505
+ break;
8506
+ case "node_waiting":
8507
+ this.currentNodeId = event.node_id;
8508
+ this.currentWaiting = event.waiting;
8509
+ if (this.toolRegistry) {
8510
+ const results = await this.executeTools(event.waiting.pending_tool_calls);
8511
+ return await this.submitToolResults(results);
8512
+ }
8513
+ return {
8514
+ status: "waiting_for_tools",
8515
+ pendingTools: event.waiting.pending_tool_calls.map((tc) => ({
8516
+ toolCallId: tc.tool_call_id,
8517
+ name: tc.name,
8518
+ arguments: tc.arguments
8519
+ })),
8520
+ runId: this.currentRunId,
8521
+ usage: this.currentUsage,
8522
+ events: this.currentEvents
8523
+ };
8524
+ case "run_completed":
8525
+ const runState = await this.client.runs.get(this.currentRunId);
8526
+ const output = extractTextOutput(runState.outputs || {});
8527
+ if (output) {
8528
+ this.addMessage(
8529
+ {
8530
+ type: "message",
8531
+ role: "assistant",
8532
+ content: [{ type: "text", text: output }]
8533
+ },
8534
+ this.currentRunId
8535
+ );
8536
+ }
8537
+ await this.persist();
8538
+ return {
8539
+ status: "complete",
8540
+ output,
8541
+ runId: this.currentRunId,
8542
+ usage: this.currentUsage,
8543
+ events: this.currentEvents
8544
+ };
8545
+ case "run_failed":
8546
+ return {
8547
+ status: "error",
8548
+ error: event.error.message,
8549
+ runId: this.currentRunId,
8550
+ usage: this.currentUsage,
8551
+ events: this.currentEvents
8552
+ };
8553
+ case "run_canceled":
8554
+ return {
8555
+ status: "canceled",
8556
+ error: event.error.message,
8557
+ runId: this.currentRunId,
8558
+ usage: this.currentUsage,
8559
+ events: this.currentEvents
8560
+ };
8561
+ }
8169
8562
  }
8170
- if (this.clientHeader && !headers.has("X-ModelRelay-Client")) {
8171
- headers.set("X-ModelRelay-Client", this.clientHeader);
8563
+ return {
8564
+ status: "error",
8565
+ error: "Run event stream ended unexpectedly",
8566
+ runId: this.currentRunId,
8567
+ usage: this.currentUsage,
8568
+ events: this.currentEvents
8569
+ };
8570
+ }
8571
+ async executeTools(pendingTools) {
8572
+ if (!this.toolRegistry) {
8573
+ throw new Error("No tool registry configured");
8172
8574
  }
8173
- const timeoutMs = options.useDefaultTimeout === false ? options.timeoutMs : options.timeoutMs ?? this.defaultTimeoutMs;
8174
- const connectTimeoutMs = options.useDefaultConnectTimeout === false ? options.connectTimeoutMs : options.connectTimeoutMs ?? this.defaultConnectTimeoutMs;
8175
- const retryCfg = normalizeRetryConfig(
8176
- options.retry === void 0 ? this.retry : options.retry
8177
- );
8178
- const attempts = retryCfg ? Math.max(1, retryCfg.maxAttempts) : 1;
8179
- let lastError;
8180
- let lastStatus;
8181
- for (let attempt = 1; attempt <= attempts; attempt++) {
8182
- let connectTimedOut = false;
8183
- let requestTimedOut = false;
8184
- const connectController = connectTimeoutMs && connectTimeoutMs > 0 ? new AbortController() : void 0;
8185
- const requestController = timeoutMs && timeoutMs > 0 ? new AbortController() : void 0;
8186
- const signal = mergeSignals(
8187
- options.signal,
8188
- connectController?.signal,
8189
- requestController?.signal
8190
- );
8191
- const connectTimer = connectController && setTimeout(() => {
8192
- connectTimedOut = true;
8193
- connectController.abort(
8194
- new DOMException("connect timeout", "AbortError")
8195
- );
8196
- }, connectTimeoutMs);
8197
- const requestTimer = requestController && setTimeout(() => {
8198
- requestTimedOut = true;
8199
- requestController.abort(
8200
- new DOMException("timeout", "AbortError")
8201
- );
8202
- }, timeoutMs);
8575
+ const results = [];
8576
+ for (const pending of pendingTools) {
8203
8577
  try {
8204
- const response = await fetchFn(url, {
8205
- method,
8206
- headers,
8207
- body: payload,
8208
- signal
8209
- });
8210
- if (connectTimer) {
8211
- clearTimeout(connectTimer);
8212
- }
8213
- if (!response.ok) {
8214
- const shouldRetry = retryCfg && shouldRetryStatus(
8215
- response.status,
8216
- method,
8217
- retryCfg.retryPost
8218
- ) && attempt < attempts;
8219
- if (shouldRetry) {
8220
- lastStatus = response.status;
8221
- await backoff(attempt, retryCfg);
8222
- continue;
8578
+ const result = await this.toolRegistry.execute({
8579
+ id: pending.tool_call_id,
8580
+ type: "function",
8581
+ function: {
8582
+ name: pending.name,
8583
+ arguments: pending.arguments
8223
8584
  }
8224
- const retries = buildRetryMetadata(attempt, response.status, lastError);
8225
- const finishedCtx2 = withRequestId(context, response.headers);
8226
- recordHttpMetrics(metrics, trace, start, retries, {
8227
- status: response.status,
8228
- context: finishedCtx2
8229
- });
8230
- throw options.raw ? await parseErrorResponse(response, retries) : await parseErrorResponse(response, retries);
8231
- }
8232
- const finishedCtx = withRequestId(context, response.headers);
8233
- recordHttpMetrics(metrics, trace, start, void 0, {
8234
- status: response.status,
8235
- context: finishedCtx
8236
8585
  });
8237
- return response;
8586
+ results.push(result);
8238
8587
  } catch (err) {
8239
- if (options.signal?.aborted) {
8240
- throw err;
8241
- }
8242
- if (err instanceof ModelRelayError) {
8243
- recordHttpMetrics(metrics, trace, start, void 0, {
8244
- error: err,
8245
- context
8246
- });
8247
- throw err;
8248
- }
8249
- const transportKind = classifyTransportErrorKind(
8250
- err,
8251
- connectTimedOut,
8252
- requestTimedOut
8253
- );
8254
- const shouldRetry = retryCfg && isRetryableError(err, transportKind) && (method !== "POST" || retryCfg.retryPost) && attempt < attempts;
8255
- if (!shouldRetry) {
8256
- const retries = buildRetryMetadata(
8257
- attempt,
8258
- lastStatus,
8259
- err instanceof Error ? err.message : String(err)
8260
- );
8261
- recordHttpMetrics(metrics, trace, start, retries, {
8262
- error: err,
8263
- context
8264
- });
8265
- throw toTransportError(err, transportKind, retries);
8266
- }
8267
- lastError = err;
8268
- await backoff(attempt, retryCfg);
8269
- } finally {
8270
- if (connectTimer) {
8271
- clearTimeout(connectTimer);
8272
- }
8273
- if (requestTimer) {
8274
- clearTimeout(requestTimer);
8275
- }
8588
+ const error = err instanceof Error ? err : new Error(String(err));
8589
+ results.push({
8590
+ toolCallId: pending.tool_call_id,
8591
+ toolName: pending.name,
8592
+ result: null,
8593
+ error: error.message
8594
+ });
8276
8595
  }
8277
8596
  }
8278
- throw lastError instanceof Error ? lastError : new TransportError("request failed", {
8279
- kind: "other",
8280
- retries: buildRetryMetadata(attempts, lastStatus)
8281
- });
8282
- }
8283
- async json(path, options = {}) {
8284
- const response = await this.request(path, {
8285
- ...options,
8286
- raw: true,
8287
- accept: options.accept || "application/json"
8288
- });
8289
- if (!response.ok) {
8290
- throw await parseErrorResponse(response);
8291
- }
8292
- if (response.status === 204) {
8293
- return void 0;
8294
- }
8295
- try {
8296
- return await response.json();
8297
- } catch (err) {
8298
- throw new APIError("failed to parse response JSON", {
8299
- status: response.status,
8300
- data: err
8301
- });
8302
- }
8597
+ return results;
8598
+ }
8599
+ async persist() {
8600
+ const state = {
8601
+ id: this.id,
8602
+ messages: this.messages.map((m) => ({
8603
+ ...m,
8604
+ createdAt: m.createdAt
8605
+ })),
8606
+ artifacts: Object.fromEntries(this.artifacts),
8607
+ metadata: this.metadata,
8608
+ createdAt: this.createdAt.toISOString(),
8609
+ updatedAt: this.updatedAt.toISOString()
8610
+ };
8611
+ await this.store.save(state);
8303
8612
  }
8304
8613
  };
8305
- function buildUrl(baseUrl, path) {
8306
- if (/^https?:\/\//i.test(path)) {
8307
- return path;
8308
- }
8309
- if (!path.startsWith("/")) {
8310
- path = `/${path}`;
8311
- }
8312
- return `${baseUrl}${path}`;
8313
- }
8314
- function normalizeBaseUrl(value) {
8315
- const trimmed = value.trim();
8316
- if (trimmed.endsWith("/")) {
8317
- return trimmed.slice(0, -1);
8614
+ function createStore(persistence, storagePath) {
8615
+ switch (persistence) {
8616
+ case "memory":
8617
+ return createMemorySessionStore();
8618
+ case "file":
8619
+ throw new Error("File persistence not yet implemented");
8620
+ case "sqlite":
8621
+ throw new Error("SQLite persistence not yet implemented");
8622
+ default:
8623
+ throw new Error(`Unknown persistence mode: ${persistence}`);
8318
8624
  }
8319
- return trimmed;
8320
- }
8321
- function isValidHttpUrl(value) {
8322
- return /^https?:\/\//i.test(value);
8323
8625
  }
8324
- function normalizeRetryConfig(retry) {
8325
- if (retry === false) return void 0;
8326
- const cfg = retry || {};
8327
- return {
8328
- maxAttempts: Math.max(1, cfg.maxAttempts ?? 3),
8329
- baseBackoffMs: Math.max(0, cfg.baseBackoffMs ?? 300),
8330
- maxBackoffMs: Math.max(0, cfg.maxBackoffMs ?? 5e3),
8331
- retryPost: cfg.retryPost ?? true
8332
- };
8333
- }
8334
- function shouldRetryStatus(status, method, retryPost) {
8335
- if (status === 408 || status === 429) {
8336
- return method !== "POST" || retryPost;
8337
- }
8338
- if (status >= 500 && status < 600) {
8339
- return method !== "POST" || retryPost;
8626
+ function mergeTools(defaults, overrides) {
8627
+ if (!defaults && !overrides) return void 0;
8628
+ if (!defaults) return overrides;
8629
+ if (!overrides) return defaults;
8630
+ const merged = /* @__PURE__ */ new Map();
8631
+ for (const tool of defaults) {
8632
+ if (tool.type === "function" && tool.function) {
8633
+ merged.set(tool.function.name, tool);
8634
+ }
8340
8635
  }
8341
- return false;
8342
- }
8343
- function isRetryableError(err, kind) {
8344
- if (!err) return false;
8345
- if (kind === "timeout" || kind === "connect") return true;
8346
- return err instanceof DOMException || err instanceof TypeError;
8347
- }
8348
- function backoff(attempt, cfg) {
8349
- const exp = Math.max(0, attempt - 1);
8350
- const base = cfg.baseBackoffMs * Math.pow(2, Math.min(exp, 10));
8351
- const capped = Math.min(base, cfg.maxBackoffMs);
8352
- const jitter = 0.5 + Math.random();
8353
- const delay = Math.min(cfg.maxBackoffMs, capped * jitter);
8354
- if (delay <= 0) return Promise.resolve();
8355
- return new Promise((resolve) => setTimeout(resolve, delay));
8356
- }
8357
- function mergeSignals(...signals) {
8358
- const active = signals.filter(Boolean);
8359
- if (active.length === 0) return void 0;
8360
- if (active.length === 1) return active[0];
8361
- const controller = new AbortController();
8362
- for (const src of active) {
8363
- if (src.aborted) {
8364
- controller.abort(src.reason);
8365
- break;
8636
+ for (const tool of overrides) {
8637
+ if (tool.type === "function" && tool.function) {
8638
+ merged.set(tool.function.name, tool);
8366
8639
  }
8367
- src.addEventListener(
8368
- "abort",
8369
- () => controller.abort(src.reason),
8370
- { once: true }
8371
- );
8372
8640
  }
8373
- return controller.signal;
8641
+ return Array.from(merged.values());
8374
8642
  }
8375
- function normalizeHeaders(headers) {
8376
- if (!headers) return {};
8377
- const normalized = {};
8378
- for (const [key, value] of Object.entries(headers)) {
8379
- if (!key || !value) continue;
8380
- const k = key.trim();
8381
- const v = value.trim();
8382
- if (k && v) {
8383
- normalized[k] = v;
8643
+ function extractTextOutput(outputs) {
8644
+ const result = outputs.result;
8645
+ if (typeof result === "string") return result;
8646
+ if (result && typeof result === "object") {
8647
+ const resp = result;
8648
+ if (Array.isArray(resp.output)) {
8649
+ const textParts = resp.output.filter((item) => item?.type === "message" && item?.role === "assistant").flatMap(
8650
+ (item) => (item.content || []).filter((c) => c?.type === "text").map((c) => c.text)
8651
+ );
8652
+ if (textParts.length > 0) {
8653
+ return textParts.join("\n");
8654
+ }
8655
+ }
8656
+ if (Array.isArray(resp.content)) {
8657
+ const textParts = resp.content.filter((c) => c?.type === "text").map((c) => c.text);
8658
+ if (textParts.length > 0) {
8659
+ return textParts.join("\n");
8660
+ }
8384
8661
  }
8385
8662
  }
8386
- return normalized;
8663
+ return void 0;
8387
8664
  }
8388
- function buildRetryMetadata(attempt, lastStatus, lastError) {
8389
- if (!attempt || attempt <= 1) return void 0;
8390
- return {
8391
- attempts: attempt,
8392
- lastStatus,
8393
- lastError: typeof lastError === "string" ? lastError : lastError instanceof Error ? lastError.message : lastError ? String(lastError) : void 0
8394
- };
8665
+ function createLocalSession(client, options = {}) {
8666
+ return LocalSession.create(client, options);
8395
8667
  }
8396
- function classifyTransportErrorKind(err, connectTimedOut, requestTimedOut) {
8397
- if (connectTimedOut) return "connect";
8398
- if (requestTimedOut) return "timeout";
8399
- if (err instanceof DOMException && err.name === "AbortError") {
8400
- return requestTimedOut ? "timeout" : "request";
8668
+
8669
+ // src/sessions/remote_session.ts
8670
+ var RemoteSession = class _RemoteSession {
8671
+ constructor(client, http, sessionData, options = {}) {
8672
+ this.type = "remote";
8673
+ this.messages = [];
8674
+ this.artifacts = /* @__PURE__ */ new Map();
8675
+ this.nextSeq = 1;
8676
+ this.currentEvents = [];
8677
+ this.currentUsage = {
8678
+ inputTokens: 0,
8679
+ outputTokens: 0,
8680
+ totalTokens: 0,
8681
+ llmCalls: 0,
8682
+ toolCalls: 0
8683
+ };
8684
+ this.client = client;
8685
+ this.http = http;
8686
+ this.id = asSessionId(sessionData.id);
8687
+ this.metadata = sessionData.metadata;
8688
+ this.endUserId = sessionData.end_user_id || options.endUserId;
8689
+ this.createdAt = new Date(sessionData.created_at);
8690
+ this.updatedAt = new Date(sessionData.updated_at);
8691
+ this.toolRegistry = options.toolRegistry;
8692
+ this.defaultModel = options.defaultModel;
8693
+ this.defaultProvider = options.defaultProvider;
8694
+ this.defaultTools = options.defaultTools;
8695
+ if ("messages" in sessionData && sessionData.messages) {
8696
+ this.messages = sessionData.messages.map((m) => ({
8697
+ type: "message",
8698
+ role: m.role,
8699
+ content: m.content,
8700
+ seq: m.seq,
8701
+ createdAt: new Date(m.created_at),
8702
+ runId: m.run_id ? parseRunId(m.run_id) : void 0
8703
+ }));
8704
+ this.nextSeq = this.messages.length + 1;
8705
+ }
8401
8706
  }
8402
- if (err instanceof TypeError) return "request";
8403
- return "other";
8404
- }
8405
- function toTransportError(err, kind, retries) {
8406
- const message = err instanceof Error ? err.message : typeof err === "string" ? err : "request failed";
8407
- return new TransportError(message, { kind, retries, cause: err });
8408
- }
8409
- function recordHttpMetrics(metrics, trace, start, retries, info) {
8410
- if (!metrics?.httpRequest && !trace?.requestFinish) return;
8411
- const latencyMs = start ? Date.now() - start : 0;
8412
- if (metrics?.httpRequest) {
8413
- metrics.httpRequest({
8414
- latencyMs,
8415
- status: info.status,
8416
- error: info.error ? String(info.error) : void 0,
8417
- retries,
8418
- context: info.context
8707
+ /**
8708
+ * Create a new remote session on the server.
8709
+ *
8710
+ * @param client - ModelRelay client
8711
+ * @param options - Session configuration
8712
+ * @returns A new RemoteSession instance
8713
+ */
8714
+ static async create(client, options = {}) {
8715
+ const http = getHTTPClient(client);
8716
+ const response = await http.request("/sessions", {
8717
+ method: "POST",
8718
+ body: {
8719
+ end_user_id: options.endUserId,
8720
+ metadata: options.metadata || {}
8721
+ }
8419
8722
  });
8723
+ const data = await response.json();
8724
+ return new _RemoteSession(client, http, data, options);
8420
8725
  }
8421
- trace?.requestFinish?.({
8422
- context: info.context,
8423
- status: info.status,
8424
- error: info.error,
8425
- retries,
8426
- latencyMs
8427
- });
8428
- }
8429
- function withRequestId(context, headers) {
8430
- const requestId = headers.get("X-ModelRelay-Request-Id") || headers.get("X-Request-Id") || context.requestId;
8431
- if (!requestId) return context;
8432
- return { ...context, requestId };
8433
- }
8434
-
8435
- // src/customer_scoped.ts
8436
- function normalizeCustomerId(customerId) {
8437
- const trimmed = customerId?.trim?.() ? customerId.trim() : "";
8438
- if (!trimmed) {
8439
- throw new ConfigError("customerId is required");
8440
- }
8441
- return trimmed;
8442
- }
8443
- function mergeCustomerOptions(customerId, options = {}) {
8444
- if (options.customerId && options.customerId !== customerId) {
8445
- throw new ConfigError("customerId mismatch", {
8446
- expected: customerId,
8447
- received: options.customerId
8726
+ /**
8727
+ * Get an existing remote session by ID.
8728
+ *
8729
+ * @param client - ModelRelay client
8730
+ * @param sessionId - ID of the session to retrieve
8731
+ * @param options - Optional configuration (toolRegistry, defaults)
8732
+ * @returns The RemoteSession instance
8733
+ */
8734
+ static async get(client, sessionId, options = {}) {
8735
+ const http = getHTTPClient(client);
8736
+ const id = typeof sessionId === "string" ? sessionId : String(sessionId);
8737
+ const response = await http.request(`/sessions/${id}`, {
8738
+ method: "GET"
8448
8739
  });
8740
+ const data = await response.json();
8741
+ return new _RemoteSession(client, http, data, options);
8449
8742
  }
8450
- return { ...options, customerId };
8451
- }
8452
- var CustomerResponsesClient = class {
8453
- constructor(base, customerId) {
8454
- this.customerId = normalizeCustomerId(customerId);
8455
- this.base = base;
8743
+ /**
8744
+ * List remote sessions.
8745
+ *
8746
+ * @param client - ModelRelay client
8747
+ * @param options - List options
8748
+ * @returns Paginated list of session info
8749
+ */
8750
+ static async list(client, options = {}) {
8751
+ const http = getHTTPClient(client);
8752
+ const params = new URLSearchParams();
8753
+ if (options.limit) params.set("limit", String(options.limit));
8754
+ if (options.offset) params.set("offset", String(options.offset));
8755
+ if (options.endUserId) params.set("end_user_id", options.endUserId);
8756
+ const response = await http.request(
8757
+ `/sessions${params.toString() ? `?${params.toString()}` : ""}`,
8758
+ { method: "GET" }
8759
+ );
8760
+ const data = await response.json();
8761
+ return {
8762
+ sessions: data.sessions.map((s) => ({
8763
+ id: asSessionId(s.id),
8764
+ messageCount: s.message_count,
8765
+ metadata: s.metadata,
8766
+ createdAt: new Date(s.created_at),
8767
+ updatedAt: new Date(s.updated_at)
8768
+ })),
8769
+ nextCursor: data.next_cursor
8770
+ };
8456
8771
  }
8457
- get id() {
8458
- return this.customerId;
8772
+ /**
8773
+ * Delete a remote session.
8774
+ *
8775
+ * @param client - ModelRelay client
8776
+ * @param sessionId - ID of the session to delete
8777
+ */
8778
+ static async delete(client, sessionId) {
8779
+ const http = getHTTPClient(client);
8780
+ const id = typeof sessionId === "string" ? sessionId : String(sessionId);
8781
+ await http.request(`/sessions/${id}`, {
8782
+ method: "DELETE"
8783
+ });
8459
8784
  }
8460
- new() {
8461
- return this.base.new().customerId(this.customerId);
8785
+ // ============================================================================
8786
+ // Session Interface Implementation
8787
+ // ============================================================================
8788
+ /**
8789
+ * Full conversation history (read-only).
8790
+ */
8791
+ get history() {
8792
+ return this.messages;
8462
8793
  }
8463
- ensureRequestCustomer(request) {
8464
- const req = asInternal(request);
8465
- const reqCustomer = req.options.customerId;
8466
- if (reqCustomer && reqCustomer !== this.customerId) {
8467
- throw new ConfigError("customerId mismatch", {
8468
- expected: this.customerId,
8469
- received: reqCustomer
8794
+ /**
8795
+ * Execute a prompt as a new turn in this session.
8796
+ */
8797
+ async run(prompt, options = {}) {
8798
+ const userMessage = this.addMessage({
8799
+ type: "message",
8800
+ role: "user",
8801
+ content: [{ type: "text", text: prompt }]
8802
+ });
8803
+ this.resetRunState();
8804
+ try {
8805
+ const input = this.buildInput();
8806
+ const tools = mergeTools2(this.defaultTools, options.tools);
8807
+ const spec = {
8808
+ kind: "workflow.v0",
8809
+ name: `session-${this.id}-turn-${this.nextSeq}`,
8810
+ nodes: [
8811
+ {
8812
+ id: "main",
8813
+ type: "llm.responses",
8814
+ input: {
8815
+ request: {
8816
+ provider: options.provider || this.defaultProvider,
8817
+ model: options.model || this.defaultModel,
8818
+ input,
8819
+ tools
8820
+ },
8821
+ tool_execution: this.toolRegistry ? { mode: "client" } : void 0
8822
+ }
8823
+ }
8824
+ ],
8825
+ outputs: [{ name: "result", from: "main" }]
8826
+ };
8827
+ const run = await this.client.runs.create(spec, {
8828
+ customerId: options.customerId || this.endUserId
8470
8829
  });
8830
+ this.currentRunId = run.run_id;
8831
+ return await this.processRunEvents(options.signal);
8832
+ } catch (err) {
8833
+ const error = err instanceof Error ? err : new Error(String(err));
8834
+ return {
8835
+ status: "error",
8836
+ error: error.message,
8837
+ runId: this.currentRunId || parseRunId("unknown"),
8838
+ usage: { ...this.currentUsage },
8839
+ events: [...this.currentEvents]
8840
+ };
8471
8841
  }
8472
8842
  }
8473
- async create(request, options = {}) {
8474
- this.ensureRequestCustomer(request);
8475
- return this.base.create(request, mergeCustomerOptions(this.customerId, options));
8476
- }
8477
- async stream(request, options = {}) {
8478
- this.ensureRequestCustomer(request);
8479
- return this.base.stream(request, mergeCustomerOptions(this.customerId, options));
8480
- }
8481
- async streamJSON(request, options = {}) {
8482
- this.ensureRequestCustomer(request);
8483
- return this.base.streamJSON(
8484
- request,
8485
- mergeCustomerOptions(this.customerId, options)
8486
- );
8843
+ /**
8844
+ * Submit tool results for a waiting run.
8845
+ */
8846
+ async submitToolResults(results) {
8847
+ if (!this.currentRunId || !this.currentNodeId || !this.currentWaiting) {
8848
+ throw new Error("No pending tool calls to submit results for");
8849
+ }
8850
+ await this.client.runs.submitToolResults(this.currentRunId, {
8851
+ node_id: this.currentNodeId,
8852
+ step: this.currentWaiting.step,
8853
+ request_id: this.currentWaiting.request_id,
8854
+ results: results.map((r) => ({
8855
+ tool_call_id: r.toolCallId,
8856
+ name: r.toolName,
8857
+ output: r.error ? `Error: ${r.error}` : typeof r.result === "string" ? r.result : JSON.stringify(r.result)
8858
+ }))
8859
+ });
8860
+ this.currentWaiting = void 0;
8861
+ return await this.processRunEvents();
8487
8862
  }
8488
- async text(system, user, options = {}) {
8489
- return this.base.textForCustomer(
8490
- this.customerId,
8491
- system,
8492
- user,
8493
- mergeCustomerOptions(this.customerId, options)
8494
- );
8863
+ /**
8864
+ * Get all artifacts produced during this session.
8865
+ */
8866
+ getArtifacts() {
8867
+ return new Map(this.artifacts);
8495
8868
  }
8496
- async streamTextDeltas(system, user, options = {}) {
8497
- return this.base.streamTextDeltasForCustomer(
8498
- this.customerId,
8499
- system,
8500
- user,
8501
- mergeCustomerOptions(this.customerId, options)
8502
- );
8869
+ /**
8870
+ * Close the session (no-op for remote sessions).
8871
+ */
8872
+ async close() {
8503
8873
  }
8504
- };
8505
- var CustomerScopedModelRelay = class {
8506
- constructor(responses, customerId, baseUrl) {
8507
- const normalized = normalizeCustomerId(customerId);
8508
- this.responses = new CustomerResponsesClient(responses, normalized);
8509
- this.customerId = normalized;
8510
- this.baseUrl = baseUrl;
8874
+ /**
8875
+ * Refresh the session state from the server.
8876
+ */
8877
+ async refresh() {
8878
+ const response = await this.http.request(`/sessions/${this.id}`, {
8879
+ method: "GET"
8880
+ });
8881
+ const data = await response.json();
8882
+ this.metadata = data.metadata;
8883
+ this.updatedAt = new Date(data.updated_at);
8884
+ if (data.messages) {
8885
+ this.messages = data.messages.map((m) => ({
8886
+ type: "message",
8887
+ role: m.role,
8888
+ content: m.content,
8889
+ seq: m.seq,
8890
+ createdAt: new Date(m.created_at),
8891
+ runId: m.run_id ? parseRunId(m.run_id) : void 0
8892
+ }));
8893
+ this.nextSeq = this.messages.length + 1;
8894
+ }
8895
+ }
8896
+ // ============================================================================
8897
+ // Private Methods
8898
+ // ============================================================================
8899
+ addMessage(input, runId) {
8900
+ const message = {
8901
+ ...input,
8902
+ seq: this.nextSeq++,
8903
+ createdAt: /* @__PURE__ */ new Date(),
8904
+ runId
8905
+ };
8906
+ this.messages.push(message);
8907
+ this.updatedAt = /* @__PURE__ */ new Date();
8908
+ return message;
8909
+ }
8910
+ buildInput() {
8911
+ return this.messages.map((m) => ({
8912
+ type: m.type,
8913
+ role: m.role,
8914
+ content: m.content,
8915
+ toolCalls: m.toolCalls,
8916
+ toolCallId: m.toolCallId
8917
+ }));
8511
8918
  }
8512
- };
8513
-
8514
- // src/token_providers.ts
8515
- function isReusable(token) {
8516
- if (!token.token) {
8517
- return false;
8919
+ resetRunState() {
8920
+ this.currentRunId = void 0;
8921
+ this.currentNodeId = void 0;
8922
+ this.currentWaiting = void 0;
8923
+ this.currentEvents = [];
8924
+ this.currentUsage = {
8925
+ inputTokens: 0,
8926
+ outputTokens: 0,
8927
+ totalTokens: 0,
8928
+ llmCalls: 0,
8929
+ toolCalls: 0
8930
+ };
8518
8931
  }
8519
- return token.expiresAt.getTime() - Date.now() > 6e4;
8520
- }
8521
- var FrontendTokenProvider = class {
8522
- constructor(cfg) {
8523
- const publishableKey = parsePublishableKey(cfg.publishableKey);
8524
- const http = new HTTPClient({
8525
- baseUrl: cfg.baseUrl || DEFAULT_BASE_URL,
8526
- fetchImpl: cfg.fetch,
8527
- clientHeader: cfg.clientHeader || DEFAULT_CLIENT_HEADER,
8528
- apiKey: publishableKey
8932
+ async processRunEvents(signal) {
8933
+ if (!this.currentRunId) {
8934
+ throw new Error("No current run");
8935
+ }
8936
+ const eventStream = await this.client.runs.events(this.currentRunId, {
8937
+ afterSeq: this.currentEvents.length
8529
8938
  });
8530
- this.publishableKey = publishableKey;
8531
- this.customer = cfg.customer;
8532
- this.auth = new AuthClient(http, { apiKey: publishableKey, customer: cfg.customer });
8533
- }
8534
- async getToken() {
8535
- if (!this.customer?.provider || !this.customer?.subject) {
8536
- throw new ConfigError("customer.provider and customer.subject are required");
8939
+ for await (const event of eventStream) {
8940
+ if (signal?.aborted) {
8941
+ return {
8942
+ status: "canceled",
8943
+ runId: this.currentRunId,
8944
+ usage: { ...this.currentUsage },
8945
+ events: [...this.currentEvents]
8946
+ };
8947
+ }
8948
+ this.currentEvents.push(event);
8949
+ switch (event.type) {
8950
+ case "node_llm_call":
8951
+ this.currentUsage = {
8952
+ ...this.currentUsage,
8953
+ llmCalls: this.currentUsage.llmCalls + 1,
8954
+ inputTokens: this.currentUsage.inputTokens + (event.llm_call.usage?.input_tokens || 0),
8955
+ outputTokens: this.currentUsage.outputTokens + (event.llm_call.usage?.output_tokens || 0),
8956
+ totalTokens: this.currentUsage.totalTokens + (event.llm_call.usage?.total_tokens || 0)
8957
+ };
8958
+ break;
8959
+ case "node_tool_call":
8960
+ this.currentUsage = {
8961
+ ...this.currentUsage,
8962
+ toolCalls: this.currentUsage.toolCalls + 1
8963
+ };
8964
+ break;
8965
+ case "node_waiting":
8966
+ this.currentNodeId = event.node_id;
8967
+ this.currentWaiting = event.waiting;
8968
+ if (this.toolRegistry && event.waiting.reason === "tool_results" && event.waiting.pending_tool_calls && event.waiting.pending_tool_calls.length > 0) {
8969
+ const results = await this.executeToolsLocally(
8970
+ event.waiting.pending_tool_calls
8971
+ );
8972
+ if (results) {
8973
+ return this.submitToolResults(results);
8974
+ }
8975
+ }
8976
+ if (event.waiting.reason === "tool_results" && event.waiting.pending_tool_calls) {
8977
+ return {
8978
+ status: "waiting_for_tools",
8979
+ pendingTools: event.waiting.pending_tool_calls.map(
8980
+ (tc) => ({
8981
+ toolCallId: tc.tool_call_id,
8982
+ name: tc.name,
8983
+ arguments: tc.arguments
8984
+ })
8985
+ ),
8986
+ runId: this.currentRunId,
8987
+ usage: { ...this.currentUsage },
8988
+ events: [...this.currentEvents]
8989
+ };
8990
+ }
8991
+ break;
8992
+ case "run_completed": {
8993
+ const runState2 = await this.client.runs.get(this.currentRunId);
8994
+ let output2;
8995
+ if (runState2.outputs && Array.isArray(runState2.outputs)) {
8996
+ output2 = runState2.outputs.filter((o) => o.type === "text").map((o) => o.text || "").join("");
8997
+ }
8998
+ if (output2) {
8999
+ this.addMessage(
9000
+ {
9001
+ type: "message",
9002
+ role: "assistant",
9003
+ content: [{ type: "text", text: output2 }]
9004
+ },
9005
+ this.currentRunId
9006
+ );
9007
+ }
9008
+ return {
9009
+ status: "complete",
9010
+ output: output2,
9011
+ runId: this.currentRunId,
9012
+ usage: { ...this.currentUsage },
9013
+ events: [...this.currentEvents]
9014
+ };
9015
+ }
9016
+ case "run_failed":
9017
+ return {
9018
+ status: "error",
9019
+ error: "Run failed",
9020
+ runId: this.currentRunId,
9021
+ usage: { ...this.currentUsage },
9022
+ events: [...this.currentEvents]
9023
+ };
9024
+ case "run_canceled":
9025
+ return {
9026
+ status: "canceled",
9027
+ runId: this.currentRunId,
9028
+ usage: { ...this.currentUsage },
9029
+ events: [...this.currentEvents]
9030
+ };
9031
+ }
8537
9032
  }
8538
- const reqBase = {
8539
- publishableKey: this.publishableKey,
8540
- identityProvider: this.customer.provider,
8541
- identitySubject: this.customer.subject,
8542
- deviceId: this.customer.deviceId,
8543
- ttlSeconds: this.customer.ttlSeconds
8544
- };
8545
- let token;
8546
- if (this.customer.email) {
8547
- const req = {
8548
- ...reqBase,
8549
- email: this.customer.email
8550
- };
8551
- token = await this.auth.frontendTokenAutoProvision(req);
8552
- } else {
8553
- const req = reqBase;
8554
- token = await this.auth.frontendToken(req);
9033
+ const runState = await this.client.runs.get(this.currentRunId);
9034
+ let output;
9035
+ if (runState.outputs && Array.isArray(runState.outputs)) {
9036
+ output = runState.outputs.filter((o) => o.type === "text").map((o) => o.text || "").join("");
8555
9037
  }
8556
- if (!token.token) {
8557
- throw new ConfigError("frontend token exchange returned an empty token");
9038
+ if (output) {
9039
+ this.addMessage(
9040
+ {
9041
+ type: "message",
9042
+ role: "assistant",
9043
+ content: [{ type: "text", text: output }]
9044
+ },
9045
+ this.currentRunId
9046
+ );
8558
9047
  }
8559
- return token.token;
8560
- }
8561
- };
8562
- var CustomerTokenProvider = class {
8563
- constructor(cfg) {
8564
- const key = parseSecretKey(cfg.secretKey);
8565
- const http = new HTTPClient({
8566
- baseUrl: cfg.baseUrl || DEFAULT_BASE_URL,
8567
- fetchImpl: cfg.fetch,
8568
- clientHeader: cfg.clientHeader || DEFAULT_CLIENT_HEADER,
8569
- apiKey: key
8570
- });
8571
- this.auth = new AuthClient(http, { apiKey: key });
8572
- this.req = cfg.request;
9048
+ return {
9049
+ status: "complete",
9050
+ output,
9051
+ runId: this.currentRunId,
9052
+ usage: { ...this.currentUsage },
9053
+ events: [...this.currentEvents]
9054
+ };
8573
9055
  }
8574
- async getToken() {
8575
- if (this.cached && isReusable(this.cached)) {
8576
- return this.cached.token;
9056
+ async executeToolsLocally(toolCalls) {
9057
+ if (!this.toolRegistry) return null;
9058
+ for (const tc of toolCalls) {
9059
+ if (!this.toolRegistry.has(tc.name)) {
9060
+ return null;
9061
+ }
8577
9062
  }
8578
- const token = await this.auth.customerToken(this.req);
8579
- this.cached = token;
8580
- return token.token;
9063
+ const results = [];
9064
+ for (const tc of toolCalls) {
9065
+ const toolCall = {
9066
+ id: tc.tool_call_id,
9067
+ type: "function",
9068
+ function: {
9069
+ name: tc.name,
9070
+ arguments: tc.arguments
9071
+ }
9072
+ };
9073
+ const result = await this.toolRegistry.execute(toolCall);
9074
+ results.push(result);
9075
+ }
9076
+ return results;
8581
9077
  }
8582
9078
  };
8583
- var OIDCExchangeTokenProvider = class {
8584
- constructor(cfg) {
8585
- const apiKey = parseApiKey(cfg.apiKey);
8586
- const http = new HTTPClient({
8587
- baseUrl: cfg.baseUrl || DEFAULT_BASE_URL,
8588
- fetchImpl: cfg.fetch,
8589
- clientHeader: cfg.clientHeader || DEFAULT_CLIENT_HEADER,
8590
- apiKey
8591
- });
8592
- this.auth = new AuthClient(http, { apiKey });
8593
- this.idTokenProvider = cfg.idTokenProvider;
8594
- this.request = { idToken: "", projectId: cfg.projectId };
8595
- }
8596
- async getToken() {
8597
- if (this.cached && isReusable(this.cached)) {
8598
- return this.cached.token;
9079
+ function getHTTPClient(client) {
9080
+ return client.http;
9081
+ }
9082
+ function mergeTools2(defaults, overrides) {
9083
+ if (!defaults && !overrides) return void 0;
9084
+ if (!defaults) return overrides;
9085
+ if (!overrides) return defaults;
9086
+ const merged = /* @__PURE__ */ new Map();
9087
+ for (const tool of defaults) {
9088
+ if (tool.type === "function" && tool.function) {
9089
+ merged.set(tool.function.name, tool);
8599
9090
  }
8600
- const idToken = (await this.idTokenProvider())?.trim();
8601
- if (!idToken) {
8602
- throw new ConfigError("idTokenProvider returned an empty id_token");
9091
+ }
9092
+ for (const tool of overrides) {
9093
+ if (tool.type === "function" && tool.function) {
9094
+ merged.set(tool.function.name, tool);
8603
9095
  }
8604
- const token = await this.auth.oidcExchange({ ...this.request, idToken });
8605
- this.cached = token;
8606
- return token.token;
8607
9096
  }
8608
- };
9097
+ return Array.from(merged.values());
9098
+ }
8609
9099
 
8610
- // src/device_flow.ts
8611
- async function startOAuthDeviceAuthorization(req) {
8612
- const deviceAuthorizationEndpoint = req.deviceAuthorizationEndpoint?.trim();
8613
- if (!deviceAuthorizationEndpoint) {
8614
- throw new ConfigError("deviceAuthorizationEndpoint is required");
9100
+ // src/sessions/client.ts
9101
+ var SessionsClient = class {
9102
+ constructor(modelRelay, http, auth) {
9103
+ this.modelRelay = modelRelay;
9104
+ this.http = http;
9105
+ this.auth = auth;
8615
9106
  }
8616
- const clientId = req.clientId?.trim();
8617
- if (!clientId) {
8618
- throw new ConfigError("clientId is required");
9107
+ // ============================================================================
9108
+ // Local Sessions (Client-Managed)
9109
+ // ============================================================================
9110
+ /**
9111
+ * Create a new local session.
9112
+ *
9113
+ * Local sessions keep history on the client side with optional persistence.
9114
+ * Use for privacy-sensitive workflows or offline-capable agents.
9115
+ *
9116
+ * @param options - Session configuration
9117
+ * @returns A new LocalSession instance
9118
+ *
9119
+ * @example
9120
+ * ```typescript
9121
+ * const session = client.sessions.createLocal({
9122
+ * toolRegistry: createLocalFSTools({ root: process.cwd() }),
9123
+ * persistence: "memory", // or "file", "sqlite"
9124
+ * });
9125
+ *
9126
+ * const result = await session.run("Create a hello world file");
9127
+ * ```
9128
+ */
9129
+ createLocal(options = {}) {
9130
+ return createLocalSession(this.modelRelay, options);
8619
9131
  }
8620
- const form = new URLSearchParams();
8621
- form.set("client_id", clientId);
8622
- if (req.scope?.trim()) {
8623
- form.set("scope", req.scope.trim());
9132
+ /**
9133
+ * Resume an existing local session from storage.
9134
+ *
9135
+ * @param sessionId - ID of the session to resume
9136
+ * @param options - Session configuration (must match original persistence settings)
9137
+ * @returns The resumed LocalSession, or null if not found
9138
+ *
9139
+ * @example
9140
+ * ```typescript
9141
+ * const session = await client.sessions.resumeLocal("session-id", {
9142
+ * persistence: "sqlite",
9143
+ * });
9144
+ *
9145
+ * if (session) {
9146
+ * console.log(`Resumed session with ${session.history.length} messages`);
9147
+ * const result = await session.run("Continue where we left off");
9148
+ * }
9149
+ * ```
9150
+ */
9151
+ async resumeLocal(sessionId, options = {}) {
9152
+ return LocalSession.resume(this.modelRelay, sessionId, options);
8624
9153
  }
8625
- if (req.audience?.trim()) {
8626
- form.set("audience", req.audience.trim());
9154
+ // ============================================================================
9155
+ // Remote Sessions (Server-Managed)
9156
+ // ============================================================================
9157
+ /**
9158
+ * Create a new remote session.
9159
+ *
9160
+ * Remote sessions store history on the server for cross-device continuity.
9161
+ * Use for browser-based agents or team collaboration.
9162
+ *
9163
+ * @param options - Session configuration
9164
+ * @returns A new RemoteSession instance
9165
+ *
9166
+ * @example
9167
+ * ```typescript
9168
+ * const session = await client.sessions.create({
9169
+ * metadata: { name: "Feature implementation" },
9170
+ * });
9171
+ *
9172
+ * const result = await session.run("Implement the login feature");
9173
+ * ```
9174
+ */
9175
+ async create(options = {}) {
9176
+ return RemoteSession.create(this.modelRelay, options);
8627
9177
  }
8628
- const payload = await postOAuthForm(deviceAuthorizationEndpoint, form, {
8629
- fetch: req.fetch,
8630
- signal: req.signal
8631
- });
8632
- const deviceCode = String(payload.device_code || "").trim();
8633
- const userCode = String(payload.user_code || "").trim();
8634
- const verificationUri = String(payload.verification_uri || payload.verification_uri_complete || "").trim();
8635
- const verificationUriComplete = String(payload.verification_uri_complete || "").trim() || void 0;
8636
- const expiresIn = Number(payload.expires_in || 0);
8637
- const intervalSeconds = Math.max(1, Number(payload.interval || 5));
8638
- if (!deviceCode || !userCode || !verificationUri || !expiresIn) {
8639
- throw new TransportError("oauth device authorization returned an invalid response", {
8640
- kind: "request",
8641
- cause: payload
9178
+ /**
9179
+ * Get an existing remote session by ID.
9180
+ *
9181
+ * @param sessionId - ID of the session to retrieve
9182
+ * @param options - Optional configuration (toolRegistry, defaults)
9183
+ * @returns The RemoteSession instance
9184
+ *
9185
+ * @example
9186
+ * ```typescript
9187
+ * const session = await client.sessions.get("session-id");
9188
+ * console.log(`Session has ${session.history.length} messages`);
9189
+ * ```
9190
+ */
9191
+ async get(sessionId, options = {}) {
9192
+ return RemoteSession.get(this.modelRelay, sessionId, options);
9193
+ }
9194
+ /**
9195
+ * List remote sessions.
9196
+ *
9197
+ * @param options - List options (limit, cursor, endUserId)
9198
+ * @returns Paginated list of session summaries
9199
+ *
9200
+ * @example
9201
+ * ```typescript
9202
+ * const { sessions, nextCursor } = await client.sessions.list({ limit: 10 });
9203
+ * for (const info of sessions) {
9204
+ * console.log(`Session ${info.id}: ${info.messageCount} messages`);
9205
+ * }
9206
+ * ```
9207
+ */
9208
+ async list(options = {}) {
9209
+ return RemoteSession.list(this.modelRelay, {
9210
+ limit: options.limit,
9211
+ offset: options.cursor ? parseInt(options.cursor, 10) : void 0,
9212
+ endUserId: options.endUserId
8642
9213
  });
8643
9214
  }
8644
- return {
8645
- deviceCode,
8646
- userCode,
8647
- verificationUri,
8648
- verificationUriComplete,
8649
- expiresAt: new Date(Date.now() + expiresIn * 1e3),
8650
- intervalSeconds
8651
- };
8652
- }
8653
- async function pollOAuthDeviceToken(req) {
8654
- const tokenEndpoint = req.tokenEndpoint?.trim();
8655
- if (!tokenEndpoint) {
8656
- throw new ConfigError("tokenEndpoint is required");
8657
- }
8658
- const clientId = req.clientId?.trim();
8659
- if (!clientId) {
8660
- throw new ConfigError("clientId is required");
9215
+ /**
9216
+ * Delete a remote session.
9217
+ *
9218
+ * Requires a secret key (not publishable key).
9219
+ *
9220
+ * @param sessionId - ID of the session to delete
9221
+ *
9222
+ * @example
9223
+ * ```typescript
9224
+ * await client.sessions.delete("session-id");
9225
+ * ```
9226
+ */
9227
+ async delete(sessionId) {
9228
+ return RemoteSession.delete(this.modelRelay, sessionId);
8661
9229
  }
8662
- const deviceCode = req.deviceCode?.trim();
8663
- if (!deviceCode) {
8664
- throw new ConfigError("deviceCode is required");
9230
+ };
9231
+
9232
+ // src/http.ts
9233
+ var HTTPClient = class {
9234
+ constructor(cfg) {
9235
+ const resolvedBase = normalizeBaseUrl(cfg.baseUrl || DEFAULT_BASE_URL);
9236
+ if (!isValidHttpUrl(resolvedBase)) {
9237
+ throw new ConfigError(
9238
+ "baseUrl must start with http:// or https://"
9239
+ );
9240
+ }
9241
+ this.baseUrl = resolvedBase;
9242
+ this.apiKey = cfg.apiKey ? parseApiKey(cfg.apiKey) : void 0;
9243
+ this.accessToken = cfg.accessToken?.trim();
9244
+ this.fetchImpl = cfg.fetchImpl;
9245
+ this.clientHeader = cfg.clientHeader?.trim() || DEFAULT_CLIENT_HEADER;
9246
+ this.defaultConnectTimeoutMs = cfg.connectTimeoutMs === void 0 ? DEFAULT_CONNECT_TIMEOUT_MS : Math.max(0, cfg.connectTimeoutMs);
9247
+ this.defaultTimeoutMs = cfg.timeoutMs === void 0 ? DEFAULT_REQUEST_TIMEOUT_MS : Math.max(0, cfg.timeoutMs);
9248
+ this.retry = normalizeRetryConfig(cfg.retry);
9249
+ this.defaultHeaders = normalizeHeaders(cfg.defaultHeaders);
9250
+ this.metrics = cfg.metrics;
9251
+ this.trace = cfg.trace;
8665
9252
  }
8666
- const deadline = req.deadline ?? new Date(Date.now() + 10 * 60 * 1e3);
8667
- let intervalMs = Math.max(1, req.intervalSeconds ?? 5) * 1e3;
8668
- while (true) {
8669
- if (Date.now() >= deadline.getTime()) {
8670
- throw new TransportError("oauth device flow timed out", { kind: "timeout" });
9253
+ async request(path2, options = {}) {
9254
+ const fetchFn = this.fetchImpl ?? globalThis.fetch;
9255
+ if (!fetchFn) {
9256
+ throw new ConfigError(
9257
+ "fetch is not available; provide a fetch implementation"
9258
+ );
8671
9259
  }
8672
- const form = new URLSearchParams();
8673
- form.set("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
8674
- form.set("device_code", deviceCode);
8675
- form.set("client_id", clientId);
8676
- const payload = await postOAuthForm(tokenEndpoint, form, {
8677
- fetch: req.fetch,
8678
- signal: req.signal,
8679
- allowErrorPayload: true
9260
+ const method = options.method || "GET";
9261
+ const url = buildUrl(this.baseUrl, path2);
9262
+ const metrics = mergeMetrics(this.metrics, options.metrics);
9263
+ const trace = mergeTrace(this.trace, options.trace);
9264
+ const context = {
9265
+ method,
9266
+ path: path2,
9267
+ ...options.context || {}
9268
+ };
9269
+ trace?.requestStart?.(context);
9270
+ const start = metrics?.httpRequest || trace?.requestFinish ? Date.now() : 0;
9271
+ const headers = new Headers({
9272
+ ...this.defaultHeaders,
9273
+ ...options.headers || {}
8680
9274
  });
8681
- const err = String(payload.error || "").trim();
8682
- if (err) {
8683
- switch (err) {
8684
- case "authorization_pending":
8685
- await sleep(intervalMs, req.signal);
8686
- continue;
8687
- case "slow_down":
8688
- intervalMs += 5e3;
8689
- await sleep(intervalMs, req.signal);
8690
- continue;
8691
- case "expired_token":
8692
- case "access_denied":
8693
- case "invalid_grant":
8694
- throw new TransportError(`oauth device flow failed: ${err}`, {
8695
- kind: "request",
8696
- cause: payload
9275
+ const accepts = options.accept || (options.raw ? void 0 : "application/json");
9276
+ if (accepts && !headers.has("Accept")) {
9277
+ headers.set("Accept", accepts);
9278
+ }
9279
+ const body = options.body;
9280
+ const shouldEncodeJSON = body !== void 0 && body !== null && typeof body === "object" && !(body instanceof FormData) && !(body instanceof Blob);
9281
+ const payload = shouldEncodeJSON ? JSON.stringify(body) : body;
9282
+ if (shouldEncodeJSON && !headers.has("Content-Type")) {
9283
+ headers.set("Content-Type", "application/json");
9284
+ }
9285
+ const accessToken = options.accessToken ?? this.accessToken;
9286
+ if (accessToken) {
9287
+ const bearer = accessToken.toLowerCase().startsWith("bearer ") ? accessToken : `Bearer ${accessToken}`;
9288
+ headers.set("Authorization", bearer);
9289
+ }
9290
+ const apiKey = options.apiKey ?? this.apiKey;
9291
+ if (apiKey) {
9292
+ headers.set("X-ModelRelay-Api-Key", apiKey);
9293
+ }
9294
+ if (this.clientHeader && !headers.has("X-ModelRelay-Client")) {
9295
+ headers.set("X-ModelRelay-Client", this.clientHeader);
9296
+ }
9297
+ const timeoutMs = options.useDefaultTimeout === false ? options.timeoutMs : options.timeoutMs ?? this.defaultTimeoutMs;
9298
+ const connectTimeoutMs = options.useDefaultConnectTimeout === false ? options.connectTimeoutMs : options.connectTimeoutMs ?? this.defaultConnectTimeoutMs;
9299
+ const retryCfg = normalizeRetryConfig(
9300
+ options.retry === void 0 ? this.retry : options.retry
9301
+ );
9302
+ const attempts = retryCfg ? Math.max(1, retryCfg.maxAttempts) : 1;
9303
+ let lastError;
9304
+ let lastStatus;
9305
+ for (let attempt = 1; attempt <= attempts; attempt++) {
9306
+ let connectTimedOut = false;
9307
+ let requestTimedOut = false;
9308
+ const connectController = connectTimeoutMs && connectTimeoutMs > 0 ? new AbortController() : void 0;
9309
+ const requestController = timeoutMs && timeoutMs > 0 ? new AbortController() : void 0;
9310
+ const signal = mergeSignals(
9311
+ options.signal,
9312
+ connectController?.signal,
9313
+ requestController?.signal
9314
+ );
9315
+ const connectTimer = connectController && setTimeout(() => {
9316
+ connectTimedOut = true;
9317
+ connectController.abort(
9318
+ new DOMException("connect timeout", "AbortError")
9319
+ );
9320
+ }, connectTimeoutMs);
9321
+ const requestTimer = requestController && setTimeout(() => {
9322
+ requestTimedOut = true;
9323
+ requestController.abort(
9324
+ new DOMException("timeout", "AbortError")
9325
+ );
9326
+ }, timeoutMs);
9327
+ try {
9328
+ const response = await fetchFn(url, {
9329
+ method,
9330
+ headers,
9331
+ body: payload,
9332
+ signal
9333
+ });
9334
+ if (connectTimer) {
9335
+ clearTimeout(connectTimer);
9336
+ }
9337
+ if (!response.ok) {
9338
+ const shouldRetry = retryCfg && shouldRetryStatus(
9339
+ response.status,
9340
+ method,
9341
+ retryCfg.retryPost
9342
+ ) && attempt < attempts;
9343
+ if (shouldRetry) {
9344
+ lastStatus = response.status;
9345
+ await backoff(attempt, retryCfg);
9346
+ continue;
9347
+ }
9348
+ const retries = buildRetryMetadata(attempt, response.status, lastError);
9349
+ const finishedCtx2 = withRequestId(context, response.headers);
9350
+ recordHttpMetrics(metrics, trace, start, retries, {
9351
+ status: response.status,
9352
+ context: finishedCtx2
8697
9353
  });
8698
- default:
8699
- throw new TransportError(`oauth device flow error: ${err}`, {
8700
- kind: "request",
8701
- cause: payload
9354
+ throw options.raw ? await parseErrorResponse(response, retries) : await parseErrorResponse(response, retries);
9355
+ }
9356
+ const finishedCtx = withRequestId(context, response.headers);
9357
+ recordHttpMetrics(metrics, trace, start, void 0, {
9358
+ status: response.status,
9359
+ context: finishedCtx
9360
+ });
9361
+ return response;
9362
+ } catch (err) {
9363
+ if (options.signal?.aborted) {
9364
+ throw err;
9365
+ }
9366
+ if (err instanceof ModelRelayError) {
9367
+ recordHttpMetrics(metrics, trace, start, void 0, {
9368
+ error: err,
9369
+ context
9370
+ });
9371
+ throw err;
9372
+ }
9373
+ const transportKind = classifyTransportErrorKind(
9374
+ err,
9375
+ connectTimedOut,
9376
+ requestTimedOut
9377
+ );
9378
+ const shouldRetry = retryCfg && isRetryableError(err, transportKind) && (method !== "POST" || retryCfg.retryPost) && attempt < attempts;
9379
+ if (!shouldRetry) {
9380
+ const retries = buildRetryMetadata(
9381
+ attempt,
9382
+ lastStatus,
9383
+ err instanceof Error ? err.message : String(err)
9384
+ );
9385
+ recordHttpMetrics(metrics, trace, start, retries, {
9386
+ error: err,
9387
+ context
8702
9388
  });
9389
+ throw toTransportError(err, transportKind, retries);
9390
+ }
9391
+ lastError = err;
9392
+ await backoff(attempt, retryCfg);
9393
+ } finally {
9394
+ if (connectTimer) {
9395
+ clearTimeout(connectTimer);
9396
+ }
9397
+ if (requestTimer) {
9398
+ clearTimeout(requestTimer);
9399
+ }
8703
9400
  }
8704
9401
  }
8705
- const accessToken = String(payload.access_token || "").trim() || void 0;
8706
- const idToken = String(payload.id_token || "").trim() || void 0;
8707
- const refreshToken = String(payload.refresh_token || "").trim() || void 0;
8708
- const tokenType = String(payload.token_type || "").trim() || void 0;
8709
- const scope = String(payload.scope || "").trim() || void 0;
8710
- const expiresIn = payload.expires_in !== void 0 ? Number(payload.expires_in) : void 0;
8711
- const expiresAt = typeof expiresIn === "number" && Number.isFinite(expiresIn) && expiresIn > 0 ? new Date(Date.now() + expiresIn * 1e3) : void 0;
8712
- if (!accessToken && !idToken) {
8713
- throw new TransportError("oauth device flow returned an invalid token response", {
8714
- kind: "request",
8715
- cause: payload
9402
+ throw lastError instanceof Error ? lastError : new TransportError("request failed", {
9403
+ kind: "other",
9404
+ retries: buildRetryMetadata(attempts, lastStatus)
9405
+ });
9406
+ }
9407
+ async json(path2, options = {}) {
9408
+ const response = await this.request(path2, {
9409
+ ...options,
9410
+ raw: true,
9411
+ accept: options.accept || "application/json"
9412
+ });
9413
+ if (!response.ok) {
9414
+ throw await parseErrorResponse(response);
9415
+ }
9416
+ if (response.status === 204) {
9417
+ return void 0;
9418
+ }
9419
+ try {
9420
+ return await response.json();
9421
+ } catch (err) {
9422
+ throw new APIError("failed to parse response JSON", {
9423
+ status: response.status,
9424
+ data: err
8716
9425
  });
8717
9426
  }
8718
- return { accessToken, idToken, refreshToken, tokenType, scope, expiresAt };
8719
9427
  }
9428
+ };
9429
+ function buildUrl(baseUrl, path2) {
9430
+ if (/^https?:\/\//i.test(path2)) {
9431
+ return path2;
9432
+ }
9433
+ if (!path2.startsWith("/")) {
9434
+ path2 = `/${path2}`;
9435
+ }
9436
+ return `${baseUrl}${path2}`;
8720
9437
  }
8721
- async function runOAuthDeviceFlowForIDToken(cfg) {
8722
- const auth = await startOAuthDeviceAuthorization({
8723
- deviceAuthorizationEndpoint: cfg.deviceAuthorizationEndpoint,
8724
- clientId: cfg.clientId,
8725
- scope: cfg.scope,
8726
- audience: cfg.audience,
8727
- fetch: cfg.fetch,
8728
- signal: cfg.signal
8729
- });
8730
- await cfg.onUserCode(auth);
8731
- const token = await pollOAuthDeviceToken({
8732
- tokenEndpoint: cfg.tokenEndpoint,
8733
- clientId: cfg.clientId,
8734
- deviceCode: auth.deviceCode,
8735
- intervalSeconds: auth.intervalSeconds,
8736
- deadline: auth.expiresAt,
8737
- fetch: cfg.fetch,
8738
- signal: cfg.signal
8739
- });
8740
- if (!token.idToken) {
8741
- throw new TransportError("oauth device flow did not return an id_token", {
8742
- kind: "request",
8743
- cause: token
8744
- });
9438
+ function normalizeBaseUrl(value) {
9439
+ const trimmed = value.trim();
9440
+ if (trimmed.endsWith("/")) {
9441
+ return trimmed.slice(0, -1);
8745
9442
  }
8746
- return token.idToken;
9443
+ return trimmed;
8747
9444
  }
8748
- async function postOAuthForm(url, form, opts) {
8749
- const fetchFn = opts.fetch ?? globalThis.fetch;
8750
- if (!fetchFn) {
8751
- throw new ConfigError("fetch is not available; provide a fetch implementation");
8752
- }
8753
- let resp;
8754
- try {
8755
- resp = await fetchFn(url, {
8756
- method: "POST",
8757
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
8758
- body: form.toString(),
8759
- signal: opts.signal
8760
- });
8761
- } catch (cause) {
8762
- throw new TransportError("oauth request failed", { kind: "request", cause });
8763
- }
8764
- let json;
8765
- try {
8766
- json = await resp.json();
8767
- } catch (cause) {
8768
- throw new TransportError("oauth response was not valid JSON", { kind: "request", cause });
8769
- }
8770
- if (!resp.ok && !opts.allowErrorPayload) {
8771
- throw new TransportError(`oauth request failed (${resp.status})`, {
8772
- kind: "request",
8773
- cause: json
8774
- });
8775
- }
8776
- return json || {};
9445
+ function isValidHttpUrl(value) {
9446
+ return /^https?:\/\//i.test(value);
8777
9447
  }
8778
- async function sleep(ms, signal) {
8779
- if (!ms || ms <= 0) {
8780
- return;
8781
- }
8782
- if (!signal) {
8783
- await new Promise((resolve) => setTimeout(resolve, ms));
8784
- return;
9448
+ function normalizeRetryConfig(retry) {
9449
+ if (retry === false) return void 0;
9450
+ const cfg = retry || {};
9451
+ return {
9452
+ maxAttempts: Math.max(1, cfg.maxAttempts ?? 3),
9453
+ baseBackoffMs: Math.max(0, cfg.baseBackoffMs ?? 300),
9454
+ maxBackoffMs: Math.max(0, cfg.maxBackoffMs ?? 5e3),
9455
+ retryPost: cfg.retryPost ?? true
9456
+ };
9457
+ }
9458
+ function shouldRetryStatus(status, method, retryPost) {
9459
+ if (status === 408 || status === 429) {
9460
+ return method !== "POST" || retryPost;
8785
9461
  }
8786
- if (signal.aborted) {
8787
- throw new TransportError("oauth device flow aborted", { kind: "request" });
9462
+ if (status >= 500 && status < 600) {
9463
+ return method !== "POST" || retryPost;
8788
9464
  }
8789
- await new Promise((resolve, reject) => {
8790
- const onAbort = () => {
8791
- signal.removeEventListener("abort", onAbort);
8792
- reject(new TransportError("oauth device flow aborted", { kind: "request" }));
8793
- };
8794
- signal.addEventListener("abort", onAbort);
8795
- setTimeout(() => {
8796
- signal.removeEventListener("abort", onAbort);
8797
- resolve();
8798
- }, ms);
8799
- });
8800
- }
8801
-
8802
- // src/workflow_builder.ts
8803
- function transformJSONValue(from, pointer) {
8804
- return pointer ? { from, pointer } : { from };
9465
+ return false;
8805
9466
  }
8806
- function transformJSONObject(object) {
8807
- return { object };
9467
+ function isRetryableError(err, kind) {
9468
+ if (!err) return false;
9469
+ if (kind === "timeout" || kind === "connect") return true;
9470
+ return err instanceof DOMException || err instanceof TypeError;
8808
9471
  }
8809
- function transformJSONMerge(merge) {
8810
- return { merge: merge.slice() };
9472
+ function backoff(attempt, cfg) {
9473
+ const exp = Math.max(0, attempt - 1);
9474
+ const base = cfg.baseBackoffMs * Math.pow(2, Math.min(exp, 10));
9475
+ const capped = Math.min(base, cfg.maxBackoffMs);
9476
+ const jitter = 0.5 + Math.random();
9477
+ const delay = Math.min(cfg.maxBackoffMs, capped * jitter);
9478
+ if (delay <= 0) return Promise.resolve();
9479
+ return new Promise((resolve2) => setTimeout(resolve2, delay));
8811
9480
  }
8812
- function wireRequest(req) {
8813
- const raw = req;
8814
- if (raw && typeof raw === "object") {
8815
- if ("input" in raw) {
8816
- return req;
8817
- }
8818
- if ("body" in raw) {
8819
- return raw.body ?? {};
9481
+ function mergeSignals(...signals) {
9482
+ const active = signals.filter(Boolean);
9483
+ if (active.length === 0) return void 0;
9484
+ if (active.length === 1) return active[0];
9485
+ const controller = new AbortController();
9486
+ for (const src of active) {
9487
+ if (src.aborted) {
9488
+ controller.abort(src.reason);
9489
+ break;
8820
9490
  }
9491
+ src.addEventListener(
9492
+ "abort",
9493
+ () => controller.abort(src.reason),
9494
+ { once: true }
9495
+ );
8821
9496
  }
8822
- return asInternal(req).body;
9497
+ return controller.signal;
8823
9498
  }
8824
- var WorkflowBuilderV0 = class _WorkflowBuilderV0 {
8825
- constructor(state = { nodes: [], edges: [], outputs: [] }) {
8826
- this.state = state;
9499
+ function normalizeHeaders(headers) {
9500
+ if (!headers) return {};
9501
+ const normalized = {};
9502
+ for (const [key, value] of Object.entries(headers)) {
9503
+ if (!key || !value) continue;
9504
+ const k = key.trim();
9505
+ const v = value.trim();
9506
+ if (k && v) {
9507
+ normalized[k] = v;
9508
+ }
8827
9509
  }
8828
- static new() {
8829
- return new _WorkflowBuilderV0();
9510
+ return normalized;
9511
+ }
9512
+ function buildRetryMetadata(attempt, lastStatus, lastError) {
9513
+ if (!attempt || attempt <= 1) return void 0;
9514
+ return {
9515
+ attempts: attempt,
9516
+ lastStatus,
9517
+ lastError: typeof lastError === "string" ? lastError : lastError instanceof Error ? lastError.message : lastError ? String(lastError) : void 0
9518
+ };
9519
+ }
9520
+ function classifyTransportErrorKind(err, connectTimedOut, requestTimedOut) {
9521
+ if (connectTimedOut) return "connect";
9522
+ if (requestTimedOut) return "timeout";
9523
+ if (err instanceof DOMException && err.name === "AbortError") {
9524
+ return requestTimedOut ? "timeout" : "request";
8830
9525
  }
8831
- with(patch) {
8832
- return new _WorkflowBuilderV0({
8833
- ...this.state,
8834
- ...patch
9526
+ if (err instanceof TypeError) return "request";
9527
+ return "other";
9528
+ }
9529
+ function toTransportError(err, kind, retries) {
9530
+ const message = err instanceof Error ? err.message : typeof err === "string" ? err : "request failed";
9531
+ return new TransportError(message, { kind, retries, cause: err });
9532
+ }
9533
+ function recordHttpMetrics(metrics, trace, start, retries, info) {
9534
+ if (!metrics?.httpRequest && !trace?.requestFinish) return;
9535
+ const latencyMs = start ? Date.now() - start : 0;
9536
+ if (metrics?.httpRequest) {
9537
+ metrics.httpRequest({
9538
+ latencyMs,
9539
+ status: info.status,
9540
+ error: info.error ? String(info.error) : void 0,
9541
+ retries,
9542
+ context: info.context
8835
9543
  });
8836
9544
  }
8837
- name(name) {
8838
- return this.with({ name: name.trim() || void 0 });
8839
- }
8840
- execution(execution) {
8841
- return this.with({ execution });
8842
- }
8843
- node(node) {
8844
- return this.with({ nodes: [...this.state.nodes, node] });
9545
+ trace?.requestFinish?.({
9546
+ context: info.context,
9547
+ status: info.status,
9548
+ error: info.error,
9549
+ retries,
9550
+ latencyMs
9551
+ });
9552
+ }
9553
+ function withRequestId(context, headers) {
9554
+ const requestId = headers.get("X-ModelRelay-Request-Id") || headers.get("X-Request-Id") || context.requestId;
9555
+ if (!requestId) return context;
9556
+ return { ...context, requestId };
9557
+ }
9558
+
9559
+ // src/customer_scoped.ts
9560
+ function normalizeCustomerId(customerId) {
9561
+ const trimmed = customerId?.trim?.() ? customerId.trim() : "";
9562
+ if (!trimmed) {
9563
+ throw new ConfigError("customerId is required");
8845
9564
  }
8846
- llmResponses(id, request, options = {}) {
8847
- const input = {
8848
- request: wireRequest(request),
8849
- ...options.stream === void 0 ? {} : { stream: options.stream },
8850
- ...options.toolExecution === void 0 ? {} : { tool_execution: { mode: options.toolExecution } },
8851
- ...options.toolLimits === void 0 ? {} : { tool_limits: { ...options.toolLimits } },
8852
- ...options.bindings === void 0 ? {} : { bindings: options.bindings.slice() }
8853
- };
8854
- return this.node({
8855
- id,
8856
- type: WorkflowNodeTypes.LLMResponses,
8857
- input
9565
+ return trimmed;
9566
+ }
9567
+ function mergeCustomerOptions(customerId, options = {}) {
9568
+ if (options.customerId && options.customerId !== customerId) {
9569
+ throw new ConfigError("customerId mismatch", {
9570
+ expected: customerId,
9571
+ received: options.customerId
8858
9572
  });
8859
9573
  }
8860
- joinAll(id) {
8861
- return this.node({ id, type: WorkflowNodeTypes.JoinAll });
8862
- }
8863
- transformJSON(id, input) {
8864
- return this.node({ id, type: WorkflowNodeTypes.TransformJSON, input });
9574
+ return { ...options, customerId };
9575
+ }
9576
+ var CustomerResponsesClient = class {
9577
+ constructor(base, customerId) {
9578
+ this.customerId = normalizeCustomerId(customerId);
9579
+ this.base = base;
8865
9580
  }
8866
- edge(from, to) {
8867
- return this.with({ edges: [...this.state.edges, { from, to }] });
9581
+ get id() {
9582
+ return this.customerId;
8868
9583
  }
8869
- output(name, from, pointer) {
8870
- return this.with({
8871
- outputs: [
8872
- ...this.state.outputs,
8873
- { name, from, ...pointer ? { pointer } : {} }
8874
- ]
8875
- });
9584
+ new() {
9585
+ return this.base.new().customerId(this.customerId);
8876
9586
  }
8877
- build() {
8878
- const edges = this.state.edges.slice().sort((a, b) => {
8879
- const af = String(a.from);
8880
- const bf = String(b.from);
8881
- if (af < bf) return -1;
8882
- if (af > bf) return 1;
8883
- const at = String(a.to);
8884
- const bt = String(b.to);
8885
- if (at < bt) return -1;
8886
- if (at > bt) return 1;
8887
- return 0;
8888
- });
8889
- const outputs = this.state.outputs.slice().sort((a, b) => {
8890
- const an = String(a.name);
8891
- const bn = String(b.name);
8892
- if (an < bn) return -1;
8893
- if (an > bn) return 1;
8894
- const af = String(a.from);
8895
- const bf = String(b.from);
8896
- if (af < bf) return -1;
8897
- if (af > bf) return 1;
8898
- const ap = a.pointer ?? "";
8899
- const bp = b.pointer ?? "";
8900
- if (ap < bp) return -1;
8901
- if (ap > bp) return 1;
8902
- return 0;
9587
+ ensureRequestCustomer(request) {
9588
+ const req = asInternal(request);
9589
+ const reqCustomer = req.options.customerId;
9590
+ if (reqCustomer && reqCustomer !== this.customerId) {
9591
+ throw new ConfigError("customerId mismatch", {
9592
+ expected: this.customerId,
9593
+ received: reqCustomer
9594
+ });
9595
+ }
9596
+ }
9597
+ async create(request, options = {}) {
9598
+ this.ensureRequestCustomer(request);
9599
+ return this.base.create(request, mergeCustomerOptions(this.customerId, options));
9600
+ }
9601
+ async stream(request, options = {}) {
9602
+ this.ensureRequestCustomer(request);
9603
+ return this.base.stream(request, mergeCustomerOptions(this.customerId, options));
9604
+ }
9605
+ async streamJSON(request, options = {}) {
9606
+ this.ensureRequestCustomer(request);
9607
+ return this.base.streamJSON(
9608
+ request,
9609
+ mergeCustomerOptions(this.customerId, options)
9610
+ );
9611
+ }
9612
+ async text(system, user, options = {}) {
9613
+ return this.base.textForCustomer(
9614
+ this.customerId,
9615
+ system,
9616
+ user,
9617
+ mergeCustomerOptions(this.customerId, options)
9618
+ );
9619
+ }
9620
+ async streamTextDeltas(system, user, options = {}) {
9621
+ return this.base.streamTextDeltasForCustomer(
9622
+ this.customerId,
9623
+ system,
9624
+ user,
9625
+ mergeCustomerOptions(this.customerId, options)
9626
+ );
9627
+ }
9628
+ };
9629
+ var CustomerScopedModelRelay = class {
9630
+ constructor(responses, customerId, baseUrl) {
9631
+ const normalized = normalizeCustomerId(customerId);
9632
+ this.responses = new CustomerResponsesClient(responses, normalized);
9633
+ this.customerId = normalized;
9634
+ this.baseUrl = baseUrl;
9635
+ }
9636
+ };
9637
+
9638
+ // src/token_providers.ts
9639
+ function isReusable(token) {
9640
+ if (!token.token) {
9641
+ return false;
9642
+ }
9643
+ return token.expiresAt.getTime() - Date.now() > 6e4;
9644
+ }
9645
+ var FrontendTokenProvider = class {
9646
+ constructor(cfg) {
9647
+ const publishableKey = parsePublishableKey(cfg.publishableKey);
9648
+ const http = new HTTPClient({
9649
+ baseUrl: cfg.baseUrl || DEFAULT_BASE_URL,
9650
+ fetchImpl: cfg.fetch,
9651
+ clientHeader: cfg.clientHeader || DEFAULT_CLIENT_HEADER,
9652
+ apiKey: publishableKey
8903
9653
  });
8904
- return {
8905
- kind: WorkflowKinds.WorkflowV0,
8906
- ...this.state.name ? { name: this.state.name } : {},
8907
- ...this.state.execution ? { execution: this.state.execution } : {},
8908
- nodes: this.state.nodes.slice(),
8909
- ...edges.length ? { edges } : {},
8910
- outputs
9654
+ this.publishableKey = publishableKey;
9655
+ this.customer = cfg.customer;
9656
+ this.auth = new AuthClient(http, { apiKey: publishableKey, customer: cfg.customer });
9657
+ }
9658
+ async getToken() {
9659
+ if (!this.customer?.provider || !this.customer?.subject) {
9660
+ throw new ConfigError("customer.provider and customer.subject are required");
9661
+ }
9662
+ const reqBase = {
9663
+ publishableKey: this.publishableKey,
9664
+ identityProvider: this.customer.provider,
9665
+ identitySubject: this.customer.subject,
9666
+ deviceId: this.customer.deviceId,
9667
+ ttlSeconds: this.customer.ttlSeconds
8911
9668
  };
9669
+ let token;
9670
+ if (this.customer.email) {
9671
+ const req = {
9672
+ ...reqBase,
9673
+ email: this.customer.email
9674
+ };
9675
+ token = await this.auth.frontendTokenAutoProvision(req);
9676
+ } else {
9677
+ const req = reqBase;
9678
+ token = await this.auth.frontendToken(req);
9679
+ }
9680
+ if (!token.token) {
9681
+ throw new ConfigError("frontend token exchange returned an empty token");
9682
+ }
9683
+ return token.token;
9684
+ }
9685
+ };
9686
+ var CustomerTokenProvider = class {
9687
+ constructor(cfg) {
9688
+ const key = parseSecretKey(cfg.secretKey);
9689
+ const http = new HTTPClient({
9690
+ baseUrl: cfg.baseUrl || DEFAULT_BASE_URL,
9691
+ fetchImpl: cfg.fetch,
9692
+ clientHeader: cfg.clientHeader || DEFAULT_CLIENT_HEADER,
9693
+ apiKey: key
9694
+ });
9695
+ this.auth = new AuthClient(http, { apiKey: key });
9696
+ this.req = cfg.request;
9697
+ }
9698
+ async getToken() {
9699
+ if (this.cached && isReusable(this.cached)) {
9700
+ return this.cached.token;
9701
+ }
9702
+ const token = await this.auth.customerToken(this.req);
9703
+ this.cached = token;
9704
+ return token.token;
9705
+ }
9706
+ };
9707
+ var OIDCExchangeTokenProvider = class {
9708
+ constructor(cfg) {
9709
+ const apiKey = parseApiKey(cfg.apiKey);
9710
+ const http = new HTTPClient({
9711
+ baseUrl: cfg.baseUrl || DEFAULT_BASE_URL,
9712
+ fetchImpl: cfg.fetch,
9713
+ clientHeader: cfg.clientHeader || DEFAULT_CLIENT_HEADER,
9714
+ apiKey
9715
+ });
9716
+ this.auth = new AuthClient(http, { apiKey });
9717
+ this.idTokenProvider = cfg.idTokenProvider;
9718
+ this.request = { idToken: "", projectId: cfg.projectId };
9719
+ }
9720
+ async getToken() {
9721
+ if (this.cached && isReusable(this.cached)) {
9722
+ return this.cached.token;
9723
+ }
9724
+ const idToken = (await this.idTokenProvider())?.trim();
9725
+ if (!idToken) {
9726
+ throw new ConfigError("idTokenProvider returned an empty id_token");
9727
+ }
9728
+ const token = await this.auth.oidcExchange({ ...this.request, idToken });
9729
+ this.cached = token;
9730
+ return token.token;
8912
9731
  }
8913
9732
  };
8914
- function workflowV0() {
8915
- return WorkflowBuilderV0.new();
8916
- }
8917
9733
 
8918
- // src/workflow_v0.schema.json
8919
- var workflow_v0_schema_default = {
8920
- $id: "https://modelrelay.ai/schemas/workflow_v0.schema.json",
8921
- $schema: "http://json-schema.org/draft-07/schema#",
8922
- additionalProperties: false,
8923
- definitions: {
8924
- edge: {
8925
- additionalProperties: false,
8926
- properties: {
8927
- from: {
8928
- minLength: 1,
8929
- type: "string"
8930
- },
8931
- to: {
8932
- minLength: 1,
8933
- type: "string"
8934
- }
8935
- },
8936
- required: [
8937
- "from",
8938
- "to"
8939
- ],
8940
- type: "object"
8941
- },
8942
- llmResponsesBinding: {
8943
- additionalProperties: false,
8944
- properties: {
8945
- encoding: {
8946
- enum: [
8947
- "json",
8948
- "json_string"
8949
- ],
8950
- type: "string"
8951
- },
8952
- from: {
8953
- minLength: 1,
8954
- type: "string"
8955
- },
8956
- pointer: {
8957
- pattern: "^(/.*)?$",
8958
- type: "string"
8959
- },
8960
- to: {
8961
- pattern: "^/.*$",
8962
- type: "string"
8963
- }
8964
- },
8965
- required: [
8966
- "from",
8967
- "to"
8968
- ],
8969
- type: "object"
8970
- },
8971
- node: {
8972
- additionalProperties: false,
8973
- oneOf: [
8974
- {
8975
- allOf: [
8976
- {
8977
- properties: {
8978
- input: {
8979
- properties: {
8980
- bindings: {
8981
- items: {
8982
- $ref: "#/definitions/llmResponsesBinding"
8983
- },
8984
- type: "array"
8985
- },
8986
- request: {
8987
- type: "object"
8988
- },
8989
- stream: {
8990
- type: "boolean"
8991
- },
8992
- tool_execution: {
8993
- additionalProperties: false,
8994
- properties: {
8995
- mode: {
8996
- default: "server",
8997
- enum: [
8998
- "server",
9734
+ // src/device_flow.ts
9735
+ async function startOAuthDeviceAuthorization(req) {
9736
+ const deviceAuthorizationEndpoint = req.deviceAuthorizationEndpoint?.trim();
9737
+ if (!deviceAuthorizationEndpoint) {
9738
+ throw new ConfigError("deviceAuthorizationEndpoint is required");
9739
+ }
9740
+ const clientId = req.clientId?.trim();
9741
+ if (!clientId) {
9742
+ throw new ConfigError("clientId is required");
9743
+ }
9744
+ const form = new URLSearchParams();
9745
+ form.set("client_id", clientId);
9746
+ if (req.scope?.trim()) {
9747
+ form.set("scope", req.scope.trim());
9748
+ }
9749
+ if (req.audience?.trim()) {
9750
+ form.set("audience", req.audience.trim());
9751
+ }
9752
+ const payload = await postOAuthForm(deviceAuthorizationEndpoint, form, {
9753
+ fetch: req.fetch,
9754
+ signal: req.signal
9755
+ });
9756
+ const deviceCode = String(payload.device_code || "").trim();
9757
+ const userCode = String(payload.user_code || "").trim();
9758
+ const verificationUri = String(payload.verification_uri || payload.verification_uri_complete || "").trim();
9759
+ const verificationUriComplete = String(payload.verification_uri_complete || "").trim() || void 0;
9760
+ const expiresIn = Number(payload.expires_in || 0);
9761
+ const intervalSeconds = Math.max(1, Number(payload.interval || 5));
9762
+ if (!deviceCode || !userCode || !verificationUri || !expiresIn) {
9763
+ throw new TransportError("oauth device authorization returned an invalid response", {
9764
+ kind: "request",
9765
+ cause: payload
9766
+ });
9767
+ }
9768
+ return {
9769
+ deviceCode,
9770
+ userCode,
9771
+ verificationUri,
9772
+ verificationUriComplete,
9773
+ expiresAt: new Date(Date.now() + expiresIn * 1e3),
9774
+ intervalSeconds
9775
+ };
9776
+ }
9777
+ async function pollOAuthDeviceToken(req) {
9778
+ const tokenEndpoint = req.tokenEndpoint?.trim();
9779
+ if (!tokenEndpoint) {
9780
+ throw new ConfigError("tokenEndpoint is required");
9781
+ }
9782
+ const clientId = req.clientId?.trim();
9783
+ if (!clientId) {
9784
+ throw new ConfigError("clientId is required");
9785
+ }
9786
+ const deviceCode = req.deviceCode?.trim();
9787
+ if (!deviceCode) {
9788
+ throw new ConfigError("deviceCode is required");
9789
+ }
9790
+ const deadline = req.deadline ?? new Date(Date.now() + 10 * 60 * 1e3);
9791
+ let intervalMs = Math.max(1, req.intervalSeconds ?? 5) * 1e3;
9792
+ while (true) {
9793
+ if (Date.now() >= deadline.getTime()) {
9794
+ throw new TransportError("oauth device flow timed out", { kind: "timeout" });
9795
+ }
9796
+ const form = new URLSearchParams();
9797
+ form.set("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
9798
+ form.set("device_code", deviceCode);
9799
+ form.set("client_id", clientId);
9800
+ const payload = await postOAuthForm(tokenEndpoint, form, {
9801
+ fetch: req.fetch,
9802
+ signal: req.signal,
9803
+ allowErrorPayload: true
9804
+ });
9805
+ const err = String(payload.error || "").trim();
9806
+ if (err) {
9807
+ switch (err) {
9808
+ case "authorization_pending":
9809
+ await sleep(intervalMs, req.signal);
9810
+ continue;
9811
+ case "slow_down":
9812
+ intervalMs += 5e3;
9813
+ await sleep(intervalMs, req.signal);
9814
+ continue;
9815
+ case "expired_token":
9816
+ case "access_denied":
9817
+ case "invalid_grant":
9818
+ throw new TransportError(`oauth device flow failed: ${err}`, {
9819
+ kind: "request",
9820
+ cause: payload
9821
+ });
9822
+ default:
9823
+ throw new TransportError(`oauth device flow error: ${err}`, {
9824
+ kind: "request",
9825
+ cause: payload
9826
+ });
9827
+ }
9828
+ }
9829
+ const accessToken = String(payload.access_token || "").trim() || void 0;
9830
+ const idToken = String(payload.id_token || "").trim() || void 0;
9831
+ const refreshToken = String(payload.refresh_token || "").trim() || void 0;
9832
+ const tokenType = String(payload.token_type || "").trim() || void 0;
9833
+ const scope = String(payload.scope || "").trim() || void 0;
9834
+ const expiresIn = payload.expires_in !== void 0 ? Number(payload.expires_in) : void 0;
9835
+ const expiresAt = typeof expiresIn === "number" && Number.isFinite(expiresIn) && expiresIn > 0 ? new Date(Date.now() + expiresIn * 1e3) : void 0;
9836
+ if (!accessToken && !idToken) {
9837
+ throw new TransportError("oauth device flow returned an invalid token response", {
9838
+ kind: "request",
9839
+ cause: payload
9840
+ });
9841
+ }
9842
+ return { accessToken, idToken, refreshToken, tokenType, scope, expiresAt };
9843
+ }
9844
+ }
9845
+ async function runOAuthDeviceFlowForIDToken(cfg) {
9846
+ const auth = await startOAuthDeviceAuthorization({
9847
+ deviceAuthorizationEndpoint: cfg.deviceAuthorizationEndpoint,
9848
+ clientId: cfg.clientId,
9849
+ scope: cfg.scope,
9850
+ audience: cfg.audience,
9851
+ fetch: cfg.fetch,
9852
+ signal: cfg.signal
9853
+ });
9854
+ await cfg.onUserCode(auth);
9855
+ const token = await pollOAuthDeviceToken({
9856
+ tokenEndpoint: cfg.tokenEndpoint,
9857
+ clientId: cfg.clientId,
9858
+ deviceCode: auth.deviceCode,
9859
+ intervalSeconds: auth.intervalSeconds,
9860
+ deadline: auth.expiresAt,
9861
+ fetch: cfg.fetch,
9862
+ signal: cfg.signal
9863
+ });
9864
+ if (!token.idToken) {
9865
+ throw new TransportError("oauth device flow did not return an id_token", {
9866
+ kind: "request",
9867
+ cause: token
9868
+ });
9869
+ }
9870
+ return token.idToken;
9871
+ }
9872
+ async function postOAuthForm(url, form, opts) {
9873
+ const fetchFn = opts.fetch ?? globalThis.fetch;
9874
+ if (!fetchFn) {
9875
+ throw new ConfigError("fetch is not available; provide a fetch implementation");
9876
+ }
9877
+ let resp;
9878
+ try {
9879
+ resp = await fetchFn(url, {
9880
+ method: "POST",
9881
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
9882
+ body: form.toString(),
9883
+ signal: opts.signal
9884
+ });
9885
+ } catch (cause) {
9886
+ throw new TransportError("oauth request failed", { kind: "request", cause });
9887
+ }
9888
+ let json;
9889
+ try {
9890
+ json = await resp.json();
9891
+ } catch (cause) {
9892
+ throw new TransportError("oauth response was not valid JSON", { kind: "request", cause });
9893
+ }
9894
+ if (!resp.ok && !opts.allowErrorPayload) {
9895
+ throw new TransportError(`oauth request failed (${resp.status})`, {
9896
+ kind: "request",
9897
+ cause: json
9898
+ });
9899
+ }
9900
+ return json || {};
9901
+ }
9902
+ async function sleep(ms, signal) {
9903
+ if (!ms || ms <= 0) {
9904
+ return;
9905
+ }
9906
+ if (!signal) {
9907
+ await new Promise((resolve2) => setTimeout(resolve2, ms));
9908
+ return;
9909
+ }
9910
+ if (signal.aborted) {
9911
+ throw new TransportError("oauth device flow aborted", { kind: "request" });
9912
+ }
9913
+ await new Promise((resolve2, reject) => {
9914
+ const onAbort = () => {
9915
+ signal.removeEventListener("abort", onAbort);
9916
+ reject(new TransportError("oauth device flow aborted", { kind: "request" }));
9917
+ };
9918
+ signal.addEventListener("abort", onAbort);
9919
+ setTimeout(() => {
9920
+ signal.removeEventListener("abort", onAbort);
9921
+ resolve2();
9922
+ }, ms);
9923
+ });
9924
+ }
9925
+
9926
+ // src/workflow_builder.ts
9927
+ function transformJSONValue(from, pointer) {
9928
+ return pointer ? { from, pointer } : { from };
9929
+ }
9930
+ function transformJSONObject(object) {
9931
+ return { object };
9932
+ }
9933
+ function transformJSONMerge(merge) {
9934
+ return { merge: merge.slice() };
9935
+ }
9936
+ function wireRequest(req) {
9937
+ const raw = req;
9938
+ if (raw && typeof raw === "object") {
9939
+ if ("input" in raw) {
9940
+ return req;
9941
+ }
9942
+ if ("body" in raw) {
9943
+ return raw.body ?? {};
9944
+ }
9945
+ }
9946
+ return asInternal(req).body;
9947
+ }
9948
+ var WorkflowBuilderV0 = class _WorkflowBuilderV0 {
9949
+ constructor(state = { nodes: [], edges: [], outputs: [] }) {
9950
+ this.state = state;
9951
+ }
9952
+ static new() {
9953
+ return new _WorkflowBuilderV0();
9954
+ }
9955
+ with(patch) {
9956
+ return new _WorkflowBuilderV0({
9957
+ ...this.state,
9958
+ ...patch
9959
+ });
9960
+ }
9961
+ name(name) {
9962
+ return this.with({ name: name.trim() || void 0 });
9963
+ }
9964
+ execution(execution) {
9965
+ return this.with({ execution });
9966
+ }
9967
+ node(node) {
9968
+ return this.with({ nodes: [...this.state.nodes, node] });
9969
+ }
9970
+ llmResponses(id, request, options = {}) {
9971
+ const input = {
9972
+ request: wireRequest(request),
9973
+ ...options.stream === void 0 ? {} : { stream: options.stream },
9974
+ ...options.toolExecution === void 0 ? {} : { tool_execution: { mode: options.toolExecution } },
9975
+ ...options.toolLimits === void 0 ? {} : { tool_limits: { ...options.toolLimits } },
9976
+ ...options.bindings === void 0 ? {} : { bindings: options.bindings.slice() }
9977
+ };
9978
+ return this.node({
9979
+ id,
9980
+ type: WorkflowNodeTypes.LLMResponses,
9981
+ input
9982
+ });
9983
+ }
9984
+ joinAll(id) {
9985
+ return this.node({ id, type: WorkflowNodeTypes.JoinAll });
9986
+ }
9987
+ transformJSON(id, input) {
9988
+ return this.node({ id, type: WorkflowNodeTypes.TransformJSON, input });
9989
+ }
9990
+ edge(from, to) {
9991
+ return this.with({ edges: [...this.state.edges, { from, to }] });
9992
+ }
9993
+ output(name, from, pointer) {
9994
+ return this.with({
9995
+ outputs: [
9996
+ ...this.state.outputs,
9997
+ { name, from, ...pointer ? { pointer } : {} }
9998
+ ]
9999
+ });
10000
+ }
10001
+ build() {
10002
+ const edges = this.state.edges.slice().sort((a, b) => {
10003
+ const af = String(a.from);
10004
+ const bf = String(b.from);
10005
+ if (af < bf) return -1;
10006
+ if (af > bf) return 1;
10007
+ const at = String(a.to);
10008
+ const bt = String(b.to);
10009
+ if (at < bt) return -1;
10010
+ if (at > bt) return 1;
10011
+ return 0;
10012
+ });
10013
+ const outputs = this.state.outputs.slice().sort((a, b) => {
10014
+ const an = String(a.name);
10015
+ const bn = String(b.name);
10016
+ if (an < bn) return -1;
10017
+ if (an > bn) return 1;
10018
+ const af = String(a.from);
10019
+ const bf = String(b.from);
10020
+ if (af < bf) return -1;
10021
+ if (af > bf) return 1;
10022
+ const ap = a.pointer ?? "";
10023
+ const bp = b.pointer ?? "";
10024
+ if (ap < bp) return -1;
10025
+ if (ap > bp) return 1;
10026
+ return 0;
10027
+ });
10028
+ return {
10029
+ kind: WorkflowKinds.WorkflowV0,
10030
+ ...this.state.name ? { name: this.state.name } : {},
10031
+ ...this.state.execution ? { execution: this.state.execution } : {},
10032
+ nodes: this.state.nodes.slice(),
10033
+ ...edges.length ? { edges } : {},
10034
+ outputs
10035
+ };
10036
+ }
10037
+ };
10038
+ function workflowV0() {
10039
+ return WorkflowBuilderV0.new();
10040
+ }
10041
+
10042
+ // src/workflow_v0.schema.json
10043
+ var workflow_v0_schema_default = {
10044
+ $id: "https://modelrelay.ai/schemas/workflow_v0.schema.json",
10045
+ $schema: "http://json-schema.org/draft-07/schema#",
10046
+ additionalProperties: false,
10047
+ definitions: {
10048
+ edge: {
10049
+ additionalProperties: false,
10050
+ properties: {
10051
+ from: {
10052
+ minLength: 1,
10053
+ type: "string"
10054
+ },
10055
+ to: {
10056
+ minLength: 1,
10057
+ type: "string"
10058
+ }
10059
+ },
10060
+ required: [
10061
+ "from",
10062
+ "to"
10063
+ ],
10064
+ type: "object"
10065
+ },
10066
+ llmResponsesBinding: {
10067
+ additionalProperties: false,
10068
+ properties: {
10069
+ encoding: {
10070
+ enum: [
10071
+ "json",
10072
+ "json_string"
10073
+ ],
10074
+ type: "string"
10075
+ },
10076
+ from: {
10077
+ minLength: 1,
10078
+ type: "string"
10079
+ },
10080
+ pointer: {
10081
+ pattern: "^(/.*)?$",
10082
+ type: "string"
10083
+ },
10084
+ to: {
10085
+ pattern: "^/.*$",
10086
+ type: "string"
10087
+ }
10088
+ },
10089
+ required: [
10090
+ "from",
10091
+ "to"
10092
+ ],
10093
+ type: "object"
10094
+ },
10095
+ node: {
10096
+ additionalProperties: false,
10097
+ oneOf: [
10098
+ {
10099
+ allOf: [
10100
+ {
10101
+ properties: {
10102
+ input: {
10103
+ properties: {
10104
+ bindings: {
10105
+ items: {
10106
+ $ref: "#/definitions/llmResponsesBinding"
10107
+ },
10108
+ type: "array"
10109
+ },
10110
+ request: {
10111
+ type: "object"
10112
+ },
10113
+ stream: {
10114
+ type: "boolean"
10115
+ },
10116
+ tool_execution: {
10117
+ additionalProperties: false,
10118
+ properties: {
10119
+ mode: {
10120
+ default: "server",
10121
+ enum: [
10122
+ "server",
8999
10123
  "client"
9000
10124
  ],
9001
10125
  type: "string"
@@ -9054,182 +10178,1499 @@ var workflow_v0_schema_default = {
9054
10178
  const: "join.all"
9055
10179
  }
9056
10180
  }
9057
- },
9058
- {
9059
- allOf: [
9060
- {
9061
- properties: {
9062
- input: {
9063
- additionalProperties: false,
9064
- oneOf: [
9065
- {
9066
- not: {
9067
- required: [
9068
- "merge"
9069
- ]
9070
- },
9071
- required: [
9072
- "object"
9073
- ]
9074
- },
9075
- {
9076
- not: {
9077
- required: [
9078
- "object"
9079
- ]
9080
- },
9081
- required: [
9082
- "merge"
9083
- ]
9084
- }
9085
- ],
9086
- properties: {
9087
- merge: {
9088
- items: {
9089
- $ref: "#/definitions/transformValue"
9090
- },
9091
- minItems: 1,
9092
- type: "array"
9093
- },
9094
- object: {
9095
- additionalProperties: {
9096
- $ref: "#/definitions/transformValue"
9097
- },
9098
- minProperties: 1,
9099
- type: "object"
9100
- }
9101
- },
9102
- type: "object"
9103
- }
10181
+ },
10182
+ {
10183
+ allOf: [
10184
+ {
10185
+ properties: {
10186
+ input: {
10187
+ additionalProperties: false,
10188
+ oneOf: [
10189
+ {
10190
+ not: {
10191
+ required: [
10192
+ "merge"
10193
+ ]
10194
+ },
10195
+ required: [
10196
+ "object"
10197
+ ]
10198
+ },
10199
+ {
10200
+ not: {
10201
+ required: [
10202
+ "object"
10203
+ ]
10204
+ },
10205
+ required: [
10206
+ "merge"
10207
+ ]
10208
+ }
10209
+ ],
10210
+ properties: {
10211
+ merge: {
10212
+ items: {
10213
+ $ref: "#/definitions/transformValue"
10214
+ },
10215
+ minItems: 1,
10216
+ type: "array"
10217
+ },
10218
+ object: {
10219
+ additionalProperties: {
10220
+ $ref: "#/definitions/transformValue"
10221
+ },
10222
+ minProperties: 1,
10223
+ type: "object"
10224
+ }
10225
+ },
10226
+ type: "object"
10227
+ }
10228
+ }
10229
+ }
10230
+ ],
10231
+ properties: {
10232
+ type: {
10233
+ const: "transform.json"
10234
+ }
10235
+ },
10236
+ required: [
10237
+ "input"
10238
+ ]
10239
+ }
10240
+ ],
10241
+ properties: {
10242
+ id: {
10243
+ minLength: 1,
10244
+ type: "string"
10245
+ },
10246
+ input: {},
10247
+ type: {
10248
+ enum: [
10249
+ "llm.responses",
10250
+ "join.all",
10251
+ "transform.json"
10252
+ ],
10253
+ type: "string"
10254
+ }
10255
+ },
10256
+ required: [
10257
+ "id",
10258
+ "type"
10259
+ ],
10260
+ type: "object"
10261
+ },
10262
+ output: {
10263
+ additionalProperties: false,
10264
+ properties: {
10265
+ from: {
10266
+ minLength: 1,
10267
+ type: "string"
10268
+ },
10269
+ name: {
10270
+ minLength: 1,
10271
+ type: "string"
10272
+ },
10273
+ pointer: {
10274
+ pattern: "^(/.*)?$",
10275
+ type: "string"
10276
+ }
10277
+ },
10278
+ required: [
10279
+ "name",
10280
+ "from"
10281
+ ],
10282
+ type: "object"
10283
+ },
10284
+ transformValue: {
10285
+ additionalProperties: false,
10286
+ properties: {
10287
+ from: {
10288
+ minLength: 1,
10289
+ type: "string"
10290
+ },
10291
+ pointer: {
10292
+ pattern: "^(/.*)?$",
10293
+ type: "string"
10294
+ }
10295
+ },
10296
+ required: [
10297
+ "from"
10298
+ ],
10299
+ type: "object"
10300
+ }
10301
+ },
10302
+ properties: {
10303
+ edges: {
10304
+ items: {
10305
+ $ref: "#/definitions/edge"
10306
+ },
10307
+ type: "array"
10308
+ },
10309
+ execution: {
10310
+ additionalProperties: false,
10311
+ properties: {
10312
+ max_parallelism: {
10313
+ minimum: 1,
10314
+ type: "integer"
10315
+ },
10316
+ node_timeout_ms: {
10317
+ minimum: 1,
10318
+ type: "integer"
10319
+ },
10320
+ run_timeout_ms: {
10321
+ minimum: 1,
10322
+ type: "integer"
10323
+ }
10324
+ },
10325
+ type: "object"
10326
+ },
10327
+ kind: {
10328
+ const: "workflow.v0",
10329
+ type: "string"
10330
+ },
10331
+ name: {
10332
+ type: "string"
10333
+ },
10334
+ nodes: {
10335
+ items: {
10336
+ $ref: "#/definitions/node"
10337
+ },
10338
+ minItems: 1,
10339
+ type: "array"
10340
+ },
10341
+ outputs: {
10342
+ items: {
10343
+ $ref: "#/definitions/output"
10344
+ },
10345
+ minItems: 1,
10346
+ type: "array"
10347
+ }
10348
+ },
10349
+ required: [
10350
+ "kind",
10351
+ "nodes",
10352
+ "outputs"
10353
+ ],
10354
+ title: "ModelRelay workflow.v0",
10355
+ type: "object"
10356
+ };
10357
+
10358
+ // src/tools_local_fs.ts
10359
+ var import_fs = require("fs");
10360
+ var path = __toESM(require("path"), 1);
10361
+ var import_child_process = require("child_process");
10362
+ var ToolNames = {
10363
+ FS_READ_FILE: "fs.read_file",
10364
+ FS_LIST_FILES: "fs.list_files",
10365
+ FS_SEARCH: "fs.search"
10366
+ };
10367
+ var FSDefaults = {
10368
+ MAX_READ_BYTES: 64e3,
10369
+ HARD_MAX_READ_BYTES: 1e6,
10370
+ MAX_LIST_ENTRIES: 2e3,
10371
+ HARD_MAX_LIST_ENTRIES: 2e4,
10372
+ MAX_SEARCH_MATCHES: 100,
10373
+ HARD_MAX_SEARCH_MATCHES: 2e3,
10374
+ SEARCH_TIMEOUT_MS: 5e3,
10375
+ MAX_SEARCH_BYTES_PER_FILE: 1e6
10376
+ };
10377
+ var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
10378
+ ".git",
10379
+ "node_modules",
10380
+ "vendor",
10381
+ "dist",
10382
+ "build",
10383
+ ".next",
10384
+ "target",
10385
+ ".idea",
10386
+ ".vscode",
10387
+ "__pycache__",
10388
+ ".pytest_cache",
10389
+ "coverage"
10390
+ ]);
10391
+ var LocalFSToolPack = class {
10392
+ constructor(options) {
10393
+ this.rgPath = null;
10394
+ this.rgChecked = false;
10395
+ const root = options.root?.trim();
10396
+ if (!root) {
10397
+ throw new Error("LocalFSToolPack: root directory required");
10398
+ }
10399
+ this.rootAbs = path.resolve(root);
10400
+ this.cfg = {
10401
+ ignoreDirs: options.ignoreDirs ?? new Set(DEFAULT_IGNORE_DIRS),
10402
+ maxReadBytes: options.maxReadBytes ?? FSDefaults.MAX_READ_BYTES,
10403
+ hardMaxReadBytes: options.hardMaxReadBytes ?? FSDefaults.HARD_MAX_READ_BYTES,
10404
+ maxListEntries: options.maxListEntries ?? FSDefaults.MAX_LIST_ENTRIES,
10405
+ hardMaxListEntries: options.hardMaxListEntries ?? FSDefaults.HARD_MAX_LIST_ENTRIES,
10406
+ maxSearchMatches: options.maxSearchMatches ?? FSDefaults.MAX_SEARCH_MATCHES,
10407
+ hardMaxSearchMatches: options.hardMaxSearchMatches ?? FSDefaults.HARD_MAX_SEARCH_MATCHES,
10408
+ searchTimeoutMs: options.searchTimeoutMs ?? FSDefaults.SEARCH_TIMEOUT_MS,
10409
+ maxSearchBytesPerFile: options.maxSearchBytesPerFile ?? FSDefaults.MAX_SEARCH_BYTES_PER_FILE
10410
+ };
10411
+ }
10412
+ /**
10413
+ * Returns the tool definitions for LLM requests.
10414
+ * Use these when constructing the tools array for /responses requests.
10415
+ */
10416
+ getToolDefinitions() {
10417
+ return [
10418
+ {
10419
+ type: "function",
10420
+ function: {
10421
+ name: ToolNames.FS_READ_FILE,
10422
+ description: "Read the contents of a file. Returns the file contents as UTF-8 text.",
10423
+ parameters: {
10424
+ type: "object",
10425
+ properties: {
10426
+ path: {
10427
+ type: "string",
10428
+ description: "Workspace-relative path to the file (e.g., 'src/index.ts')"
10429
+ },
10430
+ max_bytes: {
10431
+ type: "integer",
10432
+ description: `Maximum bytes to read. Default: ${this.cfg.maxReadBytes}, max: ${this.cfg.hardMaxReadBytes}`
10433
+ }
10434
+ },
10435
+ required: ["path"]
10436
+ }
10437
+ }
10438
+ },
10439
+ {
10440
+ type: "function",
10441
+ function: {
10442
+ name: ToolNames.FS_LIST_FILES,
10443
+ description: "List files recursively in a directory. Returns newline-separated workspace-relative paths.",
10444
+ parameters: {
10445
+ type: "object",
10446
+ properties: {
10447
+ path: {
10448
+ type: "string",
10449
+ description: "Workspace-relative directory path. Default: '.' (workspace root)"
10450
+ },
10451
+ max_entries: {
10452
+ type: "integer",
10453
+ description: `Maximum files to list. Default: ${this.cfg.maxListEntries}, max: ${this.cfg.hardMaxListEntries}`
9104
10454
  }
9105
10455
  }
9106
- ],
9107
- properties: {
9108
- type: {
9109
- const: "transform.json"
9110
- }
9111
- },
9112
- required: [
9113
- "input"
9114
- ]
10456
+ }
9115
10457
  }
9116
- ],
9117
- properties: {
9118
- id: {
9119
- minLength: 1,
9120
- type: "string"
9121
- },
9122
- input: {},
9123
- type: {
9124
- enum: [
9125
- "llm.responses",
9126
- "join.all",
9127
- "transform.json"
9128
- ],
9129
- type: "string"
10458
+ },
10459
+ {
10460
+ type: "function",
10461
+ function: {
10462
+ name: ToolNames.FS_SEARCH,
10463
+ description: "Search for text matching a regex pattern. Returns matches as 'path:line:content' format.",
10464
+ parameters: {
10465
+ type: "object",
10466
+ properties: {
10467
+ query: {
10468
+ type: "string",
10469
+ description: "Regex pattern to search for"
10470
+ },
10471
+ path: {
10472
+ type: "string",
10473
+ description: "Workspace-relative directory to search. Default: '.' (workspace root)"
10474
+ },
10475
+ max_matches: {
10476
+ type: "integer",
10477
+ description: `Maximum matches to return. Default: ${this.cfg.maxSearchMatches}, max: ${this.cfg.hardMaxSearchMatches}`
10478
+ }
10479
+ },
10480
+ required: ["query"]
10481
+ }
10482
+ }
10483
+ }
10484
+ ];
10485
+ }
10486
+ /**
10487
+ * Registers handlers into an existing ToolRegistry.
10488
+ * @param registry - The registry to register into
10489
+ * @returns The registry for chaining
10490
+ */
10491
+ registerInto(registry) {
10492
+ registry.register(ToolNames.FS_READ_FILE, this.readFile.bind(this));
10493
+ registry.register(ToolNames.FS_LIST_FILES, this.listFiles.bind(this));
10494
+ registry.register(ToolNames.FS_SEARCH, this.search.bind(this));
10495
+ return registry;
10496
+ }
10497
+ /**
10498
+ * Creates a new ToolRegistry with fs.* tools pre-registered.
10499
+ */
10500
+ toRegistry() {
10501
+ return this.registerInto(new ToolRegistry());
10502
+ }
10503
+ // ========================================================================
10504
+ // Tool Handlers
10505
+ // ========================================================================
10506
+ async readFile(_args, call) {
10507
+ const args = this.parseArgs(call, ["path"]);
10508
+ const func = call.function;
10509
+ const relPath = this.requireString(args, "path", call);
10510
+ const requestedMax = this.optionalPositiveInt(args, "max_bytes", call);
10511
+ let maxBytes = this.cfg.maxReadBytes;
10512
+ if (requestedMax !== void 0) {
10513
+ if (requestedMax > this.cfg.hardMaxReadBytes) {
10514
+ throw new ToolArgumentError({
10515
+ message: `max_bytes exceeds hard cap (${this.cfg.hardMaxReadBytes})`,
10516
+ toolCallId: call.id,
10517
+ toolName: func.name,
10518
+ rawArguments: func.arguments
10519
+ });
10520
+ }
10521
+ maxBytes = requestedMax;
10522
+ }
10523
+ const absPath = await this.resolveAndValidatePath(relPath, call);
10524
+ const stat = await import_fs.promises.stat(absPath);
10525
+ if (stat.isDirectory()) {
10526
+ throw new Error(`fs.read_file: path is a directory: ${relPath}`);
10527
+ }
10528
+ if (stat.size > maxBytes) {
10529
+ throw new Error(`fs.read_file: file exceeds max_bytes (${maxBytes})`);
10530
+ }
10531
+ const data = await import_fs.promises.readFile(absPath);
10532
+ if (!this.isValidUtf8(data)) {
10533
+ throw new Error(`fs.read_file: file is not valid UTF-8: ${relPath}`);
10534
+ }
10535
+ return data.toString("utf-8");
10536
+ }
10537
+ async listFiles(_args, call) {
10538
+ const args = this.parseArgs(call, []);
10539
+ const func = call.function;
10540
+ const startPath = this.optionalString(args, "path", call)?.trim() || ".";
10541
+ let maxEntries = this.cfg.maxListEntries;
10542
+ const requestedMax = this.optionalPositiveInt(args, "max_entries", call);
10543
+ if (requestedMax !== void 0) {
10544
+ if (requestedMax > this.cfg.hardMaxListEntries) {
10545
+ throw new ToolArgumentError({
10546
+ message: `max_entries exceeds hard cap (${this.cfg.hardMaxListEntries})`,
10547
+ toolCallId: call.id,
10548
+ toolName: func.name,
10549
+ rawArguments: func.arguments
10550
+ });
10551
+ }
10552
+ maxEntries = requestedMax;
10553
+ }
10554
+ const absPath = await this.resolveAndValidatePath(startPath, call);
10555
+ let rootReal;
10556
+ try {
10557
+ rootReal = await import_fs.promises.realpath(this.rootAbs);
10558
+ } catch {
10559
+ rootReal = this.rootAbs;
10560
+ }
10561
+ const stat = await import_fs.promises.stat(absPath);
10562
+ if (!stat.isDirectory()) {
10563
+ throw new Error(`fs.list_files: path is not a directory: ${startPath}`);
10564
+ }
10565
+ const files = [];
10566
+ await this.walkDir(absPath, async (filePath, dirent) => {
10567
+ if (files.length >= maxEntries) {
10568
+ return false;
10569
+ }
10570
+ if (dirent.isDirectory()) {
10571
+ if (this.cfg.ignoreDirs.has(dirent.name)) {
10572
+ return false;
10573
+ }
10574
+ return true;
10575
+ }
10576
+ if (dirent.isFile()) {
10577
+ const relPath = path.relative(rootReal, filePath);
10578
+ files.push(relPath.split(path.sep).join("/"));
10579
+ }
10580
+ return true;
10581
+ });
10582
+ return files.join("\n");
10583
+ }
10584
+ async search(_args, call) {
10585
+ const args = this.parseArgs(call, ["query"]);
10586
+ const func = call.function;
10587
+ const query = this.requireString(args, "query", call);
10588
+ const startPath = this.optionalString(args, "path", call)?.trim() || ".";
10589
+ let maxMatches = this.cfg.maxSearchMatches;
10590
+ const requestedMax = this.optionalPositiveInt(args, "max_matches", call);
10591
+ if (requestedMax !== void 0) {
10592
+ if (requestedMax > this.cfg.hardMaxSearchMatches) {
10593
+ throw new ToolArgumentError({
10594
+ message: `max_matches exceeds hard cap (${this.cfg.hardMaxSearchMatches})`,
10595
+ toolCallId: call.id,
10596
+ toolName: func.name,
10597
+ rawArguments: func.arguments
10598
+ });
10599
+ }
10600
+ maxMatches = requestedMax;
10601
+ }
10602
+ const absPath = await this.resolveAndValidatePath(startPath, call);
10603
+ const rgPath = await this.detectRipgrep();
10604
+ if (rgPath) {
10605
+ return this.searchWithRipgrep(
10606
+ rgPath,
10607
+ query,
10608
+ absPath,
10609
+ maxMatches
10610
+ );
10611
+ }
10612
+ return this.searchWithJS(query, absPath, maxMatches, call);
10613
+ }
10614
+ // ========================================================================
10615
+ // Path Safety
10616
+ // ========================================================================
10617
+ /**
10618
+ * Resolves a workspace-relative path and validates it stays within the sandbox.
10619
+ * @throws {ToolArgumentError} if path is invalid
10620
+ * @throws {PathEscapeError} if resolved path escapes root
10621
+ */
10622
+ async resolveAndValidatePath(relPath, call) {
10623
+ const func = call.function;
10624
+ const cleanRel = relPath.trim();
10625
+ if (!cleanRel) {
10626
+ throw new ToolArgumentError({
10627
+ message: "path cannot be empty",
10628
+ toolCallId: call.id,
10629
+ toolName: func.name,
10630
+ rawArguments: func.arguments
10631
+ });
10632
+ }
10633
+ if (path.isAbsolute(cleanRel)) {
10634
+ throw new ToolArgumentError({
10635
+ message: "path must be workspace-relative (not absolute)",
10636
+ toolCallId: call.id,
10637
+ toolName: func.name,
10638
+ rawArguments: func.arguments
10639
+ });
10640
+ }
10641
+ const normalized = path.normalize(cleanRel);
10642
+ if (normalized.startsWith("..") || normalized.startsWith(`.${path.sep}..`)) {
10643
+ throw new ToolArgumentError({
10644
+ message: "path must not escape the workspace root",
10645
+ toolCallId: call.id,
10646
+ toolName: func.name,
10647
+ rawArguments: func.arguments
10648
+ });
10649
+ }
10650
+ const target = path.join(this.rootAbs, normalized);
10651
+ let rootReal;
10652
+ try {
10653
+ rootReal = await import_fs.promises.realpath(this.rootAbs);
10654
+ } catch {
10655
+ rootReal = this.rootAbs;
10656
+ }
10657
+ let resolved;
10658
+ try {
10659
+ resolved = await import_fs.promises.realpath(target);
10660
+ } catch (err) {
10661
+ resolved = path.join(rootReal, normalized);
10662
+ }
10663
+ const relFromRoot = path.relative(rootReal, resolved);
10664
+ if (relFromRoot.startsWith("..") || relFromRoot.startsWith(`.${path.sep}..`) || path.isAbsolute(relFromRoot)) {
10665
+ throw new PathEscapeError({
10666
+ requestedPath: relPath,
10667
+ resolvedPath: resolved
10668
+ });
10669
+ }
10670
+ return resolved;
10671
+ }
10672
+ // ========================================================================
10673
+ // Ripgrep Search
10674
+ // ========================================================================
10675
+ async detectRipgrep() {
10676
+ if (this.rgChecked) {
10677
+ return this.rgPath;
10678
+ }
10679
+ this.rgChecked = true;
10680
+ return new Promise((resolve2) => {
10681
+ const proc = (0, import_child_process.spawn)("rg", ["--version"], { stdio: "ignore" });
10682
+ proc.on("error", () => {
10683
+ this.rgPath = null;
10684
+ resolve2(null);
10685
+ });
10686
+ proc.on("close", (code) => {
10687
+ if (code === 0) {
10688
+ this.rgPath = "rg";
10689
+ resolve2("rg");
10690
+ } else {
10691
+ this.rgPath = null;
10692
+ resolve2(null);
10693
+ }
10694
+ });
10695
+ });
10696
+ }
10697
+ async searchWithRipgrep(rgPath, query, dirAbs, maxMatches) {
10698
+ return new Promise((resolve2, reject) => {
10699
+ const args = ["--line-number", "--no-heading", "--color=never"];
10700
+ for (const name of this.cfg.ignoreDirs) {
10701
+ args.push("--glob", `!**/${name}/**`);
10702
+ }
10703
+ args.push(query, dirAbs);
10704
+ const proc = (0, import_child_process.spawn)(rgPath, args, {
10705
+ timeout: this.cfg.searchTimeoutMs
10706
+ });
10707
+ const lines = [];
10708
+ let stderr = "";
10709
+ let killed = false;
10710
+ proc.stdout.on("data", (chunk) => {
10711
+ if (killed) return;
10712
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
10713
+ const newLines = text.split("\n").filter((l) => l.trim());
10714
+ for (const line of newLines) {
10715
+ lines.push(this.normalizeRipgrepLine(line));
10716
+ if (lines.length >= maxMatches) {
10717
+ killed = true;
10718
+ proc.kill();
10719
+ break;
10720
+ }
10721
+ }
10722
+ });
10723
+ proc.stderr.on("data", (chunk) => {
10724
+ stderr += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
10725
+ });
10726
+ proc.on("error", (err) => {
10727
+ reject(new Error(`fs.search: ripgrep error: ${err.message}`));
10728
+ });
10729
+ proc.on("close", (code) => {
10730
+ if (killed) {
10731
+ resolve2(lines.join("\n"));
10732
+ return;
10733
+ }
10734
+ if (code === 0 || code === 1) {
10735
+ resolve2(lines.join("\n"));
10736
+ } else if (code === 2 && stderr.toLowerCase().includes("regex")) {
10737
+ reject(
10738
+ new ToolArgumentError({
10739
+ message: `invalid query regex: ${stderr.trim()}`,
10740
+ toolCallId: "",
10741
+ toolName: ToolNames.FS_SEARCH,
10742
+ rawArguments: ""
10743
+ })
10744
+ );
10745
+ } else if (stderr) {
10746
+ reject(new Error(`fs.search: ripgrep failed: ${stderr.trim()}`));
10747
+ } else {
10748
+ resolve2(lines.join("\n"));
10749
+ }
10750
+ });
10751
+ });
10752
+ }
10753
+ normalizeRipgrepLine(line) {
10754
+ const trimmed = line.trim();
10755
+ if (!trimmed || !trimmed.includes(":")) {
10756
+ return trimmed;
10757
+ }
10758
+ const colonIdx = trimmed.indexOf(":");
10759
+ const filePath = trimmed.slice(0, colonIdx);
10760
+ const rest = trimmed.slice(colonIdx + 1);
10761
+ if (path.isAbsolute(filePath)) {
10762
+ const rel = path.relative(this.rootAbs, filePath);
10763
+ if (!rel.startsWith("..")) {
10764
+ return rel.split(path.sep).join("/") + ":" + rest;
10765
+ }
10766
+ }
10767
+ return filePath.split(path.sep).join("/") + ":" + rest;
10768
+ }
10769
+ // ========================================================================
10770
+ // JavaScript Fallback Search
10771
+ // ========================================================================
10772
+ async searchWithJS(query, dirAbs, maxMatches, call) {
10773
+ const func = call.function;
10774
+ let regex;
10775
+ try {
10776
+ regex = new RegExp(query);
10777
+ } catch (err) {
10778
+ throw new ToolArgumentError({
10779
+ message: `invalid query regex: ${err.message}`,
10780
+ toolCallId: call.id,
10781
+ toolName: func.name,
10782
+ rawArguments: func.arguments
10783
+ });
10784
+ }
10785
+ const matches = [];
10786
+ const deadline = Date.now() + this.cfg.searchTimeoutMs;
10787
+ await this.walkDir(dirAbs, async (filePath, dirent) => {
10788
+ if (Date.now() > deadline) {
10789
+ return false;
10790
+ }
10791
+ if (matches.length >= maxMatches) {
10792
+ return false;
10793
+ }
10794
+ if (dirent.isDirectory()) {
10795
+ if (this.cfg.ignoreDirs.has(dirent.name)) {
10796
+ return false;
10797
+ }
10798
+ return true;
10799
+ }
10800
+ if (!dirent.isFile()) {
10801
+ return true;
10802
+ }
10803
+ try {
10804
+ const stat = await import_fs.promises.stat(filePath);
10805
+ if (stat.size > this.cfg.maxSearchBytesPerFile) {
10806
+ return true;
10807
+ }
10808
+ } catch {
10809
+ return true;
10810
+ }
10811
+ try {
10812
+ const content = await import_fs.promises.readFile(filePath, "utf-8");
10813
+ const lines = content.split("\n");
10814
+ for (let i = 0; i < lines.length && matches.length < maxMatches; i++) {
10815
+ if (regex.test(lines[i])) {
10816
+ const relPath = path.relative(this.rootAbs, filePath);
10817
+ const normalizedPath = relPath.split(path.sep).join("/");
10818
+ matches.push(`${normalizedPath}:${i + 1}:${lines[i]}`);
10819
+ }
10820
+ }
10821
+ } catch {
10822
+ }
10823
+ return true;
10824
+ });
10825
+ return matches.join("\n");
10826
+ }
10827
+ // ========================================================================
10828
+ // Helpers
10829
+ // ========================================================================
10830
+ parseArgs(call, required) {
10831
+ const func = call.function;
10832
+ if (!func) {
10833
+ throw new ToolArgumentError({
10834
+ message: "tool call missing function",
10835
+ toolCallId: call.id,
10836
+ toolName: "",
10837
+ rawArguments: ""
10838
+ });
10839
+ }
10840
+ const rawArgs = func.arguments || "{}";
10841
+ let parsed;
10842
+ try {
10843
+ parsed = JSON.parse(rawArgs);
10844
+ } catch (err) {
10845
+ throw new ToolArgumentError({
10846
+ message: `invalid JSON arguments: ${err.message}`,
10847
+ toolCallId: call.id,
10848
+ toolName: func.name,
10849
+ rawArguments: rawArgs
10850
+ });
10851
+ }
10852
+ if (typeof parsed !== "object" || parsed === null) {
10853
+ throw new ToolArgumentError({
10854
+ message: "arguments must be an object",
10855
+ toolCallId: call.id,
10856
+ toolName: func.name,
10857
+ rawArguments: rawArgs
10858
+ });
10859
+ }
10860
+ const args = parsed;
10861
+ for (const key of required) {
10862
+ const value = args[key];
10863
+ if (value === void 0 || value === null || value === "") {
10864
+ throw new ToolArgumentError({
10865
+ message: `${key} is required`,
10866
+ toolCallId: call.id,
10867
+ toolName: func.name,
10868
+ rawArguments: rawArgs
10869
+ });
10870
+ }
10871
+ }
10872
+ return args;
10873
+ }
10874
+ toolArgumentError(call, message) {
10875
+ const func = call.function;
10876
+ throw new ToolArgumentError({
10877
+ message,
10878
+ toolCallId: call.id,
10879
+ toolName: func?.name ?? "",
10880
+ rawArguments: func?.arguments ?? ""
10881
+ });
10882
+ }
10883
+ requireString(args, key, call) {
10884
+ const value = args[key];
10885
+ if (typeof value !== "string") {
10886
+ this.toolArgumentError(call, `${key} must be a string`);
10887
+ }
10888
+ if (value.trim() === "") {
10889
+ this.toolArgumentError(call, `${key} is required`);
10890
+ }
10891
+ return value;
10892
+ }
10893
+ optionalString(args, key, call) {
10894
+ const value = args[key];
10895
+ if (value === void 0 || value === null) {
10896
+ return void 0;
10897
+ }
10898
+ if (typeof value !== "string") {
10899
+ this.toolArgumentError(call, `${key} must be a string`);
10900
+ }
10901
+ const trimmed = value.trim();
10902
+ if (trimmed === "") {
10903
+ return void 0;
10904
+ }
10905
+ return value;
10906
+ }
10907
+ optionalPositiveInt(args, key, call) {
10908
+ const value = args[key];
10909
+ if (value === void 0 || value === null) {
10910
+ return void 0;
10911
+ }
10912
+ if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) {
10913
+ this.toolArgumentError(call, `${key} must be an integer`);
10914
+ }
10915
+ if (value <= 0) {
10916
+ this.toolArgumentError(call, `${key} must be > 0`);
10917
+ }
10918
+ return value;
10919
+ }
10920
+ isValidUtf8(buffer) {
10921
+ try {
10922
+ const text = buffer.toString("utf-8");
10923
+ return !text.includes("\uFFFD");
10924
+ } catch {
10925
+ return false;
10926
+ }
10927
+ }
10928
+ /**
10929
+ * Recursively walks a directory, calling visitor for each entry.
10930
+ * Visitor returns true to continue, false to skip (for dirs) or stop.
10931
+ */
10932
+ async walkDir(dir, visitor) {
10933
+ const entries = await import_fs.promises.readdir(dir, { withFileTypes: true });
10934
+ for (const entry of entries) {
10935
+ const fullPath = path.join(dir, entry.name);
10936
+ const shouldContinue = await visitor(fullPath, entry);
10937
+ if (!shouldContinue) {
10938
+ if (entry.isDirectory()) {
10939
+ continue;
10940
+ }
10941
+ return;
10942
+ }
10943
+ if (entry.isDirectory()) {
10944
+ await this.walkDir(fullPath, visitor);
10945
+ }
10946
+ }
10947
+ }
10948
+ };
10949
+ function createLocalFSToolPack(options) {
10950
+ return new LocalFSToolPack(options);
10951
+ }
10952
+ function createLocalFSTools(options) {
10953
+ return createLocalFSToolPack(options).toRegistry();
10954
+ }
10955
+
10956
+ // src/tools_browser.ts
10957
+ var BrowserToolNames = {
10958
+ /** Navigate to a URL and return accessibility tree */
10959
+ NAVIGATE: "browser.navigate",
10960
+ /** Click an element by accessible name/role */
10961
+ CLICK: "browser.click",
10962
+ /** Type text into an input field */
10963
+ TYPE: "browser.type",
10964
+ /** Get current accessibility tree */
10965
+ SNAPSHOT: "browser.snapshot",
10966
+ /** Scroll the page */
10967
+ SCROLL: "browser.scroll",
10968
+ /** Capture a screenshot */
10969
+ SCREENSHOT: "browser.screenshot",
10970
+ /** Extract data using CSS selectors */
10971
+ EXTRACT: "browser.extract"
10972
+ };
10973
+ var BrowserDefaults = {
10974
+ /** Navigation timeout in milliseconds */
10975
+ NAVIGATION_TIMEOUT_MS: 3e4,
10976
+ /** Action timeout in milliseconds */
10977
+ ACTION_TIMEOUT_MS: 5e3,
10978
+ /** Maximum nodes to include in accessibility tree output */
10979
+ MAX_SNAPSHOT_NODES: 500,
10980
+ /** Maximum screenshot size in bytes */
10981
+ MAX_SCREENSHOT_BYTES: 5e6
10982
+ };
10983
+ var BrowserToolPack = class {
10984
+ constructor(options = {}) {
10985
+ this.browser = null;
10986
+ this.context = null;
10987
+ this.page = null;
10988
+ this.cdpSession = null;
10989
+ this.ownsBrowser = false;
10990
+ this.ownsContext = false;
10991
+ this.cfg = {
10992
+ allowedDomains: options.allowedDomains ?? [],
10993
+ blockedDomains: options.blockedDomains ?? [],
10994
+ navigationTimeoutMs: options.navigationTimeoutMs ?? BrowserDefaults.NAVIGATION_TIMEOUT_MS,
10995
+ actionTimeoutMs: options.actionTimeoutMs ?? BrowserDefaults.ACTION_TIMEOUT_MS,
10996
+ maxSnapshotNodes: options.maxSnapshotNodes ?? BrowserDefaults.MAX_SNAPSHOT_NODES,
10997
+ headless: options.headless ?? true
10998
+ };
10999
+ if (options.browser) {
11000
+ this.browser = options.browser;
11001
+ this.ownsBrowser = false;
11002
+ }
11003
+ if (options.context) {
11004
+ this.context = options.context;
11005
+ this.ownsContext = false;
11006
+ }
11007
+ }
11008
+ /**
11009
+ * Initialize the browser. Must be called before using any tools.
11010
+ */
11011
+ async initialize() {
11012
+ if (this.page) {
11013
+ return;
11014
+ }
11015
+ const { chromium } = await import("playwright");
11016
+ if (!this.browser) {
11017
+ this.browser = await chromium.launch({
11018
+ headless: this.cfg.headless
11019
+ });
11020
+ this.ownsBrowser = true;
11021
+ }
11022
+ if (!this.context) {
11023
+ this.context = await this.browser.newContext();
11024
+ this.ownsContext = true;
11025
+ }
11026
+ this.page = await this.context.newPage();
11027
+ }
11028
+ /**
11029
+ * Close the browser and clean up resources.
11030
+ */
11031
+ async close() {
11032
+ if (this.cdpSession) {
11033
+ await this.cdpSession.detach().catch(() => {
11034
+ });
11035
+ this.cdpSession = null;
11036
+ }
11037
+ if (this.page) {
11038
+ await this.page.close().catch(() => {
11039
+ });
11040
+ this.page = null;
11041
+ }
11042
+ if (this.ownsContext && this.context) {
11043
+ await this.context.close().catch(() => {
11044
+ });
11045
+ this.context = null;
11046
+ }
11047
+ if (this.ownsBrowser && this.browser) {
11048
+ await this.browser.close().catch(() => {
11049
+ });
11050
+ this.browser = null;
11051
+ }
11052
+ }
11053
+ /**
11054
+ * Get tool definitions for use with LLM APIs.
11055
+ */
11056
+ getToolDefinitions() {
11057
+ return [
11058
+ {
11059
+ type: ToolTypes.Function,
11060
+ function: {
11061
+ name: BrowserToolNames.NAVIGATE,
11062
+ description: "Navigate to a URL and return the page's accessibility tree. The tree shows interactive elements (buttons, links, inputs) with their accessible names.",
11063
+ parameters: {
11064
+ type: "object",
11065
+ properties: {
11066
+ url: {
11067
+ type: "string",
11068
+ description: "The URL to navigate to (must be http/https)"
11069
+ },
11070
+ waitUntil: {
11071
+ type: "string",
11072
+ enum: ["load", "domcontentloaded", "networkidle"],
11073
+ description: "When to consider navigation complete. Default: domcontentloaded"
11074
+ }
11075
+ },
11076
+ required: ["url"]
11077
+ }
9130
11078
  }
9131
11079
  },
9132
- required: [
9133
- "id",
9134
- "type"
9135
- ],
9136
- type: "object"
9137
- },
9138
- output: {
9139
- additionalProperties: false,
9140
- properties: {
9141
- from: {
9142
- minLength: 1,
9143
- type: "string"
9144
- },
9145
- name: {
9146
- minLength: 1,
9147
- type: "string"
9148
- },
9149
- pointer: {
9150
- pattern: "^(/.*)?$",
9151
- type: "string"
11080
+ {
11081
+ type: ToolTypes.Function,
11082
+ function: {
11083
+ name: BrowserToolNames.CLICK,
11084
+ description: "Click an element by its accessible name. Returns updated accessibility tree.",
11085
+ parameters: {
11086
+ type: "object",
11087
+ properties: {
11088
+ name: {
11089
+ type: "string",
11090
+ description: "The accessible name of the element (from button text, aria-label, etc.)"
11091
+ },
11092
+ role: {
11093
+ type: "string",
11094
+ enum: [
11095
+ "button",
11096
+ "link",
11097
+ "menuitem",
11098
+ "checkbox",
11099
+ "radio",
11100
+ "tab"
11101
+ ],
11102
+ description: "ARIA role to match. If omitted, searches buttons, links, and menuitems."
11103
+ }
11104
+ },
11105
+ required: ["name"]
11106
+ }
9152
11107
  }
9153
11108
  },
9154
- required: [
9155
- "name",
9156
- "from"
9157
- ],
9158
- type: "object"
9159
- },
9160
- transformValue: {
9161
- additionalProperties: false,
9162
- properties: {
9163
- from: {
9164
- minLength: 1,
9165
- type: "string"
9166
- },
9167
- pointer: {
9168
- pattern: "^(/.*)?$",
9169
- type: "string"
11109
+ {
11110
+ type: ToolTypes.Function,
11111
+ function: {
11112
+ name: BrowserToolNames.TYPE,
11113
+ description: "Type text into an input field identified by accessible name.",
11114
+ parameters: {
11115
+ type: "object",
11116
+ properties: {
11117
+ name: {
11118
+ type: "string",
11119
+ description: "The accessible name of the input (from label, aria-label, placeholder)"
11120
+ },
11121
+ text: {
11122
+ type: "string",
11123
+ description: "The text to type"
11124
+ },
11125
+ role: {
11126
+ type: "string",
11127
+ enum: ["textbox", "searchbox", "combobox"],
11128
+ description: "ARIA role. Default: textbox"
11129
+ }
11130
+ },
11131
+ required: ["name", "text"]
11132
+ }
9170
11133
  }
9171
11134
  },
9172
- required: [
9173
- "from"
9174
- ],
9175
- type: "object"
9176
- }
9177
- },
9178
- properties: {
9179
- edges: {
9180
- items: {
9181
- $ref: "#/definitions/edge"
11135
+ {
11136
+ type: ToolTypes.Function,
11137
+ function: {
11138
+ name: BrowserToolNames.SNAPSHOT,
11139
+ description: "Get the current page's accessibility tree without navigating.",
11140
+ parameters: {
11141
+ type: "object",
11142
+ properties: {}
11143
+ }
11144
+ }
9182
11145
  },
9183
- type: "array"
9184
- },
9185
- execution: {
9186
- additionalProperties: false,
9187
- properties: {
9188
- max_parallelism: {
9189
- minimum: 1,
9190
- type: "integer"
9191
- },
9192
- node_timeout_ms: {
9193
- minimum: 1,
9194
- type: "integer"
9195
- },
9196
- run_timeout_ms: {
9197
- minimum: 1,
9198
- type: "integer"
11146
+ {
11147
+ type: ToolTypes.Function,
11148
+ function: {
11149
+ name: BrowserToolNames.SCROLL,
11150
+ description: "Scroll the page in a given direction.",
11151
+ parameters: {
11152
+ type: "object",
11153
+ properties: {
11154
+ direction: {
11155
+ type: "string",
11156
+ enum: ["up", "down"],
11157
+ description: "Scroll direction"
11158
+ },
11159
+ amount: {
11160
+ type: "string",
11161
+ enum: ["page", "half", "toTop", "toBottom"],
11162
+ description: "How much to scroll. Default: page"
11163
+ }
11164
+ },
11165
+ required: ["direction"]
11166
+ }
9199
11167
  }
9200
11168
  },
9201
- type: "object"
9202
- },
9203
- kind: {
9204
- const: "workflow.v0",
9205
- type: "string"
9206
- },
9207
- name: {
9208
- type: "string"
9209
- },
9210
- nodes: {
9211
- items: {
9212
- $ref: "#/definitions/node"
11169
+ {
11170
+ type: ToolTypes.Function,
11171
+ function: {
11172
+ name: BrowserToolNames.SCREENSHOT,
11173
+ description: "Capture a PNG screenshot of the current page. Use sparingly - prefer accessibility tree for decisions.",
11174
+ parameters: {
11175
+ type: "object",
11176
+ properties: {
11177
+ fullPage: {
11178
+ type: "boolean",
11179
+ description: "Capture full scrollable page. Default: false (viewport only)"
11180
+ }
11181
+ }
11182
+ }
11183
+ }
9213
11184
  },
9214
- minItems: 1,
9215
- type: "array"
9216
- },
9217
- outputs: {
9218
- items: {
9219
- $ref: "#/definitions/output"
11185
+ {
11186
+ type: ToolTypes.Function,
11187
+ function: {
11188
+ name: BrowserToolNames.EXTRACT,
11189
+ description: "Extract structured data from the page using CSS selectors.",
11190
+ parameters: {
11191
+ type: "object",
11192
+ properties: {
11193
+ selector: {
11194
+ type: "string",
11195
+ description: "CSS selector for elements to extract"
11196
+ },
11197
+ attribute: {
11198
+ type: "string",
11199
+ description: "Attribute to extract (textContent, href, src, etc.). Default: textContent"
11200
+ },
11201
+ multiple: {
11202
+ type: "boolean",
11203
+ description: "Return all matches as JSON array. Default: false (first match only)"
11204
+ }
11205
+ },
11206
+ required: ["selector"]
11207
+ }
11208
+ }
11209
+ }
11210
+ ];
11211
+ }
11212
+ /**
11213
+ * Register tool handlers into an existing registry.
11214
+ */
11215
+ registerInto(registry) {
11216
+ registry.register(BrowserToolNames.NAVIGATE, this.navigate.bind(this));
11217
+ registry.register(BrowserToolNames.CLICK, this.click.bind(this));
11218
+ registry.register(BrowserToolNames.TYPE, this.type.bind(this));
11219
+ registry.register(BrowserToolNames.SNAPSHOT, this.snapshot.bind(this));
11220
+ registry.register(BrowserToolNames.SCROLL, this.scroll.bind(this));
11221
+ registry.register(BrowserToolNames.SCREENSHOT, this.screenshot.bind(this));
11222
+ registry.register(BrowserToolNames.EXTRACT, this.extract.bind(this));
11223
+ return registry;
11224
+ }
11225
+ /**
11226
+ * Create a new registry with just this pack's tools.
11227
+ */
11228
+ toRegistry() {
11229
+ return this.registerInto(new ToolRegistry());
11230
+ }
11231
+ // ========================================================================
11232
+ // Private: Helpers
11233
+ // ========================================================================
11234
+ ensureInitialized() {
11235
+ if (!this.page) {
11236
+ throw new Error(
11237
+ "BrowserToolPack not initialized. Call initialize() first."
11238
+ );
11239
+ }
11240
+ }
11241
+ parseArgs(call, required) {
11242
+ const func = call.function;
11243
+ if (!func) {
11244
+ throw new ToolArgumentError({
11245
+ message: "tool call missing function",
11246
+ toolCallId: call.id,
11247
+ toolName: "",
11248
+ rawArguments: ""
11249
+ });
11250
+ }
11251
+ const rawArgs = func.arguments || "{}";
11252
+ let parsed;
11253
+ try {
11254
+ parsed = JSON.parse(rawArgs);
11255
+ } catch (err) {
11256
+ throw new ToolArgumentError({
11257
+ message: `invalid JSON arguments: ${err.message}`,
11258
+ toolCallId: call.id,
11259
+ toolName: func.name,
11260
+ rawArguments: rawArgs
11261
+ });
11262
+ }
11263
+ if (typeof parsed !== "object" || parsed === null) {
11264
+ throw new ToolArgumentError({
11265
+ message: "arguments must be an object",
11266
+ toolCallId: call.id,
11267
+ toolName: func.name,
11268
+ rawArguments: rawArgs
11269
+ });
11270
+ }
11271
+ const args = parsed;
11272
+ for (const key of required) {
11273
+ const value = args[key];
11274
+ if (value === void 0 || value === null || value === "") {
11275
+ throw new ToolArgumentError({
11276
+ message: `${key} is required`,
11277
+ toolCallId: call.id,
11278
+ toolName: func.name,
11279
+ rawArguments: rawArgs
11280
+ });
11281
+ }
11282
+ }
11283
+ return args;
11284
+ }
11285
+ validateUrl(url, call) {
11286
+ let parsed;
11287
+ try {
11288
+ parsed = new URL(url);
11289
+ } catch {
11290
+ throw new ToolArgumentError({
11291
+ message: `Invalid URL: ${url}`,
11292
+ toolCallId: call.id,
11293
+ toolName: call.function?.name ?? "",
11294
+ rawArguments: call.function?.arguments ?? ""
11295
+ });
11296
+ }
11297
+ if (!["http:", "https:"].includes(parsed.protocol)) {
11298
+ throw new ToolArgumentError({
11299
+ message: `Invalid protocol: ${parsed.protocol}. Only http/https allowed.`,
11300
+ toolCallId: call.id,
11301
+ toolName: call.function?.name ?? "",
11302
+ rawArguments: call.function?.arguments ?? ""
11303
+ });
11304
+ }
11305
+ const domain = parsed.hostname;
11306
+ if (this.cfg.blockedDomains.some((d) => domain.endsWith(d))) {
11307
+ throw new ToolArgumentError({
11308
+ message: `Domain blocked: ${domain}`,
11309
+ toolCallId: call.id,
11310
+ toolName: call.function?.name ?? "",
11311
+ rawArguments: call.function?.arguments ?? ""
11312
+ });
11313
+ }
11314
+ if (this.cfg.allowedDomains.length > 0) {
11315
+ if (!this.cfg.allowedDomains.some((d) => domain.endsWith(d))) {
11316
+ throw new ToolArgumentError({
11317
+ message: `Domain not in allowlist: ${domain}`,
11318
+ toolCallId: call.id,
11319
+ toolName: call.function?.name ?? "",
11320
+ rawArguments: call.function?.arguments ?? ""
11321
+ });
11322
+ }
11323
+ }
11324
+ }
11325
+ /**
11326
+ * Validates the current page URL against allowlist/blocklist.
11327
+ * Called after navigation and before any action to catch redirects
11328
+ * and in-session navigation to blocked domains.
11329
+ */
11330
+ ensureCurrentUrlAllowed() {
11331
+ if (!this.page) return;
11332
+ const currentUrl = this.page.url();
11333
+ if (currentUrl === "about:blank") return;
11334
+ let parsed;
11335
+ try {
11336
+ parsed = new URL(currentUrl);
11337
+ } catch {
11338
+ throw new Error(`Current page has invalid URL: ${currentUrl}`);
11339
+ }
11340
+ if (!["http:", "https:"].includes(parsed.protocol)) {
11341
+ throw new Error(
11342
+ `Current page protocol not allowed: ${parsed.protocol}. Only http/https allowed.`
11343
+ );
11344
+ }
11345
+ const domain = parsed.hostname;
11346
+ if (this.cfg.blockedDomains.some((d) => domain.endsWith(d))) {
11347
+ throw new Error(`Current page domain is blocked: ${domain}`);
11348
+ }
11349
+ if (this.cfg.allowedDomains.length > 0) {
11350
+ if (!this.cfg.allowedDomains.some((d) => domain.endsWith(d))) {
11351
+ throw new Error(`Current page domain not in allowlist: ${domain}`);
11352
+ }
11353
+ }
11354
+ }
11355
+ async getAccessibilityTree() {
11356
+ this.ensureInitialized();
11357
+ if (!this.cdpSession) {
11358
+ this.cdpSession = await this.page.context().newCDPSession(this.page);
11359
+ await this.cdpSession.send("Accessibility.enable");
11360
+ }
11361
+ const response = await this.cdpSession.send(
11362
+ "Accessibility.getFullAXTree"
11363
+ );
11364
+ return response.nodes;
11365
+ }
11366
+ formatAXTree(nodes) {
11367
+ const lines = [];
11368
+ let count = 0;
11369
+ for (const node of nodes) {
11370
+ if (count >= this.cfg.maxSnapshotNodes) {
11371
+ lines.push(`[truncated at ${this.cfg.maxSnapshotNodes} nodes]`);
11372
+ break;
11373
+ }
11374
+ if (node.ignored) {
11375
+ continue;
11376
+ }
11377
+ const role = node.role?.value || "unknown";
11378
+ const name = node.name?.value || "";
11379
+ if (!name && ["generic", "none", "text"].includes(role)) {
11380
+ continue;
11381
+ }
11382
+ const states = [];
11383
+ if (node.properties) {
11384
+ for (const prop of node.properties) {
11385
+ if (prop.value?.value === true) {
11386
+ const stateName = prop.name;
11387
+ if (["focused", "checked", "disabled", "expanded", "selected"].includes(
11388
+ stateName
11389
+ )) {
11390
+ states.push(stateName);
11391
+ }
11392
+ }
11393
+ }
11394
+ }
11395
+ const stateStr = states.length ? " " + states.join(" ") : "";
11396
+ const nameStr = name ? ` "${name}"` : "";
11397
+ lines.push(`[${role}${nameStr}${stateStr}]`);
11398
+ count++;
11399
+ }
11400
+ return lines.join("\n");
11401
+ }
11402
+ // ========================================================================
11403
+ // Private: Tool Handlers
11404
+ // ========================================================================
11405
+ async navigate(_args, call) {
11406
+ const args = this.parseArgs(call, ["url"]);
11407
+ this.validateUrl(args.url, call);
11408
+ this.ensureInitialized();
11409
+ const waitUntil = args.waitUntil ?? "domcontentloaded";
11410
+ await this.page.goto(args.url, {
11411
+ timeout: this.cfg.navigationTimeoutMs,
11412
+ waitUntil
11413
+ });
11414
+ this.ensureCurrentUrlAllowed();
11415
+ const tree = await this.getAccessibilityTree();
11416
+ return this.formatAXTree(tree);
11417
+ }
11418
+ async click(_args, call) {
11419
+ const args = this.parseArgs(call, ["name"]);
11420
+ this.ensureInitialized();
11421
+ this.ensureCurrentUrlAllowed();
11422
+ let locator;
11423
+ if (args.role) {
11424
+ locator = this.page.getByRole(args.role, {
11425
+ name: args.name
11426
+ });
11427
+ } else {
11428
+ locator = this.page.getByRole("button", { name: args.name }).or(this.page.getByRole("link", { name: args.name })).or(this.page.getByRole("menuitem", { name: args.name }));
11429
+ }
11430
+ await locator.click({ timeout: this.cfg.actionTimeoutMs });
11431
+ const tree = await this.getAccessibilityTree();
11432
+ return this.formatAXTree(tree);
11433
+ }
11434
+ async type(_args, call) {
11435
+ const args = this.parseArgs(call, ["name", "text"]);
11436
+ this.ensureInitialized();
11437
+ this.ensureCurrentUrlAllowed();
11438
+ const role = args.role ?? "textbox";
11439
+ const locator = this.page.getByRole(role, { name: args.name });
11440
+ await locator.fill(args.text, { timeout: this.cfg.actionTimeoutMs });
11441
+ return `Typed "${args.text}" into ${role} "${args.name}"`;
11442
+ }
11443
+ async snapshot(_args, _call) {
11444
+ this.ensureInitialized();
11445
+ this.ensureCurrentUrlAllowed();
11446
+ const tree = await this.getAccessibilityTree();
11447
+ return this.formatAXTree(tree);
11448
+ }
11449
+ async scroll(_args, call) {
11450
+ const args = this.parseArgs(call, ["direction"]);
11451
+ this.ensureInitialized();
11452
+ this.ensureCurrentUrlAllowed();
11453
+ const amount = args.amount ?? "page";
11454
+ if (amount === "toTop") {
11455
+ await this.page.evaluate(() => window.scrollTo(0, 0));
11456
+ } else if (amount === "toBottom") {
11457
+ await this.page.evaluate(
11458
+ () => window.scrollTo(0, document.body.scrollHeight)
11459
+ );
11460
+ } else {
11461
+ const viewport = this.page.viewportSize();
11462
+ const height = viewport?.height ?? 800;
11463
+ const scrollAmount = amount === "half" ? height / 2 : height;
11464
+ const delta = args.direction === "down" ? scrollAmount : -scrollAmount;
11465
+ await this.page.evaluate((d) => window.scrollBy(0, d), delta);
11466
+ }
11467
+ const tree = await this.getAccessibilityTree();
11468
+ return this.formatAXTree(tree);
11469
+ }
11470
+ async screenshot(_args, call) {
11471
+ const args = this.parseArgs(call, []);
11472
+ this.ensureInitialized();
11473
+ this.ensureCurrentUrlAllowed();
11474
+ const buffer = await this.page.screenshot({
11475
+ fullPage: args.fullPage ?? false,
11476
+ type: "png"
11477
+ });
11478
+ if (buffer.length > BrowserDefaults.MAX_SCREENSHOT_BYTES) {
11479
+ throw new Error(
11480
+ `Screenshot size (${buffer.length} bytes) exceeds maximum allowed (${BrowserDefaults.MAX_SCREENSHOT_BYTES} bytes). Try capturing viewport only.`
11481
+ );
11482
+ }
11483
+ const base64 = buffer.toString("base64");
11484
+ return `data:image/png;base64,${base64}`;
11485
+ }
11486
+ async extract(_args, call) {
11487
+ const args = this.parseArgs(call, ["selector"]);
11488
+ this.ensureInitialized();
11489
+ this.ensureCurrentUrlAllowed();
11490
+ const attribute = args.attribute ?? "textContent";
11491
+ const multiple = args.multiple ?? false;
11492
+ if (multiple) {
11493
+ const elements = this.page.locator(args.selector);
11494
+ const count = await elements.count();
11495
+ const results = [];
11496
+ for (let i = 0; i < count; i++) {
11497
+ const el = elements.nth(i);
11498
+ let value;
11499
+ if (attribute === "textContent") {
11500
+ value = await el.textContent();
11501
+ } else {
11502
+ value = await el.getAttribute(attribute);
11503
+ }
11504
+ if (value !== null) {
11505
+ results.push(value.trim());
11506
+ }
11507
+ }
11508
+ return JSON.stringify(results);
11509
+ } else {
11510
+ const el = this.page.locator(args.selector).first();
11511
+ let value;
11512
+ if (attribute === "textContent") {
11513
+ value = await el.textContent();
11514
+ } else {
11515
+ value = await el.getAttribute(attribute);
11516
+ }
11517
+ return value?.trim() ?? "";
11518
+ }
11519
+ }
11520
+ };
11521
+ function createBrowserToolPack(options = {}) {
11522
+ return new BrowserToolPack(options);
11523
+ }
11524
+ function createBrowserTools(options = {}) {
11525
+ const pack = new BrowserToolPack(options);
11526
+ return { pack, registry: pack.toRegistry() };
11527
+ }
11528
+
11529
+ // src/tools_runner.ts
11530
+ var ToolRunner = class {
11531
+ constructor(options) {
11532
+ this.registry = options.registry;
11533
+ this.runsClient = options.runsClient;
11534
+ this.customerId = options.customerId;
11535
+ this.onBeforeExecute = options.onBeforeExecute;
11536
+ this.onAfterExecute = options.onAfterExecute;
11537
+ this.onSubmitted = options.onSubmitted;
11538
+ this.onError = options.onError;
11539
+ }
11540
+ /**
11541
+ * Handles a node_waiting event by executing tools and submitting results.
11542
+ *
11543
+ * @param runId - The run ID
11544
+ * @param nodeId - The node ID that is waiting
11545
+ * @param waiting - The waiting state with pending tool calls
11546
+ * @returns The submission response with accepted count and new status
11547
+ *
11548
+ * @example
11549
+ * ```typescript
11550
+ * for await (const event of client.runs.events(runId)) {
11551
+ * if (event.type === "node_waiting") {
11552
+ * const result = await runner.handleNodeWaiting(
11553
+ * runId,
11554
+ * event.node_id,
11555
+ * event.waiting
11556
+ * );
11557
+ * console.log(`Submitted ${result.accepted} results, status: ${result.status}`);
11558
+ * }
11559
+ * }
11560
+ * ```
11561
+ */
11562
+ async handleNodeWaiting(runId, nodeId, waiting) {
11563
+ const results = [];
11564
+ for (const pending of waiting.pending_tool_calls) {
11565
+ try {
11566
+ await this.onBeforeExecute?.(pending);
11567
+ const toolCall = createToolCall(
11568
+ pending.tool_call_id,
11569
+ pending.name,
11570
+ pending.arguments
11571
+ );
11572
+ const result = await this.registry.execute(toolCall);
11573
+ results.push(result);
11574
+ await this.onAfterExecute?.(result);
11575
+ } catch (err) {
11576
+ const error = err instanceof Error ? err : new Error(String(err));
11577
+ await this.onError?.(error, pending);
11578
+ results.push({
11579
+ toolCallId: pending.tool_call_id,
11580
+ toolName: pending.name,
11581
+ result: null,
11582
+ error: error.message
11583
+ });
11584
+ }
11585
+ }
11586
+ const response = await this.runsClient.submitToolResults(
11587
+ runId,
11588
+ {
11589
+ node_id: nodeId,
11590
+ step: waiting.step,
11591
+ request_id: waiting.request_id,
11592
+ results: results.map((r) => ({
11593
+ tool_call_id: r.toolCallId,
11594
+ name: r.toolName,
11595
+ output: r.error ? `Error: ${r.error}` : typeof r.result === "string" ? r.result : JSON.stringify(r.result)
11596
+ }))
9220
11597
  },
9221
- minItems: 1,
9222
- type: "array"
11598
+ { customerId: this.customerId }
11599
+ );
11600
+ await this.onSubmitted?.(runId, response.accepted, response.status);
11601
+ return {
11602
+ accepted: response.accepted,
11603
+ status: response.status,
11604
+ results
11605
+ };
11606
+ }
11607
+ /**
11608
+ * Processes a stream of run events, automatically handling node_waiting events.
11609
+ *
11610
+ * This is the main entry point for running a workflow with client-side tools.
11611
+ * It yields all events through (including node_waiting after handling).
11612
+ *
11613
+ * @param runId - The run ID to process
11614
+ * @param events - AsyncIterable of run events (from RunsClient.events())
11615
+ * @yields All run events, with node_waiting events handled automatically
11616
+ *
11617
+ * @example
11618
+ * ```typescript
11619
+ * const run = await client.runs.create(workflowSpec);
11620
+ * const eventStream = client.runs.events(run.run_id);
11621
+ *
11622
+ * for await (const event of runner.processEvents(run.run_id, eventStream)) {
11623
+ * switch (event.type) {
11624
+ * case "node_started":
11625
+ * console.log(`Node ${event.node_id} started`);
11626
+ * break;
11627
+ * case "node_succeeded":
11628
+ * console.log(`Node ${event.node_id} succeeded`);
11629
+ * break;
11630
+ * case "run_succeeded":
11631
+ * console.log("Run completed!");
11632
+ * break;
11633
+ * }
11634
+ * }
11635
+ * ```
11636
+ */
11637
+ async *processEvents(runId, events) {
11638
+ for await (const event of events) {
11639
+ if (event.type === "node_waiting") {
11640
+ const waitingEvent = event;
11641
+ try {
11642
+ await this.handleNodeWaiting(
11643
+ runId,
11644
+ waitingEvent.node_id,
11645
+ waitingEvent.waiting
11646
+ );
11647
+ } catch (err) {
11648
+ const error = err instanceof Error ? err : new Error(String(err));
11649
+ await this.onError?.(error);
11650
+ throw error;
11651
+ }
11652
+ }
11653
+ yield event;
9223
11654
  }
9224
- },
9225
- required: [
9226
- "kind",
9227
- "nodes",
9228
- "outputs"
9229
- ],
9230
- title: "ModelRelay workflow.v0",
9231
- type: "object"
11655
+ }
11656
+ /**
11657
+ * Checks if a run event is a node_waiting event.
11658
+ * Utility for filtering events when not using processEvents().
11659
+ */
11660
+ static isNodeWaiting(event) {
11661
+ return event.type === "node_waiting";
11662
+ }
11663
+ /**
11664
+ * Checks if a run status is terminal (succeeded, failed, or canceled).
11665
+ * Utility for determining when to stop polling.
11666
+ */
11667
+ static isTerminalStatus(status) {
11668
+ return status === "succeeded" || status === "failed" || status === "canceled";
11669
+ }
9232
11670
  };
11671
+ function createToolRunner(options) {
11672
+ return new ToolRunner(options);
11673
+ }
9233
11674
 
9234
11675
  // src/generated/index.ts
9235
11676
  var generated_exports = {};
@@ -9286,9 +11727,11 @@ var ModelRelay = class _ModelRelay {
9286
11727
  metrics: cfg.metrics,
9287
11728
  trace: cfg.trace
9288
11729
  });
11730
+ this.images = new ImagesClient(http, auth);
9289
11731
  this.customers = new CustomersClient(http, { apiKey, accessToken, tokenProvider });
9290
11732
  this.tiers = new TiersClient(http, { apiKey });
9291
11733
  this.models = new ModelsClient(http);
11734
+ this.sessions = new SessionsClient(this, http, auth);
9292
11735
  }
9293
11736
  forCustomer(customerId) {
9294
11737
  return new CustomerScopedModelRelay(this.responses, customerId, this.baseUrl);
@@ -9302,6 +11745,10 @@ function resolveBaseUrl(override) {
9302
11745
  0 && (module.exports = {
9303
11746
  APIError,
9304
11747
  AuthClient,
11748
+ BillingProviders,
11749
+ BrowserDefaults,
11750
+ BrowserToolNames,
11751
+ BrowserToolPack,
9305
11752
  ConfigError,
9306
11753
  ContentPartTypes,
9307
11754
  CustomerResponsesClient,
@@ -9311,10 +11758,17 @@ function resolveBaseUrl(override) {
9311
11758
  DEFAULT_BASE_URL,
9312
11759
  DEFAULT_CLIENT_HEADER,
9313
11760
  DEFAULT_CONNECT_TIMEOUT_MS,
11761
+ DEFAULT_IGNORE_DIRS,
9314
11762
  DEFAULT_REQUEST_TIMEOUT_MS,
9315
11763
  ErrorCodes,
11764
+ FSDefaults,
11765
+ FSToolNames,
9316
11766
  FrontendTokenProvider,
11767
+ ImagesClient,
9317
11768
  InputItemTypes,
11769
+ LocalFSToolPack,
11770
+ LocalSession,
11771
+ MemorySessionStore,
9318
11772
  MessageRoles,
9319
11773
  ModelRelay,
9320
11774
  ModelRelayError,
@@ -9322,22 +11776,27 @@ function resolveBaseUrl(override) {
9322
11776
  OIDCExchangeTokenProvider,
9323
11777
  OutputFormatTypes,
9324
11778
  OutputItemTypes,
11779
+ PathEscapeError,
9325
11780
  ResponsesClient,
9326
11781
  ResponsesStream,
9327
11782
  RunsClient,
9328
11783
  RunsEventStream,
9329
11784
  SDK_VERSION,
11785
+ SessionsClient,
9330
11786
  StopReasons,
9331
11787
  StreamProtocolError,
9332
11788
  StreamTimeoutError,
9333
11789
  StructuredDecodeError,
9334
11790
  StructuredExhaustedError,
9335
11791
  StructuredJSONStream,
11792
+ SubscriptionStatuses,
9336
11793
  TiersClient,
9337
11794
  ToolArgsError,
11795
+ ToolArgumentError,
9338
11796
  ToolCallAccumulator,
9339
11797
  ToolChoiceTypes,
9340
11798
  ToolRegistry,
11799
+ ToolRunner,
9341
11800
  ToolTypes,
9342
11801
  TransportError,
9343
11802
  WORKFLOWS_COMPILE_PATH,
@@ -9349,17 +11808,25 @@ function resolveBaseUrl(override) {
9349
11808
  WorkflowsClient,
9350
11809
  asModelId,
9351
11810
  asProviderId,
11811
+ asSessionId,
9352
11812
  asTierCode,
9353
11813
  assistantMessageWithToolCalls,
9354
11814
  createAccessTokenAuth,
9355
11815
  createApiKeyAuth,
9356
11816
  createAssistantMessage,
11817
+ createBrowserToolPack,
11818
+ createBrowserTools,
9357
11819
  createFunctionCall,
9358
11820
  createFunctionTool,
9359
11821
  createFunctionToolFromSchema,
11822
+ createLocalFSToolPack,
11823
+ createLocalFSTools,
11824
+ createLocalSession,
11825
+ createMemorySessionStore,
9360
11826
  createRetryMessages,
9361
11827
  createSystemMessage,
9362
11828
  createToolCall,
11829
+ createToolRunner,
9363
11830
  createUsage,
9364
11831
  createUserMessage,
9365
11832
  createWebTool,
@@ -9367,6 +11834,7 @@ function resolveBaseUrl(override) {
9367
11834
  executeWithRetry,
9368
11835
  firstToolCall,
9369
11836
  formatToolErrorForModel,
11837
+ generateSessionId,
9370
11838
  generated,
9371
11839
  getRetryableErrors,
9372
11840
  hasRetryableErrors,