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

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 (106) 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/cli/skills.d.ts +1 -0
  27. package/dist/cli/skills.js +1 -1
  28. package/dist/client/chat/agentkit-agent-native.js +83 -7
  29. package/dist/client/chat/agentkit-protocol.js +140 -67
  30. package/dist/client/chat/runtime.js +5 -3
  31. package/dist/client/use-run-stuck-detection.d.ts +18 -1
  32. package/dist/client/use-run-stuck-detection.js +115 -75
  33. package/dist/db/request-telemetry.d.ts +7 -1
  34. package/dist/db/request-telemetry.js +20 -1
  35. package/dist/deploy/build.d.ts +25 -1
  36. package/dist/deploy/build.js +176 -3
  37. package/dist/deploy/workspace-deploy.js +8 -3
  38. package/dist/extensions/fetch-tool.js +50 -14
  39. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  40. package/dist/integrations/adapters/email.js +11 -3
  41. package/dist/integrations/adapters/telegram.js +4 -2
  42. package/dist/jobs/actions/automation-no-op.d.ts +12 -0
  43. package/dist/jobs/actions/automation-no-op.js +20 -0
  44. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  45. package/dist/jobs/background-automation-runner.d.ts +8 -1
  46. package/dist/jobs/background-automation-runner.js +102 -19
  47. package/dist/jobs/run-history.d.ts +1 -1
  48. package/dist/jobs/run-history.js +8 -4
  49. package/dist/jobs/scheduler.js +25 -12
  50. package/dist/localization/automation-outcome-messages.d.ts +22 -0
  51. package/dist/localization/automation-outcome-messages.js +90 -0
  52. package/dist/localization/default-messages.d.ts +2 -0
  53. package/dist/localization/default-messages.js +4 -2
  54. package/dist/notifications/routes.d.ts +1 -1
  55. package/dist/observability/metrics.d.ts +3 -0
  56. package/dist/observability/metrics.js +10 -0
  57. package/dist/observability/migrations.js +22 -0
  58. package/dist/observability/routes.d.ts +13 -8
  59. package/dist/observability/routes.js +88 -14
  60. package/dist/observability/sentiment.d.ts +5 -0
  61. package/dist/observability/sentiment.js +75 -25
  62. package/dist/observability/store.d.ts +28 -0
  63. package/dist/observability/store.js +58 -0
  64. package/dist/observability/traces.js +37 -5
  65. package/dist/org/index.d.ts +2 -2
  66. package/dist/progress/actions.js +1 -0
  67. package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
  68. package/dist/provider-api/actions/provider-api.d.ts +7 -7
  69. package/dist/provider-api/actions/provider-api.js +6 -2
  70. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  71. package/dist/provider-api/index.d.ts +1 -0
  72. package/dist/provider-api/index.js +18 -0
  73. package/dist/provider-api/result-outcome.d.ts +1 -0
  74. package/dist/provider-api/result-outcome.js +20 -0
  75. package/dist/provider-api/staging.js +10 -9
  76. package/dist/server/agent-chat/prompt-resources.d.ts +8 -2
  77. package/dist/server/agent-chat/prompt-resources.js +30 -7
  78. package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +19 -4
  79. package/dist/server/agent-chat/recurring-jobs-runtime.js +52 -16
  80. package/dist/server/agent-chat/sweep-trigger-auth.d.ts +18 -0
  81. package/dist/server/agent-chat/sweep-trigger-auth.js +36 -0
  82. package/dist/server/agent-chat-plugin.js +20 -13
  83. package/dist/server/agent-engine-default-model-route.d.ts +2 -2
  84. package/dist/server/auth.js +4 -4
  85. package/dist/server/framework-request-handler.js +19 -0
  86. package/dist/server/http-response-telemetry.js +4 -1
  87. package/dist/server/realtime-token.d.ts +1 -1
  88. package/dist/server/request-context.d.ts +6 -0
  89. package/dist/shared/context-status.d.ts +21 -0
  90. package/dist/shared/context-status.js +54 -0
  91. package/dist/templates/chat/app/components/chat/ChatRouteContent.tsx +8 -1
  92. package/dist/templates/workspace-core/.agents/skills/automations/SKILL.md +8 -1
  93. package/dist/templates/workspace-core/.agents/skills/recurring-jobs/SKILL.md +3 -1
  94. package/dist/triggers/actions/manage-automation.d.ts +7 -7
  95. package/dist/triggers/dispatcher.js +10 -7
  96. package/docs/content/audit-log.mdx +3 -3
  97. package/docs/content/cloudflare.mdx +27 -0
  98. package/docs/content/deployment-environment-variables.mdx +2 -0
  99. package/docs/content/durable-resume.mdx +3 -1
  100. package/docs/content/environment-variables.mdx +2 -0
  101. package/docs/content/recurring-jobs.mdx +29 -9
  102. package/docs/content/vercel.mdx +30 -0
  103. package/package.json +3 -3
  104. package/src/templates/chat/app/components/chat/ChatRouteContent.tsx +8 -1
  105. package/src/templates/workspace-core/.agents/skills/automations/SKILL.md +8 -1
  106. package/src/templates/workspace-core/.agents/skills/recurring-jobs/SKILL.md +3 -1
package/dist/action.js CHANGED
@@ -82,9 +82,13 @@ export const MCP_APP_MIME_TYPE = "text/html;profile=mcp-app";
82
82
  export const MCP_APP_RESOURCE_URI_META_KEY = "ui/resourceUri";
83
83
  export function defineAction(options) {
84
84
  const hasSchema = options.schema && "~standard" in options.schema;
85
+ // Converting a schema to JSON Schema is the dominant module-scope cost of an
86
+ // action-heavy app's cold start, so convert only the schema the agent sees.
85
87
  let toolParameters;
86
88
  if (hasSchema) {
87
- toolParameters = schemaToJsonSchema(options.schema, options.description);
89
+ toolParameters = schemaToJsonSchema(options.agentInputSchema && "~standard" in options.agentInputSchema
90
+ ? options.agentInputSchema
91
+ : options.schema, options.description);
88
92
  }
89
93
  else if (options.parameters) {
90
94
  toolParameters = {
@@ -92,11 +96,6 @@ export function defineAction(options) {
92
96
  properties: options.parameters,
93
97
  };
94
98
  }
95
- if (hasSchema &&
96
- options.agentInputSchema &&
97
- "~standard" in options.agentInputSchema) {
98
- toolParameters = schemaToJsonSchema(options.agentInputSchema, options.description);
99
- }
100
99
  const guardedRun = typeof options.authorize === "function" || options.access
101
100
  ? wrapRunWithAccess(options.run, options.access, options.authorize)
102
101
  : options.run;
@@ -13,6 +13,12 @@ export declare const MAX_AUTO_CONTINUES_PER_TURN = 3;
13
13
  export declare const AUTO_CONTINUE_FRESHNESS_MS: number;
14
14
  /** Request metadata naming the stopped run an automatic continuation resumes. */
15
15
  export declare const AUTO_CONTINUE_OF_RUN_METADATA_KEY = "agentNativeAutoContinueOfRunId";
16
+ /**
17
+ * Request metadata naming the stopped run a person chose to continue. Like an
18
+ * automatic continuation it stays in the stopped run's turn, so the turn's
19
+ * journal of finished steps keeps applying; unlike one it has no cap.
20
+ */
21
+ export declare const CONTINUE_OF_RUN_METADATA_KEY = "agentNativeContinueOfRunId";
16
22
  export declare const AUTO_CONTINUE_PROMPT = "Continue from where you left off.";
17
23
  export declare const AUTO_CONTINUE_REFUSAL_CODES: readonly [
18
24
  /** The turn used its automatic continuations, or its wall-clock budget. */
@@ -20,6 +26,11 @@ export declare const AUTO_CONTINUE_REFUSAL_CODES: readonly [
20
26
  /** The named run is not the turn's newest fresh time-limit stop. */
21
27
  "auto_continue_unavailable"];
22
28
  export type AutoContinueRefusalCode = (typeof AUTO_CONTINUE_REFUSAL_CODES)[number];
29
+ /** The named run is not the turn's newest run, or it did not stop unfinished. */
30
+ export declare const CONTINUE_UNAVAILABLE_CODE = "continue_unavailable";
31
+ export type ContinueRefusalCode = AutoContinueRefusalCode | typeof CONTINUE_UNAVAILABLE_CODE;
32
+ /** Who asked to continue a stopped run. */
33
+ export type ContinueTrigger = "auto" | "manual";
23
34
  /**
24
35
  * The only stops a turn continues from on its own: the server cut the run at
25
36
  * its time limit. Errors, stops, cancellations, refusals, and credential or
@@ -29,6 +40,15 @@ export declare function isTimeLimitStop(run: {
29
40
  readonly status: string;
30
41
  readonly terminalReason?: string | null;
31
42
  }): boolean;
43
+ /**
44
+ * A run that ended before it finished for any reason other than a person
45
+ * stopping it: a time limit, a crash, or an error. These are the runs a
46
+ * person can continue in the same turn.
47
+ */
48
+ export declare function isUnfinishedStop(run: {
49
+ readonly status: string;
50
+ readonly terminalReason?: string | null;
51
+ }): boolean;
32
52
  /** A turn as its run rows record it; the rows are the durable count. */
33
53
  export interface TurnRunState {
34
54
  readonly newest: {
@@ -55,3 +75,18 @@ export declare function admitAutoContinue(input: {
55
75
  admit: false;
56
76
  code: AutoContinueRefusalCode;
57
77
  };
78
+ /**
79
+ * Whether a person may continue `stoppedRunId` in its own turn. Only the
80
+ * thread's newest stop qualifies: once anything newer ran, in this turn or a
81
+ * later one, continuing the older stop would redo work that already moved on.
82
+ */
83
+ export declare function admitManualContinue(input: {
84
+ readonly turn: TurnRunState | null;
85
+ readonly stoppedRunId: string;
86
+ readonly laterTurnStarted: boolean;
87
+ }): {
88
+ admit: true;
89
+ } | {
90
+ admit: false;
91
+ code: ContinueRefusalCode;
92
+ };
@@ -1,4 +1,5 @@
1
1
  import { MAX_TURN_WALL_CLOCK_MS } from "../app-config/run-lifecycle-invariants.js";
2
+ import { isRequestedStopAbortReason } from "./abort-reasons.js";
2
3
  /**
3
4
  * Automatic continuations of one user turn after the server stopped it at its
4
5
  * run time limit. Each one replays the turn's context to the model and runs up
@@ -14,6 +15,12 @@ export const MAX_AUTO_CONTINUES_PER_TURN = 3;
14
15
  export const AUTO_CONTINUE_FRESHNESS_MS = 10 * 60_000;
15
16
  /** Request metadata naming the stopped run an automatic continuation resumes. */
16
17
  export const AUTO_CONTINUE_OF_RUN_METADATA_KEY = "agentNativeAutoContinueOfRunId";
18
+ /**
19
+ * Request metadata naming the stopped run a person chose to continue. Like an
20
+ * automatic continuation it stays in the stopped run's turn, so the turn's
21
+ * journal of finished steps keeps applying; unlike one it has no cap.
22
+ */
23
+ export const CONTINUE_OF_RUN_METADATA_KEY = "agentNativeContinueOfRunId";
17
24
  // i18n-ignore: Internal continuation instruction sent to the agent, never shown as product copy.
18
25
  export const AUTO_CONTINUE_PROMPT = "Continue from where you left off.";
19
26
  export const AUTO_CONTINUE_REFUSAL_CODES = [
@@ -22,6 +29,8 @@ export const AUTO_CONTINUE_REFUSAL_CODES = [
22
29
  /** The named run is not the turn's newest fresh time-limit stop. */
23
30
  "auto_continue_unavailable",
24
31
  ];
32
+ /** The named run is not the turn's newest run, or it did not stop unfinished. */
33
+ export const CONTINUE_UNAVAILABLE_CODE = "continue_unavailable";
25
34
  /**
26
35
  * The only stops a turn continues from on its own: the server cut the run at
27
36
  * its time limit. Errors, stops, cancellations, refusals, and credential or
@@ -30,6 +39,19 @@ export const AUTO_CONTINUE_REFUSAL_CODES = [
30
39
  export function isTimeLimitStop(run) {
31
40
  return run.status === "truncated" && run.terminalReason === "run_timeout";
32
41
  }
42
+ /**
43
+ * A run that ended before it finished for any reason other than a person
44
+ * stopping it: a time limit, a crash, or an error. These are the runs a
45
+ * person can continue in the same turn.
46
+ */
47
+ export function isUnfinishedStop(run) {
48
+ if (run.status === "truncated" || run.status === "errored")
49
+ return true;
50
+ if (run.status !== "aborted")
51
+ return false;
52
+ const reason = run.terminalReason?.trim() ?? "";
53
+ return !isRequestedStopAbortReason(reason.startsWith("aborted:") ? reason.slice("aborted:".length) : reason);
54
+ }
33
55
  /**
34
56
  * Whether the turn may start another run that continues `stoppedRunId`. Read
35
57
  * from the turn's rows, so a reload or a second tab gets the same answer.
@@ -51,3 +73,19 @@ export function admitAutoContinue(input) {
51
73
  }
52
74
  return { admit: true };
53
75
  }
76
+ /**
77
+ * Whether a person may continue `stoppedRunId` in its own turn. Only the
78
+ * thread's newest stop qualifies: once anything newer ran, in this turn or a
79
+ * later one, continuing the older stop would redo work that already moved on.
80
+ */
81
+ export function admitManualContinue(input) {
82
+ const newest = input.turn?.newest;
83
+ if (input.laterTurnStarted ||
84
+ !newest ||
85
+ newest.id !== input.stoppedRunId ||
86
+ newest.completedAt === null ||
87
+ !isUnfinishedStop(newest)) {
88
+ return { admit: false, code: CONTINUE_UNAVAILABLE_CODE };
89
+ }
90
+ return { admit: true };
91
+ }
@@ -0,0 +1,52 @@
1
+ import type { AgentChatEvent } from "./types.js";
2
+ type ConnectionRequired = Extract<AgentChatEvent, {
3
+ type: "connection_required";
4
+ }>;
5
+ /** A connection request's message, plus who can unblock it when that is known. */
6
+ export declare function connectionRequiredMessage(message: string, reason: ConnectionRequired["reason"]): string;
7
+ export interface ThreadConnectionRequest {
8
+ request: ConnectionRequired;
9
+ /** How many of the thread's newer runs came after the one that asked. */
10
+ runsAgo: number;
11
+ }
12
+ /**
13
+ * The connection requests among a thread's recent runs for one organization
14
+ * (`null` for none), newest first. Provider connections are per organization
15
+ * and a thread can be reused across them, so another org's request is neither
16
+ * a note nor a slot in the window. The turn's initiator row is the only record
17
+ * of a run's org, so a run without one has no provable org and is left out.
18
+ * Throws when the ledger can't be read.
19
+ */
20
+ export declare function readThreadConnectionRequests(threadId: string, options: {
21
+ orgId: string | null;
22
+ excludeRunId?: string;
23
+ }): Promise<ThreadConnectionRequest[]>;
24
+ export type PriorConnectionNote = {
25
+ status: "none";
26
+ } | {
27
+ status: "connected";
28
+ } | {
29
+ status: "blocked";
30
+ note: string;
31
+ } | {
32
+ status: "unreadable";
33
+ error: string;
34
+ };
35
+ /** The context note a resolved state adds to the turn, or "" when it adds none. */
36
+ export declare function priorConnectionContextNote(prior: PriorConnectionNote): string;
37
+ /**
38
+ * The note that keeps a thread's next run from retrying a provider whose
39
+ * connection request ended an earlier run in the same organization. A request
40
+ * drops out once a workspace connection for that provider is available again,
41
+ * or, when nothing can re-check it, after a couple of runs. A ledger that can't
42
+ * be read in time is reported rather than read as "no request".
43
+ */
44
+ export declare function resolvePriorConnectionNote(input: {
45
+ threadId: string;
46
+ /** The current run's organization; `null` when it has none. */
47
+ orgId: string | null;
48
+ appId?: string;
49
+ excludeRunId?: string;
50
+ timeoutMs?: number;
51
+ }): Promise<PriorConnectionNote>;
52
+ export {};
@@ -0,0 +1,187 @@
1
+ import { getDbExec } from "../db/client.js";
2
+ import { resolveWorkspaceConnectionForApp } from "../workspace-connections/store.js";
3
+ import { ensureRunTables } from "./run-store.js";
4
+ /**
5
+ * A run that ended `connection_required` leaves no mark on its `agent_runs`
6
+ * row, so the event ledger is the only durable record. The lookup is bounded
7
+ * by run count and age so it stays cheap and an old request ages out. Turn-abort
8
+ * marker rows are bookkeeping, not runs, and must not use up the window.
9
+ */
10
+ const CONNECTION_REQUEST_RUN_WINDOW = 8;
11
+ const CONNECTION_REQUEST_MAX_AGE_MS = 6 * 60 * 60 * 1000;
12
+ const MAX_REQUEST_EVENT_ROWS = 16;
13
+ /** A request nothing can re-check outlives only this many runs. */
14
+ const UNVERIFIABLE_REQUEST_RUNS = 2;
15
+ const MAX_NOTE_PROVIDERS = 3;
16
+ /** Longer than this and the read costs the user more than the note saves. */
17
+ const PRIOR_CONNECTION_READ_TIMEOUT_MS = 400;
18
+ const REMEDY_BY_REASON = {
19
+ connect: "If you cannot connect it yourself, ask a workspace admin to connect it for the workspace.",
20
+ grant: "Ask a workspace admin to grant this app access to the existing connection.",
21
+ admin_required: "Only a workspace admin can do this; ask one to connect it.",
22
+ };
23
+ /** A connection request's message, plus who can unblock it when that is known. */
24
+ export function connectionRequiredMessage(message, reason) {
25
+ const remedy = REMEDY_BY_REASON[reason];
26
+ if (!remedy || /workspace admin/i.test(message))
27
+ return message;
28
+ return `${message} ${remedy}`;
29
+ }
30
+ /**
31
+ * Provider ids come from provider adapters and A2A peers, and the note lands in
32
+ * the user turn. Quoting or stripping a free-text name makes the note's shape
33
+ * safe, not its content, so only a plain identifier is ever echoed; anything
34
+ * else is "an external provider".
35
+ */
36
+ const PROVIDER_ID = /^[A-Za-z0-9][\w.:-]{0,63}$/;
37
+ const UNNAMED_PROVIDER = "an external provider";
38
+ function noteProviderName(request) {
39
+ return typeof request.provider === "string" &&
40
+ PROVIDER_ID.test(request.provider)
41
+ ? `"${request.provider}"`
42
+ : UNNAMED_PROVIDER;
43
+ }
44
+ /**
45
+ * The connection requests among a thread's recent runs for one organization
46
+ * (`null` for none), newest first. Provider connections are per organization
47
+ * and a thread can be reused across them, so another org's request is neither
48
+ * a note nor a slot in the window. The turn's initiator row is the only record
49
+ * of a run's org, so a run without one has no provable org and is left out.
50
+ * Throws when the ledger can't be read.
51
+ */
52
+ export async function readThreadConnectionRequests(threadId, options) {
53
+ await ensureRunTables();
54
+ const db = getDbExec();
55
+ const { rows: runRows } = await db.execute({
56
+ sql: `SELECT r.id FROM agent_runs r
57
+ JOIN agent_turn_initiators i
58
+ ON i.thread_id = r.thread_id
59
+ AND i.turn_id = COALESCE(r.turn_id, r.id)
60
+ WHERE r.thread_id = ?
61
+ AND r.started_at >= ?
62
+ AND r.dispatch_mode IS DISTINCT FROM 'turn-abort'
63
+ AND r.id <> ?
64
+ AND ${options.orgId ? "i.org_id = ?" : "i.org_id IS NULL"}
65
+ ORDER BY r.started_at DESC
66
+ LIMIT ?`,
67
+ args: [
68
+ threadId,
69
+ Date.now() - CONNECTION_REQUEST_MAX_AGE_MS,
70
+ options.excludeRunId ?? "",
71
+ ...(options.orgId ? [options.orgId] : []),
72
+ CONNECTION_REQUEST_RUN_WINDOW,
73
+ ],
74
+ });
75
+ const runIds = runRows.map((row) => row.id);
76
+ if (runIds.length === 0)
77
+ return [];
78
+ const { rows } = await db.execute({
79
+ sql: `SELECT run_id, event_data
80
+ FROM agent_run_events
81
+ WHERE run_id IN (${runIds.map(() => "?").join(", ")})
82
+ AND event_data LIKE '{"type":"connection_required"%' /* guard:allow-blob-predicate — scoped to one thread's last 8 runs in 6h by the (run_id, seq) key; the marker has no column of its own, so a column plus backfill is the follow-up */
83
+ ORDER BY event_at DESC NULLS LAST, seq DESC
84
+ LIMIT ?`,
85
+ args: [...runIds, MAX_REQUEST_EVENT_ROWS],
86
+ });
87
+ const found = [];
88
+ for (const row of rows) {
89
+ if (!row.event_data)
90
+ continue;
91
+ const request = JSON.parse(row.event_data);
92
+ if (request.type === "connection_required") {
93
+ found.push({ request, runsAgo: runIds.indexOf(row.run_id) });
94
+ }
95
+ }
96
+ return found.sort((a, b) => a.runsAgo - b.runsAgo);
97
+ }
98
+ const PRIOR_CONNECTION_UNREADABLE_NOTE = "\n\n<context-note>Prior-run connection state could not be read this turn; if a provider call returns connection_required, stop and tell the user instead of retrying.</context-note>";
99
+ /** The context note a resolved state adds to the turn, or "" when it adds none. */
100
+ export function priorConnectionContextNote(prior) {
101
+ if (prior.status === "blocked")
102
+ return prior.note;
103
+ if (prior.status === "unreadable")
104
+ return PRIOR_CONNECTION_UNREADABLE_NOTE;
105
+ return "";
106
+ }
107
+ // `request.detail` and `source.label` are peer-supplied free text: stripping
108
+ // characters does not make an instruction inert, so neither reaches the note.
109
+ function priorConnectionNote(requests) {
110
+ const many = requests.length > 1;
111
+ return (`\n\n<context-note>${requests.map(noteProviderName).join(", ")} ${many ? "were" : "was"} not connected for this workspace when last tried in this thread. ` +
112
+ `Do not call ${many ? "them" : "it"} again unless the user says ${many ? "they are" : "it is"} now connected; use another connected source that can answer the request, or tell the user who can connect ${many ? "them" : "it"}.</context-note>`);
113
+ }
114
+ async function resolveNote(input) {
115
+ const appIdFor = (request) => request.appId ?? input.appId;
116
+ const canRecheck = (request) => request.source?.kind === "workspace_connection" && !!appIdFor(request);
117
+ // One entry per named provider, newest first: each run's failure names a
118
+ // different provider, and a note about only the latest sends the model to
119
+ // the one before it. Providers with no plain id share one unnamed entry.
120
+ const seen = new Set();
121
+ const candidates = (await readThreadConnectionRequests(input.threadId, {
122
+ orgId: input.orgId,
123
+ excludeRunId: input.excludeRunId,
124
+ }))
125
+ .filter(({ request }) => {
126
+ const key = noteProviderName(request).toLowerCase();
127
+ if (seen.has(key))
128
+ return false;
129
+ seen.add(key);
130
+ return true;
131
+ })
132
+ .filter(({ request, runsAgo }) => canRecheck(request) || runsAgo < UNVERIFIABLE_REQUEST_RUNS)
133
+ .slice(0, MAX_NOTE_PROVIDERS);
134
+ if (candidates.length === 0)
135
+ return { status: "none" };
136
+ const stillBlocked = await Promise.all(candidates.map(async ({ request }) => {
137
+ if (!canRecheck(request))
138
+ return true;
139
+ try {
140
+ const resolved = await resolveWorkspaceConnectionForApp({
141
+ appId: appIdFor(request),
142
+ provider: request.provider,
143
+ requireConnected: true,
144
+ });
145
+ return !resolved.available;
146
+ }
147
+ catch (error) {
148
+ console.warn("[agent-chat] could not re-check workspace connection; keeping the earlier request:", error instanceof Error ? error.message : String(error));
149
+ return true;
150
+ }
151
+ }));
152
+ const blocked = candidates.filter((_, index) => stillBlocked[index]);
153
+ if (blocked.length === 0)
154
+ return { status: "connected" };
155
+ return {
156
+ status: "blocked",
157
+ note: priorConnectionNote(blocked.map(({ request }) => request)),
158
+ };
159
+ }
160
+ /**
161
+ * The note that keeps a thread's next run from retrying a provider whose
162
+ * connection request ended an earlier run in the same organization. A request
163
+ * drops out once a workspace connection for that provider is available again,
164
+ * or, when nothing can re-check it, after a couple of runs. A ledger that can't
165
+ * be read in time is reported rather than read as "no request".
166
+ */
167
+ export async function resolvePriorConnectionNote(input) {
168
+ const timeoutMs = input.timeoutMs ?? PRIOR_CONNECTION_READ_TIMEOUT_MS;
169
+ const unreadable = (error) => {
170
+ const message = error instanceof Error ? error.message : String(error);
171
+ console.warn("[agent-chat] prior connection request unreadable:", message);
172
+ return { status: "unreadable", error: message };
173
+ };
174
+ let timer;
175
+ const timedOut = new Promise((resolve) => {
176
+ timer = setTimeout(() => resolve(unreadable(`timed out after ${timeoutMs}ms`)), timeoutMs);
177
+ });
178
+ try {
179
+ return await Promise.race([resolveNote(input), timedOut]);
180
+ }
181
+ catch (error) {
182
+ return unreadable(error);
183
+ }
184
+ finally {
185
+ clearTimeout(timer);
186
+ }
187
+ }
@@ -35,7 +35,11 @@ export interface JevRankCandidatesOptions {
35
35
  timeoutMs?: number;
36
36
  }
37
37
  export interface JevCandidateRanking {
38
- status: "selected" | "no-match" | "unavailable";
38
+ /**
39
+ * `unavailable` means there was nothing to rank against (no key, no
40
+ * candidates); `failed` and `timed_out` mean a ranking was attempted and lost.
41
+ */
42
+ status: "selected" | "no-match" | "unavailable" | "failed" | "timed_out";
39
43
  ids: string[];
40
44
  }
41
45
  export declare function buildJevRequestContext(input: {
@@ -104,11 +104,13 @@ export async function rankJevCandidatesWithStatus(options) {
104
104
  const builderAuth = options.builderAuth;
105
105
  if (!request ||
106
106
  (!apiKey && !builderAuth) ||
107
- options.candidates.length === 0 ||
108
- options.signal?.aborted ||
109
- (options.timeoutMs !== undefined && options.timeoutMs <= 0)) {
107
+ options.candidates.length === 0) {
110
108
  return { status: "unavailable", ids: [] };
111
109
  }
110
+ if (options.signal?.aborted ||
111
+ (options.timeoutMs !== undefined && options.timeoutMs <= 0)) {
112
+ return { status: "timed_out", ids: [] };
113
+ }
112
114
  const candidates = shortlistJevCandidates(request, options.candidates);
113
115
  if (candidates.length === 0)
114
116
  return { status: "unavailable", ids: [] };
@@ -161,7 +163,7 @@ export async function rankJevCandidatesWithStatus(options) {
161
163
  !Number.isFinite(noMatchProbability) ||
162
164
  noMatchProbability < 0 ||
163
165
  noMatchProbability > 1) {
164
- return { status: "unavailable", ids: [] };
166
+ return { status: "failed", ids: [] };
165
167
  }
166
168
  for (const candidate of candidates) {
167
169
  const probability = probabilities[candidate.id];
@@ -170,7 +172,7 @@ export async function rankJevCandidatesWithStatus(options) {
170
172
  !Number.isFinite(probability) ||
171
173
  probability < 0 ||
172
174
  probability > 1)) {
173
- return { status: "unavailable", ids: [] };
175
+ return { status: "failed", ids: [] };
174
176
  }
175
177
  }
176
178
  const ids = candidates
@@ -191,9 +193,20 @@ export async function rankJevCandidatesWithStatus(options) {
191
193
  if (error instanceof OptionalPeerDependencyError)
192
194
  throw error;
193
195
  console.warn("[agent] Jev context prefetch unavailable; continuing with the existing context.", error instanceof Error ? error.message : "unknown error");
194
- return { status: "unavailable", ids: [] };
196
+ return {
197
+ status: isJevTimeout(error, options.signal) ? "timed_out" : "failed",
198
+ ids: [],
199
+ };
195
200
  }
196
201
  }
202
+ function isJevTimeout(error, signal) {
203
+ if (signal?.aborted)
204
+ return true;
205
+ return (error instanceof Error &&
206
+ (error.name === "AbortError" ||
207
+ error.name === "TimeoutError" ||
208
+ /\btime(?:d)?[ -]?out\b/i.test(error.message)));
209
+ }
197
210
  export function shortlistJevCandidates(request, candidates) {
198
211
  if (candidates.length <= MAX_JEV_CANDIDATES)
199
212
  return [...candidates];
@@ -0,0 +1,9 @@
1
+ /**
2
+ * What a new turn needs to know when the turn before it stopped unfinished.
3
+ * A new turn sees earlier turns as text only, and a turn cut off mid-tool may
4
+ * have no text at all, so without this a "continue" redoes every step. The
5
+ * record comes from the run ledger, which a crash cannot lose the way it can
6
+ * lose the thread's saved reply. Null when the previous turn finished, never
7
+ * started, or ran no tools; a caution, not null, when that cannot be read.
8
+ */
9
+ export declare function loadStoppedPreviousTurnNote(threadId: string, turnId: string): Promise<string | null>;
@@ -0,0 +1,48 @@
1
+ import { captureError } from "../server/capture-error.js";
2
+ import { loadPriorTurnToolCallJournal } from "./engine/tool-call-journal-seed.js";
3
+ import { getPreviousTurnNewestRun } from "./run-store.js";
4
+ import { buildResumeJournalNote } from "./tool-call-journal.js";
5
+ // i18n-ignore: Internal model context, never shown as product copy.
6
+ const STOPPED_TURN_INTRO = "Your previous reply in this conversation stopped before it finished, so the history above does not show the tool calls it made. Unless the user asks you to redo something, treat the record below as what already happened and do not repeat a completed step.";
7
+ // i18n-ignore: Internal model context, never shown as product copy.
8
+ const STOPPED_TURN_UNREADABLE = "Your previous reply in this conversation stopped before it finished, and its record of tool calls could not be read. Some of its actions may already have run: before repeating anything with a side effect (sending, charging, creating, deleting), check whether it already happened.";
9
+ // i18n-ignore: Internal model context, never shown as product copy.
10
+ const PREVIOUS_TURN_UNCHECKED = "Whether your previous reply in this conversation finished could not be checked. Before repeating anything with a side effect (sending, charging, creating, deleting), check whether it already happened.";
11
+ /**
12
+ * What a new turn needs to know when the turn before it stopped unfinished.
13
+ * A new turn sees earlier turns as text only, and a turn cut off mid-tool may
14
+ * have no text at all, so without this a "continue" redoes every step. The
15
+ * record comes from the run ledger, which a crash cannot lose the way it can
16
+ * lose the thread's saved reply. Null when the previous turn finished, never
17
+ * started, or ran no tools; a caution, not null, when that cannot be read.
18
+ */
19
+ export async function loadStoppedPreviousTurnNote(threadId, turnId) {
20
+ let previous;
21
+ try {
22
+ previous = await getPreviousTurnNewestRun(threadId, turnId);
23
+ }
24
+ catch (error) {
25
+ captureError(error, {
26
+ tags: { source: "agent-chat", failureClass: "previous-turn-note" },
27
+ extra: { threadId, turnId },
28
+ });
29
+ return `<previous-turn-stopped>
30
+ ${PREVIOUS_TURN_UNCHECKED}
31
+ </previous-turn-stopped>`;
32
+ }
33
+ if (!previous ||
34
+ previous.status === "completed" ||
35
+ previous.status === "running") {
36
+ return null;
37
+ }
38
+ const journal = await loadPriorTurnToolCallJournal(threadId, previous.turnId);
39
+ if (journal.status === "unreadable") {
40
+ return `<previous-turn-stopped>\n${STOPPED_TURN_UNREADABLE}\n</previous-turn-stopped>`;
41
+ }
42
+ const record = journal.toolCallJournal
43
+ ? buildResumeJournalNote(journal.toolCallJournal)
44
+ : null;
45
+ return record
46
+ ? `<previous-turn-stopped>\n${STOPPED_TURN_INTRO}\n\n${record}\n</previous-turn-stopped>`
47
+ : null;
48
+ }
@@ -9,6 +9,7 @@ import { type JevPromptContextCandidate } from "../server/agent-chat/prompt-reso
9
9
  import { type BuilderGatewayAuth } from "../server/credential-provider.js";
10
10
  import { fireInternalDispatch } from "../server/self-dispatch.js";
11
11
  import { type RefusedTurnRetryContext } from "../shared/agent-chat-run-not-started.js";
12
+ import { type ContextStatus } from "../shared/context-status.js";
12
13
  import { type ReasoningEffort } from "../shared/reasoning-effort.js";
13
14
  import type { TrackingMeta } from "../tracking/registry.js";
14
15
  import { type ResolveEngineConfig } from "./engine/index.js";
@@ -171,6 +172,8 @@ export interface ActionEntry {
171
172
  deferLoading?: boolean;
172
173
  publicAgent?: import("../action.js").PublicAgentActionConfig;
173
174
  readOnly?: boolean;
175
+ /** Bookkeeping writes still need replay protection, but cannot confirm automation work. */
176
+ confirmsAutomationWork?: boolean;
174
177
  grounding?: boolean;
175
178
  allowInPlanMode?: boolean;
176
179
  planMode?: import("../action.js").ActionPlanModeConfig<any>;
@@ -238,6 +241,19 @@ export declare function createPlanModeActionRegistry(actions: Record<string, Act
238
241
  type AgentRunTrackingSource = Pick<TrackingMeta, "userId" | "authUserId" | "anonymousId" | "sessionId"> & {
239
242
  isSyntheticTraffic?: boolean;
240
243
  };
244
+ export interface PreparedAgentRequest {
245
+ message?: string;
246
+ displayMessage?: string;
247
+ attachments?: AgentChatAttachment[];
248
+ jevPromptCandidates?: JevPromptContextCandidate[];
249
+ jevFallbackCandidateIds?: string[];
250
+ /**
251
+ * Whether the app's own reference retrieval reached the model. Omit it to
252
+ * report nothing; `timed_out` and `failed` tell the model it is working
253
+ * without references it would normally have.
254
+ */
255
+ status?: ContextStatus;
256
+ }
241
257
  export interface ProductionAgentOptions {
242
258
  actions?: Record<string, ActionEntry>;
243
259
  /** @deprecated Use `actions` instead */
@@ -301,19 +317,7 @@ export interface ProductionAgentOptions {
301
317
  dispatchToBackground: boolean;
302
318
  isBackgroundWorker?: boolean;
303
319
  mode: AgentExecutionMode;
304
- }) => void | {
305
- message?: string;
306
- displayMessage?: string;
307
- attachments?: AgentChatAttachment[];
308
- jevPromptCandidates?: JevPromptContextCandidate[];
309
- jevFallbackCandidateIds?: string[];
310
- } | Promise<void | {
311
- message?: string;
312
- displayMessage?: string;
313
- attachments?: AgentChatAttachment[];
314
- jevPromptCandidates?: JevPromptContextCandidate[];
315
- jevFallbackCandidateIds?: string[];
316
- }>;
320
+ }) => void | PreparedAgentRequest | Promise<void | PreparedAgentRequest>;
317
321
  resolveActionSurface?: (details: AgentActionSurfaceDetails) => AgentActionSurfaceResolution | Promise<AgentActionSurfaceResolution>;
318
322
  runSoftTimeoutMs?: number;
319
323
  runNoProgressTimeoutMs?: number;