@stacklink/sdk 0.2.0 → 0.3.7

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 (63) hide show
  1. package/README.md +33 -0
  2. package/compatibility.json +1 -1
  3. package/dist/src/domain/bot-contract.d.ts +4336 -0
  4. package/dist/src/domain/bot-contract.d.ts.map +1 -0
  5. package/dist/src/domain/bot-contract.js +382 -0
  6. package/dist/src/domain/bot-contract.js.map +1 -0
  7. package/dist/src/domain/memory-contract.d.ts +1705 -543
  8. package/dist/src/domain/memory-contract.d.ts.map +1 -1
  9. package/dist/src/domain/memory-contract.js +331 -60
  10. package/dist/src/domain/memory-contract.js.map +1 -1
  11. package/dist/src/domain/platform-approval-contract.d.ts +508 -0
  12. package/dist/src/domain/platform-approval-contract.d.ts.map +1 -0
  13. package/dist/src/domain/platform-approval-contract.js +77 -0
  14. package/dist/src/domain/platform-approval-contract.js.map +1 -0
  15. package/dist/src/domain/provider-branding.d.ts +1 -1
  16. package/dist/src/domain/provider-branding.d.ts.map +1 -1
  17. package/dist/src/domain/provider-branding.js +306 -8
  18. package/dist/src/domain/provider-branding.js.map +1 -1
  19. package/dist/src/domain/stacklink-meta-tools.d.ts +14 -16
  20. package/dist/src/domain/stacklink-meta-tools.d.ts.map +1 -1
  21. package/dist/src/domain/stacklink-meta-tools.js +217 -128
  22. package/dist/src/domain/stacklink-meta-tools.js.map +1 -1
  23. package/dist/src/sdk/framework-tools.d.ts +24 -2
  24. package/dist/src/sdk/framework-tools.d.ts.map +1 -1
  25. package/dist/src/sdk/framework-tools.js +50 -0
  26. package/dist/src/sdk/framework-tools.js.map +1 -1
  27. package/dist/src/sdk/index.d.ts +475 -143
  28. package/dist/src/sdk/index.d.ts.map +1 -1
  29. package/dist/src/sdk/index.js +633 -236
  30. package/dist/src/sdk/index.js.map +1 -1
  31. package/dist/src/sdk/langchain.d.ts +7 -2
  32. package/dist/src/sdk/langchain.d.ts.map +1 -1
  33. package/dist/src/sdk/langchain.js +3 -2
  34. package/dist/src/sdk/langchain.js.map +1 -1
  35. package/dist/src/sdk/memory-lifecycle.d.ts +25 -0
  36. package/dist/src/sdk/memory-lifecycle.d.ts.map +1 -1
  37. package/dist/src/sdk/memory-lifecycle.js +75 -1
  38. package/dist/src/sdk/memory-lifecycle.js.map +1 -1
  39. package/dist/src/sdk/openai-agents.d.ts +6 -6
  40. package/dist/src/sdk/openai-agents.d.ts.map +1 -1
  41. package/dist/src/sdk/openai-agents.js +21 -16
  42. package/dist/src/sdk/openai-agents.js.map +1 -1
  43. package/dist/src/sdk/openai.d.ts +2 -2
  44. package/dist/src/sdk/openai.d.ts.map +1 -1
  45. package/dist/src/sdk/openai.js +67 -31
  46. package/dist/src/sdk/openai.js.map +1 -1
  47. package/dist/src/sdk/vercel-ai.d.ts +15 -4
  48. package/dist/src/sdk/vercel-ai.d.ts.map +1 -1
  49. package/dist/src/sdk/vercel-ai.js +33 -9
  50. package/dist/src/sdk/vercel-ai.js.map +1 -1
  51. package/package.json +1 -1
  52. package/dist/src/domain/knowledge-meta-tools.d.ts +0 -12
  53. package/dist/src/domain/knowledge-meta-tools.d.ts.map +0 -1
  54. package/dist/src/domain/knowledge-meta-tools.js +0 -38
  55. package/dist/src/domain/knowledge-meta-tools.js.map +0 -1
  56. package/dist/src/domain/memory-meta-tools.d.ts +0 -5
  57. package/dist/src/domain/memory-meta-tools.d.ts.map +0 -1
  58. package/dist/src/domain/memory-meta-tools.js +0 -18
  59. package/dist/src/domain/memory-meta-tools.js.map +0 -1
  60. package/dist/src/domain/platform-mcp-tools.d.ts +0 -3
  61. package/dist/src/domain/platform-mcp-tools.d.ts.map +0 -1
  62. package/dist/src/domain/platform-mcp-tools.js +0 -30
  63. package/dist/src/domain/platform-mcp-tools.js.map +0 -1
@@ -1,8 +1,7 @@
1
+ import { botApiSignaturePayload } from '../domain/bot-contract.js';
1
2
  import { materializeStacklinkWorkbenchExecutionResponse, } from '../domain/stacklink-meta-tools.js';
2
- import { platformMcpTools } from '../domain/platform-mcp-tools.js';
3
3
  export { StacklinkMemoryLifecycle, StacklinkMemoryLifecycleError, createStacklinkMemoryLifecycle, serializeCompletedConversationTurn, } from './memory-lifecycle.js';
4
- export const platformToolSurfaceNames = platformMcpTools;
5
- export const stacklinkTypeScriptSdkVersion = '0.2.0';
4
+ export const stacklinkTypeScriptSdkVersion = '0.3.7';
6
5
  const workbenchReusableRunComparisonWaitMs = 120_000;
7
6
  const workbenchReusableRunComparisonPollMs = 1_000;
8
7
  export class StacklinkApiError extends Error {
@@ -17,6 +16,18 @@ export class StacklinkApiError extends Error {
17
16
  this.name = 'StacklinkApiError';
18
17
  }
19
18
  }
19
+ function providerCatalogSdkQuery(input) {
20
+ return {
21
+ limit: input.limit,
22
+ cursor: input.cursor,
23
+ search: input.search,
24
+ providerKey: input.providerKeys,
25
+ categoryKey: input.categoryKeys,
26
+ projectStatus: input.projectStatus,
27
+ connectionStatus: input.connectionStatus,
28
+ tenantId: input.tenantId,
29
+ };
30
+ }
20
31
  function withoutCacheControl(input) {
21
32
  const { cache: _cache, ...rest } = input;
22
33
  return rest;
@@ -35,8 +46,69 @@ function stableSdkStringify(value) {
35
46
  const record = value;
36
47
  return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableSdkStringify(record[key])}`).join(',')}}`;
37
48
  }
38
- function uniqueSdkStrings(values) {
39
- return Array.from(new Set(values));
49
+ const sdkDiscoveryMcpMethods = new Set([
50
+ 'initialize',
51
+ 'tools/list',
52
+ 'resources/list',
53
+ 'resources/templates/list',
54
+ 'prompts/list',
55
+ ]);
56
+ let sdkRequestSequence = 0;
57
+ function mcpRequestIsDiscovery(input) {
58
+ const requests = Array.isArray(input) ? input : [input];
59
+ return requests.length > 0 && requests.every((request) => (typeof request.method === 'string' && sdkDiscoveryMcpMethods.has(request.method)));
60
+ }
61
+ function positiveSdkInteger(value, field) {
62
+ if (!Number.isInteger(value) || value <= 0) {
63
+ throw new Error(`Stacklink ${field} must be a positive integer.`);
64
+ }
65
+ return value;
66
+ }
67
+ function sdkString(value) {
68
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
69
+ }
70
+ function createSdkRequestId() {
71
+ const randomUuid = globalThis.crypto?.randomUUID?.();
72
+ if (randomUuid)
73
+ return randomUuid;
74
+ sdkRequestSequence += 1;
75
+ return `stacklink-${Date.now()}-${sdkRequestSequence}`;
76
+ }
77
+ function sdkRetrySafe(method, idempotencyKey) {
78
+ return method === 'GET' || method === 'HEAD' || method === 'OPTIONS' || Boolean(idempotencyKey);
79
+ }
80
+ function sdkRetryableStatus(status) {
81
+ return status === 408 || status === 429 || status === 502 || status === 503 || status === 504;
82
+ }
83
+ function sdkRetryDelayMs(response, attempt) {
84
+ const retryAfter = response?.headers.get('retry-after');
85
+ if (retryAfter) {
86
+ const seconds = Number(retryAfter);
87
+ if (Number.isFinite(seconds) && seconds >= 0)
88
+ return Math.ceil(seconds * 1000);
89
+ const at = Date.parse(retryAfter);
90
+ if (Number.isFinite(at))
91
+ return Math.max(0, at - Date.now());
92
+ }
93
+ return Math.min(1_000, 100 * (2 ** Math.max(0, attempt - 1)));
94
+ }
95
+ function sdkDelay(ms, signal) {
96
+ if (signal.aborted)
97
+ return Promise.reject(signal.reason ?? new Error('Stacklink request was aborted.'));
98
+ return new Promise((resolve, reject) => {
99
+ const timeout = setTimeout(() => {
100
+ signal.removeEventListener('abort', onAbort);
101
+ resolve();
102
+ }, ms);
103
+ const onAbort = () => {
104
+ clearTimeout(timeout);
105
+ reject(signal.reason ?? new Error('Stacklink request was aborted.'));
106
+ };
107
+ signal.addEventListener('abort', onAbort, { once: true });
108
+ });
109
+ }
110
+ function sdkTimeoutError(timeoutMs, requestId) {
111
+ return new StacklinkApiError(`Stacklink request exceeded its ${timeoutMs} ms deadline.`, 408, 'sdk_request_timeout', { requestId, timeoutMs });
40
112
  }
41
113
  function normalizeKnowledgePackSelection(value) {
42
114
  if (Array.isArray(value)) {
@@ -69,6 +141,39 @@ export async function verifyWebhookSignature(input) {
69
141
  }
70
142
  return false;
71
143
  }
144
+ /** Sign the exact JSON bytes sent to a saved inbound Bot webhook. Reuse requestId when retrying. */
145
+ export async function signBotTriggerRequest(input) {
146
+ const cryptoImpl = globalThis.crypto ?? (await import('node:crypto')).webcrypto;
147
+ const timestamp = String(Math.floor((input.now ?? new Date()).getTime() / 1000));
148
+ const requestId = input.requestId ?? cryptoImpl.randomUUID();
149
+ const digest = await cryptoImpl.subtle.digest('SHA-256', utf8(input.body));
150
+ const digestHex = Array.from(new Uint8Array(digest), value => value.toString(16).padStart(2, '0')).join('');
151
+ const key = await cryptoImpl.subtle.importKey('raw', utf8(input.secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
152
+ const signature = await cryptoImpl.subtle.sign('HMAC', key, utf8(JSON.stringify(['stacklink-trigger-v1', input.triggerId, timestamp, requestId, digestHex])));
153
+ return { 'content-type': 'application/json', 'stacklink-timestamp': timestamp, 'stacklink-request-id': requestId, 'stacklink-signature': Array.from(new Uint8Array(signature), value => value.toString(16).padStart(2, '0')).join('') };
154
+ }
155
+ /** Verify the original request bytes before using identity. consumeRequestId must atomically reject replays. */
156
+ export async function verifyBotApiRequest(input) {
157
+ const headers = new Headers(input.headers);
158
+ const get = (name) => headers.get(`x-stacklink-${name}`) ?? '';
159
+ const keyId = get('key-id'), timestamp = get('timestamp'), requestId = get('request-id');
160
+ const key = input.keys[keyId], projectId = get('project'), tenantId = get('tenant'), userId = get('customer');
161
+ const secret = typeof key === 'string' ? key : key?.secret;
162
+ const seconds = Number(timestamp), now = Math.floor((input.now ?? new Date()).getTime() / 1000), tolerance = input.toleranceSeconds ?? 300;
163
+ if (key && typeof key !== 'string' && key.validUntil !== null && (!Number.isFinite(Date.parse(key.validUntil)) || Date.parse(key.validUntil) <= now * 1000))
164
+ return null;
165
+ if (typeof secret !== 'string' || !secret || !tenantId || !requestId || projectId !== input.projectId || !Number.isInteger(seconds) || Math.abs(now - seconds) > tolerance)
166
+ return null;
167
+ const cryptoImpl = globalThis.crypto ?? (await import('node:crypto')).webcrypto;
168
+ const digest = await cryptoImpl.subtle.digest('SHA-256', utf8(input.body));
169
+ const bodySha256 = Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
170
+ const expected = await hmacSha256Base64(secret, botApiSignaturePayload({ method: input.method, url: input.url, requestId, timestamp, keyId, projectId, tenantId, userId, bodySha256 }));
171
+ if (!constantTimeStringEqual(get('signature'), `v1,${expected}`))
172
+ return null;
173
+ if (!await input.consumeRequestId(requestId, new Date((seconds + tolerance) * 1000)))
174
+ return null;
175
+ return { tenantId, userId: userId || null, requestId };
176
+ }
72
177
  function signatureCandidates(header) {
73
178
  const candidates = [];
74
179
  for (const entry of header.split(' ')) {
@@ -120,6 +225,11 @@ export class Stacklink {
120
225
  fetchImpl;
121
226
  cacheOptions;
122
227
  responseCache = new Map();
228
+ discoveryRequests = new Map();
229
+ timeoutMs;
230
+ maxAttempts;
231
+ bots;
232
+ approvals;
123
233
  webhooks;
124
234
  triggers;
125
235
  sessions;
@@ -136,6 +246,8 @@ export class Stacklink {
136
246
  this.registrationKey = options.registrationKey;
137
247
  this.baseUrl = options.baseUrl.endsWith('/') ? options.baseUrl.slice(0, -1) : options.baseUrl;
138
248
  this.fetchImpl = options.fetch ?? fetch;
249
+ this.timeoutMs = positiveSdkInteger(options.timeoutMs ?? 15_000, 'timeoutMs');
250
+ this.maxAttempts = Math.min(3, positiveSdkInteger(options.maxAttempts ?? 3, 'maxAttempts'));
139
251
  this.cacheOptions = {
140
252
  enabled: options.cache?.enabled ?? true,
141
253
  toolsListTtlMs: options.cache?.toolsListTtlMs ?? 300_000,
@@ -144,6 +256,8 @@ export class Stacklink {
144
256
  searchTtlMs: options.cache?.searchTtlMs ?? 120_000,
145
257
  maxEntries: options.cache?.maxEntries ?? 500,
146
258
  };
259
+ this.bots = new StacklinkBotsClient(this);
260
+ this.approvals = new StacklinkApprovalsClient(this);
147
261
  this.webhooks = new StacklinkWebhooksClient(this);
148
262
  this.triggers = new StacklinkTriggersClient(this);
149
263
  this.sessions = new StacklinkSessionsClient(this);
@@ -176,27 +290,24 @@ export class Stacklink {
176
290
  userEmail: input.userEmail,
177
291
  scopes: input.scopes ?? [],
178
292
  toolRouter: input.toolRouter,
179
- preset: input.preset,
180
- preload: input.preload,
293
+ modelSurface: input.modelSurface,
181
294
  toolkits: input.toolkits,
182
295
  tools: input.tools,
183
296
  authConfigs: input.authConfigs,
184
297
  connectedAccounts: input.connectedAccounts,
298
+ providerMutationMode: input.providerMutationMode,
185
299
  multiAccount: input.multiAccount,
186
300
  workbench: input.workbench,
187
301
  knowledgePacks: input.knowledgePacks,
188
302
  memory: input.memory,
189
- metaTools: input.metaTools,
190
303
  metadata: input.metadata ?? {},
191
304
  },
192
305
  });
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);
306
+ return new ToolRouterSession(this, response.session.id, response.session.tenantId, response.session.mcp, response.metaTools.contractVersion, response.metaTools.tools);
195
307
  }
196
308
  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);
309
+ const response = await this.request(`/v1/sessions/${encodeURIComponent(sessionId)}/bootstrap`);
310
+ return new ToolRouterSession(this, response.session.id, response.session.tenantId, response.session.mcp, response.metaTools.contractVersion, response.metaTools.tools);
200
311
  }
201
312
  clearCache(prefix) {
202
313
  if (!prefix) {
@@ -262,19 +373,24 @@ export class Stacklink {
262
373
  method: 'DELETE',
263
374
  });
264
375
  }
265
- async listProviders() {
266
- return this.request('/v1/providers');
376
+ async listProviders(query = {}) {
377
+ return this.request('/v1/providers', {
378
+ query: providerCatalogSdkQuery(query),
379
+ });
267
380
  }
268
381
  async listProjectProviders(query = {}) {
269
382
  return this.request('/v1/project/providers', {
270
- query: { tenantId: query.tenantId },
383
+ query: providerCatalogSdkQuery(query),
271
384
  });
272
385
  }
273
386
  async listProviderTools(providerKey, input = {}) {
274
387
  return this.request(`/v1/providers/${providerKey}/tools`, {
275
388
  query: {
389
+ limit: input.limit,
390
+ cursor: input.cursor,
391
+ search: input.search,
392
+ capability: input.capability,
276
393
  includePlanned: input.includePlanned === true ? 'true' : undefined,
277
- responseMode: input.responseMode === 'compact' ? 'compact' : undefined,
278
394
  },
279
395
  });
280
396
  }
@@ -360,6 +476,10 @@ export class Stacklink {
360
476
  method: 'POST',
361
477
  body: input,
362
478
  signal: options.signal,
479
+ timeoutMs: options.timeoutMs ?? 60_000,
480
+ idempotencyKey: options.idempotencyKey ?? sdkString(input.idempotencyKey),
481
+ requestId: options.requestId,
482
+ traceparent: options.traceparent,
363
483
  });
364
484
  }
365
485
  async executeToolsBatch(input, options = {}) {
@@ -367,6 +487,10 @@ export class Stacklink {
367
487
  method: 'POST',
368
488
  body: input,
369
489
  signal: options.signal,
490
+ timeoutMs: options.timeoutMs ?? 60_000,
491
+ idempotencyKey: options.idempotencyKey ?? sdkString(input.idempotencyKey),
492
+ requestId: options.requestId,
493
+ traceparent: options.traceparent,
370
494
  });
371
495
  }
372
496
  async getSubscription() {
@@ -611,7 +735,7 @@ export class Stacklink {
611
735
  const executionKind = accepted.executionKind === 'workbench_code' || accepted.executionKind === 'workbench_bash'
612
736
  ? accepted.executionKind
613
737
  : undefined;
614
- if (!sessionId || !executionId || !executionKind)
738
+ if (!sessionId || !executionId || !executionKind || accepted.status === 'waiting_for_approval')
615
739
  return accepted;
616
740
  while (true) {
617
741
  const waited = await this.waitForExecution({
@@ -619,10 +743,9 @@ export class Stacklink {
619
743
  executionKind,
620
744
  executionId,
621
745
  waitTimeoutMs: 10_000,
622
- pollIntervalMs: 500,
623
746
  }, options);
624
747
  const execution = isSdkRecord(waited.execution) ? waited.execution : undefined;
625
- if (execution && isTerminalSdkExecution(execution)) {
748
+ if (execution && (isTerminalSdkExecution(execution) || execution.status === 'waiting_for_approval')) {
626
749
  return materializeStacklinkWorkbenchExecutionResponse(accepted, execution);
627
750
  }
628
751
  }
@@ -660,15 +783,110 @@ export class Stacklink {
660
783
  signal: options.signal,
661
784
  });
662
785
  }
786
+ async *streamBotEvents(botId, runId, options = {}) {
787
+ let cursor = options.after ?? '0';
788
+ const deadline = Date.now() + positiveSdkInteger(options.totalTimeoutMs ?? 300_000, 'totalTimeoutMs');
789
+ const terminal = (event) => ['run.completed', 'run.failed', 'run.stopped'].includes(String(event.type));
790
+ while (!options.signal?.aborted) {
791
+ const remaining = deadline - Date.now();
792
+ if (remaining <= 0)
793
+ throw new StacklinkApiError('Bot observation timed out. The same run may still be active; resume with the last sequence.', 408, 'sdk_stream_timeout', { runId, after: cursor });
794
+ try {
795
+ const response = await this.requestRaw(`/v1/bots/${encodeURIComponent(botId)}/runs/${encodeURIComponent(runId)}/events/stream`, { headers: { accept: 'text/event-stream', 'Last-Event-ID': cursor }, signal: options.signal, timeoutMs: Math.min(options.timeoutMs ?? 30_000, remaining) });
796
+ for await (const event of this.readJsonEventStream(response)) {
797
+ if (!isSdkRecord(event) || typeof event.sequence !== 'string')
798
+ continue;
799
+ if (event.runId !== runId)
800
+ throw new StacklinkApiError('Bot event belongs to a different run.', 502, 'bot_event_invalid');
801
+ if (BigInt(event.sequence) <= BigInt(cursor))
802
+ continue;
803
+ cursor = event.sequence;
804
+ yield event;
805
+ if (terminal(event))
806
+ return;
807
+ }
808
+ }
809
+ catch (error) {
810
+ if (options.signal?.aborted)
811
+ throw options.signal.reason ?? error;
812
+ const retryable = error instanceof TypeError || (error instanceof Error && ['AbortError', 'TimeoutError'].includes(error.name))
813
+ || (error instanceof StacklinkApiError && [408, 429, 502, 503, 504].includes(error.statusCode) && error.code !== 'bot_event_invalid');
814
+ if (!retryable)
815
+ throw error;
816
+ }
817
+ // Fetch the durable tail before considering completion. A status read alone
818
+ // could miss the final text and terminal event that arrived during a disconnect.
819
+ const page = await this.bots.events(botId, runId, cursor);
820
+ for (const event of page.events) {
821
+ if (BigInt(event.sequence) <= BigInt(cursor))
822
+ continue;
823
+ cursor = event.sequence;
824
+ yield event;
825
+ if (terminal(event))
826
+ return;
827
+ }
828
+ if (page.terminal && page.events.length === 0)
829
+ return;
830
+ if (page.events.length < 200)
831
+ await sdkDelay(Math.min(1000, Math.max(1, deadline - Date.now())), options.signal ?? AbortSignal.timeout(Math.max(1, deadline - Date.now())));
832
+ }
833
+ if (options.signal?.aborted)
834
+ throw options.signal.reason;
835
+ }
836
+ async *streamExecutionEvents(input, options = {}) {
837
+ let afterSequence = input.afterSequence === undefined ? undefined : String(input.afterSequence);
838
+ while (!options.signal?.aborted) {
839
+ const response = await this.requestRaw(`/v1/executions/${input.executionKind}/${input.executionId}/events/stream`, {
840
+ headers: {
841
+ accept: 'text/event-stream',
842
+ ...(afterSequence === undefined ? {} : { 'Last-Event-ID': afterSequence }),
843
+ },
844
+ query: {
845
+ sessionId: input.sessionId,
846
+ afterSequence,
847
+ limit: input.limit,
848
+ },
849
+ signal: options.signal,
850
+ timeoutMs: options.timeoutMs ?? 30_000,
851
+ });
852
+ try {
853
+ for await (const payload of this.readJsonEventStream(response)) {
854
+ if (!isSdkRecord(payload))
855
+ continue;
856
+ const events = Array.isArray(payload.events) ? payload.events : [];
857
+ for (const event of events) {
858
+ if (isSdkRecord(event) && (typeof event.sequence === 'string' || typeof event.sequence === 'number')) {
859
+ afterSequence = String(event.sequence);
860
+ }
861
+ yield event;
862
+ }
863
+ if (typeof payload.nextCursor === 'string')
864
+ afterSequence = payload.nextCursor;
865
+ if (payload.terminal === true)
866
+ return;
867
+ }
868
+ }
869
+ catch (error) {
870
+ if (options.signal?.aborted)
871
+ throw options.signal.reason ?? error;
872
+ const errorName = error instanceof Error ? error.name : '';
873
+ if (errorName !== 'AbortError' && errorName !== 'TimeoutError')
874
+ throw error;
875
+ // The per-connection deadline bounds a stream. Reconnect from the durable cursor.
876
+ }
877
+ }
878
+ if (options.signal?.aborted)
879
+ throw options.signal.reason ?? new Error('Stacklink execution stream was aborted.');
880
+ }
663
881
  async waitForExecution(input, options = {}) {
664
882
  return this.request(`/v1/executions/${input.executionKind}/${input.executionId}/wait`, {
665
883
  method: 'POST',
666
884
  body: {
667
885
  sessionId: input.sessionId,
668
886
  waitTimeoutMs: input.waitTimeoutMs,
669
- pollIntervalMs: input.pollIntervalMs,
670
887
  },
671
- signal: options.signal,
888
+ ...options,
889
+ timeoutMs: options.timeoutMs ?? (input.waitTimeoutMs ?? 30_000) + 5_000,
672
890
  });
673
891
  }
674
892
  async cancelExecution(input, options = {}) {
@@ -719,7 +937,7 @@ export class Stacklink {
719
937
  async getConnectedAccount(accountId) {
720
938
  return this.request(`/v1/connections/${accountId}`);
721
939
  }
722
- async listConnectedAccountTools(accountId, input = {}) {
940
+ async listConnectedAccountTools(accountId, input) {
723
941
  return this.request(`/v1/connections/${accountId}/tools`, {
724
942
  query: {
725
943
  ...(input.includePlanned !== undefined ? { includePlanned: String(input.includePlanned) } : {}),
@@ -985,21 +1203,35 @@ export class Stacklink {
985
1203
  method: 'DELETE',
986
1204
  });
987
1205
  }
988
- async mcpRequest(input, options = {}) {
989
- return this.mcpRequestAtPath('/v1/mcp', input, options);
990
- }
991
1206
  async mcpSessionRequest(sessionId, input, options = {}) {
992
1207
  return this.mcpRequestAtPath(`/v1/sessions/${encodeURIComponent(sessionId)}/mcp`, input, options);
993
1208
  }
994
1209
  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);
1210
+ if (!Array.isArray(input))
1211
+ return this.mcpRequestStreamAtPath(path, input, {
1212
+ ...options, timeoutMs: options.timeoutMs ?? (mcpRequestIsDiscovery(input) ? this.timeoutMs : 60_000),
1213
+ });
1214
+ const response = await this.requestRaw(path, { method: 'POST', body: input, headers: { accept: 'application/json, text/event-stream' },
1215
+ ...options, timeoutMs: options.timeoutMs ?? 60_000 });
1216
+ if (response.status === 202)
1217
+ return null;
1218
+ const value = await response.json();
1219
+ if (!Array.isArray(value))
1220
+ throw new StacklinkApiError('Expected MCP batch responses.', 502, 'mcp_response_invalid');
1221
+ const expected = input.filter((entry) => entry.id !== undefined && entry.id !== null);
1222
+ if (expected.length !== value.length)
1223
+ throw new StacklinkApiError('MCP batch response count does not match.', 502, 'mcp_response_invalid');
1224
+ const seen = new Set();
1225
+ for (const message of value) {
1226
+ if (!isSdkRecord(message) || seen.has(message.id))
1227
+ throw new StacklinkApiError('Duplicate or invalid MCP response.', 502, 'mcp_response_invalid');
1228
+ const request = expected.find((entry) => entry.id === message.id);
1229
+ if (!request)
1230
+ throw new StacklinkApiError('MCP response belongs to another request.', 502, 'mcp_response_invalid');
1231
+ validateSdkMcpResponse(message, request.id);
1232
+ seen.add(message.id);
1233
+ }
1234
+ return value;
1003
1235
  }
1004
1236
  async mcpSessionRequestStream(sessionId, input, options = {}) {
1005
1237
  return this.mcpRequestStreamAtPath(`/v1/sessions/${encodeURIComponent(sessionId)}/mcp`, input, options);
@@ -1032,49 +1264,40 @@ export class Stacklink {
1032
1264
  method: 'POST',
1033
1265
  body: input,
1034
1266
  signal: options.signal,
1267
+ timeoutMs: options.timeoutMs ?? 60_000,
1268
+ idempotencyKey: options.idempotencyKey,
1269
+ requestId: options.requestId,
1270
+ traceparent: options.traceparent,
1035
1271
  headers: {
1036
1272
  accept: 'text/event-stream, application/json',
1037
1273
  },
1038
1274
  });
1039
1275
  const contentType = response.headers.get('content-type') ?? '';
1276
+ if (response.status === 202 && (input.id === undefined || input.id === null))
1277
+ return null;
1040
1278
  if (!contentType.includes('text/event-stream')) {
1041
- return await response.json().catch(() => null);
1279
+ const value = await response.json();
1280
+ validateSdkMcpResponse(value, input.id);
1281
+ return value;
1042
1282
  }
1043
- return await this.readMcpEventStream(response, options.onProgress);
1283
+ return await this.readMcpEventStream(response, input, options.onProgress);
1044
1284
  }
1045
1285
  finally {
1046
1286
  completed = true;
1047
1287
  options.signal?.removeEventListener('abort', cancel);
1048
1288
  }
1049
1289
  }
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({
1059
- jsonrpc: '2.0',
1060
- id: `${name}-call`,
1061
- method: 'tools/call',
1062
- params: {
1063
- name,
1064
- arguments: args,
1065
- },
1066
- });
1067
- }
1068
- async callSessionMcpTool(sessionId, name, args) {
1069
- return this.mcpSessionRequest(sessionId, {
1290
+ async callSessionMcpTool(sessionId, name, args, options = {}) {
1291
+ return this.mcpSessionRequestStream(sessionId, {
1070
1292
  jsonrpc: '2.0',
1071
- id: `${name}-call`,
1293
+ id: createSdkRequestId(),
1072
1294
  method: 'tools/call',
1073
1295
  params: {
1074
1296
  name,
1297
+ ...(options.onProgress ? { _meta: { progressToken: createSdkRequestId() } } : {}),
1075
1298
  arguments: args,
1076
1299
  },
1077
- });
1300
+ }, options);
1078
1301
  }
1079
1302
  async listKnowledgePacks() {
1080
1303
  return this.request('/v1/knowledge/packs');
@@ -1221,26 +1444,17 @@ export class Stacklink {
1221
1444
  signal.addEventListener('abort', cancel, { once: true });
1222
1445
  }
1223
1446
  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
- }
1447
+ const response = await this.performRequest(path, init);
1448
+ const responseText = await response.text();
1449
+ let body = null;
1450
+ if (responseText.length > 0) {
1451
+ try {
1452
+ body = JSON.parse(responseText);
1231
1453
  }
1232
- else if (value !== undefined && value !== null) {
1233
- url.searchParams.set(key, String(value));
1454
+ catch {
1455
+ body = null;
1234
1456
  }
1235
1457
  }
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
1458
  if (!response.ok) {
1245
1459
  const message = body && typeof body === 'object' && 'message' in body
1246
1460
  ? String(body.message)
@@ -1256,6 +1470,21 @@ export class Stacklink {
1256
1470
  return body;
1257
1471
  }
1258
1472
  async requestRaw(path, init = {}) {
1473
+ const response = await this.performRequest(path, init);
1474
+ if (!response.ok) {
1475
+ const body = await response.text();
1476
+ let parsed;
1477
+ try {
1478
+ parsed = JSON.parse(body);
1479
+ }
1480
+ catch {
1481
+ parsed = null;
1482
+ }
1483
+ 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);
1484
+ }
1485
+ return response;
1486
+ }
1487
+ async performRequest(path, init) {
1259
1488
  const url = new URL(`${this.baseUrl}${path}`);
1260
1489
  for (const [key, value] of Object.entries(init.query ?? {})) {
1261
1490
  if (Array.isArray(value)) {
@@ -1268,17 +1497,55 @@ export class Stacklink {
1268
1497
  url.searchParams.set(key, String(value));
1269
1498
  }
1270
1499
  }
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);
1500
+ const method = init.method ?? 'GET';
1501
+ const timeoutMs = positiveSdkInteger(init.timeoutMs ?? this.timeoutMs, 'timeoutMs');
1502
+ const deadlineAt = Date.now() + timeoutMs;
1503
+ const requestId = init.requestId ?? createSdkRequestId();
1504
+ const retrySafe = !path.endsWith('/mcp') && sdkRetrySafe(method, init.idempotencyKey);
1505
+ let attempt = 0;
1506
+ while (true) {
1507
+ attempt += 1;
1508
+ const remainingMs = deadlineAt - Date.now();
1509
+ if (remainingMs <= 0)
1510
+ throw sdkTimeoutError(timeoutMs, requestId);
1511
+ const deadlineSignal = AbortSignal.timeout(remainingMs);
1512
+ const signal = init.signal ? AbortSignal.any([init.signal, deadlineSignal]) : deadlineSignal;
1513
+ const headers = this.requestHeaders(init.registration === true, {
1514
+ ...init.headers,
1515
+ 'X-Stacklink-Client-Request-Id': requestId,
1516
+ 'X-Stacklink-Timeout-Ms': String(remainingMs),
1517
+ ...(init.idempotencyKey ? { 'Idempotency-Key': init.idempotencyKey } : {}),
1518
+ ...(init.traceparent ? { traceparent: init.traceparent } : {}),
1519
+ });
1520
+ try {
1521
+ const response = await this.fetchImpl(url, {
1522
+ method,
1523
+ headers,
1524
+ signal,
1525
+ ...(init.body === undefined ? {} : { body: JSON.stringify(init.body) }),
1526
+ });
1527
+ if (!retrySafe || attempt >= this.maxAttempts || !sdkRetryableStatus(response.status)) {
1528
+ return response;
1529
+ }
1530
+ const delayMs = sdkRetryDelayMs(response, attempt);
1531
+ if (delayMs >= deadlineAt - Date.now())
1532
+ return response;
1533
+ await response.body?.cancel().catch(() => undefined);
1534
+ await sdkDelay(delayMs, signal);
1535
+ }
1536
+ catch (error) {
1537
+ if (init.signal?.aborted)
1538
+ throw init.signal.reason ?? error;
1539
+ if (deadlineSignal.aborted || Date.now() >= deadlineAt)
1540
+ throw sdkTimeoutError(timeoutMs, requestId);
1541
+ if (!retrySafe || attempt >= this.maxAttempts)
1542
+ throw error;
1543
+ const delayMs = sdkRetryDelayMs(undefined, attempt);
1544
+ if (delayMs >= deadlineAt - Date.now())
1545
+ throw error;
1546
+ await sdkDelay(delayMs, signal);
1547
+ }
1280
1548
  }
1281
- return response;
1282
1549
  }
1283
1550
  async cachedDiscoveryRequest(cacheKey, ttlMs, control, producer) {
1284
1551
  if (!this.cacheOptions.enabled || control?.cache === 'bypass')
@@ -1287,13 +1554,21 @@ export class Stacklink {
1287
1554
  const existing = this.responseCache.get(cacheKey);
1288
1555
  if (existing && existing.expiresAt > now)
1289
1556
  return existing.value;
1290
- const value = await producer();
1291
- this.responseCache.set(cacheKey, {
1292
- value,
1293
- expiresAt: now + ttlMs,
1557
+ const active = this.discoveryRequests.get(cacheKey);
1558
+ if (active)
1559
+ return active;
1560
+ const request = producer().then((value) => {
1561
+ this.responseCache.set(cacheKey, {
1562
+ value,
1563
+ expiresAt: Date.now() + ttlMs,
1564
+ });
1565
+ this.trimResponseCache();
1566
+ return value;
1567
+ }).finally(() => {
1568
+ this.discoveryRequests.delete(cacheKey);
1294
1569
  });
1295
- this.trimResponseCache();
1296
- return value;
1570
+ this.discoveryRequests.set(cacheKey, request);
1571
+ return request;
1297
1572
  }
1298
1573
  discoveryCacheTtl(kind) {
1299
1574
  if (kind === 'toolsList')
@@ -1313,7 +1588,7 @@ export class Stacklink {
1313
1588
  this.responseCache.delete(firstKey);
1314
1589
  }
1315
1590
  }
1316
- async readMcpEventStream(response, onProgress) {
1591
+ async readMcpEventStream(response, request, onProgress) {
1317
1592
  if (!response.body)
1318
1593
  throw new Error('Stacklink MCP stream response has no body.');
1319
1594
  const reader = response.body.getReader();
@@ -1321,6 +1596,10 @@ export class Stacklink {
1321
1596
  let buffer = '';
1322
1597
  let dataLines = [];
1323
1598
  let finalResponse;
1599
+ let bytes = 0;
1600
+ const requestParams = isSdkRecord(request.params) ? request.params : {};
1601
+ const meta = isSdkRecord(requestParams._meta) ? requestParams._meta : {};
1602
+ const progressToken = meta.progressToken;
1324
1603
  const handleLine = (line) => {
1325
1604
  if (line.endsWith('\r'))
1326
1605
  line = line.slice(0, -1);
@@ -1330,10 +1609,24 @@ export class Stacklink {
1330
1609
  dataLines = [];
1331
1610
  if (data.length > 0) {
1332
1611
  const message = JSON.parse(data);
1612
+ if (!isSdkRecord(message) || message.jsonrpc !== '2.0') {
1613
+ throw new StacklinkApiError('Stacklink returned an invalid MCP message.', 502, 'mcp_response_invalid');
1614
+ }
1333
1615
  if (isSdkRecord(message) && message.method === 'notifications/progress' && isSdkRecord(message.params)) {
1334
- onProgress?.(message.params);
1616
+ if (progressToken !== undefined && message.params.progressToken === progressToken) {
1617
+ onProgress?.(message.params);
1618
+ }
1619
+ }
1620
+ else if (typeof message.method === 'string' && message.id !== undefined) {
1621
+ throw new StacklinkApiError('The server requested a capability this SDK did not declare.', 502, 'mcp_capability_not_supported');
1335
1622
  }
1336
- else if (isSdkRecord(message) && ('result' in message || 'error' in message)) {
1623
+ else if ('result' in message || 'error' in message) {
1624
+ if (message.id !== request.id)
1625
+ return;
1626
+ if (finalResponse !== undefined || ('result' in message) === ('error' in message)) {
1627
+ throw new StacklinkApiError('Stacklink returned a conflicting MCP result.', 502, 'mcp_response_invalid');
1628
+ }
1629
+ validateSdkMcpResponse(message, request.id);
1337
1630
  finalResponse = message;
1338
1631
  }
1339
1632
  }
@@ -1347,25 +1640,92 @@ export class Stacklink {
1347
1640
  dataLines.push(value);
1348
1641
  }
1349
1642
  };
1350
- while (true) {
1351
- const read = await reader.read();
1352
- if (read.done)
1353
- break;
1354
- buffer += decoder.decode(read.value, { stream: true });
1355
- let index = buffer.indexOf('\n');
1356
- while (index >= 0) {
1357
- handleLine(buffer.slice(0, index));
1358
- buffer = buffer.slice(index + 1);
1359
- index = buffer.indexOf('\n');
1643
+ try {
1644
+ while (true) {
1645
+ const read = await reader.read();
1646
+ if (read.done)
1647
+ break;
1648
+ bytes += read.value.byteLength;
1649
+ if (bytes > 8 * 1024 * 1024)
1650
+ throw new StacklinkApiError('MCP response exceeds 8 MiB.', 502, 'mcp_response_too_large');
1651
+ buffer += decoder.decode(read.value, { stream: true });
1652
+ let index = buffer.indexOf('\n');
1653
+ while (index >= 0) {
1654
+ handleLine(buffer.slice(0, index));
1655
+ buffer = buffer.slice(index + 1);
1656
+ if (finalResponse !== undefined)
1657
+ return finalResponse;
1658
+ index = buffer.indexOf('\n');
1659
+ }
1660
+ }
1661
+ buffer += decoder.decode();
1662
+ if (buffer.length > 0)
1663
+ handleLine(buffer);
1664
+ handleLine('');
1665
+ if (finalResponse === undefined)
1666
+ throw new Error('Stacklink MCP stream ended without a final JSON-RPC response.');
1667
+ return finalResponse;
1668
+ }
1669
+ finally {
1670
+ await reader.cancel().catch(() => undefined);
1671
+ reader.releaseLock();
1672
+ }
1673
+ }
1674
+ async *readJsonEventStream(response) {
1675
+ if (!response.body)
1676
+ throw new Error('Stacklink event stream response has no body.');
1677
+ const reader = response.body.getReader();
1678
+ const decoder = new TextDecoder();
1679
+ let buffer = '';
1680
+ let dataLines = [];
1681
+ const payloadForLine = (lineValue) => {
1682
+ let line = lineValue;
1683
+ if (line.endsWith('\r'))
1684
+ line = line.slice(0, -1);
1685
+ if (line === '') {
1686
+ if (dataLines.length === 0)
1687
+ return undefined;
1688
+ const data = dataLines.join('\n');
1689
+ dataLines = [];
1690
+ return data.length === 0 ? undefined : JSON.parse(data);
1360
1691
  }
1692
+ if (line.startsWith('data:')) {
1693
+ let value = line.slice(5);
1694
+ if (value.startsWith(' '))
1695
+ value = value.slice(1);
1696
+ dataLines.push(value);
1697
+ }
1698
+ return undefined;
1699
+ };
1700
+ try {
1701
+ while (true) {
1702
+ const read = await reader.read();
1703
+ if (read.done)
1704
+ break;
1705
+ buffer += decoder.decode(read.value, { stream: true });
1706
+ let index = buffer.indexOf('\n');
1707
+ while (index >= 0) {
1708
+ const payload = payloadForLine(buffer.slice(0, index));
1709
+ if (payload !== undefined)
1710
+ yield payload;
1711
+ buffer = buffer.slice(index + 1);
1712
+ index = buffer.indexOf('\n');
1713
+ }
1714
+ }
1715
+ buffer += decoder.decode();
1716
+ if (buffer.length > 0) {
1717
+ const payload = payloadForLine(buffer);
1718
+ if (payload !== undefined)
1719
+ yield payload;
1720
+ }
1721
+ const finalPayload = payloadForLine('');
1722
+ if (finalPayload !== undefined)
1723
+ yield finalPayload;
1724
+ }
1725
+ finally {
1726
+ await reader.cancel().catch(() => undefined);
1727
+ reader.releaseLock();
1361
1728
  }
1362
- 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;
1369
1729
  }
1370
1730
  requestHeaders(registration, extraHeaders = {}) {
1371
1731
  if (registration) {
@@ -1540,6 +1900,12 @@ export class StacklinkMemoryClient {
1540
1900
  body: input,
1541
1901
  });
1542
1902
  }
1903
+ reviewQuality(memoryId, input) {
1904
+ return this.client.request(`/v1/memory/records/${memoryId}/quality-review`, {
1905
+ method: 'POST',
1906
+ body: input,
1907
+ });
1908
+ }
1543
1909
  forget(memoryId, input) {
1544
1910
  return this.client.request(`/v1/memory/records/${memoryId}/forget`, {
1545
1911
  method: 'POST',
@@ -1629,8 +1995,8 @@ export class StacklinkMemoryClient {
1629
1995
  body: input,
1630
1996
  });
1631
1997
  }
1632
- deleteSource(bindingId) {
1633
- return this.client.request(`/v1/memory/sources/${bindingId}`, { method: 'DELETE' });
1998
+ deleteSource(bindingId, input) {
1999
+ return this.client.request(`/v1/memory/sources/${bindingId}`, { method: 'DELETE', body: input });
1634
2000
  }
1635
2001
  replaySourceItem(bindingId, itemId) {
1636
2002
  return this.client.request(`/v1/memory/sources/${bindingId}/items/${itemId}/replay`, {
@@ -1659,6 +2025,55 @@ export class StacklinkMemoryClient {
1659
2025
  return this.client.request(`/v1/memory/legal-holds/${holdId}/release`, { method: 'POST' });
1660
2026
  }
1661
2027
  }
2028
+ export class StacklinkApprovalsClient {
2029
+ client;
2030
+ constructor(client) {
2031
+ this.client = client;
2032
+ }
2033
+ /** Server-only access. Identity comes from the authenticated application session. */
2034
+ getCustomerGrant(sessionId) {
2035
+ return this.client.request('/v1/approvals/customer-grant', { query: { sessionId } });
2036
+ }
2037
+ saveCustomerGrant(sessionId, grant) {
2038
+ return this.client.request('/v1/approvals/customer-grant', { method: 'PUT', query: { sessionId }, body: grant });
2039
+ }
2040
+ removeCustomerGrant(sessionId, revision) {
2041
+ return this.client.request('/v1/approvals/customer-grant', { method: 'DELETE', query: { sessionId }, body: { revision } });
2042
+ }
2043
+ list(input) {
2044
+ return this.client.request('/v1/approvals', { query: { ...input } });
2045
+ }
2046
+ get(id, access) {
2047
+ return this.client.request(`/v1/approvals/${encodeURIComponent(id)}`, { query: { ...access } });
2048
+ }
2049
+ /** Decide only after displaying the exact proposal and checking permissions. Never resubmit the original action. */
2050
+ decide(id, access, decision) {
2051
+ return this.client.request(`/v1/approvals/${encodeURIComponent(id)}/decide`, { method: 'POST', query: { ...access }, body: decision });
2052
+ }
2053
+ change(id, access, change) {
2054
+ return this.client.request(`/v1/approvals/${encodeURIComponent(id)}/change`, { method: 'POST', query: { ...access }, body: change });
2055
+ }
2056
+ events(access, after = '0', options = {}) {
2057
+ return this.client.request('/v1/approvals/events', { ...options, query: { ...access, after } });
2058
+ }
2059
+ /** Durable cursor observation. Save each sequence and pass it as `after` when reconnecting. */
2060
+ async *observe(access, options = {}) {
2061
+ let cursor = options.after ?? '0';
2062
+ const deadline = Date.now() + (options.timeoutMs ?? 300_000);
2063
+ const signal = options.signal ?? AbortSignal.timeout(Math.max(1, deadline - Date.now()));
2064
+ while (!signal.aborted && Date.now() < deadline) {
2065
+ const page = await this.events(access, cursor, { signal, timeoutMs: Math.min(15_000, Math.max(1, deadline - Date.now())) });
2066
+ for (const event of page.events) {
2067
+ if (BigInt(event.sequence) <= BigInt(cursor))
2068
+ continue;
2069
+ cursor = event.sequence;
2070
+ yield event;
2071
+ }
2072
+ if (page.events.length < 100)
2073
+ await sdkDelay(Math.min(1000, Math.max(1, deadline - Date.now())), signal);
2074
+ }
2075
+ }
2076
+ }
1662
2077
  export class StacklinkSessionsClient {
1663
2078
  client;
1664
2079
  constructor(client) {
@@ -1688,8 +2103,10 @@ export class StacklinkSessionsClient {
1688
2103
  metaTools(sessionId) {
1689
2104
  return this.client.getSessionMetaTools(sessionId);
1690
2105
  }
1691
- toolkits(sessionId) {
1692
- return this.client.request(`/v1/sessions/${sessionId}/toolkits`);
2106
+ toolkits(sessionId, query = {}) {
2107
+ return this.client.request(`/v1/sessions/${sessionId}/toolkits`, {
2108
+ query: providerCatalogSdkQuery(query),
2109
+ });
1693
2110
  }
1694
2111
  streamEvents(sessionId, input = {}) {
1695
2112
  return this.client.requestRaw(`/v1/sessions/${sessionId}/events/stream`, {
@@ -1700,7 +2117,6 @@ export class StacklinkSessionsClient {
1700
2117
  after: input.after,
1701
2118
  eventType: input.eventTypes,
1702
2119
  limit: input.limit,
1703
- pollIntervalMs: input.pollIntervalMs,
1704
2120
  },
1705
2121
  });
1706
2122
  }
@@ -1809,7 +2225,7 @@ export class StacklinkConnectedAccountsClient {
1809
2225
  get(accountId) {
1810
2226
  return this.client.getConnectedAccount(accountId);
1811
2227
  }
1812
- listTools(accountId, input = {}) {
2228
+ listTools(accountId, input) {
1813
2229
  return this.client.listConnectedAccountTools(accountId, input);
1814
2230
  }
1815
2231
  listResources(accountId) {
@@ -1872,14 +2288,14 @@ export class StacklinkToolkitsClient {
1872
2288
  constructor(client) {
1873
2289
  this.client = client;
1874
2290
  }
1875
- list() {
1876
- return this.client.listProviders();
2291
+ list(query = {}) {
2292
+ return this.client.listProviders(query);
1877
2293
  }
1878
2294
  listProject(query = {}) {
1879
2295
  return this.client.listProjectProviders(query);
1880
2296
  }
1881
- listTools(providerKey) {
1882
- return this.client.listProviderTools(providerKey);
2297
+ listTools(providerKey, query = {}) {
2298
+ return this.client.listProviderTools(providerKey, query);
1883
2299
  }
1884
2300
  }
1885
2301
  export class StacklinkProviderToolsClient {
@@ -1952,12 +2368,6 @@ export class StacklinkSessionMemoryClient {
1952
2368
  ...(reason ? { reason } : {}),
1953
2369
  });
1954
2370
  }
1955
- restore(memoryId, reason) {
1956
- return this.memoryClient.restore(memoryId, {
1957
- sessionId: this.sessionId,
1958
- ...(reason ? { reason } : {}),
1959
- });
1960
- }
1961
2371
  }
1962
2372
  export class StacklinkWorkbenchRun {
1963
2373
  client;
@@ -1972,37 +2382,16 @@ export class StacklinkWorkbenchRun {
1972
2382
  this.executionId = response.executionId;
1973
2383
  this.executionKind = response.executionKind;
1974
2384
  }
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
- }
2385
+ events(input = {}) {
2386
+ return this.client.streamExecutionEvents({
2387
+ sessionId: this.platformSessionId,
2388
+ executionKind: this.executionKind,
2389
+ executionId: this.executionId,
2390
+ limit: input.limit ?? 200,
2391
+ }, {
2392
+ signal: input.signal,
2393
+ timeoutMs: input.streamTimeoutMs,
2394
+ });
2006
2395
  }
2007
2396
  wait(input = {}) {
2008
2397
  return this.client.waitForExecution({
@@ -2261,8 +2650,8 @@ export class ToolRouterSession {
2261
2650
  async mcpRequestStream(input, options = {}) {
2262
2651
  return this.client.mcpSessionRequestStream(this.id, input, options);
2263
2652
  }
2264
- async callMcpTool(name, args) {
2265
- return this.client.callSessionMcpTool(this.id, name, args);
2653
+ async callMcpTool(name, args, options = {}) {
2654
+ return this.client.callSessionMcpTool(this.id, name, args, options);
2266
2655
  }
2267
2656
  mcpConnection() {
2268
2657
  if (!this.mcp) {
@@ -2285,6 +2674,7 @@ export class ToolRouterSession {
2285
2674
  return this.toolDefinitions.map((definition) => ({
2286
2675
  ...definition,
2287
2676
  inputSchema: { ...definition.inputSchema },
2677
+ outputSchema: { ...definition.outputSchema },
2288
2678
  }));
2289
2679
  }
2290
2680
  async close() {
@@ -2305,18 +2695,17 @@ export class ToolRouterSession {
2305
2695
  async workbenchActivity() {
2306
2696
  return this.client.getSessionWorkbench(this.id);
2307
2697
  }
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;
2698
+ async toolkits(query = {}) {
2699
+ return this.client.request(`/v1/sessions/${this.id}/toolkits`, {
2700
+ query: providerCatalogSdkQuery(query),
2701
+ });
2311
2702
  }
2312
- async authorize(providerKey, input = {}) {
2703
+ async authorize(connectionRef, input = {}) {
2313
2704
  const response = await this.client.request(`/v1/sessions/${this.id}/connections/authorize`, {
2314
2705
  method: 'POST',
2315
2706
  body: {
2316
- providerKey,
2317
- callbackUrl: input.callbackUrl,
2318
- authConfigId: input.authConfigId,
2319
- authMode: input.authMode,
2707
+ connectionRef,
2708
+ oauthScopeProfile: input.oauthScopeProfile,
2320
2709
  metadata: input.metadata ?? {},
2321
2710
  },
2322
2711
  });
@@ -2409,70 +2798,19 @@ export class ToolRouterSession {
2409
2798
  this.client.clearCache(`session:${this.id}:`);
2410
2799
  return response;
2411
2800
  }
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`, {
2801
+ async search(input) {
2802
+ const searchInput = { queries: input.queries };
2803
+ 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
2804
  method: 'POST',
2422
2805
  body: discoveryServerBody(searchInput),
2423
2806
  }));
2424
2807
  }
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`, {
2808
+ async getContracts(contractRefs) {
2809
+ return this.client.request(`/v1/sessions/${this.id}/tools/contracts`, {
2430
2810
  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`, {
2440
- method: 'POST',
2441
- body: input,
2811
+ body: { contractRefs },
2442
2812
  });
2443
2813
  }
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
2814
  async searchKnowledge(input = {}) {
2477
2815
  return this.client.searchKnowledge({
2478
2816
  ...input,
@@ -2575,18 +2913,26 @@ export class ToolRouterSession {
2575
2913
  sessionId: this.id,
2576
2914
  });
2577
2915
  }
2578
- async executeTool(input, options = {}) {
2916
+ async execute(contractRef, input, execution = {}, options = {}) {
2917
+ if (execution.idempotencyKey !== undefined && options.idempotencyKey !== undefined
2918
+ && execution.idempotencyKey !== options.idempotencyKey) {
2919
+ throw new StacklinkApiError('Execution and request idempotency keys must match.', 400, 'idempotency_key_conflict');
2920
+ }
2921
+ const idempotencyKey = execution.idempotencyKey ?? options.idempotencyKey ?? createSdkRequestId();
2922
+ const { idempotencyKey: _childKey, ...item } = execution;
2579
2923
  return this.client.request(`/v1/sessions/${this.id}/tools/execute`, {
2580
2924
  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,
2925
+ body: {
2926
+ idempotencyKey,
2927
+ executions: [{
2928
+ contractRef,
2929
+ input,
2930
+ ...item,
2931
+ }],
2932
+ },
2589
2933
  ...options,
2934
+ timeoutMs: options.timeoutMs ?? 60_000,
2935
+ idempotencyKey,
2590
2936
  });
2591
2937
  }
2592
2938
  async runWorkbench(input, options = {}) {
@@ -2652,7 +2998,6 @@ export class ToolRouterSession {
2652
2998
  after: input.after,
2653
2999
  eventType: input.eventTypes,
2654
3000
  limit: input.limit === undefined ? undefined : String(input.limit),
2655
- pollIntervalMs: input.pollIntervalMs === undefined ? undefined : String(input.pollIntervalMs),
2656
3001
  },
2657
3002
  });
2658
3003
  }
@@ -2662,7 +3007,8 @@ export class ToolRouterSession {
2662
3007
  {
2663
3008
  description: definition.description,
2664
3009
  parameters: definition.inputSchema,
2665
- execute: async (input) => unwrapMcpToolResult(await this.callMcpTool(definition.name, input)),
3010
+ outputSchema: definition.outputSchema,
3011
+ execute: async (input, execution) => unwrapMcpToolResult(await this.callMcpTool(definition.name, input, { ...(execution?.abortSignal ? { signal: execution.abortSignal } : {}), ...(execution?.onProgress ? { onProgress: execution.onProgress } : {}) })),
2666
3012
  },
2667
3013
  ]));
2668
3014
  }
@@ -2779,4 +3125,55 @@ function isTerminalSdkExecution(value) {
2779
3125
  || status === 'cancelled'
2780
3126
  || status === 'timed_out';
2781
3127
  }
3128
+ function validateSdkMcpResponse(value, requestId) {
3129
+ if (!isSdkRecord(value) || value.jsonrpc !== '2.0' || value.id !== requestId || ('result' in value) === ('error' in value)
3130
+ || ('error' in value && (!isSdkRecord(value.error) || typeof value.error.code !== 'number' || typeof value.error.message !== 'string'))) {
3131
+ throw new StacklinkApiError('Stacklink returned an invalid or unrelated MCP response.', 502, 'mcp_response_invalid');
3132
+ }
3133
+ }
3134
+ /** Hosted bots share durable runs across the API, MCP and dashboard. */
3135
+ export class StacklinkBotsClient {
3136
+ client;
3137
+ constructor(client) {
3138
+ this.client = client;
3139
+ }
3140
+ apis() { return this.client.request('/v1/bots/apis'); }
3141
+ updateApi(apiId, revision, api) { return this.client.request(`/v1/bots/apis/${encodeURIComponent(apiId)}`, { method: 'PATCH', body: { revision, api } }); }
3142
+ deleteApi(apiId) { return this.client.request(`/v1/bots/apis/${encodeURIComponent(apiId)}`, { method: 'DELETE' }); }
3143
+ webhookSecret(botId) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/webhook-secret`, { method: 'POST', body: {} }); }
3144
+ triggerWebhook(botId, triggerId) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/triggers/${encodeURIComponent(triggerId)}/webhook`, { method: 'POST', body: {} }); }
3145
+ triggerAdmission(botId, triggerId, admissionId) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/triggers/${encodeURIComponent(triggerId)}/admissions/${encodeURIComponent(admissionId)}`); }
3146
+ apiSigningKey(botId, apiId, request = { action: 'read' }) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/apis/${encodeURIComponent(apiId)}/signing-key`, { method: 'POST', body: request }); }
3147
+ models() { return this.client.request('/v1/bots/models'); }
3148
+ setCredential(botId, secret) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/credentials`, { method: 'POST', body: { secret } }); }
3149
+ deliveries(botId, runId) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/runs/${encodeURIComponent(runId)}/deliveries`); }
3150
+ retryDelivery(botId, runId, deliveryId) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/runs/${encodeURIComponent(runId)}/deliveries/retry`, { method: 'POST', body: { deliveryId } }); }
3151
+ setCustomerCredential(botId, input) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/credentials`, { method: 'POST', body: input }); }
3152
+ delete(botId) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}`, { method: 'DELETE' }); }
3153
+ deleteCustomerData(botId, sessionId) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/customer-data/delete`, { method: 'POST', body: { sessionId } }); }
3154
+ setState(botId, input) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/state`, { method: 'POST', body: input }); }
3155
+ list() { return this.client.request('/v1/bots'); }
3156
+ create(name) { return this.client.request('/v1/bots', { method: 'POST', body: { name } }); }
3157
+ get(botId) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}`); }
3158
+ update(botId, revision, patch) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}`, { method: 'PATCH', body: { revision, patch } }); }
3159
+ publish(botId, revision, channels) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/publish`, { method: 'POST', body: { revision, channels } }); }
3160
+ run(botId, input) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/runs`, { method: 'POST', body: { ...input, runId: input.runId ?? globalThis.crypto.randomUUID() } }); }
3161
+ getRun(botId, runId) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/runs/${encodeURIComponent(runId)}`); }
3162
+ /** Observe the same durable run. Returns when finished or when a person must act. */
3163
+ async wait(botId, runId, options = {}) {
3164
+ const deadline = Date.now() + positiveSdkInteger(options.totalTimeoutMs ?? 300_000, 'totalTimeoutMs');
3165
+ while (Date.now() < deadline) {
3166
+ if (options.signal?.aborted)
3167
+ throw options.signal.reason;
3168
+ const run = await this.client.request(`/v1/bots/${encodeURIComponent(botId)}/runs/${encodeURIComponent(runId)}`, { signal: options.signal, timeoutMs: Math.min(30_000, deadline - Date.now()) });
3169
+ if (['completed', 'failed', 'stopped', 'cancelled', 'waiting_for_approval', 'waiting_for_connection'].includes(run.status))
3170
+ return run;
3171
+ await sdkDelay(Math.min(1000, Math.max(1, deadline - Date.now())), options.signal ?? AbortSignal.timeout(Math.max(1, deadline - Date.now())));
3172
+ }
3173
+ throw new StacklinkApiError('Bot observation timed out. Continue observing the same run.', 408, 'sdk_run_timeout', { botId, runId });
3174
+ }
3175
+ events(botId, runId, after = '0') { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/runs/${encodeURIComponent(runId)}/events`, { query: { after } }); }
3176
+ stream(botId, runId, options = {}) { return this.client.streamBotEvents(botId, runId, options); }
3177
+ cancel(botId, runId) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/runs/${encodeURIComponent(runId)}/stop`, { method: 'POST', body: {} }); }
3178
+ }
2782
3179
  //# sourceMappingURL=index.js.map