@agent-native/core 0.79.6 → 0.79.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +19 -0
- package/corpus/core/docs/content/locales/zh-TW/durable-background-runs.md +74 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +0 -93
- package/corpus/core/src/agent/run-manager.ts +9 -1
- package/corpus/core/src/client/agent-chat-adapter.ts +24 -16
- package/corpus/core/src/client/session-replay.ts +67 -13
- package/corpus/core/src/onboarding/default-steps.ts +3 -3
- package/corpus/core/src/server/agent-chat-plugin.ts +82 -162
- package/corpus/core/src/server/agent-run-context.ts +204 -0
- package/corpus/core/src/server/credential-provider.ts +20 -4
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +14 -4
- package/corpus/templates/analytics/changelog/2026-06-26-session-replay-uploads-now-accept-compressed-payloads-so-lar.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-26-sql-chart-tooltips-now-stay-stable-while-moving-across-dense.md +6 -0
- package/corpus/templates/analytics/server/handlers/session-replay.ts +56 -8
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +1 -1
- package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +44 -16
- package/corpus/templates/clips/app/i18n/ar-SA.ts +12 -9
- package/corpus/templates/clips/app/i18n/de-DE.ts +12 -9
- package/corpus/templates/clips/app/i18n/en-US.ts +19 -16
- package/corpus/templates/clips/app/i18n/es-ES.ts +12 -9
- package/corpus/templates/clips/app/i18n/fr-FR.ts +12 -9
- package/corpus/templates/clips/app/i18n/hi-IN.ts +13 -9
- package/corpus/templates/clips/app/i18n/ja-JP.ts +12 -9
- package/corpus/templates/clips/app/i18n/ko-KR.ts +12 -9
- package/corpus/templates/clips/app/i18n/pt-BR.ts +12 -9
- package/corpus/templates/clips/app/i18n/zh-CN.ts +19 -14
- package/corpus/templates/clips/app/i18n/zh-TW.ts +19 -14
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +7 -0
- package/corpus/templates/clips/app/routes/record.tsx +2 -2
- package/corpus/templates/clips/changelog/2026-06-26-desktop-recordings-recover-more-reliably-when-macos-reports-.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-slack-unfurls-now-recognize-recording-dashboard-links-pasted.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-storage-setup-now-explains-why-clips-needs-connected-storage.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/background.ts +24 -3
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +28 -2
- package/corpus/templates/clips/chrome-extension/src/popup.html +16 -0
- package/corpus/templates/clips/chrome-extension/src/popup.ts +59 -7
- package/corpus/templates/clips/chrome-extension/src/sentry.ts +28 -1
- package/corpus/templates/clips/chrome-extension/src/styles.css +74 -0
- package/corpus/templates/clips/desktop/src/app.tsx +85 -18
- package/corpus/templates/clips/desktop/src/styles.css +55 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +88 -51
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +12 -3
- package/corpus/templates/clips/server/lib/slack-unfurls.ts +5 -2
- package/corpus/templates/slides/actions/_app-url.ts +1 -1
- package/corpus/templates/slides/changelog/2026-06-26-deck-links-now-use-the-workspace-gateway-when-available-so-g.md +6 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +0 -79
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +12 -1
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +15 -8
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +55 -14
- package/dist/client/session-replay.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +7 -7
- package/dist/onboarding/default-steps.js +3 -3
- package/dist/onboarding/default-steps.js.map +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +69 -154
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +2 -2
- package/dist/server/agent-run-context.d.ts +35 -0
- package/dist/server/agent-run-context.d.ts.map +1 -0
- package/dist/server/agent-run-context.js +137 -0
- package/dist/server/agent-run-context.js.map +1 -0
- package/dist/server/credential-provider.d.ts.map +1 -1
- package/dist/server/credential-provider.js +15 -4
- package/dist/server/credential-provider.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/docs/content/locales/zh-TW/durable-background-runs.md +74 -0
- package/package.json +1 -1
- package/corpus/templates/.retired/README.md +0 -9
- package/corpus/templates/.retired/calls/package.json +0 -8
- package/corpus/templates/.retired/code/package.json +0 -8
- package/corpus/templates/.retired/contracts/package.json +0 -8
- package/corpus/templates/.retired/images/package.json +0 -8
- package/corpus/templates/.retired/issues/package.json +0 -8
- package/corpus/templates/.retired/meeting-notes/package.json +0 -8
- package/corpus/templates/.retired/migration/package.json +0 -8
- package/corpus/templates/.retired/recruiting/package.json +0 -8
- package/corpus/templates/.retired/scheduling/package.json +0 -8
- package/corpus/templates/.retired/visual-plans/package.json +0 -8
- package/corpus/templates/.retired/voice/package.json +0 -8
- package/corpus/templates/.retired/workbench/package.json +0 -8
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.79.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ca677a0: Reduce noisy error capture for expected provider connection failures and transient chat recovery probes.
|
|
8
|
+
|
|
9
|
+
## 0.79.8
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 4984f2b: Fail A2A agent tasks that end on terminal run errors without a final response, and keep app-native artifact actions ahead of generic extension creation in agent prompts.
|
|
14
|
+
- 4984f2b: Centralize agent chat request context resolution so foreground and durable background workers share owner, org, timezone, and background-run setup, harden hosted credential fallback detection, and support compressed session replay uploads.
|
|
15
|
+
|
|
16
|
+
## 0.79.7
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- b7cfefe: Remove the temporary durable-background-worker hang-localizer diagnostics from the agent chat setup hot path. The analytics worker-stall bug they were added to debug is fixed and verified in prod, so the ~7 extra awaited DB round-trips per durable run setup (and their 8s-hang risk under DB stress) are no longer needed. The lasting fix and observability — the `worker_stage` column, cheap fire-and-forget `workerStep` markers, `readBackgroundRunClaim`, and the centralized request-context resolution — are retained.
|
|
21
|
+
|
|
3
22
|
## 0.79.6
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "持久背景執行"
|
|
3
|
+
description: "長時間的代理聊天回合可以在獨立的 15 分鐘 Netlify 背景函式中執行,而不是佔用同步請求,讓多步驟工作在伺服器端完成,不需要用戶端看守續跑。每個應用可自行選用;前景斷路器會讓聊天在背景 worker 未啟動時仍可繼續運作。"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 持久背景執行
|
|
7
|
+
|
|
8
|
+
> **適用對象:**部署 agent-native 應用、希望長時間多步驟代理回合在伺服器端完成的人,以及想了解 `AGENT_CHAT_DURABLE_BACKGROUND` 旗標和第二個 Netlify 函式用途的人。這是框架內建的選用功能,不需要撰寫任何應用程式碼。
|
|
9
|
+
|
|
10
|
+
一般託管代理回合會在同步請求函式中執行,並受平台請求逾時限制(Netlify 免費方案約 10 秒,框架約 40 秒軟逾時,之後必須把控制權交回用戶端並「從上次中斷的地方繼續」)。這對快速回答沒有問題,但長時間的代理回合(許多工具呼叫、大量分析)會在多次由用戶端驅動的續跑之間來回跳轉。
|
|
11
|
+
|
|
12
|
+
持久背景執行會把那個長回合移到**獨立的 Netlify 背景函式**,並提供 **15 分鐘**預算。該回合會在伺服器端的一次呼叫中執行完成;UI 只需要串流結果。
|
|
13
|
+
|
|
14
|
+
## 兩個函式,不是一個 {#two-functions}
|
|
15
|
+
|
|
16
|
+
已部署的應用正好有兩個伺服器函式,幾乎所有請求都會打到第一個。只有被分派的代理回合會在第二個函式上執行。
|
|
17
|
+
|
|
18
|
+
```an-diagram title="server 處理所有事;只有被分派的代理回合在背景函式上執行" summary="同步 server 函式提供頁面、actions、API 和前景聊天 POST;它會用 HMAC 將長回合分派到有 15 分鐘預算的背景函式。"
|
|
19
|
+
{
|
|
20
|
+
"html": "<div class=\"diagram-bg-runs\"><div class=\"diagram-panel\" data-rough><div class=\"diagram-pill\">server · synchronous</div><div class=\"diagram-box\">頁面 / SSR</div><div class=\"diagram-box\">Actions</div><div class=\"diagram-box\">API + 框架路由</div><div class=\"diagram-box\">前景聊天 POST <small class=\"diagram-muted\">決定 + 分派</small></div><small class=\"diagram-muted\">約 10 秒逾時 · 幾乎所有請求</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">→<br><small>HMAC 202</small></div><div class=\"diagram-panel\" data-rough><div class=\"diagram-pill accent\">server-agent-background · background:true</div><div class=\"diagram-box\">_process-run worker</div><div class=\"diagram-box\">代理回合:LLM + 工具呼叫</div><small class=\"diagram-ok\">15 分鐘預算 · 僅限被分派的回合</small></div></div>",
|
|
21
|
+
"css": ".diagram-bg-runs{display:flex;align-items:stretch;gap:14px;flex-wrap:wrap}.diagram-bg-runs .diagram-panel{display:flex;flex-direction:column;gap:8px;padding:14px;flex:1;min-width:230px}.diagram-bg-runs .diagram-box{padding:8px 10px}.diagram-bg-runs .diagram-arrow{display:flex;flex-direction:column;justify-content:center;text-align:center;min-width:90px;font-size:20px}"
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
應用或範本程式碼中**沒有任何需要標記的地方**。當函式的 `export const config` 含有 `background: true`(或函式名稱以 `-background` 結尾)時,Netlify 就會把它視為背景函式。啟用此功能時,框架的建置步驟會自動產生 `server-agent-background` 包裝器;應用作者不需要自行宣告。
|
|
26
|
+
|
|
27
|
+
## 背景中會執行什麼,以及如何選擇 {#what-runs}
|
|
28
|
+
|
|
29
|
+
只有長時間的代理聊天回合,也就是會重新進入代理迴圈的內部 `_process-run` 分派(LLM 呼叫 + 工具執行)。頁面、actions 和一般 API 呼叫一律在 `server` 上同步執行。
|
|
30
|
+
|
|
31
|
+
前景聊天 POST 會**逐回合**決定。只有下列三個條件同時成立時,才會分派到背景函式:
|
|
32
|
+
|
|
33
|
+
- `AGENT_CHAT_DURABLE_BACKGROUND` 為真值(`true` / `1` / `yes` / `on`),
|
|
34
|
+
- 應用正在託管環境中執行(在平台上,而非本機開發),以及
|
|
35
|
+
- 已設定 `A2A_SECRET`(分派是 HMAC 簽署的自我呼叫;沒有 secret 就無法簽署,因此該回合會留在線上同步執行)。
|
|
36
|
+
|
|
37
|
+
若缺少任何條件,該回合就會和以前一樣在線上同步執行。這個旗標會在**建置時間**讀取(決定是否輸出背景函式),也會在**執行時間**讀取(決定是否分派),因此啟用後需要重新部署。
|
|
38
|
+
|
|
39
|
+
## 生命週期與斷路器 {#lifecycle}
|
|
40
|
+
|
|
41
|
+
Netlify async function 會在**排入**呼叫的瞬間回傳 `202`;這不代表 worker 已實際執行。因此前景不會盲目相信 `202`:分派後會短暫輪詢,確認 worker 是否真的**認領**該 run;若沒有認領,前景會在線上同步復原。
|
|
42
|
+
|
|
43
|
+
```an-diagram title="前景一律回傳可運作的回合" summary="分派時,前景會等待一段寬限時間讓 worker 認領 run;若認領成功,前景會訂閱它;若未認領,前景會自行認領並在線上同步執行該回合。"
|
|
44
|
+
{
|
|
45
|
+
"html": "<div class=\"diagram-bg-life\"><div class=\"diagram-box\" data-rough>前景 POST</div><div class=\"diagram-arrow diagram-muted\">→</div><div class=\"diagram-box\" data-rough>插入 run + HMAC 分派 → 202</div><div class=\"diagram-arrow diagram-muted\">→</div><div class=\"diagram-panel center\" data-rough><strong>Worker 是否在寬限時間內認領?</strong><small class=\"diagram-ok\">是 → 串流背景 worker(最多 15 分鐘)</small><small class=\"diagram-warn\">否 → 斷路器在線上同步復原該回合</small></div></div>",
|
|
46
|
+
"css": ".diagram-bg-life{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.diagram-bg-life .diagram-arrow{font-size:20px}.diagram-bg-life .center{display:flex;flex-direction:column;gap:4px;padding:12px}"
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
認領會透過 `dispatch_mode` 從 `background` 變成 `background-processing` 來觀察(只有 worker 的原子認領會這樣做)。寬限時間必須涵蓋 worker 冷啟動;較重的應用曾觀察到需要比原本 8 秒更久,因此目前是 **15 秒**(仍在前景約 40 秒軟逾時內)。如果 worker 從未認領,無論是故障或只是啟動太慢,都會在寬限時間後回退到線上同步執行,所以該回合仍會完成;代價是先等待寬限時間所增加的延遲。
|
|
51
|
+
|
|
52
|
+
```an-callout
|
|
53
|
+
{
|
|
54
|
+
"tone": "success",
|
|
55
|
+
"body": "無論哪種情況,前景都會回傳可運作的回合:worker 及時認領時使用背景 worker,否則使用線上同步回合。因此,即使 worker 失敗,持久背景執行也應該保留聊天正確性;但設定錯誤或認領過慢的 worker 可能會在同步復原接手前增加回退延遲(最多到寬限時間)。"
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 成本與取捨 {#cost}
|
|
60
|
+
|
|
61
|
+
Netlify 背景函式是**付費方案**功能(Personal / Pro,不含免費方案),並依運算時間計費(GB-hour)。因此多分鐘的代理回合會比短同步呼叫花費更多運算量;但它大致上**取代**了舊模式,也就是回合撞到約 40 秒限制後,用戶端重新送出多次續跑請求。這是把同一份工作合併到一次長呼叫中,不是純粹額外增加。
|
|
62
|
+
|
|
63
|
+
```an-callout
|
|
64
|
+
{
|
|
65
|
+
"tone": "info",
|
|
66
|
+
"body": "持久背景執行是每個應用可選用的功能,預設為關閉。使用 AGENT_CHAT_DURABLE_BACKGROUND=true(加上 A2A_SECRET)啟用並重新部署。若要驗證某個回合真的使用 worker,請檢查該 run 是否到達 dispatch_mode=background-processing,而不是回退到線上同步復原。"
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 相關
|
|
71
|
+
|
|
72
|
+
- [**持久恢復**](/docs/durable-resume) — 中斷的 run 如何恢復而不重複已完成的副作用(另一個永遠啟用的保護)。
|
|
73
|
+
- [**部署**](/docs/deployment) — 應用及其函式如何建置並部署到平台。
|
|
74
|
+
- [**A2A Protocol**](/docs/a2a-protocol) — `A2A_SECRET` 與持久 worker 重用的 HMAC 簽署自我分派。
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.79.
|
|
3
|
+
"version": "0.79.9",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -3972,25 +3972,6 @@ export function createProductionAgentHandler(
|
|
|
3972
3972
|
`${s}=${Date.now() - setupT0}ms`,
|
|
3973
3973
|
).catch(() => {});
|
|
3974
3974
|
};
|
|
3975
|
-
// DIAGNOSTIC-ONLY: AWAITED worker mark. Fire-and-forget workerStep marks
|
|
3976
|
-
// after `post_model_ok` stopped landing even though writes still work
|
|
3977
|
-
// there — so the worker's MAIN FLOW stalls between post_model and pre_claim
|
|
3978
|
-
// (not a write hang). Awaited marks block until they land (or withDbTimeout-
|
|
3979
|
-
// reject), so `worker_stage` reliably advances to the last phase the main
|
|
3980
|
-
// flow actually reached — pinpointing the stall. Bg worker only.
|
|
3981
|
-
const awaitedWorkerMark = async (s: string) => {
|
|
3982
|
-
if (!isBackgroundWorker || !bgRunId) return;
|
|
3983
|
-
try {
|
|
3984
|
-
await recordRunDiagnostic(
|
|
3985
|
-
bgRunId,
|
|
3986
|
-
RUN_DIAG_STAGE.workerSetupStep,
|
|
3987
|
-
`${s}=${Date.now() - setupT0}ms`,
|
|
3988
|
-
);
|
|
3989
|
-
} catch {
|
|
3990
|
-
// bounded by withDbTimeout; ignore — the absence of the next mark is
|
|
3991
|
-
// itself the signal.
|
|
3992
|
-
}
|
|
3993
|
-
};
|
|
3994
3975
|
// Whether this worker is REALLY executing inside a 15-min Netlify
|
|
3995
3976
|
// `-background` function (proven by the runtime function name), not merely a
|
|
3996
3977
|
// `_process-run` re-entry that may have landed on the ~60s synchronous
|
|
@@ -4215,35 +4196,6 @@ export function createProductionAgentHandler(
|
|
|
4215
4196
|
);
|
|
4216
4197
|
|
|
4217
4198
|
options.onEngineResolved?.(engine, model);
|
|
4218
|
-
// DIAGNOSTIC-ONLY: localize where the worker stalls AFTER model_done. These
|
|
4219
|
-
// land in `worker_stage` (foreground-independent), so even though the
|
|
4220
|
-
// foreground overwrites `diag_stage` on inline recovery, the worker's last
|
|
4221
|
-
// reached point survives. If the last worker_stage is `model_done`, writes
|
|
4222
|
-
// hang right after model resolution (DB connection); if it's
|
|
4223
|
-
// `engine_resolved`/`systemprompt_enter`, the stall is in the named step.
|
|
4224
|
-
workerStep("engine_resolved");
|
|
4225
|
-
// DIAGNOSTIC-ONLY: AWAITED probe (bg worker only). worker_stage stalls at
|
|
4226
|
-
// `model_done` even though the code right after is trivial sync — this
|
|
4227
|
-
// distinguishes the two causes: if `post_model_awaited` lands, DB writes
|
|
4228
|
-
// still work after model_done and the stall is later in the main flow; if it
|
|
4229
|
-
// never lands (stays `model_done`), the bg-fn DB connection itself is hung
|
|
4230
|
-
// right after model resolution. `recordRunDiagnostic` is `withDbTimeout`-
|
|
4231
|
-
// bounded, so a hung write rejects (caught) rather than blocking forever.
|
|
4232
|
-
if (isBackgroundWorker && bgRunId) {
|
|
4233
|
-
const probeStart = Date.now();
|
|
4234
|
-
try {
|
|
4235
|
-
await recordRunDiagnostic(
|
|
4236
|
-
bgRunId,
|
|
4237
|
-
RUN_DIAG_STAGE.workerSetupStep,
|
|
4238
|
-
"post_model_awaited",
|
|
4239
|
-
);
|
|
4240
|
-
workerStep(`post_model_ok=${Date.now() - probeStart}ms`);
|
|
4241
|
-
} catch (e) {
|
|
4242
|
-
workerStep(
|
|
4243
|
-
`post_model_threw=${Date.now() - probeStart}ms:${String((e as Error)?.message ?? e).slice(0, 80)}`,
|
|
4244
|
-
);
|
|
4245
|
-
}
|
|
4246
|
-
}
|
|
4247
4199
|
|
|
4248
4200
|
// One-line per-turn resolution log so it's obvious in dev which engine
|
|
4249
4201
|
// is actually handling the request. `requestEngine` is what the client
|
|
@@ -4254,14 +4206,6 @@ export function createProductionAgentHandler(
|
|
|
4254
4206
|
`[agent-chat] resolved engine=${engine.name} model=${model} requestEngine=${requestEngine ?? "(none)"}`,
|
|
4255
4207
|
);
|
|
4256
4208
|
|
|
4257
|
-
// DIAGNOSTIC-ONLY: the worker reaches post_model_ok but never aw_env — the
|
|
4258
|
-
// only exit between them is this anthropic key check. Capture the exact
|
|
4259
|
-
// resolution state so we know WHY effectiveApiKey is falsy in the worker
|
|
4260
|
-
// (owner not resolved from the HMAC/thread? owner has no stored key? deploy
|
|
4261
|
-
// fallback blocked?) vs the foreground which resolves it fine.
|
|
4262
|
-
await awaitedWorkerMark(
|
|
4263
|
-
`apikey_state:engine=${engine.name},owner=${ownerEmail ? "Y" : "N"},userKey=${userApiKey ? "Y" : "N"},effKey=${effectiveApiKey ? "Y" : "N"},block=${shouldBlockDeployCredentialFallback()}`,
|
|
4264
|
-
);
|
|
4265
4209
|
// Check for API key before starting a run (only for anthropic engine)
|
|
4266
4210
|
if (engine.name === "anthropic" && !effectiveApiKey) {
|
|
4267
4211
|
setResponseHeader(event, "Content-Type", "text/event-stream");
|
|
@@ -4289,7 +4233,6 @@ export function createProductionAgentHandler(
|
|
|
4289
4233
|
// reached db_request_ctx but not env_config hung in attachment upload or
|
|
4290
4234
|
// engine/model resolution.
|
|
4291
4235
|
workerStep("env_config");
|
|
4292
|
-
await awaitedWorkerMark("aw_env");
|
|
4293
4236
|
// Run all independent pre-send steps in parallel. Each of these hits
|
|
4294
4237
|
// the DB or invokes an action; running them sequentially was the
|
|
4295
4238
|
// single biggest contributor to pre-LLM latency.
|
|
@@ -4305,17 +4248,11 @@ export function createProductionAgentHandler(
|
|
|
4305
4248
|
const systemPromptThunk = (): Promise<string> =>
|
|
4306
4249
|
(async (): Promise<string> => {
|
|
4307
4250
|
const sysPromptStart = Date.now();
|
|
4308
|
-
// Brackets the system-prompt build (which runs the template's
|
|
4309
|
-
// extraContext / data-dictionary). If worker_stage stalls at
|
|
4310
|
-
// `systemprompt_enter` with no `systemprompt_done`, the build itself is
|
|
4311
|
-
// the stall point.
|
|
4312
|
-
workerStep("systemprompt_enter");
|
|
4313
4251
|
try {
|
|
4314
4252
|
const built =
|
|
4315
4253
|
typeof options.systemPrompt === "function"
|
|
4316
4254
|
? await options.systemPrompt(event)
|
|
4317
4255
|
: options.systemPrompt;
|
|
4318
|
-
workerStep("systemprompt_done");
|
|
4319
4256
|
return built;
|
|
4320
4257
|
} catch (error) {
|
|
4321
4258
|
systemPromptError = error;
|
|
@@ -4614,7 +4551,6 @@ export function createProductionAgentHandler(
|
|
|
4614
4551
|
// DIAGNOSTIC-ONLY: all parallel context gathering (system prompt, screen,
|
|
4615
4552
|
// files, loop settings, enriched message) resolved.
|
|
4616
4553
|
workerStep("context_all");
|
|
4617
|
-
await awaitedWorkerMark("aw_presend");
|
|
4618
4554
|
|
|
4619
4555
|
if (systemPromptError) {
|
|
4620
4556
|
setResponseHeader(event, "Content-Type", "text/event-stream");
|
|
@@ -4645,7 +4581,6 @@ export function createProductionAgentHandler(
|
|
|
4645
4581
|
setupMark("actions");
|
|
4646
4582
|
// DIAGNOSTIC-ONLY: action/tool resolution + engine-tool filtering finished.
|
|
4647
4583
|
workerStep("action_tool_setup");
|
|
4648
|
-
await awaitedWorkerMark("aw_actions");
|
|
4649
4584
|
const requestSystemPrompt =
|
|
4650
4585
|
requestMode === "plan"
|
|
4651
4586
|
? `${systemPrompt}\n\n${PLAN_MODE_SYSTEM_PROMPT}`
|
|
@@ -4760,7 +4695,6 @@ export function createProductionAgentHandler(
|
|
|
4760
4695
|
// DIAGNOSTIC-ONLY: owner/thread resolution + runId/effectiveThreadId +
|
|
4761
4696
|
// chained-continuation thread fetch finished.
|
|
4762
4697
|
workerStep("owner_thread");
|
|
4763
|
-
await awaitedWorkerMark("aw_owner");
|
|
4764
4698
|
|
|
4765
4699
|
// Persist the user's turn exactly once. The foreground POST does this
|
|
4766
4700
|
// before dispatching; the background worker must NOT repeat it (it re-enters
|
|
@@ -5178,33 +5112,6 @@ export function createProductionAgentHandler(
|
|
|
5178
5112
|
// DIAGNOSTIC-ONLY: last stage before startRun fires. A worker that reaches
|
|
5179
5113
|
// prestart but never workerStarted is hanging inside startRun itself.
|
|
5180
5114
|
workerStep("prestart");
|
|
5181
|
-
// DIAGNOSTIC-ONLY: AWAITED probe right before startRun (bg worker only). If
|
|
5182
|
-
// worker_stage reaches `pre_claim_ok` but the run never flips to
|
|
5183
|
-
// `worker_started`, the stall is inside startRun's claim itself; if it never
|
|
5184
|
-
// reaches `pre_claim_*`, the stall is somewhere in the setup between
|
|
5185
|
-
// post_model and here.
|
|
5186
|
-
if (isBackgroundWorker && bgRunId) {
|
|
5187
|
-
const claimProbeStart = Date.now();
|
|
5188
|
-
try {
|
|
5189
|
-
await recordRunDiagnostic(
|
|
5190
|
-
bgRunId,
|
|
5191
|
-
RUN_DIAG_STAGE.workerSetupStep,
|
|
5192
|
-
"pre_claim",
|
|
5193
|
-
);
|
|
5194
|
-
workerStep(`pre_claim_ok=${Date.now() - claimProbeStart}ms`);
|
|
5195
|
-
} catch (e) {
|
|
5196
|
-
workerStep(
|
|
5197
|
-
`pre_claim_threw=${Date.now() - claimProbeStart}ms:${String((e as Error)?.message ?? e).slice(0, 80)}`,
|
|
5198
|
-
);
|
|
5199
|
-
}
|
|
5200
|
-
}
|
|
5201
|
-
// DIAGNOSTIC-ONLY: peak-ish RSS (MB) + assembled system-prompt size (KB) at
|
|
5202
|
-
// prestart. The analytics bg worker dies right after model_done; if the
|
|
5203
|
-
// FOREGROUND (identical build, writes land) is already near the ~1024MB
|
|
5204
|
-
// Netlify function limit, an OOM kill in the heavier worker explains the
|
|
5205
|
-
// freeze. Both numbers ride along in the existing setup-timings detail.
|
|
5206
|
-
setupMarks.rssMB = Math.round(process.memoryUsage().rss / 1048576);
|
|
5207
|
-
setupMarks.promptKB = Math.round((systemPrompt?.length ?? 0) / 1024);
|
|
5208
5115
|
const setupDetail =
|
|
5209
5116
|
Object.entries(setupMarks)
|
|
5210
5117
|
.map(([k, v]) => `${k}=${v}`)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { captureError } from "../server/capture-error.js";
|
|
2
|
+
import { isLlmCredentialError } from "./engine/credential-errors.js";
|
|
2
3
|
import { EngineError } from "./engine/types.js";
|
|
3
4
|
import {
|
|
4
5
|
insertRun,
|
|
@@ -149,10 +150,15 @@ function getEngineRunErrorDetails(err: EngineError): string | undefined {
|
|
|
149
150
|
function shouldCaptureRunError(err: unknown): boolean {
|
|
150
151
|
if (!(err instanceof EngineError)) return true;
|
|
151
152
|
const errorCode = getEngineRunErrorCode(err);
|
|
153
|
+
if (isLlmCredentialError(err, errorCode)) return false;
|
|
154
|
+
if (err.statusCode === 401 || err.statusCode === 403) return false;
|
|
155
|
+
if (/^40[13] status code\b/i.test(err.message)) return false;
|
|
156
|
+
if (err.message.trim().toLowerCase() === "connection error.") return false;
|
|
152
157
|
if (!errorCode) return true;
|
|
153
158
|
const normalizedCode = errorCode.toLowerCase();
|
|
154
159
|
return (
|
|
155
160
|
!normalizedCode.startsWith("credits-limit") &&
|
|
161
|
+
normalizedCode !== "builder_gateway_network_error" &&
|
|
156
162
|
normalizedCode !== "provider_rate_limited" &&
|
|
157
163
|
normalizedCode !== "rate_limit_exceeded"
|
|
158
164
|
);
|
|
@@ -423,6 +429,8 @@ export function startRun(
|
|
|
423
429
|
let pendingTerminalEvent: RunEvent | null = null;
|
|
424
430
|
|
|
425
431
|
const captureRunError = (error: unknown, phase: "run" | "completion") => {
|
|
432
|
+
const errorCode =
|
|
433
|
+
error instanceof EngineError ? getEngineRunErrorCode(error) : undefined;
|
|
426
434
|
captureError(error, {
|
|
427
435
|
route: "/_agent-native/agent-chat",
|
|
428
436
|
tags: {
|
|
@@ -431,7 +439,7 @@ export function startRun(
|
|
|
431
439
|
runStatus: run.status,
|
|
432
440
|
softTimedOut: softTimedOut ? "true" : "false",
|
|
433
441
|
abortReason: run.abortReason,
|
|
434
|
-
errorCode
|
|
442
|
+
errorCode,
|
|
435
443
|
},
|
|
436
444
|
extra: {
|
|
437
445
|
runId,
|
|
@@ -717,7 +717,7 @@ function continuationRepeatSignature(content: ContentPart[]): string {
|
|
|
717
717
|
.map((segment) => segment.replace(/[^a-z0-9]+/g, " ").trim())
|
|
718
718
|
.filter((segment) => segment.length > 0);
|
|
719
719
|
if (segments.length === 0) return "";
|
|
720
|
-
return Array.from(new Set(segments)).sort().join("
|
|
720
|
+
return Array.from(new Set(segments)).sort().join("\u0000");
|
|
721
721
|
}
|
|
722
722
|
|
|
723
723
|
/**
|
|
@@ -771,7 +771,7 @@ function inFlightToolInputSignature(
|
|
|
771
771
|
part: Extract<ContentPart, { type: "tool-call" }>,
|
|
772
772
|
): string {
|
|
773
773
|
const raw = part.argsText ?? stableJson(part.args);
|
|
774
|
-
return `${raw.length}
|
|
774
|
+
return `${raw.length}\u0000${raw.slice(0, 256)}`;
|
|
775
775
|
}
|
|
776
776
|
|
|
777
777
|
function toolContinuationKey(
|
|
@@ -964,6 +964,10 @@ function retryDelay(attempt: number, abortSignal: AbortSignal): Promise<void> {
|
|
|
964
964
|
return delay(ms, abortSignal);
|
|
965
965
|
}
|
|
966
966
|
|
|
967
|
+
function shouldCaptureRecoveryHttpStatus(status: number): boolean {
|
|
968
|
+
return status < 500 || status >= 600;
|
|
969
|
+
}
|
|
970
|
+
|
|
967
971
|
function generateTurnId(): string {
|
|
968
972
|
if (
|
|
969
973
|
typeof crypto !== "undefined" &&
|
|
@@ -1518,15 +1522,17 @@ export function createAgentChatAdapter(
|
|
|
1518
1522
|
lastReconnectError = new Error(
|
|
1519
1523
|
`Reconnect failed: ${reconnectRes.status}`,
|
|
1520
1524
|
);
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1525
|
+
if (shouldCaptureRecoveryHttpStatus(reconnectRes.status)) {
|
|
1526
|
+
captureChatClientError(
|
|
1527
|
+
lastReconnectError,
|
|
1528
|
+
"reconnect-current-response",
|
|
1529
|
+
{
|
|
1530
|
+
status: reconnectRes.status,
|
|
1531
|
+
hasBody: Boolean(reconnectRes.body),
|
|
1532
|
+
attempt,
|
|
1533
|
+
},
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1530
1536
|
reconnectErrorCaptured = true;
|
|
1531
1537
|
break;
|
|
1532
1538
|
}
|
|
@@ -1610,11 +1616,13 @@ export function createAgentChatAdapter(
|
|
|
1610
1616
|
lastActiveRunError = new Error(
|
|
1611
1617
|
`Active run lookup failed: ${activeRes.status}`,
|
|
1612
1618
|
);
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1619
|
+
if (shouldCaptureRecoveryHttpStatus(activeRes.status)) {
|
|
1620
|
+
captureChatClientError(
|
|
1621
|
+
lastActiveRunError,
|
|
1622
|
+
"reconnect-active-response",
|
|
1623
|
+
{ status: activeRes.status, attempt },
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1618
1626
|
return false;
|
|
1619
1627
|
}
|
|
1620
1628
|
const active = await activeRes.json();
|
|
@@ -630,6 +630,72 @@ function buildReplayBody(
|
|
|
630
630
|
return `${envelopeJson.slice(0, -1)},"events":[${eventJsonParts.join(",")}]}`;
|
|
631
631
|
}
|
|
632
632
|
|
|
633
|
+
interface ReplayUploadBody {
|
|
634
|
+
body: BodyInit;
|
|
635
|
+
headers: Record<string, string>;
|
|
636
|
+
compressed: boolean;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function gzipReplayBody(body: string): Promise<Blob | null> {
|
|
640
|
+
if (
|
|
641
|
+
typeof CompressionStream === "undefined" ||
|
|
642
|
+
typeof Blob === "undefined" ||
|
|
643
|
+
typeof Response === "undefined"
|
|
644
|
+
) {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
const stream = new Blob([body], { type: "application/json" })
|
|
649
|
+
.stream()
|
|
650
|
+
.pipeThrough(new CompressionStream("gzip"));
|
|
651
|
+
const compressed = await new Response(stream).arrayBuffer();
|
|
652
|
+
return new Blob([compressed], { type: "application/json" });
|
|
653
|
+
} catch {
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function buildReplayUploadBody(body: string): Promise<ReplayUploadBody> {
|
|
659
|
+
const compressed = await gzipReplayBody(body);
|
|
660
|
+
if (compressed) {
|
|
661
|
+
return {
|
|
662
|
+
body: compressed,
|
|
663
|
+
compressed: true,
|
|
664
|
+
headers: {
|
|
665
|
+
"Content-Type": "application/json",
|
|
666
|
+
"Content-Encoding": "gzip",
|
|
667
|
+
},
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
body,
|
|
672
|
+
compressed: false,
|
|
673
|
+
headers: {
|
|
674
|
+
"Content-Type": "text/plain;charset=UTF-8",
|
|
675
|
+
},
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
async function sendReplayUpload(
|
|
680
|
+
options: NormalizedSessionReplayOptions,
|
|
681
|
+
body: string,
|
|
682
|
+
): Promise<void> {
|
|
683
|
+
const upload = await buildReplayUploadBody(body);
|
|
684
|
+
if (!upload.compressed && navigator.sendBeacon) {
|
|
685
|
+
const sent = navigator.sendBeacon(options.endpoint, body);
|
|
686
|
+
if (sent) return;
|
|
687
|
+
}
|
|
688
|
+
await fetch(options.endpoint, {
|
|
689
|
+
method: "POST",
|
|
690
|
+
body: upload.body,
|
|
691
|
+
keepalive: true,
|
|
692
|
+
headers: {
|
|
693
|
+
...upload.headers,
|
|
694
|
+
"X-Agent-Native-Analytics-Key": options.publicKey,
|
|
695
|
+
},
|
|
696
|
+
}).catch(() => {});
|
|
697
|
+
}
|
|
698
|
+
|
|
633
699
|
function isFinalFlushReason(reason: string): boolean {
|
|
634
700
|
return [
|
|
635
701
|
"manual",
|
|
@@ -649,19 +715,7 @@ export async function flushSessionReplay(reason = "manual"): Promise<void> {
|
|
|
649
715
|
if (!body || !state.options) return;
|
|
650
716
|
state.flushing = true;
|
|
651
717
|
try {
|
|
652
|
-
|
|
653
|
-
const sent = navigator.sendBeacon(state.options.endpoint, body);
|
|
654
|
-
if (sent) return;
|
|
655
|
-
}
|
|
656
|
-
await fetch(state.options.endpoint, {
|
|
657
|
-
method: "POST",
|
|
658
|
-
body,
|
|
659
|
-
keepalive: true,
|
|
660
|
-
headers: {
|
|
661
|
-
"Content-Type": "text/plain;charset=UTF-8",
|
|
662
|
-
"X-Agent-Native-Analytics-Key": state.options.publicKey,
|
|
663
|
-
},
|
|
664
|
-
}).catch(() => {});
|
|
718
|
+
await sendReplayUpload(state.options, body);
|
|
665
719
|
} finally {
|
|
666
720
|
state.flushing = false;
|
|
667
721
|
}
|
|
@@ -121,9 +121,9 @@ const llmStep: OnboardingStep = {
|
|
|
121
121
|
await import("../server/credential-provider.js");
|
|
122
122
|
if (await resolveHasCompleteBuilderConnection()) return true;
|
|
123
123
|
} catch {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
// Credential storage may be unavailable during early boot. Do not fall
|
|
125
|
+
// back to deployment-level Builder env here; the scoped resolver owns the
|
|
126
|
+
// policy for when that is safe.
|
|
127
127
|
}
|
|
128
128
|
try {
|
|
129
129
|
if (await detectEngineFromUserSecrets()) return true;
|