@agent-native/core 0.219.0 → 0.220.0-nightly-20261023212848

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 (97) hide show
  1. package/dist/action.d.ts +23 -3
  2. package/dist/action.js +58 -3
  3. package/dist/agent/abort-reasons.d.ts +6 -0
  4. package/dist/agent/abort-reasons.js +6 -0
  5. package/dist/agent/execution-fence.js +5 -1
  6. package/dist/agent/production-agent.d.ts +22 -6
  7. package/dist/agent/production-agent.js +136 -37
  8. package/dist/agent/run-manager.js +92 -44
  9. package/dist/agent/run-store.d.ts +1 -0
  10. package/dist/agent/run-store.js +260 -31
  11. package/dist/agent/run-terminal-tracking.d.ts +29 -0
  12. package/dist/agent/run-terminal-tracking.js +56 -0
  13. package/dist/agent/run-turn-id-header.d.ts +9 -0
  14. package/dist/agent/run-turn-id-header.js +13 -0
  15. package/dist/agent/types.d.ts +11 -0
  16. package/dist/chat-threads/store.d.ts +23 -6
  17. package/dist/chat-threads/store.js +119 -36
  18. package/dist/cli/amplify-stream.js +1 -1
  19. package/dist/client/agent-chat/index.d.ts +2 -1
  20. package/dist/client/agent-chat/index.js +2 -1
  21. package/dist/client/agent-chat.d.ts +2 -0
  22. package/dist/client/agent-chat.js +31 -5
  23. package/dist/client/chat/agentkit-agent-native.js +8 -1
  24. package/dist/client/chat/agentkit-protocol.js +7 -1
  25. package/dist/client/chat/run-outcome-telemetry.js +1 -0
  26. package/dist/client/chat/run-outcome.d.ts +5 -0
  27. package/dist/client/chat/runtime.d.ts +34 -1
  28. package/dist/client/chat/runtime.js +96 -12
  29. package/dist/client/chat/turn-submitted-telemetry.d.ts +4 -0
  30. package/dist/client/chat/turn-submitted-telemetry.js +39 -0
  31. package/dist/client/chat/turn-submitted.d.ts +39 -0
  32. package/dist/client/chat/turn-submitted.js +16 -0
  33. package/dist/client/route-chunk-recovery.d.ts +3 -8
  34. package/dist/client/route-chunk-recovery.js +11 -9
  35. package/dist/client/use-chat-thread-list.d.ts +2 -0
  36. package/dist/client/use-chat-thread-list.js +48 -2
  37. package/dist/client/use-chat-threads.d.ts +13 -0
  38. package/dist/client/use-chat-threads.js +59 -4
  39. package/dist/collab/routes.d.ts +2 -2
  40. package/dist/db/client.d.ts +1 -0
  41. package/dist/db/client.js +28 -18
  42. package/dist/db/schema-snapshot.generated.js +5 -0
  43. package/dist/identity/rekey.d.ts +2 -0
  44. package/dist/identity/rekey.js +19 -0
  45. package/dist/index.d.ts +1 -1
  46. package/dist/index.js +1 -1
  47. package/dist/mcp/agent-connected.d.ts +5 -0
  48. package/dist/mcp/agent-connected.js +10 -3
  49. package/dist/mcp/analytics.d.ts +14 -1
  50. package/dist/mcp/analytics.js +54 -2
  51. package/dist/mcp/embed-app.js +2 -1
  52. package/dist/mcp/oauth-route.js +56 -1
  53. package/dist/notifications/routes.d.ts +1 -1
  54. package/dist/observability/routes.d.ts +7 -7
  55. package/dist/observability/traces.d.ts +2 -0
  56. package/dist/observability/traces.js +11 -2
  57. package/dist/provider-api/actions/custom-provider-registration.d.ts +16 -16
  58. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  59. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  60. package/dist/resources/handlers.d.ts +1 -1
  61. package/dist/review/index.d.ts +1 -1
  62. package/dist/review/index.js +1 -1
  63. package/dist/review/suggestions/actions.js +25 -17
  64. package/dist/review/suggestions/store.d.ts +14 -1
  65. package/dist/review/suggestions/store.js +16 -3
  66. package/dist/review/suggestions/types.d.ts +15 -1
  67. package/dist/scripts/chat/search-chats.js +14 -2
  68. package/dist/server/agent-chat/browser-team-tools.js +39 -6
  69. package/dist/server/agent-chat/script-entries.js +44 -9
  70. package/dist/server/agent-chat-plugin.d.ts +2 -0
  71. package/dist/server/agent-chat-plugin.js +66 -16
  72. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  73. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  74. package/dist/server/first-touch-route.d.ts +2 -2
  75. package/dist/shared/route-chunk-recovery-bootstrap.d.ts +9 -0
  76. package/dist/shared/route-chunk-recovery-bootstrap.js +25 -0
  77. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  78. package/dist/templates/chat/.agents/skills/actions/SKILL.md +1 -0
  79. package/dist/templates/default/.agents/skills/actions/SKILL.md +1 -0
  80. package/dist/templates/factory/.agents/skills/review-prs/SKILL.md +27 -3
  81. package/dist/templates/headless/.agents/skills/actions/SKILL.md +1 -0
  82. package/dist/templates/workspace-core/.agents/skills/actions/SKILL.md +1 -0
  83. package/dist/templates/workspace-core/.agents/skills/external-agents/references/mcp-apps-embedding.md +12 -5
  84. package/dist/tracking/providers.js +3 -0
  85. package/dist/triggers/actions/manage-automation.d.ts +2 -2
  86. package/docs/content/external-agents.mdx +1 -1
  87. package/docs/content/mcp-apps.mdx +3 -3
  88. package/docs/content/mcp-protocol.mdx +1 -1
  89. package/docs/content/sharing.mdx +1 -0
  90. package/docs/content/template-assets-integrations.mdx +5 -3
  91. package/package.json +3 -3
  92. package/src/templates/chat/.agents/skills/actions/SKILL.md +1 -0
  93. package/src/templates/default/.agents/skills/actions/SKILL.md +1 -0
  94. package/src/templates/factory/.agents/skills/review-prs/SKILL.md +27 -3
  95. package/src/templates/headless/.agents/skills/actions/SKILL.md +1 -0
  96. package/src/templates/workspace-core/.agents/skills/actions/SKILL.md +1 -0
  97. package/src/templates/workspace-core/.agents/skills/external-agents/references/mcp-apps-embedding.md +12 -5
@@ -9,7 +9,7 @@ import { parseA2AAgentActivityPart } from "../a2a/activity.js";
9
9
  import { actionCallIsRetrySafe, actionChangeResource, classifyActionCall, } from "../action-call-classification.js";
10
10
  import { runOutsideActionInvocation, vouchedOutcome } from "../action-frame.js";
11
11
  import { ACTION_CHAT_UI_RECORD_CHANGE_RENDERER, normalizeActionChangeResult, } from "../action-ui.js";
12
- import { AgentConnectionRequiredError, describeToolParameterSignature, isActionContractError, isActionHiddenFromEveryAgentSurface, isAgentActionStopError, isAgentConnectionRequiredError, actionCallIntent, invocationEffect, runPreparedActionCall, boundActionCallIntent, preparedCallArguments, preparedCallNeedsApproval, recordInvalidActionCall, stripUnsupportedSchemaKeywords, } from "../action.js";
12
+ import { AgentConnectionRequiredError, describeToolParameterSignature, isActionContractError, isActionHiddenFromEveryAgentSurface, isAgentActionStopError, isAgentConnectionRequiredError, actionCallIntent, ActionInputValidationError, invocationEffect, runPreparedActionCall, boundActionCallIntent, preparedCallArguments, preparedCallNeedsApproval, recordInvalidActionCall, stripUnsupportedSchemaKeywords, } from "../action.js";
13
13
  import { getAppConfig } from "../app-config/index.js";
14
14
  import { MAX_BACKGROUND_RUN_CONTINUATIONS, MAX_CONSECUTIVE_NO_PROGRESS_CONTINUATIONS, MAX_TURN_WALL_CLOCK_MS, } from "../app-config/run-lifecycle-invariants.js";
15
15
  import { readAppState } from "../application-state/script-helpers.js";
@@ -90,6 +90,7 @@ import { ProcessorChain, TripWire, toolCallsFromContent, } from "./processors.js
90
90
  import { applyUncheckedDefaultModelReplacement } from "./provider-model-selection.js";
91
91
  import { startRun, subscribeToRun, replayCompletedTurn, getActiveRunForThread, getActiveRunForThreadAsync, getRun, abortRun, abortRunDurably, abortRunAndTurnDurably, abortRunAtSafeBoundaryDurably, abortTurnByRefDurably, abortTurnDurably, getSlotHoldingRunId, tryClaimRunSlot, isHostedRuntime, resolveRunSoftTimeoutMs, resolveRunToolTimeoutCeilingMs, endsAfterToolResultWithoutAssistantFinal, endsDuringActionPreparation, } from "./run-manager.js";
92
92
  import { writeLedgerEntry, readLedgerEntry, beginAgentExecutionStep, completeAgentExecutionStep, markAgentExecutionStepDispatched, markAgentExecutionStepUnknown, recordLateAgentExecutionStepOutcome, clearLedgerForThread, AgentTurnInitiatorMismatchError, AgentTurnInitiatorUnavailableError, insertRun, insertRunEvent, isRunAborted, isTurnAborted, markRunAborted, updateRunHeartbeat, updateRunStatusIfRunning, updateRunTerminalStateIfRunning, setRunError, setRunTerminalReason, claimBackgroundRun, readBackgroundRunClaim, recordRunDiagnostic, countRunsForTurn, RUN_DIAG_STAGE, UNCLAIMED_BACKGROUND_RUN_GRACE_MS, turnRunLedgerExhausted, } from "./run-store.js";
93
+ import { setRunTurnIdHeader } from "./run-turn-id-header.js";
93
94
  import { buildCurrentTimeUserContext } from "./runtime-context.js";
94
95
  import { claimSetupResume, setupResumeRefusedRunId, } from "./setup-resume-claim.js";
95
96
  import { getStrictToolInputValidator } from "./strict-tool-input.js";
@@ -3130,6 +3131,7 @@ function* historyToolResults(turnMessages, actions, readOnlyCalls, priorCallIden
3130
3131
  : {}),
3131
3132
  readOnly: readOnlyCalls.has(callPart),
3132
3133
  dedupe: actions[callPart.name]?.dedupe !== false,
3134
+ input: callPart.input,
3133
3135
  },
3134
3136
  };
3135
3137
  }
@@ -3323,23 +3325,42 @@ const LOOP_BREAKER_STOP_CODES = new Set([
3323
3325
  function isLoopBreakerStop(stop) {
3324
3326
  return LOOP_BREAKER_STOP_CODES.has(stop.errorCode ?? "");
3325
3327
  }
3326
- function noteUnknownOutcome(outcomes, tool, argumentsHash) {
3328
+ function noteUnknownOutcome(outcomes, tool, argumentsHash, cause = "interruption", input) {
3327
3329
  outcomes.tools.set(tool, (outcomes.tools.get(tool) ?? 0) + 1);
3328
- if (!argumentsHash)
3330
+ outcomes.causes.set(tool, cause);
3331
+ if (input !== undefined) {
3332
+ const inputKey = toolCallCacheKey(tool, input);
3333
+ outcomes.inputs.set(inputKey, (outcomes.inputs.get(inputKey) ?? 0) + 1);
3334
+ }
3335
+ if (!argumentsHash) {
3336
+ outcomes.unidentified.set(tool, (outcomes.unidentified.get(tool) ?? 0) + 1);
3329
3337
  return;
3338
+ }
3330
3339
  const key = preparedToolCallKey(tool, argumentsHash);
3331
3340
  outcomes.calls.set(key, (outcomes.calls.get(key) ?? 0) + 1);
3332
3341
  }
3342
+ /** Why a write of a tool with an unknown earlier call did not run. */
3343
+ function unknownSiblingResult(tool) {
3344
+ return `Not run: an earlier ${tool} call in this turn may have already applied, and its result is unknown. Running ${tool} again could apply the change twice. Check the current state with a read, then tell the user what you found instead of calling ${tool} again in this turn.`;
3345
+ }
3333
3346
  function noUnknownOutcomes() {
3334
- return { tools: new Map(), calls: new Map() };
3347
+ return {
3348
+ tools: new Map(),
3349
+ calls: new Map(),
3350
+ inputs: new Map(),
3351
+ unidentified: new Map(),
3352
+ causes: new Map(),
3353
+ };
3335
3354
  }
3336
3355
  /** Each count in `from` that is larger than the one in `into`. */
3337
3356
  function mergeUnknownOutcomes(into, from) {
3338
- for (const kind of ["tools", "calls"]) {
3357
+ for (const kind of ["tools", "calls", "inputs", "unidentified"]) {
3339
3358
  for (const [key, count] of from[kind]) {
3340
3359
  into[kind].set(key, Math.max(into[kind].get(key) ?? 0, count));
3341
3360
  }
3342
3361
  }
3362
+ for (const [tool, cause] of from.causes)
3363
+ into.causes.set(tool, cause);
3343
3364
  }
3344
3365
  /**
3345
3366
  * Adds to `outcomes` those a continued turn starts with, from its journal's
@@ -3350,7 +3371,7 @@ function mergeUnknownOutcomes(into, from) {
3350
3371
  function seedUnknownOutcomes(outcomes, messages, actions, readOnlyCalls, priorCallEffects, priorCallIdentities, interrupted) {
3351
3372
  const fromJournal = noUnknownOutcomes();
3352
3373
  for (const entry of interrupted) {
3353
- noteUnknownOutcome(fromJournal, entry.tool, entry.argumentsHash);
3374
+ noteUnknownOutcome(fromJournal, entry.tool, entry.argumentsHash, "interruption", entry.input);
3354
3375
  }
3355
3376
  mergeUnknownOutcomes(outcomes, fromJournal);
3356
3377
  if (!isInternalContinuationTurn(messages))
@@ -3379,7 +3400,7 @@ function seedUnknownOutcomes(outcomes, messages, actions, readOnlyCalls, priorCa
3379
3400
  (part.isError === true &&
3380
3401
  typeof part.content === "string" &&
3381
3402
  isToolCallTimeoutResult(part.content)))) {
3382
- noteUnknownOutcome(fromHistory, call.name, effect ? priorCallIdentities.get(call.identityKey ?? "") : undefined);
3403
+ noteUnknownOutcome(fromHistory, call.name, effect ? priorCallIdentities.get(call.identityKey ?? "") : undefined, "interruption", call.input);
3383
3404
  }
3384
3405
  }
3385
3406
  const callsByProviderId = new Map();
@@ -3420,7 +3441,7 @@ function seedUnknownOutcomes(outcomes, messages, actions, readOnlyCalls, priorCa
3420
3441
  }
3421
3442
  }
3422
3443
  for (const call of ambiguousUnknownCalls) {
3423
- noteUnknownOutcome(fromHistory, call.name, undefined);
3444
+ noteUnknownOutcome(fromHistory, call.name, undefined, "interruption", call.input);
3424
3445
  }
3425
3446
  mergeUnknownOutcomes(outcomes, fromHistory);
3426
3447
  }
@@ -4448,6 +4469,20 @@ async function prepareToolCall(entry, input, signal, timeoutMs) {
4448
4469
  function intentIsReadOnly(intent) {
4449
4470
  return intent?.valid === true && intent.readOnly === true;
4450
4471
  }
4472
+ function isBoundedTaskWait(toolName, input) {
4473
+ if (toolName !== "agent-teams" || !input || typeof input !== "object") {
4474
+ return false;
4475
+ }
4476
+ const args = input;
4477
+ // Only these operations honor the task reader's bounded wait contract.
4478
+ return ((args.action === "status" || args.action === "read-result") &&
4479
+ typeof args.taskId === "string" &&
4480
+ args.taskId.trim().length > 0 &&
4481
+ typeof args.waitMs === "number" &&
4482
+ Number.isInteger(args.waitMs) &&
4483
+ args.waitMs > 0 &&
4484
+ args.waitMs <= 20_000);
4485
+ }
4451
4486
  function normalizeOptionalToolPlaceholders(schema, input) {
4452
4487
  if (!schema?.properties || !input || typeof input !== "object") {
4453
4488
  return { input, changed: false };
@@ -4968,21 +5003,28 @@ export async function runAgentLoop(opts) {
4968
5003
  const readOnlyToolResultCache = seedReadOnlyToolResultsFromHistory(messages, actions, historyReadOnlyCalls, priorCallEffects, priorCallIdentities);
4969
5004
  const duplicateReadOnlyToolCalls = seedDuplicateReadOnlyToolCallsFromHistory(messages, actions, historyReadOnlyCalls, priorCallEffects, priorCallIdentities);
4970
5005
  const unknownOutcomes = opts.unknownOutcomes ?? noUnknownOutcomes();
4971
- seedUnknownOutcomes(unknownOutcomes, messages, actions, historyReadOnlyCalls, priorCallEffects, priorCallIdentities, internalContinuationTurn ? (toolCallJournal?.interrupted ?? []) : []);
5006
+ // A recovered retry (a team attempt after a crash, an A2A redelivery)
5007
+ // re-plans from the task text, so a repeated write gets a new step id and
5008
+ // only these journal entries show it may already have run. Approval
5009
+ // continuations stay out: their paused call is interrupted by design.
5010
+ seedUnknownOutcomes(unknownOutcomes, messages, actions, historyReadOnlyCalls, priorCallEffects, priorCallIdentities, internalContinuationTurn || opts.recoverCompletedExecutionSteps === true
5011
+ ? (toolCallJournal?.interrupted ?? [])
5012
+ : []);
4972
5013
  if (internalContinuationTurn) {
4973
5014
  for (const entry of toolCallJournal?.completed ?? []) {
4974
5015
  if (!journalEntriesAmbiguous.has(entry.key))
4975
5016
  continue;
4976
- unknownOutcomes.tools.set(entry.tool, Math.max(unknownOutcomes.tools.get(entry.tool) ?? 0, 1));
4977
- if (entry.argumentsHash) {
4978
- const key = preparedToolCallKey(entry.tool, entry.argumentsHash);
4979
- unknownOutcomes.calls.set(key, Math.max(unknownOutcomes.calls.get(key) ?? 0, 1));
4980
- }
5017
+ const seeded = noUnknownOutcomes();
5018
+ noteUnknownOutcome(seeded, entry.tool, entry.argumentsHash, "interruption", entry.input);
5019
+ mergeUnknownOutcomes(unknownOutcomes, seeded);
4981
5020
  }
4982
5021
  }
4983
- // Names a caller shares between its own loops hold back writes, as before.
5022
+ // Names a caller shares between its own loops carry no identity, so they
5023
+ // hold back every write of the tool.
4984
5024
  for (const tool of opts.unknownWriteToolNames ?? []) {
4985
- unknownOutcomes.tools.set(tool, Math.max(unknownOutcomes.tools.get(tool) ?? 0, 1));
5025
+ const seeded = noUnknownOutcomes();
5026
+ noteUnknownOutcome(seeded, tool, undefined);
5027
+ mergeUnknownOutcomes(unknownOutcomes, seeded);
4986
5028
  }
4987
5029
  const { sameArguments: repeatedToolErrors, sameTool: repeatedToolErrorsAnyArgs, } = seedRepeatedToolErrorCountsFromJournal(journalRead.status === "read" ? journalRead.priorToolCallSequence : [], toolCallCacheKey, normalizeToolErrorForBreaker);
4988
5030
  const repeatedToolCalls = seedRepeatedToolCallCountsFromJournal(journalRead.status === "read" ? journalRead.priorToolCallSequence : [], toolCallCacheKey, (name, result) => result.startsWith(resurfacedDuplicateReadOnlyToolResultPrefix(name)));
@@ -5038,6 +5080,8 @@ export async function runAgentLoop(opts) {
5038
5080
  error: stop.message,
5039
5081
  errorCode: stop.errorCode ?? "tool_failed",
5040
5082
  ...(stop.details ? { details: stop.details } : {}),
5083
+ ...(stop.blockedTool ? { blockedTool: stop.blockedTool } : {}),
5084
+ ...(stop.blockedBy ? { blockedBy: stop.blockedBy } : {}),
5041
5085
  recoverable: false,
5042
5086
  });
5043
5087
  };
@@ -5977,7 +6021,12 @@ export async function runAgentLoop(opts) {
5977
6021
  const actionRetrySafe = actionEntry && intent?.valid
5978
6022
  ? actionCallIsRetrySafe(actionEntry, preparedCallArguments(intent))
5979
6023
  : false;
5980
- const repeatGuardStopFromThisCall = noteRepeatedToolCall(toolCall.name, toolCall.input, toolCall.id);
6024
+ const repeatGuardStopFromThisCall = actionIsReadOnly &&
6025
+ actionEntry?.dedupe === false &&
6026
+ intent?.valid === true &&
6027
+ isBoundedTaskWait(toolCall.name, preparedCallArguments(intent))
6028
+ ? null
6029
+ : noteRepeatedToolCall(toolCall.name, toolCall.input, toolCall.id);
5981
6030
  const rollbackRepeatedToolCall = () => {
5982
6031
  const repeatKey = toolCallCacheKey(toolCall.name, toolCall.input);
5983
6032
  const repeatCall = inFlightRepeatToolCalls.get(toolCall.id);
@@ -6010,6 +6059,7 @@ export async function runAgentLoop(opts) {
6010
6059
  let actionInvoked = false;
6011
6060
  // Live once invoked, so an abandoned call still reads what ran in it.
6012
6061
  const actionExecutionOutcome = { refused: false };
6062
+ let unknownOutcomeCause = "interruption";
6013
6063
  const ranEffect = (settled) => actionInvoked && actionEntry
6014
6064
  ? invocationEffect(actionEntry, actionExecutionOutcome, {
6015
6065
  readOnly: actionIsReadOnly,
@@ -6025,7 +6075,7 @@ export async function runAgentLoop(opts) {
6025
6075
  // holds back its action's writes as a write's would.
6026
6076
  if (event.outcomeUnknown) {
6027
6077
  opts.unknownWriteToolNames?.add(toolCall.name);
6028
- noteUnknownOutcome(unknownOutcomes, toolCall.name, argumentsHash);
6078
+ noteUnknownOutcome(unknownOutcomes, toolCall.name, argumentsHash, unknownOutcomeCause, toolCall.input);
6029
6079
  }
6030
6080
  // A write that errored after it was invoked may still have committed,
6031
6081
  // retry-safe or not, so cached reads of its target are stale.
@@ -6188,7 +6238,7 @@ export async function runAgentLoop(opts) {
6188
6238
  : "";
6189
6239
  return `Error running ${toolCall.name}: ${message}${errorCode}${rateLimitRecoveryHint(message, err)}`;
6190
6240
  };
6191
- const declineToolCall = (guidance, isError = false) => {
6241
+ const declineToolCall = (guidance, isError = false, errorClass) => {
6192
6242
  const result = finalizeToolErrorResult(guidance);
6193
6243
  return (async () => {
6194
6244
  await send({
@@ -6205,6 +6255,7 @@ export async function runAgentLoop(opts) {
6205
6255
  result,
6206
6256
  isError: true,
6207
6257
  completedSideEffect: false,
6258
+ ...(errorClass ? { errorClass } : {}),
6208
6259
  });
6209
6260
  recordToolResult(result, true);
6210
6261
  return {
@@ -6296,11 +6347,11 @@ export async function runAgentLoop(opts) {
6296
6347
  }
6297
6348
  const toolCallSchemaError = toolCallSchemaErrorsByCall.get(toolCall);
6298
6349
  if (toolCallSchemaError && !repairedInput.changed) {
6299
- return declineToolCall(toolInputSchemaErrorResult(toolCall.name, toolCallSchemaError.input, toolCallSchemaError.error, actionEntry.tool.parameters, toolCallTruncatedByOutputCap), true);
6350
+ return declineToolCall(toolInputSchemaErrorResult(toolCall.name, toolCallSchemaError.input, toolCallSchemaError.error, actionEntry.tool.parameters, toolCallTruncatedByOutputCap), true, "input_invalid");
6300
6351
  }
6301
6352
  const rawToolInputError = validateRawToolInput(actionEntry, toolCall.input);
6302
6353
  if (rawToolInputError) {
6303
- return declineToolCall(toolInputSchemaErrorResult(toolCall.name, toolCall.input, rawToolInputError, actionEntry.tool.parameters, toolCallTruncatedByOutputCap), true);
6354
+ return declineToolCall(toolInputSchemaErrorResult(toolCall.name, toolCall.input, rawToolInputError, actionEntry.tool.parameters, toolCallTruncatedByOutputCap), true, "input_invalid");
6304
6355
  }
6305
6356
  if (!intent?.valid) {
6306
6357
  await recordInvalidActionCall(actionEntry, toolCall.input, {
@@ -6309,7 +6360,7 @@ export async function runAgentLoop(opts) {
6309
6360
  userEmail: userEmail ?? undefined,
6310
6361
  orgId: opts.orgId ?? getRequestOrgId() ?? null,
6311
6362
  }, intent?.error);
6312
- return declineToolCall(thrownToolResult(intent?.error), true);
6363
+ return declineToolCall(thrownToolResult(intent?.error), true, "input_invalid");
6313
6364
  }
6314
6365
  const approvalKey = toolCallCacheKey(toolCall.name, toolCall.input);
6315
6366
  // The exact arguments the call runs with, defaults applied, bind its
@@ -6601,6 +6652,8 @@ export async function runAgentLoop(opts) {
6601
6652
  requestedActionStop ??= {
6602
6653
  message: result,
6603
6654
  errorCode: "write_tool_outcome_unknown",
6655
+ blockedTool: toolCall.name,
6656
+ blockedBy: "interruption",
6604
6657
  };
6605
6658
  return {
6606
6659
  type: "tool-result",
@@ -6670,11 +6723,13 @@ export async function runAgentLoop(opts) {
6670
6723
  };
6671
6724
  }
6672
6725
  }
6673
- // An unknown write cannot become fresh work merely by rewording its
6674
- // arguments, or by eliding its tool pair from the replayed history.
6726
+ // Running an unknown call again exactly as it was prepared could apply
6727
+ // it twice. A write with no identity to compare cannot be told from a
6728
+ // repeat, whatever its arguments, nor can one whose tool pair was
6729
+ // elided from the replayed history.
6675
6730
  const priorInterruptionsBeforeStepReplay = actionIsReadOnly
6676
6731
  ? (unknownOutcomes.calls.get(preparedCallKey) ?? 0)
6677
- : (unknownOutcomes.tools.get(toolCall.name) ?? 0);
6732
+ : Math.max(unknownOutcomes.calls.get(preparedCallKey) ?? 0, unknownOutcomes.inputs.get(toolCallCacheKey(toolCall.name, toolCall.input)) ?? 0, unknownOutcomes.unidentified.get(toolCall.name) ?? 0);
6678
6733
  const priorInterruptions = ambiguousLegacyWrite
6679
6734
  ? Math.max(1, priorInterruptionsBeforeStepReplay)
6680
6735
  : (interruptedCallMatchesStep ||
@@ -6683,10 +6738,20 @@ export async function runAgentLoop(opts) {
6683
6738
  priorInterruptionsBeforeStepReplay > 0
6684
6739
  ? 0
6685
6740
  : priorInterruptionsBeforeStepReplay;
6741
+ // Another write of a tool whose earlier call may have applied could
6742
+ // apply the same change twice, so it does not run; the turn goes on,
6743
+ // and only an exact repeat stops it.
6744
+ if (!actionIsReadOnly &&
6745
+ !actionRetrySafe &&
6746
+ priorInterruptionsBeforeStepReplay === 0 &&
6747
+ (unknownOutcomes.tools.get(toolCall.name) ?? 0) > 0) {
6748
+ return declineToolCall(unknownSiblingResult(toolCall.name), true);
6749
+ }
6686
6750
  if (priorInterruptions > 0) {
6687
- // Only a result recorded for these exact arguments is recovered. Any
6688
- // other leaves the earlier attempt unknown, so the call stops below
6689
- // instead of running again.
6751
+ // Only a result recorded for these exact arguments is recovered, so
6752
+ // a result found is the interrupted call's own. Any other leaves the
6753
+ // earlier attempt unknown, so the call stops below instead of
6754
+ // running again.
6690
6755
  const ledgerResult = !ambiguousLegacyWrite && opts.threadId && !executionStepIntent
6691
6756
  ? await waitForInterruptedToolLedgerEntry({
6692
6757
  threadId: opts.threadId,
@@ -6763,6 +6828,8 @@ export async function runAgentLoop(opts) {
6763
6828
  message: `I stopped before running ${toolCall.name} again. ${priorInterruptions === 1 ? "An earlier attempt" : `${priorInterruptions} earlier attempts`} may have already saved changes and I could not confirm the result. ` +
6764
6829
  `Check what was saved, then reply "continue" and I will verify it before doing anything else.`,
6765
6830
  errorCode: "write_tool_outcome_unknown",
6831
+ blockedTool: toolCall.name,
6832
+ blockedBy: unknownOutcomes.causes.get(toolCall.name) ?? "interruption",
6766
6833
  };
6767
6834
  return {
6768
6835
  type: "tool-result",
@@ -6882,6 +6949,8 @@ export async function runAgentLoop(opts) {
6882
6949
  // commit, so a retry-safe action stays blocked for it; an error the
6883
6950
  // handler returned does not.
6884
6951
  let toolResultLost = false;
6952
+ // It returned an error result rather than throwing one.
6953
+ let toolReportedError = false;
6885
6954
  let toolErrorCode;
6886
6955
  let toolErrorDetailsLine = "";
6887
6956
  let mcpApp;
@@ -7070,6 +7139,7 @@ export async function runAgentLoop(opts) {
7070
7139
  typeof mcpResult.raw === "object" &&
7071
7140
  mcpResult.raw.isError === true) {
7072
7141
  isError = true;
7142
+ toolReportedError = true;
7073
7143
  }
7074
7144
  mcpApp = mcpResult?.mcpApp;
7075
7145
  let resultForAgent = rawForAgent;
@@ -7189,6 +7259,7 @@ export async function runAgentLoop(opts) {
7189
7259
  throw err;
7190
7260
  }
7191
7261
  thrown = err;
7262
+ toolReportedError = false;
7192
7263
  toolResultLost =
7193
7264
  signal.aborted ||
7194
7265
  isToolCallTimeoutResult(String(err?.message ?? err));
@@ -7203,8 +7274,21 @@ export async function runAgentLoop(opts) {
7203
7274
  }
7204
7275
  const call = actionExecutionOutcome.record;
7205
7276
  const { effect: callEffect, changed: callChanged } = ranEffect(isError
7206
- ? { failed: { error: thrown, lost: toolResultLost } }
7277
+ ? {
7278
+ failed: {
7279
+ error: thrown,
7280
+ lost: toolResultLost,
7281
+ reported: toolReportedError,
7282
+ },
7283
+ }
7207
7284
  : { receipt });
7285
+ const toolErrorClass = !isError
7286
+ ? undefined
7287
+ : thrown instanceof ActionInputValidationError
7288
+ ? "input_invalid"
7289
+ : callEffect === "none" && !actionIsReadOnly
7290
+ ? "rejected"
7291
+ : undefined;
7208
7292
  if (isError) {
7209
7293
  receipt = undefined;
7210
7294
  if (result !== INTERRUPTED_TOOL_RESULT_MARKER) {
@@ -7285,6 +7369,7 @@ export async function runAgentLoop(opts) {
7285
7369
  executionStepOutcomeRecorded = true;
7286
7370
  }
7287
7371
  toolDonePersistenceFailed = true;
7372
+ unknownOutcomeCause = toolResultLost ? "interruption" : "rejection";
7288
7373
  await emitToolDone({
7289
7374
  type: "tool_done",
7290
7375
  id: toolCall.id,
@@ -7293,6 +7378,7 @@ export async function runAgentLoop(opts) {
7293
7378
  result,
7294
7379
  ...(isError ? { isError: true } : {}),
7295
7380
  ...(toolErrorCode ? { errorCode: toolErrorCode } : {}),
7381
+ ...(toolErrorClass ? { errorClass: toolErrorClass } : {}),
7296
7382
  ...(callEffect === "unknown"
7297
7383
  ? { outcomeUnknown: true }
7298
7384
  : isError || receipt || callEffect === "committed"
@@ -9398,6 +9484,7 @@ export function createProductionAgentHandler(options) {
9398
9484
  setResponseHeader(event, "Cache-Control", "no-cache");
9399
9485
  setResponseHeader(event, "Connection", "keep-alive");
9400
9486
  setResponseHeader(event, "X-Run-Id", result.submittedRunId);
9487
+ setRunTurnIdHeader(event, getRun(result.submittedRunId)?.turnId);
9401
9488
  setResponseHeader(event, "X-Dispatch-Mode", "replay");
9402
9489
  return stream;
9403
9490
  }
@@ -9486,6 +9573,7 @@ export function createProductionAgentHandler(options) {
9486
9573
  setResponseHeader(event, "Cache-Control", "no-cache");
9487
9574
  setResponseHeader(event, "Connection", "keep-alive");
9488
9575
  setResponseHeader(event, "X-Run-Id", submittedRunId);
9576
+ setRunTurnIdHeader(event, getRun(submittedRunId)?.turnId);
9489
9577
  setResponseHeader(event, "X-Dispatch-Mode", "replay");
9490
9578
  return stream;
9491
9579
  }
@@ -10780,6 +10868,11 @@ export function createProductionAgentHandler(options) {
10780
10868
  !queuedMessageId &&
10781
10869
  !queuedMessageClaimId &&
10782
10870
  !isBackgroundWorker);
10871
+ const queuedSubmissionStore = shouldPreserveQueuedOrder
10872
+ ? await import("../chat-threads/store.js")
10873
+ : undefined;
10874
+ // The inbox read runs under the slot lock, but its schema must be ready first.
10875
+ await queuedSubmissionStore?.ensureChatThreadTables();
10783
10876
  slot = await tryClaimRunSlot(threadId, runId, undefined, {
10784
10877
  turnId: effectiveTurnId,
10785
10878
  ...(turnInitiator ? { turnInitiator } : {}),
@@ -10795,12 +10888,9 @@ export function createProductionAgentHandler(options) {
10795
10888
  ? { dispatchPayload: durableDispatchPayload }
10796
10889
  : {}),
10797
10890
  ...(continueOf ? { continueOf } : {}),
10798
- ...(shouldPreserveQueuedOrder
10891
+ ...(queuedSubmissionStore
10799
10892
  ? {
10800
- hasPendingQueuedSubmission: async (tx) => {
10801
- const { hasPendingQueuedSubmissionReceipt } = await import("../chat-threads/store.js");
10802
- return hasPendingQueuedSubmissionReceipt(tx, runThreadId);
10803
- },
10893
+ hasPendingQueuedSubmission: (tx) => queuedSubmissionStore.hasPendingQueuedSubmissionReceipt(tx, runThreadId),
10804
10894
  }
10805
10895
  : {}),
10806
10896
  });
@@ -10833,6 +10923,9 @@ export function createProductionAgentHandler(options) {
10833
10923
  setResponseHeader(event, "Cache-Control", "no-cache");
10834
10924
  setResponseHeader(event, "Connection", "keep-alive");
10835
10925
  setResponseHeader(event, "X-Run-Id", slot.completedRunId);
10926
+ // The client leaves a replay's turn out unless named here, and this
10927
+ // replay was found by its turn, so the stored turn is known.
10928
+ setRunTurnIdHeader(event, effectiveTurnId);
10836
10929
  setResponseHeader(event, "X-Dispatch-Mode", "replay");
10837
10930
  return stream;
10838
10931
  }
@@ -11074,6 +11167,7 @@ export function createProductionAgentHandler(options) {
11074
11167
  setResponseHeader(event, "Cache-Control", "no-cache");
11075
11168
  setResponseHeader(event, "Connection", "keep-alive");
11076
11169
  setResponseHeader(event, "X-Run-Id", duplicateRunId);
11170
+ setRunTurnIdHeader(event, getRun(duplicateRunId)?.turnId);
11077
11171
  setResponseHeader(event, "X-Dispatch-Mode", "replay");
11078
11172
  return stream;
11079
11173
  }
@@ -11174,6 +11268,7 @@ export function createProductionAgentHandler(options) {
11174
11268
  setResponseHeader(event, "Cache-Control", "no-cache");
11175
11269
  setResponseHeader(event, "Connection", "keep-alive");
11176
11270
  setResponseHeader(event, "X-Run-Id", runId);
11271
+ setRunTurnIdHeader(event, effectiveTurnId);
11177
11272
  setResponseHeader(event, "X-Dispatch-Mode", "background");
11178
11273
  return stream;
11179
11274
  }
@@ -11503,7 +11598,7 @@ export function createProductionAgentHandler(options) {
11503
11598
  void retainRequestDbPoolScope(recordRunDiagnostic(runId, RUN_DIAG_STAGE.setupTimings, setupDetail).catch(() => { })).catch(() => { });
11504
11599
  }
11505
11600
  if (options.onRunStart) {
11506
- await options.onRunStart(send, threadId ?? runId, runId);
11601
+ await options.onRunStart(send, threadId ?? runId, runId, effectiveTurnId);
11507
11602
  }
11508
11603
  const unknownOutcomes = noUnknownOutcomes();
11509
11604
  if (customAgentRefs.length > 0) {
@@ -11938,6 +12033,7 @@ export function createProductionAgentHandler(options) {
11938
12033
  loopOpts: agentLoopOpts,
11939
12034
  runId,
11940
12035
  threadId: threadId ?? null,
12036
+ turnId: effectiveTurnId,
11941
12037
  userId: ownerEmail,
11942
12038
  config: obsConfig,
11943
12039
  spanName: turnUsageLabel && turnUsageLabel !== "chat"
@@ -12047,10 +12143,12 @@ export function createProductionAgentHandler(options) {
12047
12143
  : "foreground",
12048
12144
  runRowAlreadyInserted: foregroundRunRowInserted,
12049
12145
  executionLease: true,
12146
+ // An anonymous run is not a user; `run_started` does not name one either.
12147
+ userId: turnInitiator?.anonymous
12148
+ ? undefined
12149
+ : (ownerEmail ?? undefined),
12050
12150
  // Resolved AFTER stored-model/experiment overrides — the same value
12051
12151
  // actually sent to the engine, not the raw client-requested model.
12052
- // No userId here: `ownerEmail` is the only identity known at this
12053
- // scope and is PII (email), which the terminal event must not carry.
12054
12152
  model: effectiveModel,
12055
12153
  engineName: engine.name,
12056
12154
  attemptCount: backgroundContinuationCount,
@@ -12068,6 +12166,7 @@ export function createProductionAgentHandler(options) {
12068
12166
  setResponseHeader(event, "Cache-Control", "no-cache");
12069
12167
  setResponseHeader(event, "Connection", "keep-alive");
12070
12168
  setResponseHeader(event, "X-Run-Id", runId);
12169
+ setRunTurnIdHeader(event, effectiveTurnId);
12071
12170
  setResponseHeader(event, "X-Dispatch-Mode", foregroundSelfChainEligible ? "foreground-self-chain" : "foreground");
12072
12171
  return stream;
12073
12172
  });