@agent-native/core 0.206.0 → 0.206.1-nightly-20261009183700

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 (66) hide show
  1. package/dist/agent/default-agent-engine.js +25 -0
  2. package/dist/agent/durable-background.d.ts +2 -0
  3. package/dist/agent/durable-background.js +2 -0
  4. package/dist/agent/engine/error-detail.d.ts +1 -0
  5. package/dist/agent/engine/error-detail.js +6 -1
  6. package/dist/agent/engine/translate-anthropic.d.ts +9 -0
  7. package/dist/agent/engine/translate-anthropic.js +18 -3
  8. package/dist/agent/production-agent.d.ts +4 -4
  9. package/dist/agent/production-agent.js +78 -74
  10. package/dist/agent/run-store.js +15 -3
  11. package/dist/agent/thread-data-builder.js +1 -2
  12. package/dist/automation/index.d.ts +1 -1
  13. package/dist/chat-threads/store.js +6 -2
  14. package/dist/client/chat/agentkit-agent-native.js +11 -10
  15. package/dist/client/chat/agentkit-protocol.js +5 -1
  16. package/dist/client/chat/runtime.d.ts +3 -1
  17. package/dist/client/chat/runtime.js +101 -43
  18. package/dist/client/use-action.d.ts +1 -0
  19. package/dist/client/use-action.js +2 -1
  20. package/dist/collab/routes.d.ts +1 -1
  21. package/dist/collab/struct-routes.d.ts +1 -1
  22. package/dist/deploy/build.js +1 -1
  23. package/dist/deploy/workspace-deploy.js +65 -6
  24. package/dist/file-upload/actions/upload-image.d.ts +3 -3
  25. package/dist/file-upload/registry.d.ts +7 -1
  26. package/dist/file-upload/registry.js +25 -7
  27. package/dist/mcp/build-server.js +64 -26
  28. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  29. package/dist/notifications/routes.d.ts +1 -1
  30. package/dist/observability/metrics.d.ts +5 -0
  31. package/dist/observability/metrics.js +62 -7
  32. package/dist/observability/routes.d.ts +4 -4
  33. package/dist/org/handlers.d.ts +1 -1
  34. package/dist/progress/routes.d.ts +1 -1
  35. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  36. package/dist/resources/handlers.d.ts +1 -1
  37. package/dist/secrets/index.d.ts +1 -1
  38. package/dist/secrets/index.js +1 -1
  39. package/dist/secrets/storage.d.ts +1 -0
  40. package/dist/secrets/storage.js +8 -0
  41. package/dist/server/action-routes.js +1 -1
  42. package/dist/server/agent-engine-api-key-route.d.ts +2 -1
  43. package/dist/server/agent-engine-api-key-route.js +66 -2
  44. package/dist/server/agent-engine-default-model-route.d.ts +2 -2
  45. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  46. package/dist/server/agent-run-context.js +21 -1
  47. package/dist/server/agent-teams-run-queue.js +2 -1
  48. package/dist/server/auth.js +1 -0
  49. package/dist/server/core-routes-plugin.d.ts +1 -1
  50. package/dist/server/core-routes-plugin.js +12 -0
  51. package/dist/server/http-response-telemetry.js +37 -8
  52. package/dist/server/realtime-token.d.ts +1 -1
  53. package/dist/shared/inline-bytes.d.ts +29 -0
  54. package/dist/shared/inline-bytes.js +170 -0
  55. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  56. package/dist/shared/mcp-embed-headers.js +1 -1
  57. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  58. package/dist/testing.d.ts +1 -0
  59. package/dist/testing.js +1 -0
  60. package/dist/tracking/registry.d.ts +3 -2
  61. package/dist/tracking/registry.js +12 -3
  62. package/dist/triggers/actions/manage-automation.d.ts +3 -3
  63. package/dist/vite/client.js +6 -1
  64. package/docs/content/observability.mdx +15 -10
  65. package/docs/content/tracking.mdx +1 -1
  66. package/package.json +3 -3
@@ -11,6 +11,7 @@ import { getRequestOrgId, getRequestUserEmail, } from "../server/request-context
11
11
  import { getOrgSetting, putOrgSetting } from "../settings/org-settings.js";
12
12
  import { getSetting } from "../settings/store.js";
13
13
  import { getUserSetting, putUserSetting } from "../settings/user-settings.js";
14
+ import { track } from "../tracking/registry.js";
14
15
  import { canUpdateAgentAppModelDefaultSettings } from "./app-model-defaults.js";
15
16
  export const DEFAULT_AGENT_ENGINE_SETTING_KEY = "agent-engine";
16
17
  /** Audit target type for default-model changes and refused attempts. */
@@ -98,6 +99,7 @@ async function writeScopedRow(authority, value) {
98
99
  * stores only that scope and records the change.
99
100
  */
100
101
  export async function writeDefaultAgentEngineSelection(authority, selection, meta) {
102
+ const previous = await readDefaultAgentEngineSettingDetailed(authority);
101
103
  await writeScopedRow(authority, {
102
104
  engine: selection.engine,
103
105
  model: selection.model,
@@ -112,12 +114,25 @@ export async function writeDefaultAgentEngineSelection(authority, selection, met
112
114
  operation: "set",
113
115
  selection,
114
116
  });
117
+ track("llm_default_model_changed", {
118
+ engine: selection.engine,
119
+ model: selection.model,
120
+ ...(typeof previous.value?.engine === "string"
121
+ ? { previous_engine: previous.value.engine }
122
+ : {}),
123
+ ...(typeof previous.value?.model === "string"
124
+ ? { previous_model: previous.value.model }
125
+ : {}),
126
+ scope: authority.scope,
127
+ operation: "set",
128
+ }, { userId: authority.userEmail });
115
129
  }
116
130
  /**
117
131
  * Clear the default for the authority's scope. Writes a cleared marker rather
118
132
  * than deleting the row; see {@link readDefaultAgentEngineSettingDetailed}.
119
133
  */
120
134
  export async function clearDefaultAgentEngineSelection(authority, meta) {
135
+ const previous = await readDefaultAgentEngineSettingDetailed(authority);
121
136
  await writeScopedRow(authority, {
122
137
  cleared: true,
123
138
  updatedAt: Date.now(),
@@ -130,6 +145,16 @@ export async function clearDefaultAgentEngineSelection(authority, meta) {
130
145
  status: "success",
131
146
  operation: "clear",
132
147
  });
148
+ track("llm_default_model_changed", {
149
+ ...(typeof previous.value?.engine === "string"
150
+ ? { previous_engine: previous.value.engine }
151
+ : {}),
152
+ ...(typeof previous.value?.model === "string"
153
+ ? { previous_model: previous.value.model }
154
+ : {}),
155
+ scope: authority.scope,
156
+ operation: "reset",
157
+ }, { userId: authority.userEmail });
133
158
  }
134
159
  /** Record a refused change so owners and admins can see who tried. */
135
160
  export async function recordDefaultAgentEngineRefusal(ctx, authority, operation, meta, selection) {
@@ -51,6 +51,8 @@ export declare const AGENT_CHAT_DURABLE_BACKGROUND_ENV = "AGENT_CHAT_DURABLE_BAC
51
51
  * verifies the HMAC token before invoking the handler.
52
52
  */
53
53
  export declare const AGENT_CHAT_BACKGROUND_RUN_FIELD = "__backgroundRun";
54
+ /** Analytics-only source context captured before the run leaves its browser request. */
55
+ export declare const AGENT_CHAT_BROWSER_SESSION_ID_FIELD = "__agentNativeBrowserSessionId";
54
56
  export declare function isHostedRuntimeForDurableBackground(): boolean;
55
57
  export declare function isNetlifyHostedRuntimeForDurableBackground(): boolean;
56
58
  export declare function isInBackgroundFunctionRuntime(): boolean;
@@ -131,6 +131,8 @@ export const AGENT_CHAT_DURABLE_BACKGROUND_ENV = "AGENT_CHAT_DURABLE_BACKGROUND"
131
131
  * verifies the HMAC token before invoking the handler.
132
132
  */
133
133
  export const AGENT_CHAT_BACKGROUND_RUN_FIELD = "__backgroundRun";
134
+ /** Analytics-only source context captured before the run leaves its browser request. */
135
+ export const AGENT_CHAT_BROWSER_SESSION_ID_FIELD = "__agentNativeBrowserSessionId";
134
136
  export function isHostedRuntimeForDurableBackground() {
135
137
  if (process.env.NETLIFY_LOCAL === "true")
136
138
  return false;
@@ -16,6 +16,7 @@ export interface ProviderErrorClassification {
16
16
  providerRetryable?: boolean;
17
17
  retryAfterMs?: number;
18
18
  }
19
+ export declare function isInvalidAttachmentProviderMessage(message: string): boolean;
19
20
  export declare function extractRetryAfterMs(err: unknown): number | undefined;
20
21
  export declare function classifyProviderError(err: unknown, timedOut?: boolean): ProviderErrorClassification;
21
22
  /**
@@ -90,7 +90,12 @@ export function isContextOverflowCode(code) {
90
90
  const MAX_RETRY_AFTER_MS = 60_000;
91
91
  const PROVIDER_ATTACHMENT_FIELD_PATTERN = /\b(?:images?|files?|attachments?|media)[_-](?:url|data|source|type|format|size|input|content)\b|\b(?:images?|files?)\s+(?:url|data|source|type|format|size|content)\b|\b(?:url|data|source|type|format|size|input|content)\b.{0,40}\b(?:images?|files?|attachments?|media)\b|\bmime[\s_-]?type\b/gi;
92
92
  const PROVIDER_ATTACHMENT_REJECTION_PATTERN = /\b(?:invalid|unsupported|not supported|does not support|too large|too big|too long|exceeds?|over(?:sized|size| the)? limit|maximum (?:allowed )?(?:size|length)|(?:size|length) limit|malformed)\b/i;
93
- function isInvalidAttachmentProviderMessage(message) {
93
+ // Phrasings Anthropic, OpenAI, Google, and the Builder gateway use for an
94
+ // image they refused. Specific enough to classify without a status code.
95
+ const PROVIDER_IMAGE_REJECTION_PATTERN = /\bimage exceeds \d+(?:\.\d+)?\s*mb\b|\b(?:could not|unable to) process (?:the )?(?:input )?image\b|\binvalid image\b|\bunsupported image\b|\bimage (?:is )?too (?:large|big)\b|\bprovided image is not valid\b/i;
96
+ export function isInvalidAttachmentProviderMessage(message) {
97
+ if (PROVIDER_IMAGE_REJECTION_PATTERN.test(message))
98
+ return true;
94
99
  for (const match of message.matchAll(PROVIDER_ATTACHMENT_FIELD_PATTERN)) {
95
100
  const index = match.index ?? 0;
96
101
  const start = Math.max(0, index - 100);
@@ -10,6 +10,15 @@ export declare function unmatchedToolResultReplayText(part: {
10
10
  content: unknown;
11
11
  isError?: boolean;
12
12
  }): string;
13
+ /**
14
+ * Identity of a synthetic interrupted tool result in engine messages, thread
15
+ * data, and the client (`client/sse-event-processor.ts` matches it verbatim).
16
+ * The model never sees it bare: replay rewrites it to an error whose outcome
17
+ * is unknown, or the model treats it as "did not run" and repeats a write that
18
+ * may have landed.
19
+ */
20
+ export declare const INTERRUPTED_TOOL_RESULT_MARKER = "Interrupted before this tool returned a result.";
21
+ export declare function isInterruptedToolResult(content: unknown): boolean;
13
22
  export declare function backfillEngineMessagesToolResults(messages: EngineMessage[]): EngineMessage[];
14
23
  export declare function engineMessageToAnthropic(msg: EngineMessage, opts?: {
15
24
  builderGateway?: boolean;
@@ -74,13 +74,27 @@ export function unmatchedToolResultReplayText(part) {
74
74
  const err = part.isError ? " isError=true" : "";
75
75
  return `${UNMATCHED_TOOL_RESULT_REPLAY_PREFIX} [tool_use_id=${part.toolCallId}${err}] ${body}`;
76
76
  }
77
+ /**
78
+ * Identity of a synthetic interrupted tool result in engine messages, thread
79
+ * data, and the client (`client/sse-event-processor.ts` matches it verbatim).
80
+ * The model never sees it bare: replay rewrites it to an error whose outcome
81
+ * is unknown, or the model treats it as "did not run" and repeats a write that
82
+ * may have landed.
83
+ */
84
+ export const INTERRUPTED_TOOL_RESULT_MARKER = "Interrupted before this tool returned a result.";
85
+ const INTERRUPTED_TOOL_RESULT_FOR_MODEL = `${INTERRUPTED_TOOL_RESULT_MARKER} Its outcome is UNKNOWN: the tool may have run. Verify the current state before repeating any write.`;
86
+ export function isInterruptedToolResult(content) {
87
+ return (typeof content === "string" &&
88
+ content.startsWith(INTERRUPTED_TOOL_RESULT_MARKER));
89
+ }
77
90
  function interruptedToolResultPart(part) {
78
91
  return {
79
92
  type: "tool-result",
80
93
  toolCallId: part.id,
81
94
  toolName: part.name,
82
95
  toolInput: stringifyToolUseInputForGateway(part.input),
83
- content: "Interrupted before this tool returned a result.",
96
+ content: INTERRUPTED_TOOL_RESULT_FOR_MODEL,
97
+ isError: true,
84
98
  };
85
99
  }
86
100
  export function backfillEngineMessagesToolResults(messages) {
@@ -167,13 +181,14 @@ export function backfillEngineMessagesToolResults(messages) {
167
181
  const toolInput = typeof part.toolInput === "string" && part.toolInput.length > 0
168
182
  ? part.toolInput
169
183
  : stringifyToolUseInputForGateway(pendingLookup?.input ?? lookup?.input);
184
+ const interrupted = part.content === INTERRUPTED_TOOL_RESULT_MARKER;
170
185
  const filled = {
171
186
  type: "tool-result",
172
187
  toolCallId: part.toolCallId,
173
188
  toolName,
174
189
  toolInput,
175
- content: part.content,
176
- ...(part.isError ? { isError: true } : {}),
190
+ content: interrupted ? INTERRUPTED_TOOL_RESULT_FOR_MODEL : part.content,
191
+ ...(part.isError || interrupted ? { isError: true } : {}),
177
192
  ...(part.images && part.images.length > 0
178
193
  ? { images: part.images }
179
194
  : {}),
@@ -10,6 +10,7 @@ import { type BuilderGatewayAuth } from "../server/credential-provider.js";
10
10
  import { fireInternalDispatch } from "../server/self-dispatch.js";
11
11
  import { type RefusedTurnRetryContext } from "../shared/agent-chat-run-not-started.js";
12
12
  import { type ContextStatus } from "../shared/context-status.js";
13
+ import { DurableAttachmentReferenceRequiredError } from "../shared/inline-bytes.js";
13
14
  import { type ReasoningEffort } from "../shared/reasoning-effort.js";
14
15
  import type { TrackingMeta } from "../tracking/registry.js";
15
16
  import { type ResolveEngineConfig } from "./engine/index.js";
@@ -357,10 +358,7 @@ export declare function runCompletionCallbackWithDatabaseRetry(callback: () => v
357
358
  }): Promise<void>;
358
359
  export declare function resolveSourceSweepToolCallThreshold(): number;
359
360
  declare function generateRunId(): string;
360
- export declare class DurableAttachmentReferenceRequiredError extends Error {
361
- readonly code = "attachment_storage_required";
362
- constructor();
363
- }
361
+ export { DurableAttachmentReferenceRequiredError };
364
362
  export declare function serializeDurableDispatchPayload(body: Record<string, unknown>): string;
365
363
  /** Check if an error is transient and should be retried
366
364
  * @internal exported for unit tests only
@@ -778,6 +776,8 @@ export declare function resolvePriorContinuationState(backgroundRunMarker: Recor
778
776
  };
779
777
  export declare function resolvePriorContinuationReason(backgroundRunMarker: Record<string, unknown> | null | undefined, body: Record<string, unknown>): string | undefined;
780
778
  declare function readTurnStartedAt(threadId: string, turnId: string): Promise<number | null>;
779
+ /** @internal exported for unit tests only */
780
+ export declare function queuedPromotionAttachments(attachments: unknown[]): AgentChatAttachment[];
781
781
  declare function emitRunText(run: ActiveRun, text: string): Promise<void>;
782
782
  export interface ChainServerDrivenContinuationDeps {
783
783
  countRunsForTurn?: typeof countRunsForTurn;
@@ -45,6 +45,7 @@ import { ANALYTICS_CLIENT_PLATFORM_BODY_FIELD } from "../shared/analytics-platfo
45
45
  import { isContextDegraded, preloadedContextUnavailableNote, readReportedContextStatus, screenContextUnavailableNote, SELECTION_CONTEXT_UNAVAILABLE_NOTE, worstContextStatus, } from "../shared/context-status.js";
46
46
  import { parseBase64DataUrl } from "../shared/data-url.js";
47
47
  import { stripDiagnosticSnippets } from "../shared/diagnostic-snippet.js";
48
+ import { DurableAttachmentReferenceRequiredError, stripInlineBytes, } from "../shared/inline-bytes.js";
48
49
  import { isReasoningEffort, normalizeReasoningEffortForRequest, stepDownReasoningEffort, } from "../shared/reasoning-effort.js";
49
50
  import { SYNTHETIC_TRAFFIC_BETA_E2E, SYNTHETIC_TRAFFIC_HEADER, } from "../shared/test-traffic.js";
50
51
  import { actionPreparationContinuationNote } from "./action-continuation-guidance.js";
@@ -53,17 +54,17 @@ import { CONTINUE_UNAVAILABLE_CODE, } from "./auto-continue.js";
53
54
  import { clipHead } from "./clip-text.js";
54
55
  import { connectionRequiredMessage, priorConnectionContextNote, resolvePriorConnectionNote, } from "./connection-required-note.js";
55
56
  import { buildSystemManifestSections, readContextXraySystemSections, } from "./context-xray/manifest.js";
56
- import { AGENT_CHAT_BACKGROUND_RUN_FIELD, AGENT_CHAT_PROCESS_RUN_PATH, backgroundRuntimeDiagnosticDetail, dispatchPathTargetsNetlifyBackgroundFunction, isAgentChatDurableBackgroundEnabled, isAgentChatForegroundSelfChainEnabled, isInBackgroundFunctionRuntime, resolveAgentChatProcessRunDispatchPath, shouldUseBackgroundFunctionTimeoutForWorker, } from "./durable-background.js";
57
+ import { AGENT_CHAT_BROWSER_SESSION_ID_FIELD, AGENT_CHAT_BACKGROUND_RUN_FIELD, AGENT_CHAT_PROCESS_RUN_PATH, backgroundRuntimeDiagnosticDetail, dispatchPathTargetsNetlifyBackgroundFunction, isAgentChatDurableBackgroundEnabled, isAgentChatForegroundSelfChainEnabled, isInBackgroundFunctionRuntime, resolveAgentChatProcessRunDispatchPath, shouldUseBackgroundFunctionTimeoutForWorker, } from "./durable-background.js";
57
58
  import { applyContextXrayTransformForIteration } from "./engine/context-directives-transform.js";
58
59
  import { attemptContinuationDispatch } from "./engine/continuation-dispatch-retry.js";
59
60
  import { formatLlmCredentialErrorMessage, LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, userFacingLlmCredentialError, } from "./engine/credential-errors.js";
60
- import { BUILDER_GATEWAY_INTERNAL_ERROR_CODE, isContextOverflowCode, isContextOverflowMessage, isProviderConnectionErrorMessage, PROVIDER_RATE_LIMITED_ERROR_CODE, PROVIDER_TRANSIENT_REJECTION_ERROR_CODE, } from "./engine/error-detail.js";
61
+ import { BUILDER_GATEWAY_INTERNAL_ERROR_CODE, isContextOverflowCode, isContextOverflowMessage, isInvalidAttachmentProviderMessage, isProviderConnectionErrorMessage, PROVIDER_RATE_LIMITED_ERROR_CODE, PROVIDER_TRANSIENT_REJECTION_ERROR_CODE, } from "./engine/error-detail.js";
61
62
  import { resolveEngine, explicitEngineName, registerBuiltinEngines, getStoredModelForEngine, normalizeModelForEngine, isResolvedEngineUsableForRequest, } from "./engine/index.js";
62
63
  import { limitProviderTools, MAX_PROVIDER_TOOLS, } from "./engine/limit-provider-tools.js";
63
64
  import { resolveEmptyResponseRetryMaxOutputTokens, resolveMainChatMaxOutputTokens, resolveMaxOutputTokensForEngine, } from "./engine/output-tokens.js";
64
65
  import { PROVIDER_TO_ENV } from "./engine/provider-env-vars.js";
65
66
  import { JOURNALED_TOOL_REPLAY_PREFIX, RECOVERED_TOOL_REPLAY_PREFIX, loadedSkillPagesContext, loadPriorTurnToolCallJournal, seedRepeatedToolErrorCountsFromJournal, seedRepeatedToolCallCountsFromJournal, } from "./engine/tool-call-journal-seed.js";
66
- import { backfillEngineMessagesToolResults, stringifyToolUseInputForGateway, unmatchedToolResultReplayText, } from "./engine/translate-anthropic.js";
67
+ import { backfillEngineMessagesToolResults, INTERRUPTED_TOOL_RESULT_MARKER, isInterruptedToolResult, stringifyToolUseInputForGateway, unmatchedToolResultReplayText, } from "./engine/translate-anthropic.js";
67
68
  import { EngineError } from "./engine/types.js";
68
69
  import { FOLLOW_UP_SUGGESTIONS_COMPLETION_SYSTEM_PROMPT, FOLLOW_UP_SUGGESTIONS_INSTRUCTION, FOLLOW_UP_SUGGESTIONS_MAX_OUTPUT_TOKENS, FOLLOW_UP_SUGGESTIONS_TOOL_NAME, buildFollowUpCompletionMessages, followUpSuggestionsTool, identifyFollowUpSuggestions, parseFollowUpSuggestions, } from "./follow-up-suggestions.js";
69
70
  import { filterHostedHarnessToolNames, normalizeHostedHarnessRuntime, } from "./harness/hosted.js";
@@ -1139,59 +1140,9 @@ function limitInventoryLines(lines, label) {
1139
1140
  function generateRunId() {
1140
1141
  return `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1141
1142
  }
1142
- export class DurableAttachmentReferenceRequiredError extends Error {
1143
- code = "attachment_storage_required";
1144
- constructor() {
1145
- super("An attachment has inline bytes but no durable file URL. Configure file storage and retry this background run.");
1146
- this.name = "DurableAttachmentReferenceRequiredError";
1147
- }
1148
- }
1149
- function isDataUrlReference(value) {
1150
- return typeof value === "string" && /^\s*data:/i.test(value);
1151
- }
1152
- function hasDurableAttachmentUrl(value) {
1153
- return (typeof value === "string" &&
1154
- value.trim().length > 0 &&
1155
- !isDataUrlReference(value));
1156
- }
1157
- function sanitizeDurableAttachment(value, attachmentContext = false) {
1158
- if (Array.isArray(value)) {
1159
- return value.map((item) => sanitizeDurableAttachment(item));
1160
- }
1161
- if (!value || typeof value !== "object")
1162
- return value;
1163
- const item = value;
1164
- const isAttachment = attachmentContext ||
1165
- item.type === "image" ||
1166
- item.type === "file" ||
1167
- item.type === "document";
1168
- if (isAttachment &&
1169
- (isDataUrlReference(item.url) || isDataUrlReference(item.referenceUrl))) {
1170
- throw new DurableAttachmentReferenceRequiredError();
1171
- }
1172
- const hasInlineData = isAttachment && item.data !== undefined;
1173
- if (hasInlineData &&
1174
- !hasDurableAttachmentUrl(item.url) &&
1175
- !hasDurableAttachmentUrl(item.referenceUrl)) {
1176
- throw new DurableAttachmentReferenceRequiredError();
1177
- }
1178
- const sanitized = {};
1179
- for (const [key, child] of Object.entries(item)) {
1180
- if (key === "data" && hasInlineData)
1181
- continue;
1182
- if (key === "attachments" || key === "requestAttachments") {
1183
- sanitized[key] = Array.isArray(child)
1184
- ? child.map((attachment) => sanitizeDurableAttachment(attachment, true))
1185
- : sanitizeDurableAttachment(child);
1186
- }
1187
- else {
1188
- sanitized[key] = sanitizeDurableAttachment(child);
1189
- }
1190
- }
1191
- return sanitized;
1192
- }
1143
+ export { DurableAttachmentReferenceRequiredError };
1193
1144
  export function serializeDurableDispatchPayload(body) {
1194
- const payload = JSON.stringify(sanitizeDurableAttachment(body));
1145
+ const payload = JSON.stringify(stripInlineBytes(body, "reject"));
1195
1146
  if (typeof payload !== "string") {
1196
1147
  throw new TypeError("Durable dispatch payload could not be serialized");
1197
1148
  }
@@ -1550,6 +1501,51 @@ export function buildUserContentWithAttachments(opts) {
1550
1501
  });
1551
1502
  return userContent;
1552
1503
  }
1504
+ /** Images reach only a model that reads them; the model learns one was withheld. */
1505
+ function replaceImagesForModelWithoutVision(messages, model) {
1506
+ const note = `<chat-attachment-processing-error code="model-without-vision" model="${escapeAttachmentAttribute(model)}">An attached image was not sent because the active model cannot read images. Do not infer or describe its contents. Tell the user to switch to a vision-capable model if the image matters.</chat-attachment-processing-error>`;
1507
+ return messages.map((message) => message.content.some((part) => part.type === "image")
1508
+ ? {
1509
+ ...message,
1510
+ content: message.content.map((part) => part.type === "image"
1511
+ ? { type: "text", text: note }
1512
+ : part),
1513
+ }
1514
+ : message);
1515
+ }
1516
+ /**
1517
+ * Every engine's stop error passes through here with the request in hand, so
1518
+ * this is where a provider's rejection of an inline attachment becomes the
1519
+ * non-retryable `invalid_attachment`: resending the same bytes fails the same
1520
+ * way, and an `http_400` lets Retry loop on it.
1521
+ */
1522
+ function engineStopError(event, messages) {
1523
+ const error = event.error ?? "Engine stream error";
1524
+ const parts = messages.flatMap((message) => message.content);
1525
+ const hasInlineAttachment = parts.some((part) => part.type === "image" ||
1526
+ part.type === "file" ||
1527
+ (part.type === "tool-result" && (part.images?.length ?? 0) > 0));
1528
+ const attachmentRejected = event.errorCode === "invalid_attachment" ||
1529
+ (hasInlineAttachment &&
1530
+ (event.statusCode === 413 ||
1531
+ ((event.statusCode === undefined ||
1532
+ event.statusCode === 400 ||
1533
+ event.statusCode === 422) &&
1534
+ isInvalidAttachmentProviderMessage(error))));
1535
+ const names = parts.flatMap((part) => part.type === "file" && part.filename ? [`"${part.filename}"`] : []);
1536
+ return new EngineError(attachmentRejected
1537
+ ? `The model provider rejected an attachment in this request${names.length > 0 ? ` (${names.join(", ")})` : ""}: ${error}`
1538
+ : error, {
1539
+ errorCode: attachmentRejected ? "invalid_attachment" : event.errorCode,
1540
+ upgradeUrl: event.upgradeUrl,
1541
+ statusCode: event.statusCode,
1542
+ providerRetryable: attachmentRejected ? false : event.providerRetryable,
1543
+ contextOverflow: event.contextOverflow,
1544
+ requestId: event.requestId,
1545
+ requestShape: event.requestShape,
1546
+ retryAfterMs: event.retryAfterMs,
1547
+ });
1548
+ }
1553
1549
  export function appendRequestAttachmentContextToResumedHistory(messages, attachments) {
1554
1550
  if (!attachments?.length)
1555
1551
  return;
@@ -2448,7 +2444,6 @@ export function isCachedToolResultVisibleInContext(contextMessages, toolCall, ca
2448
2444
  }
2449
2445
  return false;
2450
2446
  }
2451
- const INTERRUPTED_TOOL_RESULT_MARKER = "Interrupted before this tool returned a result.";
2452
2447
  const INTERRUPTED_TOOL_LEDGER_RECOVERY_TIMEOUT_MS = 5_000;
2453
2448
  const MAX_IDENTICAL_TOOL_ERRORS = 3;
2454
2449
  export const MAX_SAME_ERROR_ACROSS_ARGUMENTS = 3;
@@ -2535,9 +2530,10 @@ function seedWriteToolInterruptionsFromHistory(messages, actions) {
2535
2530
  const call = pendingToolCalls.get(part.toolCallId);
2536
2531
  if (!call)
2537
2532
  continue;
2538
- if (typeof part.content === "string" &&
2539
- (part.content === INTERRUPTED_TOOL_RESULT_MARKER ||
2540
- (part.isError === true && isToolCallTimeoutResult(part.content)))) {
2533
+ if (isInterruptedToolResult(part.content) ||
2534
+ (part.isError === true &&
2535
+ typeof part.content === "string" &&
2536
+ isToolCallTimeoutResult(part.content))) {
2541
2537
  const key = toolCallCacheKey(call.name, call.input);
2542
2538
  interruptions.set(key, (interruptions.get(key) ?? 0) + 1);
2543
2539
  }
@@ -3930,12 +3926,15 @@ export async function runAgentLoop(opts) {
3930
3926
  const { thinking: _thinking, ...anthropic } = providerOptions.anthropic;
3931
3927
  providerOptions = { ...providerOptions, anthropic };
3932
3928
  }
3929
+ const engineMessages = engine.capabilities.vision === false
3930
+ ? replaceImagesForModelWithoutVision(contextMessages, model)
3931
+ : contextMessages;
3933
3932
  const streamOpts = {
3934
3933
  model,
3935
3934
  systemPrompt: completingFollowUpSuggestions
3936
3935
  ? FOLLOW_UP_SUGGESTIONS_COMPLETION_SYSTEM_PROMPT
3937
3936
  : continuationSystemPrompt,
3938
- messages: contextMessages,
3937
+ messages: engineMessages,
3939
3938
  tools: loopBreakerCloseout
3940
3939
  ? []
3941
3940
  : completingFollowUpSuggestions
@@ -3954,7 +3953,7 @@ export async function runAgentLoop(opts) {
3954
3953
  providerOptions,
3955
3954
  };
3956
3955
  usage.llmCalls = (usage.llmCalls ?? 0) + 1;
3957
- notifyModelInputObserver(opts.onModelInput, contextMessages);
3956
+ notifyModelInputObserver(opts.onModelInput, engineMessages);
3958
3957
  const eventStream = engine.stream(streamOpts);
3959
3958
  let thinkingBuffer = "";
3960
3959
  const toolInputNames = new Map();
@@ -4224,16 +4223,7 @@ export async function runAgentLoop(opts) {
4224
4223
  else if (event.type === "stop") {
4225
4224
  terminalStopReason = event.reason;
4226
4225
  if (event.reason === "error") {
4227
- throw new EngineError(event.error ?? "Engine stream error", {
4228
- errorCode: event.errorCode,
4229
- upgradeUrl: event.upgradeUrl,
4230
- statusCode: event.statusCode,
4231
- providerRetryable: event.providerRetryable,
4232
- contextOverflow: event.contextOverflow,
4233
- requestId: event.requestId,
4234
- requestShape: event.requestShape,
4235
- retryAfterMs: event.retryAfterMs,
4236
- });
4226
+ throw engineStopError(event, engineMessages);
4237
4227
  }
4238
4228
  }
4239
4229
  if (hasNoProgressStalled()) {
@@ -6500,11 +6490,20 @@ async function readAdmittedQueuedMessagePromotion(opts) {
6500
6490
  ...(options ? { options: options } : {}),
6501
6491
  };
6502
6492
  }
6503
- function queuedPromotionAttachments(attachments) {
6493
+ /** @internal exported for unit tests only */
6494
+ export function queuedPromotionAttachments(attachments) {
6504
6495
  return attachments.map((value) => {
6505
6496
  const file = queuedPromotionRecord(value);
6497
+ // A saved queue entry is always a file part; the media type decides whether
6498
+ // the model sees pixels (image) or a file reference.
6499
+ const isImage = typeof file.mediaType === "string" &&
6500
+ file.mediaType
6501
+ .split(";", 1)[0]
6502
+ .trim()
6503
+ .toLowerCase()
6504
+ .startsWith("image/");
6506
6505
  return {
6507
- type: "file",
6506
+ type: isImage ? "image" : "file",
6508
6507
  name: file.name,
6509
6508
  ...(typeof file.fileId === "string" ? { id: file.fileId } : {}),
6510
6509
  ...(typeof file.mediaType === "string"
@@ -7063,6 +7062,9 @@ export function createProductionAgentHandler(options) {
7063
7062
  });
7064
7063
  const mutableBody = body;
7065
7064
  if (!isBackgroundWorker) {
7065
+ // A client can submit this private field, so overwrite it at the boundary.
7066
+ mutableBody[AGENT_CHAT_BROWSER_SESSION_ID_FIELD] =
7067
+ getRequestContext()?.browserSessionId ?? null;
7066
7068
  delete mutableBody[ANALYTICS_CLIENT_PLATFORM_BODY_FIELD];
7067
7069
  }
7068
7070
  if (dispatchToBackground) {
@@ -7531,7 +7533,9 @@ export function createProductionAgentHandler(options) {
7531
7533
  const turnUsageLabel = normalizeUsageLabel(body.usageLabel);
7532
7534
  let experimentAssignments = [];
7533
7535
  try {
7534
- if (ownerEmail) {
7536
+ // A pinned request model suppresses the experiment's model override, so
7537
+ // resolving would assign (and persist) a variant this turn never ran.
7538
+ if (ownerEmail && !requestModelIsExplicit) {
7535
7539
  const { resolveActiveExperimentConfig } = await import("../observability/experiments.js");
7536
7540
  const expConfig = await resolveActiveExperimentConfig(ownerEmail);
7537
7541
  if (expConfig) {
@@ -5,6 +5,7 @@ import { ensureColumnExists, ensureTableExists } from "../db/ddl-guard.js";
5
5
  import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
6
6
  import { lockAndAssertServicePrincipalMayStartRun, prepareServicePrincipalRunStart, recordServicePrincipalDenial, } from "../org/service-principal-guard.js";
7
7
  import { captureError } from "../server/capture-error.js";
8
+ import { stripInlineBytes, stripInlineBytesFromJson, } from "../shared/inline-bytes.js";
8
9
  import { isRequestedStopAbortReason } from "./abort-reasons.js";
9
10
  import { admitAutoContinue, admitManualContinue, } from "./auto-continue.js";
10
11
  import { LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, } from "./engine/credential-errors.js";
@@ -1335,7 +1336,7 @@ function staleRecoveryDispatchPayload(payload) {
1335
1336
  return payload;
1336
1337
  }
1337
1338
  return JSON.stringify({
1338
- ...parsed,
1339
+ ...stripInlineBytes(parsed, "placeholder"),
1339
1340
  internalContinuation: true,
1340
1341
  });
1341
1342
  }
@@ -1823,7 +1824,13 @@ export async function insertRunEvent(runId, seq, eventData) {
1823
1824
  WHERE id = ? AND status <> 'running'
1824
1825
  )
1825
1826
  ON CONFLICT (run_id, seq) DO NOTHING`,
1826
- args: [runId, seq, Date.now(), eventData, runId],
1827
+ args: [
1828
+ runId,
1829
+ seq,
1830
+ Date.now(),
1831
+ stripInlineBytesFromJson(eventData, "placeholder"),
1832
+ runId,
1833
+ ],
1827
1834
  });
1828
1835
  }
1829
1836
  export const CHECKPOINT_TERMINAL_EVENT_SEQ = 1_000_000_000;
@@ -2479,6 +2486,11 @@ async function appendTerminalRunEvent(runId, event) {
2479
2486
  const nextSeq = last ? Number(last.seq ?? -1) + 1 : 0;
2480
2487
  await client.execute({
2481
2488
  sql: `INSERT INTO agent_run_events (run_id, seq, event_at, event_data) VALUES (?, ?, ?, ?) ON CONFLICT (run_id, seq) DO NOTHING`,
2482
- args: [runId, nextSeq, Date.now(), JSON.stringify(event)],
2489
+ args: [
2490
+ runId,
2491
+ nextSeq,
2492
+ Date.now(),
2493
+ stripInlineBytesFromJson(JSON.stringify(event), "placeholder"),
2494
+ ],
2483
2495
  });
2484
2496
  }
@@ -4,10 +4,9 @@ import { isCredentialGapCodeAgentEvent, normalizeCodeAgentTranscript, } from "..
4
4
  import { stripAgentChatContextFromMessage } from "../shared/agent-chat-context.js";
5
5
  import { RUN_NOT_STARTED_METADATA_KEY, } from "../shared/agent-chat-run-not-started.js";
6
6
  import { BUILDER_GATEWAY_INTERNAL_ERROR_CODE } from "./engine/error-detail.js";
7
- import { stringifyToolUseInputForGateway } from "./engine/translate-anthropic.js";
7
+ import { INTERRUPTED_TOOL_RESULT_MARKER as INTERRUPTED_TOOL_RESULT, stringifyToolUseInputForGateway, } from "./engine/translate-anthropic.js";
8
8
  import { parseFollowUpSuggestions } from "./follow-up-suggestions.js";
9
9
  import { isContinuationTerminalReason } from "./types.js";
10
- const INTERRUPTED_TOOL_RESULT = "Interrupted before this tool returned a result.";
11
10
  const INTERRUPTED_ACTIVITY_RESULT = "Stopped before this action started.";
12
11
  export const ASSISTANT_RUN_DURATION_METADATA_KEY = "agentNativeRunDurationMs";
13
12
  const MAX_STORED_ATTACHMENT_CHARS = 60_000;
@@ -139,8 +139,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
139
139
  duplicate?: undefined;
140
140
  eventId?: undefined;
141
141
  } | {
142
- error?: undefined;
143
142
  accepted: boolean;
144
143
  duplicate: boolean;
145
144
  eventId: string;
145
+ error?: undefined;
146
146
  }>;
@@ -7,6 +7,7 @@ import { createGetDb } from "../db/create-get-db.js";
7
7
  import { ensureColumnExists, ensureIndexExists, ensureTableExists, } from "../db/ddl-guard.js";
8
8
  import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
9
9
  import { getRequestOrgId } from "../server/request-context.js";
10
+ import { stripInlineBytes } from "../shared/inline-bytes.js";
10
11
  import { resolveAccess } from "../sharing/access.js";
11
12
  import { registerShareableResource } from "../sharing/registry.js";
12
13
  import { roleSatisfies } from "../sharing/schema.js";
@@ -808,7 +809,7 @@ export async function forkThread(sourceId, ownerEmail, opts) {
808
809
  return null;
809
810
  }
810
811
  const id = opts?.id ?? generateId();
811
- const threadData = forkThreadData(source.threadData, id, snapshot?.fromMessageId);
812
+ const threadData = JSON.stringify(stripInlineBytes(JSON.parse(forkThreadData(source.threadData, id, snapshot?.fromMessageId)), "placeholder"));
812
813
  const now = Date.now();
813
814
  const title = source.title ? `${source.title} (fork)` : "";
814
815
  const client = getDbExec();
@@ -1096,7 +1097,10 @@ export async function updateThreadData(id, threadData, title, preview, messageCo
1096
1097
  preserveExistingTopLevelKeys: options.preserveExistingTopLevelKeys ?? true,
1097
1098
  onAnnotationConflict: (conflict) => annotationConflicts.push(conflict),
1098
1099
  });
1099
- nextThreadData = JSON.stringify(merged);
1100
+ // Client snapshots can predate their upload URL, so inline bytes become a
1101
+ // visible placeholder rather than failing the save; legacy rows are
1102
+ // scrubbed on their next write.
1103
+ nextThreadData = JSON.stringify(stripInlineBytes(merged, "placeholder"));
1100
1104
  nextMessageCount = countThreadMessages(merged, messageCount);
1101
1105
  const nextUpdatedAt = Math.max(Date.now(), current.updatedAt + 1);
1102
1106
  // Completion persistence can race the separate generated-title save.
@@ -1,5 +1,5 @@
1
1
  import { AgentKitRunSlotBusyError } from "@agent-native/agentkit/client";
2
- import { isAgentKitProtocolVersion, parseAgentQueuedMessage, parseAgentRunOptions, parseAgentThreadSnapshot, } from "@agent-native/agentkit/protocol";
2
+ import { isAgentKitProtocolVersion, parseAgentQueuedMessage, parseAgentRunOptions, parseAgentThreadSnapshot, persistableFilePart, } from "@agent-native/agentkit/protocol";
3
3
  import { projectRootAssistantMessages } from "../../agent/thread-message-projection.js";
4
4
  import { RUN_NOT_STARTED_METADATA_KEY, retryContextFromRequest, } from "../../shared/agent-chat-run-not-started.js";
5
5
  import { agentEngineStatusUrlForChatApi, requireAgentEngineConfiguredForDispatch, } from "../agent-engine-readiness.js";
@@ -167,6 +167,9 @@ function messagePart(value, fallbackTextFormat) {
167
167
  : typeof part.mimeType === "string"
168
168
  ? { mediaType: part.mimeType }
169
169
  : {}),
170
+ ...(part.omitted === "inline-bytes"
171
+ ? { omitted: "inline-bytes" }
172
+ : {}),
170
173
  };
171
174
  }
172
175
  return {
@@ -1046,9 +1049,6 @@ function assistantMessageIdsByRun(events) {
1046
1049
  }
1047
1050
  return uniqueMessageIdsByRun;
1048
1051
  }
1049
- function persistedFileUrl(url) {
1050
- return url && !/^\s*data:/i.test(url) ? url : undefined;
1051
- }
1052
1052
  function persistedHistoryEvents(events = []) {
1053
1053
  const sequenceByRun = new Map();
1054
1054
  return events.flatMap((event) => {
@@ -1154,16 +1154,17 @@ function persistedMessages(messages) {
1154
1154
  ];
1155
1155
  }
1156
1156
  if (part.type === "file") {
1157
- const url = persistedFileUrl(part.url);
1158
- if (!url && !part.fileId)
1157
+ const stored = persistableFilePart(part);
1158
+ if (!stored.url && !stored.fileId && !stored.omitted)
1159
1159
  return [];
1160
1160
  return [
1161
1161
  {
1162
1162
  type: "file",
1163
- name: part.name,
1164
- ...(part.mediaType ? { mediaType: part.mediaType } : {}),
1165
- ...(url ? { url } : {}),
1166
- ...(part.fileId ? { fileId: part.fileId } : {}),
1163
+ name: stored.name,
1164
+ ...(stored.mediaType ? { mediaType: stored.mediaType } : {}),
1165
+ ...(stored.url ? { url: stored.url } : {}),
1166
+ ...(stored.fileId ? { fileId: stored.fileId } : {}),
1167
+ ...(stored.omitted ? { omitted: stored.omitted } : {}),
1167
1168
  },
1168
1169
  ];
1169
1170
  }
@@ -1,4 +1,4 @@
1
- import { AGENT_TOOL_CALL_HISTORY_MEDIA_TYPE, AGENT_TOOL_RESULT_HISTORY_MEDIA_TYPE, AGENTKIT_PROTOCOL_VERSION, AgentProtocolValidationError, approvalResponseFromResume, createCapabilityUnavailableError, createCapabilityUnsupportedError, inferAgentActivityKind, negotiateAgentKitProtocolVersion, resumeEntryFromApproval, resumeOptionId, } from "@agent-native/agentkit/protocol";
1
+ import { AGENT_TOOL_CALL_HISTORY_MEDIA_TYPE, AGENT_TOOL_RESULT_HISTORY_MEDIA_TYPE, AGENTKIT_PROTOCOL_VERSION, AgentProtocolValidationError, approvalResponseFromResume, createCapabilityUnavailableError, createCapabilityUnsupportedError, inferAgentActivityKind, isInlineDataUrl, negotiateAgentKitProtocolVersion, resumeEntryFromApproval, resumeOptionId, } from "@agent-native/agentkit/protocol";
2
2
  import { AUTO_CONTINUE_OF_RUN_METADATA_KEY, AUTO_CONTINUE_PROMPT, AUTO_CONTINUE_REFUSAL_CODES, CONTINUE_OF_RUN_METADATA_KEY, CONTINUE_UNAVAILABLE_CODE, } from "../../agent/auto-continue.js";
3
3
  import { parseBase64DataUrl } from "../../shared/data-url.js";
4
4
  import { emitChatFirstOpenApp, emitChatFirstOpenBrowser, } from "../chat-first-state.js";
@@ -3255,10 +3255,14 @@ export function createAgentKitProtocolAdapter(runtime, options = {}) {
3255
3255
  if (state.status === "missing" || !state.turnId) {
3256
3256
  throw Object.assign(new Error(`Agent run ${input.runId} cannot be continued.`), { code: CONTINUE_UNAVAILABLE_CODE, retryable: false });
3257
3257
  }
3258
+ const attachments = (input.attachments ?? [])
3259
+ .filter((part) => !isInlineDataUrl(part.url))
3260
+ .map(runtimeAttachmentFromFilePart);
3258
3261
  const turn = await session.continueTurn({
3259
3262
  turnId: state.turnId,
3260
3263
  prompt: AUTO_CONTINUE_PROMPT,
3261
3264
  metadata: { [CONTINUE_OF_RUN_METADATA_KEY]: state.runId },
3265
+ ...(attachments.length ? { attachments } : {}),
3262
3266
  abortSignal: readers.signal,
3263
3267
  });
3264
3268
  const runId = await openStartedRun(input.threadId, session, turn, undefined, { continuedRunId: input.runId });
@@ -1,4 +1,4 @@
1
- import type { AgentSuggestion } from "@agent-native/agentkit/protocol";
1
+ import { type AgentSuggestion } from "@agent-native/agentkit/protocol";
2
2
  import type { ActionChatUIConfig } from "../../action-ui.js";
3
3
  import type { AgentMcpAppPayload } from "../../mcp-client/app-result.js";
4
4
  import type { ReasoningEffort } from "../../shared/reasoning-effort.js";
@@ -240,6 +240,8 @@ export interface AgentChatRuntimeContinueInput {
240
240
  readonly prompt?: string;
241
241
  readonly approval?: AgentChatRuntimeApprovalResponse;
242
242
  readonly connection?: AgentChatRuntimeConnectionResponse;
243
+ /** Durable references to the continued turn's attachments; never bytes. */
244
+ readonly attachments?: readonly AgentChatRuntimeAttachment[];
243
245
  readonly metadata?: AgentChatRuntimeMetadata;
244
246
  readonly abortSignal?: AbortSignal;
245
247
  }