@agent-native/core 0.75.0 → 0.75.1
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/core/CHANGELOG.md +16 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/durable-background.ts +15 -3
- package/corpus/core/src/cli/skills.ts +15 -11
- package/corpus/core/src/deploy/build.ts +33 -10
- package/corpus/core/src/templates/default/_gitignore +1 -0
- package/corpus/core/src/templates/headless/_gitignore +1 -0
- package/corpus/core/src/templates/workspace-root/_gitignore +1 -0
- package/corpus/templates/clips/actions/list-recordings.ts +21 -0
- package/corpus/templates/clips/app/components/library/library-layout.tsx +21 -2
- package/corpus/templates/clips/app/hooks/use-library.ts +18 -0
- package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +7 -5
- package/dist/agent/durable-background.d.ts +7 -3
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +13 -3
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/cli/skills.d.ts +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +15 -11
- package/dist/cli/skills.js.map +1 -1
- package/dist/deploy/build.d.ts +14 -9
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +33 -10
- package/dist/deploy/build.js.map +1 -1
- package/dist/templates/default/_gitignore +1 -0
- package/dist/templates/headless/_gitignore +1 -0
- package/dist/templates/workspace-root/_gitignore +1 -0
- package/package.json +1 -1
- package/src/templates/default/_gitignore +1 -0
- package/src/templates/headless/_gitignore +1 -0
- package/src/templates/workspace-root/_gitignore +1 -0
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.75.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 98b89e2: Durable background agent-chat runs now actually receive Netlify's 15-min async
|
|
8
|
+
budget. The emitted `-background` function declared a custom `config.path` but
|
|
9
|
+
omitted `background: true`, so Netlify served it SYNCHRONOUSLY (~60s) — the
|
|
10
|
+
`config` object overrides the legacy `-background` filename convention — and the
|
|
11
|
+
durable worker was capped at the 60s wall (it degraded to 40s-chunked runs and
|
|
12
|
+
never used the 15-min budget). The emit now sets `background: true` (immediate
|
|
13
|
+
202 ack + 15-min execution) and force-marks the background runtime in the
|
|
14
|
+
function entry so the worker reliably takes the ~13-min soft-timeout regardless
|
|
15
|
+
of the deployed Lambda name. Root cause confirmed with a live prod routing probe
|
|
16
|
+
(POST to the process-run path returned a synchronous 401 from the handler
|
|
17
|
+
instead of a 202 async ack).
|
|
18
|
+
|
|
3
19
|
## 0.75.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -112,11 +112,23 @@ export function isHostedRuntimeForDurableBackground(): boolean {
|
|
|
112
112
|
* hard wall and get killed at 60s, then re-dispatch/resume in a wasteful loop.
|
|
113
113
|
* So the 13-min budget is taken ONLY when this returns true.
|
|
114
114
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
115
|
+
* The PRIMARY signal is a `globalThis` marker the emitted background function's
|
|
116
|
+
* entry sets at cold start — the deployed Lambda name is not guaranteed to end
|
|
117
|
+
* in `-background` on Netlify, so the entry marks its own runtime. A `globalThis`
|
|
118
|
+
* flag (not `process.env`) keeps the no-env-mutation guard satisfied and carries
|
|
119
|
+
* no cross-request state (set once per isolate). The `AWS_LAMBDA_FUNCTION_NAME`
|
|
120
|
+
* suffix and the explicit `AGENT_CHAT_FORCE_BACKGROUND_RUNTIME` env (truthy) are
|
|
121
|
+
* additional signals — the latter an operator escape hatch. Off by default.
|
|
118
122
|
*/
|
|
119
123
|
export function isInBackgroundFunctionRuntime(): boolean {
|
|
124
|
+
// Set by the emitted `-background` function entry at cold start (the primary,
|
|
125
|
+
// most reliable signal — see the emit in deploy/build.ts).
|
|
126
|
+
if (
|
|
127
|
+
(globalThis as Record<string, unknown>)
|
|
128
|
+
.__AGENT_NATIVE_BACKGROUND_RUNTIME__ === true
|
|
129
|
+
) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
120
132
|
const lambdaName = process.env.AWS_LAMBDA_FUNCTION_NAME;
|
|
121
133
|
if (
|
|
122
134
|
typeof lambdaName === "string" &&
|
|
@@ -394,14 +394,16 @@ sign-in at setup — this is intended), so the first tool call in that client do
|
|
|
394
394
|
not hit an OAuth wall:
|
|
395
395
|
|
|
396
396
|
\`\`\`bash
|
|
397
|
-
npx @agent-native/core@latest skills add visual-
|
|
397
|
+
npx @agent-native/core@latest skills add visual-plans
|
|
398
398
|
\`\`\`
|
|
399
399
|
|
|
400
400
|
After that, \`/visual-plan\` and \`/visual-recap\` are the two installed slash
|
|
401
|
-
commands.
|
|
402
|
-
\`
|
|
403
|
-
|
|
404
|
-
|
|
401
|
+
commands. If you only need one command, use \`skills add visual-plan\` or
|
|
402
|
+
\`skills add visual-recap\` instead. The other planning modes
|
|
403
|
+
(\`create-ui-plan\`, \`create-prototype-plan\`, \`create-plan-design\`,
|
|
404
|
+
\`create-visual-questions\`) are MCP tools reachable from \`/visual-plan\`, not
|
|
405
|
+
separate slash commands. Pass \`--no-connect\` to register the connector without
|
|
406
|
+
authenticating, then run
|
|
405
407
|
\`npx @agent-native/core@latest connect https://plan.agent-native.com --client all\`
|
|
406
408
|
whenever you are ready, or choose a narrower \`--client\`. Auth and MCP tool
|
|
407
409
|
loading are per client config/session.
|
|
@@ -410,7 +412,7 @@ loading are per client config/session.
|
|
|
410
412
|
install with \`--mode local-files\`:
|
|
411
413
|
|
|
412
414
|
\`\`\`bash
|
|
413
|
-
npx @agent-native/core@latest skills add visual-
|
|
415
|
+
npx @agent-native/core@latest skills add visual-plans --mode local-files
|
|
414
416
|
\`\`\`
|
|
415
417
|
|
|
416
418
|
This mode does not register the Plan MCP connector. Before authoring structured
|
|
@@ -1648,14 +1650,16 @@ sign-in at setup — this is intended), so the first tool call in that client do
|
|
|
1648
1650
|
not hit an OAuth wall:
|
|
1649
1651
|
|
|
1650
1652
|
\`\`\`bash
|
|
1651
|
-
npx @agent-native/core@latest skills add visual-
|
|
1653
|
+
npx @agent-native/core@latest skills add visual-plans
|
|
1652
1654
|
\`\`\`
|
|
1653
1655
|
|
|
1654
1656
|
After that, \`/visual-plan\` and \`/visual-recap\` are the two installed slash
|
|
1655
|
-
commands.
|
|
1656
|
-
\`
|
|
1657
|
-
|
|
1658
|
-
|
|
1657
|
+
commands. If you only need one command, use \`skills add visual-plan\` or
|
|
1658
|
+
\`skills add visual-recap\` instead. The other planning modes
|
|
1659
|
+
(\`create-ui-plan\`, \`create-prototype-plan\`, \`create-plan-design\`,
|
|
1660
|
+
\`create-visual-questions\`) are MCP tools reachable from \`/visual-plan\`, not
|
|
1661
|
+
separate slash commands. Pass \`--no-connect\` to register the connector without
|
|
1662
|
+
authenticating, then run
|
|
1659
1663
|
\`npx @agent-native/core@latest connect https://plan.agent-native.com --client all\`
|
|
1660
1664
|
whenever you are ready, or choose a narrower \`--client\`. Auth and MCP tool
|
|
1661
1665
|
loading are per client config/session.
|
|
@@ -1512,15 +1512,20 @@ export function isDurableBackgroundDeployEnabled(): boolean {
|
|
|
1512
1512
|
* that directory to a sibling `<...>-background` function and write an entry
|
|
1513
1513
|
* with a `config.path` of the process-run route.
|
|
1514
1514
|
*
|
|
1515
|
-
*
|
|
1516
|
-
*
|
|
1517
|
-
*
|
|
1518
|
-
*
|
|
1519
|
-
*
|
|
1520
|
-
*
|
|
1521
|
-
*
|
|
1522
|
-
*
|
|
1523
|
-
*
|
|
1515
|
+
* The function declares `background: true` so Netlify invokes it ASYNC (HTTP
|
|
1516
|
+
* 202 ack) with the 15-min budget, and a `config.path` of the process-run route
|
|
1517
|
+
* so the `_process-run` self-dispatch lands on it. The earlier version set the
|
|
1518
|
+
* path but omitted `background: true` and relied on the legacy `-background`
|
|
1519
|
+
* filename — which the `config` object overrides — so Netlify served it
|
|
1520
|
+
* SYNCHRONOUSLY (~60s) and the durable worker never got the 15-min budget
|
|
1521
|
+
* (confirmed live: a POST to the process-run path returned a synchronous 401
|
|
1522
|
+
* from the handler instead of a 202 async ack).
|
|
1523
|
+
*
|
|
1524
|
+
* ⚠️ One Netlify runtime behavior still resolves only on a real deploy: the
|
|
1525
|
+
* routing precedence between this function's `config.path` and Nitro's catch-all
|
|
1526
|
+
* for that exact path. If the catch-all wins, the run still completes via the
|
|
1527
|
+
* 40s soft-timeout path on the sync function (no durable win, no regression).
|
|
1528
|
+
* See docs/design/durable-agent-runs.md (Open risks #1).
|
|
1524
1529
|
*/
|
|
1525
1530
|
export function emitSingleTemplateNetlifyBackgroundFunction(
|
|
1526
1531
|
projectCwd: string,
|
|
@@ -1543,7 +1548,16 @@ export function emitSingleTemplateNetlifyBackgroundFunction(
|
|
|
1543
1548
|
// Drop the original Nitro entry so our background entry is the entrypoint.
|
|
1544
1549
|
fs.rmSync(path.join(dest, "server.mjs"), { force: true });
|
|
1545
1550
|
|
|
1546
|
-
const entry =
|
|
1551
|
+
const entry = `// Mark this isolate as the durable background runtime BEFORE the handler
|
|
1552
|
+
// bundle is imported, so isInBackgroundFunctionRuntime() reliably returns true
|
|
1553
|
+
// in this function. The deployed Lambda name is NOT guaranteed to end in
|
|
1554
|
+
// "-background" (Netlify may mangle/prefix it), so we cannot depend on
|
|
1555
|
+
// AWS_LAMBDA_FUNCTION_NAME alone. A globalThis flag (NOT process.env) avoids the
|
|
1556
|
+
// no-env-mutation guard and carries no cross-request state — it is a static,
|
|
1557
|
+
// set-once isolate marker read back by isInBackgroundFunctionRuntime().
|
|
1558
|
+
globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true;
|
|
1559
|
+
|
|
1560
|
+
let cachedHandler;
|
|
1547
1561
|
|
|
1548
1562
|
export default async function handler(...args) {
|
|
1549
1563
|
cachedHandler ??= (await import("./main.mjs")).default;
|
|
@@ -1553,6 +1567,15 @@ export default async function handler(...args) {
|
|
|
1553
1567
|
export const config = {
|
|
1554
1568
|
name: "agent background handler",
|
|
1555
1569
|
generator: "agent-native build",
|
|
1570
|
+
// background: true is what actually makes Netlify invoke this ASYNCHRONOUSLY
|
|
1571
|
+
// (immediate HTTP 202 ack) with the 15-minute budget. Without it, a function
|
|
1572
|
+
// that declares a custom \`path\` is served SYNCHRONOUSLY (~60s) even when its
|
|
1573
|
+
// file name ends in "-background" — the \`config\` object overrides the legacy
|
|
1574
|
+
// filename convention. That omission is why the durable worker was capped at
|
|
1575
|
+
// ~60s in prod (verified live: POST to the process-run path returned a
|
|
1576
|
+
// synchronous 401 from the handler, not a 202 async ack). See Netlify docs:
|
|
1577
|
+
// build/functions/background-functions + build/functions/configuration.
|
|
1578
|
+
background: true,
|
|
1556
1579
|
path: ${JSON.stringify([AGENT_CHAT_PROCESS_RUN_PATH])},
|
|
1557
1580
|
nodeBundler: "none",
|
|
1558
1581
|
includedFiles: ["**"],
|
|
@@ -55,6 +55,15 @@ export default defineAction({
|
|
|
55
55
|
.describe("Sort order"),
|
|
56
56
|
limit: z.coerce.number().int().min(1).max(500).default(100),
|
|
57
57
|
offset: z.coerce.number().int().min(0).default(0),
|
|
58
|
+
countOnly: z
|
|
59
|
+
// Robust coercion: a GET query param arrives as the string "true"/"false",
|
|
60
|
+
// and z.coerce.boolean would treat "false" as true. Map strings explicitly.
|
|
61
|
+
.preprocess(
|
|
62
|
+
(v) => (typeof v === "string" ? v === "true" : v),
|
|
63
|
+
z.boolean(),
|
|
64
|
+
)
|
|
65
|
+
.default(false)
|
|
66
|
+
.describe("Return only the total count, skipping the row payload"),
|
|
58
67
|
}),
|
|
59
68
|
http: { method: "GET" },
|
|
60
69
|
run: async (args) => {
|
|
@@ -135,6 +144,18 @@ export default defineAction({
|
|
|
135
144
|
);
|
|
136
145
|
}
|
|
137
146
|
|
|
147
|
+
// Count-only callers (e.g. the sidebar badge) need just the total for the
|
|
148
|
+
// same filters, ignoring limit/offset. Run the COUNT and short-circuit
|
|
149
|
+
// before the row select, joins, and tag/view subqueries. Keeping it inside
|
|
150
|
+
// this branch means the normal list path doesn't pay for an extra query.
|
|
151
|
+
if (args.countOnly) {
|
|
152
|
+
const totalRows = await db
|
|
153
|
+
.select({ count: sql<number>`COUNT(1)` })
|
|
154
|
+
.from(schema.recordings)
|
|
155
|
+
.where(and(...whereClauses));
|
|
156
|
+
return { recordings: [], total: Number(totalRows[0]?.count ?? 0) };
|
|
157
|
+
}
|
|
158
|
+
|
|
138
159
|
// Sort
|
|
139
160
|
const viewCountOrder = sql<number>`(
|
|
140
161
|
SELECT COUNT(1)
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
useSpaces,
|
|
47
47
|
useOrganizations,
|
|
48
48
|
useCreateFolder,
|
|
49
|
+
useRecordingsCount,
|
|
49
50
|
} from "@/hooks/use-library";
|
|
50
51
|
import { useIsMobile } from "@/hooks/use-mobile";
|
|
51
52
|
import { useDesktopPromo } from "@/hooks/use-desktop-promo";
|
|
@@ -124,6 +125,10 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
|
|
|
124
125
|
{ enabled: hasActiveOrg && Boolean(currentOrganizationId) },
|
|
125
126
|
);
|
|
126
127
|
|
|
128
|
+
// Clip count for the "Library" nav item — count-only, no row payload or
|
|
129
|
+
// title polling across the app shell.
|
|
130
|
+
const { data: libraryCount } = useRecordingsCount({ view: "library" });
|
|
131
|
+
|
|
127
132
|
const libFolderList: FolderNode[] = useMemo(
|
|
128
133
|
() =>
|
|
129
134
|
(libFolders?.folders ?? [])
|
|
@@ -176,12 +181,14 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
|
|
|
176
181
|
label: string;
|
|
177
182
|
icon: React.ComponentType<{ className?: string }>;
|
|
178
183
|
match: (path: string) => boolean;
|
|
184
|
+
count?: number;
|
|
179
185
|
}[] = [
|
|
180
186
|
{
|
|
181
187
|
to: "/library",
|
|
182
188
|
label: "Library",
|
|
183
189
|
icon: IconInbox,
|
|
184
190
|
match: (p) => p.startsWith("/library"),
|
|
191
|
+
count: libraryCount,
|
|
185
192
|
},
|
|
186
193
|
{
|
|
187
194
|
to: "/spaces",
|
|
@@ -374,7 +381,7 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
|
|
|
374
381
|
</div>
|
|
375
382
|
|
|
376
383
|
<nav className="mt-3 space-y-0.5 px-2">
|
|
377
|
-
{navItems.map(({ to, label, icon: Icon, match }) => {
|
|
384
|
+
{navItems.map(({ to, label, icon: Icon, match, count }) => {
|
|
378
385
|
const active = match(location.pathname);
|
|
379
386
|
return (
|
|
380
387
|
<NavLink
|
|
@@ -388,7 +395,19 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
|
|
|
388
395
|
)}
|
|
389
396
|
>
|
|
390
397
|
<Icon className="h-4 w-4" />
|
|
391
|
-
{label}
|
|
398
|
+
<span className="flex-1 truncate">{label}</span>
|
|
399
|
+
{count !== undefined && count > 0 && (
|
|
400
|
+
<span
|
|
401
|
+
className={cn(
|
|
402
|
+
"shrink-0 tabular-nums text-[11px]",
|
|
403
|
+
active
|
|
404
|
+
? "text-primary/80"
|
|
405
|
+
: "text-muted-foreground",
|
|
406
|
+
)}
|
|
407
|
+
>
|
|
408
|
+
{count}
|
|
409
|
+
</span>
|
|
410
|
+
)}
|
|
392
411
|
</NavLink>
|
|
393
412
|
);
|
|
394
413
|
})}
|
|
@@ -87,6 +87,24 @@ export function useRecordings(args: ListRecordingsArgs = {}) {
|
|
|
87
87
|
);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Count-only variant for surfaces like the sidebar badge that need a total but
|
|
92
|
+
* not the rows. Hits `list-recordings` with `countOnly`, so it skips the row
|
|
93
|
+
* payload server-side and doesn't share (or pay for) the full-list query or its
|
|
94
|
+
* title polling.
|
|
95
|
+
*/
|
|
96
|
+
export function useRecordingsCount(
|
|
97
|
+
args: Omit<ListRecordingsArgs, "limit" | "offset"> = {},
|
|
98
|
+
) {
|
|
99
|
+
return useActionQuery<number>(
|
|
100
|
+
"list-recordings",
|
|
101
|
+
{ ...args, countOnly: true } as any,
|
|
102
|
+
{
|
|
103
|
+
select: (data: any) => (typeof data?.total === "number" ? data.total : 0),
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
90
108
|
export interface SearchHit {
|
|
91
109
|
id: string;
|
|
92
110
|
title: string;
|
|
@@ -489,14 +489,16 @@ sign-in at setup — this is intended), so the first tool call in that client do
|
|
|
489
489
|
not hit an OAuth wall:
|
|
490
490
|
|
|
491
491
|
```bash
|
|
492
|
-
npx @agent-native/core@latest skills add visual-
|
|
492
|
+
npx @agent-native/core@latest skills add visual-plans
|
|
493
493
|
```
|
|
494
494
|
|
|
495
495
|
After that, `/visual-plan` and `/visual-recap` are the two installed slash
|
|
496
|
-
commands.
|
|
497
|
-
`
|
|
498
|
-
|
|
499
|
-
|
|
496
|
+
commands. If you only need one command, use `skills add visual-plan` or
|
|
497
|
+
`skills add visual-recap` instead. The other planning modes
|
|
498
|
+
(`create-ui-plan`, `create-prototype-plan`, `create-plan-design`,
|
|
499
|
+
`create-visual-questions`) are MCP tools reachable from `/visual-plan`, not
|
|
500
|
+
separate slash commands. Pass `--no-connect` to register the connector without
|
|
501
|
+
authenticating, then run
|
|
500
502
|
`npx @agent-native/core@latest connect https://plan.agent-native.com --client all`
|
|
501
503
|
whenever you are ready, or choose a narrower `--client`. Auth and MCP tool
|
|
502
504
|
loading are per client config/session.
|
|
@@ -39,9 +39,13 @@ export declare function isHostedRuntimeForDurableBackground(): boolean;
|
|
|
39
39
|
* hard wall and get killed at 60s, then re-dispatch/resume in a wasteful loop.
|
|
40
40
|
* So the 13-min budget is taken ONLY when this returns true.
|
|
41
41
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* The PRIMARY signal is a `globalThis` marker the emitted background function's
|
|
43
|
+
* entry sets at cold start — the deployed Lambda name is not guaranteed to end
|
|
44
|
+
* in `-background` on Netlify, so the entry marks its own runtime. A `globalThis`
|
|
45
|
+
* flag (not `process.env`) keeps the no-env-mutation guard satisfied and carries
|
|
46
|
+
* no cross-request state (set once per isolate). The `AWS_LAMBDA_FUNCTION_NAME`
|
|
47
|
+
* suffix and the explicit `AGENT_CHAT_FORCE_BACKGROUND_RUNTIME` env (truthy) are
|
|
48
|
+
* additional signals — the latter an operator escape hatch. Off by default.
|
|
45
49
|
*/
|
|
46
50
|
export declare function isInBackgroundFunctionRuntime(): boolean;
|
|
47
51
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"durable-background.d.ts","sourceRoot":"","sources":["../../src/agent/durable-background.ts"],"names":[],"mappings":"AA6CA;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,2CACE,CAAC;AAE3C;;;GAGG;AACH,eAAO,MAAM,iCAAiC,kCACb,CAAC;AAElC;;;;;;GAMG;AACH,eAAO,MAAM,+BAA+B,oBAAoB,CAAC;AAEjE;;;;GAIG;AACH,wBAAgB,mCAAmC,IAAI,OAAO,CAsB7D;AAED
|
|
1
|
+
{"version":3,"file":"durable-background.d.ts","sourceRoot":"","sources":["../../src/agent/durable-background.ts"],"names":[],"mappings":"AA6CA;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,2CACE,CAAC;AAE3C;;;GAGG;AACH,eAAO,MAAM,iCAAiC,kCACb,CAAC;AAElC;;;;;;GAMG;AACH,eAAO,MAAM,+BAA+B,oBAAoB,CAAC;AAEjE;;;;GAIG;AACH,wBAAgB,mCAAmC,IAAI,OAAO,CAsB7D;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,6BAA6B,IAAI,OAAO,CAsBvD;AAyBD;;;;;;GAMG;AACH,wBAAgB,mCAAmC,IAAI,OAAO,CAM7D;AAED,uDAAuD;AACvD,MAAM,MAAM,qBAAqB,GAC7B;IACE,EAAE,EAAE,IAAI,CAAC;IACT,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEN;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,OAAO,EACb,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,qBAAqB,CA0CvB"}
|
|
@@ -93,11 +93,21 @@ export function isHostedRuntimeForDurableBackground() {
|
|
|
93
93
|
* hard wall and get killed at 60s, then re-dispatch/resume in a wasteful loop.
|
|
94
94
|
* So the 13-min budget is taken ONLY when this returns true.
|
|
95
95
|
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
96
|
+
* The PRIMARY signal is a `globalThis` marker the emitted background function's
|
|
97
|
+
* entry sets at cold start — the deployed Lambda name is not guaranteed to end
|
|
98
|
+
* in `-background` on Netlify, so the entry marks its own runtime. A `globalThis`
|
|
99
|
+
* flag (not `process.env`) keeps the no-env-mutation guard satisfied and carries
|
|
100
|
+
* no cross-request state (set once per isolate). The `AWS_LAMBDA_FUNCTION_NAME`
|
|
101
|
+
* suffix and the explicit `AGENT_CHAT_FORCE_BACKGROUND_RUNTIME` env (truthy) are
|
|
102
|
+
* additional signals — the latter an operator escape hatch. Off by default.
|
|
99
103
|
*/
|
|
100
104
|
export function isInBackgroundFunctionRuntime() {
|
|
105
|
+
// Set by the emitted `-background` function entry at cold start (the primary,
|
|
106
|
+
// most reliable signal — see the emit in deploy/build.ts).
|
|
107
|
+
if (globalThis
|
|
108
|
+
.__AGENT_NATIVE_BACKGROUND_RUNTIME__ === true) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
101
111
|
const lambdaName = process.env.AWS_LAMBDA_FUNCTION_NAME;
|
|
102
112
|
if (typeof lambdaName === "string" &&
|
|
103
113
|
lambdaName.toLowerCase().endsWith("-background")) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"durable-background.js","sourceRoot":"","sources":["../../src/agent/durable-background.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Durable background agent-chat runs (Netlify background functions).\n *\n * Off by default. When enabled, a long in-app agent-chat turn is dispatched\n * into a Netlify *background* function (15-min budget) instead of completing\n * synchronously under the ~40s soft-timeout. The foreground POST claims the\n * run slot, inserts the run row, fires an HMAC-signed self-dispatch to\n * `AGENT_CHAT_PROCESS_RUN_PATH`, and returns the existing SSE subscription so\n * the client streams the same events (via the cross-isolate SQL-poll path)\n * with no client change.\n *\n * This module owns ONLY the gating decision + shared constants so both the\n * HTTP handler (`production-agent.ts`) and the processor route\n * (`agent-chat-plugin.ts`) agree on when the path is active without a circular\n * import. The actual run machinery is reused verbatim from run-manager /\n * run-store / self-dispatch / internal-token.\n *\n * GUARDRAIL: when `isAgentChatDurableBackgroundEnabled()` returns false, the\n * agent-chat handler must behave byte-for-byte like the current synchronous\n * path. The gate is true only when ALL of these hold:\n * 1. `AGENT_CHAT_DURABLE_BACKGROUND` env is not explicitly disabled. It is\n * DEFAULT-ON: unset/empty/unknown counts as enabled; set it to a falsy\n * value (`false`/`0`/`no`/`off`) to opt a specific app back out.\n * 2. The runtime is hosted/serverless (local dev keeps the inline path so SSE\n * stays a single live stream and no second function is needed).\n * 3. `A2A_SECRET` is configured (the HMAC handoff is required to authenticate\n * the background dispatch; without it the dispatch can't be trusted).\n *\n * Default-on is safe because a *dispatch failure degrades to an inline run*: if\n * the self-dispatch self-POST can't be delivered (fast connection error or\n * fast non-2xx), the foreground handler runs the turn synchronously instead of\n * erroring (see `production-agent.ts` — the inline fallback claims the run row\n * atomically so a delayed delivery can never double-execute). So an app where\n * durable dispatch happens to fail still gets a working chat, just without the\n * 15-min budget.\n */\nimport {\n hasConfiguredA2ASecret,\n isA2AProductionRuntime,\n} from \"../a2a/auth-policy.js\";\nimport {\n extractBearerToken,\n verifyInternalToken,\n} from \"../integrations/internal-token.js\";\n\n/**\n * Framework route the background function actually runs — sibling to\n * `AGENT_TEAM_PROCESS_RUN_PATH`. Reached *through* the Netlify background\n * function, so it inherits the 15-min budget.\n */\nexport const AGENT_CHAT_PROCESS_RUN_PATH =\n \"/_agent-native/agent-chat/_process-run\";\n\n/**\n * Env flag for durable background runs. DEFAULT-ON: unset means enabled; an app\n * opts OUT with an explicit falsy value (`false`/`0`/`no`/`off`).\n */\nexport const AGENT_CHAT_DURABLE_BACKGROUND_ENV =\n \"AGENT_CHAT_DURABLE_BACKGROUND\";\n\n/**\n * Body field the foreground handler injects when self-dispatching to the\n * background processor. Its presence is how the re-entered handler knows it is\n * the background worker (run inline with the background soft-timeout; do NOT\n * re-claim the slot or re-dispatch). Untrusted on its own — the route also\n * verifies the HMAC token before invoking the handler.\n */\nexport const AGENT_CHAT_BACKGROUND_RUN_FIELD = \"__backgroundRun\";\n\n/**\n * Mirror of run-manager's private `isHostedRuntime`. Kept in sync deliberately:\n * the durable-background gate must agree with the soft-timeout regime about\n * what \"hosted\" means.\n */\nexport function isHostedRuntimeForDurableBackground(): boolean {\n if (\n process.env.NETLIFY &&\n process.env.NETLIFY !== \"false\" &&\n process.env.NETLIFY_LOCAL !== \"true\"\n ) {\n return true;\n }\n if (\n process.env.AWS_LAMBDA_FUNCTION_NAME &&\n process.env.NETLIFY_LOCAL !== \"true\"\n ) {\n return true;\n }\n return Boolean(\n process.env.CF_PAGES ||\n process.env.VERCEL ||\n process.env.VERCEL_ENV ||\n process.env.RENDER ||\n process.env.FLY_APP_NAME ||\n process.env.K_SERVICE,\n );\n}\n\n/**\n * True when THIS process is actually executing inside a Netlify *background*\n * function (the long, 15-min-budget async function whose deployed name ends in\n * `-background`). Netlify runs functions on AWS Lambda and sets\n * `AWS_LAMBDA_FUNCTION_NAME` to the function's name, so a `-background` suffix is\n * the runtime proof that the ~60s synchronous wall does NOT apply here.\n *\n * This is the SAFETY GUARD for the soft-timeout regime. The `_process-run`\n * self-dispatch worker (`isBackgroundWorker`) is NOT enough on its own: if the\n * `-background` function was never emitted (deploy gate off, or Netlify routed\n * the path to the synchronous function), the self-POST lands on the regular\n * ~60s `server` function. A worker there MUST use the 40s soft-timeout and\n * checkpoint before the 60s wall — using the ~13min budget would overshoot the\n * hard wall and get killed at 60s, then re-dispatch/resume in a wasteful loop.\n * So the 13-min budget is taken ONLY when this returns true.\n *\n * Falls back to the explicit `AGENT_CHAT_FORCE_BACKGROUND_RUNTIME` env (truthy)\n * for hosts that don't expose a `-background`-suffixed function name but where\n * the operator has confirmed a long async budget. Off by default.\n */\nexport function isInBackgroundFunctionRuntime(): boolean {\n const lambdaName = process.env.AWS_LAMBDA_FUNCTION_NAME;\n if (\n typeof lambdaName === \"string\" &&\n lambdaName.toLowerCase().endsWith(\"-background\")\n ) {\n return true;\n }\n const forced = process.env.AGENT_CHAT_FORCE_BACKGROUND_RUNTIME;\n if (forced != null) {\n const v = forced.trim().toLowerCase();\n return v === \"1\" || v === \"true\" || v === \"yes\" || v === \"on\";\n }\n return false;\n}\n\nfunction isFlagEnabled(): boolean {\n // Read the literal key (not `process.env[CONST]`) so guard:no-env-credentials\n // can statically verify it against the allowlisted `AGENT_*` prefix. Keep this\n // in sync with AGENT_CHAT_DURABLE_BACKGROUND_ENV.\n //\n // DEFAULT-ON: durable background runs are the desired behavior for every\n // hosted app. So an unset/empty/unknown flag means ON; an app opts OUT only\n // with an explicit falsy value. This still composes with the hosted +\n // A2A_SECRET gates below, so non-hosted / unconfigured apps stay synchronous.\n // Safety net: a failed dispatch degrades to a synchronous inline run (see\n // production-agent.ts), so default-on cannot break chat even if the\n // self-dispatch can't be delivered on a given app.\n const raw = process.env.AGENT_CHAT_DURABLE_BACKGROUND;\n if (raw == null) return true;\n const normalized = raw.trim().toLowerCase();\n return !(\n normalized === \"0\" ||\n normalized === \"false\" ||\n normalized === \"no\" ||\n normalized === \"off\"\n );\n}\n\n/**\n * The single gate. True when the flag is not explicitly disabled (default-on)\n * AND the runtime is hosted AND A2A_SECRET is configured. False otherwise — and\n * false means the current synchronous behavior is used, unchanged. So a local /\n * non-hosted / unconfigured app stays synchronous even with the flag defaulting\n * on; durable only engages where the runtime actually supports it.\n */\nexport function isAgentChatDurableBackgroundEnabled(): boolean {\n return (\n isFlagEnabled() &&\n isHostedRuntimeForDurableBackground() &&\n hasConfiguredA2ASecret()\n );\n}\n\n/** Decision returned by `prepareProcessRunRequest`. */\nexport type ProcessRunPreparation =\n | {\n ok: true;\n /** The pre-claimed run id the background worker must reuse. */\n runId: string;\n /** Body to stash for the re-entered handler (marker guaranteed present). */\n body: Record<string, unknown>;\n }\n | {\n ok: false;\n /** HTTP status the route should return. */\n status: number;\n /** Error payload. */\n error: string;\n };\n\n/**\n * Pure, transport-agnostic core of the `_process-run` route: validate the body,\n * authenticate the HMAC self-dispatch, and produce the body the re-entered\n * agent-chat handler should run as the background worker.\n *\n * Auth policy mirrors the agent-teams processor exactly:\n * - `A2A_SECRET` set → require a valid `verifyInternalToken(runId, token)`.\n * - no secret but a production runtime → refuse (503) — never run unsigned in\n * prod.\n * - no secret + non-prod (local dev) → allow unsigned; the SQL atomic claim\n * in the worker still prevents double-processing.\n *\n * Extracted from the route handler so the auth + marker-prep decision is unit\n * testable without booting the whole Nitro plugin. The route only adds body\n * reading and the final handler invocation around this.\n */\nexport function prepareProcessRunRequest(\n body: unknown,\n authHeader: string | undefined,\n): ProcessRunPreparation {\n if (!body || typeof body !== \"object\") {\n return { ok: false, status: 400, error: \"Invalid request body\" };\n }\n const record = body as Record<string, unknown>;\n const marker = record[AGENT_CHAT_BACKGROUND_RUN_FIELD] as\n | { runId?: unknown }\n | undefined;\n const runId =\n marker && typeof marker.runId === \"string\"\n ? marker.runId\n : typeof record.taskId === \"string\"\n ? (record.taskId as string)\n : \"\";\n if (!runId) {\n return { ok: false, status: 400, error: \"runId required\" };\n }\n\n if (hasConfiguredA2ASecret()) {\n const token = extractBearerToken(authHeader);\n if (!verifyInternalToken(runId, token ?? \"\")) {\n return {\n ok: false,\n status: 401,\n error: \"Invalid or expired processor token\",\n };\n }\n } else if (isA2AProductionRuntime()) {\n return {\n ok: false,\n status: 503,\n error:\n \"Agent chat background processor not configured — set A2A_SECRET on this deployment.\",\n };\n }\n\n // Ensure the marker is present so the re-entered handler runs as the\n // background worker (reuses runId/turnId, no re-claim, no re-dispatch).\n if (!marker || typeof marker.runId !== \"string\") {\n record[AGENT_CHAT_BACKGROUND_RUN_FIELD] = { runId };\n }\n return { ok: true, runId, body: record };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"durable-background.js","sourceRoot":"","sources":["../../src/agent/durable-background.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Durable background agent-chat runs (Netlify background functions).\n *\n * Off by default. When enabled, a long in-app agent-chat turn is dispatched\n * into a Netlify *background* function (15-min budget) instead of completing\n * synchronously under the ~40s soft-timeout. The foreground POST claims the\n * run slot, inserts the run row, fires an HMAC-signed self-dispatch to\n * `AGENT_CHAT_PROCESS_RUN_PATH`, and returns the existing SSE subscription so\n * the client streams the same events (via the cross-isolate SQL-poll path)\n * with no client change.\n *\n * This module owns ONLY the gating decision + shared constants so both the\n * HTTP handler (`production-agent.ts`) and the processor route\n * (`agent-chat-plugin.ts`) agree on when the path is active without a circular\n * import. The actual run machinery is reused verbatim from run-manager /\n * run-store / self-dispatch / internal-token.\n *\n * GUARDRAIL: when `isAgentChatDurableBackgroundEnabled()` returns false, the\n * agent-chat handler must behave byte-for-byte like the current synchronous\n * path. The gate is true only when ALL of these hold:\n * 1. `AGENT_CHAT_DURABLE_BACKGROUND` env is not explicitly disabled. It is\n * DEFAULT-ON: unset/empty/unknown counts as enabled; set it to a falsy\n * value (`false`/`0`/`no`/`off`) to opt a specific app back out.\n * 2. The runtime is hosted/serverless (local dev keeps the inline path so SSE\n * stays a single live stream and no second function is needed).\n * 3. `A2A_SECRET` is configured (the HMAC handoff is required to authenticate\n * the background dispatch; without it the dispatch can't be trusted).\n *\n * Default-on is safe because a *dispatch failure degrades to an inline run*: if\n * the self-dispatch self-POST can't be delivered (fast connection error or\n * fast non-2xx), the foreground handler runs the turn synchronously instead of\n * erroring (see `production-agent.ts` — the inline fallback claims the run row\n * atomically so a delayed delivery can never double-execute). So an app where\n * durable dispatch happens to fail still gets a working chat, just without the\n * 15-min budget.\n */\nimport {\n hasConfiguredA2ASecret,\n isA2AProductionRuntime,\n} from \"../a2a/auth-policy.js\";\nimport {\n extractBearerToken,\n verifyInternalToken,\n} from \"../integrations/internal-token.js\";\n\n/**\n * Framework route the background function actually runs — sibling to\n * `AGENT_TEAM_PROCESS_RUN_PATH`. Reached *through* the Netlify background\n * function, so it inherits the 15-min budget.\n */\nexport const AGENT_CHAT_PROCESS_RUN_PATH =\n \"/_agent-native/agent-chat/_process-run\";\n\n/**\n * Env flag for durable background runs. DEFAULT-ON: unset means enabled; an app\n * opts OUT with an explicit falsy value (`false`/`0`/`no`/`off`).\n */\nexport const AGENT_CHAT_DURABLE_BACKGROUND_ENV =\n \"AGENT_CHAT_DURABLE_BACKGROUND\";\n\n/**\n * Body field the foreground handler injects when self-dispatching to the\n * background processor. Its presence is how the re-entered handler knows it is\n * the background worker (run inline with the background soft-timeout; do NOT\n * re-claim the slot or re-dispatch). Untrusted on its own — the route also\n * verifies the HMAC token before invoking the handler.\n */\nexport const AGENT_CHAT_BACKGROUND_RUN_FIELD = \"__backgroundRun\";\n\n/**\n * Mirror of run-manager's private `isHostedRuntime`. Kept in sync deliberately:\n * the durable-background gate must agree with the soft-timeout regime about\n * what \"hosted\" means.\n */\nexport function isHostedRuntimeForDurableBackground(): boolean {\n if (\n process.env.NETLIFY &&\n process.env.NETLIFY !== \"false\" &&\n process.env.NETLIFY_LOCAL !== \"true\"\n ) {\n return true;\n }\n if (\n process.env.AWS_LAMBDA_FUNCTION_NAME &&\n process.env.NETLIFY_LOCAL !== \"true\"\n ) {\n return true;\n }\n return Boolean(\n process.env.CF_PAGES ||\n process.env.VERCEL ||\n process.env.VERCEL_ENV ||\n process.env.RENDER ||\n process.env.FLY_APP_NAME ||\n process.env.K_SERVICE,\n );\n}\n\n/**\n * True when THIS process is actually executing inside a Netlify *background*\n * function (the long, 15-min-budget async function whose deployed name ends in\n * `-background`). Netlify runs functions on AWS Lambda and sets\n * `AWS_LAMBDA_FUNCTION_NAME` to the function's name, so a `-background` suffix is\n * the runtime proof that the ~60s synchronous wall does NOT apply here.\n *\n * This is the SAFETY GUARD for the soft-timeout regime. The `_process-run`\n * self-dispatch worker (`isBackgroundWorker`) is NOT enough on its own: if the\n * `-background` function was never emitted (deploy gate off, or Netlify routed\n * the path to the synchronous function), the self-POST lands on the regular\n * ~60s `server` function. A worker there MUST use the 40s soft-timeout and\n * checkpoint before the 60s wall — using the ~13min budget would overshoot the\n * hard wall and get killed at 60s, then re-dispatch/resume in a wasteful loop.\n * So the 13-min budget is taken ONLY when this returns true.\n *\n * The PRIMARY signal is a `globalThis` marker the emitted background function's\n * entry sets at cold start — the deployed Lambda name is not guaranteed to end\n * in `-background` on Netlify, so the entry marks its own runtime. A `globalThis`\n * flag (not `process.env`) keeps the no-env-mutation guard satisfied and carries\n * no cross-request state (set once per isolate). The `AWS_LAMBDA_FUNCTION_NAME`\n * suffix and the explicit `AGENT_CHAT_FORCE_BACKGROUND_RUNTIME` env (truthy) are\n * additional signals — the latter an operator escape hatch. Off by default.\n */\nexport function isInBackgroundFunctionRuntime(): boolean {\n // Set by the emitted `-background` function entry at cold start (the primary,\n // most reliable signal — see the emit in deploy/build.ts).\n if (\n (globalThis as Record<string, unknown>)\n .__AGENT_NATIVE_BACKGROUND_RUNTIME__ === true\n ) {\n return true;\n }\n const lambdaName = process.env.AWS_LAMBDA_FUNCTION_NAME;\n if (\n typeof lambdaName === \"string\" &&\n lambdaName.toLowerCase().endsWith(\"-background\")\n ) {\n return true;\n }\n const forced = process.env.AGENT_CHAT_FORCE_BACKGROUND_RUNTIME;\n if (forced != null) {\n const v = forced.trim().toLowerCase();\n return v === \"1\" || v === \"true\" || v === \"yes\" || v === \"on\";\n }\n return false;\n}\n\nfunction isFlagEnabled(): boolean {\n // Read the literal key (not `process.env[CONST]`) so guard:no-env-credentials\n // can statically verify it against the allowlisted `AGENT_*` prefix. Keep this\n // in sync with AGENT_CHAT_DURABLE_BACKGROUND_ENV.\n //\n // DEFAULT-ON: durable background runs are the desired behavior for every\n // hosted app. So an unset/empty/unknown flag means ON; an app opts OUT only\n // with an explicit falsy value. This still composes with the hosted +\n // A2A_SECRET gates below, so non-hosted / unconfigured apps stay synchronous.\n // Safety net: a failed dispatch degrades to a synchronous inline run (see\n // production-agent.ts), so default-on cannot break chat even if the\n // self-dispatch can't be delivered on a given app.\n const raw = process.env.AGENT_CHAT_DURABLE_BACKGROUND;\n if (raw == null) return true;\n const normalized = raw.trim().toLowerCase();\n return !(\n normalized === \"0\" ||\n normalized === \"false\" ||\n normalized === \"no\" ||\n normalized === \"off\"\n );\n}\n\n/**\n * The single gate. True when the flag is not explicitly disabled (default-on)\n * AND the runtime is hosted AND A2A_SECRET is configured. False otherwise — and\n * false means the current synchronous behavior is used, unchanged. So a local /\n * non-hosted / unconfigured app stays synchronous even with the flag defaulting\n * on; durable only engages where the runtime actually supports it.\n */\nexport function isAgentChatDurableBackgroundEnabled(): boolean {\n return (\n isFlagEnabled() &&\n isHostedRuntimeForDurableBackground() &&\n hasConfiguredA2ASecret()\n );\n}\n\n/** Decision returned by `prepareProcessRunRequest`. */\nexport type ProcessRunPreparation =\n | {\n ok: true;\n /** The pre-claimed run id the background worker must reuse. */\n runId: string;\n /** Body to stash for the re-entered handler (marker guaranteed present). */\n body: Record<string, unknown>;\n }\n | {\n ok: false;\n /** HTTP status the route should return. */\n status: number;\n /** Error payload. */\n error: string;\n };\n\n/**\n * Pure, transport-agnostic core of the `_process-run` route: validate the body,\n * authenticate the HMAC self-dispatch, and produce the body the re-entered\n * agent-chat handler should run as the background worker.\n *\n * Auth policy mirrors the agent-teams processor exactly:\n * - `A2A_SECRET` set → require a valid `verifyInternalToken(runId, token)`.\n * - no secret but a production runtime → refuse (503) — never run unsigned in\n * prod.\n * - no secret + non-prod (local dev) → allow unsigned; the SQL atomic claim\n * in the worker still prevents double-processing.\n *\n * Extracted from the route handler so the auth + marker-prep decision is unit\n * testable without booting the whole Nitro plugin. The route only adds body\n * reading and the final handler invocation around this.\n */\nexport function prepareProcessRunRequest(\n body: unknown,\n authHeader: string | undefined,\n): ProcessRunPreparation {\n if (!body || typeof body !== \"object\") {\n return { ok: false, status: 400, error: \"Invalid request body\" };\n }\n const record = body as Record<string, unknown>;\n const marker = record[AGENT_CHAT_BACKGROUND_RUN_FIELD] as\n | { runId?: unknown }\n | undefined;\n const runId =\n marker && typeof marker.runId === \"string\"\n ? marker.runId\n : typeof record.taskId === \"string\"\n ? (record.taskId as string)\n : \"\";\n if (!runId) {\n return { ok: false, status: 400, error: \"runId required\" };\n }\n\n if (hasConfiguredA2ASecret()) {\n const token = extractBearerToken(authHeader);\n if (!verifyInternalToken(runId, token ?? \"\")) {\n return {\n ok: false,\n status: 401,\n error: \"Invalid or expired processor token\",\n };\n }\n } else if (isA2AProductionRuntime()) {\n return {\n ok: false,\n status: 503,\n error:\n \"Agent chat background processor not configured — set A2A_SECRET on this deployment.\",\n };\n }\n\n // Ensure the marker is present so the re-entered handler runs as the\n // background worker (reuses runId/turnId, no re-claim, no re-dispatch).\n if (!marker || typeof marker.runId !== \"string\") {\n record[AGENT_CHAT_BACKGROUND_RUN_FIELD] = { runId };\n }\n return { ok: true, runId, body: record };\n}\n"]}
|