@agent-native/core 0.75.4 → 0.75.5
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 +40 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/durable-background.ts +62 -28
- package/corpus/core/src/agent/production-agent.ts +16 -12
- package/corpus/core/src/cli/skills.ts +22 -4
- package/corpus/core/src/deploy/build.ts +47 -113
- package/corpus/core/src/deploy/workspace-deploy.ts +62 -30
- package/corpus/core/src/server/self-dispatch.ts +29 -2
- package/corpus/templates/clips/changelog/2026-06-24-chrome-extension-setup-now-shows-a-clear-all-done-confirmati.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/permission.html +60 -0
- package/corpus/templates/clips/chrome-extension/src/permission.ts +28 -15
- package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +22 -4
- package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +22 -4
- package/dist/agent/durable-background.d.ts +32 -28
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +59 -28
- 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 +16 -12
- package/dist/agent/production-agent.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 +22 -4
- package/dist/cli/skills.js.map +1 -1
- package/dist/deploy/build.d.ts +32 -36
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +47 -104
- package/dist/deploy/build.js.map +1 -1
- package/dist/deploy/workspace-deploy.js +62 -30
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/server/self-dispatch.d.ts +0 -10
- package/dist/server/self-dispatch.d.ts.map +1 -1
- package/dist/server/self-dispatch.js +27 -2
- package/dist/server/self-dispatch.js.map +1 -1
- package/package.json +1 -1
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
* A2A, integration webhooks, and Agent Teams sub-agents share one tested
|
|
21
21
|
* implementation.
|
|
22
22
|
*/
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
getConfiguredAppBasePath,
|
|
25
|
+
withConfiguredAppBasePath,
|
|
26
|
+
} from "./app-base-path.js";
|
|
24
27
|
import { isLocalDatabase } from "../db/client.js";
|
|
25
28
|
import { signInternalToken } from "../integrations/internal-token.js";
|
|
26
29
|
|
|
@@ -119,11 +122,35 @@ async function dispatchResponseError(
|
|
|
119
122
|
* processor accepts unsigned dispatches in dev and relies on the SQL atomic
|
|
120
123
|
* claim for double-processing protection, mirroring the A2A/webhook flow.
|
|
121
124
|
*/
|
|
125
|
+
/**
|
|
126
|
+
* For host-root dispatch targets (`/.netlify/functions/*`), strip the configured
|
|
127
|
+
* app base path suffix from the resolved base url so the request reaches the
|
|
128
|
+
* function at the host root rather than under the workspace app base path. For
|
|
129
|
+
* every other (framework-route) path the base-path-prefixed base url is returned
|
|
130
|
+
* unchanged, preserving the existing self-dispatch behavior.
|
|
131
|
+
*/
|
|
132
|
+
function rootBaseUrlForPath(baseUrl: string, path: string): string {
|
|
133
|
+
if (!path.startsWith("/.netlify/")) return baseUrl;
|
|
134
|
+
const basePath = getConfiguredAppBasePath();
|
|
135
|
+
if (!basePath) return baseUrl;
|
|
136
|
+
const trimmed = baseUrl.replace(/\/$/, "");
|
|
137
|
+
if (trimmed.endsWith(basePath)) {
|
|
138
|
+
return trimmed.slice(0, trimmed.length - basePath.length);
|
|
139
|
+
}
|
|
140
|
+
return trimmed;
|
|
141
|
+
}
|
|
142
|
+
|
|
122
143
|
export async function fireInternalDispatch(
|
|
123
144
|
options: FireInternalDispatchOptions,
|
|
124
145
|
): Promise<void> {
|
|
125
146
|
const baseUrl = options.baseUrl ?? resolveSelfDispatchBaseUrl(options.event);
|
|
126
|
-
|
|
147
|
+
// Netlify function default urls (`/.netlify/functions/<name>`) live at the
|
|
148
|
+
// HOST ROOT, not under the workspace app base path. `resolveSelfDispatchBaseUrl`
|
|
149
|
+
// appends the configured base path (e.g. `https://host/starter`) so framework
|
|
150
|
+
// routes land on the right app; for a host-root function url we must dispatch
|
|
151
|
+
// to `https://host/.netlify/functions/<name>` instead. Strip the base path
|
|
152
|
+
// suffix from the resolved base url for `/.netlify/*` dispatch targets only.
|
|
153
|
+
const url = `${rootBaseUrlForPath(baseUrl, options.path)}${options.path}`;
|
|
127
154
|
const headers: Record<string, string> = {
|
|
128
155
|
"Content-Type": "application/json",
|
|
129
156
|
};
|
|
@@ -52,6 +52,9 @@
|
|
|
52
52
|
* {
|
|
53
53
|
box-sizing: border-box;
|
|
54
54
|
}
|
|
55
|
+
[hidden] {
|
|
56
|
+
display: none !important;
|
|
57
|
+
}
|
|
55
58
|
body {
|
|
56
59
|
margin: 0;
|
|
57
60
|
min-height: 100vh;
|
|
@@ -143,6 +146,45 @@
|
|
|
143
146
|
color: var(--fg-subtle);
|
|
144
147
|
font-size: 12px;
|
|
145
148
|
}
|
|
149
|
+
.success-state {
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
align-items: center;
|
|
153
|
+
gap: 10px;
|
|
154
|
+
margin-top: -4px;
|
|
155
|
+
padding: 18px 16px;
|
|
156
|
+
border: 1px solid var(--status-ok);
|
|
157
|
+
border-radius: var(--radius-sm);
|
|
158
|
+
background: rgba(34, 197, 94, 0.1);
|
|
159
|
+
text-align: center;
|
|
160
|
+
}
|
|
161
|
+
.success-icon {
|
|
162
|
+
display: inline-flex;
|
|
163
|
+
width: 58px;
|
|
164
|
+
height: 58px;
|
|
165
|
+
align-items: center;
|
|
166
|
+
justify-content: center;
|
|
167
|
+
border: 2px solid var(--status-ok);
|
|
168
|
+
border-radius: 50%;
|
|
169
|
+
background: var(--bg);
|
|
170
|
+
color: var(--status-ok);
|
|
171
|
+
}
|
|
172
|
+
.success-icon svg {
|
|
173
|
+
width: 32px;
|
|
174
|
+
height: 32px;
|
|
175
|
+
}
|
|
176
|
+
.success-title {
|
|
177
|
+
margin: 0;
|
|
178
|
+
color: var(--fg);
|
|
179
|
+
font-size: 18px;
|
|
180
|
+
font-weight: 700;
|
|
181
|
+
line-height: 1.2;
|
|
182
|
+
}
|
|
183
|
+
.success-copy {
|
|
184
|
+
margin: 0;
|
|
185
|
+
color: var(--fg-muted);
|
|
186
|
+
font-size: 13.5px;
|
|
187
|
+
}
|
|
146
188
|
button {
|
|
147
189
|
font: inherit;
|
|
148
190
|
appearance: none;
|
|
@@ -219,6 +261,24 @@
|
|
|
219
261
|
>
|
|
220
262
|
</div>
|
|
221
263
|
</div>
|
|
264
|
+
<div class="success-state" id="success-state" role="status" hidden>
|
|
265
|
+
<div class="success-icon" aria-hidden="true">
|
|
266
|
+
<svg
|
|
267
|
+
viewBox="0 0 24 24"
|
|
268
|
+
fill="none"
|
|
269
|
+
stroke="currentColor"
|
|
270
|
+
stroke-width="2.4"
|
|
271
|
+
stroke-linecap="round"
|
|
272
|
+
stroke-linejoin="round"
|
|
273
|
+
>
|
|
274
|
+
<path d="M20 6 9 17l-5-5" />
|
|
275
|
+
</svg>
|
|
276
|
+
</div>
|
|
277
|
+
<div class="success-title" id="success-title">You're all done</div>
|
|
278
|
+
<p class="success-copy" id="success-copy">
|
|
279
|
+
You can close this tab and start recording from the Clips icon.
|
|
280
|
+
</p>
|
|
281
|
+
</div>
|
|
222
282
|
<button id="enable">Enable camera & microphone</button>
|
|
223
283
|
<div class="status" id="status"></div>
|
|
224
284
|
</div>
|
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
const enableBtn = document.getElementById("enable") as HTMLButtonElement;
|
|
9
9
|
const statusEl = document.getElementById("status") as HTMLDivElement;
|
|
10
|
+
const successEl = document.getElementById("success-state") as HTMLDivElement;
|
|
11
|
+
const successTitle = document.getElementById("success-title") as HTMLDivElement;
|
|
12
|
+
const successCopy = document.getElementById(
|
|
13
|
+
"success-copy",
|
|
14
|
+
) as HTMLParagraphElement;
|
|
10
15
|
const rowMic = document.getElementById("row-mic") as HTMLDivElement;
|
|
11
16
|
const rowCam = document.getElementById("row-cam") as HTMLDivElement;
|
|
12
17
|
const checkMic = document.getElementById("check-mic") as HTMLSpanElement;
|
|
@@ -20,6 +25,25 @@ function setStatus(text: string, isError = false): void {
|
|
|
20
25
|
statusEl.classList.toggle("error", isError);
|
|
21
26
|
}
|
|
22
27
|
|
|
28
|
+
function showEnableButton(text: string, disabled: boolean): void {
|
|
29
|
+
enableBtn.hidden = false;
|
|
30
|
+
enableBtn.textContent = text;
|
|
31
|
+
enableBtn.disabled = disabled;
|
|
32
|
+
successEl.hidden = true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function showSuccess(title: string): void {
|
|
36
|
+
enableBtn.hidden = true;
|
|
37
|
+
enableBtn.disabled = true;
|
|
38
|
+
successTitle.textContent = title;
|
|
39
|
+
successCopy.textContent =
|
|
40
|
+
"You can close this tab and start recording from the Clips icon.";
|
|
41
|
+
successEl.hidden = false;
|
|
42
|
+
setStatus(
|
|
43
|
+
"Chrome still asks you what to share before each recording starts.",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
23
47
|
function markRow(kind: "mic" | "cam", granted: boolean): void {
|
|
24
48
|
const row = kind === "mic" ? rowMic : rowCam;
|
|
25
49
|
const check = kind === "mic" ? checkMic : checkCam;
|
|
@@ -59,16 +83,9 @@ function finish(camOk: boolean, micOk: boolean): void {
|
|
|
59
83
|
markRow("mic", micOk);
|
|
60
84
|
markRow("cam", camOk);
|
|
61
85
|
if (camOk || micOk) {
|
|
62
|
-
|
|
63
|
-
enableBtn.disabled = true;
|
|
64
|
-
setStatus(
|
|
65
|
-
camOk && micOk
|
|
66
|
-
? "Camera and microphone are ready. Click the Clips icon to record."
|
|
67
|
-
: "Saved. Click the Clips icon to record.",
|
|
68
|
-
);
|
|
86
|
+
showSuccess(camOk && micOk ? "You're all done" : "Saved");
|
|
69
87
|
} else {
|
|
70
|
-
|
|
71
|
-
enableBtn.textContent = "Try again";
|
|
88
|
+
showEnableButton("Try again", false);
|
|
72
89
|
setStatus(
|
|
73
90
|
"Access was blocked. Click the camera icon in Chrome's address bar to allow it, then try again.",
|
|
74
91
|
true,
|
|
@@ -77,7 +94,7 @@ function finish(camOk: boolean, micOk: boolean): void {
|
|
|
77
94
|
}
|
|
78
95
|
|
|
79
96
|
async function enable(): Promise<void> {
|
|
80
|
-
enableBtn.
|
|
97
|
+
showEnableButton(enableBtn.textContent ?? "Enable camera & microphone", true);
|
|
81
98
|
setStatus("Waiting for Chrome's permission prompt…");
|
|
82
99
|
// Request separately so a camera denial doesn't also block the microphone.
|
|
83
100
|
const micOk = await requestOne("mic");
|
|
@@ -96,10 +113,6 @@ void (async () => {
|
|
|
96
113
|
markRow("mic", mic === "granted");
|
|
97
114
|
markRow("cam", cam === "granted");
|
|
98
115
|
if (cam === "granted" && mic === "granted") {
|
|
99
|
-
|
|
100
|
-
enableBtn.disabled = true;
|
|
101
|
-
setStatus(
|
|
102
|
-
"Camera and microphone are ready. Click the Clips icon to record.",
|
|
103
|
-
);
|
|
116
|
+
showSuccess("You're all done");
|
|
104
117
|
}
|
|
105
118
|
})();
|
|
@@ -64,6 +64,13 @@ themes. For any inline border, background, or text color, reference a token:
|
|
|
64
64
|
and `--wf-radius`. Never hard-code a hex color and never set `font-family` — the
|
|
65
65
|
renderer owns the sketch/clean font.
|
|
66
66
|
|
|
67
|
+
**Use literal CSS lengths for spacing.** The `--wf-*` tokens are for colors and
|
|
68
|
+
renderer-owned visual styling, not layout spacing. Do not use guessed spacing
|
|
69
|
+
tokens such as `var(--wf-space-4)`, Tailwind spacing classes, or theme spacing
|
|
70
|
+
variables inside wireframe HTML; if a token is unavailable in the Plan renderer,
|
|
71
|
+
padding collapses and content hugs the border. Use explicit CSS lengths for
|
|
72
|
+
layout: `padding:16px`, `gap:12px`, `margin-top:18px`, `minmax(0,1fr)`.
|
|
73
|
+
|
|
67
74
|
**Lay out with inline `style` flex/grid.** You write the real layout —
|
|
68
75
|
`display:flex; flex-direction:column; gap:10px; padding:16px` and so on — and the
|
|
69
76
|
renderer never repositions anything. Compose the actual product: reproduce the
|
|
@@ -148,10 +155,21 @@ check/serve or verify command for `<plan-dir>`.
|
|
|
148
155
|
**Treat the wireframe border as part of the visible design.** Always wrap HTML
|
|
149
156
|
wireframe content in a root container with real inner padding before drawing
|
|
150
157
|
cards, fields, pills, labels, or controls. Use at least 14-16px of padding,
|
|
151
|
-
`box-sizing: border-box`, `height: 100%`, and `gap` between child rows
|
|
152
|
-
first row never sits flush against the screen border.
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
`box-sizing: border-box`, `height: 100%`, and `gap` between child rows on the
|
|
159
|
+
root node itself so the first row never sits flush against the screen border. Do
|
|
160
|
+
not rely on padding on a nested page section as the first visible inset; the
|
|
161
|
+
outermost element must create the breathing room. Keep text away from borders:
|
|
162
|
+
every container, field, button, menu item, and annotation needs enough padding
|
|
163
|
+
and line-height to read cleanly in the rendered Plan view.
|
|
164
|
+
|
|
165
|
+
**For feature-cloud or abundance visuals, optimize the composition over line-by-line
|
|
166
|
+
reading.** Some marketing/product sections need to feel like a large surface area
|
|
167
|
+
of capability rather than a precise app workflow. In those cases, use one padded
|
|
168
|
+
root with a short headline and a dense, aesthetic cloud of short feature labels,
|
|
169
|
+
chips, rings, or columns. Vary scale and opacity with tokens, cluster by meaning,
|
|
170
|
+
and let many labels be glanceable rather than individually essential. Do not
|
|
171
|
+
force dozens of features into equal cards with long wrapped sentences; that
|
|
172
|
+
usually creates a messy unreadable mockup.
|
|
155
173
|
|
|
156
174
|
**Lay out children safely so they never collide.** Use HTML flex/grid with
|
|
157
175
|
`gap`, `min-width: 0`, and sensible overflow. Avoid negative margins, absolute
|
|
@@ -64,6 +64,13 @@ themes. For any inline border, background, or text color, reference a token:
|
|
|
64
64
|
and `--wf-radius`. Never hard-code a hex color and never set `font-family` — the
|
|
65
65
|
renderer owns the sketch/clean font.
|
|
66
66
|
|
|
67
|
+
**Use literal CSS lengths for spacing.** The `--wf-*` tokens are for colors and
|
|
68
|
+
renderer-owned visual styling, not layout spacing. Do not use guessed spacing
|
|
69
|
+
tokens such as `var(--wf-space-4)`, Tailwind spacing classes, or theme spacing
|
|
70
|
+
variables inside wireframe HTML; if a token is unavailable in the Plan renderer,
|
|
71
|
+
padding collapses and content hugs the border. Use explicit CSS lengths for
|
|
72
|
+
layout: `padding:16px`, `gap:12px`, `margin-top:18px`, `minmax(0,1fr)`.
|
|
73
|
+
|
|
67
74
|
**Lay out with inline `style` flex/grid.** You write the real layout —
|
|
68
75
|
`display:flex; flex-direction:column; gap:10px; padding:16px` and so on — and the
|
|
69
76
|
renderer never repositions anything. Compose the actual product: reproduce the
|
|
@@ -148,10 +155,21 @@ check/serve or verify command for `<plan-dir>`.
|
|
|
148
155
|
**Treat the wireframe border as part of the visible design.** Always wrap HTML
|
|
149
156
|
wireframe content in a root container with real inner padding before drawing
|
|
150
157
|
cards, fields, pills, labels, or controls. Use at least 14-16px of padding,
|
|
151
|
-
`box-sizing: border-box`, `height: 100%`, and `gap` between child rows
|
|
152
|
-
first row never sits flush against the screen border.
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
`box-sizing: border-box`, `height: 100%`, and `gap` between child rows on the
|
|
159
|
+
root node itself so the first row never sits flush against the screen border. Do
|
|
160
|
+
not rely on padding on a nested page section as the first visible inset; the
|
|
161
|
+
outermost element must create the breathing room. Keep text away from borders:
|
|
162
|
+
every container, field, button, menu item, and annotation needs enough padding
|
|
163
|
+
and line-height to read cleanly in the rendered Plan view.
|
|
164
|
+
|
|
165
|
+
**For feature-cloud or abundance visuals, optimize the composition over line-by-line
|
|
166
|
+
reading.** Some marketing/product sections need to feel like a large surface area
|
|
167
|
+
of capability rather than a precise app workflow. In those cases, use one padded
|
|
168
|
+
root with a short headline and a dense, aesthetic cloud of short feature labels,
|
|
169
|
+
chips, rings, or columns. Vary scale and opacity with tokens, cluster by meaning,
|
|
170
|
+
and let many labels be glanceable rather than individually essential. Do not
|
|
171
|
+
force dozens of features into equal cards with long wrapped sentences; that
|
|
172
|
+
usually creates a messy unreadable mockup.
|
|
155
173
|
|
|
156
174
|
**Lay out children safely so they never collide.** Use HTML flex/grid with
|
|
157
175
|
`gap`, `min-width: 0`, and sensible overflow. Avoid negative margins, absolute
|
|
@@ -15,42 +15,46 @@ export declare const AGENT_CHAT_PROCESS_RUN_PATH = "/_agent-native/agent-chat/_p
|
|
|
15
15
|
*/
|
|
16
16
|
export declare const AGENT_BACKGROUND_FUNCTION_NAME = "server-agent-background";
|
|
17
17
|
/**
|
|
18
|
-
* Default function URL of the background function on Netlify
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* the name/url shape stays asserted and discoverable.
|
|
18
|
+
* Default function URL of the background function on Netlify. Every Netlify
|
|
19
|
+
* function is reachable at `/.netlify/functions/<name>` BY DEFAULT; that default
|
|
20
|
+
* url is removed ONLY if the function declares a custom `config.path`. The
|
|
21
|
+
* emitted background function declares NO custom `config.path` (it sets
|
|
22
|
+
* `background: true` and nothing else routing-related), so it KEEPS this default
|
|
23
|
+
* url — and the Nitro `server` function already excludes `/.netlify/*` from its
|
|
24
|
+
* `/*` catch-all, so this namespace is never shadowed. The foreground therefore
|
|
25
|
+
* dispatches HERE on hosted Netlify (see `resolveAgentChatProcessRunDispatchPath`).
|
|
27
26
|
*/
|
|
28
27
|
export declare const AGENT_BACKGROUND_FUNCTION_URL_PATH = "/.netlify/functions/server-agent-background";
|
|
29
28
|
/**
|
|
30
29
|
* Resolve the path the foreground POST should self-dispatch the chat background
|
|
31
30
|
* worker to.
|
|
32
31
|
*
|
|
33
|
-
* GROUNDED IN THE REAL NETLIFY BUILD OUTPUT
|
|
34
|
-
* INTO the scanned dir
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
32
|
+
* GROUNDED IN THE REAL NETLIFY BUILD OUTPUT + THE NETLIFY DOCS DEFAULT-URL RULE:
|
|
33
|
+
* the background function is emitted INTO the scanned dir
|
|
34
|
+
* (`.netlify/functions-internal/server-agent-background`, or per-app
|
|
35
|
+
* `<app>-agent-background` for workspaces) with `export const config = {
|
|
36
|
+
* background: true, ... }` and NO custom `config.path`. Because it has no custom
|
|
37
|
+
* path, Netlify keeps its DEFAULT function url `/.netlify/functions/<name>`, and
|
|
38
|
+
* `background: true` makes any invocation of that url ASYNC (immediate 202,
|
|
39
|
+
* 15-min budget). The Nitro `server` function already excludes `/.netlify/*`
|
|
40
|
+
* from its `/*` catch-all, so the default-url namespace is NEVER shadowed by the
|
|
41
|
+
* synchronous function.
|
|
41
42
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
43
|
+
* Therefore on hosted Netlify the foreground dispatches to the function's DEFAULT
|
|
44
|
+
* url (`/.netlify/functions/<name>`); the function entry then rewrites the
|
|
45
|
+
* incoming pathname to `AGENT_CHAT_PROCESS_RUN_PATH` (base-path-prefixed for
|
|
46
|
+
* workspaces) before delegating to the Nitro router, so the `_process-run`
|
|
47
|
+
* plugin runs with the async 15-min budget. Everywhere else (local dev, `netlify
|
|
48
|
+
* dev`, non-Netlify hosts where no second function exists) there is no second
|
|
49
|
+
* function, so the foreground dispatches to the framework route
|
|
50
|
+
* `AGENT_CHAT_PROCESS_RUN_PATH` and the same in-process catch-all handles it
|
|
51
|
+
* inline. The HMAC token (signed over the runId) is unchanged either way.
|
|
48
52
|
*
|
|
49
|
-
* NOTE: this is
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
53
|
+
* NOTE: this is the DOC-CORRECT approach. An earlier attempt gave the function a
|
|
54
|
+
* custom `config.path` + a catch-all `excludedPath` patch; the custom path was
|
|
55
|
+
* NOT honored as a route in prod (probe → 404). Using the default function url
|
|
56
|
+
* (no custom path) is what Netlify documents and is simpler — there is nothing
|
|
57
|
+
* to shadow because `/.netlify/*` is already excluded from the `server` catch-all.
|
|
54
58
|
*/
|
|
55
59
|
export declare function resolveAgentChatProcessRunDispatchPath(): string;
|
|
56
60
|
/**
|
|
@@ -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;;;;;;;;GAQG;AACH,eAAO,MAAM,8BAA8B,4BAA4B,CAAC;AAExE
|
|
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;;;;;;;;GAQG;AACH,eAAO,MAAM,8BAA8B,4BAA4B,CAAC;AAExE;;;;;;;;;GASG;AACH,eAAO,MAAM,kCAAkC,gDAA0D,CAAC;AAsB1G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,sCAAsC,IAAI,MAAM,CAY/D;AAED;;;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"}
|
|
@@ -53,44 +53,75 @@ export const AGENT_CHAT_PROCESS_RUN_PATH = "/_agent-native/agent-chat/_process-r
|
|
|
53
53
|
*/
|
|
54
54
|
export const AGENT_BACKGROUND_FUNCTION_NAME = "server-agent-background";
|
|
55
55
|
/**
|
|
56
|
-
* Default function URL of the background function on Netlify
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* the name/url shape stays asserted and discoverable.
|
|
56
|
+
* Default function URL of the background function on Netlify. Every Netlify
|
|
57
|
+
* function is reachable at `/.netlify/functions/<name>` BY DEFAULT; that default
|
|
58
|
+
* url is removed ONLY if the function declares a custom `config.path`. The
|
|
59
|
+
* emitted background function declares NO custom `config.path` (it sets
|
|
60
|
+
* `background: true` and nothing else routing-related), so it KEEPS this default
|
|
61
|
+
* url — and the Nitro `server` function already excludes `/.netlify/*` from its
|
|
62
|
+
* `/*` catch-all, so this namespace is never shadowed. The foreground therefore
|
|
63
|
+
* dispatches HERE on hosted Netlify (see `resolveAgentChatProcessRunDispatchPath`).
|
|
65
64
|
*/
|
|
66
65
|
export const AGENT_BACKGROUND_FUNCTION_URL_PATH = `/.netlify/functions/${AGENT_BACKGROUND_FUNCTION_NAME}`;
|
|
66
|
+
/**
|
|
67
|
+
* The per-app workspace background function URL path. Workspace deploy emits one
|
|
68
|
+
* background function per app named `<app>-agent-background`, reachable at its
|
|
69
|
+
* DEFAULT url `/.netlify/functions/<app>-agent-background` (no custom
|
|
70
|
+
* `config.path`). The foreground resolves the current workspace app id from
|
|
71
|
+
* `AGENT_NATIVE_WORKSPACE_APP_ID` (set by the workspace function entry) so it can
|
|
72
|
+
* dispatch to the right per-app function url. Returns `null` when no workspace
|
|
73
|
+
* app id is configured (single-template deploy).
|
|
74
|
+
*/
|
|
75
|
+
function resolveWorkspaceBackgroundFunctionUrlPath() {
|
|
76
|
+
const raw = process.env.AGENT_NATIVE_WORKSPACE_APP_ID;
|
|
77
|
+
if (typeof raw !== "string")
|
|
78
|
+
return null;
|
|
79
|
+
// Mirror the workspace app-id normalization (resources/store.ts): take the
|
|
80
|
+
// first path segment and accept only the safe slug shape used for function
|
|
81
|
+
// names. Anything else falls back to the single-template name.
|
|
82
|
+
const candidate = raw.trim().replace(/^\/+/, "").split("/")[0] ?? "";
|
|
83
|
+
if (!/^[a-z0-9][a-z0-9-]{0,127}$/.test(candidate))
|
|
84
|
+
return null;
|
|
85
|
+
return `/.netlify/functions/${candidate}-agent-background`;
|
|
86
|
+
}
|
|
67
87
|
/**
|
|
68
88
|
* Resolve the path the foreground POST should self-dispatch the chat background
|
|
69
89
|
* worker to.
|
|
70
90
|
*
|
|
71
|
-
* GROUNDED IN THE REAL NETLIFY BUILD OUTPUT
|
|
72
|
-
* INTO the scanned dir
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
91
|
+
* GROUNDED IN THE REAL NETLIFY BUILD OUTPUT + THE NETLIFY DOCS DEFAULT-URL RULE:
|
|
92
|
+
* the background function is emitted INTO the scanned dir
|
|
93
|
+
* (`.netlify/functions-internal/server-agent-background`, or per-app
|
|
94
|
+
* `<app>-agent-background` for workspaces) with `export const config = {
|
|
95
|
+
* background: true, ... }` and NO custom `config.path`. Because it has no custom
|
|
96
|
+
* path, Netlify keeps its DEFAULT function url `/.netlify/functions/<name>`, and
|
|
97
|
+
* `background: true` makes any invocation of that url ASYNC (immediate 202,
|
|
98
|
+
* 15-min budget). The Nitro `server` function already excludes `/.netlify/*`
|
|
99
|
+
* from its `/*` catch-all, so the default-url namespace is NEVER shadowed by the
|
|
100
|
+
* synchronous function.
|
|
79
101
|
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
102
|
+
* Therefore on hosted Netlify the foreground dispatches to the function's DEFAULT
|
|
103
|
+
* url (`/.netlify/functions/<name>`); the function entry then rewrites the
|
|
104
|
+
* incoming pathname to `AGENT_CHAT_PROCESS_RUN_PATH` (base-path-prefixed for
|
|
105
|
+
* workspaces) before delegating to the Nitro router, so the `_process-run`
|
|
106
|
+
* plugin runs with the async 15-min budget. Everywhere else (local dev, `netlify
|
|
107
|
+
* dev`, non-Netlify hosts where no second function exists) there is no second
|
|
108
|
+
* function, so the foreground dispatches to the framework route
|
|
109
|
+
* `AGENT_CHAT_PROCESS_RUN_PATH` and the same in-process catch-all handles it
|
|
110
|
+
* inline. The HMAC token (signed over the runId) is unchanged either way.
|
|
86
111
|
*
|
|
87
|
-
* NOTE: this is
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
112
|
+
* NOTE: this is the DOC-CORRECT approach. An earlier attempt gave the function a
|
|
113
|
+
* custom `config.path` + a catch-all `excludedPath` patch; the custom path was
|
|
114
|
+
* NOT honored as a route in prod (probe → 404). Using the default function url
|
|
115
|
+
* (no custom path) is what Netlify documents and is simpler — there is nothing
|
|
116
|
+
* to shadow because `/.netlify/*` is already excluded from the `server` catch-all.
|
|
92
117
|
*/
|
|
93
118
|
export function resolveAgentChatProcessRunDispatchPath() {
|
|
119
|
+
if (process.env.NETLIFY &&
|
|
120
|
+
process.env.NETLIFY !== "false" &&
|
|
121
|
+
process.env.NETLIFY_LOCAL !== "true") {
|
|
122
|
+
return (resolveWorkspaceBackgroundFunctionUrlPath() ??
|
|
123
|
+
AGENT_BACKGROUND_FUNCTION_URL_PATH);
|
|
124
|
+
}
|
|
94
125
|
return AGENT_CHAT_PROCESS_RUN_PATH;
|
|
95
126
|
}
|
|
96
127
|
/**
|
|
@@ -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 * Name of the standalone Netlify background function the build emits (see\n * `emitSingleTemplateNetlifyBackgroundFunction` in deploy/build.ts). Shared so\n * the emit and the dispatch-path helper below can never drift on the name.\n *\n * MUST end in `-background` — both because that is the conventional Netlify\n * async-function suffix and because `isInBackgroundFunctionRuntime()` reads the\n * `AWS_LAMBDA_FUNCTION_NAME` `-background` suffix as a secondary runtime signal.\n */\nexport const AGENT_BACKGROUND_FUNCTION_NAME = \"server-agent-background\";\n\n/**\n * Default function URL of the background function on Netlify, kept for\n * diagnostics/tests. Every Netlify function is ALSO reachable at\n * `/.netlify/functions/<name>` unless a custom `config.path` removes the default\n * url. The emitted background function declares `config.path =\n * AGENT_CHAT_PROCESS_RUN_PATH`, which means Netlify routes the process-run path\n * to it directly AND (per Netlify docs) removes this default url — so the\n * foreground does NOT dispatch here; it dispatches to the framework route (see\n * `resolveAgentChatProcessRunDispatchPath`). This constant is retained only so\n * the name/url shape stays asserted and discoverable.\n */\nexport const AGENT_BACKGROUND_FUNCTION_URL_PATH = `/.netlify/functions/${AGENT_BACKGROUND_FUNCTION_NAME}`;\n\n/**\n * Resolve the path the foreground POST should self-dispatch the chat background\n * worker to.\n *\n * GROUNDED IN THE REAL NETLIFY BUILD OUTPUT: the background function is emitted\n * INTO the scanned dir (`.netlify/functions-internal/server-agent-background`)\n * with `export const config = { background: true, path:\n * AGENT_CHAT_PROCESS_RUN_PATH }`. Netlify evaluates serverless functions BEFORE\n * redirects (request-chain step 10 vs 11), and the build excludes this exact path\n * from the `server` `/*` catch-all — so a POST to `AGENT_CHAT_PROCESS_RUN_PATH`\n * matches ONLY the async background function (immediate 202, 15-min budget),\n * never the synchronous `server` catch-all.\n *\n * So the dispatch path is the SAME framework route on every host. On hosted\n * Netlify it lands on the async function (because of the exclude + the\n * background function's `config.path`); everywhere else (local dev, `netlify\n * dev`, non-Netlify hosts where no second function exists) the same in-process\n * catch-all handles it inline. The HMAC token (signed over the runId) is\n * unchanged.\n *\n * NOTE: this is a deliberate change from the earlier \"dispatch to the direct\n * `/.netlify/functions/<name>` url\" attempt, which only worked if the function\n * was reachable at its default url. With a custom `config.path` the default url\n * is removed, and there is no shadowing to bypass anyway, so dispatching to the\n * framework route is both correct and simpler.\n */\nexport function resolveAgentChatProcessRunDispatchPath(): string {\n return AGENT_CHAT_PROCESS_RUN_PATH;\n}\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"]}
|
|
1
|
+
{"version":3,"file":"durable-background.js","sourceRoot":"","sources":["../../src/agent/durable-background.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,OAAO,EACL,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,mCAAmC,CAAC;AAE3C;;;;GAIG;AACH,MAAM,CAAC,MAAM,2BAA2B,GACtC,wCAAwC,CAAC;AAE3C;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,yBAAyB,CAAC;AAExE;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,kCAAkC,GAAG,uBAAuB,8BAA8B,EAAE,CAAC;AAE1G;;;;;;;;GAQG;AACH,SAAS,yCAAyC;IAChD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,6BAA6B,CAAC;IACtD,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACzC,2EAA2E;IAC3E,2EAA2E;IAC3E,+DAA+D;IAC/D,MAAM,SAAS,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACrE,IAAI,CAAC,4BAA4B,CAAC,IAAI,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/D,OAAO,uBAAuB,SAAS,mBAAmB,CAAC;AAC7D,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,UAAU,sCAAsC;IACpD,IACE,OAAO,CAAC,GAAG,CAAC,OAAO;QACnB,OAAO,CAAC,GAAG,CAAC,OAAO,KAAK,OAAO;QAC/B,OAAO,CAAC,GAAG,CAAC,aAAa,KAAK,MAAM,EACpC,CAAC;QACD,OAAO,CACL,yCAAyC,EAAE;YAC3C,kCAAkC,CACnC,CAAC;IACJ,CAAC;IACD,OAAO,2BAA2B,CAAC;AACrC,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,iCAAiC,GAC5C,+BAA+B,CAAC;AAElC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG,iBAAiB,CAAC;AAEjE;;;;GAIG;AACH,MAAM,UAAU,mCAAmC;IACjD,IACE,OAAO,CAAC,GAAG,CAAC,OAAO;QACnB,OAAO,CAAC,GAAG,CAAC,OAAO,KAAK,OAAO;QAC/B,OAAO,CAAC,GAAG,CAAC,aAAa,KAAK,MAAM,EACpC,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IACE,OAAO,CAAC,GAAG,CAAC,wBAAwB;QACpC,OAAO,CAAC,GAAG,CAAC,aAAa,KAAK,MAAM,EACpC,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,OAAO,CACZ,OAAO,CAAC,GAAG,CAAC,QAAQ;QACpB,OAAO,CAAC,GAAG,CAAC,MAAM;QAClB,OAAO,CAAC,GAAG,CAAC,UAAU;QACtB,OAAO,CAAC,GAAG,CAAC,MAAM;QAClB,OAAO,CAAC,GAAG,CAAC,YAAY;QACxB,OAAO,CAAC,GAAG,CAAC,SAAS,CACtB,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,6BAA6B;IAC3C,8EAA8E;IAC9E,2DAA2D;IAC3D,IACG,UAAsC;SACpC,mCAAmC,KAAK,IAAI,EAC/C,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC;IACxD,IACE,OAAO,UAAU,KAAK,QAAQ;QAC9B,UAAU,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,aAAa,CAAC,EAChD,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC;IAC/D,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;QACnB,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACtC,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,IAAI,CAAC;IAChE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,aAAa;IACpB,8EAA8E;IAC9E,+EAA+E;IAC/E,kDAAkD;IAClD,EAAE;IACF,yEAAyE;IACzE,4EAA4E;IAC5E,sEAAsE;IACtE,8EAA8E;IAC9E,0EAA0E;IAC1E,oEAAoE;IACpE,mDAAmD;IACnD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,6BAA6B,CAAC;IACtD,IAAI,GAAG,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IAC7B,MAAM,UAAU,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC5C,OAAO,CAAC,CACN,UAAU,KAAK,GAAG;QAClB,UAAU,KAAK,OAAO;QACtB,UAAU,KAAK,IAAI;QACnB,UAAU,KAAK,KAAK,CACrB,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mCAAmC;IACjD,OAAO,CACL,aAAa,EAAE;QACf,mCAAmC,EAAE;QACrC,sBAAsB,EAAE,CACzB,CAAC;AACJ,CAAC;AAmBD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,wBAAwB,CACtC,IAAa,EACb,UAA8B;IAE9B,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAC;IACnE,CAAC;IACD,MAAM,MAAM,GAAG,IAA+B,CAAC;IAC/C,MAAM,MAAM,GAAG,MAAM,CAAC,+BAA+B,CAExC,CAAC;IACd,MAAM,KAAK,GACT,MAAM,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ;QACxC,CAAC,CAAC,MAAM,CAAC,KAAK;QACd,CAAC,CAAC,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ;YACjC,CAAC,CAAE,MAAM,CAAC,MAAiB;YAC3B,CAAC,CAAC,EAAE,CAAC;IACX,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC7D,CAAC;IAED,IAAI,sBAAsB,EAAE,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,kBAAkB,CAAC,UAAU,CAAC,CAAC;QAC7C,IAAI,CAAC,mBAAmB,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE,CAAC,EAAE,CAAC;YAC7C,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,MAAM,EAAE,GAAG;gBACX,KAAK,EAAE,oCAAoC;aAC5C,CAAC;QACJ,CAAC;IACH,CAAC;SAAM,IAAI,sBAAsB,EAAE,EAAE,CAAC;QACpC,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG;YACX,KAAK,EACH,qFAAqF;SACxF,CAAC;IACJ,CAAC;IAED,qEAAqE;IACrE,wEAAwE;IACxE,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,MAAM,CAAC,+BAA+B,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC;IACtD,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC3C,CAAC","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 * Name of the standalone Netlify background function the build emits (see\n * `emitSingleTemplateNetlifyBackgroundFunction` in deploy/build.ts). Shared so\n * the emit and the dispatch-path helper below can never drift on the name.\n *\n * MUST end in `-background` — both because that is the conventional Netlify\n * async-function suffix and because `isInBackgroundFunctionRuntime()` reads the\n * `AWS_LAMBDA_FUNCTION_NAME` `-background` suffix as a secondary runtime signal.\n */\nexport const AGENT_BACKGROUND_FUNCTION_NAME = \"server-agent-background\";\n\n/**\n * Default function URL of the background function on Netlify. Every Netlify\n * function is reachable at `/.netlify/functions/<name>` BY DEFAULT; that default\n * url is removed ONLY if the function declares a custom `config.path`. The\n * emitted background function declares NO custom `config.path` (it sets\n * `background: true` and nothing else routing-related), so it KEEPS this default\n * url — and the Nitro `server` function already excludes `/.netlify/*` from its\n * `/*` catch-all, so this namespace is never shadowed. The foreground therefore\n * dispatches HERE on hosted Netlify (see `resolveAgentChatProcessRunDispatchPath`).\n */\nexport const AGENT_BACKGROUND_FUNCTION_URL_PATH = `/.netlify/functions/${AGENT_BACKGROUND_FUNCTION_NAME}`;\n\n/**\n * The per-app workspace background function URL path. Workspace deploy emits one\n * background function per app named `<app>-agent-background`, reachable at its\n * DEFAULT url `/.netlify/functions/<app>-agent-background` (no custom\n * `config.path`). The foreground resolves the current workspace app id from\n * `AGENT_NATIVE_WORKSPACE_APP_ID` (set by the workspace function entry) so it can\n * dispatch to the right per-app function url. Returns `null` when no workspace\n * app id is configured (single-template deploy).\n */\nfunction resolveWorkspaceBackgroundFunctionUrlPath(): string | null {\n const raw = process.env.AGENT_NATIVE_WORKSPACE_APP_ID;\n if (typeof raw !== \"string\") return null;\n // Mirror the workspace app-id normalization (resources/store.ts): take the\n // first path segment and accept only the safe slug shape used for function\n // names. Anything else falls back to the single-template name.\n const candidate = raw.trim().replace(/^\\/+/, \"\").split(\"/\")[0] ?? \"\";\n if (!/^[a-z0-9][a-z0-9-]{0,127}$/.test(candidate)) return null;\n return `/.netlify/functions/${candidate}-agent-background`;\n}\n\n/**\n * Resolve the path the foreground POST should self-dispatch the chat background\n * worker to.\n *\n * GROUNDED IN THE REAL NETLIFY BUILD OUTPUT + THE NETLIFY DOCS DEFAULT-URL RULE:\n * the background function is emitted INTO the scanned dir\n * (`.netlify/functions-internal/server-agent-background`, or per-app\n * `<app>-agent-background` for workspaces) with `export const config = {\n * background: true, ... }` and NO custom `config.path`. Because it has no custom\n * path, Netlify keeps its DEFAULT function url `/.netlify/functions/<name>`, and\n * `background: true` makes any invocation of that url ASYNC (immediate 202,\n * 15-min budget). The Nitro `server` function already excludes `/.netlify/*`\n * from its `/*` catch-all, so the default-url namespace is NEVER shadowed by the\n * synchronous function.\n *\n * Therefore on hosted Netlify the foreground dispatches to the function's DEFAULT\n * url (`/.netlify/functions/<name>`); the function entry then rewrites the\n * incoming pathname to `AGENT_CHAT_PROCESS_RUN_PATH` (base-path-prefixed for\n * workspaces) before delegating to the Nitro router, so the `_process-run`\n * plugin runs with the async 15-min budget. Everywhere else (local dev, `netlify\n * dev`, non-Netlify hosts where no second function exists) there is no second\n * function, so the foreground dispatches to the framework route\n * `AGENT_CHAT_PROCESS_RUN_PATH` and the same in-process catch-all handles it\n * inline. The HMAC token (signed over the runId) is unchanged either way.\n *\n * NOTE: this is the DOC-CORRECT approach. An earlier attempt gave the function a\n * custom `config.path` + a catch-all `excludedPath` patch; the custom path was\n * NOT honored as a route in prod (probe → 404). Using the default function url\n * (no custom path) is what Netlify documents and is simpler — there is nothing\n * to shadow because `/.netlify/*` is already excluded from the `server` catch-all.\n */\nexport function resolveAgentChatProcessRunDispatchPath(): string {\n if (\n process.env.NETLIFY &&\n process.env.NETLIFY !== \"false\" &&\n process.env.NETLIFY_LOCAL !== \"true\"\n ) {\n return (\n resolveWorkspaceBackgroundFunctionUrlPath() ??\n AGENT_BACKGROUND_FUNCTION_URL_PATH\n );\n }\n return AGENT_CHAT_PROCESS_RUN_PATH;\n}\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"]}
|