@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.
- package/dist/action.js +5 -6
- package/dist/agent/auto-continue.d.ts +35 -0
- package/dist/agent/auto-continue.js +38 -0
- package/dist/agent/connection-required-note.d.ts +52 -0
- package/dist/agent/connection-required-note.js +187 -0
- package/dist/agent/jev-tool-prefetch.d.ts +5 -1
- package/dist/agent/jev-tool-prefetch.js +19 -6
- package/dist/agent/previous-turn-note.d.ts +9 -0
- package/dist/agent/previous-turn-note.js +48 -0
- package/dist/agent/production-agent.d.ts +17 -13
- package/dist/agent/production-agent.js +185 -43
- package/dist/agent/reaped-run-thread.d.ts +13 -0
- package/dist/agent/reaped-run-thread.js +45 -0
- package/dist/agent/run-store.d.ts +29 -5
- package/dist/agent/run-store.js +144 -30
- package/dist/agent/thread-data-builder.d.ts +16 -0
- package/dist/agent/thread-data-builder.js +30 -8
- package/dist/agent/thread-message-projection.js +5 -3
- package/dist/agent/tool-search.js +5 -1
- package/dist/agent/types.d.ts +3 -0
- package/dist/audit/config.d.ts +6 -0
- package/dist/audit/config.js +12 -1
- package/dist/audit/index.d.ts +1 -1
- package/dist/audit/index.js +1 -1
- package/dist/audit/store.js +39 -4
- package/dist/cli/skills.d.ts +1 -0
- package/dist/cli/skills.js +1 -1
- package/dist/client/chat/agentkit-agent-native.js +83 -7
- package/dist/client/chat/agentkit-protocol.js +140 -67
- package/dist/client/chat/runtime.js +5 -3
- package/dist/client/use-run-stuck-detection.d.ts +18 -1
- package/dist/client/use-run-stuck-detection.js +115 -75
- package/dist/db/request-telemetry.d.ts +7 -1
- package/dist/db/request-telemetry.js +20 -1
- package/dist/deploy/build.d.ts +25 -1
- package/dist/deploy/build.js +176 -3
- package/dist/deploy/workspace-deploy.js +8 -3
- package/dist/extensions/fetch-tool.js +50 -14
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/integrations/adapters/email.js +11 -3
- package/dist/integrations/adapters/telegram.js +4 -2
- package/dist/jobs/actions/automation-no-op.d.ts +12 -0
- package/dist/jobs/actions/automation-no-op.js +20 -0
- package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
- package/dist/jobs/background-automation-runner.d.ts +8 -1
- package/dist/jobs/background-automation-runner.js +102 -19
- package/dist/jobs/run-history.d.ts +1 -1
- package/dist/jobs/run-history.js +8 -4
- package/dist/jobs/scheduler.js +25 -12
- package/dist/localization/automation-outcome-messages.d.ts +22 -0
- package/dist/localization/automation-outcome-messages.js +90 -0
- package/dist/localization/default-messages.d.ts +2 -0
- package/dist/localization/default-messages.js +4 -2
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/metrics.d.ts +3 -0
- package/dist/observability/metrics.js +10 -0
- package/dist/observability/migrations.js +22 -0
- package/dist/observability/routes.d.ts +13 -8
- package/dist/observability/routes.js +88 -14
- package/dist/observability/sentiment.d.ts +5 -0
- package/dist/observability/sentiment.js +75 -25
- package/dist/observability/store.d.ts +28 -0
- package/dist/observability/store.js +58 -0
- package/dist/observability/traces.js +37 -5
- package/dist/org/index.d.ts +2 -2
- package/dist/progress/actions.js +1 -0
- package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
- package/dist/provider-api/actions/provider-api.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.js +6 -2
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/provider-api/index.d.ts +1 -0
- package/dist/provider-api/index.js +18 -0
- package/dist/provider-api/result-outcome.d.ts +1 -0
- package/dist/provider-api/result-outcome.js +20 -0
- package/dist/provider-api/staging.js +10 -9
- package/dist/server/agent-chat/prompt-resources.d.ts +8 -2
- package/dist/server/agent-chat/prompt-resources.js +30 -7
- package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +19 -4
- package/dist/server/agent-chat/recurring-jobs-runtime.js +52 -16
- package/dist/server/agent-chat/sweep-trigger-auth.d.ts +18 -0
- package/dist/server/agent-chat/sweep-trigger-auth.js +36 -0
- package/dist/server/agent-chat-plugin.js +20 -13
- package/dist/server/agent-engine-default-model-route.d.ts +2 -2
- package/dist/server/auth.js +4 -4
- package/dist/server/framework-request-handler.js +19 -0
- package/dist/server/http-response-telemetry.js +4 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/request-context.d.ts +6 -0
- package/dist/shared/context-status.d.ts +21 -0
- package/dist/shared/context-status.js +54 -0
- package/dist/templates/chat/app/components/chat/ChatRouteContent.tsx +8 -1
- package/dist/templates/workspace-core/.agents/skills/automations/SKILL.md +8 -1
- package/dist/templates/workspace-core/.agents/skills/recurring-jobs/SKILL.md +3 -1
- package/dist/triggers/actions/manage-automation.d.ts +7 -7
- package/dist/triggers/dispatcher.js +10 -7
- package/docs/content/audit-log.mdx +3 -3
- package/docs/content/cloudflare.mdx +27 -0
- package/docs/content/deployment-environment-variables.mdx +2 -0
- package/docs/content/durable-resume.mdx +3 -1
- package/docs/content/environment-variables.mdx +2 -0
- package/docs/content/recurring-jobs.mdx +29 -9
- package/docs/content/vercel.mdx +30 -0
- package/package.json +3 -3
- package/src/templates/chat/app/components/chat/ChatRouteContent.tsx +8 -1
- package/src/templates/workspace-core/.agents/skills/automations/SKILL.md +8 -1
- 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.
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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 {
|
|
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;
|