@agent-native/core 0.203.0 → 0.203.1-nightly-20261007154213

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 (50) hide show
  1. package/dist/action.d.ts +34 -0
  2. package/dist/agent/clip-text.d.ts +4 -0
  3. package/dist/agent/clip-text.js +17 -0
  4. package/dist/agent/follow-up-suggestions.d.ts +16 -2
  5. package/dist/agent/follow-up-suggestions.js +69 -1
  6. package/dist/agent/production-agent.d.ts +20 -2
  7. package/dist/agent/production-agent.js +322 -132
  8. package/dist/agent/provider-model-selection.d.ts +21 -2
  9. package/dist/agent/provider-model-selection.js +19 -2
  10. package/dist/agent/run-loop-with-resume.js +2 -16
  11. package/dist/agent/tool-search.d.ts +31 -1
  12. package/dist/agent/tool-search.js +196 -15
  13. package/dist/agent/write-receipts.d.ts +27 -0
  14. package/dist/agent/write-receipts.js +181 -0
  15. package/dist/automation/index.d.ts +1 -1
  16. package/dist/collab/routes.d.ts +2 -2
  17. package/dist/collab/struct-routes.d.ts +1 -1
  18. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  19. package/dist/observability/routes.d.ts +10 -10
  20. package/dist/observability/trace-error.d.ts +5 -3
  21. package/dist/observability/trace-error.js +29 -4
  22. package/dist/observability/traces.js +6 -0
  23. package/dist/org/handlers.d.ts +2 -2
  24. package/dist/provider-api/actions/provider-api.d.ts +2 -2
  25. package/dist/resources/handlers.d.ts +1 -1
  26. package/dist/secrets/routes.d.ts +5 -5
  27. package/dist/server/action-routes.js +3 -0
  28. package/dist/server/agent-chat/prompt-resources.d.ts +13 -0
  29. package/dist/server/agent-chat/prompt-resources.js +21 -12
  30. package/dist/server/agent-chat-plugin.js +25 -9
  31. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  32. package/dist/server/agent-engine-default-model-route.d.ts +2 -2
  33. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  34. package/dist/server/embed-route.js +2 -1
  35. package/dist/server/index.d.ts +3 -3
  36. package/dist/server/index.js +3 -3
  37. package/dist/server/realtime-voice.js +2 -4
  38. package/dist/shared/embed-auth.d.ts +1 -0
  39. package/dist/shared/embed-auth.js +1 -0
  40. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  41. package/dist/templates/chat/.agents/skills/actions/SKILL.md +25 -0
  42. package/dist/templates/default/.agents/skills/actions/SKILL.md +25 -0
  43. package/dist/templates/headless/.agents/skills/actions/SKILL.md +25 -0
  44. package/dist/templates/workspace-core/.agents/skills/actions/SKILL.md +25 -0
  45. package/dist/triggers/actions/manage-automation.d.ts +7 -7
  46. package/package.json +3 -3
  47. package/src/templates/chat/.agents/skills/actions/SKILL.md +25 -0
  48. package/src/templates/default/.agents/skills/actions/SKILL.md +25 -0
  49. package/src/templates/headless/.agents/skills/actions/SKILL.md +25 -0
  50. package/src/templates/workspace-core/.agents/skills/actions/SKILL.md +25 -0
@@ -44,6 +44,7 @@ import { isReasoningEffort, normalizeReasoningEffortForRequest, stepDownReasonin
44
44
  import { SYNTHETIC_TRAFFIC_BETA_E2E, SYNTHETIC_TRAFFIC_HEADER, } from "../shared/test-traffic.js";
45
45
  import { actionPreparationContinuationNote } from "./action-continuation-guidance.js";
46
46
  import { drainAgentWarnings, formatAgentWarningsForToolResult, } from "./action-warnings.js";
47
+ import { clipHead } from "./clip-text.js";
47
48
  import { buildSystemManifestSections, readContextXraySystemSections, } from "./context-xray/manifest.js";
48
49
  import { AGENT_CHAT_BACKGROUND_RUN_FIELD, AGENT_CHAT_PROCESS_RUN_PATH, backgroundRuntimeDiagnosticDetail, dispatchPathTargetsNetlifyBackgroundFunction, isAgentChatDurableBackgroundEnabled, isAgentChatForegroundSelfChainEnabled, isInBackgroundFunctionRuntime, resolveAgentChatProcessRunDispatchPath, shouldUseBackgroundFunctionTimeoutForWorker, } from "./durable-background.js";
49
50
  import { applyContextXrayTransformForIteration } from "./engine/context-directives-transform.js";
@@ -51,19 +52,20 @@ import { attemptContinuationDispatch } from "./engine/continuation-dispatch-retr
51
52
  import { formatLlmCredentialErrorMessage, LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, userFacingLlmCredentialError, } from "./engine/credential-errors.js";
52
53
  import { BUILDER_GATEWAY_INTERNAL_ERROR_CODE, isContextOverflowCode, isContextOverflowMessage, isProviderConnectionErrorMessage, PROVIDER_RATE_LIMITED_ERROR_CODE, PROVIDER_TRANSIENT_REJECTION_ERROR_CODE, } from "./engine/error-detail.js";
53
54
  import { resolveEngine, explicitEngineName, registerBuiltinEngines, getStoredModelForEngine, normalizeModelForEngine, isResolvedEngineUsableForRequest, } from "./engine/index.js";
55
+ import { limitProviderTools, MAX_PROVIDER_TOOLS, } from "./engine/limit-provider-tools.js";
54
56
  import { resolveEmptyResponseRetryMaxOutputTokens, resolveMainChatMaxOutputTokens, resolveMaxOutputTokensForEngine, } from "./engine/output-tokens.js";
55
57
  import { PROVIDER_TO_ENV } from "./engine/provider-env-vars.js";
56
58
  import { JOURNALED_TOOL_REPLAY_PREFIX, RECOVERED_TOOL_REPLAY_PREFIX, loadedSkillPagesContext, loadPriorTurnToolCallJournal, seedRepeatedToolErrorCountsFromJournal, seedRepeatedToolCallCountsFromJournal, } from "./engine/tool-call-journal-seed.js";
57
59
  import { backfillEngineMessagesToolResults, stringifyToolUseInputForGateway, unmatchedToolResultReplayText, } from "./engine/translate-anthropic.js";
58
60
  import { EngineError } from "./engine/types.js";
59
- import { FOLLOW_UP_SUGGESTIONS_COMPLETION_INSTRUCTION, FOLLOW_UP_SUGGESTIONS_INSTRUCTION, FOLLOW_UP_SUGGESTIONS_MAX_OUTPUT_TOKENS, FOLLOW_UP_SUGGESTIONS_TOOL_NAME, followUpSuggestionsTool, identifyFollowUpSuggestions, parseFollowUpSuggestions, } from "./follow-up-suggestions.js";
61
+ 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";
60
62
  import { filterHostedHarnessToolNames, normalizeHostedHarnessRuntime, } from "./harness/hosted.js";
61
63
  import { buildRecentUserRequestContext, buildJevRequestContext, preloadJevTools, } from "./jev-tool-prefetch.js";
62
64
  import { getDefaultMaxIterations, getDefaultMaxRunInputTokens, MAX_AGENT_MAX_ITERATIONS, MIN_AGENT_MAX_ITERATIONS, normalizeMaxIterations, normalizeMaxRunInputTokens, readAgentLoopSettings, } from "./loop-settings.js";
63
65
  import { getContextWindowForModel, resolveFallbackModel, } from "./model-config.js";
64
66
  import { maybeCompactThread, buildObservationalContext, hasObservationalMemory, serializeObservationalMemoryBlock, } from "./observational-memory/index.js";
65
67
  import { ProcessorChain, TripWire, toolCallsFromContent, } from "./processors.js";
66
- import { resolveUncheckedDefaultModelReplacement } from "./provider-model-selection.js";
68
+ import { applyUncheckedDefaultModelReplacement } from "./provider-model-selection.js";
67
69
  import { startRun, subscribeToRun, replayCompletedTurn, getActiveRunForThread, getActiveRunForThreadAsync, getRun, abortRun, abortRunDurably, abortTurnByRefDurably, abortTurnDurably, getSlotHoldingRunId, tryClaimRunSlot, isHostedRuntime, resolveRunSoftTimeoutMs, resolveRunToolTimeoutCeilingMs, endsAfterToolResultWithoutAssistantFinal, endsDuringActionPreparation, } from "./run-manager.js";
68
70
  import { writeLedgerEntry, readLedgerEntry, clearLedgerForThread, AgentTurnInitiatorMismatchError, AgentTurnInitiatorUnavailableError, insertRun, insertRunEvent, isTurnAborted, markRunAborted, updateRunHeartbeat, updateRunStatusIfRunning, setRunError, setRunTerminalReason, claimBackgroundRun, readBackgroundRunClaim, recordRunDiagnostic, countRunsForTurn, RUN_DIAG_STAGE, UNCLAIMED_BACKGROUND_RUN_GRACE_MS, turnRunLedgerExhausted, } from "./run-store.js";
69
71
  import { buildCurrentTimeUserContext } from "./runtime-context.js";
@@ -72,8 +74,9 @@ import { consumeAgentToolApproval, createAgentToolApproval, isAgentToolAlwaysAll
72
74
  import { buildResumeJournalNote, findCompletedJournalEntry, } from "./tool-call-journal.js";
73
75
  import { redactSensitiveFields, sanitizeToolErrorText, sanitizeToolErrorValue, } from "./tool-error-redaction.js";
74
76
  import { describeToolResultImages, extractAgentImagesFromActionResult, } from "./tool-result-images.js";
75
- import { createToolSearchEntry, filterActionsForAgentDiscovery, searchToolRegistry, TOOL_SEARCH_ACTION_NAME, } from "./tool-search.js";
77
+ import { createToolSearchEntry, extractToolSearchResultNames, filterActionsForAgentDiscovery, readLoadedToolNames, searchToolRegistry, TOOL_SEARCH_ACTION_NAME, withLoadedToolNames, } from "./tool-search.js";
76
78
  import { normalizeAgentActionScope, } from "./types.js";
79
+ import { mergeFinalResponseGuards, readWriteReceipt, writeReceiptGuard, } from "./write-receipts.js";
77
80
  registerBuiltinEngines();
78
81
  export { PROVIDER_TO_ENV };
79
82
  /**
@@ -1081,6 +1084,11 @@ export function resolveSourceSweepToolCallThreshold() {
1081
1084
  return getAppConfig().agent.sourceSweepToolCallThreshold;
1082
1085
  }
1083
1086
  const EXPANDED_TOOL_SCHEMA_WARN_BYTES = 32_000;
1087
+ const MAX_TOOL_SEARCH_OVERFLOW_NAMES_SHOWN = 5;
1088
+ const PROVIDER_CORE_TOOL_NAMES = [
1089
+ TOOL_SEARCH_ACTION_NAME,
1090
+ FOLLOW_UP_SUGGESTIONS_TOOL_NAME,
1091
+ ];
1084
1092
  const MAX_SCREEN_CONTEXT_CHARS = 10_000;
1085
1093
  function capScreenContext(text) {
1086
1094
  if (text.length <= MAX_SCREEN_CONTEXT_CHARS)
@@ -1841,6 +1849,36 @@ async function enrichMessage(message, references) {
1841
1849
  }
1842
1850
  return `${parts.join("\n\n")}\n\n${message}`;
1843
1851
  }
1852
+ /**
1853
+ * Folds one loop attempt's usage into a running total. Every wrapper that
1854
+ * re-enters `runAgentLoop` merges through this, so a field added to
1855
+ * `AgentLoopUsage` cannot be reported by the loop and dropped by a wrapper.
1856
+ */
1857
+ export function mergeAgentLoopUsage(total, next) {
1858
+ total.inputTokens += next.inputTokens;
1859
+ total.outputTokens += next.outputTokens;
1860
+ total.cacheReadTokens += next.cacheReadTokens;
1861
+ total.cacheWriteTokens += next.cacheWriteTokens;
1862
+ for (const key of [
1863
+ "builderCreditsUsed",
1864
+ "llmCalls",
1865
+ "followUpMs",
1866
+ "followUpInputTokens",
1867
+ "receiptUnverifiedCount",
1868
+ "receiptChangedFalseCount",
1869
+ ]) {
1870
+ if (typeof next[key] === "number") {
1871
+ total[key] = (total[key] ?? 0) + next[key];
1872
+ }
1873
+ }
1874
+ total.engineName = next.engineName ?? total.engineName;
1875
+ total.model = next.model;
1876
+ if (next.usageReported)
1877
+ total.usageReported = true;
1878
+ // Keep the earliest attempt's first event — a later continuation
1879
+ // attempt starting fresh must not overwrite genuine first-token timing.
1880
+ total.firstEngineEventAtMs ??= next.firstEngineEventAtMs;
1881
+ }
1844
1882
  function collectTextParts(parts) {
1845
1883
  return parts
1846
1884
  .filter((part) => part.type === "text")
@@ -2255,6 +2293,51 @@ const INTERRUPTED_TOOL_LEDGER_RECOVERY_TIMEOUT_MS = 5_000;
2255
2293
  const MAX_IDENTICAL_TOOL_ERRORS = 3;
2256
2294
  export const MAX_SAME_ERROR_ACROSS_ARGUMENTS = 3;
2257
2295
  export const MAX_IDENTICAL_TOOL_CALLS = 8;
2296
+ const MAX_ALREADY_LOADED_TOOL_SEARCHES = 4;
2297
+ const MAX_STOP_ERROR_LINE_CHARS = 300;
2298
+ /** `error: message` for a JSON `{ error, message }` result; `undefined` when the text is not one. */
2299
+ function jsonErrorCause(text) {
2300
+ if (!text.startsWith("{"))
2301
+ return undefined;
2302
+ let parsed;
2303
+ try {
2304
+ parsed = JSON.parse(text);
2305
+ // coercion-ok: unparseable text is not a JSON error result; the caller reads its first line
2306
+ }
2307
+ catch {
2308
+ return undefined;
2309
+ }
2310
+ const { error, message } = (parsed ?? {});
2311
+ if (typeof error !== "string" || !error.trim())
2312
+ return undefined;
2313
+ return typeof message === "string" && message.trim()
2314
+ ? `${error}: ${message}`
2315
+ : error;
2316
+ }
2317
+ function errorCauseLine(text) {
2318
+ const body = text.trim().replace(/^Error running [^\s:]+:\s*/, "");
2319
+ return ((jsonErrorCause(body) ?? body)
2320
+ .split(/\r?\n/)
2321
+ .find((line) => line.trim())
2322
+ ?.trim() ?? "");
2323
+ }
2324
+ /**
2325
+ * The line of a failed tool result that names the cause, for a message the
2326
+ * user reads. It takes the credential-sanitized text and keeps identifiers and
2327
+ * emails, since a column or account name is the diagnosis; the trace-grade
2328
+ * `toolErrorSignature` redacts those. A pretty-printed JSON error opens with
2329
+ * `Error running x: {`, and a `(errorCode: ...)` suffix keeps it from parsing,
2330
+ * so a bare first line can be only a bracket.
2331
+ */
2332
+ function stopErrorLine(sanitizedResult) {
2333
+ const lines = sanitizedResult.split(/\r?\n/).filter((line) => line.trim());
2334
+ for (const candidate of [sanitizedResult, ...lines]) {
2335
+ const cause = errorCauseLine(candidate);
2336
+ if (/[\p{L}\p{N}]/u.test(cause))
2337
+ return cause;
2338
+ }
2339
+ return errorCauseLine(sanitizedResult) || "(no error text)";
2340
+ }
2258
2341
  function isToolCallTimeoutResult(content) {
2259
2342
  return /tool call timed out after \d+(?:\.\d+)? seconds?/i.test(content);
2260
2343
  }
@@ -2500,30 +2583,9 @@ const DEFAULT_INITIAL_TOOL_NAMES = new Set([
2500
2583
  function isDefaultInitialToolName(name) {
2501
2584
  return DEFAULT_INITIAL_TOOL_NAMES.has(name);
2502
2585
  }
2503
- function extractToolSearchResultNames(value) {
2504
- if (!value || typeof value !== "object")
2505
- return [];
2506
- const result = value;
2507
- if (typeof result.query !== "string" || result.query.trim().length === 0) {
2508
- return [];
2509
- }
2510
- if (!Array.isArray(result.results))
2511
- return [];
2512
- const names = [];
2513
- for (const item of result.results) {
2514
- if (!item || typeof item !== "object")
2515
- continue;
2516
- const record = item;
2517
- if (record.callable === false)
2518
- continue;
2519
- const name = record.name;
2520
- if (typeof name === "string" && name.trim())
2521
- names.push(name);
2522
- }
2523
- return names;
2524
- }
2525
- function extractToolSearchResultNamesFromMessages(messages) {
2586
+ function readToolSearchHistory(messages) {
2526
2587
  const names = [];
2588
+ let unreadable = 0;
2527
2589
  for (const message of messages) {
2528
2590
  if (message.role !== "user")
2529
2591
  continue;
@@ -2533,15 +2595,14 @@ function extractToolSearchResultNamesFromMessages(messages) {
2533
2595
  typeof part.content !== "string") {
2534
2596
  continue;
2535
2597
  }
2536
- try {
2537
- names.push(...extractToolSearchResultNames(JSON.parse(part.content)));
2538
- }
2539
- catch {
2540
- // Tool results are best-effort history hints; ignore non-JSON content.
2541
- }
2598
+ const loaded = readLoadedToolNames(part.content);
2599
+ if (loaded)
2600
+ names.push(...loaded);
2601
+ else
2602
+ unreadable += 1;
2542
2603
  }
2543
2604
  }
2544
- return names;
2605
+ return { names, unreadable };
2545
2606
  }
2546
2607
  function normalizeToolInputSchema(schema) {
2547
2608
  if (!schema)
@@ -3347,6 +3408,8 @@ export async function runAgentLoop(opts) {
3347
3408
  : providedAvailableTools;
3348
3409
  let completedFollowUpSuggestions;
3349
3410
  let completingFollowUpSuggestions = false;
3411
+ let followUpCompletionContext = [];
3412
+ let followUpStartedAt;
3350
3413
  let followUpCompletionFailed = false;
3351
3414
  const failFollowUpCompletion = (code) => {
3352
3415
  if (followUpCompletionFailed)
@@ -3382,8 +3445,8 @@ export async function runAgentLoop(opts) {
3382
3445
  const finalResponseGuardRequestText = opts.finalResponseGuardRequestText ??
3383
3446
  resolveFinalResponseGuardRequestText(messages);
3384
3447
  const availableToolMap = new Map((availableTools ?? tools).map((tool) => [tool.name, tool]));
3385
- const activeToolNames = new Set(tools.map((tool) => tool.name));
3386
- let activeTools = tools;
3448
+ let activeTools = limitProviderTools(tools, PROVIDER_CORE_TOOL_NAMES);
3449
+ const activeToolNames = new Set(activeTools.map((tool) => tool.name));
3387
3450
  let appAuthorizationPromise;
3388
3451
  const resolveTurnAppAuthorization = (userEmail, orgId) => {
3389
3452
  if (!appAuthorizationPromise) {
@@ -3401,36 +3464,85 @@ export async function runAgentLoop(opts) {
3401
3464
  };
3402
3465
  let expandedToolSchemaBytes = 0;
3403
3466
  let reportedExpandedToolSchemaBytes = false;
3404
- const expandActiveTools = (names) => {
3405
- const added = [];
3406
- for (const name of names) {
3407
- if (activeToolNames.has(name))
3408
- continue;
3467
+ let alreadyLoadedToolSearches = 0;
3468
+ // Tools loaded after the first request, least recently needed first.
3469
+ const loadedToolNames = new Set();
3470
+ const noteToolNeeded = (name) => {
3471
+ if (loadedToolNames.delete(name))
3472
+ loadedToolNames.add(name);
3473
+ };
3474
+ // `names` lead with what the model needs most. Below MAX_PROVIDER_TOOLS they
3475
+ // append in registry order, so the tools prefix of the prompt cache survives.
3476
+ // Past it the request keeps, in this order: tool-search and the follow-up
3477
+ // tool, `pinned` (already promised this step) and `names`, the initial
3478
+ // tools, then the other loaded tools most recently needed first, so the least
3479
+ // recently needed loaded tools are evicted before any initial one. A tool
3480
+ // that still does not fit is returned as overflow and never becomes active,
3481
+ // so `activeTools` is exactly what the engine sends and nothing it dropped is
3482
+ // reported as loaded.
3483
+ const expandActiveTools = (names, pinned = new Set()) => {
3484
+ const requested = new Set(names);
3485
+ for (const name of requested)
3486
+ noteToolNeeded(name);
3487
+ const wanted = [...requested].flatMap((name) => {
3409
3488
  const tool = availableToolMap.get(name);
3410
- if (!tool)
3411
- continue;
3412
- activeToolNames.add(name);
3413
- expandedToolSchemaBytes += JSON.stringify(tool).length;
3414
- added.push(name);
3415
- }
3416
- if (added.length > 0) {
3417
- const expandedTools = (availableTools ?? tools).filter((tool) => activeToolNames.has(tool.name));
3418
- const prioritizedNames = new Set(names);
3419
- if (followUpRunId)
3420
- prioritizedNames.add(FOLLOW_UP_SUGGESTIONS_TOOL_NAME);
3421
- activeTools = [
3422
- ...expandedTools.filter((tool) => prioritizedNames.has(tool.name)),
3423
- ...expandedTools.filter((tool) => !prioritizedNames.has(tool.name)),
3424
- ];
3489
+ return tool && !activeToolNames.has(name) ? [tool] : [];
3490
+ });
3491
+ const added = [];
3492
+ const overflow = [];
3493
+ if (wanted.length > 0) {
3494
+ if (activeTools.length + wanted.length <= MAX_PROVIDER_TOOLS) {
3495
+ const wantedNames = new Set(wanted.map((tool) => tool.name));
3496
+ activeTools = [
3497
+ ...activeTools,
3498
+ ...(availableTools ?? tools).filter((tool) => wantedNames.has(tool.name)),
3499
+ ];
3500
+ }
3501
+ else {
3502
+ const byName = new Map([...activeTools, ...wanted].map((tool) => [tool.name, tool]));
3503
+ const priority = new Set([
3504
+ ...PROVIDER_CORE_TOOL_NAMES,
3505
+ ...pinned,
3506
+ ...names,
3507
+ ...activeTools
3508
+ .map((tool) => tool.name)
3509
+ .filter((name) => !loadedToolNames.has(name)),
3510
+ ...[...loadedToolNames].reverse(),
3511
+ ]);
3512
+ activeTools = [...priority]
3513
+ .flatMap((name) => byName.get(name) ?? [])
3514
+ .slice(0, MAX_PROVIDER_TOOLS);
3515
+ }
3516
+ activeToolNames.clear();
3517
+ for (const tool of activeTools)
3518
+ activeToolNames.add(tool.name);
3519
+ for (const name of loadedToolNames) {
3520
+ if (!activeToolNames.has(name))
3521
+ loadedToolNames.delete(name);
3522
+ }
3523
+ for (const tool of wanted) {
3524
+ if (!activeToolNames.has(tool.name)) {
3525
+ overflow.push(tool.name);
3526
+ continue;
3527
+ }
3528
+ added.push(tool.name);
3529
+ loadedToolNames.add(tool.name);
3530
+ expandedToolSchemaBytes += JSON.stringify(tool).length;
3531
+ }
3425
3532
  }
3426
3533
  if (!reportedExpandedToolSchemaBytes &&
3427
3534
  expandedToolSchemaBytes > EXPANDED_TOOL_SCHEMA_WARN_BYTES) {
3428
3535
  reportedExpandedToolSchemaBytes = true;
3429
3536
  console.warn(`[agent-loop] expanded tool schemas reached ${expandedToolSchemaBytes} bytes across ${activeToolNames.size} active tools (runId=${opts.runId ?? "none"})`);
3430
3537
  }
3431
- return added;
3538
+ return { added, overflow };
3432
3539
  };
3433
- expandActiveTools(extractToolSearchResultNamesFromMessages(messages));
3540
+ const priorSearches = readToolSearchHistory(messages);
3541
+ if (priorSearches.unreadable > 0) {
3542
+ console.warn(`[agent-loop] ${priorSearches.unreadable} earlier tool-search result(s) could not be read, so the tools they loaded were not restored (runId=${opts.runId ?? "none"})`);
3543
+ }
3544
+ // Newest searches lead, so they win if history alone overflows the cap.
3545
+ expandActiveTools(priorSearches.names.reverse());
3434
3546
  const processorChain = opts.processors && opts.processors.length > 0
3435
3547
  ? new ProcessorChain(opts.processors)
3436
3548
  : null;
@@ -3460,6 +3572,8 @@ export async function runAgentLoop(opts) {
3460
3572
  actions,
3461
3573
  });
3462
3574
  const toolResultHistory = [];
3575
+ const turnReceipts = () => toolResultHistory.flatMap((result) => result.receipt ? [{ tool: result.name, ...result.receipt }] : []);
3576
+ let receiptGuardRetried = false;
3463
3577
  const runCtx = getRequestRunContext();
3464
3578
  if (runCtx) {
3465
3579
  runCtx.toolCalls = toolCallHistory;
@@ -3618,44 +3732,28 @@ export async function runAgentLoop(opts) {
3618
3732
  let terminalStopReason;
3619
3733
  const toolCallErrors = new Map();
3620
3734
  let contextMessages = completingFollowUpSuggestions
3621
- ? [
3622
- ...messages,
3623
- {
3624
- role: "user",
3625
- content: [
3626
- {
3627
- type: "text",
3628
- text: FOLLOW_UP_SUGGESTIONS_COMPLETION_INSTRUCTION,
3629
- },
3630
- ],
3631
- },
3632
- ]
3735
+ ? followUpCompletionContext
3633
3736
  : messages;
3634
- try {
3635
- if (opts.threadId) {
3636
- contextMessages = await applyContextXrayTransformForIteration({
3737
+ // The completion call carries a digest, not the conversation: the manifest
3738
+ // and memory window describe the full thread, and the transform would
3739
+ // overwrite this turn's manifest with the digest.
3740
+ if (opts.threadId && !completingFollowUpSuggestions) {
3741
+ contextMessages = await applyContextXrayTransformForIteration({
3742
+ threadId: opts.threadId,
3743
+ ownerEmail: opts.ownerEmail,
3744
+ turnId: opts.turnId,
3745
+ model,
3746
+ messages: contextMessages,
3747
+ systemSections: opts.systemSections,
3748
+ });
3749
+ if (opts.ownerEmail) {
3750
+ contextMessages = await applyObservationalMemoryToContext(contextMessages, {
3637
3751
  threadId: opts.threadId,
3638
3752
  ownerEmail: opts.ownerEmail,
3639
- turnId: opts.turnId,
3640
- model,
3641
- messages: contextMessages,
3642
- systemSections: opts.systemSections,
3753
+ orgId: opts.orgId ?? null,
3643
3754
  });
3644
- if (opts.ownerEmail) {
3645
- contextMessages = await applyObservationalMemoryToContext(contextMessages, {
3646
- threadId: opts.threadId,
3647
- ownerEmail: opts.ownerEmail,
3648
- orgId: opts.orgId ?? null,
3649
- });
3650
- }
3651
3755
  }
3652
3756
  }
3653
- catch (error) {
3654
- if (signal.aborted || !completingFollowUpSuggestions)
3655
- throw error;
3656
- failFollowUpCompletion("context_error");
3657
- break;
3658
- }
3659
3757
  if (signal.aborted)
3660
3758
  break;
3661
3759
  for (let retry = 0;; retry++) {
@@ -3674,7 +3772,9 @@ export async function runAgentLoop(opts) {
3674
3772
  }
3675
3773
  const streamOpts = {
3676
3774
  model,
3677
- systemPrompt: continuationSystemPrompt,
3775
+ systemPrompt: completingFollowUpSuggestions
3776
+ ? FOLLOW_UP_SUGGESTIONS_COMPLETION_SYSTEM_PROMPT
3777
+ : continuationSystemPrompt,
3678
3778
  messages: contextMessages,
3679
3779
  tools: loopBreakerCloseout
3680
3780
  ? []
@@ -3949,6 +4049,10 @@ export async function runAgentLoop(opts) {
3949
4049
  usage.outputTokens += eventUsage.outputTokens;
3950
4050
  usage.cacheReadTokens += eventUsage.cacheReadTokens;
3951
4051
  usage.cacheWriteTokens += eventUsage.cacheWriteTokens;
4052
+ if (completingFollowUpSuggestions) {
4053
+ usage.followUpInputTokens =
4054
+ (usage.followUpInputTokens ?? 0) + eventUsage.inputTokens;
4055
+ }
3952
4056
  if (eventUsage.builderCreditsUsed !== undefined) {
3953
4057
  usage.builderCreditsUsed =
3954
4058
  (usage.builderCreditsUsed ?? 0) +
@@ -4070,6 +4174,9 @@ export async function runAgentLoop(opts) {
4070
4174
  throw err;
4071
4175
  }
4072
4176
  }
4177
+ if (completingFollowUpSuggestions && followUpStartedAt !== undefined) {
4178
+ usage.followUpMs = Date.now() - followUpStartedAt;
4179
+ }
4073
4180
  if (tripwire || followUpCompletionFailed)
4074
4181
  break;
4075
4182
  if (!assistantContent && toolCallErrors.size > 0) {
@@ -4252,6 +4359,8 @@ export async function runAgentLoop(opts) {
4252
4359
  }
4253
4360
  let guard = null;
4254
4361
  const finalResponseDraftText = collectTextParts(assistantContentForHistory);
4362
+ const receipts = turnReceipts();
4363
+ const receiptGuard = writeReceiptGuard(receipts, receiptGuardRetried);
4255
4364
  if (opts.finalResponseGuard) {
4256
4365
  try {
4257
4366
  guard = await opts.finalResponseGuard({
@@ -4261,6 +4370,7 @@ export async function runAgentLoop(opts) {
4261
4370
  text: finalResponseDraftText,
4262
4371
  toolCalls: [...toolCallHistory],
4263
4372
  toolResults: [...toolResultHistory],
4373
+ receipts,
4264
4374
  retryCount: finalGuardRetries,
4265
4375
  executionMode: opts.executionMode ?? "act",
4266
4376
  });
@@ -4270,6 +4380,11 @@ export async function runAgentLoop(opts) {
4270
4380
  throw err;
4271
4381
  }
4272
4382
  }
4383
+ if (receiptGuard) {
4384
+ guard = guard
4385
+ ? mergeFinalResponseGuards(receiptGuard, guard)
4386
+ : receiptGuard;
4387
+ }
4273
4388
  if (guard) {
4274
4389
  completedFollowUpSuggestions = undefined;
4275
4390
  const retryMessage = typeof guard === "string" ? guard : guard.retryMessage;
@@ -4279,7 +4394,10 @@ export async function runAgentLoop(opts) {
4279
4394
  : Math.max(0, Math.min(3, Math.trunc(guard.maxRetries ?? 1)));
4280
4395
  if (finalGuardRetries < maxGuardRetries) {
4281
4396
  if (typeof guard !== "string" && guard.expandToolSurface) {
4282
- expandActiveTools([...availableToolMap.keys()]);
4397
+ expandActiveTools([...activeToolNames, ...availableToolMap.keys()]);
4398
+ }
4399
+ if (receiptGuard && receiptGuard.maxRetries > 0) {
4400
+ receiptGuardRetried = true;
4283
4401
  }
4284
4402
  finalGuardRetries += 1;
4285
4403
  send({ type: "clear" });
@@ -4309,6 +4427,12 @@ export async function runAgentLoop(opts) {
4309
4427
  completedFollowUpSuggestions === undefined &&
4310
4428
  terminalStopReason === "end_turn") {
4311
4429
  completingFollowUpSuggestions = true;
4430
+ followUpStartedAt = Date.now();
4431
+ followUpCompletionContext = buildFollowUpCompletionMessages({
4432
+ requestText: finalResponseGuardRequestText,
4433
+ replyText: finalResponseDraftText,
4434
+ toolNames: toolCallHistory.map((call) => call.name),
4435
+ });
4312
4436
  continue;
4313
4437
  }
4314
4438
  }
@@ -4370,7 +4494,12 @@ export async function runAgentLoop(opts) {
4370
4494
  return requestedActionStop === stop ? stop : null;
4371
4495
  };
4372
4496
  const approvedToolCallKeys = new Set(opts.approvedToolCalls ?? []);
4497
+ // A tool-search in this step loads schemas the model can call only on the
4498
+ // next step, so a sibling search must not read them as already callable.
4499
+ const toolNamesLoadedThisStep = new Set();
4500
+ let countedAlreadyLoadedSearchThisStep = false;
4373
4501
  const runToolCall = async (toolCall) => {
4502
+ noteToolNeeded(toolCall.name);
4374
4503
  const actionEntry = actions[toolCall.name];
4375
4504
  const placeholderNormalization = actionEntry
4376
4505
  ? normalizeOptionalToolPlaceholders(actionEntry.tool.parameters, toolCall.input)
@@ -4442,12 +4571,13 @@ export async function runAgentLoop(opts) {
4442
4571
  name: toolCall.name,
4443
4572
  input: normalizedToolInput,
4444
4573
  });
4445
- const recordToolResult = (content, isError, artifacts) => {
4574
+ const recordToolResult = (content, isError, artifacts, receipt) => {
4446
4575
  toolResultHistory.push({
4447
4576
  name: toolCall.name,
4448
4577
  content,
4449
4578
  isError,
4450
4579
  ...(artifacts?.length ? { artifacts } : {}),
4580
+ ...(receipt ? { receipt } : {}),
4451
4581
  });
4452
4582
  };
4453
4583
  let directStop = null;
@@ -4483,9 +4613,12 @@ export async function runAgentLoop(opts) {
4483
4613
  anyArgsCount >= MAX_SAME_ERROR_ACROSS_ARGUMENTS) {
4484
4614
  const result = `Stopped after ${anyArgsCount} attempts at ${toolCall.name} that all failed the same way ` +
4485
4615
  `with different arguments. Last error: ${sanitizedResult}`;
4616
+ const lastErrorLine = stopErrorLine(sanitizedResult);
4486
4617
  requestedActionStop ??= {
4487
- message: `I stopped because the ${toolCall.name} action rejected ${anyArgsCount} different attempts the same way, ` +
4488
- "so changing the arguments again would not have worked. Anything completed before this is saved.",
4618
+ message: `I stopped because the ${toolCall.name} action rejected ${anyArgsCount} different attempts the same way. ` +
4619
+ `Last error: ${lastErrorLine.length > MAX_STOP_ERROR_LINE_CHARS
4620
+ ? `${clipHead(lastErrorLine, MAX_STOP_ERROR_LINE_CHARS)}…`
4621
+ : lastErrorLine}\n\nAnything completed before this is saved.`,
4489
4622
  errorCode: "repeated_tool_error_across_arguments",
4490
4623
  details: sanitizedResult,
4491
4624
  };
@@ -4955,6 +5088,7 @@ export async function runAgentLoop(opts) {
4955
5088
  let toolResultImages;
4956
5089
  let toolArtifacts = [];
4957
5090
  let fileMutation;
5091
+ let receipt;
4958
5092
  try {
4959
5093
  // The run may have been aborted while we waited above for an
4960
5094
  // interrupted tool's ledger result (the wait can poll for minutes).
@@ -5083,6 +5217,8 @@ export async function runAgentLoop(opts) {
5083
5217
  }
5084
5218
  }
5085
5219
  chatUIResult = resultForAgent;
5220
+ // Before stringify and truncation: a large result must not lose it.
5221
+ receipt = readWriteReceipt(resultForAgent);
5086
5222
  toolArtifacts = detectArtifactReceipts(resultForAgent, toolCall.name);
5087
5223
  if (toolResultImages) {
5088
5224
  imageNotes = [
@@ -5090,6 +5226,18 @@ export async function runAgentLoop(opts) {
5090
5226
  ...imageNotes,
5091
5227
  ];
5092
5228
  }
5229
+ let toolSearch;
5230
+ if (toolCall.name === TOOL_SEARCH_ACTION_NAME && !isError) {
5231
+ const matched = extractToolSearchResultNames(rawForAgent);
5232
+ const { added, overflow } = expandActiveTools(matched, toolNamesLoadedThisStep);
5233
+ for (const name of added)
5234
+ toolNamesLoadedThisStep.add(name);
5235
+ const loadedThisStep = matched.filter((name) => toolNamesLoadedThisStep.has(name));
5236
+ toolSearch = { matched, overflow, loadedThisStep };
5237
+ if (matched.length > 0) {
5238
+ resultForAgent = withLoadedToolNames(rawForAgent, loadedThisStep);
5239
+ }
5240
+ }
5093
5241
  let resultStr = typeof resultForAgent === "string"
5094
5242
  ? resultForAgent
5095
5243
  : JSON.stringify(resultForAgent, null, 2);
@@ -5101,10 +5249,63 @@ export async function runAgentLoop(opts) {
5101
5249
  resultStr = `${resultStr}\n\n${imageNotes.join("\n")}`;
5102
5250
  }
5103
5251
  result = resultStr;
5104
- if (toolCall.name === TOOL_SEARCH_ACTION_NAME && !isError) {
5105
- const added = expandActiveTools(extractToolSearchResultNames(rawForAgent));
5106
- if (added.length > 0) {
5107
- result += `\n\nLoaded matching tool schemas for next step: ${added.join(", ")}`;
5252
+ if (toolSearch) {
5253
+ const { matched, overflow, loadedThisStep } = toolSearch;
5254
+ const loadedNote = `Loaded matching tool schemas for the next step, not this one: ${loadedThisStep.join(", ")}`;
5255
+ if (loadedThisStep.length > 0) {
5256
+ result += `\n\n${loadedNote}`;
5257
+ alreadyLoadedToolSearches = 0;
5258
+ }
5259
+ else if (matched.length > 0 &&
5260
+ matched.every((name) => activeToolNames.has(name))) {
5261
+ const priorMessage = rawForAgent
5262
+ .message;
5263
+ result = JSON.stringify({
5264
+ ...resultForAgent,
5265
+ alreadyLoaded: true,
5266
+ message: [
5267
+ `All ${matched.length} matches are already callable; call them directly.`,
5268
+ typeof priorMessage === "string" ? priorMessage : "",
5269
+ ]
5270
+ .filter(Boolean)
5271
+ .join(" "),
5272
+ }, null, 2);
5273
+ // One count per step, reset by any progress: parallel searches
5274
+ // are one repeat, and a run that searches between writes is not
5275
+ // repeating.
5276
+ if (!countedAlreadyLoadedSearchThisStep) {
5277
+ countedAlreadyLoadedSearchThisStep = true;
5278
+ alreadyLoadedToolSearches += 1;
5279
+ }
5280
+ if (alreadyLoadedToolSearches >= MAX_ALREADY_LOADED_TOOL_SEARCHES) {
5281
+ requestedActionStop ??= {
5282
+ message: `Stopped because tool-search was used ${alreadyLoadedToolSearches} times in a row to look for tools that were already callable, ` +
5283
+ "which means the same step is repeating rather than making progress. " +
5284
+ "Everything completed before this point is preserved above.",
5285
+ errorCode: "repeated_tool_call",
5286
+ };
5287
+ }
5288
+ }
5289
+ if (overflow.length > 0) {
5290
+ const shown = overflow.slice(0, MAX_TOOL_SEARCH_OVERFLOW_NAMES_SHOWN);
5291
+ const more = overflow.length > shown.length
5292
+ ? `, and ${overflow.length - shown.length} more`
5293
+ : "";
5294
+ const overflowNote = `Could not load ${overflow.length} matched ${overflow.length === 1 ? "tool" : "tools"} because the per-request tool limit (${MAX_PROVIDER_TOOLS}) was reached: ${shown.join(", ")}${more}. They are not callable; use tools you already have.`;
5295
+ // A repeated search's own message says its matches are
5296
+ // available, which these are not.
5297
+ result =
5298
+ rawForAgent.repeated === true
5299
+ ? JSON.stringify({
5300
+ ...resultForAgent,
5301
+ message: [
5302
+ loadedThisStep.length > 0 ? loadedNote : "",
5303
+ overflowNote,
5304
+ ]
5305
+ .filter(Boolean)
5306
+ .join(" "),
5307
+ }, null, 2)
5308
+ : `${result}\n\n${overflowNote}`;
5108
5309
  }
5109
5310
  }
5110
5311
  }
@@ -5168,12 +5369,25 @@ export async function runAgentLoop(opts) {
5168
5369
  writeToolInterruptions.set(key, (writeToolInterruptions.get(key) ?? 0) + 1);
5169
5370
  }
5170
5371
  if (isError) {
5372
+ receipt = undefined;
5171
5373
  if (result !== INTERRUPTED_TOOL_RESULT_MARKER) {
5172
5374
  result = finalizeToolErrorResult(result);
5173
5375
  }
5174
5376
  }
5175
5377
  else {
5176
5378
  fileMutation = actionEntry.fileMutationProof?.(toolCall.input);
5379
+ // Only a write or a receipt is progress; a read-only call between
5380
+ // redundant searches is the same repeating step.
5381
+ if (!actionIsReadOnly || receipt)
5382
+ alreadyLoadedToolSearches = 0;
5383
+ if (receipt && receipt.verified !== true) {
5384
+ usage.receiptUnverifiedCount =
5385
+ (usage.receiptUnverifiedCount ?? 0) + 1;
5386
+ }
5387
+ if (receipt && !receipt.changed) {
5388
+ usage.receiptChangedFalseCount =
5389
+ (usage.receiptChangedFalseCount ?? 0) + 1;
5390
+ }
5177
5391
  }
5178
5392
  const agentWarnings = drainAgentWarnings();
5179
5393
  if (agentWarnings.length > 0) {
@@ -5207,16 +5421,18 @@ export async function runAgentLoop(opts) {
5207
5421
  ...(isError ? { isError: true } : {}),
5208
5422
  ...(isError
5209
5423
  ? { completedSideEffect: false }
5210
- : !actionIsReadOnly
5211
- ? { completedSideEffect: true }
5212
- : {}),
5424
+ : receipt
5425
+ ? { completedSideEffect: receipt.changed }
5426
+ : !actionIsReadOnly
5427
+ ? { completedSideEffect: true }
5428
+ : {}),
5213
5429
  ...(mcpApp ? { mcpApp } : {}),
5214
5430
  ...(resolvedChatUI ? { chatUI: resolvedChatUI.chatUI } : {}),
5215
5431
  ...(resolvedChatUI ? { chatUIResult: resolvedChatUI.result } : {}),
5216
5432
  ...(fileMutation ? { fileMutation } : {}),
5217
5433
  ...(toolArtifacts.length > 0 ? { artifacts: toolArtifacts } : {}),
5218
5434
  });
5219
- recordToolResult(result, isError, toolArtifacts);
5435
+ recordToolResult(result, isError, toolArtifacts, receipt);
5220
5436
  if (!isError) {
5221
5437
  noteToolCallSucceeded(actionEntry);
5222
5438
  if (cacheKey) {
@@ -5675,26 +5891,7 @@ export async function runAgentLoopWithMainChatInternalContinuations(opts) {
5675
5891
  engineName: opts.engine.name,
5676
5892
  model: opts.model,
5677
5893
  };
5678
- const addUsage = (next) => {
5679
- usage.inputTokens += next.inputTokens;
5680
- usage.outputTokens += next.outputTokens;
5681
- usage.cacheReadTokens += next.cacheReadTokens;
5682
- usage.cacheWriteTokens += next.cacheWriteTokens;
5683
- if (next.builderCreditsUsed !== undefined) {
5684
- usage.builderCreditsUsed =
5685
- (usage.builderCreditsUsed ?? 0) + next.builderCreditsUsed;
5686
- }
5687
- usage.engineName = next.engineName ?? usage.engineName;
5688
- usage.model = next.model;
5689
- if (typeof next.llmCalls === "number") {
5690
- usage.llmCalls = (usage.llmCalls ?? 0) + next.llmCalls;
5691
- }
5692
- if (next.usageReported)
5693
- usage.usageReported = true;
5694
- // Keep the earliest attempt's first event — a later continuation
5695
- // attempt starting fresh must not overwrite genuine first-token timing.
5696
- usage.firstEngineEventAtMs ??= next.firstEngineEventAtMs;
5697
- };
5894
+ const addUsage = (next) => mergeAgentLoopUsage(usage, next);
5698
5895
  const budgetStartedAt = opts.budgetStartedAt ?? Date.now();
5699
5896
  const resumeResumableErrorsInProcess = opts.resumeResumableErrorsInProcess === true;
5700
5897
  const maxContinuations = typeof opts.maxContinuations === "number" && opts.maxContinuations > 0
@@ -6832,21 +7029,14 @@ export function createProductionAgentHandler(options) {
6832
7029
  const storedModel = !requestModelIsExplicit && !configuredModelIsExplicit
6833
7030
  ? await getStoredModelForEngine(engine, { appId: options.appId })
6834
7031
  : undefined;
6835
- const modelSelection = resolveAgentModelSelection({
7032
+ const modelSelection = await applyUncheckedDefaultModelReplacement(engine, resolveAgentModelSelection({
6836
7033
  requestModel,
6837
7034
  configuredModel,
6838
7035
  storedModel,
6839
7036
  defaultModel: engine.defaultModel,
6840
- });
6841
- // Only the engine default yields to the provider's checked models. A model
6842
- // the request or a stored default names still runs after it is unchecked,
6843
- // so chats already on it keep working.
6844
- const modelCandidate = modelSelection.source === "default"
6845
- ? ((await resolveUncheckedDefaultModelReplacement(engine)) ??
6846
- modelSelection.model)
6847
- : modelSelection.model;
7037
+ }));
6848
7038
  workerStep("model_done");
6849
- const model = normalizeModelForEngine(engine, modelCandidate);
7039
+ const model = normalizeModelForEngine(engine, modelSelection.model);
6850
7040
  let effectiveModel = model;
6851
7041
  let modelSelectionSource = modelSelection.source;
6852
7042
  const turnUsageLabel = normalizeUsageLabel(body.usageLabel);