orchajs 0.21.0 → 0.22.0

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.
@@ -0,0 +1,21 @@
1
+ import type { AmarsiaTelemetryRunV1, CompiledAgentManifest, SessionStore } from "../types.js";
2
+ export type AmarsiaRunFact = AmarsiaTelemetryRunV1;
3
+ export declare class AmarsiaTelemetryClient {
4
+ #private;
5
+ constructor(options: {
6
+ key: string;
7
+ endpoint?: string;
8
+ });
9
+ enqueue(fact: AmarsiaRunFact): void;
10
+ drain(options?: {
11
+ timeoutMs?: number;
12
+ }): Promise<void>;
13
+ close(options?: {
14
+ timeoutMs?: number;
15
+ }): Promise<void>;
16
+ }
17
+ export declare function withAmarsiaTelemetry(options: {
18
+ store: SessionStore;
19
+ manifest: CompiledAgentManifest;
20
+ client: AmarsiaTelemetryClient;
21
+ }): SessionStore;
@@ -0,0 +1,457 @@
1
+ import { resolveAmarsiaEndpoint } from "./endpoint.js";
2
+ const RELEVANT_EVENTS = new Set([
3
+ "run.started",
4
+ "run.completed",
5
+ "run.failed",
6
+ "run.paused",
7
+ "evaluation.completed",
8
+ "evaluation.failed",
9
+ ]);
10
+ const MAX_BATCH_RUNS = 100;
11
+ const MAX_KEEPALIVE_BYTES = 48 * 1024;
12
+ const HEARTBEAT_INTERVAL_MS = 30_000;
13
+ export class AmarsiaTelemetryClient {
14
+ #key;
15
+ #baseUrl;
16
+ #queue = new Map();
17
+ #activeRuns = new Map();
18
+ #timer;
19
+ #heartbeatTimer;
20
+ #attempt = 0;
21
+ #flushPromise;
22
+ constructor(options) {
23
+ this.#key = options.key;
24
+ this.#baseUrl = resolveAmarsiaEndpoint(options.endpoint);
25
+ }
26
+ enqueue(fact) {
27
+ this.#queue.set(fact.event_id, fact);
28
+ if (fact.status === "running") {
29
+ this.#activeRuns.set(fact.event_id, fact);
30
+ this.#scheduleHeartbeat();
31
+ }
32
+ else {
33
+ this.#activeRuns.delete(fact.event_id);
34
+ if (this.#activeRuns.size === 0 && this.#heartbeatTimer) {
35
+ clearTimeout(this.#heartbeatTimer);
36
+ this.#heartbeatTimer = undefined;
37
+ }
38
+ }
39
+ if (fact.status !== "running") {
40
+ this.#scheduleTerminal();
41
+ return;
42
+ }
43
+ if (this.#queue.size >= 20) {
44
+ void this.#flush();
45
+ return;
46
+ }
47
+ this.#schedule(250);
48
+ }
49
+ #schedule(delay) {
50
+ if (this.#timer)
51
+ return;
52
+ this.#timer = setTimeout(() => {
53
+ this.#timer = undefined;
54
+ void this.#flush();
55
+ }, delay);
56
+ const timer = this.#timer;
57
+ timer.unref?.();
58
+ }
59
+ #scheduleTerminal() {
60
+ if (this.#timer)
61
+ clearTimeout(this.#timer);
62
+ this.#timer = setTimeout(() => {
63
+ this.#timer = undefined;
64
+ void this.#flush();
65
+ }, 0);
66
+ }
67
+ #scheduleHeartbeat() {
68
+ if (this.#heartbeatTimer || this.#activeRuns.size === 0)
69
+ return;
70
+ this.#heartbeatTimer = setTimeout(() => {
71
+ this.#heartbeatTimer = undefined;
72
+ for (const fact of this.#activeRuns.values()) {
73
+ this.#queue.set(fact.event_id, fact);
74
+ }
75
+ void this.#flush();
76
+ this.#scheduleHeartbeat();
77
+ }, HEARTBEAT_INTERVAL_MS);
78
+ const timer = this.#heartbeatTimer;
79
+ timer.unref?.();
80
+ }
81
+ async drain(options = {}) {
82
+ const timeoutMs = Math.max(0, options.timeoutMs ?? 2_000);
83
+ const deadline = Date.now() + timeoutMs;
84
+ if (this.#timer) {
85
+ clearTimeout(this.#timer);
86
+ this.#timer = undefined;
87
+ }
88
+ while (this.#queue.size > 0 || this.#flushPromise) {
89
+ const remaining = deadline - Date.now();
90
+ if (remaining <= 0)
91
+ return;
92
+ await Promise.race([
93
+ this.#flush(),
94
+ new Promise((resolve) => setTimeout(resolve, remaining)),
95
+ ]);
96
+ }
97
+ }
98
+ async close(options = {}) {
99
+ if (this.#heartbeatTimer) {
100
+ clearTimeout(this.#heartbeatTimer);
101
+ this.#heartbeatTimer = undefined;
102
+ }
103
+ this.#activeRuns.clear();
104
+ await this.drain(options);
105
+ }
106
+ async #flush() {
107
+ if (this.#flushPromise)
108
+ return this.#flushPromise;
109
+ if (this.#queue.size === 0)
110
+ return;
111
+ this.#flushPromise = this.#performFlush();
112
+ try {
113
+ await this.#flushPromise;
114
+ }
115
+ finally {
116
+ this.#flushPromise = undefined;
117
+ if (this.#queue.size > 0 && !this.#timer) {
118
+ const hasTerminalFact = [...this.#queue.values()].some((fact) => fact.status !== "running");
119
+ if (hasTerminalFact) {
120
+ void this.#flush();
121
+ }
122
+ else {
123
+ this.#schedule(250);
124
+ }
125
+ }
126
+ }
127
+ }
128
+ async #performFlush() {
129
+ const batch = takeBatch(this.#queue);
130
+ if (batch.length === 0)
131
+ return;
132
+ const body = JSON.stringify({
133
+ schema_version: 1,
134
+ runs: batch,
135
+ });
136
+ for (const fact of batch)
137
+ this.#queue.delete(fact.event_id);
138
+ try {
139
+ const response = await fetch(`${this.#baseUrl}/v3/project-analytics/runs/batch`, {
140
+ method: "POST",
141
+ headers: {
142
+ "content-type": "application/json",
143
+ "x-amarsia-project-key": this.#key,
144
+ },
145
+ keepalive: utf8ByteLength(body) <= MAX_KEEPALIVE_BYTES,
146
+ body,
147
+ });
148
+ if (!response.ok) {
149
+ throw new Error(`HTTP ${response.status}`);
150
+ }
151
+ this.#attempt = 0;
152
+ }
153
+ catch {
154
+ this.#attempt += 1;
155
+ if (this.#attempt <= 6) {
156
+ for (const fact of batch) {
157
+ if (!this.#queue.has(fact.event_id)) {
158
+ this.#queue.set(fact.event_id, fact);
159
+ }
160
+ }
161
+ this.#schedule(Math.min(30_000, 500 * 2 ** this.#attempt));
162
+ }
163
+ else {
164
+ this.#attempt = 0;
165
+ console.warn(`Amarsia telemetry delivery failed after 6 retries; dropping ${batch.length} run update(s).`);
166
+ }
167
+ }
168
+ }
169
+ }
170
+ export function withAmarsiaTelemetry(options) {
171
+ const { store, manifest, client } = options;
172
+ const projections = new Map();
173
+ const pending = new Map();
174
+ return {
175
+ read: (sessionId) => store.read(sessionId),
176
+ listSessionIds: () => store.listSessionIds(),
177
+ describeLocation: store.describeLocation?.bind(store),
178
+ subscribe: store.subscribe?.bind(store),
179
+ subscribeAll: store.subscribeAll?.bind(store),
180
+ async append(sessionId, events) {
181
+ await store.append(sessionId, events);
182
+ const runs = relevantRuns(events);
183
+ if (runs.size === 0 &&
184
+ !projections.has(sessionId) &&
185
+ !pending.has(sessionId)) {
186
+ return;
187
+ }
188
+ const previous = pending.get(sessionId) ?? Promise.resolve();
189
+ const processing = previous
190
+ .then(async () => {
191
+ let projection = projections.get(sessionId);
192
+ if (!projection) {
193
+ projection = createProjection(await store.read(sessionId));
194
+ projections.set(sessionId, projection);
195
+ }
196
+ else {
197
+ appendToProjection(projection, events);
198
+ }
199
+ if (projection.isTest)
200
+ return;
201
+ for (const run of runs) {
202
+ const fact = projectRunFact(manifest, sessionId, run, projection);
203
+ if (fact)
204
+ client.enqueue(fact);
205
+ }
206
+ })
207
+ .catch(() => undefined);
208
+ pending.set(sessionId, processing);
209
+ void processing.finally(() => {
210
+ if (pending.get(sessionId) === processing) {
211
+ pending.delete(sessionId);
212
+ }
213
+ });
214
+ },
215
+ };
216
+ }
217
+ function createProjection(events) {
218
+ const projection = {
219
+ seenSequences: new Set(),
220
+ runs: new Map(),
221
+ isTest: false,
222
+ };
223
+ appendToProjection(projection, events);
224
+ return projection;
225
+ }
226
+ function appendToProjection(projection, events) {
227
+ for (const event of events) {
228
+ if (projection.seenSequences.has(event.sequence))
229
+ continue;
230
+ projection.seenSequences.add(event.sequence);
231
+ if (event.type === "session.created") {
232
+ projection.sessionCreated = event;
233
+ const metadata = asRecord(event.data.metadata);
234
+ projection.isTest = metadata?.orchaTest === true;
235
+ }
236
+ if (typeof event.run !== "number")
237
+ continue;
238
+ const runEvents = projection.runs.get(event.run);
239
+ if (runEvents) {
240
+ runEvents.push(event);
241
+ }
242
+ else {
243
+ projection.runs.set(event.run, [event]);
244
+ }
245
+ }
246
+ }
247
+ function relevantRuns(events) {
248
+ const runs = new Set();
249
+ for (const event of events) {
250
+ if (typeof event.run === "number" && RELEVANT_EVENTS.has(event.type)) {
251
+ runs.add(event.run);
252
+ }
253
+ }
254
+ return runs;
255
+ }
256
+ function projectRunFact(manifest, sessionId, run, projection) {
257
+ const runEvents = projection.runs.get(run);
258
+ if (!runEvents)
259
+ return undefined;
260
+ const started = runEvents.find((event) => event.type === "run.started");
261
+ if (!started)
262
+ return undefined;
263
+ const terminal = [...runEvents]
264
+ .reverse()
265
+ .find((event) => ["run.completed", "run.failed", "run.paused"].includes(event.type));
266
+ const lineage = asRecord(projection.sessionCreated?.data.lineage);
267
+ const assistants = runEvents.filter((event) => event.type === "message.created" && event.data.role === "assistant");
268
+ const usage = normalizeUsage(terminal?.data.usage, assistants);
269
+ const completedActions = runEvents.filter((event) => event.type === "action.completed");
270
+ const failedActions = runEvents.filter((event) => event.type === "action.failed");
271
+ const evaluations = runEvents.filter((event) => event.type === "evaluation.completed" ||
272
+ event.type === "evaluation.failed");
273
+ const error = asRecord(terminal?.data.error);
274
+ const lastAssistant = assistants.at(-1);
275
+ const status = terminal?.type === "run.completed"
276
+ ? "completed"
277
+ : terminal?.type === "run.failed"
278
+ ? "failed"
279
+ : terminal?.type === "run.paused"
280
+ ? "paused"
281
+ : "running";
282
+ return {
283
+ event_id: `${manifest.key}:${sessionId}:${run}`,
284
+ agent_external_id: manifest.key,
285
+ session_id: sessionId,
286
+ run_number: run,
287
+ ...(typeof lineage?.parentAgent === "string"
288
+ ? { parent_agent_external_id: lineage.parentAgent }
289
+ : {}),
290
+ ...(typeof lineage?.parentSessionId === "string"
291
+ ? { parent_session_id: lineage.parentSessionId }
292
+ : {}),
293
+ status,
294
+ provider: typeof started.data.provider === "string"
295
+ ? started.data.provider
296
+ : manifest.provider,
297
+ model: typeof started.data.model === "string"
298
+ ? started.data.model
299
+ : manifest.model,
300
+ ...(typeof started.data.region === "string"
301
+ ? { region: started.data.region }
302
+ : {}),
303
+ ...(typeof started.data.outputType === "string"
304
+ ? { output_type: started.data.outputType }
305
+ : {}),
306
+ ...(typeof lastAssistant?.data.stopReason === "string"
307
+ ? { stop_reason: lastAssistant.data.stopReason }
308
+ : {}),
309
+ started_at: started.timestamp,
310
+ ...(terminal ? { ended_at: terminal.timestamp } : {}),
311
+ ...(typeof terminal?.data.durationMs === "number"
312
+ ? { duration_ms: terminal.data.durationMs }
313
+ : {}),
314
+ model_duration_ms: sumNumber(assistants, "durationMs"),
315
+ action_duration_ms: sumNumber([...completedActions, ...failedActions], "durationMs"),
316
+ ...usage,
317
+ user_message_count: runEvents.filter((event) => event.type === "message.created" && event.data.role === "user").length,
318
+ assistant_message_count: assistants.length,
319
+ action_count: runEvents.filter((event) => event.type === "action.requested").length,
320
+ action_success_count: completedActions.length,
321
+ action_failure_count: failedActions.length,
322
+ subagent_count: runEvents.filter((event) => event.type === "subagent.initiated").length,
323
+ actions: projectActionLogs(runEvents),
324
+ evaluation_count: evaluations.length,
325
+ evaluation_pass_count: evaluations.filter((event) => event.type === "evaluation.completed" &&
326
+ event.data.status === "passed").length,
327
+ ...(typeof error?.code === "string" ? { error_code: error.code } : {}),
328
+ ...(typeof error?.message === "string"
329
+ ? { error_message: sanitizeErrorMessage(error.message) }
330
+ : {}),
331
+ ...(typeof error?.retryable === "boolean"
332
+ ? { error_retryable: error.retryable }
333
+ : {}),
334
+ };
335
+ }
336
+ function normalizeUsage(terminalUsage, assistants) {
337
+ const direct = asRecord(terminalUsage);
338
+ if (direct) {
339
+ return usageFields(direct);
340
+ }
341
+ const total = {
342
+ inputTokens: 0,
343
+ outputTokens: 0,
344
+ reasoningTokens: 0,
345
+ cacheReadTokens: 0,
346
+ cacheWriteTokens: 0,
347
+ };
348
+ for (const event of assistants) {
349
+ const usage = asRecord(event.data.usage);
350
+ if (!usage)
351
+ continue;
352
+ total.inputTokens += numberValue(usage.inputTokens);
353
+ total.outputTokens += numberValue(usage.outputTokens);
354
+ total.reasoningTokens =
355
+ (total.reasoningTokens ?? 0) + numberValue(usage.reasoningTokens);
356
+ total.cacheReadTokens += numberValue(usage.cacheReadTokens);
357
+ total.cacheWriteTokens += numberValue(usage.cacheWriteTokens);
358
+ }
359
+ return usageFields(total);
360
+ }
361
+ function usageFields(usage) {
362
+ const reasoning = numberValue(usage.reasoningTokens);
363
+ return {
364
+ input_tokens: numberValue(usage.inputTokens),
365
+ output_tokens: numberValue(usage.outputTokens),
366
+ ...(reasoning > 0 ? { reasoning_tokens: reasoning } : {}),
367
+ cache_read_tokens: numberValue(usage.cacheReadTokens),
368
+ cache_write_tokens: numberValue(usage.cacheWriteTokens),
369
+ };
370
+ }
371
+ function sumNumber(events, key) {
372
+ return events.reduce((sum, event) => sum + numberValue(event.data[key]), 0);
373
+ }
374
+ function projectActionLogs(events) {
375
+ const requestedByCallId = new Map();
376
+ for (const event of events) {
377
+ if (event.type === "action.requested" &&
378
+ typeof event.data.callId === "string") {
379
+ requestedByCallId.set(event.data.callId, event);
380
+ }
381
+ }
382
+ return events
383
+ .filter((event) => event.type === "action.completed" || event.type === "action.failed")
384
+ .slice(0, 100)
385
+ .map((event) => {
386
+ const error = asRecord(event.data.error);
387
+ const requested = typeof event.data.callId === "string"
388
+ ? requestedByCallId.get(event.data.callId)
389
+ : undefined;
390
+ return {
391
+ call_id: typeof event.data.callId === "string"
392
+ ? event.data.callId
393
+ : `action-${event.sequence}`,
394
+ name: typeof event.data.name === "string" ? event.data.name : "unknown",
395
+ status: event.type === "action.completed"
396
+ ? "completed"
397
+ : "failed",
398
+ started_at: requested?.timestamp ?? event.timestamp,
399
+ ended_at: event.timestamp,
400
+ duration_ms: numberValue(event.data.durationMs),
401
+ ...(typeof error?.code === "string" ? { error_code: error.code } : {}),
402
+ ...(typeof error?.message === "string"
403
+ ? { error_message: sanitizeErrorMessage(error.message) }
404
+ : {}),
405
+ };
406
+ });
407
+ }
408
+ function takeBatch(queue) {
409
+ const batch = [];
410
+ let bytes = utf8ByteLength('{"schema_version":1,"runs":[]}');
411
+ for (const fact of queue.values()) {
412
+ const factBytes = utf8ByteLength(JSON.stringify(fact)) + 1;
413
+ if (batch.length > 0 &&
414
+ (batch.length >= MAX_BATCH_RUNS ||
415
+ bytes + factBytes > MAX_KEEPALIVE_BYTES)) {
416
+ break;
417
+ }
418
+ batch.push(fact);
419
+ bytes += factBytes;
420
+ }
421
+ return batch;
422
+ }
423
+ function utf8ByteLength(value) {
424
+ if (typeof TextEncoder !== "undefined") {
425
+ return new TextEncoder().encode(value).byteLength;
426
+ }
427
+ let bytes = 0;
428
+ for (let index = 0; index < value.length; index += 1) {
429
+ const code = value.charCodeAt(index);
430
+ if (code < 0x80)
431
+ bytes += 1;
432
+ else if (code < 0x800)
433
+ bytes += 2;
434
+ else if (code >= 0xd800 && code <= 0xdbff) {
435
+ bytes += 4;
436
+ index += 1;
437
+ }
438
+ else
439
+ bytes += 3;
440
+ }
441
+ return bytes;
442
+ }
443
+ function numberValue(value) {
444
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
445
+ }
446
+ function asRecord(value) {
447
+ return typeof value === "object" && value !== null && !Array.isArray(value)
448
+ ? value
449
+ : undefined;
450
+ }
451
+ function sanitizeErrorMessage(value) {
452
+ return value
453
+ .replace(/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [redacted]")
454
+ .replace(/\b(authorization|api[-_ ]?key|token|secret|password)\b\s*[:=]\s*[^\s,;]+/gi, "$1=[redacted]")
455
+ .replace(/\b(?:sk|pk|amarsia)_[A-Za-z0-9_-]{12,}\b/gi, "[redacted]")
456
+ .slice(0, 8000);
457
+ }
@@ -0,0 +1 @@
1
+ export declare function resolveAmarsiaEndpoint(endpoint: string | undefined): string;
@@ -0,0 +1,8 @@
1
+ const DEFAULT_AMARSIA_API_URL = "https://api.amarsia.com";
2
+ export function resolveAmarsiaEndpoint(endpoint) {
3
+ return (endpoint?.trim() ||
4
+ (typeof process !== "undefined"
5
+ ? process.env?.AMARSIA_API_URL
6
+ : undefined) ||
7
+ DEFAULT_AMARSIA_API_URL).replace(/\/$/, "");
8
+ }
@@ -1,5 +1,5 @@
1
1
  import { createAmarsiaProjectMetadata, } from "./metadata.js";
2
- const DEFAULT_AMARSIA_API_URL = "https://api.amarsia.com";
2
+ import { resolveAmarsiaEndpoint } from "./endpoint.js";
3
3
  const syncedHashes = new Map();
4
4
  export function syncAmarsiaProject(options) {
5
5
  const metadata = createAmarsiaProjectMetadata(options.bundle, options.projectRoot);
@@ -19,9 +19,6 @@ export function syncAmarsiaProject(options) {
19
19
  console.warn(`[orcha] Amarsia project sync failed: ${message}`);
20
20
  });
21
21
  }
22
- function resolveAmarsiaEndpoint(endpoint) {
23
- return (endpoint?.trim() || process.env.AMARSIA_API_URL || DEFAULT_AMARSIA_API_URL).replace(/\/$/, "");
24
- }
25
22
  async function postProjectMetadata(options) {
26
23
  const response = await fetch(`${options.baseUrl}/v3/project-sync`, {
27
24
  method: "PUT",
package/dist/errors.d.ts CHANGED
@@ -9,3 +9,8 @@ export declare class OrchaError extends Error {
9
9
  cause?: unknown;
10
10
  });
11
11
  }
12
+ export declare function describeProviderError(error: unknown): {
13
+ message: string;
14
+ details?: Record<string, unknown>;
15
+ };
16
+ export declare function isRetryableProviderConnectionError(error: unknown): boolean;
package/dist/errors.js CHANGED
@@ -10,3 +10,105 @@ export class OrchaError extends Error {
10
10
  this.details = options.details;
11
11
  }
12
12
  }
13
+ export function describeProviderError(error) {
14
+ const rootMessage = errorMessage(error);
15
+ const cause = deepestUsefulCause(error);
16
+ if (!cause) {
17
+ return { message: rootMessage };
18
+ }
19
+ const causeMessage = errorMessage(cause);
20
+ const causeCode = stringOrNumberField(cause, "code");
21
+ const syscall = stringField(cause, "syscall");
22
+ const hostname = stringField(cause, "hostname");
23
+ const explanation = causeMessage !== rootMessage
24
+ ? causeMessage
25
+ : causeCode
26
+ ? String(causeCode)
27
+ : undefined;
28
+ const details = {
29
+ ...(causeCode !== undefined ? { causeCode } : {}),
30
+ ...(syscall ? { syscall } : {}),
31
+ ...(hostname ? { hostname } : {}),
32
+ ...(explanation ? { causeMessage: explanation } : {}),
33
+ };
34
+ return {
35
+ message: explanation ? `${rootMessage}: ${explanation}` : rootMessage,
36
+ ...(Object.keys(details).length > 0 ? { details } : {}),
37
+ };
38
+ }
39
+ export function isRetryableProviderConnectionError(error) {
40
+ const retryableCodes = new Set([
41
+ "UND_ERR_CONNECT_TIMEOUT",
42
+ "EAI_AGAIN",
43
+ "ENETUNREACH",
44
+ "EHOSTUNREACH",
45
+ "ECONNREFUSED",
46
+ ]);
47
+ const pending = [error];
48
+ const visited = new Set();
49
+ while (pending.length > 0 && visited.size < 8) {
50
+ const current = pending.shift();
51
+ if (typeof current !== "object" ||
52
+ current === null ||
53
+ visited.has(current)) {
54
+ continue;
55
+ }
56
+ visited.add(current);
57
+ const record = current;
58
+ if (typeof record.code === "string" &&
59
+ retryableCodes.has(record.code)) {
60
+ return true;
61
+ }
62
+ if (record.cause !== undefined)
63
+ pending.push(record.cause);
64
+ if (Array.isArray(record.errors))
65
+ pending.push(...record.errors);
66
+ }
67
+ return false;
68
+ }
69
+ function deepestUsefulCause(error) {
70
+ let current = error;
71
+ let useful;
72
+ const visited = new Set();
73
+ for (let depth = 0; depth < 5; depth += 1) {
74
+ if ((typeof current !== "object" || current === null) ||
75
+ visited.has(current)) {
76
+ break;
77
+ }
78
+ visited.add(current);
79
+ const record = current;
80
+ const next = record.cause ??
81
+ (Array.isArray(record.errors) ? record.errors[0] : undefined);
82
+ if (next === undefined)
83
+ break;
84
+ useful = next;
85
+ current = next;
86
+ }
87
+ return useful;
88
+ }
89
+ function errorMessage(error) {
90
+ if (error instanceof Error && error.message.trim()) {
91
+ return error.message.trim();
92
+ }
93
+ if (typeof error === "object" && error !== null) {
94
+ const message = error.message;
95
+ if (typeof message === "string" && message.trim()) {
96
+ return message.trim();
97
+ }
98
+ }
99
+ return String(error);
100
+ }
101
+ function stringField(value, key) {
102
+ if (typeof value !== "object" || value === null)
103
+ return undefined;
104
+ const field = value[key];
105
+ return typeof field === "string" && field.trim() ? field : undefined;
106
+ }
107
+ function stringOrNumberField(value, key) {
108
+ if (typeof value !== "object" || value === null)
109
+ return undefined;
110
+ const field = value[key];
111
+ return typeof field === "string" || typeof field === "number"
112
+ ? field
113
+ : undefined;
114
+ }
@@ -1,4 +1,4 @@
1
- import { OrchaError } from "../errors.js";
1
+ import { describeProviderError, isRetryableProviderConnectionError, OrchaError, } from "../errors.js";
2
2
  import { normalizeLocalFile, normalizeUrlContent, } from "../file-content.js";
3
3
  import { messagesFromEvents, normalizePersistedToolResults, SessionEventWriter, } from "./session-events.js";
4
4
  import { validateStructuredValue } from "./structured-output.js";
@@ -620,7 +620,8 @@ export class RuntimeAgent {
620
620
  }
621
621
  providerTools.push(...this.#subagentToolDefinitions());
622
622
  try {
623
- response = await this.#generateProviderResponse(this.#manifest.provider, provider, {
623
+ const signal = this.#sessionControls.get(sessionId)?.controller.signal;
624
+ response = await withProviderConnectionRetry(() => this.#generateProviderResponse(this.#manifest.provider, provider, {
624
625
  model: this.#manifest.model,
625
626
  region: this.#manifest.region,
626
627
  maxTokens: this.#manifest.maxTokens,
@@ -634,8 +635,8 @@ export class RuntimeAgent {
634
635
  streamedOutput = output;
635
636
  publishOutput?.(output);
636
637
  },
637
- signal: this.#sessionControls.get(sessionId)?.controller.signal,
638
- });
638
+ signal,
639
+ }), signal);
639
640
  }
640
641
  catch (error) {
641
642
  if (this.#sessionControls.get(sessionId)?.pauseRequested) {
@@ -665,7 +666,16 @@ export class RuntimeAgent {
665
666
  if (error instanceof OrchaError) {
666
667
  throw error;
667
668
  }
668
- throw new OrchaError("provider_error", error instanceof Error ? error.message : String(error), { retryable: true, cause: error });
669
+ const providerError = describeProviderError(error);
670
+ throw new OrchaError("provider_error", providerError.message, {
671
+ retryable: true,
672
+ cause: error,
673
+ details: {
674
+ provider: this.#manifest.provider,
675
+ model: this.#manifest.model,
676
+ ...providerError.details,
677
+ },
678
+ });
669
679
  }
670
680
  const modelDurationMs = Math.round(performance.now() - modelStartedAt);
671
681
  const usage = aggregateUsage(previousEvents, writer.run, response.usage);
@@ -2609,6 +2619,21 @@ function findLast(events, predicate) {
2609
2619
  }
2610
2620
  return undefined;
2611
2621
  }
2622
+ async function withProviderConnectionRetry(operation, signal) {
2623
+ try {
2624
+ return await operation();
2625
+ }
2626
+ catch (error) {
2627
+ if (signal?.aborted ||
2628
+ !isRetryableProviderConnectionError(error)) {
2629
+ throw error;
2630
+ }
2631
+ await new Promise((resolve) => setTimeout(resolve, 250));
2632
+ if (signal?.aborted)
2633
+ throw error;
2634
+ return operation();
2635
+ }
2636
+ }
2612
2637
  function failure(sessionId, code, message, options = {}) {
2613
2638
  const retryable = options.retryable ??
2614
2639
  (code === "session_busy" ||