@tangle-network/agent-runtime 0.11.1 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -35,12 +35,38 @@ declare function createSandboxPromptBackend<TBox, TInput extends AgentBackendInp
35
35
  getSessionId?: (box: TBox, input: TInput) => string | undefined;
36
36
  }): AgentExecutionBackend<TInput>;
37
37
  /** @stable */
38
+ /**
39
+ * Retry policy for transient transport errors (rate limits, upstream
40
+ * timeouts). Defaults to 5 attempts with exponential backoff starting at
41
+ * 1s, ±25% jitter, capped at 30s. Set `maxAttempts: 1` to disable retries.
42
+ *
43
+ * Retried status codes:
44
+ * - 408 Request Timeout
45
+ * - 425 Too Early
46
+ * - 429 Too Many Requests
47
+ * - 500 / 502 / 503 / 504 — upstream transient failures
48
+ *
49
+ * Hard failures (401, 403, 4xx other than the above) propagate immediately.
50
+ */
51
+ interface BackendRetryPolicy {
52
+ /** Total attempts including the first try. Default 5. */
53
+ maxAttempts?: number;
54
+ /** Initial backoff in ms before the second attempt. Default 1000. */
55
+ initialBackoffMs?: number;
56
+ /** Hard ceiling on backoff in ms. Default 30000. */
57
+ maxBackoffMs?: number;
58
+ /** Jitter fraction in [0, 1]. Default 0.25 (±25%). */
59
+ jitter?: number;
60
+ /** Status codes that trigger a retry. Default: 408, 425, 429, 500, 502, 503, 504. */
61
+ retryStatuses?: ReadonlyArray<number>;
62
+ }
38
63
  declare function createOpenAICompatibleBackend<TInput extends AgentBackendInput = AgentBackendInput>(options: {
39
64
  apiKey: string;
40
65
  baseUrl: string;
41
66
  model: string;
42
67
  kind?: string;
43
68
  fetchImpl?: typeof fetch;
69
+ retry?: BackendRetryPolicy;
44
70
  }): AgentExecutionBackend<TInput>;
45
71
 
46
72
  /**
package/dist/index.js CHANGED
@@ -106,33 +106,78 @@ function createSandboxPromptBackend(options) {
106
106
  }
107
107
  };
108
108
  }
109
+ var DEFAULT_RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
110
+ function pickRetryDelayMs(attempt, policy) {
111
+ const exp = policy.initialBackoffMs * 2 ** (attempt - 1);
112
+ const capped = Math.min(exp, policy.maxBackoffMs);
113
+ const jitter = capped * policy.jitter * (Math.random() * 2 - 1);
114
+ return Math.max(0, Math.round(capped + jitter));
115
+ }
116
+ function sleep(ms, signal) {
117
+ return new Promise((resolve, reject) => {
118
+ if (signal?.aborted) {
119
+ reject(signal.reason ?? new Error("aborted"));
120
+ return;
121
+ }
122
+ const t = setTimeout(() => {
123
+ signal?.removeEventListener("abort", onAbort);
124
+ resolve();
125
+ }, ms);
126
+ const onAbort = () => {
127
+ clearTimeout(t);
128
+ reject(signal?.reason ?? new Error("aborted"));
129
+ };
130
+ signal?.addEventListener("abort", onAbort, { once: true });
131
+ });
132
+ }
109
133
  function createOpenAICompatibleBackend(options) {
110
134
  const fetcher = options.fetchImpl ?? fetch;
111
135
  const kind = options.kind ?? "tcloud";
136
+ const retryPolicy = {
137
+ maxAttempts: options.retry?.maxAttempts ?? 5,
138
+ initialBackoffMs: options.retry?.initialBackoffMs ?? 1e3,
139
+ maxBackoffMs: options.retry?.maxBackoffMs ?? 3e4,
140
+ jitter: options.retry?.jitter ?? 0.25,
141
+ retryStatuses: options.retry?.retryStatuses ?? DEFAULT_RETRY_STATUSES
142
+ };
112
143
  return {
113
144
  kind,
114
145
  start(_input, context) {
115
146
  return newRuntimeSession(kind, context.requestedSessionId);
116
147
  },
117
148
  async *stream(input, context) {
118
- const response = await fetcher(`${options.baseUrl.replace(/\/$/, "")}/chat/completions`, {
119
- method: "POST",
120
- headers: {
121
- Authorization: `Bearer ${options.apiKey}`,
122
- "Content-Type": "application/json"
123
- },
124
- body: JSON.stringify({
125
- model: options.model,
126
- stream: true,
127
- messages: input.messages ?? [
128
- { role: "user", content: input.message ?? context.task.intent }
129
- ]
130
- }),
131
- signal: context.signal
132
- });
133
- if (!response.ok) {
134
- throw new BackendTransportError(kind, `chat backend returned ${response.status}`, {
135
- status: response.status
149
+ let response;
150
+ let lastStatus = 0;
151
+ for (let attempt = 1; attempt <= retryPolicy.maxAttempts; attempt++) {
152
+ response = await fetcher(`${options.baseUrl.replace(/\/$/, "")}/chat/completions`, {
153
+ method: "POST",
154
+ headers: {
155
+ Authorization: `Bearer ${options.apiKey}`,
156
+ "Content-Type": "application/json"
157
+ },
158
+ body: JSON.stringify({
159
+ model: options.model,
160
+ stream: true,
161
+ messages: input.messages ?? [
162
+ { role: "user", content: input.message ?? context.task.intent }
163
+ ]
164
+ }),
165
+ signal: context.signal
166
+ });
167
+ if (response.ok) break;
168
+ lastStatus = response.status;
169
+ if (!retryPolicy.retryStatuses.includes(response.status)) break;
170
+ if (attempt === retryPolicy.maxAttempts) break;
171
+ try {
172
+ await response.body?.cancel();
173
+ } catch {
174
+ }
175
+ const delayMs = pickRetryDelayMs(attempt, retryPolicy);
176
+ await sleep(delayMs, context.signal);
177
+ }
178
+ if (!response || !response.ok) {
179
+ throw new BackendTransportError(kind, `chat backend returned ${lastStatus || "unknown"}`, {
180
+ status: lastStatus || 0
136
181
  });
137
182
  }
138
183
  yield* streamResponseEvents(response, context);