@tangle-network/agent-runtime 0.14.0 → 0.14.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
@@ -59,6 +59,14 @@ interface BackendRetryPolicy {
59
59
  jitter?: number;
60
60
  /** Status codes that trigger a retry. Default: 408, 425, 429, 500, 502, 503, 504. */
61
61
  retryStatuses?: ReadonlyArray<number>;
62
+ /**
63
+ * Per-attempt wall-clock deadline in ms. If a single fetch attempt does
64
+ * not return headers within this window the attempt is aborted and
65
+ * retried. Default 120000 (2 min). Without this a hung upstream blocks
66
+ * the attempt indefinitely — observed in production as a 15-minute
67
+ * `fetch failed` that burned an entire eval persona. Set to 0 to disable.
68
+ */
69
+ requestTimeoutMs?: number;
62
70
  }
63
71
  declare function createOpenAICompatibleBackend<TInput extends AgentBackendInput = AgentBackendInput>(options: {
64
72
  apiKey: string;
package/dist/index.js CHANGED
@@ -113,6 +113,32 @@ function pickRetryDelayMs(attempt, policy) {
113
113
  const jitter = capped * policy.jitter * (Math.random() * 2 - 1);
114
114
  return Math.max(0, Math.round(capped + jitter));
115
115
  }
116
+ function withTimeout(callerSignal, timeoutMs) {
117
+ if (timeoutMs <= 0) {
118
+ return { signal: callerSignal ?? new AbortController().signal, dispose: () => void 0 };
119
+ }
120
+ const controller = new AbortController();
121
+ const timer = setTimeout(
122
+ () => controller.abort(new Error(`request timed out after ${timeoutMs}ms`)),
123
+ timeoutMs
124
+ );
125
+ if (typeof timer.unref === "function") {
126
+ ;
127
+ timer.unref();
128
+ }
129
+ const onCallerAbort = () => controller.abort(callerSignal?.reason ?? new Error("aborted"));
130
+ if (callerSignal) {
131
+ if (callerSignal.aborted) onCallerAbort();
132
+ else callerSignal.addEventListener("abort", onCallerAbort, { once: true });
133
+ }
134
+ return {
135
+ signal: controller.signal,
136
+ dispose: () => {
137
+ clearTimeout(timer);
138
+ callerSignal?.removeEventListener("abort", onCallerAbort);
139
+ }
140
+ };
141
+ }
116
142
  function sleep(ms, signal) {
117
143
  return new Promise((resolve, reject) => {
118
144
  if (signal?.aborted) {
@@ -138,7 +164,8 @@ function createOpenAICompatibleBackend(options) {
138
164
  initialBackoffMs: options.retry?.initialBackoffMs ?? 1e3,
139
165
  maxBackoffMs: options.retry?.maxBackoffMs ?? 3e4,
140
166
  jitter: options.retry?.jitter ?? 0.25,
141
- retryStatuses: options.retry?.retryStatuses ?? DEFAULT_RETRY_STATUSES
167
+ retryStatuses: options.retry?.retryStatuses ?? DEFAULT_RETRY_STATUSES,
168
+ requestTimeoutMs: options.retry?.requestTimeoutMs ?? 12e4
142
169
  };
143
170
  return {
144
171
  kind,
@@ -146,24 +173,40 @@ function createOpenAICompatibleBackend(options) {
146
173
  return newRuntimeSession(kind, context.requestedSessionId);
147
174
  },
148
175
  async *stream(input, context) {
176
+ const url = `${options.baseUrl.replace(/\/$/, "")}/chat/completions`;
177
+ const requestBody = JSON.stringify({
178
+ model: options.model,
179
+ stream: true,
180
+ messages: input.messages ?? [
181
+ { role: "user", content: input.message ?? context.task.intent }
182
+ ]
183
+ });
149
184
  let response;
150
185
  let lastStatus = 0;
186
+ let lastThrown;
151
187
  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
- });
188
+ lastThrown = void 0;
189
+ const attemptSignal = withTimeout(context.signal, retryPolicy.requestTimeoutMs);
190
+ try {
191
+ response = await fetcher(url, {
192
+ method: "POST",
193
+ headers: {
194
+ Authorization: `Bearer ${options.apiKey}`,
195
+ "Content-Type": "application/json"
196
+ },
197
+ body: requestBody,
198
+ signal: attemptSignal.signal
199
+ });
200
+ } catch (err) {
201
+ attemptSignal.dispose();
202
+ if (context.signal?.aborted) throw err;
203
+ lastThrown = err;
204
+ response = void 0;
205
+ if (attempt === retryPolicy.maxAttempts) break;
206
+ await sleep(pickRetryDelayMs(attempt, retryPolicy), context.signal);
207
+ continue;
208
+ }
209
+ attemptSignal.dispose();
167
210
  if (response.ok) break;
168
211
  lastStatus = response.status;
169
212
  if (!retryPolicy.retryStatuses.includes(response.status)) break;
@@ -175,7 +218,15 @@ function createOpenAICompatibleBackend(options) {
175
218
  const delayMs = pickRetryDelayMs(attempt, retryPolicy);
176
219
  await sleep(delayMs, context.signal);
177
220
  }
178
- if (!response || !response.ok) {
221
+ if (!response) {
222
+ const reason = lastThrown instanceof Error ? lastThrown.message : String(lastThrown);
223
+ throw new BackendTransportError(
224
+ kind,
225
+ `chat backend unreachable after ${retryPolicy.maxAttempts} attempts: ${reason}`,
226
+ { status: 0 }
227
+ );
228
+ }
229
+ if (!response.ok) {
179
230
  throw new BackendTransportError(kind, `chat backend returned ${lastStatus || "unknown"}`, {
180
231
  status: lastStatus || 0
181
232
  });