@agent-native/core 0.204.0 → 0.204.1-nightly-20261008065100

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 (139) hide show
  1. package/dist/action.js +5 -6
  2. package/dist/agent/auto-continue.d.ts +35 -0
  3. package/dist/agent/auto-continue.js +38 -0
  4. package/dist/agent/connection-required-note.d.ts +52 -0
  5. package/dist/agent/connection-required-note.js +187 -0
  6. package/dist/agent/jev-tool-prefetch.d.ts +5 -1
  7. package/dist/agent/jev-tool-prefetch.js +19 -6
  8. package/dist/agent/previous-turn-note.d.ts +9 -0
  9. package/dist/agent/previous-turn-note.js +48 -0
  10. package/dist/agent/production-agent.d.ts +17 -13
  11. package/dist/agent/production-agent.js +185 -43
  12. package/dist/agent/reaped-run-thread.d.ts +13 -0
  13. package/dist/agent/reaped-run-thread.js +45 -0
  14. package/dist/agent/run-store.d.ts +29 -5
  15. package/dist/agent/run-store.js +144 -30
  16. package/dist/agent/thread-data-builder.d.ts +16 -0
  17. package/dist/agent/thread-data-builder.js +30 -8
  18. package/dist/agent/thread-message-projection.js +5 -3
  19. package/dist/agent/tool-search.js +5 -1
  20. package/dist/agent/types.d.ts +3 -0
  21. package/dist/audit/config.d.ts +6 -0
  22. package/dist/audit/config.js +12 -1
  23. package/dist/audit/index.d.ts +1 -1
  24. package/dist/audit/index.js +1 -1
  25. package/dist/audit/store.js +39 -4
  26. package/dist/automation/index.d.ts +1 -1
  27. package/dist/cli/skills.d.ts +1 -0
  28. package/dist/cli/skills.js +1 -1
  29. package/dist/client/agent-chat/index.d.ts +1 -1
  30. package/dist/client/agent-chat/index.js +1 -1
  31. package/dist/client/chat/agentkit-agent-native.js +83 -7
  32. package/dist/client/chat/agentkit-protocol.js +140 -67
  33. package/dist/client/chat/runtime.js +10 -4
  34. package/dist/client/embed-auth.d.ts +15 -0
  35. package/dist/client/embed-auth.js +77 -7
  36. package/dist/client/host/index.d.ts +1 -1
  37. package/dist/client/host/index.js +1 -1
  38. package/dist/client/mcp-app-host.d.ts +19 -0
  39. package/dist/client/mcp-app-host.js +88 -2
  40. package/dist/client/use-chat-threads.d.ts +1 -1
  41. package/dist/client/use-chat-threads.js +2 -0
  42. package/dist/client/use-run-stuck-detection.d.ts +18 -1
  43. package/dist/client/use-run-stuck-detection.js +115 -75
  44. package/dist/collab/routes.d.ts +1 -1
  45. package/dist/collab/struct-routes.d.ts +1 -1
  46. package/dist/db/request-telemetry.d.ts +7 -1
  47. package/dist/db/request-telemetry.js +20 -1
  48. package/dist/deploy/build.d.ts +25 -1
  49. package/dist/deploy/build.js +176 -3
  50. package/dist/deploy/workspace-deploy.js +8 -3
  51. package/dist/extensions/fetch-tool.js +50 -14
  52. package/dist/file-upload/builder.js +1 -1
  53. package/dist/file-upload/s3.js +1 -1
  54. package/dist/file-upload/types.d.ts +1 -0
  55. package/dist/integrations/adapters/email.js +11 -3
  56. package/dist/integrations/adapters/telegram.js +4 -2
  57. package/dist/jobs/actions/automation-no-op.d.ts +12 -0
  58. package/dist/jobs/actions/automation-no-op.js +20 -0
  59. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  60. package/dist/jobs/background-automation-runner.d.ts +8 -1
  61. package/dist/jobs/background-automation-runner.js +102 -19
  62. package/dist/jobs/run-history.d.ts +1 -1
  63. package/dist/jobs/run-history.js +8 -4
  64. package/dist/jobs/scheduler.js +25 -12
  65. package/dist/localization/automation-outcome-messages.d.ts +22 -0
  66. package/dist/localization/automation-outcome-messages.js +90 -0
  67. package/dist/localization/default-messages.d.ts +2 -0
  68. package/dist/localization/default-messages.js +4 -2
  69. package/dist/mcp/build-server.js +22 -11
  70. package/dist/mcp/embed-app.js +143 -9
  71. package/dist/mcp/index.d.ts +1 -1
  72. package/dist/mcp/index.js +1 -1
  73. package/dist/observability/metrics.d.ts +3 -0
  74. package/dist/observability/metrics.js +10 -0
  75. package/dist/observability/migrations.js +22 -0
  76. package/dist/observability/routes.d.ts +10 -5
  77. package/dist/observability/routes.js +88 -14
  78. package/dist/observability/sentiment.d.ts +5 -0
  79. package/dist/observability/sentiment.js +75 -25
  80. package/dist/observability/store.d.ts +28 -0
  81. package/dist/observability/store.js +58 -0
  82. package/dist/observability/traces.js +37 -5
  83. package/dist/org/handlers.d.ts +1 -1
  84. package/dist/progress/actions.js +1 -0
  85. package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
  86. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  87. package/dist/provider-api/actions/provider-api.js +6 -2
  88. package/dist/provider-api/index.d.ts +1 -0
  89. package/dist/provider-api/index.js +18 -0
  90. package/dist/provider-api/result-outcome.d.ts +1 -0
  91. package/dist/provider-api/result-outcome.js +20 -0
  92. package/dist/provider-api/staging.js +10 -9
  93. package/dist/resources/handlers.d.ts +1 -1
  94. package/dist/resources/script-helpers.d.ts +1 -0
  95. package/dist/resources/script-helpers.js +1 -1
  96. package/dist/resources/store.js +81 -0
  97. package/dist/scripts/resources/save-memory.js +3 -0
  98. package/dist/server/action-routes.js +34 -6
  99. package/dist/server/agent-chat/prompt-resources.d.ts +8 -2
  100. package/dist/server/agent-chat/prompt-resources.js +31 -8
  101. package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +19 -4
  102. package/dist/server/agent-chat/recurring-jobs-runtime.js +52 -16
  103. package/dist/server/agent-chat/script-entries.js +30 -3
  104. package/dist/server/agent-chat/sweep-trigger-auth.d.ts +18 -0
  105. package/dist/server/agent-chat/sweep-trigger-auth.js +36 -0
  106. package/dist/server/agent-chat-plugin.js +51 -47
  107. package/dist/server/agent-engine-default-model-route.d.ts +2 -2
  108. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  109. package/dist/server/auth.js +21 -6
  110. package/dist/server/embed-route.js +5 -2
  111. package/dist/server/framework-request-handler.js +19 -0
  112. package/dist/server/http-response-telemetry.js +4 -1
  113. package/dist/server/realtime-token.d.ts +1 -1
  114. package/dist/server/request-context.d.ts +6 -0
  115. package/dist/shared/context-status.d.ts +21 -0
  116. package/dist/shared/context-status.js +54 -0
  117. package/dist/shared/mcp-app-display.d.ts +20 -0
  118. package/dist/shared/mcp-app-display.js +34 -0
  119. package/dist/shared/mcp-app-widget-embed.d.ts +18 -0
  120. package/dist/shared/mcp-app-widget-embed.js +41 -0
  121. package/dist/templates/chat/app/components/chat/ChatRouteContent.tsx +9 -2
  122. package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +9 -2
  123. package/dist/templates/workspace-core/.agents/skills/automations/SKILL.md +8 -1
  124. package/dist/templates/workspace-core/.agents/skills/recurring-jobs/SKILL.md +3 -1
  125. package/dist/triggers/actions/manage-automation.d.ts +5 -5
  126. package/dist/triggers/dispatcher.js +10 -7
  127. package/docs/content/audit-log.mdx +3 -3
  128. package/docs/content/cloudflare.mdx +27 -0
  129. package/docs/content/deployment-environment-variables.mdx +2 -0
  130. package/docs/content/durable-resume.mdx +3 -1
  131. package/docs/content/environment-variables.mdx +2 -0
  132. package/docs/content/mcp-apps.mdx +1 -1
  133. package/docs/content/recurring-jobs.mdx +29 -9
  134. package/docs/content/vercel.mdx +30 -0
  135. package/package.json +4 -3
  136. package/src/templates/chat/app/components/chat/ChatRouteContent.tsx +9 -2
  137. package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +9 -2
  138. package/src/templates/workspace-core/.agents/skills/automations/SKILL.md +8 -1
  139. package/src/templates/workspace-core/.agents/skills/recurring-jobs/SKILL.md +3 -1
@@ -10,12 +10,14 @@ import { buildAssistantMessage, buildUserMessage, extractThreadMeta, foldAssista
10
10
  import { attachToolSearch } from "../agent/tool-search.js";
11
11
  import { lookupOwnerAccount, resolveAutomationExecutionIdentity, } from "../automations/service.js";
12
12
  import { createThread, getThread, updateThreadData, withThreadDataLock, } from "../chat-threads/store.js";
13
+ import { automationOutcomeMessagesForUser } from "../localization/automation-outcome-messages.js";
13
14
  import { queryOrgMembers } from "../org/context.js";
14
15
  import { organizationIdFromResourceOwner, organizationResourceOwner, } from "../resources/store.js";
15
16
  import { captureError } from "../server/capture-error.js";
16
17
  import { BuilderCredentialLookupError, CredentialStoreUnavailableError, } from "../server/credential-provider.js";
17
18
  import { runWithRequestContext, } from "../server/request-context.js";
18
19
  import { normalizeReasoningEffortForRequest } from "../shared/reasoning-effort.js";
20
+ import automationNoOpAction, { AUTOMATION_NO_OP_TOOL, automationNoOpSchema, } from "./actions/automation-no-op.js";
19
21
  import { applyAutomationFailure, automationOwnerKind, classifyAutomationFailure, CONFIG_INVALID_ERROR_CODE, MISSING_TOOLS_ERROR_CODE, OWNER_MISSING_ERROR_CODE, pausedMessage, withDeliveryNote, } from "./automation-outcome.js";
20
22
  import { recoveredFactoryOwnerOrgId, } from "./frontmatter.js";
21
23
  import { attachAutomationRunThread, finishAutomationRun, startAutomationRun, } from "./run-history.js";
@@ -352,7 +354,7 @@ export async function runBackgroundAutomation(options, deps) {
352
354
  !(failure.precondition && !transition.pause));
353
355
  throw err;
354
356
  }
355
- await recordRunOutcome(historyId, "success");
357
+ await recordRunOutcome(historyId, result.status, result.status === "skipped" ? result.reason : undefined);
356
358
  return result;
357
359
  }
358
360
  async function recordRunThread(historyId, threadId, runId) {
@@ -445,6 +447,45 @@ async function recordRunOutcome(historyId, status, error, errorCode, notify = tr
445
447
  console.error(`[automations] Could not record run ${historyId} as ${status}:`, err);
446
448
  }
447
449
  }
450
+ async function confirmAutomationWork(automation, ownerEmail, run, responseText, actions, noOpReason) {
451
+ const events = run.events ?? [];
452
+ const hasConfirmedAction = events.some(({ event }) => event.type === "tool_done" &&
453
+ !event.isError &&
454
+ event.completedSideEffect === true &&
455
+ actions[event.tool]?.confirmsAutomationWork !== false);
456
+ const lastFailedTool = [...events]
457
+ .reverse()
458
+ .find(({ event }) => event.type === "tool_done" && event.isError);
459
+ if (noOpReason && !hasConfirmedAction && !lastFailedTool) {
460
+ return { status: "skipped", reason: noOpReason };
461
+ }
462
+ const { deliveryPlatform, deliveryDestination } = automation.meta;
463
+ if ((!noOpReason || hasConfirmedAction) &&
464
+ deliveryPlatform &&
465
+ deliveryDestination &&
466
+ responseText.trim()) {
467
+ const { getDefaultAdapter } = await import("../integrations/adapters/index.js");
468
+ const adapter = getDefaultAdapter(deliveryPlatform);
469
+ if (!adapter?.sendMessageToTarget) {
470
+ throw new BackgroundAutomationRunError(`Automation delivery is not supported for ${deliveryPlatform}`, CONFIG_INVALID_ERROR_CODE);
471
+ }
472
+ await adapter.sendMessageToTarget(adapter.formatAgentResponse(responseText), {
473
+ destination: deliveryDestination,
474
+ threadRef: automation.meta.deliveryThreadRef ?? null,
475
+ tenantId: automation.meta.deliveryTenantId,
476
+ });
477
+ return { status: "success" };
478
+ }
479
+ if (hasConfirmedAction)
480
+ return { status: "success" };
481
+ const messages = await automationOutcomeMessagesForUser(ownerEmail);
482
+ const detail = lastFailedTool?.event.type === "tool_done"
483
+ ? lastFailedTool.event.result
484
+ : undefined;
485
+ throw new BackgroundAutomationRunError(`${deliveryPlatform && deliveryDestination ? messages.emptyDelivery : messages.noWork}${detail ? ` ${detail}` : ""}`, lastFailedTool?.event.type === "tool_done"
486
+ ? (lastFailedTool.event.errorCode ?? "automation_no_confirmed_work")
487
+ : "automation_no_confirmed_work");
488
+ }
448
489
  /**
449
490
  * Everything below runs before any thread or `agent_runs` row exists, so an
450
491
  * automation that cannot run fails here, once, instead of leaving a "Job:"
@@ -514,7 +555,38 @@ async function executeBackgroundAutomation(options, deps, historyId, runIdRef) {
514
555
  orgId,
515
556
  }, async () => {
516
557
  assertHardDeadline(options.hardDeadlineAt);
517
- const baseActions = await deps.getActions(automation);
558
+ let noOpReason;
559
+ const messages = await automationOutcomeMessagesForUser(ownerEmail);
560
+ const baseActions = {
561
+ ...(await deps.getActions(automation)),
562
+ [AUTOMATION_NO_OP_TOOL]: {
563
+ ...automationNoOpAction,
564
+ agentTool: true,
565
+ confirmsAutomationWork: false,
566
+ tool: {
567
+ ...automationNoOpAction.tool,
568
+ description: messages.noOpInstruction,
569
+ parameters: {
570
+ ...automationNoOpAction.tool.parameters,
571
+ type: "object",
572
+ properties: {
573
+ ...automationNoOpAction.tool.parameters?.properties,
574
+ reason: {
575
+ ...automationNoOpAction.tool.parameters?.properties
576
+ ?.reason,
577
+ type: "string",
578
+ description: messages.noOpReason,
579
+ },
580
+ },
581
+ },
582
+ },
583
+ run: async (args, ctx) => {
584
+ const result = await automationNoOpAction.run(automationNoOpSchema.parse(args), ctx);
585
+ noOpReason = result.reason;
586
+ return result;
587
+ },
588
+ },
589
+ };
518
590
  assertHardDeadline(options.hardDeadlineAt);
519
591
  assertRequestedMcpToolsAvailable(automation, baseActions);
520
592
  const configuredInitialTools = deps.getInitialToolNames?.(automation);
@@ -524,6 +596,10 @@ async function executeBackgroundAutomation(options, deps, historyId, runIdRef) {
524
596
  ...(automation.meta.mcpTools ?? []),
525
597
  ])
526
598
  : undefined;
599
+ if (initialToolNames &&
600
+ !initialToolNames.includes(AUTOMATION_NO_OP_TOOL)) {
601
+ initialToolNames.push(AUTOMATION_NO_OP_TOOL);
602
+ }
527
603
  const actions = initialToolNames
528
604
  ? attachToolSearch({ ...baseActions })
529
605
  : baseActions;
@@ -535,7 +611,7 @@ async function executeBackgroundAutomation(options, deps, historyId, runIdRef) {
535
611
  assertHardDeadline(options.hardDeadlineAt);
536
612
  const model = await resolveBackgroundAutomationModel(engine, automation.meta.model, deps);
537
613
  assertHardDeadline(options.hardDeadlineAt);
538
- const systemPrompt = await deps.getSystemPrompt(ownerEmail);
614
+ const systemPrompt = `${await deps.getSystemPrompt(ownerEmail)}\n\n${messages.noOpInstruction}`;
539
615
  assertHardDeadline(options.hardDeadlineAt);
540
616
  const thread = await createThread(ownerEmail, {
541
617
  title: threadTitle,
@@ -553,6 +629,7 @@ async function executeBackgroundAutomation(options, deps, historyId, runIdRef) {
553
629
  const softTimeoutMs = resolveBackgroundAutomationSoftTimeoutMs();
554
630
  const usageRef = { current: null };
555
631
  let responseText = "";
632
+ let outcome;
556
633
  let hardAbortTimer = null;
557
634
  let hardTimedOut = false;
558
635
  assertHardDeadline(options.hardDeadlineAt);
@@ -631,12 +708,25 @@ async function executeBackgroundAutomation(options, deps, historyId, runIdRef) {
631
708
  clearTimeout(hardAbortTimer);
632
709
  hardAbortTimer = null;
633
710
  }
634
- const persistFailure = backgroundAutomationPersistFailure({
711
+ let persistFailure = backgroundAutomationPersistFailure({
635
712
  run,
636
713
  hardTimedOut,
637
714
  hardTimeoutMs,
638
715
  });
639
716
  try {
717
+ responseText = collectFinalResponseTextFromAgentEvents((run.events ?? []).map((entry) => entry.event), { fallbackToPreToolText: false });
718
+ if (!persistFailure && run.status === "completed") {
719
+ try {
720
+ outcome = await confirmAutomationWork(automation, ownerEmail, run, responseText, actions, noOpReason);
721
+ }
722
+ catch (error) {
723
+ const failure = classifyAutomationFailure(error);
724
+ persistFailure = {
725
+ message: failure.message,
726
+ errorCode: failure.code,
727
+ };
728
+ }
729
+ }
640
730
  await persistBackgroundAutomationTurn({
641
731
  threadId: thread.id,
642
732
  threadTitle,
@@ -652,6 +742,11 @@ async function executeBackgroundAutomation(options, deps, historyId, runIdRef) {
652
742
  if (hardTimedOut)
653
743
  return;
654
744
  if (persistFailure) {
745
+ run.continuationTerminalEvent = {
746
+ type: "error",
747
+ error: persistFailure.message,
748
+ errorCode: persistFailure.errorCode,
749
+ };
655
750
  reject(new BackgroundAutomationRunError(persistFailure.message, persistFailure.errorCode));
656
751
  return;
657
752
  }
@@ -661,7 +756,6 @@ async function executeBackgroundAutomation(options, deps, historyId, runIdRef) {
661
756
  `Background automation ended with status: ${run.status} and recorded no error detail`, cause?.errorCode ?? `background_automation_${run.status}`));
662
757
  return;
663
758
  }
664
- responseText = collectFinalResponseTextFromAgentEvents((run.events ?? []).map((entry) => entry.event));
665
759
  resolve();
666
760
  }, {
667
761
  softTimeoutMs,
@@ -722,20 +816,9 @@ async function executeBackgroundAutomation(options, deps, historyId, runIdRef) {
722
816
  // Usage attribution must not break an otherwise successful run.
723
817
  }
724
818
  }
725
- if (responseText.trim() &&
726
- automation.meta.deliveryPlatform &&
727
- automation.meta.deliveryDestination) {
728
- const { getDefaultAdapter } = await import("../integrations/adapters/index.js");
729
- const adapter = getDefaultAdapter(automation.meta.deliveryPlatform);
730
- if (!adapter?.sendMessageToTarget) {
731
- throw new BackgroundAutomationRunError(`Automation delivery is not supported for ${automation.meta.deliveryPlatform}`, CONFIG_INVALID_ERROR_CODE);
732
- }
733
- await adapter.sendMessageToTarget(adapter.formatAgentResponse(responseText), {
734
- destination: automation.meta.deliveryDestination,
735
- threadRef: automation.meta.deliveryThreadRef ?? null,
736
- tenantId: automation.meta.deliveryTenantId,
737
- });
819
+ if (!outcome) {
820
+ throw new BackgroundAutomationRunError(messages.noWork, "automation_no_confirmed_work");
738
821
  }
739
- return { responseText, runId };
822
+ return { ...outcome, responseText, runId };
740
823
  });
741
824
  }
@@ -1,5 +1,5 @@
1
1
  import { type MigrationEntry } from "../db/migrations.js";
2
- export type AutomationRunStatus = "running" | "success" | "error" | "interrupted";
2
+ export type AutomationRunStatus = "running" | "success" | "error" | "interrupted" | "skipped";
3
3
  export interface AutomationRun {
4
4
  id: string;
5
5
  owner: string;
@@ -18,7 +18,7 @@ registerEvent({
18
18
  orgId: z.string().nullable(),
19
19
  runId: z.string().nullable(),
20
20
  threadId: z.string().nullable(),
21
- status: z.enum(["success", "error", "interrupted"]),
21
+ status: z.enum(["success", "error", "interrupted", "skipped"]),
22
22
  error: z.string().nullable(),
23
23
  errorCode: z.string().nullable(),
24
24
  durationMs: z.number().nullable(),
@@ -575,7 +575,7 @@ export async function finishAutomationRun(id, status, error, errorCode, options
575
575
  });
576
576
  const row = existing.rows?.[0];
577
577
  const finishedAt = Date.now();
578
- const shouldQueueFailureAlert = status !== "success" &&
578
+ const shouldQueueFailureAlert = (status === "error" || status === "interrupted") &&
579
579
  options.notify !== false &&
580
580
  Boolean(row?.notification_email);
581
581
  const claimGuard = options.expectedClaimedAt !== undefined;
@@ -589,7 +589,9 @@ export async function finishAutomationRun(id, status, error, errorCode, options
589
589
  status,
590
590
  finishedAt,
591
591
  error?.slice(0, MAX_ERROR_LENGTH) ?? null,
592
- errorCode?.slice(0, MAX_ERROR_CODE_LENGTH) ?? null,
592
+ status === "skipped"
593
+ ? null
594
+ : (errorCode?.slice(0, MAX_ERROR_CODE_LENGTH) ?? null),
593
595
  shouldQueueFailureAlert ? "evaluating" : null,
594
596
  shouldQueueFailureAlert ? finishedAt : null,
595
597
  id,
@@ -611,7 +613,9 @@ export async function finishAutomationRun(id, status, error, errorCode, options
611
613
  threadId: row.thread_id == null ? null : stringifyValue(row.thread_id),
612
614
  status,
613
615
  error: error?.slice(0, MAX_ERROR_LENGTH) ?? null,
614
- errorCode: errorCode?.slice(0, MAX_ERROR_CODE_LENGTH) ?? null,
616
+ errorCode: status === "skipped"
617
+ ? null
618
+ : (errorCode?.slice(0, MAX_ERROR_CODE_LENGTH) ?? null),
615
619
  durationMs: startedAt === null ? null : Math.max(0, finishedAt - startedAt),
616
620
  }, { owner: stringifyValue(row.owner) });
617
621
  }
@@ -267,7 +267,7 @@ async function processRecurringJobsWithLease(deps) {
267
267
  });
268
268
  continue;
269
269
  }
270
- await recordIdentityFailure(candidate.resource, candidate.meta, candidate.body, now, identity.reason);
270
+ await recordIdentityFailure(candidate.resource, candidate.meta, candidate.body, now, identity.reason, identity.code);
271
271
  continue;
272
272
  }
273
273
  if (isReservedIdentityBlocked(identity.identity.userEmail)) {
@@ -386,23 +386,32 @@ async function recoverStaleAutomationHistory(owner, path, now) {
386
386
  }
387
387
  }
388
388
  export const jobRunCutOffReason = backgroundRunCutOffReason;
389
- async function recordIdentityFailure(resource, meta, body, now, reason, historyId) {
389
+ async function recordIdentityFailure(resource, meta, body, now, reason, errorCode = "owner_unverifiable", historyId) {
390
390
  const jobName = resource.path.replace(/^jobs\//, "").replace(/\.md$/, "");
391
391
  console.warn(`[recurring-jobs] Skipping job "${jobName}": ${reason}. ` +
392
392
  `User/membership no longer valid — leaving cron entry for admin review.`);
393
393
  const alreadyRecorded = meta.lastError === reason && hasRecentIdentityFailure(meta, now);
394
394
  meta.lastCheck = now.toISOString();
395
- meta.lastStatus = "skipped";
395
+ meta.lastStatus = "error";
396
396
  meta.lastError = reason;
397
+ meta.lastErrorCode = errorCode;
397
398
  if (!alreadyRecorded)
398
- await updateResource(resource, meta, body);
399
+ await updateResource(resource, meta, body, { lastErrorCode: errorCode });
399
400
  if (historyId) {
400
- await finishAutomationRun(historyId, "error", `Automation did not run: ${reason}. No delivery was confirmed.`);
401
+ await finishAutomationRun(historyId, "error", `Automation did not run: ${reason}. No delivery was confirmed.`, errorCode);
401
402
  }
402
- return { status: "skipped", error: reason };
403
+ return { status: "error", error: reason };
403
404
  }
404
405
  function hasRecentIdentityFailure(meta, now) {
405
- if (meta.lastStatus !== "skipped" || !meta.lastCheck || !meta.lastError) {
406
+ if (meta.lastStatus !== "error" ||
407
+ !meta.lastErrorCode ||
408
+ ![
409
+ OWNER_MISSING_ERROR_CODE,
410
+ CONFIG_INVALID_ERROR_CODE,
411
+ "owner_unverifiable",
412
+ ].includes(meta.lastErrorCode) ||
413
+ !meta.lastCheck ||
414
+ !meta.lastError) {
406
415
  return false;
407
416
  }
408
417
  const lastCheckMs = Date.parse(meta.lastCheck);
@@ -571,7 +580,7 @@ async function executeJob(resource, meta, body, deps, now, options = {}) {
571
580
  // failure; leave the cron entry alone so an admin can purge after
572
581
  // investigation.
573
582
  if (!identity.ok) {
574
- return recordIdentityFailure(resource, meta, body, now, identity.reason, options.historyId);
583
+ return recordIdentityFailure(resource, meta, body, now, identity.reason, identity.code, options.historyId);
575
584
  }
576
585
  const jobUserEmail = identity.identity.userEmail;
577
586
  const jobOrgId = identity.identity.orgId;
@@ -693,12 +702,16 @@ async function executeJob(resource, meta, body, deps, now, options = {}) {
693
702
  }, deps);
694
703
  await recordExecutionOutcome(resource, {
695
704
  lastRun: meta.lastRun,
696
- lastStatus: "success",
697
- lastError: undefined,
705
+ lastStatus: result.status,
706
+ lastError: result.status === "skipped" ? result.reason : undefined,
698
707
  advanceSchedule: options.advanceSchedule,
699
708
  });
700
- console.log(`[recurring-jobs] Job "${jobName}" completed.`);
701
- return { status: "success", runId: result.runId };
709
+ console.log(`[recurring-jobs] Job "${jobName}" ${result.status}.`);
710
+ return {
711
+ status: result.status,
712
+ runId: result.runId,
713
+ ...(result.status === "skipped" ? { error: result.reason } : {}),
714
+ };
702
715
  }
703
716
  catch (err) {
704
717
  const failure = classifyAutomationFailure(err);
@@ -0,0 +1,22 @@
1
+ import { type BuiltinLocaleCode } from "./shared.js";
2
+ export declare const AUTOMATION_OUTCOME_MESSAGES: Record<BuiltinLocaleCode, {
3
+ noWork: string;
4
+ emptyDelivery: string;
5
+ emailNotSent: string;
6
+ noOpInstruction: string;
7
+ noOpReason: string;
8
+ }>;
9
+ export declare function automationOutcomeMessagesForLocale(locale: string): {
10
+ noWork: string;
11
+ emptyDelivery: string;
12
+ emailNotSent: string;
13
+ noOpInstruction: string;
14
+ noOpReason: string;
15
+ };
16
+ export declare function automationOutcomeMessagesForUser(userEmail: string | null | undefined): Promise<{
17
+ noWork: string;
18
+ emptyDelivery: string;
19
+ emailNotSent: string;
20
+ noOpInstruction: string;
21
+ noOpReason: string;
22
+ }>;
@@ -0,0 +1,90 @@
1
+ import { getUserSetting } from "../settings/user-settings.js";
2
+ import { DEFAULT_LOCALE, isLocaleCode, LOCALIZATION_SETTING_KEY, normalizeLocalizationPreference, } from "./shared.js";
3
+ export const AUTOMATION_OUTCOME_MESSAGES = {
4
+ "en-US": {
5
+ noOpInstruction: "If nothing needs doing, call automation-no-op with a short reason. Failed tools, sends, or missing credentials are errors, not no-ops.",
6
+ noOpReason: "A short reason why no action or report was needed (1–500 characters).",
7
+ emailNotSent: "The email was not sent by a delivery provider.",
8
+ noWork: "The automation ended without a confirmed action. Configure a delivery destination for read-only reports.",
9
+ emptyDelivery: "The automation produced no message for its configured delivery destination.",
10
+ },
11
+ "es-ES": {
12
+ noOpInstruction: "Si no hay nada que hacer, llama a automation-no-op con un motivo breve. Las herramientas o los envíos fallidos y las credenciales ausentes son errores, no casos sin trabajo.",
13
+ noOpReason: "Motivo breve por el que no se necesitaba ninguna acción ni informe (1–500 caracteres).",
14
+ emailNotSent: "El correo no fue enviado por un proveedor de entrega.",
15
+ noWork: "La automatización terminó sin una acción confirmada. Configura un destino de entrega para los informes de solo lectura.",
16
+ emptyDelivery: "La automatización no produjo ningún mensaje para su destino de entrega configurado.",
17
+ },
18
+ "fr-FR": {
19
+ noOpInstruction: "Si rien n’est à faire, appelez automation-no-op avec une courte raison. Les échecs d’outils ou d’envoi et les identifiants manquants sont des erreurs, pas une absence de travail.",
20
+ noOpReason: "Courte raison pour laquelle aucune action ni aucun rapport n’était nécessaire (1–500 caractères).",
21
+ emailNotSent: "L’e-mail n’a pas été envoyé par un fournisseur de messagerie.",
22
+ noWork: "L’automatisation s’est terminée sans action confirmée. Configurez une destination de livraison pour les rapports en lecture seule.",
23
+ emptyDelivery: "L’automatisation n’a produit aucun message pour sa destination de livraison configurée.",
24
+ },
25
+ "de-DE": {
26
+ noOpInstruction: "Wenn nichts zu tun ist, rufen Sie automation-no-op mit einer kurzen Begründung auf. Fehlgeschlagene Tools oder Zustellungen und fehlende Zugangsdaten sind Fehler, keine Fälle ohne Handlungsbedarf.",
27
+ noOpReason: "Kurze Begründung, warum keine Aktion oder kein Bericht nötig war (1–500 Zeichen).",
28
+ emailNotSent: "Die E-Mail wurde von keinem Zustellanbieter versendet.",
29
+ noWork: "Die Automatisierung endete ohne bestätigte Aktion. Konfigurieren Sie ein Zustellungsziel für schreibgeschützte Berichte.",
30
+ emptyDelivery: "Die Automatisierung hat keine Nachricht für ihr konfiguriertes Zustellungsziel erzeugt.",
31
+ },
32
+ "pt-BR": {
33
+ noOpInstruction: "Se não houver nada a fazer, chame automation-no-op com um motivo breve. Falhas de ferramentas ou envios e credenciais ausentes são erros, não ausência de trabalho.",
34
+ noOpReason: "Motivo breve pelo qual nenhuma ação ou relatório era necessário (1–500 caracteres).",
35
+ emailNotSent: "O e-mail não foi enviado por um provedor de entrega.",
36
+ noWork: "A automação terminou sem uma ação confirmada. Configure um destino de entrega para relatórios somente de leitura.",
37
+ emptyDelivery: "A automação não produziu nenhuma mensagem para seu destino de entrega configurado.",
38
+ },
39
+ "zh-CN": {
40
+ noOpInstruction: "如果没有需要执行的工作,请调用 automation-no-op 并提供简短原因。工具失败、发送失败或缺少凭据属于错误,不是无需操作。",
41
+ noOpReason: "简要说明为何无需执行操作或发送报告(1–500 个字符)。",
42
+ emailNotSent: "电子邮件未通过邮件服务提供商发送。",
43
+ noWork: "自动化结束时没有已确认的操作。请为只读报告配置发送目标。",
44
+ emptyDelivery: "自动化未生成要发送到已配置目标的消息。",
45
+ },
46
+ "zh-TW": {
47
+ noOpInstruction: "如果沒有需要執行的工作,請呼叫 automation-no-op 並提供簡短原因。工具失敗、傳送失敗或缺少憑證屬於錯誤,不是無需操作。",
48
+ noOpReason: "簡要說明為何無需執行操作或傳送報告(1–500 個字元)。",
49
+ emailNotSent: "電子郵件未透過郵件服務供應商傳送。",
50
+ noWork: "自動化結束時沒有已確認的操作。請為唯讀報告設定傳送目標。",
51
+ emptyDelivery: "自動化未產生要傳送到已設定目標的訊息。",
52
+ },
53
+ "ja-JP": {
54
+ noOpInstruction: "対応が不要な場合は、短い理由を添えて automation-no-op を呼び出してください。ツールや送信の失敗、認証情報の不足はエラーであり、対応不要には該当しません。",
55
+ noOpReason: "操作やレポートが不要だった短い理由(1~500文字)。",
56
+ emailNotSent: "メールは配信プロバイダーから送信されませんでした。",
57
+ noWork: "自動化は操作が確認されないまま終了しました。読み取り専用のレポートには配信先を設定してください。",
58
+ emptyDelivery: "自動化は設定された配信先へのメッセージを生成しませんでした。",
59
+ },
60
+ "ko-KR": {
61
+ noOpInstruction: "할 일이 없으면 짧은 이유와 함께 automation-no-op을 호출하세요. 도구 또는 전송 실패와 자격 증명 누락은 작업 없음이 아닌 오류입니다.",
62
+ noOpReason: "작업이나 보고서가 필요하지 않았던 짧은 이유(1–500자).",
63
+ emailNotSent: "이메일이 전송 제공업체를 통해 전송되지 않았습니다.",
64
+ noWork: "확인된 작업 없이 자동화가 종료되었습니다. 읽기 전용 보고서에는 전송 대상을 설정하세요.",
65
+ emptyDelivery: "자동화가 설정된 전송 대상에 보낼 메시지를 생성하지 않았습니다.",
66
+ },
67
+ "hi-IN": {
68
+ noOpInstruction: "यदि कुछ करने की आवश्यकता नहीं है, तो संक्षिप्त कारण के साथ automation-no-op कॉल करें। विफल टूल, विफल भेजने की कोशिश या अनुपलब्ध क्रेडेंशियल त्रुटियाँ हैं, काम न होने की स्थिति नहीं।",
69
+ noOpReason: "किसी कार्रवाई या रिपोर्ट की आवश्यकता न होने का संक्षिप्त कारण (1–500 अक्षर)।",
70
+ emailNotSent: "ईमेल किसी डिलीवरी प्रदाता द्वारा नहीं भेजा गया।",
71
+ noWork: "ऑटोमेशन किसी पुष्ट कार्रवाई के बिना समाप्त हो गया। केवल पढ़ने वाली रिपोर्ट के लिए डिलीवरी गंतव्य कॉन्फ़िगर करें।",
72
+ emptyDelivery: "ऑटोमेशन ने अपने कॉन्फ़िगर किए गए डिलीवरी गंतव्य के लिए कोई संदेश नहीं बनाया।",
73
+ },
74
+ "ar-SA": {
75
+ noOpInstruction: "إذا لم يكن هناك ما يجب فعله، استدعِ automation-no-op مع سبب موجز. فشل الأدوات أو الإرسال أو غياب بيانات الاعتماد أخطاء، وليس حالات لا تتطلب عملاً.",
76
+ noOpReason: "سبب موجز لعدم الحاجة إلى إجراء أو تقرير (1–500 حرف).",
77
+ emailNotSent: "لم تُرسل رسالة البريد الإلكتروني عبر مزوّد تسليم.",
78
+ noWork: "انتهت الأتمتة دون إجراء مؤكد. اضبط وجهة تسليم للتقارير المخصصة للقراءة فقط.",
79
+ emptyDelivery: "لم تنتج الأتمتة أي رسالة لوجهة التسليم المحددة.",
80
+ },
81
+ };
82
+ export function automationOutcomeMessagesForLocale(locale) {
83
+ return AUTOMATION_OUTCOME_MESSAGES[isLocaleCode(locale) ? locale : DEFAULT_LOCALE];
84
+ }
85
+ export async function automationOutcomeMessagesForUser(userEmail) {
86
+ const preference = normalizeLocalizationPreference(userEmail
87
+ ? await getUserSetting(userEmail, LOCALIZATION_SETTING_KEY)
88
+ : undefined);
89
+ return automationOutcomeMessagesForLocale(preference.locale);
90
+ }
@@ -1018,9 +1018,11 @@ declare const messages: {
1018
1018
  scheduleUnavailableLocalTitle: string;
1019
1019
  scheduleUnavailableDisabled: string;
1020
1020
  scheduleUnavailableNoScheduler: string;
1021
+ scheduleUnavailableMissingSecret: string;
1021
1022
  scheduleUnavailableLocal: string;
1022
1023
  scheduleUnavailableDisabledFix: string;
1023
1024
  scheduleUnavailableLocalFix: string;
1025
+ scheduleUnavailableMissingSecretFix: string;
1024
1026
  scheduleUnavailableFixLabel: string;
1025
1027
  scheduleUnavailableFixLabelOpen: string;
1026
1028
  };
@@ -940,11 +940,13 @@ const messages = {
940
940
  scheduleUnknownDetail: "The scheduler status check failed, so the next run times below are unconfirmed. Reload to check again.",
941
941
  scheduleUnavailableTitle: "Schedules won't run in this deploy",
942
942
  scheduleUnavailableLocalTitle: "Schedules don't run in local development",
943
- scheduleUnavailableDisabled: "This app was built with recurring jobs turned off, so no scheduled automation will fire. Event- and webhook-triggered automations and Run now still work.",
944
- scheduleUnavailableNoScheduler: "This hosting target has no durable scheduler, so no scheduled automation will fire. Event- and webhook-triggered automations and Run now still work.",
943
+ scheduleUnavailableDisabled: "This app was built with recurring jobs turned off, so no scheduled automation will fire. Webhook-triggered automations and Run now still work.",
944
+ scheduleUnavailableNoScheduler: "This deploy has no scheduler, so scheduled automations won't fire, event-triggered automations stay queued, and crashed runs aren't recovered. Webhook-triggered automations and Run now still work.",
945
+ scheduleUnavailableMissingSecret: "This deploy has a scheduler, but {{secret}} isn't set, so every scheduler request is rejected. Scheduled automations won't fire, event-triggered automations stay queued, and crashed runs aren't recovered. Webhook-triggered automations and Run now still work.",
945
946
  scheduleUnavailableLocal: "Schedules stay off on a dev machine unless you opt in. Event- and webhook-triggered automations and Run now still work.",
946
947
  scheduleUnavailableDisabledFix: "To enable recurring jobs, set AGENT_NATIVE_DISABLE_RECURRING_JOBS=false in the build environment.",
947
948
  scheduleUnavailableLocalFix: "Set AGENT_NATIVE_ENABLE_LOCAL_RECURRING_JOBS=true to run schedules on this machine.",
949
+ scheduleUnavailableMissingSecretFix: "Set {{secret}} in this deployment's environment variables, then redeploy.",
948
950
  scheduleUnavailableFixLabel: "Show more",
949
951
  scheduleUnavailableFixLabelOpen: "Show less",
950
952
  },
@@ -24,6 +24,14 @@ import { MCP_APP_REQUEST_ORIGIN_CSP_SOURCE } from "./embed-app.js";
24
24
  import { hasMcpOAuthScope, parseMcpOAuthOrgIdClaim, verifyMcpOAuthAccessToken, } from "./oauth-token.js";
25
25
  import { mcpToolInputSchema } from "./tool-input-schema.js";
26
26
  const PRESERVE_MCP_OBJECT_RESULT = Symbol("preserveMcpObjectResult");
27
+ // A GET action is a query, so its result is the payload the model asked for
28
+ // even when it declares readOnly: false because the read also repairs or caches
29
+ // (Slides get-deck). Collapsing that result into a write confirmation leaves
30
+ // the model with only "<title> is ready.".
31
+ function returnsQueryPayload(entry) {
32
+ return (entry.readOnly === true ||
33
+ (entry.http !== false && entry.http?.method === "GET"));
34
+ }
27
35
  function hasVerifiedMcpUserIdentity(identity) {
28
36
  return (identity?.identityAssurance === "user" &&
29
37
  Boolean(identity.userEmail?.trim()));
@@ -237,15 +245,10 @@ export function validateMcpDirectoryProfile(config, sourceActions = config.produ
237
245
  if (unprofiledWidgetReadAction) {
238
246
  throw new McpDirectoryProfileValidationError(`[agent-native] MCP directory widget read "${unprofiledWidgetReadAction}" must be listed in its scoped read category.`);
239
247
  }
240
- const widgetTargetNames = Object.keys(profile?.widgetTargets ?? {});
241
248
  if (profile && profile.widgets !== false && profile.widgetTargets) {
242
- const missingTarget = widgetActionNames.find((name) => !profile?.widgetTargets?.[name]);
243
- const unknownTarget = widgetTargetNames.find((name) => !widgetActionNames.includes(name));
244
- if (missingTarget || unknownTarget) {
245
- throw new McpDirectoryProfileValidationError(`[agent-native] MCP directory widget target resolvers must match the listed widget actions (missing: ${missingTarget ?? "none"}; unknown: ${unknownTarget ?? "none"}).`);
246
- }
247
- if (widgetActionNames.length > 0 && !profile?.widgetTargets) {
248
- throw new McpDirectoryProfileValidationError("[agent-native] MCP directory widgets require a server-owned target resolver for every widget action.");
249
+ const unknownTarget = Object.keys(profile.widgetTargets).find((name) => !widgetActionNames.includes(name));
250
+ if (unknownTarget) {
251
+ throw new McpDirectoryProfileValidationError(`[agent-native] MCP directory widget target "${unknownTarget}" must name a listed action with an mcpApp resource. Listed widget actions without a target resolver serve as plain tools.`);
249
252
  }
250
253
  }
251
254
  for (const name of widgetReadOnlyActions) {
@@ -1095,7 +1098,7 @@ function safeUiSegment(value, fallback) {
1095
1098
  return normalized || fallback;
1096
1099
  }
1097
1100
  const MCP_APP_RESOURCE_SHELL_VERSION = "shell-v65";
1098
- const MCP_DIRECTORY_APP_RESOURCE_SHELL_VERSION = "shell-v67";
1101
+ const MCP_DIRECTORY_APP_RESOURCE_SHELL_VERSION = "shell-v68";
1099
1102
  export function getMcpDirectoryWidgetResourceUri(appId, fallback = "agent-native") {
1100
1103
  const app = safeUiSegment(appId, fallback);
1101
1104
  return (versionMcpAppResourceUri(`ui://${app}/${MCP_DIRECTORY_APP_RESOURCE_SHELL_VERSION}`, MCP_DIRECTORY_APP_RESOURCE_SHELL_VERSION)?.uri ?? `ui://${app}/${MCP_DIRECTORY_APP_RESOURCE_SHELL_VERSION}`);
@@ -1307,6 +1310,14 @@ async function resolveMcpAppResource(config, actionName, entry, requestMeta) {
1307
1310
  const resource = entry.mcpApp?.resource;
1308
1311
  if (!resource)
1309
1312
  return null;
1313
+ // Directory widgets open a host pane on every call, so only the profile's
1314
+ // widgetTargets (create/present tools) attach one; a read tool whose action
1315
+ // still carries mcpApp.resource for the non-directory surface must not.
1316
+ const widgetTargets = config.directoryProfile?.widgetTargets;
1317
+ if (config.catalogMode === "directory" &&
1318
+ (!widgetTargets || !Object.hasOwn(widgetTargets, actionName))) {
1319
+ return null;
1320
+ }
1310
1321
  const resolvedUri = getMcpAppResourceUri(config, actionName, entry);
1311
1322
  if (!resolvedUri)
1312
1323
  return null;
@@ -2251,7 +2262,7 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
2251
2262
  const isAppOnlyVisibility = Array.isArray(toolVisibility) &&
2252
2263
  toolVisibility.length > 0 &&
2253
2264
  toolVisibility.every((v) => v === "app");
2254
- const structuredResult = (entry.readOnly === true ||
2265
+ const structuredResult = (returnsQueryPayload(entry) ||
2255
2266
  entry.mcpApp?.structuredContent === true) &&
2256
2267
  actionResultForClient &&
2257
2268
  typeof actionResultForClient === "object"
@@ -2272,7 +2283,7 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
2272
2283
  const text = mcpAppResource
2273
2284
  ? conciseMcpAppToolText(name, textResultForClient, structuredContent)
2274
2285
  : conciseToolResultText(name, textResultForClient, {
2275
- preserveObjectResult: entry.readOnly === true ||
2286
+ preserveObjectResult: returnsQueryPayload(entry) ||
2276
2287
  entry[PRESERVE_MCP_OBJECT_RESULT] ===
2277
2288
  true,
2278
2289
  });