@agent-native/core 0.70.3 → 0.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +51 -0
  3. package/corpus/core/docs/content/actions.md +23 -0
  4. package/corpus/core/docs/content/audit-log.md +111 -0
  5. package/corpus/core/docs/content/multi-app-workspace.md +2 -2
  6. package/corpus/core/docs/design/durable-agent-runs.md +458 -4
  7. package/corpus/core/package.json +2 -1
  8. package/corpus/core/src/action.ts +80 -1
  9. package/corpus/core/src/agent/durable-background.ts +192 -0
  10. package/corpus/core/src/agent/production-agent.ts +357 -13
  11. package/corpus/core/src/agent/run-manager.ts +66 -3
  12. package/corpus/core/src/agent/run-store.ts +143 -23
  13. package/corpus/core/src/agent/types.ts +20 -0
  14. package/corpus/core/src/application-state/store.ts +5 -0
  15. package/corpus/core/src/audit/actions/get-audit-event.ts +23 -0
  16. package/corpus/core/src/audit/actions/list-audit-events.ts +65 -0
  17. package/corpus/core/src/audit/cleanup-job.ts +100 -0
  18. package/corpus/core/src/audit/config.ts +91 -0
  19. package/corpus/core/src/audit/index.ts +43 -0
  20. package/corpus/core/src/audit/record.ts +143 -0
  21. package/corpus/core/src/audit/redact.ts +109 -0
  22. package/corpus/core/src/audit/store.ts +244 -0
  23. package/corpus/core/src/audit/types.ts +125 -0
  24. package/corpus/core/src/chat-threads/store.ts +10 -0
  25. package/corpus/core/src/client/AgentPanel.tsx +12 -9
  26. package/corpus/core/src/client/AssistantChat.tsx +6 -0
  27. package/corpus/core/src/client/blocks/library/FileTreeBlock.tsx +72 -14
  28. package/corpus/core/src/client/chat/repo-helpers.ts +38 -0
  29. package/corpus/core/src/db/client.ts +4 -0
  30. package/corpus/core/src/db/widen-columns.ts +75 -0
  31. package/corpus/core/src/deploy/build.ts +96 -0
  32. package/corpus/core/src/deploy/workspace-deploy.ts +121 -0
  33. package/corpus/core/src/mcp/build-server.ts +23 -13
  34. package/corpus/core/src/oauth-tokens/store.ts +6 -0
  35. package/corpus/core/src/provider-api/custom-registry.ts +8 -0
  36. package/corpus/core/src/resources/store.ts +10 -0
  37. package/corpus/core/src/scripts/runner.ts +6 -3
  38. package/corpus/core/src/server/action-discovery.ts +6 -0
  39. package/corpus/core/src/server/action-routes.ts +1 -0
  40. package/corpus/core/src/server/agent-chat-plugin.ts +133 -66
  41. package/corpus/core/src/server/auth.ts +5 -0
  42. package/corpus/core/src/server/core-routes-plugin.ts +14 -0
  43. package/corpus/core/src/server/security-headers.ts +9 -6
  44. package/corpus/core/src/settings/store.ts +6 -0
  45. package/corpus/core/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
  46. package/corpus/core/src/usage/store.ts +6 -0
  47. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -0
  48. package/corpus/templates/analytics/AGENTS.md +8 -0
  49. package/corpus/templates/analytics/actions/compose-dashboard.ts +317 -0
  50. package/corpus/templates/analytics/changelog/2026-06-23-build-large-first-party-analytics-dashboards-in-one-fast-cal.md +6 -0
  51. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +574 -0
  52. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +60 -6
  53. package/corpus/templates/calendar/changelog/2026-06-23-added-a-heads-up-explaining-google-s-app-not-verified-screen.md +6 -0
  54. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +20 -43
  55. package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +5 -1
  56. package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +3 -51
  57. package/corpus/templates/clips/changelog/2026-06-23-dragging-the-desktop-camera-bubble-now-glides-to-a-stop-at-t.md +6 -0
  58. package/corpus/templates/clips/changelog/2026-06-23-public-clips-now-play-inline-in-slack-connect-a-workspac.md +6 -0
  59. package/corpus/templates/clips/chrome-extension/PERMISSIONS.md +72 -0
  60. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -10
  61. package/corpus/templates/clips/chrome-extension/src/background.ts +163 -32
  62. package/corpus/templates/clips/chrome-extension/src/content-script.ts +301 -24
  63. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +245 -67
  64. package/corpus/templates/clips/chrome-extension/src/overlay.css +77 -43
  65. package/corpus/templates/clips/chrome-extension/src/overlay.html +7 -1
  66. package/corpus/templates/clips/chrome-extension/src/overlay.ts +102 -38
  67. package/corpus/templates/clips/chrome-extension/src/popup.html +62 -5
  68. package/corpus/templates/clips/chrome-extension/src/popup.ts +275 -4
  69. package/corpus/templates/clips/chrome-extension/src/styles.css +34 -0
  70. package/corpus/templates/clips/chrome-extension/vite.config.ts +5 -0
  71. package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +21 -16
  72. package/corpus/templates/clips/desktop/src/lib/recorder.ts +10 -17
  73. package/corpus/templates/clips/desktop/src/overlays/bubble.tsx +73 -23
  74. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +0 -6
  75. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +56 -46
  76. package/corpus/templates/clips/desktop/src/styles.css +33 -5
  77. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +113 -0
  78. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
  79. package/corpus/templates/clips/server/lib/media-permissions.ts +5 -1
  80. package/corpus/templates/clips/shared/recording-audio.ts +62 -0
  81. package/corpus/templates/clips/shared/recording-core.ts +94 -0
  82. package/dist/action.d.ts +31 -0
  83. package/dist/action.d.ts.map +1 -1
  84. package/dist/action.js +45 -1
  85. package/dist/action.js.map +1 -1
  86. package/dist/agent/context-xray/schema.d.ts +1 -1
  87. package/dist/agent/durable-background.d.ts +60 -0
  88. package/dist/agent/durable-background.d.ts.map +1 -0
  89. package/dist/agent/durable-background.js +144 -0
  90. package/dist/agent/durable-background.js.map +1 -0
  91. package/dist/agent/observational-memory/schema.d.ts +1 -1
  92. package/dist/agent/production-agent.d.ts +20 -0
  93. package/dist/agent/production-agent.d.ts.map +1 -1
  94. package/dist/agent/production-agent.js +320 -14
  95. package/dist/agent/production-agent.js.map +1 -1
  96. package/dist/agent/run-manager.d.ts +48 -0
  97. package/dist/agent/run-manager.d.ts.map +1 -1
  98. package/dist/agent/run-manager.js +45 -3
  99. package/dist/agent/run-manager.js.map +1 -1
  100. package/dist/agent/run-store.d.ts +30 -1
  101. package/dist/agent/run-store.d.ts.map +1 -1
  102. package/dist/agent/run-store.js +138 -24
  103. package/dist/agent/run-store.js.map +1 -1
  104. package/dist/agent/types.d.ts +20 -0
  105. package/dist/agent/types.d.ts.map +1 -1
  106. package/dist/agent/types.js.map +1 -1
  107. package/dist/application-state/store.d.ts.map +1 -1
  108. package/dist/application-state/store.js +5 -0
  109. package/dist/application-state/store.js.map +1 -1
  110. package/dist/audit/actions/get-audit-event.d.ts +11 -0
  111. package/dist/audit/actions/get-audit-event.d.ts.map +1 -0
  112. package/dist/audit/actions/get-audit-event.js +22 -0
  113. package/dist/audit/actions/get-audit-event.js.map +1 -0
  114. package/dist/audit/actions/list-audit-events.d.ts +22 -0
  115. package/dist/audit/actions/list-audit-events.d.ts.map +1 -0
  116. package/dist/audit/actions/list-audit-events.js +61 -0
  117. package/dist/audit/actions/list-audit-events.js.map +1 -0
  118. package/dist/audit/cleanup-job.d.ts +12 -0
  119. package/dist/audit/cleanup-job.d.ts.map +1 -0
  120. package/dist/audit/cleanup-job.js +93 -0
  121. package/dist/audit/cleanup-job.js.map +1 -0
  122. package/dist/audit/config.d.ts +30 -0
  123. package/dist/audit/config.d.ts.map +1 -0
  124. package/dist/audit/config.js +65 -0
  125. package/dist/audit/config.js.map +1 -0
  126. package/dist/audit/index.d.ts +13 -0
  127. package/dist/audit/index.d.ts.map +1 -0
  128. package/dist/audit/index.js +6 -0
  129. package/dist/audit/index.js.map +1 -0
  130. package/dist/audit/record.d.ts +24 -0
  131. package/dist/audit/record.d.ts.map +1 -0
  132. package/dist/audit/record.js +106 -0
  133. package/dist/audit/record.js.map +1 -0
  134. package/dist/audit/redact.d.ts +28 -0
  135. package/dist/audit/redact.d.ts.map +1 -0
  136. package/dist/audit/redact.js +109 -0
  137. package/dist/audit/redact.js.map +1 -0
  138. package/dist/audit/store.d.ts +14 -0
  139. package/dist/audit/store.d.ts.map +1 -0
  140. package/dist/audit/store.js +219 -0
  141. package/dist/audit/store.js.map +1 -0
  142. package/dist/audit/types.d.ts +114 -0
  143. package/dist/audit/types.d.ts.map +1 -0
  144. package/dist/audit/types.js +15 -0
  145. package/dist/audit/types.js.map +1 -0
  146. package/dist/chat-threads/store.d.ts.map +1 -1
  147. package/dist/chat-threads/store.js +10 -0
  148. package/dist/chat-threads/store.js.map +1 -1
  149. package/dist/client/AgentPanel.d.ts.map +1 -1
  150. package/dist/client/AgentPanel.js +2 -2
  151. package/dist/client/AgentPanel.js.map +1 -1
  152. package/dist/client/AssistantChat.d.ts.map +1 -1
  153. package/dist/client/AssistantChat.js +6 -1
  154. package/dist/client/AssistantChat.js.map +1 -1
  155. package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -1
  156. package/dist/client/blocks/library/FileTreeBlock.js +37 -4
  157. package/dist/client/blocks/library/FileTreeBlock.js.map +1 -1
  158. package/dist/client/chat/repo-helpers.d.ts +16 -0
  159. package/dist/client/chat/repo-helpers.d.ts.map +1 -1
  160. package/dist/client/chat/repo-helpers.js +40 -0
  161. package/dist/client/chat/repo-helpers.js.map +1 -1
  162. package/dist/db/client.d.ts.map +1 -1
  163. package/dist/db/client.js +3 -0
  164. package/dist/db/client.js.map +1 -1
  165. package/dist/db/widen-columns.d.ts +39 -0
  166. package/dist/db/widen-columns.d.ts.map +1 -0
  167. package/dist/db/widen-columns.js +73 -0
  168. package/dist/db/widen-columns.js.map +1 -0
  169. package/dist/deploy/build.d.ts +29 -0
  170. package/dist/deploy/build.d.ts.map +1 -1
  171. package/dist/deploy/build.js +85 -0
  172. package/dist/deploy/build.js.map +1 -1
  173. package/dist/deploy/workspace-deploy.d.ts.map +1 -1
  174. package/dist/deploy/workspace-deploy.js +108 -0
  175. package/dist/deploy/workspace-deploy.js.map +1 -1
  176. package/dist/mcp/build-server.d.ts.map +1 -1
  177. package/dist/mcp/build-server.js +24 -9
  178. package/dist/mcp/build-server.js.map +1 -1
  179. package/dist/oauth-tokens/store.d.ts.map +1 -1
  180. package/dist/oauth-tokens/store.js +6 -0
  181. package/dist/oauth-tokens/store.js.map +1 -1
  182. package/dist/provider-api/custom-registry.d.ts.map +1 -1
  183. package/dist/provider-api/custom-registry.js +8 -0
  184. package/dist/provider-api/custom-registry.js.map +1 -1
  185. package/dist/resources/store.d.ts.map +1 -1
  186. package/dist/resources/store.js +9 -0
  187. package/dist/resources/store.js.map +1 -1
  188. package/dist/scripts/runner.js +4 -3
  189. package/dist/scripts/runner.js.map +1 -1
  190. package/dist/server/action-discovery.d.ts.map +1 -1
  191. package/dist/server/action-discovery.js +6 -0
  192. package/dist/server/action-discovery.js.map +1 -1
  193. package/dist/server/action-routes.d.ts.map +1 -1
  194. package/dist/server/action-routes.js +1 -0
  195. package/dist/server/action-routes.js.map +1 -1
  196. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  197. package/dist/server/agent-chat-plugin.js +68 -13
  198. package/dist/server/agent-chat-plugin.js.map +1 -1
  199. package/dist/server/auth.d.ts.map +1 -1
  200. package/dist/server/auth.js +5 -0
  201. package/dist/server/auth.js.map +1 -1
  202. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  203. package/dist/server/core-routes-plugin.js +13 -0
  204. package/dist/server/core-routes-plugin.js.map +1 -1
  205. package/dist/server/security-headers.d.ts +8 -5
  206. package/dist/server/security-headers.d.ts.map +1 -1
  207. package/dist/server/security-headers.js +9 -6
  208. package/dist/server/security-headers.js.map +1 -1
  209. package/dist/settings/store.d.ts.map +1 -1
  210. package/dist/settings/store.js +6 -0
  211. package/dist/settings/store.js.map +1 -1
  212. package/dist/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
  213. package/dist/usage/store.d.ts.map +1 -1
  214. package/dist/usage/store.js +5 -0
  215. package/dist/usage/store.js.map +1 -1
  216. package/docs/content/actions.md +23 -0
  217. package/docs/content/audit-log.md +111 -0
  218. package/docs/content/multi-app-workspace.md +2 -2
  219. package/docs/design/durable-agent-runs.md +458 -4
  220. package/package.json +2 -1
  221. package/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
@@ -1,10 +1,28 @@
1
1
  # Design: Durable / Checkpointed Agent Runs
2
2
 
3
- Status: proposed
4
- Owner: core / run-manager
3
+ Status: Phase 1 + Phase 2 implemented (flagged, off by default); Phase 0 shipped;
4
+ internal per-step checkpointing (Option A core) still recommended-not-built.
5
+ Owner: core / run-manager + deploy
5
6
  Related code: `packages/core/src/agent/run-manager.ts`,
7
+ `packages/core/src/agent/durable-background.ts`,
8
+ `packages/core/src/agent/production-agent.ts`,
9
+ `packages/core/src/server/agent-chat-plugin.ts`,
10
+ `packages/core/src/deploy/build.ts`,
11
+ `packages/core/src/deploy/workspace-deploy.ts`,
6
12
  `packages/core/src/agent/engine/builder-engine.ts`
7
13
 
14
+ > **Final architecture, in one line.** Durable background runs are a
15
+ > **host-agnostic core** — the foreground turn fires an HMAC-signed _self
16
+ > dispatch_ to a sibling worker route, the worker runs the full multi-step loop
17
+ > and persists every event to SQL, and the browser streams those events through
18
+ > the existing cross-isolate SQL-poll reconnect path — with the **Netlify 15-min
19
+ > `-background` function as a per-host optimization** layered on top. The
20
+ > portable baseline works on any host that can re-invoke itself (it just
21
+ > server-chains continuations when the host budget is short); the Netlify layer
22
+ > simply lets one invocation run ~15 min so a long turn finishes in **one**
23
+ > chunk instead of many re-hydrating ones. Nothing about the baseline is
24
+ > Netlify-specific; see [Final layered architecture](#final-layered-architecture).
25
+
8
26
  ## Problem
9
27
 
10
28
  Hosted agent runs are bounded by a ~40s soft timeout enforced in
@@ -179,22 +197,34 @@ itself checkpoint).
179
197
 
180
198
  ### Phased plan
181
199
 
200
+ > Status: **Phase 0 shipped. Phase 1 + Phase 2 implemented** (the host-agnostic
201
+ > durable-background worker and the Netlify 15-min `-background` optimization),
202
+ > behind `AGENT_CHAT_DURABLE_BACKGROUND`, off by default. Internal per-step
203
+ > checkpointing (the Option A core of Phase 1) is recommended-not-yet-built; the
204
+ > worker today gets its durability from the long single invocation plus
205
+ > server-chained continuations rather than per-step idempotent checkpoints. See
206
+ > [Final layered architecture](#final-layered-architecture).
207
+
182
208
  1. **Phase 0 — Stop hitting the ceiling so often (near-term, cheapest).** Land
183
209
  the mitigations in the Tie-in below (one-call atomic primitives,
184
210
  self-documenting actions, loud termination, proof-of-done verification).
185
211
  These don't fix the ceiling but sharply cut how often multi-step loops are
186
212
  even attempted, and make the failures that remain _loud and truthful_ instead
187
213
  of silent. Capture the agent-facing half as the `reliable-mutations` skill.
214
+ _(Shipped.)_
188
215
  2. **Phase 1 — Checkpointed continuation (Option A).** Add a SQL-backed progress
189
216
  checkpoint for long operations and make their steps idempotent/resumable so
190
217
  each `auto_continue` chunk advances committed progress instead of replanning.
191
218
  Drive terminal state ("N of N", or "M of N + remainder") from the checkpoint.
192
- This is the primary reliability win.
219
+ This is the primary reliability win. _(The durable-background worker +
220
+ host-agnostic self-dispatch/SQL-event/reconnect baseline is implemented;
221
+ per-step idempotent checkpointing is still recommended-not-built.)_
193
222
  3. **Phase 2 — Durable background execution (Option B).** For operations that
194
223
  can exceed any reasonable number of continuation chunks, enqueue them onto the
195
224
  core background infrastructure, have the durable worker run them to completion
196
225
  (checkpointing internally per Phase 1), and stream truthful progress back to
197
- the foreground run and UI.
226
+ the foreground run and UI. _(Implemented: host-agnostic worker baseline +
227
+ Netlify 15-min `-background` per-host optimization, flagged off by default.)_
198
228
 
199
229
  ## Tie-in: cheaper near-term mitigations reduce, but do not replace, the fix
200
230
 
@@ -215,3 +245,427 @@ failures honest. They are valuable and should ship first (Phase 0), but the
215
245
  The agent-facing rules for these live in the `reliable-mutations` skill
216
246
  (`.agents/skills/reliable-mutations/SKILL.md`). They lower the blast radius;
217
247
  checkpointed and durable runs remove the ceiling itself.
248
+
249
+ ---
250
+
251
+ # Final layered architecture
252
+
253
+ Status: implemented (flagged, off by default)
254
+ Owner: core / run-manager + deploy
255
+
256
+ Durable background runs ship as **two layers**. Both are gated behind
257
+ `AGENT_CHAT_DURABLE_BACKGROUND` and default off; when off, the agent-chat run
258
+ path and the deploy output are byte-for-byte the pre-existing synchronous
259
+ behavior.
260
+
261
+ ### Layer 1 (portable baseline) — host-agnostic durable execution
262
+
263
+ This is the actual durability mechanism and it is **not tied to any host**:
264
+
265
+ - **Self-dispatch worker.** The foreground POST claims the run slot, inserts the
266
+ run row, and `fireInternalDispatch`es (`server/self-dispatch.ts`) an
267
+ HMAC-signed request to a sibling worker route,
268
+ `AGENT_CHAT_PROCESS_RUN_PATH = /_agent-native/agent-chat/_process-run`
269
+ (`agent/durable-background.ts`). The worker re-enters the same agent-chat
270
+ handler set as the background worker and runs the full multi-step
271
+ `runAgentLoop` (`agent/production-agent.ts`).
272
+ - **SQL event log as the transport.** The worker persists every event to
273
+ `agent_run_events` (`run-store.ts`, idempotent on `(run_id, seq)`). The
274
+ browser streams those events through the existing `subscribeToRun` →
275
+ cross-isolate SQL-poll path (`run-manager.ts subscribeFromSQL`), so the client
276
+ needs no change and reconnect/leave-and-return already works
277
+ (`GET /runs/active`, `GET /runs/:id/events?after=N`).
278
+ - **Idempotent claim + auth.** The worker claims the run with a conditional
279
+ update (`claimBackgroundRun`) so a duplicate delivery no-ops, and verifies the
280
+ HMAC token (`prepareProcessRunRequest`) exactly like the agent-teams / A2A /
281
+ webhook processors.
282
+ - **Continuation by self-chaining, not by the browser.** If a worker chunk hits
283
+ its soft-timeout unfinished, it emits `auto_continue` and **re-fires another
284
+ background dispatch** (mode `continue`) instead of bouncing back to the client.
285
+ On a host with a short invocation budget this just produces more chunks; the
286
+ run still completes, server-driven. This is what makes the baseline portable:
287
+ it degrades to chained self-dispatch on any host that can re-invoke itself.
288
+
289
+ Nothing in Layer 1 assumes Netlify, a 15-minute budget, or a particular preset.
290
+ It is the host-agnostic Option B worker with a SQL fan-in.
291
+
292
+ ### Layer 2 (per-host optimization) — Netlify 15-min `-background` function
293
+
294
+ On Netlify, Layer 1's worker invocation can be made to run for up to **15
295
+ minutes** in a single shot, eliminating almost all re-hydration (the costly
296
+ part). This is purely an optimization of _where_ Layer 1's worker runs:
297
+
298
+ - **Deploy emit (build-time gated).** When the flag is set at build time, the
299
+ deploy emits a **second** Netlify function whose name ends in `-background`,
300
+ re-exporting the **same** `main.mjs` handler bundle, with a `config.path` of
301
+ the process-run route. Netlify invokes any function whose deployed name ends in
302
+ `-background` asynchronously (202 immediately, up to 15-min budget). Single
303
+ template: `emitSingleTemplateNetlifyBackgroundFunction` (`deploy/build.ts`).
304
+ Workspace: `emitNetlifyBackgroundFunction` (`deploy/workspace-deploy.ts`),
305
+ per app with a base-path-scoped `config.path`. **When the flag is not set at
306
+ build time, neither emit runs and the single-function output is byte-identical
307
+ to today** (see [Build-time gate](#build-time-gate-byte-identical-when-off)).
308
+ - **Raised soft-timeout on that invocation only.** When the worker is running
309
+ inside the background function it calls
310
+ `startRun(..., { backgroundFunction: true })`, which lifts
311
+ `resolveRunSoftTimeoutMs`'s hosted ceiling from 40s to
312
+ `BACKGROUND_SOFT_TIMEOUT_CEILING_MS` (~13 min, ~2 min under Netlify's 15-min
313
+ hard kill) for that invocation. The interactive/foreground 40s clamp is
314
+ unchanged (see Guardrail).
315
+
316
+ If Layer 2 is absent (or its routing resolves to the synchronous function),
317
+ Layer 1 still works — the run just completes via more, shorter, server-chained
318
+ chunks. **No regression, only a missed optimization.**
319
+
320
+ ### Other hosts
321
+
322
+ Any host that can re-invoke itself runs Layer 1 unchanged (chained
323
+ self-dispatch). A host-specific Layer 2 would mirror the Netlify pattern: emit
324
+ or configure a long-lived async worker invocation and pass
325
+ `backgroundFunction: true` to `startRun` on that path. No such layer is built for
326
+ non-Netlify hosts yet; they get the portable baseline.
327
+
328
+ ## Build-time gate (byte-identical when off)
329
+
330
+ The safety-critical claim is that with `AGENT_CHAT_DURABLE_BACKGROUND` **unset at
331
+ build time**, the emitted Netlify deploy output is unchanged. How it is
332
+ guaranteed:
333
+
334
+ - The only code that produces the `-background` artifact is the two emit
335
+ functions above, and each is reached **only** through a call site guarded by
336
+ `isDurableBackgroundDeployEnabled()` (single template: `deploy/build.ts`,
337
+ inside the `preset === "netlify"` block; workspace:
338
+ `deploy/workspace-deploy.ts`, inside `copyNetlifyFunctionIntoWorkspace`).
339
+ `isDurableBackgroundDeployEnabled()` returns `false` for an unset or
340
+ non-truthy flag, so when the flag is absent the emit functions are **never
341
+ invoked** — no second function directory, no extra entry file, no extra
342
+ `config.path` route is written. The deploy walks exactly the same code path it
343
+ does today and emits exactly one function per app.
344
+ - The gate reads the flag at **build time** (in the deploy process env), so a
345
+ build produced without the flag can never contain the artifact regardless of
346
+ runtime env.
347
+ - This is covered by tests that run the real deploy path with the flag unset and
348
+ assert that no `*-agent-background` directory exists, and with the flag set and
349
+ assert that exactly one does (`deploy/workspace-deploy.spec.ts`,
350
+ `deploy/build.spec.ts`).
351
+
352
+ ---
353
+
354
+ # Netlify Layer 2: concrete implementation notes
355
+
356
+ This section details the Netlify-specific Layer 2 optimization (the deploy emit
357
+ and the 15-min budget), reusing the framework's existing background-run
358
+ machinery and Netlify's **background functions** (async, up to **15 minutes**).
359
+ It is deliberately not a from-scratch system: ~90% of the required plumbing
360
+ already ships for Agent Teams, A2A, and integration webhooks — and the
361
+ host-agnostic Layer 1 above is what actually carries the run.
362
+
363
+ ## Why a 15-min background invocation changes the math
364
+
365
+ The whole `auto_continue` / 40s soft-timeout dance exists to stay under the
366
+ **serverless function wall** (~60–65s synchronous on Netlify), not under any
367
+ model limit. Evidence:
368
+
369
+ - `builder-engine.ts:62` `MAX_LOCAL_BUILDER_GATEWAY_TIMEOUT_MS = 180_000`
370
+ (3 min) is allowed locally; the run loop has no inherent reason to stop at 40s.
371
+ - `run-manager.ts:58,68` `DEFAULT_HOSTED_RUN_SOFT_TIMEOUT_MS` /
372
+ `HOSTED_SOFT_TIMEOUT_CEILING_MS = 40_000` are pinned just under the function
373
+ wall, and `templates/brain/netlify.toml` sets `[functions."*"] timeout = 75`.
374
+
375
+ Netlify **background functions** (any function whose deployed name ends in
376
+ `-background`) are invoked asynchronously: the HTTP POST returns `202 Accepted`
377
+ immediately and the function runs detached for up to **15 minutes**. Inside that
378
+ function there is no ~60s wall, so:
379
+
380
+ - The agent loop can run for minutes in a single invocation with **few or no
381
+ `auto_continue` continuations**.
382
+ - The per-model-call gateway cap (`MAX_BUILDER_GATEWAY_TIMEOUT_MS = 45_000`,
383
+ `builder-engine.ts:60`) still applies per call — see
384
+ [Per-model-call gateway cap](#per-model-call-gateway-cap) — but the _run_ is no
385
+ longer chopped into 40s chunks.
386
+
387
+ This is exactly the Layer 1 worker with a concrete long-lived host: Netlify is
388
+ the durable worker, reached through the existing self-dispatch primitive — the
389
+ same primitive that, absent this layer, just chains shorter invocations.
390
+
391
+ ## What already exists and is reused verbatim
392
+
393
+ The Agent Teams background processor is the template. The chat path can reuse
394
+ nearly all of it:
395
+
396
+ | Capability | Existing code | Reuse for chat |
397
+ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
398
+ | Fire a fresh function invocation with its own budget | `server/self-dispatch.ts:122` `fireInternalDispatch()` (HMAC-signed POST, 250ms settle race) | Point it at the `-background` function path instead of the same-function processor route |
399
+ | Processor route pattern (claim → run full loop → persist → finalize) | `agent-teams.ts` `processAgentTeamRun()` + route at `agent-chat-plugin.ts:6001` (`AGENT_TEAM_PROCESS_RUN_PATH = /_agent-native/agent-teams/_process-run`) | Add a sibling `/_agent-native/agent-chat/_process-run` that runs the **chat** loop |
400
+ | HMAC processor auth | `integrations/internal-token.ts:61,72` `signInternalToken` / `verifyInternalToken` (5-min TTL, task-id-bound, timing-safe); gated by `A2A_SECRET` | Same — sign with `runId` |
401
+ | Atomic single-claim (no double-processing) | `agent-teams-run-queue.ts:177` `claimAgentTeamRun()` (`UPDATE … WHERE status='queued' OR (status='running' AND updated_at < stuckCutoff)`) | Same shape; the chat already has an equivalent with `updateRunStatusIfRunning()` (`run-store.ts:392`, `UPDATE … WHERE id=? AND status='running'`) and `tryClaimRunSlot(threadId)` |
402
+ | The actual multi-step agent loop | `runAgentLoop()` (imported into both `production-agent.ts` and `agent-teams.ts:1790`) | Identical — call it from the background processor |
403
+ | Event persistence (SQL, ordered, idempotent) | `run-store.ts`: `insertRunEvent(runId, seq, json)` with `ON CONFLICT (run_id, seq) DO NOTHING` (`:452,466`); `getRunEventsSince(runId, fromSeq)` (`:471`) | Unchanged — background worker writes the same `agent_run_events` rows |
404
+ | Client reconnect / replay by cursor | `agent-chat-plugin.ts:7103` `GET /runs/:id/events?after=N` → `subscribeToRun(runId, after)`; cross-isolate SQL polling path `run-manager.ts:750` `subscribeFromSQL()` (polls `getRunEventsSince` every 500ms) | **This is the key reuse**: the background worker is a different isolate, so the client already falls through to the SQL-polling subscription. Reconnect already works across isolates. |
405
+ | Discover an in-flight run on reload | `agent-chat-plugin.ts:7130` `GET /runs/active?threadId=X` (returns `runId`, `turnId`, `status`, `heartbeatAt`, `lastProgressAt`, `serverNow`) | Unchanged |
406
+ | Heartbeat + stale reaper | `run-manager.ts:340` (1.5s heartbeat), `run-store.ts:19` `RUN_STALE_MS = 15_000`, `reapIfStale` / `reapAllStaleRuns` | Reused; see [Failure handling](#failure-handling-and-loud-terminal-state) for the 15s-vs-15min tension |
407
+ | Stuck-dispatch re-fire / reconcile | `agent-teams.ts:689` refire, `reconcileAgentTeamRunsForOwner` (`:797`) | Optional reuse for chat reconcile |
408
+
409
+ The single genuinely **new** infrastructure piece is **deploy-time function
410
+ splitting**: today the Nitro `netlify` preset emits exactly one function
411
+ (`.netlify/functions-internal/server/` → `server.mjs` re-exporting `main.mjs`,
412
+ patched in `workspace-deploy.ts:670` `patchNetlifyFunctionEntry`; routed via the
413
+ `config.path` array at `:742`). We must additionally emit a second function whose
414
+ name ends in `-background` that re-exports the **same** `main.mjs` handler.
415
+
416
+ ## The dispatch flow (designed)
417
+
418
+ ```
419
+ ┌── Browser ─────────────────────────────────────────────────────────────┐
420
+ │ 1. POST /_agent-native/agent-chat { message, threadId, turnId } │
421
+ └────────────────────────────────────────────────────────────────────────┘
422
+ │ (foreground function: the normal interactive handler)
423
+
424
+ ┌── agent-chat handler (production-agent.ts) ────────────────────────────┐
425
+ │ 2. tryClaimRunSlot(threadId) → 409 if a run is already active │
426
+ │ 3. runId = generateRunId(); insertRun(runId, threadId, turnId) │
427
+ │ (status='running', heartbeat set) — run-store.ts:233 │
428
+ │ 4. IF durable-background enabled (hosted + flag + A2A_SECRET): │
429
+ │ fireInternalDispatch({ │
430
+ │ path: BACKGROUND_FUNCTION_INVOKE_PATH, // 202, detached │
431
+ │ taskId: runId, body: { threadId, turnId, message, … } }) │
432
+ │ return SSE stream = subscribeToRun(runId, 0) // immediately │
433
+ │ ELSE (local / flag off): │
434
+ │ startRun(runId, …) inline (today's behavior, unchanged) │
435
+ └────────────────────────────────────────────────────────────────────────┘
436
+ │ 202 leaves the box (250ms settle race)
437
+
438
+ ┌── Netlify background function (…-background, up to 15 min) ───────────┐
439
+ │ 5. Route: POST /_agent-native/agent-chat/_process-run │
440
+ │ 6. verifyInternalToken(runId, bearer) // internal-token.ts:72 │
441
+ │ 7. atomically claim runId (updateRunStatusIfRunning-style guard) │
442
+ │ 8. raise gateway cap for this invocation (see §gateway-cap) │
443
+ │ 9. startRun(runId, …, { softTimeoutMs: 0 OR ~13min }) │
444
+ │ → runAgentLoop(...) runs the FULL multi-step turn │
445
+ │ → emitRunEvent(...) persists every event to agent_run_events (SQL) │
446
+ │ 10. on finish: updateRunStatusIfRunning(runId,'completed'|'errored') │
447
+ │ + terminal event ('done'/'error') persisted │
448
+ └────────────────────────────────────────────────────────────────────────┘
449
+ ▲ writes SQL events
450
+ │ reads SQL events (cross-isolate)
451
+ ┌── Browser (same SSE response from step 4, OR reconnect) ──────────────┐
452
+ │ 11. The SSE stream is subscribeToRun(runId,0). Because the producer is │
453
+ │ a *different* isolate, it serves via subscribeFromSQL() — │
454
+ │ run-manager.ts:750 — polling getRunEventsSince every 500ms. │
455
+ │ 12. On disconnect/reload: GET /runs/active?threadId → runId+lastSeq, │
456
+ │ then GET /runs/:id/events?after=lastSeq resumes the same stream. │
457
+ └────────────────────────────────────────────────────────────────────────┘
458
+ ```
459
+
460
+ ### Endpoints / constants to add
461
+
462
+ - `BACKGROUND_FUNCTION_INVOKE_PATH` — the Netlify async path,
463
+ `/.netlify/functions/<app>-agent-background` (Netlify maps `…-background` to
464
+ async/202). The dispatch URL is built with `resolveSelfDispatchBaseUrl(event)`
465
+ (`self-dispatch.ts:59`) + that path. (Alternatively, give the background
466
+ function a `config.path` like `/_agent-native/_bg/*` so dispatch stays a clean
467
+ framework path and Netlify still routes it to the `-background` function and
468
+ invokes it async.)
469
+ - `AGENT_CHAT_PROCESS_RUN_PATH = "/_agent-native/agent-chat/_process-run"` — the
470
+ handler the background function actually runs (sibling to
471
+ `AGENT_TEAM_PROCESS_RUN_PATH`). It is reached _through_ the background
472
+ function, so it inherits the 15-min budget.
473
+ - Feature flag: `AGENT_CHAT_DURABLE_BACKGROUND` (env or per-app config),
474
+ defaulting **off**, gated additionally on `isHostedRuntime()` and
475
+ `hasConfiguredA2ASecret()`. Local dev keeps the inline path so SSE stays a
476
+ single live stream and no second function is needed.
477
+
478
+ ## Where the soft-timeout / auto_continue logic changes
479
+
480
+ Today (`run-manager.ts:344`) every hosted run gets a 40s soft timeout that emits
481
+ `auto_continue` and the client re-POSTs. In the background-function path:
482
+
483
+ - **The background `startRun` gets `softTimeoutMs` ≈ 13 min** (a margin under
484
+ Netlify's 15-min hard kill), not 40s. To allow this, `resolveRunSoftTimeoutMs`
485
+ must learn a **background context**: the `HOSTED_SOFT_TIMEOUT_CEILING_MS = 40_000`
486
+ clamp (`run-manager.ts:170`) currently _defeats_ any larger value on hosted.
487
+ Add a `backgroundFunction: true` option that raises the ceiling to ~`780_000`
488
+ (13 min) for that one invocation. **Do not** change the default hosted ceiling
489
+ — the 40s clamp stays correct for the interactive/foreground path; the
490
+ Guardrail above still holds for non-background runs.
491
+ - **`auto_continue` becomes the rare exception, not the rule.** Most turns finish
492
+ inside 13 min with zero continuations, killing the re-hydration thrash
493
+ described in the Problem section. If a turn _does_ exceed 13 min, the existing
494
+ mechanism still works: emit `auto_continue` and re-fire **another background
495
+ dispatch** (mode `continue`) exactly as `agent-teams.ts:1886` does — i.e. the
496
+ continuation chains background invocations instead of bouncing back to the
497
+ browser. The client never has to drive continuation.
498
+ - **Internally, the worker should still checkpoint (Option A).** 15 min is large
499
+ but not infinite; combining background execution with idempotent/checkpointed
500
+ steps means even a continued run advances monotonically. Option B is strongest
501
+ containing Option A.
502
+
503
+ ## How the client UX changes (SSE → SSE-over-SQL + reconnect)
504
+
505
+ Minimal client change, because the reconnect machinery already exists:
506
+
507
+ - **The response shape is unchanged.** Step 4 still returns an SSE
508
+ `ReadableStream` from `subscribeToRun(runId, 0)`. The client keeps reading SSE
509
+ exactly as today. The only difference is that the events are produced in
510
+ another isolate and arrive via the 500ms SQL-poll path
511
+ (`subscribeFromSQL`, `run-manager.ts:750`) instead of the in-memory fast path.
512
+ Latency goes from ~instant to ≤500ms per event — acceptable for chat.
513
+ - **Reconnect/leave-and-return becomes a first-class, reliable flow.** Because
514
+ the producer is detached on Netlify, closing the tab no longer kills the run.
515
+ On return, the client calls `GET /runs/active?threadId` (`:7130`) → gets
516
+ `runId` + status, then `GET /runs/:id/events?after=<lastSeq>` (`:7103`) to
517
+ replay from the cursor. This already works; we are just making it the primary
518
+ UX. Recommend the client persist `lastSeq` per thread so reconnect resumes
519
+ precisely.
520
+ - **Optional: drop the long-held foreground SSE entirely and poll.** Instead of
521
+ holding step-4's SSE open against the 75s foreground-function `timeout`
522
+ (`netlify.toml`), the foreground POST can return `{ runId, turnId }` (202-style
523
+ JSON) and the client immediately opens `GET /runs/:id/events?after=0`. This
524
+ avoids tying up a foreground function for the run's lifetime. Either works;
525
+ the JSON-then-poll variant is cleaner on Netlify because the interactive
526
+ function returns in well under 75s.
527
+
528
+ ## Per-model-call gateway cap
529
+
530
+ Even in a 15-min function, a _single_ model call is still capped by the Builder
531
+ gateway: `MAX_BUILDER_GATEWAY_TIMEOUT_MS = 45_000` (`builder-engine.ts:60`),
532
+ resolved by `getBuilderGatewayTimeoutMs()` (`:770`) /
533
+ `resolveMaxBuilderGatewayTimeoutMs()` (`:758`). Locally the cap is already
534
+ `180_000` (`:62`) when the gateway base URL is localhost.
535
+
536
+ Options, in scope-order:
537
+
538
+ 1. **In scope / safe now:** keep the 45s per-call cap. A 15-min function makes
539
+ _many_ 45s calls; the cap bounds one call, not the run. This alone delivers
540
+ the goal (long multi-step runs finish) with **zero gateway changes**.
541
+ 2. **Flag, out of scope until confirmed:** raise the per-call cap toward 180s in
542
+ the background context. `getBuilderGatewayTimeoutMs()` already honors
543
+ `AGENT_NATIVE_BUILDER_GATEWAY_TIMEOUT_MS`, but it is hard-clamped to
544
+ `resolveMaxBuilderGatewayTimeoutMs()` = 45s for non-localhost gateways. Raising
545
+ it requires the **hosted Builder gateway to actually allow >45s upstream** —
546
+ unverified, and not the framework's call. Until confirmed, do not raise it.
547
+ 3. **Escape hatch:** a direct Anthropic engine (no Builder gateway) has no 45s
548
+ cap, so a single very long call could use it. Out of scope for this change;
549
+ note it as the lever if a single model call truly needs >45s.
550
+
551
+ **Recommendation:** ship with option 1. The win is removing the _run-level_
552
+ ceiling; the per-call cap is orthogonal and rarely the binding constraint.
553
+
554
+ ## Idempotency / dedup
555
+
556
+ Already strong; make the new claim match:
557
+
558
+ - **Run claim.** The foreground inserts the run row (`insertRun`,
559
+ `run-store.ts:233`) _before_ dispatching. The background processor must claim
560
+ it with a conditional update (mirror `updateRunStatusIfRunning`,
561
+ `run-store.ts:392`, or add a `claimRunForProcessing(runId)` that flips a
562
+ `processing` marker only from the unclaimed state). A duplicate Netlify
563
+ delivery (background functions can in theory be retried) then no-ops on the
564
+ second claim, exactly like `claimAgentTeamRun` returning `null`
565
+ (`agent-teams-run-queue.ts:177`).
566
+ - **Event dedup.** `insertRunEvent` is already idempotent on `(run_id, seq)`
567
+ via `ON CONFLICT … DO NOTHING` (`run-store.ts:452,466`). Re-emitting an event
568
+ with the same seq is a safe no-op, so a retried/overlapping producer cannot
569
+ duplicate the stream.
570
+ - **Dispatch token.** `fireInternalDispatch` signs `runId`
571
+ (`self-dispatch.ts:131`); the processor verifies it (`internal-token.ts:72`,
572
+ 5-min TTL). A stale or forged dispatch is rejected.
573
+
574
+ ## Failure handling and loud terminal state
575
+
576
+ - **15s heartbeat vs 15-min runs — the one real conflict.** The stale reaper
577
+ (`RUN_STALE_MS = 15_000`, `run-store.ts:19`; `reapIfStale`/`reapAllStaleRuns`)
578
+ marks a run `errored` after 15s without a heartbeat. The background worker
579
+ _does_ run `startRun`'s 1.5s heartbeat (`run-manager.ts:340`), so as long as
580
+ the worker is alive the row stays fresh and the reaper leaves it alone — this
581
+ is fine. The risk is a worker that is _slow to start_ (Netlify cold-start of
582
+ the background function) leaving a freshly-inserted `running` row unheartbeaten
583
+ for >15s, which the reaper would falsely kill. Mitigation: the foreground
584
+ insert sets `heartbeat_at = now` (`insertRun`), and the background claim should
585
+ bump the heartbeat immediately on entry; if cold starts can exceed 15s, widen
586
+ `RUN_STALE_MS` for rows known to be background-dispatched (e.g. a
587
+ `dispatch_mode` column or a separate, larger stale window for background runs).
588
+ - **Worker dies mid-run (crash / 15-min kill).** Heartbeat stops, the reaper
589
+ flips the row to `errored` and appends a synthetic terminal event
590
+ (`reapIfStale`), so the client's SQL-poll subscription sees a terminal event
591
+ and stops — no infinite spinner. This is the **loud terminal** contract from
592
+ Phase 0 already wired.
593
+ - **Dispatch never lands (202 lost).** Reuse the Agent-Teams reconcile pattern
594
+ (`reconcileAgentTeamRunsForOwner`, `agent-teams.ts:797`): if a `running` row
595
+ has no heartbeat after a grace window, re-fire the dispatch once, then fail
596
+ loud. Optional for the first slice.
597
+ - **Truthful terminal state.** Combined with the `reliable-mutations` Phase-0
598
+ proof-of-done discipline, a completed background run reports concrete proof;
599
+ a killed one reports `errored` with what was committed — never a false success.
600
+
601
+ ## Phased implementation plan (smallest working slice first)
602
+
603
+ > Status: \*\*Slices 0–1 implemented and the Slice-3 background-aware stale window
604
+ >
605
+ > - background→background continuation chaining are implemented\*\*, all behind
606
+ > `AGENT_CHAT_DURABLE_BACKGROUND` (off by default). The host-agnostic baseline
607
+ > (Layer 1) carries the run on any host; the Netlify `-background` emit (Layer 2)
608
+ > is the deploy-time optimization. Slice 2's richer reconnect-first client UX and
609
+ > the internal per-step checkpointing (Option A) remain follow-ups; Slice 4
610
+ > (raising the per-call gateway cap) is intentionally out of scope (see
611
+ > [Per-model-call gateway cap](#per-model-call-gateway-cap)).
612
+
613
+ **Slice 0 — prove async dispatch on Netlify (no chat yet).**
614
+ Emit one extra `-background` function in the deploy build that re-exports the
615
+ existing `main.mjs` handler (extend `patchNetlifyFunctionEntry`,
616
+ `workspace-deploy.ts:670`, and the single-template Netlify output in
617
+ `deploy/build.ts:1935`). Add `AGENT_CHAT_PROCESS_RUN_PATH` returning a stub that
618
+ just writes a run event. Dispatch to it from a temporary test route via
619
+ `fireInternalDispatch`. **Done when:** a POST returns 202 immediately and the
620
+ stub writes an event to `agent_run_events` from the background function, visible
621
+ via `GET /runs/:id/events`. This de-risks the only genuinely new infra.
622
+
623
+ **Slice 1 — route the real chat loop through the background function (flagged).**
624
+ Behind `AGENT_CHAT_DURABLE_BACKGROUND` (off by default; hosted + `A2A_SECRET`
625
+ only): foreground handler inserts the run row, dispatches, returns the SSE
626
+ stream from `subscribeToRun`. The background `_process-run` claims the run and
627
+ calls the same `startRun` + `runAgentLoop` the inline path uses, with
628
+ `softTimeoutMs ≈ 13min` (new `backgroundFunction` option in
629
+ `resolveRunSoftTimeoutMs`). Per-call gateway cap stays at 45s (option 1).
630
+ **Done when:** a long multi-step turn that thrashes today completes in one
631
+ background invocation with zero `auto_continue`, events streaming to the client
632
+ via the SQL-poll path, terminal `done` persisted.
633
+
634
+ **Slice 2 — reconnect/leave-and-return UX.**
635
+ Make the client persist `lastSeq` per thread and, on load, use
636
+ `/runs/active` + `/runs/:id/events?after=lastSeq` as the primary resume path.
637
+ Optionally switch the foreground POST to return `{ runId, turnId }` JSON instead
638
+ of holding SSE, then poll. **Done when:** closing/reopening the tab mid-run
639
+ resumes the live stream with no lost or duplicated events.
640
+
641
+ **Slice 3 — robustness.**
642
+ Background-aware stale window (cold-start tolerance), reconcile/re-fire for lost
643
+ dispatches, and background→background `auto_continue` chaining for the rare
644
+
645
+ > 13-min turn (mirror `agent-teams.ts:1886`). Internal checkpointing (Option A)
646
+ > for monotonic progress across any continuation.
647
+
648
+ **Slice 4 (optional, separate decision) — raise the per-call gateway cap.**
649
+ Only after confirming the hosted Builder gateway accepts >45s upstream; wire
650
+ `AGENT_NATIVE_BUILDER_GATEWAY_TIMEOUT_MS` through with a background-context max.
651
+ Out of scope until that confirmation exists.
652
+
653
+ ## Open risks / unknowns
654
+
655
+ 1. **Netlify background-function invocation contract.** Confirm the exact
656
+ trigger: name-suffix `-background` invoked at `/.netlify/functions/<name>`
657
+ returning 202, and whether a `config.path` route can also mark a function
658
+ background. The deploy currently emits a single function — splitting it is the
659
+ main new work and must not regress the existing `config.path` routing
660
+ (`workspace-deploy.ts:742`).
661
+ 2. **Cold-start vs 15s stale reaper** (detailed above) — the highest-likelihood
662
+ false-failure; needs the background-aware stale window or an on-entry
663
+ heartbeat bump.
664
+ 3. **Hosted gateway >45s** — unknown whether upstream allows it; gates Slice 4.
665
+ 4. **Two functions sharing one bundle** — both re-export the same `main.mjs`, so
666
+ `includedFiles: ["**"]` (`workspace-deploy.ts:747`) must cover the background
667
+ function dir too; verify bundle size and that env (`A2A_SECRET`, DB URL) is
668
+ present in the background function's environment.
669
+ 5. **Cost / concurrency** — background functions are billed and concurrency-
670
+ limited differently; long runs hold a slot for minutes. Out of scope to
671
+ solve, but flag for capacity planning.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.70.3",
3
+ "version": "0.72.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -97,6 +97,7 @@
97
97
  "./org": "./dist/org/index.js",
98
98
  "./client/org": "./dist/client/org/index.js",
99
99
  "./client/db-admin": "./dist/client/db-admin/index.js",
100
+ "./audit": "./dist/audit/index.js",
100
101
  "./sharing": "./dist/sharing/index.js",
101
102
  "./sharing/actions/share-resource": "./dist/sharing/actions/share-resource.js",
102
103
  "./sharing/actions/unshare-resource": "./dist/sharing/actions/unshare-resource.js",
@@ -7,6 +7,8 @@ import {
7
7
  normalizeActionChatUIConfig,
8
8
  type ActionChatUIConfig,
9
9
  } from "./action-ui.js";
10
+ import type { ActionAuditConfig } from "./audit/types.js";
11
+ import { normalizeAuditConfig, resolveAuditAttach } from "./audit/config.js";
10
12
  import type { StandardSchemaV1 } from "@standard-schema/spec";
11
13
 
12
14
  /**
@@ -75,6 +77,21 @@ export interface ActionRunContext {
75
77
  * attaching an `"abort"` listener is always safe.
76
78
  */
77
79
  signal?: AbortSignal;
80
+ /**
81
+ * Name of the action being invoked (the registry key, e.g.
82
+ * `delete-recording`). Set at each dispatch site so cross-cutting concerns —
83
+ * notably the audit log — can attribute the call. `undefined` for direct
84
+ * programmatic `run()` calls that bypass the dispatcher.
85
+ */
86
+ actionName?: string;
87
+ /**
88
+ * Agent conversation thread + turn that triggered this call, populated only
89
+ * inside the agent tool loop (`caller: "tool"`). Lets the audit log link a
90
+ * mutation to the specific agent run/turn that caused it. `undefined` on
91
+ * every human/programmatic surface.
92
+ */
93
+ threadId?: string;
94
+ turnId?: string;
78
95
  }
79
96
 
80
97
  export interface AgentActionStopOptions {
@@ -378,6 +395,14 @@ interface DefineActionWithSchema<
378
395
  args: StandardSchemaV1.InferOutput<TSchema>,
379
396
  ctx?: ActionRunContext,
380
397
  ) => boolean | Promise<boolean>);
398
+ /**
399
+ * Audit-log configuration. **Default-on for mutating actions** — you only
400
+ * need this to tune capture: declare the mutated `target` (so the change
401
+ * shows up in the owner's audit trail) and/or a `summary`, opt a read-only
402
+ * action in via `onRead`, or opt a noisy action out via `enabled: false`.
403
+ * See the `audit-log` skill.
404
+ */
405
+ audit?: ActionAuditConfig;
381
406
  }
382
407
 
383
408
  // ---------------------------------------------------------------------------
@@ -443,6 +468,9 @@ interface DefineActionWithParams<
443
468
  args: InferParams<TParams>,
444
469
  ctx?: ActionRunContext,
445
470
  ) => boolean | Promise<boolean>);
471
+ /** Audit-log configuration (default-on for mutations). See the schema
472
+ * overload above and the `audit-log` skill. */
473
+ audit?: ActionAuditConfig;
446
474
  }
447
475
 
448
476
  // ---------------------------------------------------------------------------
@@ -497,6 +525,10 @@ export interface ActionDefinition<TInput, TReturn> {
497
525
  readonly needsApproval?:
498
526
  | boolean
499
527
  | ((args: TInput, ctx?: ActionRunContext) => boolean | Promise<boolean>);
528
+ /** Resolved audit-log configuration. Present only when the caller passed
529
+ * `audit`. The audit capture wrapper is baked into `run`; this field is for
530
+ * introspection. */
531
+ readonly audit?: ActionAuditConfig;
500
532
  }
501
533
 
502
534
  // ---------------------------------------------------------------------------
@@ -617,6 +649,16 @@ export function defineAction(options: any) {
617
649
  ? true
618
650
  : undefined;
619
651
 
652
+ // Audit: wrap the validated run so every mutating call records an audit
653
+ // event (who/what/when/from-where, and for the agent which run). Default-on
654
+ // for mutations; read-only actions opt in via `audit.onRead`. The wrapper
655
+ // lazily imports the DB-touching recorder so `action.ts` keeps no static DB
656
+ // dependency.
657
+ const auditConfig = normalizeAuditConfig(options.audit);
658
+ const finalRun = resolveAuditAttach(auditConfig, readOnly)
659
+ ? wrapRunWithAudit(run, auditConfig)
660
+ : run;
661
+
620
662
  // toolCallable: thread through whatever the caller declared. We DO NOT
621
663
  // default to `true` here — the absence of an explicit field is meaningful
622
664
  // to the tools bridge: it lets us emit a one-shot warning when an action
@@ -675,7 +717,7 @@ export function defineAction(options: any) {
675
717
  description: options.description,
676
718
  parameters: toolParameters,
677
719
  },
678
- run,
720
+ run: finalRun,
679
721
  ...(hasSchema ? { schema: options.schema } : {}),
680
722
  ...(options.http !== undefined ? { http: options.http } : {}),
681
723
  ...(typeof options.requiresAuth === "boolean"
@@ -702,6 +744,43 @@ export function defineAction(options: any) {
702
744
  typeof options.needsApproval === "function"
703
745
  ? { needsApproval: options.needsApproval }
704
746
  : {}),
747
+ ...(auditConfig ? { audit: auditConfig } : {}),
748
+ };
749
+ }
750
+
751
+ /**
752
+ * Wrap an action's (already input/output-validated) run so each call records an
753
+ * audit event after it resolves — on success and on error. Best-effort: the
754
+ * recorder swallows its own failures and the original result/throw is always
755
+ * preserved, so auditing can never change an action's behavior. The DB-touching
756
+ * recorder is imported lazily so merely defining an action pulls in no DB code.
757
+ */
758
+ function wrapRunWithAudit(
759
+ run: (args: any, ctx?: ActionRunContext) => any,
760
+ auditConfig: ActionAuditConfig | undefined,
761
+ ): (args: any, ctx?: ActionRunContext) => Promise<any> {
762
+ return async function auditedRun(args: any, ctx?: ActionRunContext) {
763
+ let result: any;
764
+ let error: unknown;
765
+ let threw = false;
766
+ try {
767
+ result = await run(args, ctx);
768
+ } catch (err) {
769
+ error = err;
770
+ threw = true;
771
+ }
772
+ try {
773
+ const { recordActionAudit } = await import("./audit/record.js");
774
+ await recordActionAudit(
775
+ threw
776
+ ? { config: auditConfig, args, ctx, status: "error", error }
777
+ : { config: auditConfig, args, ctx, status: "success", result },
778
+ );
779
+ } catch {
780
+ // Recorder failed to load/run — never affect the action.
781
+ }
782
+ if (threw) throw error;
783
+ return result;
705
784
  };
706
785
  }
707
786