@agent-native/core 0.144.2 → 0.145.0
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/corpus/README.md +1 -1
- package/corpus/templates/analytics/app/pages/DataSources.tsx +28 -4
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +7 -3
- package/corpus/templates/analytics/server/plugins/dashboard-report-jobs.ts +13 -3
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +64 -23
- package/corpus/templates/calendar/app/hooks/use-google-auth.ts +69 -46
- package/corpus/templates/calendar/app/hooks/use-view-preferences.ts +35 -6
- package/corpus/templates/calendar/app/hooks/use-zoom-auth.ts +24 -3
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +45 -6
- package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +38 -5
- package/corpus/templates/clips/changelog/2026-08-06-desktop-full-screen-recordings-now-normalize-microphone-leve.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +146 -45
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +185 -69
- package/corpus/templates/clips/server/jobs/brain-export.ts +13 -6
- package/corpus/templates/clips/server/jobs/builder-media-compression.ts +10 -3
- package/corpus/templates/clips/server/jobs/meeting-reminders.ts +10 -3
- package/corpus/templates/clips/server/jobs/poll-calendars.ts +23 -21
- package/corpus/templates/clips/server/jobs/stale-meeting-sweeper.ts +10 -3
- package/corpus/templates/clips/server/jobs/transactional-emails.ts +10 -3
- package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +1 -0
- package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +15 -2
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +47 -4
- package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +82 -39
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +31 -9
- package/corpus/templates/mail/server/plugins/mail-jobs.ts +36 -17
- package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
- package/dist/agent/production-agent.d.ts +6 -0
- package/dist/client/AssistantChat.js +73 -43
- package/dist/client/MultiTabAssistantChat.d.ts +3 -1
- package/dist/client/MultiTabAssistantChat.js +79 -92
- package/dist/client/browser-session-bridge.js +97 -54
- package/dist/client/db-admin/useAgentSync.js +34 -43
- package/dist/client/dynamic-suggestions.d.ts +3 -0
- package/dist/client/dynamic-suggestions.js +54 -52
- package/dist/client/hooks/index.d.ts +1 -0
- package/dist/client/hooks/index.js +1 -0
- package/dist/client/i18n.d.ts +7 -0
- package/dist/client/i18n.js +12 -3
- package/dist/client/integrations/useIntegrationStatus.d.ts +1 -1
- package/dist/client/integrations/useIntegrationStatus.js +9 -22
- package/dist/client/notifications/NotificationsBell.js +14 -9
- package/dist/client/progress/RunsTray.js +6 -5
- package/dist/client/settings/useBuilderStatus.js +95 -73
- package/dist/client/transcription/BuilderTranscriptionCta.js +27 -31
- package/dist/client/use-agent-engine-configured.js +21 -3
- package/dist/client/use-pausing-interval.d.ts +6 -0
- package/dist/client/use-pausing-interval.js +6 -0
- package/dist/client/use-poll-loop.d.ts +39 -0
- package/dist/client/use-poll-loop.js +73 -0
- package/dist/client/use-run-stuck-detection.js +41 -13
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/framework-tools.d.ts +126 -0
- package/dist/framework-tools.js +114 -0
- package/dist/integrations/google-docs-poller.js +26 -7
- package/dist/integrations/pending-tasks-retry-job.js +17 -23
- package/dist/integrations/plugin.js +16 -15
- package/dist/integrations/remote-push-delivery-job.js +16 -25
- package/dist/integrations/remote-retry-job.js +19 -23
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/oauth-tokens/google-refresh.js +12 -19
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/action-discovery.d.ts +27 -0
- package/dist/server/action-discovery.js +89 -0
- package/dist/server/agent-chat/action-filters-a2a.d.ts +11 -0
- package/dist/server/agent-chat/action-filters-a2a.js +17 -1
- package/dist/server/agent-chat/framework-prompts.d.ts +2 -0
- package/dist/server/agent-chat/plugin-options.d.ts +30 -0
- package/dist/server/agent-chat/prompt-resources.d.ts +4 -1
- package/dist/server/agent-chat/prompt-resources.js +8 -2
- package/dist/server/agent-chat-plugin.js +148 -100
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/interval-job.d.ts +25 -0
- package/dist/server/interval-job.js +19 -0
- package/dist/server/prompts/framework-core-compact.d.ts +5 -0
- package/dist/server/prompts/framework-core-compact.js +28 -12
- package/dist/server/prompts/framework-core.d.ts +5 -0
- package/dist/server/prompts/framework-core.js +33 -16
- package/dist/server/prompts/index.d.ts +1 -1
- package/dist/server/prompts/index.js +1 -1
- package/dist/server/prompts/shared-rules.d.ts +12 -4
- package/dist/server/prompts/shared-rules.js +14 -4
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/poll-engine.d.ts +49 -0
- package/dist/shared/poll-engine.js +148 -0
- package/dist/templates/default/.agents/skills/internationalization/SKILL.md +5 -0
- package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -3
- package/dist/templates/workspace-core/.agents/skills/internationalization/SKILL.md +5 -0
- package/dist/tracking/providers.js +10 -1
- package/docs/content/deployment.mdx +47 -13
- package/docs/content/key-concepts.mdx +13 -6
- package/package.json +2 -1
- package/src/templates/default/.agents/skills/internationalization/SKILL.md +5 -0
- package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -3
- package/src/templates/workspace-core/.agents/skills/internationalization/SKILL.md +5 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState, useCallback } from "react";
|
|
2
|
+
import { createPollEngine } from "../shared/poll-engine.js";
|
|
2
3
|
import { agentNativePath } from "./api-path.js";
|
|
3
4
|
const DEFAULT_STUCK_THRESHOLD_MS = 90_000;
|
|
4
5
|
export const DEFAULT_BACKGROUND_STUCK_THRESHOLD_MS = 180_000;
|
|
@@ -7,6 +8,12 @@ const DEFAULT_POLL_INTERVAL_MS = 5_000;
|
|
|
7
8
|
const IDLE_BACKOFF_INTERVAL_MS = 15_000;
|
|
8
9
|
const MAX_POLL_ERROR_BACKOFF_MS = 30_000;
|
|
9
10
|
const FRESH_BACKGROUND_HEARTBEAT_MS = 30_000;
|
|
11
|
+
// Bounds each poll fetch so a hung request can't stall the self-rescheduling
|
|
12
|
+
// setTimeout loop forever (the reschedule only fires once the fetch settles).
|
|
13
|
+
const POLL_ABORT_MIN_MS = 10_000;
|
|
14
|
+
function getPollAbortMs(interval) {
|
|
15
|
+
return Math.max(POLL_ABORT_MIN_MS, interval * 4);
|
|
16
|
+
}
|
|
10
17
|
const EMPTY_STATE = {
|
|
11
18
|
isStuck: false,
|
|
12
19
|
runId: null,
|
|
@@ -28,10 +35,15 @@ export function useRunStuckDetection({ threadId, enabled = true, stuckThresholdM
|
|
|
28
35
|
return;
|
|
29
36
|
const base = apiUrl ?? agentNativePath("/_agent-native/agent-chat");
|
|
30
37
|
let cancelled = false;
|
|
31
|
-
let timer = null;
|
|
32
38
|
let snapshotTransitionTimer = null;
|
|
33
39
|
let snapshotVersion = 0;
|
|
34
40
|
let consecutivePollFailures = 0;
|
|
41
|
+
// The engine's own cadence varies per tick (idle backoff, error backoff),
|
|
42
|
+
// which is why intervalMs below reads from this ref instead of a fixed
|
|
43
|
+
// number. Stagger the very first poll so a freshly-started run isn't
|
|
44
|
+
// immediately classified as stuck before the server has had a chance to
|
|
45
|
+
// record any progress events.
|
|
46
|
+
const nextDelayRef = { current: 2_000 };
|
|
35
47
|
const pollFailureDelay = () => {
|
|
36
48
|
consecutivePollFailures += 1;
|
|
37
49
|
return Math.min(Math.max(pollIntervalMs, 1) * 2 ** consecutivePollFailures, MAX_POLL_ERROR_BACKOFF_MS);
|
|
@@ -124,12 +136,10 @@ export function useRunStuckDetection({ threadId, enabled = true, stuckThresholdM
|
|
|
124
136
|
scheduleSnapshotTransition(snapshot, observedAtMs, version);
|
|
125
137
|
}, delayMs);
|
|
126
138
|
};
|
|
127
|
-
const
|
|
128
|
-
if (cancelled)
|
|
129
|
-
return;
|
|
139
|
+
const attempt = async (signal) => {
|
|
130
140
|
let nextDelay = pollIntervalMs;
|
|
131
141
|
try {
|
|
132
|
-
const res = await fetch(`${base}/runs/active?threadId=${encodeURIComponent(threadId)}`, { credentials: "same-origin" });
|
|
142
|
+
const res = await fetch(`${base}/runs/active?threadId=${encodeURIComponent(threadId)}`, { credentials: "same-origin", signal });
|
|
133
143
|
if (cancelled)
|
|
134
144
|
return;
|
|
135
145
|
if (res.ok) {
|
|
@@ -203,21 +213,39 @@ export function useRunStuckDetection({ threadId, enabled = true, stuckThresholdM
|
|
|
203
213
|
// while the server or database recovers.
|
|
204
214
|
nextDelay = pollFailureDelay();
|
|
205
215
|
}
|
|
206
|
-
|
|
207
|
-
|
|
216
|
+
finally {
|
|
217
|
+
nextDelayRef.current = nextDelay;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
// The per-tick backoff above needs a variable cadence, which is why this
|
|
221
|
+
// uses createPollEngine directly instead of usePollLoop (whose public
|
|
222
|
+
// intervalMs is a fixed number) — see poll-engine.ts for the
|
|
223
|
+
// never-overlaps/never-stalls guarantees this still gets for free.
|
|
224
|
+
const engine = createPollEngine(attempt, {
|
|
225
|
+
intervalMs: () => nextDelayRef.current,
|
|
226
|
+
timeoutMs: getPollAbortMs(pollIntervalMs),
|
|
227
|
+
leading: false,
|
|
228
|
+
});
|
|
229
|
+
engine.start();
|
|
230
|
+
const onVisibilityChange = () => {
|
|
231
|
+
if (document.hidden) {
|
|
232
|
+
engine.stop();
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
// Resume immediately rather than waiting out whatever delay was
|
|
236
|
+
// pending before the tab was hidden (which may be a long backoff).
|
|
237
|
+
engine.start();
|
|
238
|
+
engine.pollNow();
|
|
208
239
|
}
|
|
209
240
|
};
|
|
210
|
-
|
|
211
|
-
// classified as stuck before the server has had a chance to record
|
|
212
|
-
// any progress events.
|
|
213
|
-
timer = setTimeout(poll, 2_000);
|
|
241
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
214
242
|
return () => {
|
|
215
243
|
cancelled = true;
|
|
216
244
|
snapshotVersion += 1;
|
|
217
|
-
|
|
218
|
-
clearTimeout(timer);
|
|
245
|
+
engine.stop();
|
|
219
246
|
if (snapshotTransitionTimer)
|
|
220
247
|
clearTimeout(snapshotTransitionTimer);
|
|
248
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
221
249
|
};
|
|
222
250
|
}, [
|
|
223
251
|
threadId,
|
|
@@ -17,11 +17,11 @@ declare const _default: import("../../action.js").ActionDefinition<{
|
|
|
17
17
|
id?: undefined;
|
|
18
18
|
provider?: undefined;
|
|
19
19
|
} | {
|
|
20
|
+
error?: undefined;
|
|
20
21
|
configured?: undefined;
|
|
21
22
|
connectPath?: undefined;
|
|
22
23
|
url: string;
|
|
23
24
|
id: string;
|
|
24
25
|
provider: string;
|
|
25
|
-
error?: undefined;
|
|
26
26
|
}>;
|
|
27
27
|
export default _default;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { type DatabaseToolsOption } from "./scripts/db/tool-mode.js";
|
|
2
|
+
/**
|
|
3
|
+
* Optional framework tool groups an app can turn off through `frameworkTools`.
|
|
4
|
+
*
|
|
5
|
+
* Each name is also stamped onto the group's actions as
|
|
6
|
+
* `ActionEntry.frameworkGroup`. That tag is what lets a single filter subtract a
|
|
7
|
+
* group from every agent tool surface at once: `agent-chat-plugin.ts` spreads
|
|
8
|
+
* these registries at thirteen separate composition sites (interactive chat,
|
|
9
|
+
* dev, MCP-full, A2A, background jobs, triggers), and a per-site condition would
|
|
10
|
+
* be the same omission waiting to happen thirteen times.
|
|
11
|
+
*
|
|
12
|
+
* The tag has a second job: framework-tagged actions are excluded from the
|
|
13
|
+
* DEFAULT first-request tool list, so an app pays for the ~40 kit schemas only
|
|
14
|
+
* when it names them in `initialToolNames`. They stay in the searchable registry
|
|
15
|
+
* either way.
|
|
16
|
+
*/
|
|
17
|
+
export declare const FRAMEWORK_TOOL_GROUPS: readonly ["sharing", "review", "history", "featureFlags", "localization", "audit", "contextXray", "userProfile", "automation", "docs", "resources", "web", "workspaceApps", "chat", "email"];
|
|
18
|
+
export type FrameworkToolGroup = (typeof FRAMEWORK_TOOL_GROUPS)[number];
|
|
19
|
+
/**
|
|
20
|
+
* Per-group switches for the framework's own agent tools. Every group defaults
|
|
21
|
+
* to today's behavior, so omitting this option leaves the tool surface
|
|
22
|
+
* byte-identical.
|
|
23
|
+
*
|
|
24
|
+
* Turning a group off removes it from the agent surfaces only — interactive
|
|
25
|
+
* chat, MCP, A2A, and background runs. The matching HTTP action routes stay
|
|
26
|
+
* mounted (see `httpActions` in `agent-chat-plugin.ts`) because the UI reaches
|
|
27
|
+
* them through client hooks; a flag meant to trim the model's tool list must not
|
|
28
|
+
* 404 the share dialog.
|
|
29
|
+
*
|
|
30
|
+
* Note the parity cost before disabling one: the framework's contract is that
|
|
31
|
+
* anything the UI can do, the agent can do. `sharing: false` on an app that
|
|
32
|
+
* still renders `ShareButton` breaks that on purpose, and only makes sense when
|
|
33
|
+
* the app has no such surface at all.
|
|
34
|
+
*/
|
|
35
|
+
export interface FrameworkToolsOption {
|
|
36
|
+
/** Raw SQL tools. `"read"` (default) exposes `db-schema`/`db-query`;
|
|
37
|
+
* `"write"` adds `db-exec`/`db-patch`; `"off"` exposes neither. */
|
|
38
|
+
database?: DatabaseToolsOption;
|
|
39
|
+
/** Extension management (`create-extension`, `update-extension`, …).
|
|
40
|
+
* Defaults to false — only apps that intentionally let the agent build
|
|
41
|
+
* sandboxed mini-apps should enable it. */
|
|
42
|
+
extensions?: boolean;
|
|
43
|
+
/** `share-resource`, `unshare-resource`, `list-resource-shares`,
|
|
44
|
+
* `set-resource-visibility`, `create-agent-resource-link`. */
|
|
45
|
+
sharing?: boolean;
|
|
46
|
+
/** The inline review/comment kit (`list-review-comments`,
|
|
47
|
+
* `create-review-comment`, `resolve-review-thread`, …). */
|
|
48
|
+
review?: boolean;
|
|
49
|
+
/** Version snapshots and restore (`create-resource-version`,
|
|
50
|
+
* `list-resource-versions`, `restore-resource-version`, …). */
|
|
51
|
+
history?: boolean;
|
|
52
|
+
/** `get-feature-flags`, `list-feature-flags`, `set-feature-flag`. */
|
|
53
|
+
featureFlags?: boolean;
|
|
54
|
+
/** `get-localization-preference`, `set-localization-preference`. */
|
|
55
|
+
localization?: boolean;
|
|
56
|
+
/** `list-audit-events`, `get-audit-event`, `export-audit-events`. */
|
|
57
|
+
audit?: boolean;
|
|
58
|
+
/** Context X-Ray (`context-manifest-get`, `context-pin`, `context-evict`, …). */
|
|
59
|
+
contextXray?: boolean;
|
|
60
|
+
/** `get-user-profile`, `update-user-profile`, `change-appearance`. */
|
|
61
|
+
userProfile?: boolean;
|
|
62
|
+
/** Recurring jobs, automations, notifications, and progress runs. */
|
|
63
|
+
automation?: boolean;
|
|
64
|
+
/** `docs-search` over the bundled framework documentation. */
|
|
65
|
+
docs?: boolean;
|
|
66
|
+
/** The `resources` tool — workspace notes, memory, and context files. */
|
|
67
|
+
resources?: boolean;
|
|
68
|
+
/** `web-request` and `web-search`. */
|
|
69
|
+
web?: boolean;
|
|
70
|
+
/** `describe-workspace-apps` and `call-agent` for cross-app delegation. */
|
|
71
|
+
workspaceApps?: boolean;
|
|
72
|
+
/** `chat-history`, `manage-agent-engine`, `manage-agent-loop-settings`. */
|
|
73
|
+
chat?: boolean;
|
|
74
|
+
/** `core-send-email`. */
|
|
75
|
+
email?: boolean;
|
|
76
|
+
/** `"minimal"` turns every group above off, for voice-first and
|
|
77
|
+
* single-purpose apps that want the template's own actions and nothing else.
|
|
78
|
+
* Any explicit group key wins over the preset, so
|
|
79
|
+
* `{ preset: "minimal", resources: true }` keeps exactly one group. */
|
|
80
|
+
preset?: "minimal";
|
|
81
|
+
}
|
|
82
|
+
export type FrameworkToolsConfig = FrameworkToolsOption | "minimal";
|
|
83
|
+
export interface FrameworkToolsInput {
|
|
84
|
+
frameworkTools?: FrameworkToolsConfig;
|
|
85
|
+
/** @deprecated Use `frameworkTools.database`. */
|
|
86
|
+
databaseTools?: DatabaseToolsOption;
|
|
87
|
+
/** @deprecated Use `frameworkTools.extensions`. */
|
|
88
|
+
extensionTools?: boolean;
|
|
89
|
+
}
|
|
90
|
+
export interface ResolvedFrameworkTools {
|
|
91
|
+
/** Pass-through for `normalizeDatabaseToolsMode`; `undefined` means default. */
|
|
92
|
+
database: DatabaseToolsOption | undefined;
|
|
93
|
+
extensions: boolean;
|
|
94
|
+
disabledGroups: ReadonlySet<FrameworkToolGroup>;
|
|
95
|
+
isEnabled(group: FrameworkToolGroup): boolean;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Resolve the public option surface into one shape the plugin threads inward.
|
|
99
|
+
*
|
|
100
|
+
* Legacy `databaseTools` / `extensionTools` remain accepted for one minor. When
|
|
101
|
+
* both forms are present and disagree we throw at plugin init rather than
|
|
102
|
+
* picking a winner: an app that boots with a tool surface nobody chose is how a
|
|
103
|
+
* "why can't the agent see db-query" report ends up unexplainable.
|
|
104
|
+
*/
|
|
105
|
+
export declare function resolveFrameworkTools(input: FrameworkToolsInput | undefined): ResolvedFrameworkTools;
|
|
106
|
+
/** Structural view of the one field these helpers read, so tagging utilities
|
|
107
|
+
* stay free of an import cycle with `ActionEntry`. */
|
|
108
|
+
interface FrameworkGrouped {
|
|
109
|
+
frameworkGroup?: FrameworkToolGroup;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Drop every action belonging to a disabled group. Returns the input untouched
|
|
113
|
+
* when nothing is disabled so the default path allocates nothing.
|
|
114
|
+
*/
|
|
115
|
+
export declare function filterFrameworkToolGroups<T extends FrameworkGrouped>(actions: Record<string, T>, disabledGroups: ReadonlySet<FrameworkToolGroup>): Record<string, T>;
|
|
116
|
+
/** True for actions the framework contributes rather than the app's own. */
|
|
117
|
+
export declare function isFrameworkGroupedAction(entry: FrameworkGrouped): boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Whether a group is live, for prompt builders that only receive the disabled
|
|
120
|
+
* set. Prompt text and tool schemas must agree: a prompt that names a tool the
|
|
121
|
+
* request does not carry makes the model try it, fail, and often report the
|
|
122
|
+
* capability as nonexistent — so every block naming a group's tool by name has
|
|
123
|
+
* to be gated on the same set that gates the registry.
|
|
124
|
+
*/
|
|
125
|
+
export declare function frameworkGroupEnabled(disabledGroups: ReadonlySet<FrameworkToolGroup> | undefined, group: FrameworkToolGroup): boolean;
|
|
126
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { normalizeDatabaseToolsMode, } from "./scripts/db/tool-mode.js";
|
|
2
|
+
/**
|
|
3
|
+
* Optional framework tool groups an app can turn off through `frameworkTools`.
|
|
4
|
+
*
|
|
5
|
+
* Each name is also stamped onto the group's actions as
|
|
6
|
+
* `ActionEntry.frameworkGroup`. That tag is what lets a single filter subtract a
|
|
7
|
+
* group from every agent tool surface at once: `agent-chat-plugin.ts` spreads
|
|
8
|
+
* these registries at thirteen separate composition sites (interactive chat,
|
|
9
|
+
* dev, MCP-full, A2A, background jobs, triggers), and a per-site condition would
|
|
10
|
+
* be the same omission waiting to happen thirteen times.
|
|
11
|
+
*
|
|
12
|
+
* The tag has a second job: framework-tagged actions are excluded from the
|
|
13
|
+
* DEFAULT first-request tool list, so an app pays for the ~40 kit schemas only
|
|
14
|
+
* when it names them in `initialToolNames`. They stay in the searchable registry
|
|
15
|
+
* either way.
|
|
16
|
+
*/
|
|
17
|
+
export const FRAMEWORK_TOOL_GROUPS = [
|
|
18
|
+
"sharing",
|
|
19
|
+
"review",
|
|
20
|
+
"history",
|
|
21
|
+
"featureFlags",
|
|
22
|
+
"localization",
|
|
23
|
+
"audit",
|
|
24
|
+
"contextXray",
|
|
25
|
+
"userProfile",
|
|
26
|
+
"automation",
|
|
27
|
+
"docs",
|
|
28
|
+
"resources",
|
|
29
|
+
"web",
|
|
30
|
+
"workspaceApps",
|
|
31
|
+
"chat",
|
|
32
|
+
"email",
|
|
33
|
+
];
|
|
34
|
+
function normalizeFrameworkToolsConfig(config) {
|
|
35
|
+
if (config === "minimal")
|
|
36
|
+
return { option: {}, minimal: true };
|
|
37
|
+
if (!config || typeof config !== "object") {
|
|
38
|
+
return { option: {}, minimal: false };
|
|
39
|
+
}
|
|
40
|
+
const { preset, ...option } = config;
|
|
41
|
+
return { option, minimal: preset === "minimal" };
|
|
42
|
+
}
|
|
43
|
+
function conflict(key, legacyKey, legacyValue, nestedValue) {
|
|
44
|
+
throw new Error(`[agent-native] Conflicting agent-chat options: \`${legacyKey}: ${JSON.stringify(legacyValue)}\` ` +
|
|
45
|
+
`and \`frameworkTools.${key}: ${JSON.stringify(nestedValue)}\` disagree. ` +
|
|
46
|
+
`Remove the deprecated \`${legacyKey}\` and keep \`frameworkTools.${key}\`.`);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the public option surface into one shape the plugin threads inward.
|
|
50
|
+
*
|
|
51
|
+
* Legacy `databaseTools` / `extensionTools` remain accepted for one minor. When
|
|
52
|
+
* both forms are present and disagree we throw at plugin init rather than
|
|
53
|
+
* picking a winner: an app that boots with a tool surface nobody chose is how a
|
|
54
|
+
* "why can't the agent see db-query" report ends up unexplainable.
|
|
55
|
+
*/
|
|
56
|
+
export function resolveFrameworkTools(input) {
|
|
57
|
+
const { option, minimal } = normalizeFrameworkToolsConfig(input?.frameworkTools);
|
|
58
|
+
const legacyDatabase = input?.databaseTools;
|
|
59
|
+
if (legacyDatabase !== undefined) {
|
|
60
|
+
if (option.database !== undefined &&
|
|
61
|
+
normalizeDatabaseToolsMode(legacyDatabase) !==
|
|
62
|
+
normalizeDatabaseToolsMode(option.database)) {
|
|
63
|
+
conflict("database", "databaseTools", legacyDatabase, option.database);
|
|
64
|
+
}
|
|
65
|
+
console.warn("[agent-native] `databaseTools` is deprecated — use `frameworkTools: { database: … }`.");
|
|
66
|
+
}
|
|
67
|
+
const legacyExtensions = input?.extensionTools;
|
|
68
|
+
if (legacyExtensions !== undefined) {
|
|
69
|
+
if (option.extensions !== undefined &&
|
|
70
|
+
(legacyExtensions === true) !== (option.extensions === true)) {
|
|
71
|
+
conflict("extensions", "extensionTools", legacyExtensions, option.extensions);
|
|
72
|
+
}
|
|
73
|
+
console.warn("[agent-native] `extensionTools` is deprecated — use `frameworkTools: { extensions: … }`.");
|
|
74
|
+
}
|
|
75
|
+
const database = option.database ?? legacyDatabase ?? (minimal ? "off" : undefined);
|
|
76
|
+
const extensions = option.extensions ?? legacyExtensions ?? false;
|
|
77
|
+
const disabledGroups = new Set();
|
|
78
|
+
for (const group of FRAMEWORK_TOOL_GROUPS) {
|
|
79
|
+
const explicit = option[group];
|
|
80
|
+
if (explicit === false || (explicit === undefined && minimal)) {
|
|
81
|
+
disabledGroups.add(group);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
database,
|
|
86
|
+
extensions,
|
|
87
|
+
disabledGroups,
|
|
88
|
+
isEnabled: (group) => !disabledGroups.has(group),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Drop every action belonging to a disabled group. Returns the input untouched
|
|
93
|
+
* when nothing is disabled so the default path allocates nothing.
|
|
94
|
+
*/
|
|
95
|
+
export function filterFrameworkToolGroups(actions, disabledGroups) {
|
|
96
|
+
if (disabledGroups.size === 0)
|
|
97
|
+
return actions;
|
|
98
|
+
return Object.fromEntries(Object.entries(actions).filter(([, entry]) => entry.frameworkGroup === undefined ||
|
|
99
|
+
!disabledGroups.has(entry.frameworkGroup)));
|
|
100
|
+
}
|
|
101
|
+
/** True for actions the framework contributes rather than the app's own. */
|
|
102
|
+
export function isFrameworkGroupedAction(entry) {
|
|
103
|
+
return entry.frameworkGroup !== undefined;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Whether a group is live, for prompt builders that only receive the disabled
|
|
107
|
+
* set. Prompt text and tool schemas must agree: a prompt that names a tool the
|
|
108
|
+
* request does not carry makes the model try it, fail, and often report the
|
|
109
|
+
* capability as nonexistent — so every block naming a group's tool by name has
|
|
110
|
+
* to be gated on the same set that gates the registry.
|
|
111
|
+
*/
|
|
112
|
+
export function frameworkGroupEnabled(disabledGroups, group) {
|
|
113
|
+
return !disabledGroups?.has(group);
|
|
114
|
+
}
|
|
@@ -7,6 +7,7 @@ import { buildAssistantMessage, extractThreadMeta, } from "../agent/thread-data-
|
|
|
7
7
|
import { attachToolSearch } from "../agent/tool-search.js";
|
|
8
8
|
import { createThread, getThread, setThreadSourceIfMissing, updateThreadData, } from "../chat-threads/store.js";
|
|
9
9
|
import { resolveOrgIdForEmail } from "../org/context.js";
|
|
10
|
+
import { startIntervalJob, } from "../server/interval-job.js";
|
|
10
11
|
import { runWithRequestContext } from "../server/request-context.js";
|
|
11
12
|
import { getServiceAccountAccessToken, getServiceAccountEmail, getStartPageToken, googleDocsAdapter, listChanges, listDocComments, } from "./adapters/google-docs.js";
|
|
12
13
|
import { getIntegrationConfig, integrationConfigWriteEpoch, saveIntegrationConfig, } from "./config-store.js";
|
|
@@ -17,7 +18,13 @@ const DEFAULT_TRIGGER = "@agent";
|
|
|
17
18
|
const processedComments = new Set();
|
|
18
19
|
/** Track last-checked time per document for comment filtering */
|
|
19
20
|
const lastCheckedTimes = new Map();
|
|
20
|
-
let
|
|
21
|
+
let pollerJob = null;
|
|
22
|
+
// Held only for the startup delay below. It counts as "already running" for
|
|
23
|
+
// both the idempotency guard and stop(): `pollerJob` is still null during that
|
|
24
|
+
// window, so without this a second start() would build a second independent
|
|
25
|
+
// poller, and a stop() inside the window could not prevent the loop from
|
|
26
|
+
// arming itself afterwards.
|
|
27
|
+
let pollerStartTimer = null;
|
|
21
28
|
let activeOptions = null;
|
|
22
29
|
// ─── Watch Channel Management ───────────────────────────────────────────────
|
|
23
30
|
/** How long a watch channel lasts (Google max is ~24h, we use 23h to renew early) */
|
|
@@ -429,7 +436,7 @@ async function persistThreadData(threadId, userText, completedRun, thread) {
|
|
|
429
436
|
* in case a push notification is missed
|
|
430
437
|
*/
|
|
431
438
|
export async function startGoogleDocsPoller(options) {
|
|
432
|
-
if (
|
|
439
|
+
if (pollerJob || pollerStartTimer) {
|
|
433
440
|
console.warn("[google-docs] Already running");
|
|
434
441
|
return;
|
|
435
442
|
}
|
|
@@ -490,8 +497,16 @@ function startPollLoop(options, intervalMs) {
|
|
|
490
497
|
console.error("[google-docs] Poller error:", detail);
|
|
491
498
|
}
|
|
492
499
|
}
|
|
493
|
-
|
|
494
|
-
|
|
500
|
+
// processChanges ultimately calls the Drive/Docs REST API
|
|
501
|
+
// (adapters/google-docs.ts), none of whose fetch()s carry a request
|
|
502
|
+
// timeout — startIntervalJob's own cadence-scaled timeout backstops a hung
|
|
503
|
+
// call, same convention as SyncTransport's poll abort.
|
|
504
|
+
if (pollerStartTimer)
|
|
505
|
+
clearTimeout(pollerStartTimer);
|
|
506
|
+
pollerStartTimer = setTimeout(() => {
|
|
507
|
+
pollerStartTimer = null;
|
|
508
|
+
pollerJob = startIntervalJob(poll, { intervalMs });
|
|
509
|
+
}, 5000);
|
|
495
510
|
const email = getServiceAccountEmail();
|
|
496
511
|
if (process.env.DEBUG) {
|
|
497
512
|
console.log(`[google-docs] Poll loop started (interval: ${intervalMs / 1000}s, service account: ${email ?? "not configured"})`);
|
|
@@ -501,9 +516,13 @@ function startPollLoop(options, intervalMs) {
|
|
|
501
516
|
* Stop the Google Docs integration.
|
|
502
517
|
*/
|
|
503
518
|
export async function stopGoogleDocsPoller() {
|
|
504
|
-
if (
|
|
505
|
-
|
|
506
|
-
|
|
519
|
+
if (pollerStartTimer) {
|
|
520
|
+
clearTimeout(pollerStartTimer);
|
|
521
|
+
pollerStartTimer = null;
|
|
522
|
+
}
|
|
523
|
+
if (pollerJob) {
|
|
524
|
+
pollerJob.stop();
|
|
525
|
+
pollerJob = null;
|
|
507
526
|
}
|
|
508
527
|
if (watchRenewalTimer) {
|
|
509
528
|
clearTimeout(watchRenewalTimer);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getDbExec } from "../db/client.js";
|
|
2
|
+
import { startIntervalJob, } from "../server/interval-job.js";
|
|
2
3
|
import { hasActiveIntegrationCampaign } from "./integration-campaigns-store.js";
|
|
3
4
|
import { configuredIntegrationDurableDispatchScopes, dispatchPendingIntegrationTask, isIntegrationDurableDispatchEnabledForTask, } from "./integration-durable-dispatch.js";
|
|
4
5
|
import { ensurePendingTasksTable, MAX_PENDING_TASK_ATTEMPTS, } from "./pending-tasks-store.js";
|
|
@@ -39,8 +40,8 @@ const SERVERLESS_PROCESSING_STUCK_AFTER_MS = 75_000;
|
|
|
39
40
|
const DURABLE_BACKGROUND_PROCESSING_STUCK_AFTER_MS = 16 * 60 * 1000;
|
|
40
41
|
/** After this many attempts we give up and mark the task failed */
|
|
41
42
|
const DEFAULT_SWEEP_LIMIT = 100;
|
|
42
|
-
let
|
|
43
|
-
let
|
|
43
|
+
let job = null;
|
|
44
|
+
let startupTimer = null;
|
|
44
45
|
let activeWebhookBaseUrl;
|
|
45
46
|
/**
|
|
46
47
|
* Whether the table exists. Cached after first probe so we don't log every
|
|
@@ -268,38 +269,31 @@ function getProcessingStuckAfterMs() {
|
|
|
268
269
|
* is a no-op.
|
|
269
270
|
*/
|
|
270
271
|
export function startPendingTasksRetryJob(options) {
|
|
271
|
-
if (
|
|
272
|
+
if (job || startupTimer)
|
|
272
273
|
return;
|
|
273
274
|
activeWebhookBaseUrl = options?.webhookBaseUrl;
|
|
274
275
|
// Stagger the first run a bit so we don't hammer the DB immediately on boot.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
startupTimer = setTimeout(() => {
|
|
277
|
+
startupTimer = null;
|
|
278
|
+
job = startIntervalJob(async () => void (await retryStuckPendingTasks()), {
|
|
279
|
+
intervalMs: RETRY_INTERVAL_MS,
|
|
280
|
+
onError: (err) => {
|
|
281
|
+
console.error("[integrations] Pending-tasks retry job error:", err);
|
|
282
|
+
},
|
|
278
283
|
});
|
|
279
284
|
}, 10_000);
|
|
280
|
-
|
|
281
|
-
retryInterval = setInterval(() => {
|
|
282
|
-
void retryStuckPendingTasks().catch((err) => {
|
|
283
|
-
console.error("[integrations] Pending-tasks retry job error:", err);
|
|
284
|
-
});
|
|
285
|
-
}, RETRY_INTERVAL_MS);
|
|
286
|
-
unrefTimer(retryInterval);
|
|
285
|
+
startupTimer.unref?.();
|
|
287
286
|
if (process.env.DEBUG) {
|
|
288
287
|
console.log(`[integrations] Pending-tasks retry job started (every ${RETRY_INTERVAL_MS / 1000}s)`);
|
|
289
288
|
}
|
|
290
289
|
}
|
|
291
290
|
/** Stop the retry loop. */
|
|
292
291
|
export function stopPendingTasksRetryJob() {
|
|
293
|
-
if (
|
|
294
|
-
clearTimeout(
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
if (retryInterval) {
|
|
298
|
-
clearInterval(retryInterval);
|
|
299
|
-
retryInterval = null;
|
|
292
|
+
if (startupTimer) {
|
|
293
|
+
clearTimeout(startupTimer);
|
|
294
|
+
startupTimer = null;
|
|
300
295
|
}
|
|
296
|
+
job?.stop();
|
|
297
|
+
job = null;
|
|
301
298
|
activeWebhookBaseUrl = undefined;
|
|
302
299
|
}
|
|
303
|
-
function unrefTimer(timer) {
|
|
304
|
-
timer.unref?.();
|
|
305
|
-
}
|
|
@@ -13,6 +13,7 @@ import { resolveSecret } from "../server/credential-provider.js";
|
|
|
13
13
|
import { getH3App, markDefaultPluginProvided, } from "../server/framework-request-handler.js";
|
|
14
14
|
import { decodeOAuthState, encodeOAuthState, oauthCallbackResponse, oauthErrorPage, resolveOAuthRedirectUri, } from "../server/google-oauth.js";
|
|
15
15
|
import { readBody } from "../server/h3-helpers.js";
|
|
16
|
+
import { startIntervalJob, } from "../server/interval-job.js";
|
|
16
17
|
import { runWithRequestContext } from "../server/request-context.js";
|
|
17
18
|
import { processA2AContinuationById, processDueA2AContinuations, reconcileTerminalA2AParentIfDisabled, recoverA2AContinuationAfterProcessorFailure, recoverDueA2AContinuations, } from "./a2a-continuation-processor.js";
|
|
18
19
|
import { getA2AContinuationTaskOutcome } from "./a2a-continuations-store.js";
|
|
@@ -50,7 +51,12 @@ import { assertSlackInstallAccess, buildSlackAuthorizeUrl, exchangeSlackOAuthCod
|
|
|
50
51
|
import { getTaskQueueStats } from "./task-queue-stats.js";
|
|
51
52
|
import { listIntegrationUsageBudgets, saveIntegrationUsageBudget, } from "./usage-budget-store.js";
|
|
52
53
|
import { handleWebhook, processIntegrationTask, recordIntegrationResponseDelivery, } from "./webhook-handler.js";
|
|
53
|
-
|
|
54
|
+
// Timer-driven sweep only — the atomic DB claim inside
|
|
55
|
+
// processDueA2AContinuations already makes concurrent calls (this timer, the
|
|
56
|
+
// opportunistic post-dispatch sweep, etc.) safe.
|
|
57
|
+
let a2aContinuationJob = null;
|
|
58
|
+
let a2aContinuationStartupTimer = null;
|
|
59
|
+
const A2A_CONTINUATION_SWEEP_INTERVAL_MS = 60_000;
|
|
54
60
|
const INTEGRATION_DELIVERY_LEASE_MS = 2 * 60_000;
|
|
55
61
|
async function checkpointIntegrationDeliveryRetry(task, payload, errorMessage, event, campaignLease) {
|
|
56
62
|
let terminalStatus;
|
|
@@ -156,23 +162,18 @@ async function containFailedDeliveryTransition(task, errorMessage, event) {
|
|
|
156
162
|
});
|
|
157
163
|
}
|
|
158
164
|
function startA2AContinuationRetryJob(adapters) {
|
|
159
|
-
if (
|
|
165
|
+
if (a2aContinuationJob || a2aContinuationStartupTimer)
|
|
160
166
|
return;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
167
|
+
a2aContinuationStartupTimer = setTimeout(() => {
|
|
168
|
+
a2aContinuationStartupTimer = null;
|
|
169
|
+
a2aContinuationJob = startIntervalJob(() => processDueA2AContinuations({ adapters }), {
|
|
170
|
+
intervalMs: A2A_CONTINUATION_SWEEP_INTERVAL_MS,
|
|
171
|
+
onError: (err) => {
|
|
172
|
+
console.error("[integrations] A2A continuation retry job failed:", err);
|
|
173
|
+
},
|
|
164
174
|
});
|
|
165
175
|
}, 10_000);
|
|
166
|
-
|
|
167
|
-
a2aContinuationRetryInterval = setInterval(() => {
|
|
168
|
-
processDueA2AContinuations({ adapters }).catch((err) => {
|
|
169
|
-
console.error("[integrations] A2A continuation retry job failed:", err);
|
|
170
|
-
});
|
|
171
|
-
}, 60_000);
|
|
172
|
-
unrefTimer(a2aContinuationRetryInterval);
|
|
173
|
-
}
|
|
174
|
-
function unrefTimer(timer) {
|
|
175
|
-
timer.unref?.();
|
|
176
|
+
a2aContinuationStartupTimer.unref?.();
|
|
176
177
|
}
|
|
177
178
|
// ─── Google Pub/Sub OIDC verifier (for Drive changes.watch push) ────────────
|
|
178
179
|
// Cache Google's public keys for OIDC verification. jose handles TTL +
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
+
import { startIntervalJob, } from "../server/interval-job.js";
|
|
1
2
|
import { deliverPendingRemotePushNotifications } from "./remote-push-delivery.js";
|
|
2
3
|
const DELIVERY_INTERVAL_MS = 60_000;
|
|
3
|
-
let
|
|
4
|
-
let
|
|
5
|
-
let running = false;
|
|
4
|
+
let job = null;
|
|
5
|
+
let startupTimer = null;
|
|
6
6
|
export async function runRemotePushDelivery() {
|
|
7
|
-
if (running)
|
|
8
|
-
return;
|
|
9
|
-
running = true;
|
|
10
7
|
try {
|
|
11
8
|
await deliverPendingRemotePushNotifications();
|
|
12
9
|
}
|
|
@@ -15,29 +12,23 @@ export async function runRemotePushDelivery() {
|
|
|
15
12
|
error: error instanceof Error ? error.name : "UnknownError",
|
|
16
13
|
});
|
|
17
14
|
}
|
|
18
|
-
finally {
|
|
19
|
-
running = false;
|
|
20
|
-
}
|
|
21
15
|
}
|
|
22
16
|
export function startRemotePushDeliveryJob() {
|
|
23
|
-
if (
|
|
17
|
+
if (job || startupTimer)
|
|
24
18
|
return;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
startupTimer = setTimeout(() => {
|
|
20
|
+
startupTimer = null;
|
|
21
|
+
job = startIntervalJob(runRemotePushDelivery, {
|
|
22
|
+
intervalMs: DELIVERY_INTERVAL_MS,
|
|
23
|
+
});
|
|
24
|
+
}, 10_000);
|
|
25
|
+
startupTimer.unref?.();
|
|
29
26
|
}
|
|
30
27
|
export function stopRemotePushDeliveryJob() {
|
|
31
|
-
if (
|
|
32
|
-
clearTimeout(
|
|
33
|
-
|
|
28
|
+
if (startupTimer) {
|
|
29
|
+
clearTimeout(startupTimer);
|
|
30
|
+
startupTimer = null;
|
|
34
31
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
retryInterval = null;
|
|
38
|
-
}
|
|
39
|
-
running = false;
|
|
40
|
-
}
|
|
41
|
-
function unrefTimer(timer) {
|
|
42
|
-
timer.unref?.();
|
|
32
|
+
job?.stop();
|
|
33
|
+
job = null;
|
|
43
34
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { startIntervalJob, } from "../server/interval-job.js";
|
|
1
2
|
import { retryStaleRemoteCommands } from "./remote-commands-store.js";
|
|
2
3
|
const RETRY_INTERVAL_MS = 60_000;
|
|
3
|
-
let
|
|
4
|
-
let
|
|
4
|
+
let job = null;
|
|
5
|
+
let startupTimer = null;
|
|
5
6
|
export async function retryRemoteCommands() {
|
|
6
7
|
try {
|
|
7
8
|
return await retryStaleRemoteCommands();
|
|
@@ -14,31 +15,26 @@ export async function retryRemoteCommands() {
|
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
export function startRemoteCommandsRetryJob() {
|
|
17
|
-
if (
|
|
18
|
+
if (job || startupTimer)
|
|
18
19
|
return;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
startupTimer = setTimeout(() => {
|
|
21
|
+
startupTimer = null;
|
|
22
|
+
job = startIntervalJob(async () => {
|
|
23
|
+
await retryRemoteCommands();
|
|
24
|
+
}, {
|
|
25
|
+
intervalMs: RETRY_INTERVAL_MS,
|
|
26
|
+
onError: (err) => {
|
|
27
|
+
console.error("[integrations] Remote command retry job error:", err);
|
|
28
|
+
},
|
|
22
29
|
});
|
|
23
30
|
}, 10_000);
|
|
24
|
-
|
|
25
|
-
retryInterval = setInterval(() => {
|
|
26
|
-
void retryRemoteCommands().catch((err) => {
|
|
27
|
-
console.error("[integrations] Remote command retry job error:", err);
|
|
28
|
-
});
|
|
29
|
-
}, RETRY_INTERVAL_MS);
|
|
30
|
-
unrefTimer(retryInterval);
|
|
31
|
+
startupTimer.unref?.();
|
|
31
32
|
}
|
|
32
33
|
export function stopRemoteCommandsRetryJob() {
|
|
33
|
-
if (
|
|
34
|
-
clearTimeout(
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
if (retryInterval) {
|
|
38
|
-
clearInterval(retryInterval);
|
|
39
|
-
retryInterval = null;
|
|
34
|
+
if (startupTimer) {
|
|
35
|
+
clearTimeout(startupTimer);
|
|
36
|
+
startupTimer = null;
|
|
40
37
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
timer.unref?.();
|
|
38
|
+
job?.stop();
|
|
39
|
+
job = null;
|
|
44
40
|
}
|