@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.
- package/README.md +33 -0
- package/compatibility.json +1 -1
- package/dist/src/domain/bot-contract.d.ts +4336 -0
- package/dist/src/domain/bot-contract.d.ts.map +1 -0
- package/dist/src/domain/bot-contract.js +382 -0
- package/dist/src/domain/bot-contract.js.map +1 -0
- package/dist/src/domain/memory-contract.d.ts +1705 -543
- package/dist/src/domain/memory-contract.d.ts.map +1 -1
- package/dist/src/domain/memory-contract.js +331 -60
- package/dist/src/domain/memory-contract.js.map +1 -1
- package/dist/src/domain/platform-approval-contract.d.ts +508 -0
- package/dist/src/domain/platform-approval-contract.d.ts.map +1 -0
- package/dist/src/domain/platform-approval-contract.js +77 -0
- package/dist/src/domain/platform-approval-contract.js.map +1 -0
- package/dist/src/domain/provider-branding.d.ts +1 -1
- package/dist/src/domain/provider-branding.d.ts.map +1 -1
- package/dist/src/domain/provider-branding.js +306 -8
- package/dist/src/domain/provider-branding.js.map +1 -1
- package/dist/src/domain/stacklink-meta-tools.d.ts +14 -16
- package/dist/src/domain/stacklink-meta-tools.d.ts.map +1 -1
- package/dist/src/domain/stacklink-meta-tools.js +217 -128
- package/dist/src/domain/stacklink-meta-tools.js.map +1 -1
- package/dist/src/sdk/framework-tools.d.ts +24 -2
- package/dist/src/sdk/framework-tools.d.ts.map +1 -1
- package/dist/src/sdk/framework-tools.js +50 -0
- package/dist/src/sdk/framework-tools.js.map +1 -1
- package/dist/src/sdk/index.d.ts +475 -143
- package/dist/src/sdk/index.d.ts.map +1 -1
- package/dist/src/sdk/index.js +633 -236
- package/dist/src/sdk/index.js.map +1 -1
- package/dist/src/sdk/langchain.d.ts +7 -2
- package/dist/src/sdk/langchain.d.ts.map +1 -1
- package/dist/src/sdk/langchain.js +3 -2
- package/dist/src/sdk/langchain.js.map +1 -1
- package/dist/src/sdk/memory-lifecycle.d.ts +25 -0
- package/dist/src/sdk/memory-lifecycle.d.ts.map +1 -1
- package/dist/src/sdk/memory-lifecycle.js +75 -1
- package/dist/src/sdk/memory-lifecycle.js.map +1 -1
- package/dist/src/sdk/openai-agents.d.ts +6 -6
- package/dist/src/sdk/openai-agents.d.ts.map +1 -1
- package/dist/src/sdk/openai-agents.js +21 -16
- package/dist/src/sdk/openai-agents.js.map +1 -1
- package/dist/src/sdk/openai.d.ts +2 -2
- package/dist/src/sdk/openai.d.ts.map +1 -1
- package/dist/src/sdk/openai.js +67 -31
- package/dist/src/sdk/openai.js.map +1 -1
- package/dist/src/sdk/vercel-ai.d.ts +15 -4
- package/dist/src/sdk/vercel-ai.d.ts.map +1 -1
- package/dist/src/sdk/vercel-ai.js +33 -9
- package/dist/src/sdk/vercel-ai.js.map +1 -1
- package/package.json +1 -1
- package/dist/src/domain/knowledge-meta-tools.d.ts +0 -12
- package/dist/src/domain/knowledge-meta-tools.d.ts.map +0 -1
- package/dist/src/domain/knowledge-meta-tools.js +0 -38
- package/dist/src/domain/knowledge-meta-tools.js.map +0 -1
- package/dist/src/domain/memory-meta-tools.d.ts +0 -5
- package/dist/src/domain/memory-meta-tools.d.ts.map +0 -1
- package/dist/src/domain/memory-meta-tools.js +0 -18
- package/dist/src/domain/memory-meta-tools.js.map +0 -1
- package/dist/src/domain/platform-mcp-tools.d.ts +0 -3
- package/dist/src/domain/platform-mcp-tools.d.ts.map +0 -1
- package/dist/src/domain/platform-mcp-tools.js +0 -30
- package/dist/src/domain/platform-mcp-tools.js.map +0 -1
package/dist/src/sdk/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
198
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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
|
-
|
|
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
|
|
1051
|
-
return this.
|
|
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:
|
|
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
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
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
|
-
|
|
1233
|
-
|
|
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
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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.
|
|
1296
|
-
return
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
buffer
|
|
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
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
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
|
-
|
|
2310
|
-
|
|
2698
|
+
async toolkits(query = {}) {
|
|
2699
|
+
return this.client.request(`/v1/sessions/${this.id}/toolkits`, {
|
|
2700
|
+
query: providerCatalogSdkQuery(query),
|
|
2701
|
+
});
|
|
2311
2702
|
}
|
|
2312
|
-
async authorize(
|
|
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
|
-
|
|
2317
|
-
|
|
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
|
|
2413
|
-
const searchInput = {
|
|
2414
|
-
|
|
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
|
|
2426
|
-
return this.client.
|
|
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
|
|
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:
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
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
|
-
|
|
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
|