@stacklink/sdk 0.3.8 → 0.3.10

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.
@@ -1,18 +1,20 @@
1
1
  import { botApiSignaturePayload } from '../domain/bot-contract.js';
2
2
  import { materializeStacklinkWorkbenchExecutionResponse, } from '../domain/stacklink-meta-tools.js';
3
3
  export { StacklinkMemoryLifecycle, StacklinkMemoryLifecycleError, createStacklinkMemoryLifecycle, serializeCompletedConversationTurn, } from './memory-lifecycle.js';
4
- export const stacklinkTypeScriptSdkVersion = '0.3.8';
4
+ export const stacklinkTypeScriptSdkVersion = '0.3.10';
5
5
  const workbenchReusableRunComparisonWaitMs = 120_000;
6
6
  const workbenchReusableRunComparisonPollMs = 1_000;
7
7
  export class StacklinkApiError extends Error {
8
8
  statusCode;
9
9
  code;
10
10
  details;
11
- constructor(message, statusCode, code, details) {
11
+ retryAfterMs;
12
+ constructor(message, statusCode, code, details, retryAfterMs) {
12
13
  super(message);
13
14
  this.statusCode = statusCode;
14
15
  this.code = code;
15
16
  this.details = details;
17
+ this.retryAfterMs = retryAfterMs;
16
18
  this.name = 'StacklinkApiError';
17
19
  }
18
20
  }
@@ -835,48 +837,66 @@ export class Stacklink {
835
837
  }
836
838
  async *streamExecutionEvents(input, options = {}) {
837
839
  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
- });
840
+ const totalTimeoutMs = positiveSdkInteger(options.totalTimeoutMs ?? 3_600_000, 'totalTimeoutMs');
841
+ const maxReconnectAttempts = positiveSdkInteger(options.maxReconnectAttempts ?? 8, 'maxReconnectAttempts');
842
+ const deadline = Date.now() + totalTimeoutMs;
843
+ const observationSignal = AbortSignal.timeout(totalTimeoutMs);
844
+ const signal = options.signal ? AbortSignal.any([options.signal, observationSignal]) : observationSignal;
845
+ let failures = 0;
846
+ while (!signal.aborted) {
847
+ let delayMs = sdkRetryDelayMs(undefined, failures + 1);
852
848
  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);
849
+ const connectionSignal = AbortSignal.any([signal, AbortSignal.timeout(Math.min(options.timeoutMs ?? 30_000, Math.max(1, deadline - Date.now())))]);
850
+ const response = await this.requestRaw(`/v1/executions/${input.executionKind}/${input.executionId}/events/stream`, { headers: { accept: 'text/event-stream', ...(afterSequence === undefined ? {} : { 'Last-Event-ID': afterSequence }) },
851
+ query: { sessionId: input.sessionId, afterSequence, limit: input.limit },
852
+ signal, timeoutMs: Math.min(options.timeoutMs ?? 30_000, Math.max(1, deadline - Date.now())) });
853
+ for await (const payload of this.readJsonEventStream(response, connectionSignal)) {
854
+ if (!isSdkRecord(payload) || !Array.isArray(payload.events)) {
855
+ throw new StacklinkApiError('Execution stream returned an invalid event page.', 502, 'execution_event_invalid');
856
+ }
857
+ failures = 0;
858
+ for (const event of payload.events) {
859
+ if (!isSdkRecord(event) || (typeof event.sequence !== 'string' && typeof event.sequence !== 'number')) {
860
+ throw new StacklinkApiError('Execution event has no durable sequence.', 502, 'execution_event_invalid');
860
861
  }
862
+ const sequence = BigInt(event.sequence);
863
+ if (afterSequence !== undefined && sequence <= BigInt(afterSequence))
864
+ continue;
865
+ afterSequence = String(event.sequence);
861
866
  yield event;
862
867
  }
863
- if (typeof payload.nextCursor === 'string')
868
+ if (typeof payload.nextCursor === 'string' && (afterSequence === undefined || BigInt(payload.nextCursor) > BigInt(afterSequence)))
864
869
  afterSequence = payload.nextCursor;
865
870
  if (payload.terminal === true)
866
871
  return;
867
872
  }
868
873
  }
869
874
  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')
875
+ if (signal.aborted)
876
+ break;
877
+ const retryable = error instanceof TypeError
878
+ || (error instanceof Error && ['AbortError', 'TimeoutError'].includes(error.name))
879
+ || (error instanceof StacklinkApiError && sdkRetryableStatus(error.statusCode) && error.code !== 'execution_event_invalid');
880
+ if (!retryable)
874
881
  throw error;
875
- // The per-connection deadline bounds a stream. Reconnect from the durable cursor.
882
+ if (error instanceof StacklinkApiError && error.retryAfterMs !== undefined)
883
+ delayMs = error.retryAfterMs;
884
+ }
885
+ failures += 1;
886
+ if (failures > maxReconnectAttempts)
887
+ throw new StacklinkApiError('Execution observation could not reconnect. Resume this execution from the saved cursor.', 503, 'sdk_stream_reconnect_exhausted', { executionId: input.executionId, afterSequence });
888
+ if (delayMs >= deadline - Date.now())
889
+ break;
890
+ try {
891
+ await sdkDelay(delayMs, signal);
892
+ }
893
+ catch {
894
+ break;
876
895
  }
877
896
  }
878
897
  if (options.signal?.aborted)
879
898
  throw options.signal.reason ?? new Error('Stacklink execution stream was aborted.');
899
+ throw new StacklinkApiError('Execution observation reached its deadline. The operation has not been resubmitted or cancelled.', 408, 'sdk_stream_observation_timeout', { executionId: input.executionId, afterSequence });
880
900
  }
881
901
  async waitForExecution(input, options = {}) {
882
902
  return this.request(`/v1/executions/${input.executionKind}/${input.executionId}/wait`, {
@@ -1486,7 +1506,7 @@ export class Stacklink {
1486
1506
  catch {
1487
1507
  parsed = null;
1488
1508
  }
1489
- 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);
1509
+ 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, response.headers.has('retry-after') ? sdkRetryDelayMs(response, 1) : undefined);
1490
1510
  }
1491
1511
  return response;
1492
1512
  }
@@ -1677,10 +1697,12 @@ export class Stacklink {
1677
1697
  reader.releaseLock();
1678
1698
  }
1679
1699
  }
1680
- async *readJsonEventStream(response) {
1700
+ async *readJsonEventStream(response, signal) {
1681
1701
  if (!response.body)
1682
1702
  throw new Error('Stacklink event stream response has no body.');
1683
1703
  const reader = response.body.getReader();
1704
+ const abortRead = () => { void reader.cancel(signal?.reason).catch(() => undefined); };
1705
+ signal?.addEventListener('abort', abortRead, { once: true });
1684
1706
  const decoder = new TextDecoder();
1685
1707
  let buffer = '';
1686
1708
  let dataLines = [];
@@ -1705,7 +1727,9 @@ export class Stacklink {
1705
1727
  };
1706
1728
  try {
1707
1729
  while (true) {
1730
+ signal?.throwIfAborted();
1708
1731
  const read = await reader.read();
1732
+ signal?.throwIfAborted();
1709
1733
  if (read.done)
1710
1734
  break;
1711
1735
  buffer += decoder.decode(read.value, { stream: true });
@@ -1729,6 +1753,7 @@ export class Stacklink {
1729
1753
  yield finalPayload;
1730
1754
  }
1731
1755
  finally {
1756
+ signal?.removeEventListener('abort', abortRead);
1732
1757
  await reader.cancel().catch(() => undefined);
1733
1758
  reader.releaseLock();
1734
1759
  }
@@ -2394,9 +2419,12 @@ export class StacklinkWorkbenchRun {
2394
2419
  executionKind: this.executionKind,
2395
2420
  executionId: this.executionId,
2396
2421
  limit: input.limit ?? 200,
2422
+ afterSequence: input.afterSequence,
2397
2423
  }, {
2398
2424
  signal: input.signal,
2399
2425
  timeoutMs: input.streamTimeoutMs,
2426
+ totalTimeoutMs: input.totalTimeoutMs,
2427
+ maxReconnectAttempts: input.maxReconnectAttempts,
2400
2428
  });
2401
2429
  }
2402
2430
  wait(input = {}) {