@agent-native/core 0.75.1 → 0.75.4
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/core/CHANGELOG.md +76 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/durable-background.ts +54 -0
- package/corpus/core/src/agent/production-agent.ts +15 -3
- package/corpus/core/src/cli/sync-builder-starter-manifest.ts +36 -12
- package/corpus/core/src/deploy/build.ts +176 -41
- package/corpus/templates/analytics/actions/get-sql-dashboard.ts +0 -8
- package/corpus/templates/analytics/actions/update-dashboard.ts +3 -1
- package/corpus/templates/analytics/app/components/SqlEditor.tsx +57 -0
- package/corpus/templates/analytics/app/components/SqlHighlight.tsx +27 -28
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +130 -8
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +2 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/ViewSqlPopover.tsx +4 -4
- package/corpus/templates/analytics/changelog/2026-06-24-active-user-dashboard-panels-classify-templates-more-accurat.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-24-dashboard-panel-deletes-now-stay-deleted-after-refresh.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-24-sql-panel-editors-now-highlight-query-syntax-and-keep-one-cl.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +2 -2
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +9 -11
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +6 -20
- package/corpus/templates/plan/app/components/plan/DocumentArea.tsx +45 -33
- package/corpus/templates/plan/changelog/2026-06-24-invalid-generated-recap-blocks-now-show-a-clear-warning-with.md +6 -0
- package/corpus/templates/plan/server/plan-content.ts +53 -3
- package/dist/agent/durable-background.d.ts +49 -0
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +51 -0
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +15 -3
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/sync-builder-starter-manifest.d.ts.map +1 -1
- package/dist/cli/sync-builder-starter-manifest.js +21 -10
- package/dist/cli/sync-builder-starter-manifest.js.map +1 -1
- package/dist/deploy/build.d.ts +61 -21
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +164 -41
- package/dist/deploy/build.js.map +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,81 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.75.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- dbfbe42: Preserve starter-only manifest fields when syncing builder-agent-native-starter from templates/chat.
|
|
8
|
+
|
|
9
|
+
## 0.75.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 8a00851: Durable background agent-chat runs now reach Netlify's 15-min async function by
|
|
14
|
+
emitting the background function INTO the scanned functions dir with a real
|
|
15
|
+
`config.path`, and excluding that path from the Nitro `server` `/*` catch-all so
|
|
16
|
+
the match is unambiguous.
|
|
17
|
+
|
|
18
|
+
Grounded in the real Netlify build output: Nitro's `netlify` preset writes no
|
|
19
|
+
`netlify.toml` and no redirects — the `/*` catch-all is an in-code Functions API
|
|
20
|
+
v2 `config.path: "/*"` on `.netlify/functions-internal/server/server.mjs`.
|
|
21
|
+
Netlify scans exactly the configured `functionsDirectory`
|
|
22
|
+
(`.netlify/functions-internal`); `.netlify/functions/` is the build OUTPUT dir
|
|
23
|
+
(where `@netlify/build` later writes the zips + `manifest.json`) and is never
|
|
24
|
+
scanned. On CI, Netlify reads each scanned function's `export const config` to
|
|
25
|
+
build the manifest routes — so per-file `background`/`path` config is honored.
|
|
26
|
+
|
|
27
|
+
The build now emits the background function into
|
|
28
|
+
`.netlify/functions-internal/server-agent-background` (the scanned dir), sharing
|
|
29
|
+
the same `main.mjs` bundle, with `export const config = { background: true, path:
|
|
30
|
+
"/_agent-native/agent-chat/_process-run" }`. It also appends that path to the
|
|
31
|
+
`server` function's `config.excludedPath`, so the `/*` catch-all no longer
|
|
32
|
+
matches the process-run route. Netlify evaluates serverless functions before
|
|
33
|
+
redirects, so a POST to the framework process-run route matches only the async
|
|
34
|
+
background function (immediate 202 ack, 15-min budget) — never the synchronous
|
|
35
|
+
`server` catch-all. The entry sets
|
|
36
|
+
`globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true` at cold start and
|
|
37
|
+
normalizes the request path before delegating to Nitro, preserving the method,
|
|
38
|
+
all headers (the HMAC `Authorization: Bearer` the plugin verifies), and the body.
|
|
39
|
+
|
|
40
|
+
This supersedes the two earlier approaches that failed in production: emitting
|
|
41
|
+
into `functions-internal` with a `config.path` but WITHOUT excluding it from the
|
|
42
|
+
`/*` catch-all (both functions matched the path; the synchronous `server`
|
|
43
|
+
catch-all won, returning a sync 401 instead of a 202), and emitting a standalone
|
|
44
|
+
function into `.netlify/functions/` (never scanned, returned 404). The foreground
|
|
45
|
+
self-dispatch now always targets the framework process-run route on every host
|
|
46
|
+
via `resolveAgentChatProcessRunDispatchPath`. The graceful inline 40s fallback on
|
|
47
|
+
a dispatch fast-fail is unchanged.
|
|
48
|
+
|
|
49
|
+
## 0.75.2
|
|
50
|
+
|
|
51
|
+
### Patch Changes
|
|
52
|
+
|
|
53
|
+
- 4b3543d: Durable background agent-chat runs now actually receive Netlify's 15-min async
|
|
54
|
+
budget by reaching a STANDALONE background function at its DIRECT url, bypassing
|
|
55
|
+
Nitro's synchronous `/*` catch-all.
|
|
56
|
+
|
|
57
|
+
The previous emit wrote the background function into `.netlify/functions-internal`
|
|
58
|
+
with a custom `config.path` of the `_process-run` route. A custom `config.path`
|
|
59
|
+
makes a function reachable ONLY at that path (not at its default function url),
|
|
60
|
+
`functions-internal` is not exposed at a default url, and Netlify routed
|
|
61
|
+
`/_agent-native/agent-chat/_process-run` to the synchronous Nitro `server`
|
|
62
|
+
catch-all instead — so the worker was capped at the ~60s wall and degraded to
|
|
63
|
+
40s-chunked runs (confirmed live: a POST to the process-run path returned a
|
|
64
|
+
synchronous 401 from the handler rather than a 202 async ack).
|
|
65
|
+
|
|
66
|
+
The build now emits a standalone function into the STANDARD
|
|
67
|
+
`.netlify/functions/server-agent-background` dir with `background: true` and NO
|
|
68
|
+
custom `path`, so Netlify exposes it at `/.netlify/functions/server-agent-background`
|
|
69
|
+
and invokes it asynchronously (immediate 202 ack, 15-min budget). Its entry sets
|
|
70
|
+
`globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true` at cold start and rewrites
|
|
71
|
+
the incoming request path back to the `_process-run` route before delegating to
|
|
72
|
+
the Nitro handler, preserving the method, all headers (the HMAC
|
|
73
|
+
`Authorization: Bearer` the plugin verifies survives the rewrite), and the body.
|
|
74
|
+
The foreground self-dispatch (and server-driven continuation chunks) now target
|
|
75
|
+
that direct url on hosted Netlify via a shared `resolveAgentChatProcessRunDispatchPath`
|
|
76
|
+
helper, and stay on the framework route everywhere else. The graceful inline
|
|
77
|
+
40s fallback on a dispatch fast-fail is unchanged.
|
|
78
|
+
|
|
3
79
|
## 0.75.1
|
|
4
80
|
|
|
5
81
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -51,6 +51,60 @@ import {
|
|
|
51
51
|
export const AGENT_CHAT_PROCESS_RUN_PATH =
|
|
52
52
|
"/_agent-native/agent-chat/_process-run";
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Name of the standalone Netlify background function the build emits (see
|
|
56
|
+
* `emitSingleTemplateNetlifyBackgroundFunction` in deploy/build.ts). Shared so
|
|
57
|
+
* the emit and the dispatch-path helper below can never drift on the name.
|
|
58
|
+
*
|
|
59
|
+
* MUST end in `-background` — both because that is the conventional Netlify
|
|
60
|
+
* async-function suffix and because `isInBackgroundFunctionRuntime()` reads the
|
|
61
|
+
* `AWS_LAMBDA_FUNCTION_NAME` `-background` suffix as a secondary runtime signal.
|
|
62
|
+
*/
|
|
63
|
+
export const AGENT_BACKGROUND_FUNCTION_NAME = "server-agent-background";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Default function URL of the background function on Netlify, kept for
|
|
67
|
+
* diagnostics/tests. Every Netlify function is ALSO reachable at
|
|
68
|
+
* `/.netlify/functions/<name>` unless a custom `config.path` removes the default
|
|
69
|
+
* url. The emitted background function declares `config.path =
|
|
70
|
+
* AGENT_CHAT_PROCESS_RUN_PATH`, which means Netlify routes the process-run path
|
|
71
|
+
* to it directly AND (per Netlify docs) removes this default url — so the
|
|
72
|
+
* foreground does NOT dispatch here; it dispatches to the framework route (see
|
|
73
|
+
* `resolveAgentChatProcessRunDispatchPath`). This constant is retained only so
|
|
74
|
+
* the name/url shape stays asserted and discoverable.
|
|
75
|
+
*/
|
|
76
|
+
export const AGENT_BACKGROUND_FUNCTION_URL_PATH = `/.netlify/functions/${AGENT_BACKGROUND_FUNCTION_NAME}`;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the path the foreground POST should self-dispatch the chat background
|
|
80
|
+
* worker to.
|
|
81
|
+
*
|
|
82
|
+
* GROUNDED IN THE REAL NETLIFY BUILD OUTPUT: the background function is emitted
|
|
83
|
+
* INTO the scanned dir (`.netlify/functions-internal/server-agent-background`)
|
|
84
|
+
* with `export const config = { background: true, path:
|
|
85
|
+
* AGENT_CHAT_PROCESS_RUN_PATH }`. Netlify evaluates serverless functions BEFORE
|
|
86
|
+
* redirects (request-chain step 10 vs 11), and the build excludes this exact path
|
|
87
|
+
* from the `server` `/*` catch-all — so a POST to `AGENT_CHAT_PROCESS_RUN_PATH`
|
|
88
|
+
* matches ONLY the async background function (immediate 202, 15-min budget),
|
|
89
|
+
* never the synchronous `server` catch-all.
|
|
90
|
+
*
|
|
91
|
+
* So the dispatch path is the SAME framework route on every host. On hosted
|
|
92
|
+
* Netlify it lands on the async function (because of the exclude + the
|
|
93
|
+
* background function's `config.path`); everywhere else (local dev, `netlify
|
|
94
|
+
* dev`, non-Netlify hosts where no second function exists) the same in-process
|
|
95
|
+
* catch-all handles it inline. The HMAC token (signed over the runId) is
|
|
96
|
+
* unchanged.
|
|
97
|
+
*
|
|
98
|
+
* NOTE: this is a deliberate change from the earlier "dispatch to the direct
|
|
99
|
+
* `/.netlify/functions/<name>` url" attempt, which only worked if the function
|
|
100
|
+
* was reachable at its default url. With a custom `config.path` the default url
|
|
101
|
+
* is removed, and there is no shadowing to bypass anyway, so dispatching to the
|
|
102
|
+
* framework route is both correct and simpler.
|
|
103
|
+
*/
|
|
104
|
+
export function resolveAgentChatProcessRunDispatchPath(): string {
|
|
105
|
+
return AGENT_CHAT_PROCESS_RUN_PATH;
|
|
106
|
+
}
|
|
107
|
+
|
|
54
108
|
/**
|
|
55
109
|
* Env flag for durable background runs. DEFAULT-ON: unset means enabled; an app
|
|
56
110
|
* opts OUT with an explicit falsy value (`false`/`0`/`no`/`off`).
|
|
@@ -65,10 +65,10 @@ import {
|
|
|
65
65
|
} from "./run-manager.js";
|
|
66
66
|
import type { ActiveRun } from "./run-manager.js";
|
|
67
67
|
import {
|
|
68
|
-
AGENT_CHAT_PROCESS_RUN_PATH,
|
|
69
68
|
AGENT_CHAT_BACKGROUND_RUN_FIELD,
|
|
70
69
|
isAgentChatDurableBackgroundEnabled,
|
|
71
70
|
isInBackgroundFunctionRuntime,
|
|
71
|
+
resolveAgentChatProcessRunDispatchPath,
|
|
72
72
|
} from "./durable-background.js";
|
|
73
73
|
import { fireInternalDispatch } from "../server/self-dispatch.js";
|
|
74
74
|
import { readBody } from "../server/h3-helpers.js";
|
|
@@ -4394,7 +4394,14 @@ export function createProductionAgentHandler(
|
|
|
4394
4394
|
try {
|
|
4395
4395
|
await fireInternalDispatch({
|
|
4396
4396
|
event,
|
|
4397
|
-
|
|
4397
|
+
// The framework `_process-run` route on every host. On hosted Netlify
|
|
4398
|
+
// the build emits an async background function (in the scanned dir)
|
|
4399
|
+
// that CLAIMS this exact path via `config.path` AND excludes it from
|
|
4400
|
+
// the `server` /* catch-all, so this POST matches ONLY the async
|
|
4401
|
+
// function (immediate 202, 15-min budget) — Netlify matches functions
|
|
4402
|
+
// before redirects. Off-Netlify the same in-process catch-all handles
|
|
4403
|
+
// it. The Authorization Bearer HMAC is preserved either way.
|
|
4404
|
+
path: resolveAgentChatProcessRunDispatchPath(),
|
|
4398
4405
|
taskId: runId,
|
|
4399
4406
|
body: {
|
|
4400
4407
|
...body,
|
|
@@ -4636,7 +4643,12 @@ export function createProductionAgentHandler(
|
|
|
4636
4643
|
try {
|
|
4637
4644
|
await fireInternalDispatch({
|
|
4638
4645
|
event,
|
|
4639
|
-
|
|
4646
|
+
// Continuation chunks dispatch to the same framework
|
|
4647
|
+
// `_process-run` route; on hosted Netlify it matches the
|
|
4648
|
+
// async background function (config.path + excluded from the
|
|
4649
|
+
// /* catch-all) so each chunk keeps the 15-min budget. Same
|
|
4650
|
+
// path-resolution as the initial dispatch.
|
|
4651
|
+
path: resolveAgentChatProcessRunDispatchPath(),
|
|
4640
4652
|
taskId: nextRunId,
|
|
4641
4653
|
body: {
|
|
4642
4654
|
...body,
|
|
@@ -59,6 +59,26 @@ export function generateStandaloneChatManifest(repoRoot?: string): {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function mergePackageJsonRecords(
|
|
63
|
+
canonical: Record<string, string> | undefined,
|
|
64
|
+
starter: Record<string, string> | undefined,
|
|
65
|
+
starterPinnedKeys: string[] = [],
|
|
66
|
+
): Record<string, string> {
|
|
67
|
+
const merged = { ...(canonical ?? {}) };
|
|
68
|
+
for (const [key, value] of Object.entries(starter ?? {})) {
|
|
69
|
+
if (!(key in merged)) {
|
|
70
|
+
merged[key] = value;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const key of starterPinnedKeys) {
|
|
74
|
+
const pinned = starter?.[key];
|
|
75
|
+
if (pinned) {
|
|
76
|
+
merged[key] = pinned;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return merged;
|
|
80
|
+
}
|
|
81
|
+
|
|
62
82
|
export function mergeStarterManifest(
|
|
63
83
|
starterPackageJson: PackageJson,
|
|
64
84
|
canonicalPackageJson: PackageJson,
|
|
@@ -75,19 +95,23 @@ export function mergeStarterManifest(
|
|
|
75
95
|
if (starterPackageJson.private !== undefined) {
|
|
76
96
|
merged.private = starterPackageJson.private;
|
|
77
97
|
}
|
|
98
|
+
if (typeof starterPackageJson.packageManager === "string") {
|
|
99
|
+
merged.packageManager = starterPackageJson.packageManager;
|
|
100
|
+
}
|
|
78
101
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
merged.dependencies = mergePackageJsonRecords(
|
|
103
|
+
canonicalPackageJson.dependencies as Record<string, string> | undefined,
|
|
104
|
+
starterPackageJson.dependencies as Record<string, string> | undefined,
|
|
105
|
+
["@agent-native/core"],
|
|
106
|
+
);
|
|
107
|
+
merged.devDependencies = mergePackageJsonRecords(
|
|
108
|
+
canonicalPackageJson.devDependencies as Record<string, string> | undefined,
|
|
109
|
+
starterPackageJson.devDependencies as Record<string, string> | undefined,
|
|
110
|
+
);
|
|
111
|
+
merged.scripts = mergePackageJsonRecords(
|
|
112
|
+
canonicalPackageJson.scripts as Record<string, string> | undefined,
|
|
113
|
+
starterPackageJson.scripts as Record<string, string> | undefined,
|
|
114
|
+
);
|
|
91
115
|
|
|
92
116
|
return merged;
|
|
93
117
|
}
|
|
@@ -34,7 +34,10 @@ 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 {
|
|
37
|
+
import {
|
|
38
|
+
AGENT_BACKGROUND_FUNCTION_NAME,
|
|
39
|
+
AGENT_CHAT_PROCESS_RUN_PATH,
|
|
40
|
+
} from "../agent/durable-background.js";
|
|
38
41
|
import {
|
|
39
42
|
AGENT_NATIVE_SOCIAL_IMAGE_ALT,
|
|
40
43
|
AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER,
|
|
@@ -1502,36 +1505,76 @@ export function isDurableBackgroundDeployEnabled(): boolean {
|
|
|
1502
1505
|
}
|
|
1503
1506
|
|
|
1504
1507
|
/**
|
|
1505
|
-
* Single-template Netlify build: emit
|
|
1506
|
-
*
|
|
1507
|
-
*
|
|
1508
|
-
*
|
|
1508
|
+
* Single-template Netlify build: emit an async (background) function INSIDE the
|
|
1509
|
+
* scanned functions dir so the chat `_process-run` worker runs on Netlify's
|
|
1510
|
+
* 15-min async function instead of the synchronous `/*` catch-all.
|
|
1511
|
+
* Additive + flag-gated (see `isDurableBackgroundDeployEnabled`).
|
|
1512
|
+
*
|
|
1513
|
+
* GROUNDED IN THE REAL NETLIFY BUILD OUTPUT (verified from a local Nitro build):
|
|
1514
|
+
* - Nitro's `netlify` preset emits exactly ONE function source at
|
|
1515
|
+
* `.netlify/functions-internal/server/`. `server.mjs` re-exports `main.mjs`
|
|
1516
|
+
* and declares `export const config = { path: "/*", excludedPath:
|
|
1517
|
+
* ["/.netlify/*"], preferStatic: true, ... }`. Nitro writes NO `netlify.toml`
|
|
1518
|
+
* and NO `[[redirects]]`; the `/*` catch-all is an IN-CODE Functions-API-v2
|
|
1519
|
+
* `config.path`.
|
|
1520
|
+
* - The generated `.netlify/netlify.toml` sets
|
|
1521
|
+
* `functionsDirectory = ".netlify/functions-internal"`. Netlify scans EXACTLY
|
|
1522
|
+
* that dir; functions placed anywhere else (e.g. `.netlify/functions/`, which
|
|
1523
|
+
* is the BUILD OUTPUT dir where `@netlify/build` later writes the zipped
|
|
1524
|
+
* functions + `manifest.json`) are NEVER deployed.
|
|
1525
|
+
* - On Netlify CI, `@netlify/build` reads each scanned function's
|
|
1526
|
+
* `export const config`, zips it, and materializes
|
|
1527
|
+
* `.netlify/functions/manifest.json` with `routes` derived from each
|
|
1528
|
+
* `config.path`. So per-file `export const config` (including `background` and
|
|
1529
|
+
* `path`) IS honored — the `server` manifest entry's `routes: [{ pattern:
|
|
1530
|
+
* "/*" }]` came straight from `server.mjs`'s `config.path`.
|
|
1509
1531
|
*
|
|
1510
|
-
*
|
|
1511
|
-
*
|
|
1512
|
-
*
|
|
1513
|
-
*
|
|
1532
|
+
* THEREFORE we:
|
|
1533
|
+
* 1. Emit the background function INTO the scanned dir
|
|
1534
|
+
* (`.netlify/functions-internal/server-agent-background/`), sharing the same
|
|
1535
|
+
* built `main.mjs` bundle, so Netlify discovers it and honors its config.
|
|
1536
|
+
* 2. Give its `export const config` BOTH `background: true` (→ async invoke,
|
|
1537
|
+
* immediate 202, 15-min budget) AND `path: AGENT_CHAT_PROCESS_RUN_PATH` (so
|
|
1538
|
+
* it claims that exact path at function-matching time, which Netlify
|
|
1539
|
+
* evaluates BEFORE redirects — step 10 vs 11 in the request chain).
|
|
1540
|
+
* 3. PATCH the Nitro `server` function's own `server.mjs` so its catch-all
|
|
1541
|
+
* `config.path: "/*"` EXCLUDES `AGENT_CHAT_PROCESS_RUN_PATH` (append it to
|
|
1542
|
+
* `excludedPath`). Netlify does NOT define a winner when two serverless
|
|
1543
|
+
* functions both match a path; rather than rely on that undocumented order,
|
|
1544
|
+
* we make the match UNAMBIGUOUS — only the background function matches
|
|
1545
|
+
* `_process-run`, and `server` matches everything else exactly as before.
|
|
1546
|
+
* 4. Set `globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true` at cold start
|
|
1547
|
+
* (read back by `isInBackgroundFunctionRuntime()` so the worker takes the
|
|
1548
|
+
* ~13-min soft-timeout). A `globalThis` flag — NOT `process.env` — keeps the
|
|
1549
|
+
* no-env-mutation guard satisfied and carries no cross-request state.
|
|
1514
1550
|
*
|
|
1515
|
-
*
|
|
1516
|
-
*
|
|
1517
|
-
*
|
|
1518
|
-
*
|
|
1519
|
-
*
|
|
1520
|
-
*
|
|
1521
|
-
* (confirmed live: a POST to the process-run path returned a synchronous 401
|
|
1522
|
-
* from the handler instead of a 202 async ack).
|
|
1551
|
+
* With a real `config.path` the function is reachable at that path directly, so
|
|
1552
|
+
* the foreground POSTs to `AGENT_CHAT_PROCESS_RUN_PATH`
|
|
1553
|
+
* (`resolveAgentChatProcessRunDispatchPath`) and the request entry no longer
|
|
1554
|
+
* needs to rewrite the path — it already arrives at the framework route. We keep
|
|
1555
|
+
* a defensive normalize-to-PROCESS_RUN_PATH in the entry anyway (cheap, and it
|
|
1556
|
+
* makes the function correct even if reached via its default function url).
|
|
1523
1557
|
*
|
|
1524
|
-
*
|
|
1525
|
-
*
|
|
1526
|
-
*
|
|
1527
|
-
*
|
|
1528
|
-
*
|
|
1558
|
+
* WHY THIS BEATS ALL THREE PRIOR FAILURES:
|
|
1559
|
+
* - Attempts 1 & 2 emitted into `functions-internal` (correct dir) with a
|
|
1560
|
+
* `config.path` but did NOT exclude that path from the `server` `/*`
|
|
1561
|
+
* catch-all. Two functions matched `_process-run`; the order is undocumented
|
|
1562
|
+
* and the catch-all `server` (priority 0, synchronous) won → SYNC 401, not a
|
|
1563
|
+
* 202. We now exclude the path from `server`, so only the async function
|
|
1564
|
+
* matches.
|
|
1565
|
+
* - Attempt 3 emitted a standalone function into `.netlify/functions/` — the
|
|
1566
|
+
* OUTPUT dir, which Netlify does not scan — so it never entered the manifest
|
|
1567
|
+
* → 404. We now emit into the SCANNED `functions-internal` dir.
|
|
1568
|
+
*
|
|
1569
|
+
* Safety net regardless of Netlify routing nuance: if the dispatch fast-fails
|
|
1570
|
+
* (e.g. the function was not emitted), the foreground handler degrades to an
|
|
1571
|
+
* inline 40s synchronous run (see production-agent.ts).
|
|
1529
1572
|
*/
|
|
1530
1573
|
export function emitSingleTemplateNetlifyBackgroundFunction(
|
|
1531
1574
|
projectCwd: string,
|
|
1532
1575
|
): void {
|
|
1533
|
-
const
|
|
1534
|
-
const serverDir = path.join(
|
|
1576
|
+
const internalDir = path.join(projectCwd, ".netlify", "functions-internal");
|
|
1577
|
+
const serverDir = path.join(internalDir, "server");
|
|
1535
1578
|
if (!fs.existsSync(path.join(serverDir, "main.mjs"))) {
|
|
1536
1579
|
// Nitro output layout differs from what we expected — skip rather than
|
|
1537
1580
|
// guess. The single-function deploy is unaffected.
|
|
@@ -1541,13 +1584,24 @@ export function emitSingleTemplateNetlifyBackgroundFunction(
|
|
|
1541
1584
|
);
|
|
1542
1585
|
return;
|
|
1543
1586
|
}
|
|
1544
|
-
const backgroundName =
|
|
1545
|
-
|
|
1587
|
+
const backgroundName = AGENT_BACKGROUND_FUNCTION_NAME;
|
|
1588
|
+
// Emit INTO the SCANNED functions dir (functions-internal) so Netlify discovers
|
|
1589
|
+
// the function and honors its `export const config`. `.netlify/functions/` is
|
|
1590
|
+
// the build OUTPUT dir (where @netlify/build writes the zip + manifest) and is
|
|
1591
|
+
// NOT scanned — emitting there is why the standalone attempt 404'd.
|
|
1592
|
+
const dest = path.join(internalDir, backgroundName);
|
|
1546
1593
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
1547
1594
|
copyDir(serverDir, dest);
|
|
1548
|
-
// Drop the original Nitro entry so our
|
|
1595
|
+
// Drop the original Nitro `/*` entry so our entry is the entrypoint and the
|
|
1596
|
+
// copied bundle does NOT re-register the catch-all `config.path`.
|
|
1549
1597
|
fs.rmSync(path.join(dest, "server.mjs"), { force: true });
|
|
1550
1598
|
|
|
1599
|
+
// Make the `server` `/*` catch-all NOT match the process-run path, so only the
|
|
1600
|
+
// async background function matches it (function-vs-function path order is
|
|
1601
|
+
// undocumented on Netlify — don't rely on it).
|
|
1602
|
+
excludeProcessRunPathFromServerCatchAll(serverDir);
|
|
1603
|
+
|
|
1604
|
+
const processRunPath = JSON.stringify(AGENT_CHAT_PROCESS_RUN_PATH);
|
|
1551
1605
|
const entry = `// Mark this isolate as the durable background runtime BEFORE the handler
|
|
1552
1606
|
// bundle is imported, so isInBackgroundFunctionRuntime() reliably returns true
|
|
1553
1607
|
// in this function. The deployed Lambda name is NOT guaranteed to end in
|
|
@@ -1557,26 +1611,48 @@ export function emitSingleTemplateNetlifyBackgroundFunction(
|
|
|
1557
1611
|
// set-once isolate marker read back by isInBackgroundFunctionRuntime().
|
|
1558
1612
|
globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true;
|
|
1559
1613
|
|
|
1614
|
+
// The framework route the Nitro router dispatches to (the _process-run plugin).
|
|
1615
|
+
const PROCESS_RUN_PATH = ${processRunPath};
|
|
1616
|
+
|
|
1560
1617
|
let cachedHandler;
|
|
1561
1618
|
|
|
1562
|
-
|
|
1619
|
+
// Netlify v2 invokes this as (request, context). The Nitro netlify handler is a
|
|
1620
|
+
// Web-standard \`async (Request) => Response\` (see nitro/presets/netlify/runtime).
|
|
1621
|
+
// Because this function declares \`config.path = PROCESS_RUN_PATH\`, Netlify routes
|
|
1622
|
+
// that exact path here and the request already arrives as PROCESS_RUN_PATH — no
|
|
1623
|
+
// rewrite is needed. We still NORMALIZE the pathname to PROCESS_RUN_PATH so the
|
|
1624
|
+
// function stays correct even if it is ever reached via its default function url
|
|
1625
|
+
// (/.netlify/functions/${backgroundName}). Method, ALL headers (the HMAC
|
|
1626
|
+
// Authorization: Bearer MUST survive — the plugin verifies it) and the body are
|
|
1627
|
+
// preserved by cloning the incoming Request with only its URL pathname set.
|
|
1628
|
+
export default async function handler(request) {
|
|
1563
1629
|
cachedHandler ??= (await import("./main.mjs")).default;
|
|
1564
|
-
|
|
1630
|
+
const url = new URL(request.url);
|
|
1631
|
+
url.pathname = PROCESS_RUN_PATH;
|
|
1632
|
+
// Read the body once and pass it through. GET/HEAD have no body.
|
|
1633
|
+
const method = request.method || "POST";
|
|
1634
|
+
const hasBody = method !== "GET" && method !== "HEAD";
|
|
1635
|
+
const body = hasBody ? await request.text() : undefined;
|
|
1636
|
+
const rewritten = new Request(url.toString(), {
|
|
1637
|
+
method,
|
|
1638
|
+
headers: request.headers,
|
|
1639
|
+
body,
|
|
1640
|
+
});
|
|
1641
|
+
return cachedHandler(rewritten);
|
|
1565
1642
|
}
|
|
1566
1643
|
|
|
1567
1644
|
export const config = {
|
|
1568
1645
|
name: "agent background handler",
|
|
1569
1646
|
generator: "agent-native build",
|
|
1570
|
-
// background: true
|
|
1571
|
-
//
|
|
1572
|
-
//
|
|
1573
|
-
//
|
|
1574
|
-
//
|
|
1575
|
-
//
|
|
1576
|
-
//
|
|
1577
|
-
// build/functions/background-functions + build/functions/configuration.
|
|
1647
|
+
// background: true makes Netlify invoke this ASYNCHRONOUSLY (immediate HTTP
|
|
1648
|
+
// 202 ack) with the 15-minute budget (Netlify docs:
|
|
1649
|
+
// build/functions/background-functions + build/functions/api). path claims the
|
|
1650
|
+
// process-run route directly; Netlify evaluates serverless functions BEFORE
|
|
1651
|
+
// redirects (request-chain step 10 vs 11), and we exclude this path from the
|
|
1652
|
+
// \`server\` /* catch-all so only THIS function matches it — no ambiguous
|
|
1653
|
+
// function-vs-function order.
|
|
1578
1654
|
background: true,
|
|
1579
|
-
path:
|
|
1655
|
+
path: PROCESS_RUN_PATH,
|
|
1580
1656
|
nodeBundler: "none",
|
|
1581
1657
|
includedFiles: ["**"],
|
|
1582
1658
|
preferStatic: false,
|
|
@@ -1584,9 +1660,68 @@ export const config = {
|
|
|
1584
1660
|
`;
|
|
1585
1661
|
fs.writeFileSync(path.join(dest, `${backgroundName}.mjs`), entry);
|
|
1586
1662
|
console.log(
|
|
1587
|
-
`[build] Emitted durable-background function "${backgroundName}" ` +
|
|
1588
|
-
`
|
|
1589
|
-
`
|
|
1663
|
+
`[build] Emitted durable-background function "${backgroundName}" into the ` +
|
|
1664
|
+
`scanned dir .netlify/functions-internal with config { background:true, ` +
|
|
1665
|
+
`path:"${AGENT_CHAT_PROCESS_RUN_PATH}" } and excluded that path from the ` +
|
|
1666
|
+
`server /* catch-all. REQUIRES real-deploy verification of Netlify async ` +
|
|
1667
|
+
`(202) invocation — see docs/design/durable-agent-runs.md.`,
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
/**
|
|
1672
|
+
* Append `AGENT_CHAT_PROCESS_RUN_PATH` to the Nitro `server` function's
|
|
1673
|
+
* `config.excludedPath` so its `/*` catch-all does NOT match the process-run
|
|
1674
|
+
* path. That guarantees the async background function (which declares
|
|
1675
|
+
* `config.path = AGENT_CHAT_PROCESS_RUN_PATH`) is the ONLY function that matches
|
|
1676
|
+
* that path — Netlify does not define a winner when two serverless functions
|
|
1677
|
+
* both match, so we make the match unambiguous instead of relying on order.
|
|
1678
|
+
*
|
|
1679
|
+
* The Nitro-generated `server/server.mjs` is small and deterministic:
|
|
1680
|
+
* export { default } from "./main.mjs";
|
|
1681
|
+
* export const config = { ... excludedPath: ["/.netlify/*"], ... };
|
|
1682
|
+
* We parse the `excludedPath: [...]` array literal and add our path if absent.
|
|
1683
|
+
* If the shape ever changes and we can't find/parse it, we log and leave the
|
|
1684
|
+
* file untouched (the inline-40s fallback still keeps chat working).
|
|
1685
|
+
*/
|
|
1686
|
+
function excludeProcessRunPathFromServerCatchAll(serverDir: string): void {
|
|
1687
|
+
const serverEntry = path.join(serverDir, "server.mjs");
|
|
1688
|
+
if (!fs.existsSync(serverEntry)) {
|
|
1689
|
+
console.warn(
|
|
1690
|
+
"[build] Durable-background: server/server.mjs not found; cannot exclude " +
|
|
1691
|
+
`${AGENT_CHAT_PROCESS_RUN_PATH} from the /* catch-all.`,
|
|
1692
|
+
);
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
const original = fs.readFileSync(serverEntry, "utf8");
|
|
1696
|
+
if (original.includes(AGENT_CHAT_PROCESS_RUN_PATH)) {
|
|
1697
|
+
// Already excluded (idempotent — emit may run on a re-used output tree).
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
const excludedPathRe = /excludedPath:\s*\[([^\]]*)\]/;
|
|
1701
|
+
const match = original.match(excludedPathRe);
|
|
1702
|
+
const quotedPath = JSON.stringify(AGENT_CHAT_PROCESS_RUN_PATH);
|
|
1703
|
+
if (match) {
|
|
1704
|
+
const existing = match[1].trim();
|
|
1705
|
+
const next = existing
|
|
1706
|
+
? `excludedPath: [${existing.replace(/,\s*$/, "")}, ${quotedPath}]`
|
|
1707
|
+
: `excludedPath: [${quotedPath}]`;
|
|
1708
|
+
fs.writeFileSync(serverEntry, original.replace(excludedPathRe, next));
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
// No existing `excludedPath` — inject one into the config object. Match the
|
|
1712
|
+
// `path: "/*"` line and add `excludedPath` right after it.
|
|
1713
|
+
const pathLineRe = /(path:\s*("\/\*"|'\/\*'),?)/;
|
|
1714
|
+
if (pathLineRe.test(original)) {
|
|
1715
|
+
fs.writeFileSync(
|
|
1716
|
+
serverEntry,
|
|
1717
|
+
original.replace(pathLineRe, `$1\n excludedPath: [${quotedPath}],`),
|
|
1718
|
+
);
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
console.warn(
|
|
1722
|
+
"[build] Durable-background: could not locate excludedPath/path in " +
|
|
1723
|
+
"server/server.mjs; leaving the /* catch-all unchanged (the background " +
|
|
1724
|
+
"function may be shadowed — the inline-40s fallback still applies).",
|
|
1590
1725
|
);
|
|
1591
1726
|
}
|
|
1592
1727
|
|
|
@@ -24,10 +24,6 @@ function seededResponse(
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function isEmptyConfig(config: Record<string, unknown>): boolean {
|
|
28
|
-
return !Array.isArray(config.panels) || config.panels.length === 0;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
27
|
export default defineAction({
|
|
32
28
|
description:
|
|
33
29
|
"Get a SQL analytics dashboard by ID, including its full panel config, visibility, and access metadata.",
|
|
@@ -78,10 +74,6 @@ export default defineAction({
|
|
|
78
74
|
});
|
|
79
75
|
}
|
|
80
76
|
const config = dash.config as Record<string, unknown>;
|
|
81
|
-
if (isEmptyConfig(config)) {
|
|
82
|
-
const seed = loadDashboardSeed(args.id);
|
|
83
|
-
if (seed) return seededResponse(args.id, seed);
|
|
84
|
-
}
|
|
85
77
|
return {
|
|
86
78
|
id: args.id,
|
|
87
79
|
...config,
|
|
@@ -466,7 +466,9 @@ export default defineAction({
|
|
|
466
466
|
.optional()
|
|
467
467
|
.describe("Replace the whole dashboard config (or a JSON string)."),
|
|
468
468
|
}),
|
|
469
|
-
|
|
469
|
+
// The SQL dashboard editor persists user edits through callAction(), which
|
|
470
|
+
// needs this action mounted under /_agent-native/actions/update-dashboard.
|
|
471
|
+
http: { method: "POST" },
|
|
470
472
|
mcpApp: {
|
|
471
473
|
compactCatalog: true,
|
|
472
474
|
resource: embedApp({
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
import { SqlHighlight } from "@/components/SqlHighlight";
|
|
4
|
+
|
|
5
|
+
export interface SqlEditorProps extends Omit<
|
|
6
|
+
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
|
|
7
|
+
"value"
|
|
8
|
+
> {
|
|
9
|
+
value: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const SqlEditor = React.forwardRef<HTMLTextAreaElement, SqlEditorProps>(
|
|
13
|
+
({ className, onScroll, value, disabled, readOnly, ...props }, ref) => {
|
|
14
|
+
const highlightRef = React.useRef<HTMLPreElement | null>(null);
|
|
15
|
+
|
|
16
|
+
const handleScroll = (event: React.UIEvent<HTMLTextAreaElement>) => {
|
|
17
|
+
const highlight = highlightRef.current;
|
|
18
|
+
if (highlight) {
|
|
19
|
+
highlight.scrollTop = event.currentTarget.scrollTop;
|
|
20
|
+
highlight.scrollLeft = event.currentTarget.scrollLeft;
|
|
21
|
+
}
|
|
22
|
+
onScroll?.(event);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
className={cn(
|
|
28
|
+
"relative min-h-[200px] rounded-md border border-input bg-background ring-offset-background focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2",
|
|
29
|
+
disabled && "cursor-not-allowed opacity-50",
|
|
30
|
+
className,
|
|
31
|
+
)}
|
|
32
|
+
>
|
|
33
|
+
<SqlHighlight
|
|
34
|
+
ref={highlightRef}
|
|
35
|
+
aria-hidden="true"
|
|
36
|
+
sql={value || " "}
|
|
37
|
+
preClassName="pointer-events-none absolute inset-0 overflow-hidden rounded-md bg-transparent p-3 text-xs leading-5"
|
|
38
|
+
className="text-foreground"
|
|
39
|
+
/>
|
|
40
|
+
<textarea
|
|
41
|
+
ref={ref}
|
|
42
|
+
value={value}
|
|
43
|
+
disabled={disabled}
|
|
44
|
+
readOnly={readOnly}
|
|
45
|
+
onScroll={handleScroll}
|
|
46
|
+
spellCheck={false}
|
|
47
|
+
className={cn(
|
|
48
|
+
"relative z-10 min-h-[inherit] w-full resize-y rounded-md border-0 bg-transparent p-3 font-mono text-xs leading-5 text-transparent caret-foreground outline-none selection:bg-primary/30 placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed",
|
|
49
|
+
readOnly && "cursor-default",
|
|
50
|
+
)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
SqlEditor.displayName = "SqlEditor";
|