@agent-native/core 0.159.5 → 0.160.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.
Files changed (81) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/assets/server/lib/generation.ts +10 -6
  3. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +18 -4
  4. package/corpus/templates/calendar/actions/create-event.ts +4 -2
  5. package/corpus/templates/calendar/actions/event-action-helpers.ts +56 -9
  6. package/corpus/templates/calendar/actions/update-event.ts +1 -0
  7. package/corpus/templates/calendar/agent-native.config.ts +1 -0
  8. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +8 -2
  9. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +102 -68
  10. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +150 -49
  11. package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +57 -23
  12. package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +4 -1
  13. package/corpus/templates/calendar/app/components/calendar/TimezoneSwitchDialog.tsx +70 -0
  14. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +127 -88
  15. package/corpus/templates/calendar/app/components/calendar/WorkingLocationEditor.tsx +59 -25
  16. package/corpus/templates/calendar/app/hooks/use-event-drag.ts +55 -18
  17. package/corpus/templates/calendar/app/i18n-data.ts +60 -0
  18. package/corpus/templates/calendar/app/lib/all-day-layout.ts +16 -8
  19. package/corpus/templates/calendar/app/lib/calendar-drafts.ts +56 -0
  20. package/corpus/templates/calendar/app/lib/calendar-timezone.ts +351 -0
  21. package/corpus/templates/calendar/app/lib/event-layout.ts +14 -8
  22. package/corpus/templates/calendar/app/lib/out-of-office.ts +29 -61
  23. package/corpus/templates/calendar/app/lib/working-location.ts +34 -2
  24. package/corpus/templates/calendar/app/pages/CalendarView.tsx +317 -139
  25. package/corpus/templates/calendar/changelog/2026-08-13-calendar-stays-pinned-to-the-saved-timezone-and-asks-before-.md +6 -0
  26. package/corpus/templates/calendar/changelog/2026-08-13-working-locations-can-be-added-from-calendar-days-with-a-ful.md +6 -0
  27. package/corpus/templates/calendar/changelog/2026-08-14-choosing-office-or-other-when-adding-a-working-location-creates.md +6 -0
  28. package/corpus/templates/calendar/changelog/2026-08-14-creating-an-other-working-location-now-keeps-the-custom-name.md +6 -0
  29. package/corpus/templates/calendar/changelog/2026-08-14-timed-working-locations-keep-home-office-or-custom-titles.md +6 -0
  30. package/corpus/templates/calendar/changelog/2026-08-14-timed-working-locations-now-keep-a-home-office-or-custom-title.md +6 -0
  31. package/corpus/templates/calendar/changelog/2026-08-14-turning-a-midnight-ending-working-location-back-to-all-day.md +6 -0
  32. package/corpus/templates/calendar/server/lib/google-calendar.ts +22 -1
  33. package/dist/agent/engine/builder-engine.d.ts +16 -2
  34. package/dist/agent/engine/builder-engine.js +178 -115
  35. package/dist/agent/engine/builtin.js +13 -4
  36. package/dist/agent/engine/credential-errors.d.ts +23 -0
  37. package/dist/agent/engine/credential-errors.js +19 -0
  38. package/dist/agent/engine/error-detail.d.ts +15 -0
  39. package/dist/agent/engine/error-detail.js +29 -0
  40. package/dist/agent/engine/registry.d.ts +14 -3
  41. package/dist/agent/engine/registry.js +106 -61
  42. package/dist/agent/engine/types.d.ts +16 -0
  43. package/dist/agent/engine/types.js +9 -0
  44. package/dist/agent/production-agent.d.ts +7 -0
  45. package/dist/agent/production-agent.js +68 -31
  46. package/dist/agent/run-manager.js +30 -0
  47. package/dist/agent/thread-data-builder.js +4 -0
  48. package/dist/agent/tool-approval-migrations.d.ts +1 -0
  49. package/dist/agent/tool-approval-migrations.js +10 -0
  50. package/dist/agent/tool-approval-store.d.ts +5 -3
  51. package/dist/agent/tool-approval-store.js +11 -6
  52. package/dist/agent/types.d.ts +9 -0
  53. package/dist/cli/code-agent-connector.js +79 -7
  54. package/dist/cli/portal-transfer.d.ts +43 -0
  55. package/dist/cli/portal-transfer.js +201 -0
  56. package/dist/client/agent-chat-adapter.js +46 -15
  57. package/dist/client/error-format.js +25 -2
  58. package/dist/client/sse-event-processor.d.ts +2 -0
  59. package/dist/client/sse-event-processor.js +12 -1
  60. package/dist/collab/awareness.d.ts +2 -2
  61. package/dist/collab/struct-routes.d.ts +1 -1
  62. package/dist/jobs/background-automation-runner.js +9 -15
  63. package/dist/notifications/routes.d.ts +6 -6
  64. package/dist/observability/routes.d.ts +5 -5
  65. package/dist/resources/handlers.d.ts +1 -1
  66. package/dist/secrets/register-framework-secrets.js +25 -0
  67. package/dist/secrets/routes.d.ts +9 -9
  68. package/dist/server/agent-chat-plugin.js +9 -2
  69. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  70. package/dist/server/credential-provider.d.ts +80 -0
  71. package/dist/server/credential-provider.js +168 -1
  72. package/dist/server/google-realtime-session.d.ts +1 -1
  73. package/dist/server/google-realtime-session.js +6 -6
  74. package/dist/server/index.d.ts +1 -1
  75. package/dist/server/index.js +6 -1
  76. package/dist/server/realtime-token.d.ts +1 -1
  77. package/dist/server/realtime-voice.js +12 -6
  78. package/dist/server/transcribe-voice.d.ts +1 -1
  79. package/dist/server/transcribe-voice.js +18 -16
  80. package/dist/transcription/builder-transcription.js +8 -4
  81. package/package.json +3 -3
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: changed
3
+ date: 2026-08-13
4
+ ---
5
+
6
+ Calendar stays pinned to the saved timezone and asks before adopting a changed browser timezone
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-08-13
4
+ ---
5
+
6
+ Working locations can be added from calendar days, with a full-day first entry and timed same-day additions
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-14
4
+ ---
5
+
6
+ Choosing Office or Other when adding a working location creates that type, and adding one on a day that already has a location updates that day instead of extending Home.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-14
4
+ ---
5
+
6
+ Creating an Other working location now keeps the custom name instead of saving it as Working.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-14
4
+ ---
5
+
6
+ Timed working locations keep a Home, Office, or custom title instead of the generated Working location label.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-14
4
+ ---
5
+
6
+ Timed working locations now keep a Home, Office, or custom title instead of showing as Untitled.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-14
4
+ ---
5
+
6
+ Turning a timed working location that ends at midnight back to all-day no longer adds an extra day.
@@ -1209,6 +1209,21 @@ export async function getEvent(
1209
1209
  };
1210
1210
  }
1211
1211
 
1212
+ function timedWorkingLocationSummary(event: CalendarEvent): string | undefined {
1213
+ if (event.eventType !== "workingLocation" || event.allDay) return undefined;
1214
+ const properties = event.workingLocationProperties;
1215
+ if (properties?.type === "officeLocation") {
1216
+ return properties.officeLocation?.label || "Office";
1217
+ }
1218
+ if (properties?.type === "customLocation") {
1219
+ return properties.customLocation?.label || "Working location";
1220
+ }
1221
+ if (properties?.type === "homeOffice") return "Home";
1222
+ const trimmed = event.title?.trim();
1223
+ if (trimmed && !event.titleIsGenerated) return trimmed;
1224
+ return "Home";
1225
+ }
1226
+
1212
1227
  export async function createEvent(
1213
1228
  event: CalendarEvent,
1214
1229
  opts: {
@@ -1230,9 +1245,15 @@ export async function createEvent(
1230
1245
  throw new Error("Out of office and focus time events must be timed.");
1231
1246
  }
1232
1247
 
1248
+ const workingLocationSummary = timedWorkingLocationSummary(event);
1233
1249
  const body: any =
1234
1250
  event.eventType === "workingLocation"
1235
- ? buildDateRange(event)
1251
+ ? {
1252
+ ...buildDateRange(event),
1253
+ ...(workingLocationSummary
1254
+ ? { summary: workingLocationSummary }
1255
+ : {}),
1256
+ }
1236
1257
  : {
1237
1258
  summary: event.title,
1238
1259
  description: event.description,
@@ -8,14 +8,26 @@
8
8
  * concurrency) into structured stop events that carry an upgrade URL when
9
9
  * the chat UI needs to prompt the user to upgrade.
10
10
  *
11
- * Credentials come from BUILDER_PRIVATE_KEY + BUILDER_PUBLIC_KEY (set via the
12
- * Builder CLI-auth onboarding flow). Base URL is overridable via
11
+ * Credentials come from the gateway lane (`resolveBuilderGatewayCredentials`):
12
+ * the user's own Builder connection when they have one, otherwise the
13
+ * deployment's Builder-credits pair. Base URL is overridable via
13
14
  * BUILDER_GATEWAY_BASE_URL.
14
15
  */
16
+ import { type BuilderGatewayLane } from "../../server/credential-provider.js";
15
17
  import type { AgentEngine, EngineCapabilities } from "./types.js";
16
18
  export declare const BUILDER_CAPABILITIES: EngineCapabilities;
17
19
  export declare const BUILDER_SUPPORTED_MODELS: readonly ["auto", string, "gpt-5-6-terra", "gpt-5-6-sol", "claude-haiku-4-5", "claude-sonnet-4-6" | "claude-sonnet-5", "claude-opus-4-8", "gemini-3-5-flash", "gemini-3-1-pro"];
18
20
  export declare const BUILDER_MODEL_UNAUTHORIZED_ERROR_CODE = "builder_model_unauthorized";
21
+ /**
22
+ * A truncated stream, not a rejected request: the client continues the partial
23
+ * turn, so this is absent from `isRetryableError` on purpose.
24
+ *
25
+ * Every predicate that recognises `builder_gateway_network_error` must list this
26
+ * too, or a truncated stream silently stops recovering: `isResumableEngineError`,
27
+ * `isRecoverableContinuationError`, `shouldCaptureRunError`,
28
+ * `isInternalContinuationError` and the client's `sse-event-processor`.
29
+ */
30
+ export declare const BUILDER_GATEWAY_STREAM_ENDED_ERROR_CODE = "builder_gateway_stream_ended";
19
31
  export declare const BUILDER_DEFAULT_MODEL: string;
20
32
  /**
21
33
  * Credentials captured by a durable caller that cannot rely on ambient
@@ -26,5 +38,7 @@ export interface BuilderEngineCredentials {
26
38
  publicKey: string | null;
27
39
  userId?: string | null;
28
40
  orgName?: string | null;
41
+ /** Which lane these came from, when the capturing caller knew. */
42
+ lane?: BuilderGatewayLane | null;
29
43
  }
30
44
  export declare function createBuilderEngine(config?: Record<string, unknown>): AgentEngine;
@@ -8,19 +8,20 @@
8
8
  * concurrency) into structured stop events that carry an upgrade URL when
9
9
  * the chat UI needs to prompt the user to upgrade.
10
10
  *
11
- * Credentials come from BUILDER_PRIVATE_KEY + BUILDER_PUBLIC_KEY (set via the
12
- * Builder CLI-auth onboarding flow). Base URL is overridable via
11
+ * Credentials come from the gateway lane (`resolveBuilderGatewayCredentials`):
12
+ * the user's own Builder connection when they have one, otherwise the
13
+ * deployment's Builder-credits pair. Base URL is overridable via
13
14
  * BUILDER_GATEWAY_BASE_URL.
14
15
  */
15
16
  import { captureError } from "../../server/capture-error.js";
16
- import { clearBuilderCredentialAuthFailure, resolveBuilderCredentials, getBuilderGatewayBaseUrl, recordBuilderCredentialAuthFailure, } from "../../server/credential-provider.js";
17
+ import { clearBuilderGatewayAuthFailure, isBuilderGatewayDeployConfigured, resolveBuilderGatewayCredentialsDetailed, getBuilderGatewayBaseUrl, recordBuilderGatewayAuthFailure, } from "../../server/credential-provider.js";
17
18
  import { applyBuilderUtmTrackingParams } from "../../shared/builder-link-tracking.js";
18
19
  import { isGPTReasoningModel, normalizeReasoningEffortForModel, } from "../../shared/reasoning-effort.js";
19
20
  import { isInBackgroundFunctionRuntime } from "../durable-background.js";
20
21
  import { BUILDER_MODEL_CONFIG } from "../model-config.js";
21
22
  import { getBuilderGatewayRequestHeaders } from "./builder-gateway-headers.js";
22
- import { LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, } from "./credential-errors.js";
23
- import { describeErrorWithCauses, isProviderConnectionErrorMessage, } from "./error-detail.js";
23
+ import { gatewayVisitorFacingError, LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, } from "./credential-errors.js";
24
+ import { classifyTerminalErrorCode, describeErrorWithCauses, isContextOverflowCode, isContextOverflowMessage, isProviderConnectionErrorMessage, } from "./error-detail.js";
24
25
  import { FIRST_STREAM_EVENT_TIMEOUT_MS } from "./first-event-timeout.js";
25
26
  import { resolveMaxOutputTokensForEngine } from "./output-tokens.js";
26
27
  import { splitSystemPromptForCache, stablePrefixCacheControl, } from "./prompt-cache.js";
@@ -48,6 +49,16 @@ const MAX_BACKGROUND_BUILDER_GATEWAY_TIMEOUT_MS = 14 * 60_000;
48
49
  const MAX_LOCAL_BUILDER_GATEWAY_TIMEOUT_MS = MAX_BACKGROUND_BUILDER_GATEWAY_TIMEOUT_MS;
49
50
  const BUILDER_GATEWAY_NETWORK_ERROR_CODE = "builder_gateway_network_error";
50
51
  export const BUILDER_MODEL_UNAUTHORIZED_ERROR_CODE = "builder_model_unauthorized";
52
+ /**
53
+ * A truncated stream, not a rejected request: the client continues the partial
54
+ * turn, so this is absent from `isRetryableError` on purpose.
55
+ *
56
+ * Every predicate that recognises `builder_gateway_network_error` must list this
57
+ * too, or a truncated stream silently stops recovering: `isResumableEngineError`,
58
+ * `isRecoverableContinuationError`, `shouldCaptureRunError`,
59
+ * `isInternalContinuationError` and the client's `sse-event-processor`.
60
+ */
61
+ export const BUILDER_GATEWAY_STREAM_ENDED_ERROR_CODE = "builder_gateway_stream_ended";
51
62
  export const BUILDER_DEFAULT_MODEL = BUILDER_MODEL_CONFIG.defaultModel;
52
63
  /**
53
64
  * Bucket an Anthropic `thinking.budgetTokens` value into the gateway's
@@ -94,6 +105,15 @@ async function buildUpgradeUrl() {
94
105
  });
95
106
  return url.toString();
96
107
  }
108
+ /**
109
+ * `isBuilderGatewayDeployConfigured()` must gate both answers: it owns the
110
+ * dev-preview exclusion, and a captured lane cannot substitute for it.
111
+ */
112
+ function isBuilderCreditsLane(creds) {
113
+ if (!isBuilderGatewayDeployConfigured())
114
+ return false;
115
+ return creds.lane ? creds.lane === "gateway-deploy" : true;
116
+ }
97
117
  class BuilderEngine {
98
118
  configuredCredentials;
99
119
  name = "builder";
@@ -105,17 +125,17 @@ class BuilderEngine {
105
125
  this.configuredCredentials = configuredCredentials;
106
126
  }
107
127
  async *stream(opts) {
108
- const creds = this.configuredCredentials ?? (await resolveBuilderCredentials());
128
+ const creds = this.configuredCredentials ??
129
+ (await resolveBuilderGatewayCredentialsDetailed());
130
+ const creditsLane = isBuilderCreditsLane(creds);
109
131
  const authHeader = creds.privateKey ? `Bearer ${creds.privateKey}` : null;
110
132
  const spaceId = creds.publicKey;
111
133
  const builderUserId = creds.userId;
112
134
  if (!authHeader || !spaceId) {
113
- yield {
114
- type: "stop",
115
- reason: "error",
135
+ yield gatewayErrorStop({
116
136
  error: LLM_MISSING_CREDENTIALS_MESSAGE,
117
137
  errorCode: LLM_MISSING_CREDENTIALS_ERROR_CODE,
118
- };
138
+ }, creditsLane);
119
139
  return;
120
140
  }
121
141
  const messages = engineMessagesToBuilderGatewayAnthropic(opts.messages);
@@ -245,12 +265,12 @@ class BuilderEngine {
245
265
  elapsedMs: Date.now() - tStart,
246
266
  });
247
267
  }
248
- yield createBuilderGatewayTimeoutStop(err, timedOut, gatewayAbort.effectiveTimeoutMs());
268
+ yield createBuilderGatewayTimeoutStop(err, timedOut, gatewayAbort.effectiveTimeoutMs(), creditsLane);
249
269
  return;
250
270
  }
251
271
  console.log(`[builder-engine] ← ${response.status} ${response.statusText} in ${Date.now() - tStart}ms`);
252
272
  if (!response.ok) {
253
- yield* emitHttpError(response);
273
+ yield* emitHttpError(response, { creditsLane });
254
274
  return;
255
275
  }
256
276
  // A successful gateway call proves the connected credentials are valid
@@ -259,8 +279,9 @@ class BuilderEngine {
259
279
  // self-healing path for workspace/env-managed credentials, which never
260
280
  // flow through writeBuilderCredentials.
261
281
  try {
262
- const creds = this.configuredCredentials ?? (await resolveBuilderCredentials());
263
- await clearBuilderCredentialAuthFailure({
282
+ const creds = this.configuredCredentials ??
283
+ (await resolveBuilderGatewayCredentialsDetailed());
284
+ await clearBuilderGatewayAuthFailure({
264
285
  privateKey: creds.privateKey,
265
286
  publicKey: creds.publicKey,
266
287
  });
@@ -272,24 +293,29 @@ class BuilderEngine {
272
293
  const contentType = response.headers.get("content-type") ?? "";
273
294
  if (contentType.includes("text/html")) {
274
295
  const rawText = await response.text().catch(() => "");
275
- yield {
276
- type: "stop",
277
- reason: "error",
278
- error: normalizeGatewayErrorText(rawText, response.status || 502),
279
- errorCode: `http_${response.status || 502}`,
280
- };
296
+ const status = response.status || 502;
297
+ const error = normalizeGatewayErrorText(rawText, status);
298
+ yield gatewayErrorStop({
299
+ error,
300
+ errorCode: `http_${status}`,
301
+ statusCode: status,
302
+ ...(isTransientGatewayFailure(error, status)
303
+ ? { providerRetryable: true }
304
+ : {}),
305
+ }, creditsLane);
281
306
  return;
282
307
  }
283
308
  const reader = response.body?.getReader();
284
309
  if (!reader) {
285
- yield {
286
- type: "stop",
287
- reason: "error",
310
+ yield gatewayErrorStop({
288
311
  error: "Builder gateway response has no body",
289
- };
312
+ errorCode: "builder_gateway_error",
313
+ statusCode: response.status,
314
+ }, creditsLane);
290
315
  return;
291
316
  }
292
317
  yield* parseJsonlStream(reader, opts.model, {
318
+ creditsLane,
293
319
  abortSignal: gatewayAbort.signal,
294
320
  didGatewayTimeout: gatewayAbort.didTimeout,
295
321
  getGatewayTimeoutMs: gatewayAbort.effectiveTimeoutMs,
@@ -303,7 +329,49 @@ class BuilderEngine {
303
329
  }
304
330
  }
305
331
  }
306
- async function* emitHttpError(response) {
332
+ /**
333
+ * Gateway statuses another attempt can clear. 402/401/403 quota and auth
334
+ * rejections are absent on purpose — they are terminal until someone acts.
335
+ */
336
+ const RETRYABLE_GATEWAY_STATUSES = new Set([408, 429, 500, 502, 503, 504, 529]);
337
+ /** Read against the RAW reply: on the credits lane the message is replaced. */
338
+ const TRANSIENT_UPSTREAM_PATTERN = /overloaded|rate_limit|rate limit reached|too many requests|\b429\b|\b529\b|\b502\b|\b503\b|\b504\b|resource_exhausted|quota exceeded|socket hang up|connection reset|temporarily unavailable|timeout/i;
339
+ function isTransientGatewayFailure(rawMessage, status) {
340
+ if (status !== undefined && RETRYABLE_GATEWAY_STATUSES.has(status)) {
341
+ return true;
342
+ }
343
+ return TRANSIENT_UPSTREAM_PATTERN.test(rawMessage);
344
+ }
345
+ /**
346
+ * EVERY terminal `reason: "error"` this module emits must go through here,
347
+ * including those with no HTTP response behind them. A branch building its own
348
+ * stop literal ships owner copy to a visitor; `gateway-error-retryability.spec.ts`
349
+ * fails on a second literal in this file.
350
+ *
351
+ * On the credits lane the message collapses to one visitor line, so
352
+ * `statusCode` / `providerRetryable` / `contextOverflow` are the only retry
353
+ * signals downstream may read: keyword coupling to the message turns a retryable
354
+ * throttle into a dead turn on credits sites alone.
355
+ */
356
+ function gatewayErrorStop(details, creditsLane) {
357
+ const { error, errorCode, upgradeUrl, ...retry } = details;
358
+ return {
359
+ type: "stop",
360
+ reason: "error",
361
+ ...(creditsLane
362
+ ? gatewayVisitorFacingError(errorCode)
363
+ : {
364
+ error,
365
+ ...(errorCode ? { errorCode } : {}),
366
+ ...(upgradeUrl ? { upgradeUrl } : {}),
367
+ }),
368
+ ...(isContextOverflowMessage(error) || isContextOverflowCode(errorCode)
369
+ ? { contextOverflow: true }
370
+ : {}),
371
+ ...retry,
372
+ };
373
+ }
374
+ async function* emitHttpError(response, opts) {
307
375
  const status = response.status;
308
376
  // Read the body once as text and then try to parse — calling `.json()`
309
377
  // and then `.text()` as a fallback fails because the body stream is
@@ -321,84 +389,67 @@ async function* emitHttpError(response) {
321
389
  }
322
390
  const code = errBody.code ?? `http_${status}`;
323
391
  const message = errBody.message ?? `Builder gateway returned ${status}`;
392
+ const stop = (details) => gatewayErrorStop(details, opts.creditsLane);
324
393
  // Belt-and-suspenders: 402 without a structured `credits-limit` code
325
394
  // (e.g. bare proxy response) still means quota → show upgrade CTA.
326
395
  if (code.startsWith("credits-limit") || status === 402) {
327
- yield {
328
- type: "stop",
329
- reason: "error",
396
+ yield stop({
330
397
  error: message,
331
398
  errorCode: code,
332
399
  upgradeUrl: await buildUpgradeUrl(),
333
- };
400
+ });
334
401
  return;
335
402
  }
336
403
  if (code === "gateway_not_enabled") {
337
- yield {
338
- type: "stop",
339
- reason: "error",
340
- error: message,
341
- errorCode: code,
342
- };
404
+ yield stop({ error: message, errorCode: code });
343
405
  return;
344
406
  }
345
407
  if (status === 401 || code === "unauthorized") {
346
- await recordBuilderCredentialAuthFailure({ status, code, message });
347
- yield {
348
- type: "stop",
349
- reason: "error",
408
+ await recordBuilderGatewayAuthFailure({ status, code, message });
409
+ yield stop({
350
410
  error: "Builder authentication failed. Reconnect Builder (free tier available) via Settings.",
351
411
  errorCode: "builder_auth_error",
352
- };
412
+ });
353
413
  return;
354
414
  }
355
415
  if (status === 403 && isBuilderCredentialAuthError(message)) {
356
- await recordBuilderCredentialAuthFailure({ status, code, message });
357
- yield {
358
- type: "stop",
359
- reason: "error",
416
+ await recordBuilderGatewayAuthFailure({ status, code, message });
417
+ yield stop({
360
418
  error: "Builder authentication failed. Reconnect Builder (free tier available) via Settings.",
361
419
  errorCode: "builder_auth_error",
362
- };
420
+ });
363
421
  return;
364
422
  }
365
423
  if (status === 403) {
366
- yield {
367
- type: "stop",
368
- reason: "error",
369
- error: message,
370
- errorCode: code,
371
- };
424
+ yield stop({ error: message, errorCode: code });
372
425
  return;
373
426
  }
374
427
  if (code === "rate_limit_exceeded") {
375
- yield {
376
- type: "stop",
377
- reason: "error",
378
- error: message,
379
- errorCode: code,
380
- };
428
+ // The daily cap shares 429 with the transient throttle below and must not
429
+ // loop, so it carries NEITHER retry field: a bare `statusCode: 429` reads as
430
+ // retryable on its own.
431
+ yield stop({ error: message, errorCode: code });
381
432
  return;
382
433
  }
383
434
  if (status === 429 || code === "too_many_concurrent_requests") {
384
- // Include "too many requests" in the message so production-agent's
385
- // isRetryableError picks up transient concurrency throttles and retries
386
- // the turn. Daily gateway caps use `rate_limit_exceeded` above and must
387
- // not loop.
388
- yield {
389
- type: "stop",
390
- reason: "error",
391
- error: `${message} (too many requests)`,
435
+ // Daily gateway caps use `rate_limit_exceeded` above and must not loop;
436
+ // this branch is the transient concurrency throttle, which does.
437
+ yield stop({
438
+ error: message,
392
439
  errorCode: code,
393
- };
440
+ statusCode: status,
441
+ providerRetryable: true,
442
+ });
394
443
  return;
395
444
  }
396
- yield {
397
- type: "stop",
398
- reason: "error",
445
+ yield stop({
399
446
  error: message,
400
447
  errorCode: code,
401
- };
448
+ statusCode: status,
449
+ ...(isTransientGatewayFailure(message, status)
450
+ ? { providerRetryable: true }
451
+ : {}),
452
+ });
402
453
  }
403
454
  // Yields one non-empty JSONL line at a time. Flushes any trailing content
404
455
  // after the stream ends so a final event without a newline terminator
@@ -482,12 +533,12 @@ async function* parseJsonlStream(reader, model, captureContext = {}) {
482
533
  }
483
534
  catch {
484
535
  const normalized = normalizeGatewayErrorText(line, 502);
485
- yield {
486
- type: "stop",
487
- reason: "error",
536
+ yield gatewayErrorStop({
488
537
  error: `Builder gateway returned invalid JSONL: ${normalized.slice(0, 240)}`,
489
538
  errorCode: "http_502",
490
- };
539
+ statusCode: 502,
540
+ providerRetryable: true,
541
+ }, captureContext.creditsLane);
491
542
  return;
492
543
  }
493
544
  // Heartbeats are transport-level keepalives, not proof the model is
@@ -568,15 +619,13 @@ async function* parseJsonlStream(reader, model, captureContext = {}) {
568
619
  yield* recoverUndeliveredToolCalls();
569
620
  yield { type: "assistant-content", parts };
570
621
  const reason = event.reason ?? "end_turn";
622
+ const stop = (details) => gatewayErrorStop(details, captureContext.creditsLane);
571
623
  if (reason === "rate_limited") {
572
- // Include "rate_limit" in the message so production-agent's
573
- // isRetryableError picks it up and retries.
574
- yield {
575
- type: "stop",
576
- reason: "error",
624
+ yield stop({
577
625
  error: `rate_limit exceeded: ${event.error ?? "upstream provider rate limited"}`,
578
626
  errorCode: "rate_limited",
579
- };
627
+ providerRetryable: true,
628
+ });
580
629
  }
581
630
  else if (reason === "invalid_request") {
582
631
  // errorCode has no retry-trigger keywords, so isRetryableError
@@ -590,12 +639,7 @@ async function* parseJsonlStream(reader, model, captureContext = {}) {
590
639
  ? event.code
591
640
  : "invalid_request";
592
641
  console.warn(`[builder-engine] stop reason=invalid_request model=${model} code=${errCode} error=${errMsg}`);
593
- yield {
594
- type: "stop",
595
- reason: "error",
596
- error: errMsg,
597
- errorCode: errCode,
598
- };
642
+ yield stop({ error: errMsg, errorCode: errCode });
599
643
  }
600
644
  else if (reason === "error") {
601
645
  // Surface every diagnostic the gateway gave us so the user (and
@@ -628,10 +672,16 @@ async function* parseJsonlStream(reader, model, captureContext = {}) {
628
672
  : isProviderConnectionError
629
673
  ? BUILDER_GATEWAY_NETWORK_ERROR_CODE
630
674
  : (gatewayErrCode ??
631
- (!explicitErrMsg ? "builder_gateway_error" : undefined));
675
+ (!explicitErrMsg
676
+ ? "builder_gateway_error"
677
+ : // A detailed in-stream error the gateway left uncoded:
678
+ // classify the RAW sentence here, because run persistence
679
+ // would otherwise do it downstream on the visitor line and
680
+ // record `unknown` on the credits lane alone.
681
+ classifyTerminalErrorCode(String(errMsg))));
632
682
  console.error(`[builder-engine] stop reason=error model=${model} code=${errCode ?? "(none)"} error=${errMsg}`);
633
683
  if (isCredentialAuthError) {
634
- await recordBuilderCredentialAuthFailure({
684
+ await recordBuilderGatewayAuthFailure({
635
685
  code: typeof gatewayErrCode === "string" ? gatewayErrCode : errCode,
636
686
  message: String(errMsg),
637
687
  });
@@ -652,12 +702,16 @@ async function* parseJsonlStream(reader, model, captureContext = {}) {
652
702
  rawEvent: event,
653
703
  });
654
704
  }
655
- yield {
656
- type: "stop",
657
- reason: "error",
658
- error: errMsg,
705
+ yield stop({
706
+ error: String(errMsg),
659
707
  ...(errCode ? { errorCode: errCode } : {}),
660
- };
708
+ // The upstream provider giving up ("Overloaded", a bare 529) is
709
+ // retryable, and the raw text is the only place it says so — a
710
+ // stop event carries no status.
711
+ ...(isTransientGatewayFailure(String(errMsg))
712
+ ? { providerRetryable: true }
713
+ : {}),
714
+ });
661
715
  }
662
716
  else if (reason === "end_turn" ||
663
717
  reason === "tool_use" ||
@@ -666,11 +720,7 @@ async function* parseJsonlStream(reader, model, captureContext = {}) {
666
720
  yield { type: "stop", reason };
667
721
  }
668
722
  else {
669
- yield {
670
- type: "stop",
671
- reason: "error",
672
- error: `Unknown stop reason: ${reason}`,
673
- };
723
+ yield stop({ error: `Unknown stop reason: ${reason}` });
674
724
  }
675
725
  return;
676
726
  }
@@ -683,11 +733,10 @@ async function* parseJsonlStream(reader, model, captureContext = {}) {
683
733
  flushPending();
684
734
  yield* recoverUndeliveredToolCalls();
685
735
  yield { type: "assistant-content", parts };
686
- yield {
687
- type: "stop",
688
- reason: "error",
736
+ yield gatewayErrorStop({
689
737
  error: "Builder gateway stream ended without a stop event",
690
- };
738
+ errorCode: BUILDER_GATEWAY_STREAM_ENDED_ERROR_CODE,
739
+ }, captureContext.creditsLane);
691
740
  }
692
741
  catch (err) {
693
742
  const timedOut = captureContext.didGatewayTimeout?.() ?? false;
@@ -705,7 +754,7 @@ async function* parseJsonlStream(reader, model, captureContext = {}) {
705
754
  : undefined,
706
755
  });
707
756
  }
708
- yield createBuilderGatewayTimeoutStop(err, timedOut, gatewayTimeoutMs);
757
+ yield createBuilderGatewayTimeoutStop(err, timedOut, gatewayTimeoutMs, captureContext.creditsLane);
709
758
  }
710
759
  finally {
711
760
  // Release the reader on every exit path — early returns (invalid JSONL,
@@ -914,18 +963,32 @@ function normalizeBuilderGatewayFetchError(err, timedOut, timeoutMs) {
914
963
  }
915
964
  return message;
916
965
  }
917
- function createBuilderGatewayTimeoutStop(err, timedOut, timeoutMs) {
918
- const networkError = !timedOut && isBuilderGatewayNetworkError(err);
919
- return {
920
- type: "stop",
921
- reason: "error",
922
- error: normalizeBuilderGatewayFetchError(err, timedOut, timeoutMs),
923
- ...(timedOut
924
- ? { errorCode: "builder_gateway_timeout" }
925
- : networkError
926
- ? { errorCode: BUILDER_GATEWAY_NETWORK_ERROR_CODE }
927
- : {}),
928
- };
966
+ /**
967
+ * Derived from the RAW error before `gatewayErrorStop` replaces the message: on
968
+ * the credits lane run-manager has no text left to classify at persistence time,
969
+ * and a run persisted as `unknown` reads as "do not attempt recovery".
970
+ */
971
+ function createBuilderGatewayTimeoutStop(err, timedOut, timeoutMs, creditsLane) {
972
+ const error = normalizeBuilderGatewayFetchError(err, timedOut, timeoutMs);
973
+ if (timedOut) {
974
+ // Deliberately no `providerRetryable`: the timeout spent the whole request
975
+ // budget, so the recovery is a fresh invocation (the client's
976
+ // `builder_gateway_timeout` continuation), never an in-call retry.
977
+ return gatewayErrorStop({ error, errorCode: "builder_gateway_timeout" }, creditsLane);
978
+ }
979
+ if (isBuilderGatewayNetworkError(err)) {
980
+ return gatewayErrorStop({
981
+ error,
982
+ errorCode: BUILDER_GATEWAY_NETWORK_ERROR_CODE,
983
+ providerRetryable: true,
984
+ }, creditsLane);
985
+ }
986
+ const errorCode = classifyTerminalErrorCode(error);
987
+ return gatewayErrorStop({
988
+ error,
989
+ ...(errorCode ? { errorCode } : {}),
990
+ ...(isTransientGatewayFailure(error) ? { providerRetryable: true } : {}),
991
+ }, creditsLane);
929
992
  }
930
993
  function formatTimeoutMs(timeoutMs) {
931
994
  if (timeoutMs < 1000)
@@ -18,10 +18,13 @@ export function registerBuiltinEngines() {
18
18
  return;
19
19
  _registered = true;
20
20
  // ── Builder.io managed gateway ─────────────────────────────────────────────
21
- // Registered first so detectEngineFromEnv picks it when both Builder keys
22
- // are set Builder is the managed path we want everyone on long-term.
23
- // Users who prefer BYO keys can opt out via AGENT_ENGINE_PREFER_BYO_KEY=true,
24
- // which drops Builder to the fallback slot in detectEngineFromEnv.
21
+ // Registered first, so it wins whenever a customer configured Builder — the
22
+ // legacy key pair included. Two credential shapes select it: a user's own
23
+ // connection via that pair, or the deployment's Builder-credits token plus
24
+ // space id, which is the only lane an anonymous visitor on a hosted site has.
25
+ // Only the injected Builder-credits set is marked deployInjected, so it alone
26
+ // steps aside for a provider key the customer set (see selectDetectedEngine);
27
+ // users who prefer BYO everywhere can still set AGENT_ENGINE_PREFER_BYO_KEY.
25
28
  registerAgentEngine({
26
29
  name: "builder",
27
30
  label: "Builder.io Gateway",
@@ -30,6 +33,12 @@ export function registerBuiltinEngines() {
30
33
  defaultModel: BUILDER_DEFAULT_MODEL,
31
34
  supportedModels: BUILDER_SUPPORTED_MODELS,
32
35
  requiredEnvVars: ["BUILDER_PRIVATE_KEY", "BUILDER_PUBLIC_KEY"],
36
+ alternateRequiredEnvVars: [
37
+ {
38
+ envVars: ["BUILDER_GATEWAY_TOKEN", "BUILDER_GATEWAY_SPACE_ID"],
39
+ deployInjected: true,
40
+ },
41
+ ],
33
42
  create: (config) => createBuilderEngine(config),
34
43
  });
35
44
  // ── Anthropic ──────────────────────────────────────────────────────────────