@stacklink/sdk 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +10 -0
  2. package/compatibility.json +1 -1
  3. package/dist/src/domain/memory-contract.d.ts +1497 -335
  4. package/dist/src/domain/memory-contract.d.ts.map +1 -1
  5. package/dist/src/domain/memory-contract.js +330 -59
  6. package/dist/src/domain/memory-contract.js.map +1 -1
  7. package/dist/src/domain/provider-branding.d.ts +1 -1
  8. package/dist/src/domain/provider-branding.d.ts.map +1 -1
  9. package/dist/src/domain/provider-branding.js +296 -8
  10. package/dist/src/domain/provider-branding.js.map +1 -1
  11. package/dist/src/domain/stacklink-meta-tools.d.ts +14 -16
  12. package/dist/src/domain/stacklink-meta-tools.d.ts.map +1 -1
  13. package/dist/src/domain/stacklink-meta-tools.js +205 -127
  14. package/dist/src/domain/stacklink-meta-tools.js.map +1 -1
  15. package/dist/src/sdk/framework-tools.d.ts +3 -2
  16. package/dist/src/sdk/framework-tools.d.ts.map +1 -1
  17. package/dist/src/sdk/framework-tools.js +1 -0
  18. package/dist/src/sdk/framework-tools.js.map +1 -1
  19. package/dist/src/sdk/index.d.ts +280 -142
  20. package/dist/src/sdk/index.d.ts.map +1 -1
  21. package/dist/src/sdk/index.js +433 -224
  22. package/dist/src/sdk/index.js.map +1 -1
  23. package/dist/src/sdk/langchain.d.ts +7 -2
  24. package/dist/src/sdk/langchain.d.ts.map +1 -1
  25. package/dist/src/sdk/langchain.js +3 -2
  26. package/dist/src/sdk/langchain.js.map +1 -1
  27. package/dist/src/sdk/memory-lifecycle.d.ts +25 -0
  28. package/dist/src/sdk/memory-lifecycle.d.ts.map +1 -1
  29. package/dist/src/sdk/memory-lifecycle.js +75 -1
  30. package/dist/src/sdk/memory-lifecycle.js.map +1 -1
  31. package/dist/src/sdk/openai-agents.d.ts +6 -6
  32. package/dist/src/sdk/openai-agents.d.ts.map +1 -1
  33. package/dist/src/sdk/openai-agents.js +21 -16
  34. package/dist/src/sdk/openai-agents.js.map +1 -1
  35. package/dist/src/sdk/openai.d.ts +2 -2
  36. package/dist/src/sdk/openai.d.ts.map +1 -1
  37. package/dist/src/sdk/openai.js +67 -31
  38. package/dist/src/sdk/openai.js.map +1 -1
  39. package/dist/src/sdk/vercel-ai.d.ts +11 -4
  40. package/dist/src/sdk/vercel-ai.d.ts.map +1 -1
  41. package/dist/src/sdk/vercel-ai.js +21 -6
  42. package/dist/src/sdk/vercel-ai.js.map +1 -1
  43. package/package.json +1 -1
  44. package/dist/src/domain/knowledge-meta-tools.d.ts +0 -12
  45. package/dist/src/domain/knowledge-meta-tools.d.ts.map +0 -1
  46. package/dist/src/domain/knowledge-meta-tools.js +0 -38
  47. package/dist/src/domain/knowledge-meta-tools.js.map +0 -1
  48. package/dist/src/domain/memory-meta-tools.d.ts +0 -5
  49. package/dist/src/domain/memory-meta-tools.d.ts.map +0 -1
  50. package/dist/src/domain/memory-meta-tools.js +0 -18
  51. package/dist/src/domain/memory-meta-tools.js.map +0 -1
  52. package/dist/src/domain/platform-mcp-tools.d.ts +0 -3
  53. package/dist/src/domain/platform-mcp-tools.d.ts.map +0 -1
  54. package/dist/src/domain/platform-mcp-tools.js +0 -30
  55. package/dist/src/domain/platform-mcp-tools.js.map +0 -1
@@ -1,8 +1,6 @@
1
1
  import { materializeStacklinkWorkbenchExecutionResponse, } from '../domain/stacklink-meta-tools.js';
2
- import { platformMcpTools } from '../domain/platform-mcp-tools.js';
3
2
  export { StacklinkMemoryLifecycle, StacklinkMemoryLifecycleError, createStacklinkMemoryLifecycle, serializeCompletedConversationTurn, } from './memory-lifecycle.js';
4
- export const platformToolSurfaceNames = platformMcpTools;
5
- export const stacklinkTypeScriptSdkVersion = '0.2.0';
3
+ export const stacklinkTypeScriptSdkVersion = '0.2.2';
6
4
  const workbenchReusableRunComparisonWaitMs = 120_000;
7
5
  const workbenchReusableRunComparisonPollMs = 1_000;
8
6
  export class StacklinkApiError extends Error {
@@ -17,6 +15,18 @@ export class StacklinkApiError extends Error {
17
15
  this.name = 'StacklinkApiError';
18
16
  }
19
17
  }
18
+ function providerCatalogSdkQuery(input) {
19
+ return {
20
+ limit: input.limit,
21
+ cursor: input.cursor,
22
+ search: input.search,
23
+ providerKey: input.providerKeys,
24
+ categoryKey: input.categoryKeys,
25
+ projectStatus: input.projectStatus,
26
+ connectionStatus: input.connectionStatus,
27
+ tenantId: input.tenantId,
28
+ };
29
+ }
20
30
  function withoutCacheControl(input) {
21
31
  const { cache: _cache, ...rest } = input;
22
32
  return rest;
@@ -35,8 +45,69 @@ function stableSdkStringify(value) {
35
45
  const record = value;
36
46
  return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableSdkStringify(record[key])}`).join(',')}}`;
37
47
  }
38
- function uniqueSdkStrings(values) {
39
- return Array.from(new Set(values));
48
+ const sdkDiscoveryMcpMethods = new Set([
49
+ 'initialize',
50
+ 'tools/list',
51
+ 'resources/list',
52
+ 'resources/templates/list',
53
+ 'prompts/list',
54
+ ]);
55
+ let sdkRequestSequence = 0;
56
+ function mcpRequestIsDiscovery(input) {
57
+ const requests = Array.isArray(input) ? input : [input];
58
+ return requests.length > 0 && requests.every((request) => (typeof request.method === 'string' && sdkDiscoveryMcpMethods.has(request.method)));
59
+ }
60
+ function positiveSdkInteger(value, field) {
61
+ if (!Number.isInteger(value) || value <= 0) {
62
+ throw new Error(`Stacklink ${field} must be a positive integer.`);
63
+ }
64
+ return value;
65
+ }
66
+ function sdkString(value) {
67
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
68
+ }
69
+ function createSdkRequestId() {
70
+ const randomUuid = globalThis.crypto?.randomUUID?.();
71
+ if (randomUuid)
72
+ return randomUuid;
73
+ sdkRequestSequence += 1;
74
+ return `stacklink-${Date.now()}-${sdkRequestSequence}`;
75
+ }
76
+ function sdkRetrySafe(method, idempotencyKey) {
77
+ return method === 'GET' || method === 'HEAD' || method === 'OPTIONS' || Boolean(idempotencyKey);
78
+ }
79
+ function sdkRetryableStatus(status) {
80
+ return status === 408 || status === 429 || status === 502 || status === 503 || status === 504;
81
+ }
82
+ function sdkRetryDelayMs(response, attempt) {
83
+ const retryAfter = response?.headers.get('retry-after');
84
+ if (retryAfter) {
85
+ const seconds = Number(retryAfter);
86
+ if (Number.isFinite(seconds) && seconds >= 0)
87
+ return Math.ceil(seconds * 1000);
88
+ const at = Date.parse(retryAfter);
89
+ if (Number.isFinite(at))
90
+ return Math.max(0, at - Date.now());
91
+ }
92
+ return Math.min(1_000, 100 * (2 ** Math.max(0, attempt - 1)));
93
+ }
94
+ function sdkDelay(ms, signal) {
95
+ if (signal.aborted)
96
+ return Promise.reject(signal.reason ?? new Error('Stacklink request was aborted.'));
97
+ return new Promise((resolve, reject) => {
98
+ const timeout = setTimeout(() => {
99
+ signal.removeEventListener('abort', onAbort);
100
+ resolve();
101
+ }, ms);
102
+ const onAbort = () => {
103
+ clearTimeout(timeout);
104
+ reject(signal.reason ?? new Error('Stacklink request was aborted.'));
105
+ };
106
+ signal.addEventListener('abort', onAbort, { once: true });
107
+ });
108
+ }
109
+ function sdkTimeoutError(timeoutMs, requestId) {
110
+ return new StacklinkApiError(`Stacklink request exceeded its ${timeoutMs} ms deadline.`, 408, 'sdk_request_timeout', { requestId, timeoutMs });
40
111
  }
41
112
  function normalizeKnowledgePackSelection(value) {
42
113
  if (Array.isArray(value)) {
@@ -120,6 +191,9 @@ export class Stacklink {
120
191
  fetchImpl;
121
192
  cacheOptions;
122
193
  responseCache = new Map();
194
+ discoveryRequests = new Map();
195
+ timeoutMs;
196
+ maxAttempts;
123
197
  webhooks;
124
198
  triggers;
125
199
  sessions;
@@ -136,6 +210,8 @@ export class Stacklink {
136
210
  this.registrationKey = options.registrationKey;
137
211
  this.baseUrl = options.baseUrl.endsWith('/') ? options.baseUrl.slice(0, -1) : options.baseUrl;
138
212
  this.fetchImpl = options.fetch ?? fetch;
213
+ this.timeoutMs = positiveSdkInteger(options.timeoutMs ?? 15_000, 'timeoutMs');
214
+ this.maxAttempts = Math.min(3, positiveSdkInteger(options.maxAttempts ?? 3, 'maxAttempts'));
139
215
  this.cacheOptions = {
140
216
  enabled: options.cache?.enabled ?? true,
141
217
  toolsListTtlMs: options.cache?.toolsListTtlMs ?? 300_000,
@@ -176,27 +252,24 @@ export class Stacklink {
176
252
  userEmail: input.userEmail,
177
253
  scopes: input.scopes ?? [],
178
254
  toolRouter: input.toolRouter,
179
- preset: input.preset,
180
- preload: input.preload,
255
+ modelSurface: input.modelSurface,
181
256
  toolkits: input.toolkits,
182
257
  tools: input.tools,
183
258
  authConfigs: input.authConfigs,
184
259
  connectedAccounts: input.connectedAccounts,
260
+ providerMutationMode: input.providerMutationMode,
185
261
  multiAccount: input.multiAccount,
186
262
  workbench: input.workbench,
187
263
  knowledgePacks: input.knowledgePacks,
188
264
  memory: input.memory,
189
- metaTools: input.metaTools,
190
265
  metadata: input.metadata ?? {},
191
266
  },
192
267
  });
193
- const projection = await this.getSessionMetaTools(response.session.id);
194
- return new ToolRouterSession(this, response.session.id, response.session.tenantId, response.session.mcp, projection.contractVersion, projection.tools);
268
+ return new ToolRouterSession(this, response.session.id, response.session.tenantId, response.session.mcp, response.metaTools.contractVersion, response.metaTools.tools);
195
269
  }
196
270
  async use(sessionId) {
197
- const response = await this.getSession(sessionId);
198
- const projection = await this.getSessionMetaTools(sessionId);
199
- return new ToolRouterSession(this, response.session.id, response.session.tenantId, response.session.mcp, projection.contractVersion, projection.tools);
271
+ const response = await this.request(`/v1/sessions/${encodeURIComponent(sessionId)}/bootstrap`);
272
+ return new ToolRouterSession(this, response.session.id, response.session.tenantId, response.session.mcp, response.metaTools.contractVersion, response.metaTools.tools);
200
273
  }
201
274
  clearCache(prefix) {
202
275
  if (!prefix) {
@@ -262,19 +335,24 @@ export class Stacklink {
262
335
  method: 'DELETE',
263
336
  });
264
337
  }
265
- async listProviders() {
266
- return this.request('/v1/providers');
338
+ async listProviders(query = {}) {
339
+ return this.request('/v1/providers', {
340
+ query: providerCatalogSdkQuery(query),
341
+ });
267
342
  }
268
343
  async listProjectProviders(query = {}) {
269
344
  return this.request('/v1/project/providers', {
270
- query: { tenantId: query.tenantId },
345
+ query: providerCatalogSdkQuery(query),
271
346
  });
272
347
  }
273
348
  async listProviderTools(providerKey, input = {}) {
274
349
  return this.request(`/v1/providers/${providerKey}/tools`, {
275
350
  query: {
351
+ limit: input.limit,
352
+ cursor: input.cursor,
353
+ search: input.search,
354
+ capability: input.capability,
276
355
  includePlanned: input.includePlanned === true ? 'true' : undefined,
277
- responseMode: input.responseMode === 'compact' ? 'compact' : undefined,
278
356
  },
279
357
  });
280
358
  }
@@ -360,6 +438,10 @@ export class Stacklink {
360
438
  method: 'POST',
361
439
  body: input,
362
440
  signal: options.signal,
441
+ timeoutMs: options.timeoutMs ?? 60_000,
442
+ idempotencyKey: options.idempotencyKey ?? sdkString(input.idempotencyKey),
443
+ requestId: options.requestId,
444
+ traceparent: options.traceparent,
363
445
  });
364
446
  }
365
447
  async executeToolsBatch(input, options = {}) {
@@ -367,6 +449,10 @@ export class Stacklink {
367
449
  method: 'POST',
368
450
  body: input,
369
451
  signal: options.signal,
452
+ timeoutMs: options.timeoutMs ?? 60_000,
453
+ idempotencyKey: options.idempotencyKey ?? sdkString(input.idempotencyKey),
454
+ requestId: options.requestId,
455
+ traceparent: options.traceparent,
370
456
  });
371
457
  }
372
458
  async getSubscription() {
@@ -619,7 +705,6 @@ export class Stacklink {
619
705
  executionKind,
620
706
  executionId,
621
707
  waitTimeoutMs: 10_000,
622
- pollIntervalMs: 500,
623
708
  }, options);
624
709
  const execution = isSdkRecord(waited.execution) ? waited.execution : undefined;
625
710
  if (execution && isTerminalSdkExecution(execution)) {
@@ -660,15 +745,60 @@ export class Stacklink {
660
745
  signal: options.signal,
661
746
  });
662
747
  }
748
+ async *streamExecutionEvents(input, options = {}) {
749
+ let afterSequence = input.afterSequence === undefined ? undefined : String(input.afterSequence);
750
+ while (!options.signal?.aborted) {
751
+ const response = await this.requestRaw(`/v1/executions/${input.executionKind}/${input.executionId}/events/stream`, {
752
+ headers: {
753
+ accept: 'text/event-stream',
754
+ ...(afterSequence === undefined ? {} : { 'Last-Event-ID': afterSequence }),
755
+ },
756
+ query: {
757
+ sessionId: input.sessionId,
758
+ afterSequence,
759
+ limit: input.limit,
760
+ },
761
+ signal: options.signal,
762
+ timeoutMs: options.timeoutMs ?? 30_000,
763
+ });
764
+ try {
765
+ for await (const payload of this.readJsonEventStream(response)) {
766
+ if (!isSdkRecord(payload))
767
+ continue;
768
+ const events = Array.isArray(payload.events) ? payload.events : [];
769
+ for (const event of events) {
770
+ if (isSdkRecord(event) && (typeof event.sequence === 'string' || typeof event.sequence === 'number')) {
771
+ afterSequence = String(event.sequence);
772
+ }
773
+ yield event;
774
+ }
775
+ if (typeof payload.nextCursor === 'string')
776
+ afterSequence = payload.nextCursor;
777
+ if (payload.terminal === true)
778
+ return;
779
+ }
780
+ }
781
+ catch (error) {
782
+ if (options.signal?.aborted)
783
+ throw options.signal.reason ?? error;
784
+ const errorName = error instanceof Error ? error.name : '';
785
+ if (errorName !== 'AbortError' && errorName !== 'TimeoutError')
786
+ throw error;
787
+ // The per-connection deadline bounds a stream. Reconnect from the durable cursor.
788
+ }
789
+ }
790
+ if (options.signal?.aborted)
791
+ throw options.signal.reason ?? new Error('Stacklink execution stream was aborted.');
792
+ }
663
793
  async waitForExecution(input, options = {}) {
664
794
  return this.request(`/v1/executions/${input.executionKind}/${input.executionId}/wait`, {
665
795
  method: 'POST',
666
796
  body: {
667
797
  sessionId: input.sessionId,
668
798
  waitTimeoutMs: input.waitTimeoutMs,
669
- pollIntervalMs: input.pollIntervalMs,
670
799
  },
671
- signal: options.signal,
800
+ ...options,
801
+ timeoutMs: options.timeoutMs ?? (input.waitTimeoutMs ?? 30_000) + 5_000,
672
802
  });
673
803
  }
674
804
  async cancelExecution(input, options = {}) {
@@ -719,7 +849,7 @@ export class Stacklink {
719
849
  async getConnectedAccount(accountId) {
720
850
  return this.request(`/v1/connections/${accountId}`);
721
851
  }
722
- async listConnectedAccountTools(accountId, input = {}) {
852
+ async listConnectedAccountTools(accountId, input) {
723
853
  return this.request(`/v1/connections/${accountId}/tools`, {
724
854
  query: {
725
855
  ...(input.includePlanned !== undefined ? { includePlanned: String(input.includePlanned) } : {}),
@@ -985,21 +1115,35 @@ export class Stacklink {
985
1115
  method: 'DELETE',
986
1116
  });
987
1117
  }
988
- async mcpRequest(input, options = {}) {
989
- return this.mcpRequestAtPath('/v1/mcp', input, options);
990
- }
991
1118
  async mcpSessionRequest(sessionId, input, options = {}) {
992
1119
  return this.mcpRequestAtPath(`/v1/sessions/${encodeURIComponent(sessionId)}/mcp`, input, options);
993
1120
  }
994
1121
  async mcpRequestAtPath(path, input, options = {}) {
995
- return this.request(path, {
996
- method: 'POST',
997
- body: input,
998
- signal: options.signal,
999
- });
1000
- }
1001
- async mcpRequestStream(input, options = {}) {
1002
- return this.mcpRequestStreamAtPath('/v1/mcp', input, options);
1122
+ if (!Array.isArray(input))
1123
+ return this.mcpRequestStreamAtPath(path, input, {
1124
+ ...options, timeoutMs: options.timeoutMs ?? (mcpRequestIsDiscovery(input) ? this.timeoutMs : 60_000),
1125
+ });
1126
+ const response = await this.requestRaw(path, { method: 'POST', body: input, headers: { accept: 'application/json, text/event-stream' },
1127
+ ...options, timeoutMs: options.timeoutMs ?? 60_000 });
1128
+ if (response.status === 202)
1129
+ return null;
1130
+ const value = await response.json();
1131
+ if (!Array.isArray(value))
1132
+ throw new StacklinkApiError('Expected MCP batch responses.', 502, 'mcp_response_invalid');
1133
+ const expected = input.filter((entry) => entry.id !== undefined && entry.id !== null);
1134
+ if (expected.length !== value.length)
1135
+ throw new StacklinkApiError('MCP batch response count does not match.', 502, 'mcp_response_invalid');
1136
+ const seen = new Set();
1137
+ for (const message of value) {
1138
+ if (!isSdkRecord(message) || seen.has(message.id))
1139
+ throw new StacklinkApiError('Duplicate or invalid MCP response.', 502, 'mcp_response_invalid');
1140
+ const request = expected.find((entry) => entry.id === message.id);
1141
+ if (!request)
1142
+ throw new StacklinkApiError('MCP response belongs to another request.', 502, 'mcp_response_invalid');
1143
+ validateSdkMcpResponse(message, request.id);
1144
+ seen.add(message.id);
1145
+ }
1146
+ return value;
1003
1147
  }
1004
1148
  async mcpSessionRequestStream(sessionId, input, options = {}) {
1005
1149
  return this.mcpRequestStreamAtPath(`/v1/sessions/${encodeURIComponent(sessionId)}/mcp`, input, options);
@@ -1032,49 +1176,40 @@ export class Stacklink {
1032
1176
  method: 'POST',
1033
1177
  body: input,
1034
1178
  signal: options.signal,
1179
+ timeoutMs: options.timeoutMs ?? 60_000,
1180
+ idempotencyKey: options.idempotencyKey,
1181
+ requestId: options.requestId,
1182
+ traceparent: options.traceparent,
1035
1183
  headers: {
1036
1184
  accept: 'text/event-stream, application/json',
1037
1185
  },
1038
1186
  });
1039
1187
  const contentType = response.headers.get('content-type') ?? '';
1188
+ if (response.status === 202 && (input.id === undefined || input.id === null))
1189
+ return null;
1040
1190
  if (!contentType.includes('text/event-stream')) {
1041
- return await response.json().catch(() => null);
1191
+ const value = await response.json();
1192
+ validateSdkMcpResponse(value, input.id);
1193
+ return value;
1042
1194
  }
1043
- return await this.readMcpEventStream(response, options.onProgress);
1195
+ return await this.readMcpEventStream(response, input, options.onProgress);
1044
1196
  }
1045
1197
  finally {
1046
1198
  completed = true;
1047
1199
  options.signal?.removeEventListener('abort', cancel);
1048
1200
  }
1049
1201
  }
1050
- async listMcpTools() {
1051
- return this.mcpRequest({
1052
- jsonrpc: '2.0',
1053
- id: 'tools-list',
1054
- method: 'tools/list',
1055
- });
1056
- }
1057
- async callMcpTool(name, args) {
1058
- return this.mcpRequest({
1202
+ async callSessionMcpTool(sessionId, name, args, options = {}) {
1203
+ return this.mcpSessionRequestStream(sessionId, {
1059
1204
  jsonrpc: '2.0',
1060
- id: `${name}-call`,
1205
+ id: createSdkRequestId(),
1061
1206
  method: 'tools/call',
1062
1207
  params: {
1063
1208
  name,
1209
+ ...(options.onProgress ? { _meta: { progressToken: createSdkRequestId() } } : {}),
1064
1210
  arguments: args,
1065
1211
  },
1066
- });
1067
- }
1068
- async callSessionMcpTool(sessionId, name, args) {
1069
- return this.mcpSessionRequest(sessionId, {
1070
- jsonrpc: '2.0',
1071
- id: `${name}-call`,
1072
- method: 'tools/call',
1073
- params: {
1074
- name,
1075
- arguments: args,
1076
- },
1077
- });
1212
+ }, options);
1078
1213
  }
1079
1214
  async listKnowledgePacks() {
1080
1215
  return this.request('/v1/knowledge/packs');
@@ -1221,26 +1356,17 @@ export class Stacklink {
1221
1356
  signal.addEventListener('abort', cancel, { once: true });
1222
1357
  }
1223
1358
  async request(path, init = {}) {
1224
- const url = new URL(`${this.baseUrl}${path}`);
1225
- for (const [key, value] of Object.entries(init.query ?? {})) {
1226
- if (Array.isArray(value)) {
1227
- for (const entry of value) {
1228
- if (entry !== undefined && entry !== null)
1229
- url.searchParams.append(key, String(entry));
1230
- }
1359
+ const response = await this.performRequest(path, init);
1360
+ const responseText = await response.text();
1361
+ let body = null;
1362
+ if (responseText.length > 0) {
1363
+ try {
1364
+ body = JSON.parse(responseText);
1231
1365
  }
1232
- else if (value !== undefined && value !== null) {
1233
- url.searchParams.set(key, String(value));
1366
+ catch {
1367
+ body = null;
1234
1368
  }
1235
1369
  }
1236
- const headers = this.requestHeaders(init.registration === true, init.headers);
1237
- const response = await this.fetchImpl(url, {
1238
- method: init.method ?? 'GET',
1239
- headers,
1240
- ...(init.signal ? { signal: init.signal } : {}),
1241
- ...(init.body === undefined ? {} : { body: JSON.stringify(init.body) }),
1242
- });
1243
- const body = await response.json().catch(() => null);
1244
1370
  if (!response.ok) {
1245
1371
  const message = body && typeof body === 'object' && 'message' in body
1246
1372
  ? String(body.message)
@@ -1256,6 +1382,21 @@ export class Stacklink {
1256
1382
  return body;
1257
1383
  }
1258
1384
  async requestRaw(path, init = {}) {
1385
+ const response = await this.performRequest(path, init);
1386
+ if (!response.ok) {
1387
+ const body = await response.text();
1388
+ let parsed;
1389
+ try {
1390
+ parsed = JSON.parse(body);
1391
+ }
1392
+ catch {
1393
+ parsed = null;
1394
+ }
1395
+ throw new StacklinkApiError(isSdkRecord(parsed) && typeof parsed.message === 'string' ? parsed.message : `Stacklink request failed with ${response.status}`, response.status, isSdkRecord(parsed) && typeof parsed.code === 'string' ? parsed.code : undefined, isSdkRecord(parsed) ? parsed.details : undefined);
1396
+ }
1397
+ return response;
1398
+ }
1399
+ async performRequest(path, init) {
1259
1400
  const url = new URL(`${this.baseUrl}${path}`);
1260
1401
  for (const [key, value] of Object.entries(init.query ?? {})) {
1261
1402
  if (Array.isArray(value)) {
@@ -1268,17 +1409,55 @@ export class Stacklink {
1268
1409
  url.searchParams.set(key, String(value));
1269
1410
  }
1270
1411
  }
1271
- const response = await this.fetchImpl(url, {
1272
- method: init.method ?? 'GET',
1273
- headers: this.requestHeaders(init.registration === true, init.headers),
1274
- ...(init.signal ? { signal: init.signal } : {}),
1275
- ...(init.body === undefined ? {} : { body: JSON.stringify(init.body) }),
1276
- });
1277
- if (!response.ok) {
1278
- const body = await response.text().catch(() => '');
1279
- throw new StacklinkApiError(body || `Stacklink request failed with ${response.status}`, response.status);
1412
+ const method = init.method ?? 'GET';
1413
+ const timeoutMs = positiveSdkInteger(init.timeoutMs ?? this.timeoutMs, 'timeoutMs');
1414
+ const deadlineAt = Date.now() + timeoutMs;
1415
+ const requestId = init.requestId ?? createSdkRequestId();
1416
+ const retrySafe = !path.endsWith('/mcp') && sdkRetrySafe(method, init.idempotencyKey);
1417
+ let attempt = 0;
1418
+ while (true) {
1419
+ attempt += 1;
1420
+ const remainingMs = deadlineAt - Date.now();
1421
+ if (remainingMs <= 0)
1422
+ throw sdkTimeoutError(timeoutMs, requestId);
1423
+ const deadlineSignal = AbortSignal.timeout(remainingMs);
1424
+ const signal = init.signal ? AbortSignal.any([init.signal, deadlineSignal]) : deadlineSignal;
1425
+ const headers = this.requestHeaders(init.registration === true, {
1426
+ ...init.headers,
1427
+ 'X-Stacklink-Client-Request-Id': requestId,
1428
+ 'X-Stacklink-Timeout-Ms': String(remainingMs),
1429
+ ...(init.idempotencyKey ? { 'Idempotency-Key': init.idempotencyKey } : {}),
1430
+ ...(init.traceparent ? { traceparent: init.traceparent } : {}),
1431
+ });
1432
+ try {
1433
+ const response = await this.fetchImpl(url, {
1434
+ method,
1435
+ headers,
1436
+ signal,
1437
+ ...(init.body === undefined ? {} : { body: JSON.stringify(init.body) }),
1438
+ });
1439
+ if (!retrySafe || attempt >= this.maxAttempts || !sdkRetryableStatus(response.status)) {
1440
+ return response;
1441
+ }
1442
+ const delayMs = sdkRetryDelayMs(response, attempt);
1443
+ if (delayMs >= deadlineAt - Date.now())
1444
+ return response;
1445
+ await response.body?.cancel().catch(() => undefined);
1446
+ await sdkDelay(delayMs, signal);
1447
+ }
1448
+ catch (error) {
1449
+ if (init.signal?.aborted)
1450
+ throw init.signal.reason ?? error;
1451
+ if (deadlineSignal.aborted || Date.now() >= deadlineAt)
1452
+ throw sdkTimeoutError(timeoutMs, requestId);
1453
+ if (!retrySafe || attempt >= this.maxAttempts)
1454
+ throw error;
1455
+ const delayMs = sdkRetryDelayMs(undefined, attempt);
1456
+ if (delayMs >= deadlineAt - Date.now())
1457
+ throw error;
1458
+ await sdkDelay(delayMs, signal);
1459
+ }
1280
1460
  }
1281
- return response;
1282
1461
  }
1283
1462
  async cachedDiscoveryRequest(cacheKey, ttlMs, control, producer) {
1284
1463
  if (!this.cacheOptions.enabled || control?.cache === 'bypass')
@@ -1287,13 +1466,21 @@ export class Stacklink {
1287
1466
  const existing = this.responseCache.get(cacheKey);
1288
1467
  if (existing && existing.expiresAt > now)
1289
1468
  return existing.value;
1290
- const value = await producer();
1291
- this.responseCache.set(cacheKey, {
1292
- value,
1293
- expiresAt: now + ttlMs,
1469
+ const active = this.discoveryRequests.get(cacheKey);
1470
+ if (active)
1471
+ return active;
1472
+ const request = producer().then((value) => {
1473
+ this.responseCache.set(cacheKey, {
1474
+ value,
1475
+ expiresAt: Date.now() + ttlMs,
1476
+ });
1477
+ this.trimResponseCache();
1478
+ return value;
1479
+ }).finally(() => {
1480
+ this.discoveryRequests.delete(cacheKey);
1294
1481
  });
1295
- this.trimResponseCache();
1296
- return value;
1482
+ this.discoveryRequests.set(cacheKey, request);
1483
+ return request;
1297
1484
  }
1298
1485
  discoveryCacheTtl(kind) {
1299
1486
  if (kind === 'toolsList')
@@ -1313,7 +1500,7 @@ export class Stacklink {
1313
1500
  this.responseCache.delete(firstKey);
1314
1501
  }
1315
1502
  }
1316
- async readMcpEventStream(response, onProgress) {
1503
+ async readMcpEventStream(response, request, onProgress) {
1317
1504
  if (!response.body)
1318
1505
  throw new Error('Stacklink MCP stream response has no body.');
1319
1506
  const reader = response.body.getReader();
@@ -1321,6 +1508,10 @@ export class Stacklink {
1321
1508
  let buffer = '';
1322
1509
  let dataLines = [];
1323
1510
  let finalResponse;
1511
+ let bytes = 0;
1512
+ const requestParams = isSdkRecord(request.params) ? request.params : {};
1513
+ const meta = isSdkRecord(requestParams._meta) ? requestParams._meta : {};
1514
+ const progressToken = meta.progressToken;
1324
1515
  const handleLine = (line) => {
1325
1516
  if (line.endsWith('\r'))
1326
1517
  line = line.slice(0, -1);
@@ -1330,10 +1521,24 @@ export class Stacklink {
1330
1521
  dataLines = [];
1331
1522
  if (data.length > 0) {
1332
1523
  const message = JSON.parse(data);
1524
+ if (!isSdkRecord(message) || message.jsonrpc !== '2.0') {
1525
+ throw new StacklinkApiError('Stacklink returned an invalid MCP message.', 502, 'mcp_response_invalid');
1526
+ }
1333
1527
  if (isSdkRecord(message) && message.method === 'notifications/progress' && isSdkRecord(message.params)) {
1334
- onProgress?.(message.params);
1528
+ if (progressToken !== undefined && message.params.progressToken === progressToken) {
1529
+ onProgress?.(message.params);
1530
+ }
1335
1531
  }
1336
- else if (isSdkRecord(message) && ('result' in message || 'error' in message)) {
1532
+ else if (typeof message.method === 'string' && message.id !== undefined) {
1533
+ throw new StacklinkApiError('The server requested a capability this SDK did not declare.', 502, 'mcp_capability_not_supported');
1534
+ }
1535
+ else if ('result' in message || 'error' in message) {
1536
+ if (message.id !== request.id)
1537
+ return;
1538
+ if (finalResponse !== undefined || ('result' in message) === ('error' in message)) {
1539
+ throw new StacklinkApiError('Stacklink returned a conflicting MCP result.', 502, 'mcp_response_invalid');
1540
+ }
1541
+ validateSdkMcpResponse(message, request.id);
1337
1542
  finalResponse = message;
1338
1543
  }
1339
1544
  }
@@ -1347,6 +1552,63 @@ export class Stacklink {
1347
1552
  dataLines.push(value);
1348
1553
  }
1349
1554
  };
1555
+ try {
1556
+ while (true) {
1557
+ const read = await reader.read();
1558
+ if (read.done)
1559
+ break;
1560
+ bytes += read.value.byteLength;
1561
+ if (bytes > 8 * 1024 * 1024)
1562
+ throw new StacklinkApiError('MCP response exceeds 8 MiB.', 502, 'mcp_response_too_large');
1563
+ buffer += decoder.decode(read.value, { stream: true });
1564
+ let index = buffer.indexOf('\n');
1565
+ while (index >= 0) {
1566
+ handleLine(buffer.slice(0, index));
1567
+ buffer = buffer.slice(index + 1);
1568
+ if (finalResponse !== undefined)
1569
+ return finalResponse;
1570
+ index = buffer.indexOf('\n');
1571
+ }
1572
+ }
1573
+ buffer += decoder.decode();
1574
+ if (buffer.length > 0)
1575
+ handleLine(buffer);
1576
+ handleLine('');
1577
+ if (finalResponse === undefined)
1578
+ throw new Error('Stacklink MCP stream ended without a final JSON-RPC response.');
1579
+ return finalResponse;
1580
+ }
1581
+ finally {
1582
+ await reader.cancel().catch(() => undefined);
1583
+ reader.releaseLock();
1584
+ }
1585
+ }
1586
+ async *readJsonEventStream(response) {
1587
+ if (!response.body)
1588
+ throw new Error('Stacklink event stream response has no body.');
1589
+ const reader = response.body.getReader();
1590
+ const decoder = new TextDecoder();
1591
+ let buffer = '';
1592
+ let dataLines = [];
1593
+ const payloadForLine = (lineValue) => {
1594
+ let line = lineValue;
1595
+ if (line.endsWith('\r'))
1596
+ line = line.slice(0, -1);
1597
+ if (line === '') {
1598
+ if (dataLines.length === 0)
1599
+ return undefined;
1600
+ const data = dataLines.join('\n');
1601
+ dataLines = [];
1602
+ return data.length === 0 ? undefined : JSON.parse(data);
1603
+ }
1604
+ if (line.startsWith('data:')) {
1605
+ let value = line.slice(5);
1606
+ if (value.startsWith(' '))
1607
+ value = value.slice(1);
1608
+ dataLines.push(value);
1609
+ }
1610
+ return undefined;
1611
+ };
1350
1612
  while (true) {
1351
1613
  const read = await reader.read();
1352
1614
  if (read.done)
@@ -1354,18 +1616,22 @@ export class Stacklink {
1354
1616
  buffer += decoder.decode(read.value, { stream: true });
1355
1617
  let index = buffer.indexOf('\n');
1356
1618
  while (index >= 0) {
1357
- handleLine(buffer.slice(0, index));
1619
+ const payload = payloadForLine(buffer.slice(0, index));
1620
+ if (payload !== undefined)
1621
+ yield payload;
1358
1622
  buffer = buffer.slice(index + 1);
1359
1623
  index = buffer.indexOf('\n');
1360
1624
  }
1361
1625
  }
1362
1626
  buffer += decoder.decode();
1363
- if (buffer.length > 0)
1364
- handleLine(buffer);
1365
- handleLine('');
1366
- if (finalResponse === undefined)
1367
- throw new Error('Stacklink MCP stream ended without a final JSON-RPC response.');
1368
- return finalResponse;
1627
+ if (buffer.length > 0) {
1628
+ const payload = payloadForLine(buffer);
1629
+ if (payload !== undefined)
1630
+ yield payload;
1631
+ }
1632
+ const finalPayload = payloadForLine('');
1633
+ if (finalPayload !== undefined)
1634
+ yield finalPayload;
1369
1635
  }
1370
1636
  requestHeaders(registration, extraHeaders = {}) {
1371
1637
  if (registration) {
@@ -1540,6 +1806,12 @@ export class StacklinkMemoryClient {
1540
1806
  body: input,
1541
1807
  });
1542
1808
  }
1809
+ reviewQuality(memoryId, input) {
1810
+ return this.client.request(`/v1/memory/records/${memoryId}/quality-review`, {
1811
+ method: 'POST',
1812
+ body: input,
1813
+ });
1814
+ }
1543
1815
  forget(memoryId, input) {
1544
1816
  return this.client.request(`/v1/memory/records/${memoryId}/forget`, {
1545
1817
  method: 'POST',
@@ -1629,8 +1901,8 @@ export class StacklinkMemoryClient {
1629
1901
  body: input,
1630
1902
  });
1631
1903
  }
1632
- deleteSource(bindingId) {
1633
- return this.client.request(`/v1/memory/sources/${bindingId}`, { method: 'DELETE' });
1904
+ deleteSource(bindingId, input) {
1905
+ return this.client.request(`/v1/memory/sources/${bindingId}`, { method: 'DELETE', body: input });
1634
1906
  }
1635
1907
  replaySourceItem(bindingId, itemId) {
1636
1908
  return this.client.request(`/v1/memory/sources/${bindingId}/items/${itemId}/replay`, {
@@ -1688,8 +1960,10 @@ export class StacklinkSessionsClient {
1688
1960
  metaTools(sessionId) {
1689
1961
  return this.client.getSessionMetaTools(sessionId);
1690
1962
  }
1691
- toolkits(sessionId) {
1692
- return this.client.request(`/v1/sessions/${sessionId}/toolkits`);
1963
+ toolkits(sessionId, query = {}) {
1964
+ return this.client.request(`/v1/sessions/${sessionId}/toolkits`, {
1965
+ query: providerCatalogSdkQuery(query),
1966
+ });
1693
1967
  }
1694
1968
  streamEvents(sessionId, input = {}) {
1695
1969
  return this.client.requestRaw(`/v1/sessions/${sessionId}/events/stream`, {
@@ -1700,7 +1974,6 @@ export class StacklinkSessionsClient {
1700
1974
  after: input.after,
1701
1975
  eventType: input.eventTypes,
1702
1976
  limit: input.limit,
1703
- pollIntervalMs: input.pollIntervalMs,
1704
1977
  },
1705
1978
  });
1706
1979
  }
@@ -1809,7 +2082,7 @@ export class StacklinkConnectedAccountsClient {
1809
2082
  get(accountId) {
1810
2083
  return this.client.getConnectedAccount(accountId);
1811
2084
  }
1812
- listTools(accountId, input = {}) {
2085
+ listTools(accountId, input) {
1813
2086
  return this.client.listConnectedAccountTools(accountId, input);
1814
2087
  }
1815
2088
  listResources(accountId) {
@@ -1872,14 +2145,14 @@ export class StacklinkToolkitsClient {
1872
2145
  constructor(client) {
1873
2146
  this.client = client;
1874
2147
  }
1875
- list() {
1876
- return this.client.listProviders();
2148
+ list(query = {}) {
2149
+ return this.client.listProviders(query);
1877
2150
  }
1878
2151
  listProject(query = {}) {
1879
2152
  return this.client.listProjectProviders(query);
1880
2153
  }
1881
- listTools(providerKey) {
1882
- return this.client.listProviderTools(providerKey);
2154
+ listTools(providerKey, query = {}) {
2155
+ return this.client.listProviderTools(providerKey, query);
1883
2156
  }
1884
2157
  }
1885
2158
  export class StacklinkProviderToolsClient {
@@ -1952,12 +2225,6 @@ export class StacklinkSessionMemoryClient {
1952
2225
  ...(reason ? { reason } : {}),
1953
2226
  });
1954
2227
  }
1955
- restore(memoryId, reason) {
1956
- return this.memoryClient.restore(memoryId, {
1957
- sessionId: this.sessionId,
1958
- ...(reason ? { reason } : {}),
1959
- });
1960
- }
1961
2228
  }
1962
2229
  export class StacklinkWorkbenchRun {
1963
2230
  client;
@@ -1972,37 +2239,16 @@ export class StacklinkWorkbenchRun {
1972
2239
  this.executionId = response.executionId;
1973
2240
  this.executionKind = response.executionKind;
1974
2241
  }
1975
- async *events(input = {}) {
1976
- let afterSequence;
1977
- while (true) {
1978
- const page = await this.client.readExecutionEvents({
1979
- sessionId: this.platformSessionId,
1980
- executionKind: this.executionKind,
1981
- executionId: this.executionId,
1982
- afterSequence,
1983
- limit: input.limit ?? 200,
1984
- });
1985
- for (const event of page.events) {
1986
- yield event;
1987
- if (isSdkRecord(event) && (typeof event.sequence === 'string' || typeof event.sequence === 'number')) {
1988
- afterSequence = String(event.sequence);
1989
- }
1990
- }
1991
- if (page.hasMore) {
1992
- if (!page.nextCursor)
1993
- throw new Error('Execution event page reported hasMore without nextCursor.');
1994
- afterSequence = page.nextCursor;
1995
- continue;
1996
- }
1997
- const state = await this.client.getExecution({
1998
- sessionId: this.platformSessionId,
1999
- executionKind: this.executionKind,
2000
- executionId: this.executionId,
2001
- });
2002
- if (isTerminalSdkExecution(state.execution))
2003
- return;
2004
- await sleep(input.pollIntervalMs ?? 500);
2005
- }
2242
+ events(input = {}) {
2243
+ return this.client.streamExecutionEvents({
2244
+ sessionId: this.platformSessionId,
2245
+ executionKind: this.executionKind,
2246
+ executionId: this.executionId,
2247
+ limit: input.limit ?? 200,
2248
+ }, {
2249
+ signal: input.signal,
2250
+ timeoutMs: input.streamTimeoutMs,
2251
+ });
2006
2252
  }
2007
2253
  wait(input = {}) {
2008
2254
  return this.client.waitForExecution({
@@ -2261,8 +2507,8 @@ export class ToolRouterSession {
2261
2507
  async mcpRequestStream(input, options = {}) {
2262
2508
  return this.client.mcpSessionRequestStream(this.id, input, options);
2263
2509
  }
2264
- async callMcpTool(name, args) {
2265
- return this.client.callSessionMcpTool(this.id, name, args);
2510
+ async callMcpTool(name, args, options = {}) {
2511
+ return this.client.callSessionMcpTool(this.id, name, args, options);
2266
2512
  }
2267
2513
  mcpConnection() {
2268
2514
  if (!this.mcp) {
@@ -2285,6 +2531,7 @@ export class ToolRouterSession {
2285
2531
  return this.toolDefinitions.map((definition) => ({
2286
2532
  ...definition,
2287
2533
  inputSchema: { ...definition.inputSchema },
2534
+ outputSchema: { ...definition.outputSchema },
2288
2535
  }));
2289
2536
  }
2290
2537
  async close() {
@@ -2305,18 +2552,17 @@ export class ToolRouterSession {
2305
2552
  async workbenchActivity() {
2306
2553
  return this.client.getSessionWorkbench(this.id);
2307
2554
  }
2308
- async toolkits() {
2309
- const response = await this.client.cachedDiscoveryRequest(`session:${this.id}:toolkits`, this.client.discoveryCacheTtl('toolkits'), undefined, () => this.client.request(`/v1/sessions/${this.id}/toolkits`));
2310
- return response.toolkits;
2555
+ async toolkits(query = {}) {
2556
+ return this.client.request(`/v1/sessions/${this.id}/toolkits`, {
2557
+ query: providerCatalogSdkQuery(query),
2558
+ });
2311
2559
  }
2312
- async authorize(providerKey, input = {}) {
2560
+ async authorize(connectionRef, input = {}) {
2313
2561
  const response = await this.client.request(`/v1/sessions/${this.id}/connections/authorize`, {
2314
2562
  method: 'POST',
2315
2563
  body: {
2316
- providerKey,
2317
- callbackUrl: input.callbackUrl,
2318
- authConfigId: input.authConfigId,
2319
- authMode: input.authMode,
2564
+ connectionRef,
2565
+ oauthScopeProfile: input.oauthScopeProfile,
2320
2566
  metadata: input.metadata ?? {},
2321
2567
  },
2322
2568
  });
@@ -2409,70 +2655,19 @@ export class ToolRouterSession {
2409
2655
  this.client.clearCache(`session:${this.id}:`);
2410
2656
  return response;
2411
2657
  }
2412
- async searchTools(input = {}) {
2413
- const searchInput = {
2414
- responseMode: 'compact',
2415
- includeSchemas: false,
2416
- includeRelatedTools: false,
2417
- includeExecutionPlan: false,
2418
- ...input,
2419
- };
2420
- return this.client.cachedDiscoveryRequest(`session:${this.id}:search:${stableSdkStringify(withoutCacheControl(searchInput))}`, this.client.discoveryCacheTtl('search'), searchInput, () => this.client.request(`/v1/sessions/${this.id}/tools/search`, {
2658
+ async search(input) {
2659
+ const searchInput = { queries: input.queries };
2660
+ return this.client.cachedDiscoveryRequest(`session:${this.id}:search:${stableSdkStringify(withoutCacheControl(searchInput))}`, this.client.discoveryCacheTtl('search'), input, () => this.client.request(`/v1/sessions/${this.id}/tools/search`, {
2421
2661
  method: 'POST',
2422
2662
  body: discoveryServerBody(searchInput),
2423
2663
  }));
2424
2664
  }
2425
- async getToolSchemas(toolKeys, input = {}) {
2426
- return this.client.cachedDiscoveryRequest(`session:${this.id}:schemas:${stableSdkStringify({
2427
- toolKeys: [...toolKeys].sort(),
2428
- includePlanned: input.includePlanned === true,
2429
- })}`, this.client.discoveryCacheTtl('schemas'), input, () => this.client.request(`/v1/sessions/${this.id}/tools/schemas`, {
2430
- method: 'POST',
2431
- body: {
2432
- toolKeys,
2433
- includePlanned: input.includePlanned,
2434
- ...(input.cache === 'bypass' ? { cache: 'bypass' } : {}),
2435
- },
2436
- }));
2437
- }
2438
- async prepareRuntimeTools(input = {}) {
2439
- return this.client.request(`/v1/sessions/${this.id}/tools/runtime-contracts`, {
2665
+ async getContracts(contractRefs) {
2666
+ return this.client.request(`/v1/sessions/${this.id}/tools/contracts`, {
2440
2667
  method: 'POST',
2441
- body: input,
2668
+ body: { contractRefs },
2442
2669
  });
2443
2670
  }
2444
- async prepareTools(input) {
2445
- const limitPerIntent = input.limitPerIntent ?? 5;
2446
- const search = await this.searchTools({
2447
- cache: input.cache,
2448
- responseMode: 'compact',
2449
- includeSchemas: false,
2450
- includeRelatedTools: false,
2451
- includeExecutionPlan: false,
2452
- queries: input.intents.map((intent) => ({
2453
- query: intent,
2454
- limit: limitPerIntent,
2455
- ...(input.toolkitKey ? { toolkitKey: input.toolkitKey } : {}),
2456
- ...(input.capability ? { capability: input.capability } : {}),
2457
- })),
2458
- includePlanned: input.includePlanned === true,
2459
- model: input.model,
2460
- limit: Math.max(limitPerIntent, input.intents.length * limitPerIntent),
2461
- });
2462
- const toolKeys = uniqueSdkStrings(search.tools.map((tool) => tool.key));
2463
- const schemaResponse = input.includeSchemas === false || toolKeys.length === 0
2464
- ? { tools: [] }
2465
- : await this.getToolSchemas(toolKeys, {
2466
- cache: input.cache,
2467
- includePlanned: input.includePlanned,
2468
- });
2469
- return {
2470
- tools: search.tools,
2471
- schemas: schemaResponse.tools,
2472
- toolKeys,
2473
- search,
2474
- };
2475
- }
2476
2671
  async searchKnowledge(input = {}) {
2477
2672
  return this.client.searchKnowledge({
2478
2673
  ...input,
@@ -2575,18 +2770,26 @@ export class ToolRouterSession {
2575
2770
  sessionId: this.id,
2576
2771
  });
2577
2772
  }
2578
- async executeTool(input, options = {}) {
2773
+ async execute(contractRef, input, execution = {}, options = {}) {
2774
+ if (execution.idempotencyKey !== undefined && options.idempotencyKey !== undefined
2775
+ && execution.idempotencyKey !== options.idempotencyKey) {
2776
+ throw new StacklinkApiError('Execution and request idempotency keys must match.', 400, 'idempotency_key_conflict');
2777
+ }
2778
+ const idempotencyKey = execution.idempotencyKey ?? options.idempotencyKey ?? createSdkRequestId();
2779
+ const { idempotencyKey: _childKey, ...item } = execution;
2579
2780
  return this.client.request(`/v1/sessions/${this.id}/tools/execute`, {
2580
2781
  method: 'POST',
2581
- body: input,
2582
- ...options,
2583
- });
2584
- }
2585
- async executeToolsBatch(input, options = {}) {
2586
- return this.client.request(`/v1/sessions/${this.id}/tools/execute-batch`, {
2587
- method: 'POST',
2588
- body: input,
2782
+ body: {
2783
+ idempotencyKey,
2784
+ executions: [{
2785
+ contractRef,
2786
+ input,
2787
+ ...item,
2788
+ }],
2789
+ },
2589
2790
  ...options,
2791
+ timeoutMs: options.timeoutMs ?? 60_000,
2792
+ idempotencyKey,
2590
2793
  });
2591
2794
  }
2592
2795
  async runWorkbench(input, options = {}) {
@@ -2652,7 +2855,6 @@ export class ToolRouterSession {
2652
2855
  after: input.after,
2653
2856
  eventType: input.eventTypes,
2654
2857
  limit: input.limit === undefined ? undefined : String(input.limit),
2655
- pollIntervalMs: input.pollIntervalMs === undefined ? undefined : String(input.pollIntervalMs),
2656
2858
  },
2657
2859
  });
2658
2860
  }
@@ -2662,7 +2864,8 @@ export class ToolRouterSession {
2662
2864
  {
2663
2865
  description: definition.description,
2664
2866
  parameters: definition.inputSchema,
2665
- execute: async (input) => unwrapMcpToolResult(await this.callMcpTool(definition.name, input)),
2867
+ outputSchema: definition.outputSchema,
2868
+ execute: async (input, execution) => unwrapMcpToolResult(await this.callMcpTool(definition.name, input, { ...(execution?.abortSignal ? { signal: execution.abortSignal } : {}), ...(execution?.onProgress ? { onProgress: execution.onProgress } : {}) })),
2666
2869
  },
2667
2870
  ]));
2668
2871
  }
@@ -2779,4 +2982,10 @@ function isTerminalSdkExecution(value) {
2779
2982
  || status === 'cancelled'
2780
2983
  || status === 'timed_out';
2781
2984
  }
2985
+ function validateSdkMcpResponse(value, requestId) {
2986
+ if (!isSdkRecord(value) || value.jsonrpc !== '2.0' || value.id !== requestId || ('result' in value) === ('error' in value)
2987
+ || ('error' in value && (!isSdkRecord(value.error) || typeof value.error.code !== 'number' || typeof value.error.message !== 'string'))) {
2988
+ throw new StacklinkApiError('Stacklink returned an invalid or unrelated MCP response.', 502, 'mcp_response_invalid');
2989
+ }
2990
+ }
2782
2991
  //# sourceMappingURL=index.js.map