@agent-native/core 0.70.2 → 0.71.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 +2 -2
- package/corpus/core/CHANGELOG.md +19 -0
- package/corpus/core/docs/content/multi-app-workspace.md +2 -2
- package/corpus/core/docs/design/durable-agent-runs.md +458 -4
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/action.ts +21 -10
- package/corpus/core/src/agent/durable-background.ts +192 -0
- package/corpus/core/src/agent/production-agent.ts +330 -13
- package/corpus/core/src/agent/run-manager.ts +66 -3
- package/corpus/core/src/agent/run-store.ts +129 -23
- package/corpus/core/src/agent/types.ts +20 -0
- package/corpus/core/src/client/AgentPanel.tsx +12 -9
- package/corpus/core/src/client/blocks/library/FileTreeBlock.tsx +72 -14
- package/corpus/core/src/deploy/build.ts +96 -0
- package/corpus/core/src/deploy/workspace-deploy.ts +121 -0
- package/corpus/core/src/mcp/build-server.ts +22 -13
- package/corpus/core/src/server/agent-chat-plugin.ts +133 -66
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -0
- package/corpus/templates/analytics/AGENTS.md +8 -0
- package/corpus/templates/analytics/actions/compose-dashboard.ts +317 -0
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +33 -6
- package/corpus/templates/analytics/changelog/2026-06-23-build-large-first-party-analytics-dashboards-in-one-fast-cal.md +6 -0
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +574 -0
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +8 -2
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +1 -9
- package/corpus/templates/clips/changelog/2026-06-23-dragging-the-desktop-camera-bubble-now-glides-to-a-stop-at-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-screen-recordings-now-capture-at-a-crisp-1080p-bitrate-inste.md +6 -0
- package/corpus/templates/clips/chrome-extension/package.json +1 -0
- package/corpus/templates/clips/chrome-extension/scripts/dev.ts +102 -0
- package/corpus/templates/clips/chrome-extension/src/background.ts +164 -32
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +163 -13
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +198 -35
- package/corpus/templates/clips/chrome-extension/src/overlay.css +73 -23
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +82 -13
- package/corpus/templates/clips/chrome-extension/src/popup.html +62 -5
- package/corpus/templates/clips/chrome-extension/src/popup.ts +290 -1
- package/corpus/templates/clips/chrome-extension/src/styles.css +34 -0
- package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +21 -16
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +17 -20
- package/corpus/templates/clips/desktop/src/overlays/bubble.tsx +73 -23
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +0 -6
- package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +56 -46
- package/corpus/templates/clips/desktop/src/styles.css +33 -5
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +113 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +2 -2
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +9 -1
- package/corpus/templates/clips/shared/upload-limits.ts +5 -2
- package/corpus/templates/plan/app/pages/PlansPage.tsx +11 -7
- package/corpus/templates/plan/changelog/2026-06-23-plan-loading-skeleton-no-longer-cuts-off-the-canvas-preview-.md +6 -0
- package/dist/action.js +21 -10
- package/dist/action.js.map +1 -1
- package/dist/agent/durable-background.d.ts +60 -0
- package/dist/agent/durable-background.d.ts.map +1 -0
- package/dist/agent/durable-background.js +144 -0
- package/dist/agent/durable-background.js.map +1 -0
- package/dist/agent/production-agent.d.ts +20 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +292 -14
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +48 -0
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +45 -3
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +30 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +124 -24
- package/dist/agent/run-store.js.map +1 -1
- package/dist/agent/types.d.ts +20 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +2 -2
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/FileTreeBlock.js +37 -4
- package/dist/client/blocks/library/FileTreeBlock.js.map +1 -1
- package/dist/deploy/build.d.ts +29 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +85 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/deploy/workspace-deploy.d.ts.map +1 -1
- package/dist/deploy/workspace-deploy.js +108 -0
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +23 -9
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +68 -13
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/docs/content/multi-app-workspace.md +2 -2
- package/docs/design/durable-agent-runs.md +458 -4
- package/package.json +1 -1
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from "./workspace-core.js";
|
|
35
35
|
import { generateActionRegistryForProject } from "../vite/action-types-plugin.js";
|
|
36
36
|
import { mcpEmbedStaticAssetRouteRules } from "../shared/mcp-embed-headers.js";
|
|
37
|
+
import { AGENT_CHAT_PROCESS_RUN_PATH } from "../agent/durable-background.js";
|
|
37
38
|
import {
|
|
38
39
|
AGENT_NATIVE_SOCIAL_IMAGE_ALT,
|
|
39
40
|
AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER,
|
|
@@ -1476,6 +1477,85 @@ export function findInstalledResvgPackages(
|
|
|
1476
1477
|
.map(([packageName, packageDir]) => ({ packageName, packageDir }));
|
|
1477
1478
|
}
|
|
1478
1479
|
|
|
1480
|
+
/**
|
|
1481
|
+
* Deploy-time gate for emitting the second `-background` Netlify function.
|
|
1482
|
+
* Reads the same env flag the runtime gate uses
|
|
1483
|
+
* (`AGENT_CHAT_DURABLE_BACKGROUND`). Off by default — when off, the deploy
|
|
1484
|
+
* emits exactly one function (today's behavior, byte-for-byte).
|
|
1485
|
+
*/
|
|
1486
|
+
export function isDurableBackgroundDeployEnabled(): boolean {
|
|
1487
|
+
const raw = process.env.AGENT_CHAT_DURABLE_BACKGROUND;
|
|
1488
|
+
if (raw == null) return false;
|
|
1489
|
+
const v = raw.trim().toLowerCase();
|
|
1490
|
+
return v === "1" || v === "true" || v === "yes" || v === "on";
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/**
|
|
1494
|
+
* Single-template Netlify build: emit a SECOND function whose name ends in
|
|
1495
|
+
* `-background`, re-exporting the same `main.mjs` handler bundle, so the chat
|
|
1496
|
+
* `_process-run` POST lands on Netlify's async (15-min) function instead of the
|
|
1497
|
+
* synchronous one. Additive + flag-gated (see `isDurableBackgroundDeployEnabled`).
|
|
1498
|
+
*
|
|
1499
|
+
* Nitro's `netlify` preset emits a single function at
|
|
1500
|
+
* `.netlify/functions-internal/server` (`server.mjs` → `main.mjs`). We copy
|
|
1501
|
+
* that directory to a sibling `<...>-background` function and write an entry
|
|
1502
|
+
* with a `config.path` of the process-run route.
|
|
1503
|
+
*
|
|
1504
|
+
* ⚠️ REAL-DEPLOY VERIFICATION REQUIRED. Whether Netlify routes
|
|
1505
|
+
* `/_agent-native/agent-chat/_process-run` to this `-background` function (and
|
|
1506
|
+
* invokes it asynchronously with the 15-min budget) vs the synchronous Nitro
|
|
1507
|
+
* function cannot be verified in this environment — Nitro owns the primary
|
|
1508
|
+
* function's routing manifest, and the precedence between the two functions for
|
|
1509
|
+
* that path is a Netlify runtime behavior. If routing resolves to the
|
|
1510
|
+
* synchronous function, the run still completes via the existing 40s
|
|
1511
|
+
* soft-timeout path (no durable win, no regression). See
|
|
1512
|
+
* docs/design/durable-agent-runs.md (Open risks #1).
|
|
1513
|
+
*/
|
|
1514
|
+
export function emitSingleTemplateNetlifyBackgroundFunction(
|
|
1515
|
+
projectCwd: string,
|
|
1516
|
+
): void {
|
|
1517
|
+
const functionsDir = path.join(projectCwd, ".netlify", "functions-internal");
|
|
1518
|
+
const serverDir = path.join(functionsDir, "server");
|
|
1519
|
+
if (!fs.existsSync(path.join(serverDir, "main.mjs"))) {
|
|
1520
|
+
// Nitro output layout differs from what we expected — skip rather than
|
|
1521
|
+
// guess. The single-function deploy is unaffected.
|
|
1522
|
+
console.warn(
|
|
1523
|
+
"[build] Durable-background emit skipped: expected Nitro Netlify function " +
|
|
1524
|
+
"at .netlify/functions-internal/server/main.mjs was not found.",
|
|
1525
|
+
);
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
const backgroundName = "server-agent-background";
|
|
1529
|
+
const dest = path.join(functionsDir, backgroundName);
|
|
1530
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
1531
|
+
copyDir(serverDir, dest);
|
|
1532
|
+
// Drop the original Nitro entry so our background entry is the entrypoint.
|
|
1533
|
+
fs.rmSync(path.join(dest, "server.mjs"), { force: true });
|
|
1534
|
+
|
|
1535
|
+
const entry = `let cachedHandler;
|
|
1536
|
+
|
|
1537
|
+
export default async function handler(...args) {
|
|
1538
|
+
cachedHandler ??= (await import("./main.mjs")).default;
|
|
1539
|
+
return cachedHandler(...args);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
export const config = {
|
|
1543
|
+
name: "agent background handler",
|
|
1544
|
+
generator: "agent-native build",
|
|
1545
|
+
path: ${JSON.stringify([AGENT_CHAT_PROCESS_RUN_PATH])},
|
|
1546
|
+
nodeBundler: "none",
|
|
1547
|
+
includedFiles: ["**"],
|
|
1548
|
+
preferStatic: false,
|
|
1549
|
+
};
|
|
1550
|
+
`;
|
|
1551
|
+
fs.writeFileSync(path.join(dest, `${backgroundName}.mjs`), entry);
|
|
1552
|
+
console.log(
|
|
1553
|
+
`[build] Emitted durable-background function "${backgroundName}" ` +
|
|
1554
|
+
`(path ${AGENT_CHAT_PROCESS_RUN_PATH}). REQUIRES real-deploy verification ` +
|
|
1555
|
+
`of Netlify async routing — see docs/design/durable-agent-runs.md.`,
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1479
1559
|
function copyInstalledLibsqlNativePackages(serverDir: string | undefined) {
|
|
1480
1560
|
if (!serverDir || !fs.existsSync(serverDir)) return;
|
|
1481
1561
|
const nodeModulesRoots = nodeModulesAncestors(cwd);
|
|
@@ -1938,6 +2018,22 @@ export default bundle;
|
|
|
1938
2018
|
copyInstalledFfmpegStaticPackage(nitro.options.output.serverDir);
|
|
1939
2019
|
}
|
|
1940
2020
|
|
|
2021
|
+
// Durable background agent runs (off by default). Additive ONLY: emits a
|
|
2022
|
+
// SECOND Netlify function whose name ends in `-background` re-exporting the
|
|
2023
|
+
// same handler bundle, so the chat `_process-run` POST lands on Netlify's
|
|
2024
|
+
// async (15-min) function. When the flag is off this is a no-op and the
|
|
2025
|
+
// single-function deploy is byte-for-byte unchanged.
|
|
2026
|
+
if (preset === "netlify" && isDurableBackgroundDeployEnabled()) {
|
|
2027
|
+
try {
|
|
2028
|
+
emitSingleTemplateNetlifyBackgroundFunction(cwd);
|
|
2029
|
+
} catch (err) {
|
|
2030
|
+
console.warn(
|
|
2031
|
+
"[build] Failed to emit durable-background Netlify function (non-fatal):",
|
|
2032
|
+
err instanceof Error ? err.message : err,
|
|
2033
|
+
);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
|
|
1941
2037
|
// Resolve remaining bare npm imports by bundling them into _libs/.
|
|
1942
2038
|
// Nitro sometimes leaves small packages as externals even with noExternals.
|
|
1943
2039
|
if (preset.startsWith("cloudflare") || preset.startsWith("deno")) {
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
collectImmutableAssetPaths,
|
|
33
33
|
IMMUTABLE_ASSET_CACHE_HEADERS,
|
|
34
34
|
} from "./immutable-assets.js";
|
|
35
|
+
import { AGENT_CHAT_PROCESS_RUN_PATH } from "../agent/durable-background.js";
|
|
35
36
|
|
|
36
37
|
export type WorkspaceDeployPreset = "cloudflare_pages" | "netlify" | "vercel";
|
|
37
38
|
|
|
@@ -665,6 +666,126 @@ function copyNetlifyFunctionIntoWorkspace(
|
|
|
665
666
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
666
667
|
copyDir(src, dest);
|
|
667
668
|
patchNetlifyFunctionEntry(dest, app, workspaceApps, staticDir);
|
|
669
|
+
|
|
670
|
+
// Durable background agent runs (off by default). Additive ONLY: when the
|
|
671
|
+
// flag is off this emits nothing and the single-function deploy is unchanged.
|
|
672
|
+
if (isDurableBackgroundDeployEnabled()) {
|
|
673
|
+
emitNetlifyBackgroundFunction(workspaceRoot, app, src, workspaceApps);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Deploy-time gate for emitting the second `-background` Netlify function. Reads
|
|
679
|
+
* the same env flag the runtime gate uses (`AGENT_CHAT_DURABLE_BACKGROUND`).
|
|
680
|
+
* Off by default — when off, the deploy emits exactly one function per app
|
|
681
|
+
* (today's behavior, byte-for-byte).
|
|
682
|
+
*/
|
|
683
|
+
function isDurableBackgroundDeployEnabled(): boolean {
|
|
684
|
+
const raw = process.env.AGENT_CHAT_DURABLE_BACKGROUND;
|
|
685
|
+
if (raw == null) return false;
|
|
686
|
+
const v = raw.trim().toLowerCase();
|
|
687
|
+
return v === "1" || v === "true" || v === "yes" || v === "on";
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Emit a SECOND Netlify function for `app` whose name ends in `-background`,
|
|
692
|
+
* re-exporting the SAME `main.mjs` handler bundle. Netlify invokes any function
|
|
693
|
+
* whose deployed name ends in `-background` asynchronously (202 immediately, up
|
|
694
|
+
* to 15-min budget), which is exactly what the durable-background chat
|
|
695
|
+
* dispatch (`fireInternalDispatch` → AGENT_CHAT_PROCESS_RUN_PATH) needs.
|
|
696
|
+
*
|
|
697
|
+
* The function is given a `config.path` of the chat process-run route so
|
|
698
|
+
* Netlify routes that POST to this async function instead of the synchronous
|
|
699
|
+
* `<app>-server` function. It shares the same bundle (`includedFiles: ["**"]`)
|
|
700
|
+
* so `A2A_SECRET`, the DB URL, and the rest of the env/bundle are present.
|
|
701
|
+
*
|
|
702
|
+
* ⚠️ REAL-DEPLOY VERIFICATION REQUIRED. The exact Netlify routing/precedence
|
|
703
|
+
* between this `-background` function's `config.path` and the synchronous
|
|
704
|
+
* `<app>-server` function's broader `config.path` (which also matches
|
|
705
|
+
* `/_agent-native/*` for the dispatch app, or `/<app>/*` otherwise) cannot be
|
|
706
|
+
* verified in this environment. It is emitted additively and only under the
|
|
707
|
+
* flag; if Netlify resolves the overlap to the wrong function, the dispatch
|
|
708
|
+
* would land on the synchronous function (no 15-min budget) — the run would
|
|
709
|
+
* still work via the existing 40s soft-timeout path, just without the durable
|
|
710
|
+
* win. See packages/core/docs/design/durable-agent-runs.md.
|
|
711
|
+
*/
|
|
712
|
+
function emitNetlifyBackgroundFunction(
|
|
713
|
+
workspaceRoot: string,
|
|
714
|
+
app: string,
|
|
715
|
+
srcServerDir: string,
|
|
716
|
+
workspaceApps: WorkspaceAppManifestEntry[],
|
|
717
|
+
): void {
|
|
718
|
+
// Name MUST end in `-background` for Netlify async invocation.
|
|
719
|
+
const backgroundName = `${app}-agent-background`;
|
|
720
|
+
const dest = path.join(netlifyFunctionsDir(workspaceRoot), backgroundName);
|
|
721
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
722
|
+
copyDir(srcServerDir, dest);
|
|
723
|
+
|
|
724
|
+
const basePath = `/${app}`;
|
|
725
|
+
const workspaceAppAudience = workspaceAppAudienceForApp(workspaceApps, app);
|
|
726
|
+
const workspaceAppRouteAccess = workspaceAppRouteAccessForApp(
|
|
727
|
+
workspaceApps,
|
|
728
|
+
app,
|
|
729
|
+
);
|
|
730
|
+
// Only the chat process-run route needs the async function. Keeping the path
|
|
731
|
+
// narrow avoids stealing any other traffic from the synchronous function.
|
|
732
|
+
// The dispatch URL is app-base-path-prefixed (same as the agent-teams
|
|
733
|
+
// processor dispatch), so the route Netlify must map to this async function
|
|
734
|
+
// is `/<app>/_agent-native/agent-chat/_process-run`.
|
|
735
|
+
const processRunPath = `${basePath}${AGENT_CHAT_PROCESS_RUN_PATH}`;
|
|
736
|
+
const pathConfig = [processRunPath];
|
|
737
|
+
const server = `const basePath = ${JSON.stringify(basePath)};
|
|
738
|
+
|
|
739
|
+
function setBasePathEnv() {
|
|
740
|
+
const processRef = globalThis.process ??= { env: {} };
|
|
741
|
+
processRef.env ??= {};
|
|
742
|
+
Object.assign(processRef.env, {
|
|
743
|
+
AGENT_NATIVE_WORKSPACE: "1",
|
|
744
|
+
AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
|
|
745
|
+
APP_BASE_PATH: basePath,
|
|
746
|
+
AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
|
|
747
|
+
AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
|
|
748
|
+
AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
|
|
749
|
+
VITE_AGENT_NATIVE_WORKSPACE: "1",
|
|
750
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
|
|
751
|
+
VITE_APP_BASE_PATH: basePath,
|
|
752
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
|
|
753
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
|
|
754
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
|
|
755
|
+
VITE_AGENT_NATIVE_WORKSPACE_APPS_JSON: ${JSON.stringify(JSON.stringify(workspaceApps))},
|
|
756
|
+
${JSON.stringify(WORKSPACE_APPS_ENV_KEY)}: ${JSON.stringify(JSON.stringify(workspaceApps))},
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
setBasePathEnv();
|
|
761
|
+
|
|
762
|
+
let cachedHandler;
|
|
763
|
+
|
|
764
|
+
export default async function handler(...args) {
|
|
765
|
+
setBasePathEnv();
|
|
766
|
+
cachedHandler ??= (await import("./main.mjs")).default;
|
|
767
|
+
return cachedHandler(...args);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export const config = {
|
|
771
|
+
name: ${JSON.stringify(`${app} agent background handler`)},
|
|
772
|
+
generator: "agent-native workspace deploy",
|
|
773
|
+
path: ${JSON.stringify(pathConfig)},
|
|
774
|
+
nodeBundler: "none",
|
|
775
|
+
includedFiles: ["**"],
|
|
776
|
+
preferStatic: false,
|
|
777
|
+
};
|
|
778
|
+
`;
|
|
779
|
+
// Remove the original Nitro entry (server.mjs) so only our background entry
|
|
780
|
+
// is the function entrypoint, mirroring patchNetlifyFunctionEntry.
|
|
781
|
+
fs.rmSync(path.join(dest, "server.mjs"), { force: true });
|
|
782
|
+
fs.writeFileSync(path.join(dest, `${backgroundName}.mjs`), server);
|
|
783
|
+
console.log(
|
|
784
|
+
`[workspace-deploy] Emitted durable-background function "${backgroundName}" ` +
|
|
785
|
+
`for app "${app}" (path ${processRunPath}). ` +
|
|
786
|
+
`REQUIRES real-deploy verification of Netlify async routing — see ` +
|
|
787
|
+
`docs/design/durable-agent-runs.md.`,
|
|
788
|
+
);
|
|
668
789
|
}
|
|
669
790
|
|
|
670
791
|
function patchNetlifyFunctionEntry(
|
|
@@ -952,12 +952,13 @@ async function resolveMcpAppResource(
|
|
|
952
952
|
): Promise<ResolvedMcpAppResource | null> {
|
|
953
953
|
const resource = entry.mcpApp?.resource;
|
|
954
954
|
if (!resource) return null;
|
|
955
|
-
//
|
|
956
|
-
//
|
|
957
|
-
//
|
|
958
|
-
//
|
|
959
|
-
//
|
|
960
|
-
|
|
955
|
+
// NB: the inline kill switch is intentionally NOT enforced here. This
|
|
956
|
+
// resolver also backs `resources/read`, which must keep serving the shell
|
|
957
|
+
// for a URI the host already holds (e.g. a cached descriptor) so it degrades
|
|
958
|
+
// gracefully instead of throwing a hard `-32603`. The switch is enforced at
|
|
959
|
+
// the *advertisement/render* sites (`tools/list` descriptor meta,
|
|
960
|
+
// `tools/call` result meta, `resources/list`) so disabled embeds never get
|
|
961
|
+
// advertised in the first place.
|
|
961
962
|
const resolvedUri = getMcpAppResourceUri(config, actionName, entry);
|
|
962
963
|
if (!resolvedUri) return null;
|
|
963
964
|
const description = resource.description ?? entry.tool.description;
|
|
@@ -1006,6 +1007,9 @@ async function getMcpAppResources(
|
|
|
1006
1007
|
actions: Record<string, ActionEntry>,
|
|
1007
1008
|
requestMeta?: MCPRequestMeta,
|
|
1008
1009
|
): Promise<ResolvedMcpAppResource[]> {
|
|
1010
|
+
// Advertisement path (resources/list + resources/templates/list): suppressed
|
|
1011
|
+
// by the inline kill switch so disabled embeds are never listed.
|
|
1012
|
+
if (!requestMeta?.inlineMcpApps) return [];
|
|
1009
1013
|
const resources = await Promise.all(
|
|
1010
1014
|
Object.entries(actions).map(([name, entry]) =>
|
|
1011
1015
|
resolveMcpAppResourceSafely(config, name, entry, requestMeta),
|
|
@@ -1383,7 +1387,10 @@ export async function createMCPServerForRequest(
|
|
|
1383
1387
|
: {};
|
|
1384
1388
|
const toolMeta = {
|
|
1385
1389
|
...rawToolMeta,
|
|
1386
|
-
|
|
1390
|
+
// Advertisement path: only tag the tool with its inline-embed
|
|
1391
|
+
// descriptor when the kill switch is on, so disabled embeds never
|
|
1392
|
+
// prompt a host to render/read the `ui://` resource.
|
|
1393
|
+
...(mcpAppResource && requestMeta?.inlineMcpApps
|
|
1387
1394
|
? {
|
|
1388
1395
|
...openAiToolDescriptorMeta(mcpAppResource),
|
|
1389
1396
|
[MCP_APP_RESOURCE_URI_META_KEY]: mcpAppResource.uri,
|
|
@@ -1533,12 +1540,14 @@ export async function createMCPServerForRequest(
|
|
|
1533
1540
|
!!mcpResult.raw &&
|
|
1534
1541
|
typeof mcpResult.raw === "object" &&
|
|
1535
1542
|
(mcpResult.raw as Record<string, unknown>).isError === true;
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1543
|
+
// Render path: only treat the result as an inline embed when the kill
|
|
1544
|
+
// switch is on. When off, `mcpAppResource` is null so every embed
|
|
1545
|
+
// branch below degrades to the plain deep-link artifacts the tool would
|
|
1546
|
+
// otherwise return — no `openai/outputTemplate`, no minted embed-start,
|
|
1547
|
+
// no embed structuredContent — so the host shows a link, not an iframe.
|
|
1548
|
+
const mcpAppResource = requestMeta?.inlineMcpApps
|
|
1549
|
+
? await resolveMcpAppResourceSafely(config, name, entry, requestMeta)
|
|
1550
|
+
: null;
|
|
1542
1551
|
const rawResultForClient = mcpAppResource
|
|
1543
1552
|
? await withServerMintedMcpAppEmbedStart(rawResult, requestMeta)
|
|
1544
1553
|
: rawResult;
|
|
@@ -150,6 +150,10 @@ import {
|
|
|
150
150
|
hasConfiguredA2ASecret,
|
|
151
151
|
isA2AProductionRuntime,
|
|
152
152
|
} from "../a2a/auth-policy.js";
|
|
153
|
+
import {
|
|
154
|
+
AGENT_CHAT_PROCESS_RUN_PATH,
|
|
155
|
+
prepareProcessRunRequest,
|
|
156
|
+
} from "../agent/durable-background.js";
|
|
153
157
|
import {
|
|
154
158
|
getBuilderBrowserConnectUrlForOwner,
|
|
155
159
|
resolveBuilderBranchProjectId,
|
|
@@ -7695,6 +7699,134 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
7695
7699
|
}),
|
|
7696
7700
|
);
|
|
7697
7701
|
|
|
7702
|
+
// Shared per-request invocation: resolve auth/org/timezone context, then
|
|
7703
|
+
// pick the dev/prod/anonymous handler and run it inside the request
|
|
7704
|
+
// context. Used by the main chat POST and by the durable-background
|
|
7705
|
+
// `_process-run` processor route (which re-enters the same handler set as
|
|
7706
|
+
// the background worker), so both go through identical context + handler
|
|
7707
|
+
// selection.
|
|
7708
|
+
const invokeAgentChatHandler = async (event: any) => {
|
|
7709
|
+
// Resolve per-request auth context
|
|
7710
|
+
const ownerContext = await resolveOwnerContext(event);
|
|
7711
|
+
const owner = ownerContext.owner;
|
|
7712
|
+
|
|
7713
|
+
// Resolve org ID: explicit callback > session.orgId from Better Auth
|
|
7714
|
+
// > implicit org membership. Better Auth leaves session.orgId null
|
|
7715
|
+
// until the user explicitly switches orgs, so a fresh signup with
|
|
7716
|
+
// implicit membership (e.g. domain-matched org) would otherwise see
|
|
7717
|
+
// no org-scoped credentials. getOrgContext() does the same DB lookup
|
|
7718
|
+
// the /builder/status endpoint uses to decide "Connected".
|
|
7719
|
+
let resolvedOrgId: string | undefined;
|
|
7720
|
+
if (options?.resolveOrgId) {
|
|
7721
|
+
resolvedOrgId = (await options.resolveOrgId(event)) ?? undefined;
|
|
7722
|
+
} else {
|
|
7723
|
+
try {
|
|
7724
|
+
const session = await getSession(event);
|
|
7725
|
+
resolvedOrgId = session?.orgId ?? undefined;
|
|
7726
|
+
} catch {
|
|
7727
|
+
// Session not available
|
|
7728
|
+
}
|
|
7729
|
+
if (!resolvedOrgId) {
|
|
7730
|
+
try {
|
|
7731
|
+
const { getOrgContext } = await import("../org/context.js");
|
|
7732
|
+
const ctx = await getOrgContext(event);
|
|
7733
|
+
resolvedOrgId = ctx.orgId ?? undefined;
|
|
7734
|
+
} catch {
|
|
7735
|
+
// org_members table may not exist yet on first boot
|
|
7736
|
+
}
|
|
7737
|
+
}
|
|
7738
|
+
}
|
|
7739
|
+
|
|
7740
|
+
// Propagate the caller's IANA timezone from `x-user-timezone` so that
|
|
7741
|
+
// tool calls made by the agent (e.g. log-meal with no explicit date)
|
|
7742
|
+
// resolve "today" in the user's local timezone instead of server UTC.
|
|
7743
|
+
const tzRaw = getHeader(event, "x-user-timezone");
|
|
7744
|
+
const timezone =
|
|
7745
|
+
typeof tzRaw === "string" &&
|
|
7746
|
+
tzRaw.trim().length > 0 &&
|
|
7747
|
+
tzRaw.trim().length < 64
|
|
7748
|
+
? tzRaw.trim()
|
|
7749
|
+
: undefined;
|
|
7750
|
+
|
|
7751
|
+
return runWithRequestContext(
|
|
7752
|
+
{
|
|
7753
|
+
userEmail: owner,
|
|
7754
|
+
userName: ownerContext.name,
|
|
7755
|
+
orgId: resolvedOrgId,
|
|
7756
|
+
timezone,
|
|
7757
|
+
},
|
|
7758
|
+
() => {
|
|
7759
|
+
// App-rendered chat can't host direct code edits — HMR/full
|
|
7760
|
+
// reloads would kill the same chat surface mid-run. Force the
|
|
7761
|
+
// prod handler (no shell / no fs); the prompt block injected by
|
|
7762
|
+
// `prodHandler.systemPrompt` then steers source changes to a
|
|
7763
|
+
// separate agent surface such as Builder or the dev frame.
|
|
7764
|
+
const blockInProductCodeEditing =
|
|
7765
|
+
shouldBlockInProductCodeEditing(event);
|
|
7766
|
+
const handler =
|
|
7767
|
+
ownerContext.anonymous && anonymousHandler
|
|
7768
|
+
? anonymousHandler
|
|
7769
|
+
: !blockInProductCodeEditing && currentDevMode && devHandler
|
|
7770
|
+
? devHandler
|
|
7771
|
+
: prodHandler;
|
|
7772
|
+
return handler(event);
|
|
7773
|
+
},
|
|
7774
|
+
);
|
|
7775
|
+
};
|
|
7776
|
+
|
|
7777
|
+
// ─── Durable background agent-chat run processor ──────────────────────
|
|
7778
|
+
// Self-fire target for a long chat turn. The foreground POST claims the
|
|
7779
|
+
// run slot, inserts the run row, and `fireInternalDispatch`es here; this
|
|
7780
|
+
// route runs INSIDE the Netlify background function (15-min budget). It
|
|
7781
|
+
// HMAC-verifies the dispatch (same internal-token scheme as the agent-
|
|
7782
|
+
// teams / A2A / webhook processors), injects the background-run marker,
|
|
7783
|
+
// and re-enters the SAME agent-chat handler as the background worker,
|
|
7784
|
+
// which runs the full multi-step turn inline with the ~13min soft
|
|
7785
|
+
// timeout. With AGENT_CHAT_DURABLE_BACKGROUND off, the foreground never
|
|
7786
|
+
// dispatches here, so this route is never exercised.
|
|
7787
|
+
getH3App(nitroApp).use(
|
|
7788
|
+
AGENT_CHAT_PROCESS_RUN_PATH,
|
|
7789
|
+
defineEventHandler(async (event) => {
|
|
7790
|
+
if (getMethod(event) !== "POST") {
|
|
7791
|
+
setResponseStatus(event, 405);
|
|
7792
|
+
return { error: "Method not allowed" };
|
|
7793
|
+
}
|
|
7794
|
+
// Consume the body ONCE (h3 v2's web Request stream is single-use).
|
|
7795
|
+
let processBody: any;
|
|
7796
|
+
try {
|
|
7797
|
+
processBody = await readBody(event);
|
|
7798
|
+
} catch {
|
|
7799
|
+
setResponseStatus(event, 400);
|
|
7800
|
+
return { error: "Invalid request body" };
|
|
7801
|
+
}
|
|
7802
|
+
|
|
7803
|
+
// Validate + HMAC-authenticate the self-dispatch and prepare the
|
|
7804
|
+
// background-worker body. Pure decision (unit-tested in
|
|
7805
|
+
// durable-background.spec.ts); the route only wires it to h3.
|
|
7806
|
+
const prepared = prepareProcessRunRequest(
|
|
7807
|
+
processBody,
|
|
7808
|
+
getHeader(event, "authorization"),
|
|
7809
|
+
);
|
|
7810
|
+
if (!prepared.ok) {
|
|
7811
|
+
setResponseStatus(event, prepared.status);
|
|
7812
|
+
return { error: prepared.error };
|
|
7813
|
+
}
|
|
7814
|
+
|
|
7815
|
+
// Stash the verified+augmented body for the handler — the body stream
|
|
7816
|
+
// is already consumed, so the handler reads this instead.
|
|
7817
|
+
(event as any).context = (event as any).context ?? {};
|
|
7818
|
+
(event as any).context.__agentChatBackgroundBody = prepared.body;
|
|
7819
|
+
|
|
7820
|
+
try {
|
|
7821
|
+
return await invokeAgentChatHandler(event);
|
|
7822
|
+
} catch (err: any) {
|
|
7823
|
+
console.error("[agent-chat] _process-run failed:", err);
|
|
7824
|
+
setResponseStatus(event, 500);
|
|
7825
|
+
return { error: "process-run failed" };
|
|
7826
|
+
}
|
|
7827
|
+
}),
|
|
7828
|
+
);
|
|
7829
|
+
|
|
7698
7830
|
// Mount the main chat handler — delegates to dev or prod handler based on current mode.
|
|
7699
7831
|
// This is mounted last because h3's use() is prefix-based, meaning /_agent-native/agent-chat
|
|
7700
7832
|
// also matches /_agent-native/agent-chat/threads/... — we skip sub-path requests here so the
|
|
@@ -7713,72 +7845,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
7713
7845
|
return { error: "Not found" };
|
|
7714
7846
|
}
|
|
7715
7847
|
|
|
7716
|
-
|
|
7717
|
-
const ownerContext = await resolveOwnerContext(event);
|
|
7718
|
-
const owner = ownerContext.owner;
|
|
7719
|
-
|
|
7720
|
-
// Resolve org ID: explicit callback > session.orgId from Better Auth
|
|
7721
|
-
// > implicit org membership. Better Auth leaves session.orgId null
|
|
7722
|
-
// until the user explicitly switches orgs, so a fresh signup with
|
|
7723
|
-
// implicit membership (e.g. domain-matched org) would otherwise see
|
|
7724
|
-
// no org-scoped credentials. getOrgContext() does the same DB lookup
|
|
7725
|
-
// the /builder/status endpoint uses to decide "Connected".
|
|
7726
|
-
let resolvedOrgId: string | undefined;
|
|
7727
|
-
if (options?.resolveOrgId) {
|
|
7728
|
-
resolvedOrgId = (await options.resolveOrgId(event)) ?? undefined;
|
|
7729
|
-
} else {
|
|
7730
|
-
try {
|
|
7731
|
-
const session = await getSession(event);
|
|
7732
|
-
resolvedOrgId = session?.orgId ?? undefined;
|
|
7733
|
-
} catch {
|
|
7734
|
-
// Session not available
|
|
7735
|
-
}
|
|
7736
|
-
if (!resolvedOrgId) {
|
|
7737
|
-
try {
|
|
7738
|
-
const { getOrgContext } = await import("../org/context.js");
|
|
7739
|
-
const ctx = await getOrgContext(event);
|
|
7740
|
-
resolvedOrgId = ctx.orgId ?? undefined;
|
|
7741
|
-
} catch {
|
|
7742
|
-
// org_members table may not exist yet on first boot
|
|
7743
|
-
}
|
|
7744
|
-
}
|
|
7745
|
-
}
|
|
7746
|
-
|
|
7747
|
-
// Propagate the caller's IANA timezone from `x-user-timezone` so that
|
|
7748
|
-
// tool calls made by the agent (e.g. log-meal with no explicit date)
|
|
7749
|
-
// resolve "today" in the user's local timezone instead of server UTC.
|
|
7750
|
-
const tzRaw = getHeader(event, "x-user-timezone");
|
|
7751
|
-
const timezone =
|
|
7752
|
-
typeof tzRaw === "string" &&
|
|
7753
|
-
tzRaw.trim().length > 0 &&
|
|
7754
|
-
tzRaw.trim().length < 64
|
|
7755
|
-
? tzRaw.trim()
|
|
7756
|
-
: undefined;
|
|
7757
|
-
|
|
7758
|
-
return runWithRequestContext(
|
|
7759
|
-
{
|
|
7760
|
-
userEmail: owner,
|
|
7761
|
-
userName: ownerContext.name,
|
|
7762
|
-
orgId: resolvedOrgId,
|
|
7763
|
-
timezone,
|
|
7764
|
-
},
|
|
7765
|
-
() => {
|
|
7766
|
-
// App-rendered chat can't host direct code edits — HMR/full
|
|
7767
|
-
// reloads would kill the same chat surface mid-run. Force the
|
|
7768
|
-
// prod handler (no shell / no fs); the prompt block injected by
|
|
7769
|
-
// `prodHandler.systemPrompt` then steers source changes to a
|
|
7770
|
-
// separate agent surface such as Builder or the dev frame.
|
|
7771
|
-
const blockInProductCodeEditing =
|
|
7772
|
-
shouldBlockInProductCodeEditing(event);
|
|
7773
|
-
const handler =
|
|
7774
|
-
ownerContext.anonymous && anonymousHandler
|
|
7775
|
-
? anonymousHandler
|
|
7776
|
-
: !blockInProductCodeEditing && currentDevMode && devHandler
|
|
7777
|
-
? devHandler
|
|
7778
|
-
: prodHandler;
|
|
7779
|
-
return handler(event);
|
|
7780
|
-
},
|
|
7781
|
-
);
|
|
7848
|
+
return invokeAgentChatHandler(event);
|
|
7782
7849
|
}),
|
|
7783
7850
|
);
|
|
7784
7851
|
|
|
@@ -174,12 +174,31 @@ pnpm action update-dashboard --dashboardId weekly-metrics --config '<full json>'
|
|
|
174
174
|
|
|
175
175
|
After a mutation, navigate to the dashboard if the user is elsewhere. The app syncs through the framework's polling/query invalidation path.
|
|
176
176
|
|
|
177
|
+
## Building Large First-Party Dashboards (compose-dashboard)
|
|
178
|
+
|
|
179
|
+
For a **first-party analytics** dashboard, prefer `compose-dashboard` over hand-authoring a big `update-dashboard` config. You name the metrics; the SERVER expands each into a full, validated panel (SQL + chart config) from the shipped metric catalog and saves them in ONE atomic call. This avoids the failure mode where the agent must stream a giant multi-panel `update-dashboard` argument inside the ~40s budget — that big tool-call can't be resumed mid-stream and is all-or-nothing on validation, so the agent thrashes (repeated update-dashboard + tool-search, never landing).
|
|
180
|
+
|
|
181
|
+
- **Never hand-author large first-party configs panel-by-panel.** Call `compose-dashboard` with the metric keys instead.
|
|
182
|
+
- Unknown metric keys are skipped and reported in `unknownMetrics` (not fatal). Each panel's SQL is validated independently — valid panels save, invalid ones are reported in `invalidMetrics`.
|
|
183
|
+
- By default (no `overwrite`), composing into an existing dashboard APPENDS the new panels and skips ids already present. `overwrite: true` replaces the whole config.
|
|
184
|
+
- Each metric accepts an optional per-metric `window` of `'30d' | '90d' | 'all'` (only affects windowed virality/time metrics) and `title` / `chartType` / `width` overrides.
|
|
185
|
+
- Returns `{ dashboardId, panelCount, createdMetrics, unknownMetrics, invalidMetrics, skippedExistingIds }` — report `panelCount` as proof-of-done.
|
|
186
|
+
|
|
187
|
+
Available metric keys: `total-signups`, `signups-over-time`, `signups-by-template`, `sessions-by-app`, `sessions-over-time`, `signed-in-vs-anon`, `total-template-clicks`, `total-demo-clicks`, `total-cli-copies`, `template-interest-over-time`, `clicks-by-template`, `demo-clicks-by-template`, `cli-copies-by-template`, `cli-copies-over-time`, `pageviews-over-time`, `referred-signups-30d`, `viral-signup-share-30d`, `clip-share-signups-30d`, `signups-by-referral-source`, `referred-signups-over-time`, `top-referrers`, `share-funnel-30d`, `viral-participation-rate-90d`, `viral-coefficient-90d`, `activated-referrers-90d`.
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# Build a large first-party dashboard in ONE call (server generates the panels)
|
|
191
|
+
pnpm action compose-dashboard --dashboardId first-party-overview --title "First-Party Overview" \
|
|
192
|
+
--metrics '["total-signups","signups-over-time","signups-by-template","sessions-by-app","viral-coefficient-90d","top-referrers","share-funnel-30d"]'
|
|
193
|
+
```
|
|
194
|
+
|
|
177
195
|
## Reliable Bulk Edits
|
|
178
196
|
|
|
179
197
|
This is the dashboard-specific application of the framework-wide `reliable-mutations` skill — read that for the general rule (one atomic write, verify end state, report proof-of-done).
|
|
180
198
|
|
|
181
199
|
Hosted agent runs have a **~40s budget**. Many sequential `update-dashboard` calls (one per panel, plus schema-discovery calls) will blow that budget and leave the dashboard in a partial state — earlier inserts looked like they succeeded (✓), but nothing actually persisted. Avoid this:
|
|
182
200
|
|
|
201
|
+
- **For a large first-party dashboard, use `compose-dashboard`** (see the section above): name the metrics, the server generates the panels in one call. Do not hand-author the big config.
|
|
183
202
|
- **Batch ALL changes into ONE `update-dashboard` call.** A single `update-dashboard` is atomic: it applies every op to an in-memory config, validates all panel SQL, then upserts once. Never loop the action.
|
|
184
203
|
- To add N panels, pass N ops in one call: `ops: [{op:"insert", path:"/panels/-", value:<panel>}, … ]` (`/panels/-` appends to the end).
|
|
185
204
|
- The `ops` format needs no discovery: each op is `{ op, path, from?, value? }`, `op ∈ set | replace | remove | insert | move | move-before`, and `path` is a JSON Pointer (e.g. `/panels/3`, `/panels/3/title`, `/name`).
|
|
@@ -83,6 +83,14 @@ details live in `.agents/skills/`.
|
|
|
83
83
|
|
|
84
84
|
## Dashboard Template Catalog
|
|
85
85
|
|
|
86
|
+
- To build or extend a LARGE first-party dashboard, prefer `compose-dashboard`:
|
|
87
|
+
name the metrics and the server generates the validated SQL/config for every
|
|
88
|
+
panel in ONE fast call. Do NOT hand-author big `update-dashboard` configs
|
|
89
|
+
panel-by-panel or loop `update-dashboard` — streaming a giant multi-panel
|
|
90
|
+
argument inside the ~40s budget fails and thrashes. Unknown metric keys are
|
|
91
|
+
skipped and reported; per-panel SQL validates independently; existing
|
|
92
|
+
dashboards append by default (`overwrite: true` replaces). Report the returned
|
|
93
|
+
`panelCount` as proof-of-done.
|
|
86
94
|
- `list-dashboard-templates` lists source-controlled dashboard templates with
|
|
87
95
|
`id`, category, data sources, panel count, and installed dashboard IDs.
|
|
88
96
|
- `install-dashboard-template` installs a catalog template into normal
|