@nylorun/runtime 0.13.0-beta → 0.15.0-beta

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 (309) hide show
  1. package/CHANGELOG.md +434 -0
  2. package/README.md +137 -41
  3. package/dist/adapters/execution/restate.d.ts +7 -2
  4. package/dist/adapters/execution/restate.js +40 -10
  5. package/dist/adapters/replication/pgoutput.d.ts +21 -0
  6. package/dist/adapters/replication/pgoutput.js +170 -0
  7. package/dist/adapters/streams/s2.js +37 -20
  8. package/dist/admin-openapi.json +570 -0
  9. package/dist/api/a2a/card.d.ts +15 -0
  10. package/dist/api/a2a/card.js +24 -0
  11. package/dist/api/a2a/endpoint.d.ts +8 -0
  12. package/dist/api/a2a/endpoint.js +81 -0
  13. package/dist/api/a2a/protocol.d.ts +144 -0
  14. package/dist/api/a2a/protocol.js +188 -0
  15. package/dist/api/a2a/routes.d.ts +19 -0
  16. package/dist/api/a2a/routes.js +289 -0
  17. package/dist/api/a2a/tasks.d.ts +50 -0
  18. package/dist/api/a2a/tasks.js +186 -0
  19. package/dist/api/ag-ui/endpoint.d.ts +8 -0
  20. package/dist/api/ag-ui/endpoint.js +126 -0
  21. package/dist/api/ag-ui/history.d.ts +7 -0
  22. package/dist/api/ag-ui/history.js +60 -0
  23. package/dist/api/ag-ui/routes.d.ts +32 -0
  24. package/dist/api/ag-ui/routes.js +305 -0
  25. package/dist/api/ag-ui/session-id.d.ts +1 -0
  26. package/dist/api/ag-ui/session-id.js +12 -0
  27. package/dist/api/ag-ui/sse.d.ts +9 -0
  28. package/dist/api/ag-ui/sse.js +7 -0
  29. package/dist/api/ag-ui/translate.d.ts +38 -0
  30. package/dist/api/ag-ui/translate.js +222 -0
  31. package/dist/api/components.d.ts +8065 -0
  32. package/dist/api/components.js +82 -0
  33. package/dist/api/gate/keys.d.ts +14 -0
  34. package/dist/api/gate/keys.js +59 -0
  35. package/dist/api/gate/routes.d.ts +34 -0
  36. package/dist/api/gate/routes.js +235 -0
  37. package/dist/api/http/app.d.ts +18 -0
  38. package/dist/api/http/app.js +58 -0
  39. package/dist/api/http/body.d.ts +4 -0
  40. package/dist/api/http/body.js +37 -0
  41. package/dist/api/http/define.d.ts +39 -0
  42. package/dist/api/http/define.js +122 -0
  43. package/dist/api/http/respond.d.ts +9 -0
  44. package/dist/api/http/respond.js +44 -0
  45. package/dist/api/http/routes/access.d.ts +24 -0
  46. package/dist/api/http/routes/access.js +266 -0
  47. package/dist/api/http/routes/actions.d.ts +8 -0
  48. package/dist/api/http/routes/actions.js +76 -0
  49. package/dist/api/http/routes/endpoints.d.ts +7 -0
  50. package/dist/api/http/routes/endpoints.js +100 -0
  51. package/dist/api/http/routes/sessions.d.ts +3 -0
  52. package/dist/api/http/routes/sessions.js +205 -0
  53. package/dist/api/http/routes/tenant.d.ts +11 -0
  54. package/dist/api/http/routes/tenant.js +244 -0
  55. package/dist/api/http/routes/vaults.d.ts +8 -0
  56. package/dist/api/http/routes/vaults.js +167 -0
  57. package/dist/api/openapi.d.ts +19 -0
  58. package/dist/api/openapi.js +137 -0
  59. package/dist/api/route.d.ts +12 -0
  60. package/dist/api/route.js +4 -0
  61. package/dist/core/bearer.d.ts +8 -0
  62. package/dist/core/bearer.js +11 -0
  63. package/dist/core/flow-host.d.ts +7 -15
  64. package/dist/core/flow-host.js +16 -41
  65. package/dist/core/sandbox-routes.d.ts +12 -7
  66. package/dist/core/sandbox-routes.js +24 -25
  67. package/dist/execution/memory.d.ts +8 -5
  68. package/dist/execution/memory.js +33 -13
  69. package/dist/execution/types.d.ts +38 -6
  70. package/dist/execution/types.js +12 -3
  71. package/dist/gates/contract.d.ts +51 -0
  72. package/dist/gates/contract.js +37 -0
  73. package/dist/gates/handler.d.ts +21 -0
  74. package/dist/gates/handler.js +55 -0
  75. package/dist/gates/http-client.d.ts +12 -0
  76. package/dist/gates/http-client.js +160 -0
  77. package/dist/gates/in-process.d.ts +32 -0
  78. package/dist/gates/in-process.js +42 -0
  79. package/dist/gates/inflight.d.ts +39 -0
  80. package/dist/gates/inflight.js +70 -0
  81. package/dist/gates/mcp-handler.d.ts +35 -0
  82. package/dist/gates/mcp-handler.js +142 -0
  83. package/dist/gates/meter.d.ts +24 -0
  84. package/dist/gates/meter.js +165 -0
  85. package/dist/gates/model-gate.d.ts +45 -0
  86. package/dist/gates/model-gate.js +1 -0
  87. package/dist/gates/tenant-vaults.d.ts +64 -0
  88. package/dist/gates/tenant-vaults.js +94 -0
  89. package/dist/gates/tool-calls.d.ts +51 -0
  90. package/dist/gates/tool-calls.js +64 -0
  91. package/dist/gates/tool-client.d.ts +12 -0
  92. package/dist/gates/tool-client.js +201 -0
  93. package/dist/gates/tool-contract.d.ts +116 -0
  94. package/dist/gates/tool-contract.js +64 -0
  95. package/dist/gates/tool-gate.d.ts +45 -0
  96. package/dist/gates/tool-gate.js +10 -0
  97. package/dist/host/admin-api.d.ts +23 -0
  98. package/dist/host/admin-api.js +74 -0
  99. package/dist/host/app.d.ts +34 -0
  100. package/dist/host/app.js +181 -0
  101. package/dist/host/config-for.d.ts +3 -1
  102. package/dist/host/config-for.js +3 -2
  103. package/dist/host/config.d.ts +11 -0
  104. package/dist/host/cors.d.ts +27 -0
  105. package/dist/host/cors.js +64 -0
  106. package/dist/host/create-host.d.ts +36 -8
  107. package/dist/host/create-host.js +161 -330
  108. package/dist/host/execution.d.ts +20 -18
  109. package/dist/host/execution.js +2 -2
  110. package/dist/host/gates.d.ts +58 -0
  111. package/dist/host/gates.js +127 -0
  112. package/dist/host/http.d.ts +20 -4
  113. package/dist/host/http.js +81 -23
  114. package/dist/host/index.d.ts +1 -1
  115. package/dist/host/main.js +189 -58
  116. package/dist/host/stack-config.d.ts +91 -19
  117. package/dist/host/stack-config.js +246 -17
  118. package/dist/infra/database.d.ts +2 -2
  119. package/dist/infra/execution.d.ts +5 -5
  120. package/dist/infra/execution.js +8 -6
  121. package/dist/infra/index.d.ts +1 -1
  122. package/dist/infra/workers.d.ts +17 -17
  123. package/dist/infra/workers.js +9 -9
  124. package/dist/keys/client.d.ts +9 -0
  125. package/dist/keys/client.js +70 -0
  126. package/dist/keys/contract.d.ts +24 -0
  127. package/dist/keys/contract.js +12 -0
  128. package/dist/keys/keys.d.ts +41 -0
  129. package/dist/keys/keys.js +50 -0
  130. package/dist/mcp/connect.d.ts +43 -3
  131. package/dist/mcp/connect.js +12 -5
  132. package/dist/mcp/pool.d.ts +39 -3
  133. package/dist/mcp/pool.js +74 -11
  134. package/dist/model/catalog.d.ts +2 -10
  135. package/dist/model/classify.d.ts +13 -0
  136. package/dist/model/classify.js +61 -0
  137. package/dist/model/models.d.ts +10 -1
  138. package/dist/model/models.js +14 -6
  139. package/dist/model/pi-model.d.ts +17 -2
  140. package/dist/model/pi-model.js +366 -222
  141. package/dist/openapi.json +25889 -0
  142. package/dist/record/append.d.ts +51 -0
  143. package/dist/record/append.js +20 -0
  144. package/dist/record/envelope.d.ts +19 -0
  145. package/dist/record/envelope.js +49 -0
  146. package/dist/record/index.d.ts +11 -0
  147. package/dist/record/index.js +11 -0
  148. package/dist/sandbox/manager.d.ts +13 -1
  149. package/dist/sandbox/manager.js +8 -11
  150. package/dist/sandbox/resolve.d.ts +33 -0
  151. package/dist/sandbox/resolve.js +81 -0
  152. package/dist/sandbox/session-sandbox.d.ts +11 -0
  153. package/dist/sandbox/session-sandbox.js +51 -0
  154. package/dist/sandbox/share.d.ts +14 -2
  155. package/dist/sandbox/share.js +33 -3
  156. package/dist/sandbox/tenant-config.d.ts +37 -0
  157. package/dist/sandbox/tenant-config.js +51 -0
  158. package/dist/sandbox/tools.d.ts +7 -9
  159. package/dist/sandbox/tools.js +11 -3
  160. package/dist/sandbox/types.d.ts +8 -2
  161. package/dist/store/postgres/connect.d.ts +25 -9
  162. package/dist/store/postgres/connect.js +27 -10
  163. package/dist/store/postgres/db.d.ts +16 -0
  164. package/dist/store/postgres/db.js +28 -0
  165. package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
  166. package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
  167. package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
  168. package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
  169. package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
  170. package/dist/store/postgres/migrate.d.ts +68 -0
  171. package/dist/store/postgres/migrate.js +183 -0
  172. package/dist/store/postgres/record-writer.d.ts +4 -0
  173. package/dist/store/postgres/record-writer.js +34 -0
  174. package/dist/store/postgres/record.d.ts +5 -0
  175. package/dist/store/postgres/record.js +60 -0
  176. package/dist/store/postgres/schema.d.ts +3096 -0
  177. package/dist/store/postgres/schema.js +363 -0
  178. package/dist/store/postgres/store.d.ts +2 -4
  179. package/dist/store/postgres/store.js +590 -434
  180. package/dist/store/postgres/tenant.d.ts +56 -0
  181. package/dist/store/postgres/tenant.js +154 -0
  182. package/dist/store/types.d.ts +183 -132
  183. package/dist/store/types.js +0 -1
  184. package/dist/streams/basin.d.ts +18 -1
  185. package/dist/streams/basin.js +34 -2
  186. package/dist/streams/control.d.ts +18 -0
  187. package/dist/streams/control.js +33 -0
  188. package/dist/streams/memory.d.ts +3 -1
  189. package/dist/streams/memory.js +14 -8
  190. package/dist/streams/relay/core.d.ts +35 -0
  191. package/dist/streams/relay/core.js +366 -0
  192. package/dist/streams/relay/types.d.ts +78 -0
  193. package/dist/streams/relay/types.js +8 -0
  194. package/dist/streams/types.d.ts +71 -36
  195. package/dist/streams/types.js +18 -53
  196. package/dist/tenant/access-policy.d.ts +30 -0
  197. package/dist/tenant/access-policy.js +52 -0
  198. package/dist/tenant/actions.d.ts +10 -149
  199. package/dist/tenant/actions.js +5 -167
  200. package/dist/tenant/advance.d.ts +7 -2
  201. package/dist/tenant/advance.js +125 -14
  202. package/dist/tenant/auth.d.ts +19 -17
  203. package/dist/tenant/auth.js +90 -93
  204. package/dist/tenant/browser.d.ts +15 -0
  205. package/dist/tenant/browser.js +39 -0
  206. package/dist/tenant/cause.d.ts +25 -0
  207. package/dist/tenant/cause.js +45 -0
  208. package/dist/tenant/commands.d.ts +18 -2
  209. package/dist/tenant/commands.js +111 -115
  210. package/dist/tenant/context.d.ts +101 -21
  211. package/dist/tenant/context.js +14 -10
  212. package/dist/tenant/delivery-token.d.ts +41 -0
  213. package/dist/tenant/delivery-token.js +104 -0
  214. package/dist/tenant/delivery.d.ts +62 -0
  215. package/dist/tenant/delivery.js +481 -0
  216. package/dist/tenant/effects.d.ts +20 -1
  217. package/dist/tenant/effects.js +159 -42
  218. package/dist/tenant/endpoints.d.ts +67 -0
  219. package/dist/tenant/endpoints.js +90 -0
  220. package/dist/tenant/ephemeral.d.ts +22 -7
  221. package/dist/tenant/ephemeral.js +83 -125
  222. package/dist/tenant/history.d.ts +64 -0
  223. package/dist/tenant/history.js +120 -0
  224. package/dist/tenant/http.d.ts +8 -9
  225. package/dist/tenant/http.js +13 -27
  226. package/dist/tenant/jwt.d.ts +16 -0
  227. package/dist/tenant/jwt.js +50 -0
  228. package/dist/tenant/model-setting.d.ts +2 -2
  229. package/dist/tenant/module.d.ts +17 -28
  230. package/dist/tenant/module.js +133 -279
  231. package/dist/tenant/outbound.d.ts +44 -0
  232. package/dist/tenant/outbound.js +127 -0
  233. package/dist/tenant/paths.d.ts +11 -16
  234. package/dist/tenant/paths.js +24 -68
  235. package/dist/tenant/pool.d.ts +1 -3
  236. package/dist/tenant/pool.js +1 -19
  237. package/dist/tenant/principals.d.ts +23 -11
  238. package/dist/tenant/principals.js +61 -49
  239. package/dist/tenant/reset.d.ts +0 -4
  240. package/dist/tenant/reset.js +0 -5
  241. package/dist/tenant/runtime.d.ts +28 -20
  242. package/dist/tenant/runtime.js +95 -64
  243. package/dist/tenant/scheduler.d.ts +10 -3
  244. package/dist/tenant/scheduler.js +9 -5
  245. package/dist/tenant/session-streams.d.ts +127 -0
  246. package/dist/tenant/session-streams.js +404 -0
  247. package/dist/tenant/sessions.d.ts +28 -4
  248. package/dist/tenant/sessions.js +118 -24
  249. package/dist/tenant/signing-keys.d.ts +55 -0
  250. package/dist/tenant/signing-keys.js +172 -0
  251. package/dist/tenant/slim.d.ts +8 -0
  252. package/dist/tenant/slim.js +21 -0
  253. package/dist/tenant/status.d.ts +3 -3
  254. package/dist/tenant/status.js +20 -13
  255. package/dist/tenant/store-pg.d.ts +9 -6
  256. package/dist/tenant/store-pg.js +60 -125
  257. package/dist/tenant/streams.d.ts +62 -85
  258. package/dist/tenant/streams.js +143 -226
  259. package/dist/tenant/subject-limits.d.ts +16 -0
  260. package/dist/tenant/subject-limits.js +45 -0
  261. package/dist/tenant/sweep.d.ts +1 -10
  262. package/dist/tenant/sweep.js +29 -69
  263. package/dist/tenant/tokens.d.ts +14 -0
  264. package/dist/tenant/tokens.js +180 -0
  265. package/dist/tenant/transcript.d.ts +48 -5
  266. package/dist/tenant/transcript.js +58 -1
  267. package/dist/tenant/types.d.ts +69 -98
  268. package/dist/tenant/types.js +4 -15
  269. package/dist/tenant/worker.d.ts +3 -1
  270. package/dist/tenant/worker.js +13 -7
  271. package/dist/vault/host-model.d.ts +31 -0
  272. package/dist/vault/host-model.js +57 -0
  273. package/dist/vault/service.d.ts +44 -17
  274. package/dist/vault/service.js +126 -121
  275. package/dist/version.d.ts +1 -1
  276. package/dist/version.js +1 -1
  277. package/package.json +25 -9
  278. package/dist/core/executors.d.ts +0 -44
  279. package/dist/core/executors.js +0 -95
  280. package/dist/store/memory.d.ts +0 -57
  281. package/dist/store/memory.js +0 -645
  282. package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
  283. package/dist/store/postgres/migrations/001_initial.js +0 -196
  284. package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
  285. package/dist/store/postgres/migrations/002_session_owner.js +0 -13
  286. package/dist/store/postgres/migrations/index.d.ts +0 -49
  287. package/dist/store/postgres/migrations/index.js +0 -76
  288. package/dist/store/postgres/names.d.ts +0 -11
  289. package/dist/store/postgres/names.js +0 -42
  290. package/dist/store/postgres/tenants.d.ts +0 -90
  291. package/dist/store/postgres/tenants.js +0 -214
  292. package/dist/streams/relay.d.ts +0 -37
  293. package/dist/streams/relay.js +0 -254
  294. package/dist/tenant/envelope.d.ts +0 -7
  295. package/dist/tenant/envelope.js +0 -22
  296. package/dist/tenant/live.d.ts +0 -92
  297. package/dist/tenant/live.js +0 -290
  298. package/dist/tenant/quarantine-error.d.ts +0 -6
  299. package/dist/tenant/quarantine-error.js +0 -9
  300. package/dist/tenant/quarantine.d.ts +0 -27
  301. package/dist/tenant/quarantine.js +0 -72
  302. package/dist/tenant/routes-tenant.d.ts +0 -12
  303. package/dist/tenant/routes-tenant.js +0 -137
  304. package/dist/tenant/routes.d.ts +0 -9
  305. package/dist/tenant/routes.js +0 -151
  306. package/dist/tenant/store-memory.d.ts +0 -16
  307. package/dist/tenant/store-memory.js +0 -111
  308. /package/dist/{store → record}/cursor.d.ts +0 -0
  309. /package/dist/{store → record}/cursor.js +0 -0
@@ -0,0 +1,481 @@
1
+ /**
2
+ * The Action deliverer (design: Action endpoints §6, §7): sends a pending Action to its agent's
3
+ * Action endpoint and records what comes back, run by `DurableExecution.deliver` one delivery
4
+ * per Action at a time.
5
+ *
6
+ * 1. In one transaction, holding the session's lock: the Action must be `pending`, its turn
7
+ * active and its agent registered. Below the endpoint's `maxConcurrent`, it becomes
8
+ * `delivering`, its generation moves on, it gets a deadline, and `action.delivered` is
9
+ * written.
10
+ * 2. Outside any transaction: the body is signed with a delivery token and POSTed.
11
+ * 3. In a second transaction, only while the Action is still `delivering` at that generation:
12
+ * the answer becomes the outcome (`recordActionOutcome`, shared with background results), the
13
+ * Action goes back to `pending` to be tried again, or it is lost.
14
+ *
15
+ * Nothing sent (connection refused, unknown host, a refused address), `429`, `503` and a
16
+ * version mismatch (`409`) are retried with backoff, whatever the kind. A delivery that may have
17
+ * reached the endpoint but got no answer is lost: a tool becomes `uncertain`, a hook, `fn` or
18
+ * `verify` is delivered again. So is a delivery whose deadline passes with no answer, which is
19
+ * how a Worker that died mid-delivery is recovered (the sweep, `expireDeliveries`).
20
+ */
21
+ import { OUTCOME_HEADER, PROTOCOL_HEADER, PROTOCOL_VERSION, SIGNATURE_HEADER, } from "@nylorun/core/compatibility";
22
+ import { ActionOutcomeSchema, EndpointPingResponseSchema, } from "@nylorun/core/contracts";
23
+ import { sessionHasSandbox } from "../sandbox/share.js";
24
+ import { canonical } from "../store/canonical.js";
25
+ import { scoped } from "./auth.js";
26
+ import { acceptedOutcome, recordActionOutcome } from "./commands.js";
27
+ import { sandboxLookup } from "./context.js";
28
+ import { mintDeliveryToken } from "./delivery-token.js";
29
+ import { fail } from "./http.js";
30
+ const DONE = { status: "done" };
31
+ /** Extra time past the endpoint's timeout before an unanswered delivery counts as lost. */
32
+ const DEADLINE_GRACE_MS = 5_000;
33
+ /** How long a delivery token outlives the request it signs. */
34
+ const TOKEN_SLACK_SECONDS = 60;
35
+ /** How soon a delivery refused by a full endpoint is tried again. */
36
+ const BUSY_RETRY_MS = 1_000;
37
+ const BACKOFF_FIRST_MS = 250;
38
+ const BACKOFF_MAX_MS = 30_000;
39
+ /** The longest `Retry-After` honoured. */
40
+ const RETRY_AFTER_MAX_MS = 5 * 60_000;
41
+ /** One `action.delivery_failed` per Action per this long. */
42
+ const NOTICE_INTERVAL_MS = 10_000;
43
+ /**
44
+ * Offers an Action that just became `pending` to its agent's endpoint, after the transaction
45
+ * commits. Without an endpoint it waits for the agent's next registration (`PUT /v1/endpoints`).
46
+ */
47
+ export async function offerAction(t, ctx, action) {
48
+ if (await t.getEndpoint(action.agentId))
49
+ t.afterCommit(() => void ctx.deliver(action.actionId));
50
+ }
51
+ /** Delivers one Action (`WorkerHandlers.deliver`). Throws only on infrastructure errors. */
52
+ export async function deliverAction(ctx, actionId, signal) {
53
+ const started = await ctx.store.tx(async (t) => {
54
+ const found = await t.get("actions", actionId);
55
+ if (!found)
56
+ return { kind: "none" };
57
+ const s = await t.lockSession(found.sessionId);
58
+ // Read again under the session lock.
59
+ const action = await t.get("actions", actionId);
60
+ if (!action || !s)
61
+ return { kind: "none" };
62
+ if (action.status === "delivering") {
63
+ // In flight elsewhere, or answered with 202: its deadline settles it. Past the deadline
64
+ // (a Worker died mid-delivery), it is lost now.
65
+ if (Date.parse(action.deadlineAt ?? "") > Date.now())
66
+ return { kind: "none" };
67
+ await loseAction(t, ctx, s, action, { redeliver: "after-commit" });
68
+ return { kind: "none" };
69
+ }
70
+ if (action.status !== "pending" ||
71
+ s.status === "cancelled" ||
72
+ s.activeTurnId !== action.turnId)
73
+ return { kind: "none" };
74
+ const endpoint = await t.getEndpoint(action.agentId);
75
+ // No endpoint: it waits for the next registration.
76
+ if (!endpoint)
77
+ return { kind: "none" };
78
+ if ((await t.deliveringCount(action.agentId)) >= endpoint.maxConcurrent)
79
+ return { kind: "busy" };
80
+ action.status = "delivering";
81
+ action.generation += 1;
82
+ action.deadlineAt = new Date(Date.now() + endpoint.timeoutMs + DEADLINE_GRACE_MS).toISOString();
83
+ await t.put("actions", actionId, action);
84
+ await t.event(s.id, s.activeTurnId, "action.delivered", {
85
+ actionId,
86
+ generation: action.generation,
87
+ ...(action.agent ? { agent: action.agent } : {}),
88
+ });
89
+ return {
90
+ kind: "deliver",
91
+ action: structuredClone(action),
92
+ endpoint,
93
+ sandbox: sessionHasSandbox(s, await sandboxLookup(t, s.sandboxOwnerId)),
94
+ };
95
+ });
96
+ if (started.kind === "none")
97
+ return DONE;
98
+ if (started.kind === "busy")
99
+ return { status: "retry", retryAfterMs: BUSY_RETRY_MS };
100
+ const { action, endpoint } = started;
101
+ const body = JSON.stringify({ type: "action", action, sandbox: started.sandbox });
102
+ const minted = await mintDeliveryToken(ctx, {
103
+ for: {
104
+ kind: "action",
105
+ actionId,
106
+ agentId: action.agentId,
107
+ generation: action.generation,
108
+ },
109
+ audience: endpoint.url,
110
+ body,
111
+ ttlSeconds: Math.ceil(endpoint.timeoutMs / 1000) + TOKEN_SLACK_SECONDS,
112
+ });
113
+ const cancel = new AbortController();
114
+ const inFlight = track(ctx, action.sessionId, cancel);
115
+ let result;
116
+ try {
117
+ // Through the Tool Gate (F4.1): the gates service POSTs it under its own address policy.
118
+ result = await ctx.toolGate.post({
119
+ url: endpoint.url,
120
+ body,
121
+ headers: {
122
+ [SIGNATURE_HEADER]: minted.token,
123
+ [PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
124
+ "idempotency-key": actionId,
125
+ },
126
+ timeoutMs: endpoint.timeoutMs,
127
+ }, AbortSignal.any([signal, cancel.signal, AbortSignal.timeout(endpoint.timeoutMs)]));
128
+ }
129
+ finally {
130
+ inFlight();
131
+ }
132
+ // A cancel already made the Action `uncertain`; a Worker that stops leaves it to its deadline.
133
+ if (cancel.signal.aborted || signal.aborted)
134
+ return DONE;
135
+ return settle(ctx, action, endpoint, result);
136
+ }
137
+ /** Registers a delivery under its session for `abortLocal`; the returned function removes it. */
138
+ function track(ctx, sessionId, controller) {
139
+ let set = ctx.work.deliveries.get(sessionId);
140
+ if (!set)
141
+ ctx.work.deliveries.set(sessionId, (set = new Set()));
142
+ set.add(controller);
143
+ return () => {
144
+ set.delete(controller);
145
+ if (set.size === 0 && ctx.work.deliveries.get(sessionId) === set)
146
+ ctx.work.deliveries.delete(sessionId);
147
+ };
148
+ }
149
+ /** What an endpoint's answer means for the Action (design §5.2, §5.3). */
150
+ export function interpret(action, result) {
151
+ if (result.kind === "not_sent")
152
+ return { kind: "retry", code: "endpoint.unreachable", message: result.message };
153
+ if (result.kind === "lost")
154
+ return { kind: "lost", code: "endpoint.no-answer", message: result.message };
155
+ if (result.kind === "too_large")
156
+ return rejected("endpoint.answer-too-large", "The endpoint's answer was too large");
157
+ const { status, headers, body } = result;
158
+ if (status === 202)
159
+ return { kind: "accepted" };
160
+ if (status === 429 || status === 503)
161
+ return {
162
+ kind: "retry",
163
+ code: status === 429 ? "endpoint.busy" : "endpoint.unavailable",
164
+ message: messageOf(body) ?? `The endpoint answered ${status}`,
165
+ ...retryAfter(headers["retry-after"]),
166
+ };
167
+ if (status === 409)
168
+ return {
169
+ kind: "retry",
170
+ code: "endpoint.version-mismatch",
171
+ message: messageOf(body) ?? "The endpoint serves another version of the definition",
172
+ };
173
+ if (status >= 500)
174
+ return { kind: "lost", code: "endpoint.failed", message: messageOf(body) ?? `The endpoint answered ${status}` };
175
+ if (status !== 200)
176
+ return rejected("endpoint.rejected", messageOf(body) ?? `The endpoint answered ${status}`);
177
+ let value;
178
+ try {
179
+ value = JSON.parse(body.toString("utf8"));
180
+ }
181
+ catch {
182
+ return rejected("endpoint.invalid-answer", "The endpoint's answer is not JSON");
183
+ }
184
+ if (headers[OUTCOME_HEADER.toLowerCase()] === "1") {
185
+ const parsed = ActionOutcomeSchema.safeParse(value);
186
+ return parsed.success
187
+ ? { kind: "outcome", outcome: parsed.data }
188
+ : rejected("endpoint.invalid-answer", "The endpoint's answer is not an Action outcome");
189
+ }
190
+ // A plain answer: a tool's output, or what an `fn` or `verify` returned.
191
+ if (action.kind === "tool")
192
+ return { kind: "outcome", outcome: { value: { kind: "completed", output: value } } };
193
+ if (action.kind === "fn" || action.kind === "verify")
194
+ return { kind: "outcome", outcome: { value } };
195
+ return rejected("endpoint.invalid-answer", `A ${action.kind} needs a tagged answer (${OUTCOME_HEADER}: 1)`);
196
+ }
197
+ function rejected(code, message) {
198
+ return { kind: "outcome", outcome: { value: { kind: "failed", code, message } } };
199
+ }
200
+ function messageOf(body) {
201
+ try {
202
+ const parsed = JSON.parse(body.toString("utf8"));
203
+ return typeof parsed.message === "string" ? parsed.message : undefined;
204
+ }
205
+ catch {
206
+ const text = body.toString("utf8").trim();
207
+ return text ? text.slice(0, 500) : undefined;
208
+ }
209
+ }
210
+ function retryAfter(header) {
211
+ const value = Array.isArray(header) ? header[0] : header;
212
+ if (!value)
213
+ return {};
214
+ const seconds = Number(value);
215
+ const ms = Number.isFinite(seconds) ? seconds * 1000 : Date.parse(value) - Date.now();
216
+ return Number.isFinite(ms) && ms > 0 ? { retryAfterMs: Math.min(ms, RETRY_AFTER_MAX_MS) } : {};
217
+ }
218
+ /** Records the answer, in a transaction that still finds the Action at this delivery. */
219
+ async function settle(ctx, delivered, endpoint, result) {
220
+ const settlement = interpret(delivered, result);
221
+ const at = new Date().toISOString();
222
+ return ctx.store.tx(async (t) => {
223
+ const s = await t.lockSession(delivered.sessionId);
224
+ const action = await t.get("actions", delivered.actionId);
225
+ // A cancel, a deadline or another delivery settled it first.
226
+ if (!s ||
227
+ !action ||
228
+ action.status !== "delivering" ||
229
+ action.generation !== delivered.generation)
230
+ return DONE;
231
+ switch (settlement.kind) {
232
+ case "outcome": {
233
+ await t.recordEndpointHealth(endpoint.agentId, { kind: "success", at });
234
+ if (s.status === "cancelled" || s.activeTurnId !== action.turnId)
235
+ return DONE;
236
+ await recordActionOutcome(t, ctx, s, action, settlement.outcome);
237
+ await t.put("sessions", s.id, s);
238
+ return DONE;
239
+ }
240
+ case "accepted": {
241
+ // The endpoint answers later (`POST /v1/actions/:id/result`) and heartbeats meanwhile.
242
+ await t.recordEndpointHealth(endpoint.agentId, { kind: "success", at });
243
+ action.deadlineAt = new Date(Date.now() + (ctx.config.leaseMs ?? 30_000)).toISOString();
244
+ await t.put("actions", action.actionId, action);
245
+ return DONE;
246
+ }
247
+ case "retry": {
248
+ await t.recordEndpointHealth(endpoint.agentId, {
249
+ kind: "failure",
250
+ at,
251
+ code: settlement.code,
252
+ message: settlement.message,
253
+ });
254
+ action.status = "pending";
255
+ action.deadlineAt = null;
256
+ await t.put("actions", action.actionId, action);
257
+ const retryAfterMs = settlement.retryAfterMs ?? (await backoff(t, endpoint.agentId));
258
+ await notice(t, ctx, s, action, settlement, retryAfterMs);
259
+ return { status: "retry", retryAfterMs };
260
+ }
261
+ case "lost": {
262
+ await t.recordEndpointHealth(endpoint.agentId, {
263
+ kind: "failure",
264
+ at,
265
+ code: settlement.code,
266
+ message: settlement.message,
267
+ });
268
+ const again = await loseAction(t, ctx, s, action, { redeliver: "retry" });
269
+ if (!again)
270
+ return DONE;
271
+ const retryAfterMs = await backoff(t, endpoint.agentId);
272
+ await notice(t, ctx, s, action, settlement, retryAfterMs);
273
+ return { status: "retry", retryAfterMs };
274
+ }
275
+ }
276
+ });
277
+ }
278
+ /** Backoff from the endpoint's consecutive failures: 250 ms doubling to 30 s. */
279
+ async function backoff(t, agentId) {
280
+ const failures = (await t.getEndpoint(agentId))?.consecutiveFailures ?? 1;
281
+ return Math.min(BACKOFF_MAX_MS, BACKOFF_FIRST_MS * 2 ** Math.max(0, Math.min(failures - 1, 16)));
282
+ }
283
+ /** `action.delivery_failed`, at most once per Action per `NOTICE_INTERVAL_MS` on this process. */
284
+ async function notice(t, ctx, s, action, settlement, retryInMs) {
285
+ const last = ctx.work.deliveryNotices.get(action.actionId) ?? 0;
286
+ if (Date.now() - last < NOTICE_INTERVAL_MS)
287
+ return;
288
+ ctx.work.deliveryNotices.set(action.actionId, Date.now());
289
+ if (s.status === "cancelled" || s.activeTurnId !== action.turnId)
290
+ return;
291
+ await t.event(s.id, s.activeTurnId, "action.delivery_failed", {
292
+ actionId: action.actionId,
293
+ generation: action.generation,
294
+ reason: settlement.code,
295
+ message: settlement.message,
296
+ retryInMs,
297
+ });
298
+ }
299
+ /**
300
+ * A delivery that may have reached the endpoint got no answer. A tool becomes `uncertain`, with
301
+ * its effect and its session. A hook,
302
+ * `fn` or `verify` is pure or repeat-safe and goes back to `pending`: delivered again right away
303
+ * (`after-commit`) or by the caller's retry (`retry`). Returns true when it will be delivered again.
304
+ */
305
+ export async function loseAction(t, ctx, s, action, options) {
306
+ if (action.kind === "hook" || action.kind === "fn" || action.kind === "verify") {
307
+ action.status = "pending";
308
+ action.deadlineAt = null;
309
+ await t.put("actions", action.actionId, action);
310
+ if (options.redeliver === "after-commit")
311
+ t.afterCommit(() => void ctx.deliver(action.actionId));
312
+ return options.redeliver === "retry";
313
+ }
314
+ action.status = "uncertain";
315
+ action.deadlineAt = null;
316
+ await t.put("actions", action.actionId, action);
317
+ const effect = await t.get("effects", action.actionId);
318
+ if (effect) {
319
+ effect.status = "uncertain";
320
+ await t.put("effects", action.actionId, effect);
321
+ }
322
+ if (s.status !== "cancelled" && s.activeTurnId === action.turnId) {
323
+ s.status = "uncertain";
324
+ await t.put("sessions", s.id, s);
325
+ await t.event(s.id, s.activeTurnId, "action.uncertain", {
326
+ actionId: action.actionId,
327
+ ...(action.agent ? { agent: action.agent } : {}),
328
+ });
329
+ }
330
+ return false;
331
+ }
332
+ /**
333
+ * The sweep's part (design §7): deliveries whose deadline passed without an answer are lost,
334
+ * and pending Actions of agents with an endpoint are sent again, in case a send was lost
335
+ * between a commit and the execution. Returns how many deliveries it settled.
336
+ */
337
+ export async function sweepDeliveries(ctx, now = new Date()) {
338
+ let settled = 0;
339
+ for (const expired of await ctx.store.tx((t) => t.expiredDeliveries(now, 100))) {
340
+ const changed = await ctx.store.tx(async (t) => {
341
+ const s = await t.lockSession(expired.sessionId);
342
+ const action = await t.get("actions", expired.actionId);
343
+ if (!s ||
344
+ !action ||
345
+ action.status !== "delivering" ||
346
+ Date.parse(action.deadlineAt ?? "") > now.getTime())
347
+ return false;
348
+ await loseAction(t, ctx, s, action, { redeliver: "after-commit" });
349
+ return true;
350
+ });
351
+ if (changed)
352
+ settled += 1;
353
+ }
354
+ for (const pending of await ctx.store.tx((t) => t.pendingActionsWithEndpoint(100)))
355
+ await ctx.deliver(pending.actionId);
356
+ return settled;
357
+ }
358
+ /**
359
+ * `POST /v1/endpoints/:agentId/ping`: a signed ping through the endpoint, so a wrong URL, a
360
+ * tunnel that is down or a handler serving other definitions shows up before the first turn.
361
+ */
362
+ export async function pingEndpoint(ctx, agentId) {
363
+ const endpoint = (await ctx.store.tx((t) => t.getEndpoint(agentId))) ?? fail(404, "Endpoint not found");
364
+ const body = JSON.stringify({
365
+ type: "ping",
366
+ agentId,
367
+ ...(endpoint.manifestHash === undefined ? {} : { manifestHash: endpoint.manifestHash }),
368
+ });
369
+ const minted = await mintDeliveryToken(ctx, {
370
+ for: { kind: "ping", agentId },
371
+ audience: endpoint.url,
372
+ body,
373
+ ttlSeconds: TOKEN_SLACK_SECONDS,
374
+ });
375
+ const timeoutMs = Math.min(endpoint.timeoutMs, 10_000);
376
+ const result = await ctx.toolGate.post({
377
+ url: endpoint.url,
378
+ body,
379
+ headers: { [SIGNATURE_HEADER]: minted.token, [PROTOCOL_HEADER]: String(PROTOCOL_VERSION) },
380
+ timeoutMs,
381
+ }, AbortSignal.timeout(timeoutMs));
382
+ const at = new Date().toISOString();
383
+ const refuse = async (code, message) => {
384
+ await ctx.store.tx((t) => t.recordEndpointHealth(agentId, { kind: "failure", at, code, message }));
385
+ return fail(502, `The endpoint did not answer the ping: ${message}`);
386
+ };
387
+ if (result.kind !== "response")
388
+ return refuse("endpoint.unreachable", result.kind === "too_large" ? "The answer was too large" : result.message);
389
+ if (result.status !== 200)
390
+ return refuse("endpoint.rejected", messageOf(result.body) ?? `The endpoint answered ${result.status}`);
391
+ let answer;
392
+ try {
393
+ answer = EndpointPingResponseSchema.parse(JSON.parse(result.body.toString("utf8")));
394
+ }
395
+ catch {
396
+ return refuse("endpoint.invalid-answer", "The endpoint's answer is not a ping answer");
397
+ }
398
+ if (answer.agentId !== agentId)
399
+ return refuse("endpoint.invalid-answer", `The endpoint answered for '${answer.agentId}'`);
400
+ await ctx.store.tx(async (t) => {
401
+ await t.recordEndpointHealth(agentId, { kind: "success", at });
402
+ await t.recordEndpointHealth(agentId, {
403
+ kind: "served",
404
+ implementationVersion: answer.implementationVersion,
405
+ ...(answer.manifestHash === undefined ? {} : { manifestHash: answer.manifestHash }),
406
+ });
407
+ });
408
+ return answer;
409
+ }
410
+ const STALE_DELIVERY = "The delivery was cancelled, lost or delivered again";
411
+ /**
412
+ * The Action a delivery token's callback is about, read under its session's lock, and whether
413
+ * that delivery is still the live one: `delivering` at the token's generation, before its
414
+ * deadline, in the session's active turn.
415
+ */
416
+ async function deliveredAction(t, scope, actionId) {
417
+ const found = (await t.get("actions", actionId)) ?? fail(404, "Action not found");
418
+ scoped(scope, found);
419
+ const s = (await t.lockSession(found.sessionId)) ?? fail(404, "Action not found");
420
+ const action = (await t.get("actions", actionId));
421
+ const live = action.status === "delivering" &&
422
+ action.generation === scope.generation &&
423
+ Date.parse(action.deadlineAt ?? "") > Date.now() &&
424
+ s.status !== "cancelled" &&
425
+ s.activeTurnId === action.turnId;
426
+ return { s, action, live };
427
+ }
428
+ /**
429
+ * `POST /v1/actions/:id/heartbeat` with a delivery token: an endpoint that answered `202` keeps
430
+ * its delivery alive for another lease, and gets a fresh token for its next callbacks. `409`
431
+ * when the delivery was cancelled, lost or sent again, which tells the endpoint to stop.
432
+ */
433
+ export async function deliveryHeartbeat(ctx, scope, actionId) {
434
+ const leaseMs = ctx.config.leaseMs ?? 30_000;
435
+ const renewed = await ctx.store.tx(async (t) => {
436
+ const { action, live } = await deliveredAction(t, scope, actionId);
437
+ if (!live)
438
+ fail(409, STALE_DELIVERY);
439
+ action.deadlineAt = new Date(Date.now() + leaseMs).toISOString();
440
+ await t.put("actions", action.actionId, action);
441
+ return { action, endpoint: await t.getEndpoint(action.agentId) };
442
+ });
443
+ const minted = await mintDeliveryToken(ctx, {
444
+ for: {
445
+ kind: "action",
446
+ actionId,
447
+ agentId: renewed.action.agentId,
448
+ generation: renewed.action.generation,
449
+ },
450
+ audience: renewed.endpoint?.url ?? "",
451
+ body: "",
452
+ ttlSeconds: Math.ceil(leaseMs / 1000) + TOKEN_SLACK_SECONDS,
453
+ });
454
+ return { token: minted.token, deadlineAt: renewed.action.deadlineAt };
455
+ }
456
+ /**
457
+ * `POST /v1/actions/:id/result` with a delivery token: the outcome of a delivery answered with
458
+ * `202`. The same result again returns the first receipt; a different one is `409`.
459
+ */
460
+ export async function deliveryResult(ctx, scope, actionId, outcome) {
461
+ return ctx.store.tx(async (t) => {
462
+ const { s, action, live } = await deliveredAction(t, scope, actionId);
463
+ if (action.status === "completed") {
464
+ const prior = await t.get("effects", actionId);
465
+ if (action.generation === scope.generation &&
466
+ prior?.receipt &&
467
+ canonical(prior.outcome) === canonical(acceptedOutcome(action, outcome)))
468
+ return prior.receipt;
469
+ return fail(409, "The Action already has another result");
470
+ }
471
+ if (!live)
472
+ return fail(409, STALE_DELIVERY);
473
+ const { receipt } = await recordActionOutcome(t, ctx, s, action, outcome);
474
+ await t.put("sessions", s.id, s);
475
+ await t.recordEndpointHealth(action.agentId, {
476
+ kind: "success",
477
+ at: new Date().toISOString(),
478
+ });
479
+ return receipt;
480
+ });
481
+ }
@@ -1,13 +1,32 @@
1
1
  import type { EffectResolution, HostEffect } from "@nylorun/harness/run";
2
2
  import type { AuthorizeResult } from "../vault/service.js";
3
- import { type Lease, type TenantContext } from "./context.js";
3
+ import { type Lease, type Session, type TenantContext } from "./context.js";
4
4
  import type { ModelProvider } from "../core/provider.js";
5
5
  /** What one advance's segment decides for all its effects. */
6
6
  export interface SegmentOptions {
7
7
  /** Overrides the Tenant's model (the fixture-model Tenant setting, `model-setting.ts`). */
8
8
  model?: ModelProvider;
9
9
  }
10
+ /**
11
+ * Call the model for one effect. A provider failure comes back as a failure outcome
12
+ * (Model Calls §6), whichever provider serves the call; only an abort throws, and the
13
+ * advance decides what the abort means.
14
+ */
10
15
  export declare function invokeModel(ctx: TenantContext, request: HostEffect, signal: AbortSignal, model?: ModelProvider): Promise<unknown>;
16
+ /**
17
+ * True when this Tenant's remote MCP calls outlive the process that sent them (the gates
18
+ * service, F4.1 G3): after a takeover or a shutdown, the journaled call is re-sent and joins
19
+ * the running call or gets its answer, instead of becoming `uncertain`.
20
+ */
21
+ export declare function recoversMcpCalls(ctx: TenantContext): boolean;
22
+ /** True when `request` calls a tool of a remote (`streamable-http` or `sse`) MCP server. */
23
+ export declare function isRemoteMcpEffect(s: Session, request: HostEffect): boolean;
24
+ /**
25
+ * True when this Tenant's vault-backed model calls outlive the process that sent them (the
26
+ * gates service, P1.2): after a takeover or a shutdown, the journaled call is re-sent and joins
27
+ * the running call or gets its outcome, instead of becoming `uncertain`.
28
+ */
29
+ export declare function recoversModelCalls(ctx: TenantContext): boolean;
11
30
  export declare function resolveEffect(ctx: TenantContext, request: HostEffect, signal: AbortSignal, lease: Lease, segment?: SegmentOptions): Promise<EffectResolution>;
12
31
  /** Journal and dispatch a new flow effect (agent / tool node / fn / verify). */
13
32
  export declare function resolveNewFlowEffect(ctx: TenantContext, request: HostEffect, signal: AbortSignal, lease: Lease): Promise<EffectResolution>;