@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,60 @@
1
+ import { parseTranscriptEvent } from "@nylorun/core/contracts";
2
+ import { outputText, toolResultOf } from "./translate.js";
3
+ export function messagesFromEvents(items) {
4
+ const messages = [];
5
+ const results = new Set();
6
+ let sawAssistant = false;
7
+ for (const raw of items) {
8
+ if (raw.type === "command.message") {
9
+ const p = (raw.payload ?? {});
10
+ messages.push({
11
+ // The AG-UI endpoint sends the client's message id as the idempotency key.
12
+ id: p.idempotencyKey ?? raw.eventId,
13
+ role: "user",
14
+ content: typeof p.content === "string" ? p.content : JSON.stringify(p.data),
15
+ });
16
+ sawAssistant = false;
17
+ continue;
18
+ }
19
+ const event = parseTranscriptEvent(raw);
20
+ if (!event)
21
+ continue;
22
+ const result = toolResultOf(event);
23
+ if (result) {
24
+ if (results.has(result.callId))
25
+ continue;
26
+ results.add(result.callId);
27
+ messages.push({
28
+ id: `${result.callId}:result`,
29
+ role: "tool",
30
+ toolCallId: result.callId,
31
+ content: result.content,
32
+ });
33
+ continue;
34
+ }
35
+ if (event.type === "message.assistant" && !event.payload.agent) {
36
+ const p = event.payload;
37
+ sawAssistant = true;
38
+ const message = { id: p.invocationId, role: "assistant" };
39
+ if (p.text)
40
+ message.content = p.text;
41
+ if (p.toolCalls.length)
42
+ message.toolCalls = p.toolCalls.map((call) => ({
43
+ id: call.callId,
44
+ type: "function",
45
+ function: { name: call.name, arguments: JSON.stringify(call.input ?? {}) },
46
+ }));
47
+ messages.push(message);
48
+ }
49
+ else if (event.type === "turn.completed" && !sawAssistant) {
50
+ const output = event.payload.output;
51
+ if (output !== undefined && output !== null && outputText(output))
52
+ messages.push({
53
+ id: raw.eventId,
54
+ role: "assistant",
55
+ content: outputText(output),
56
+ });
57
+ }
58
+ }
59
+ return messages;
60
+ }
@@ -0,0 +1,32 @@
1
+ import type { ServerResponse } from "node:http";
2
+ import { type Message } from "@ag-ui/core";
3
+ import { type AuthScope, type SessionAccess, type TenantContext } from "../../tenant/context.js";
4
+ /**
5
+ * Who an AG-UI request acts for, once its agent is known: a person (a subject token's subject,
6
+ * or `Nylorun-Subject`), who may use the agent; else 400, or the 404 of a missing agent.
7
+ */
8
+ export declare function agUiCaller(scope: AuthScope, agentId: string): {
9
+ subject: string;
10
+ access: SessionAccess | undefined;
11
+ };
12
+ /** `POST /v1/ag-ui/agents/:agent`: start a run, and stream it on `response`. */
13
+ export declare function startRun(ctx: TenantContext, scope: AuthScope, agentId: string, caller: {
14
+ subject: string;
15
+ access: SessionAccess | undefined;
16
+ }, body: unknown, response: ServerResponse): Promise<void>;
17
+ /** `GET …/threads/:thread/messages`: the thread's messages; none before its first run. */
18
+ export declare function threadMessages(ctx: TenantContext, id: string, access: SessionAccess | undefined): Promise<Message[]>;
19
+ /**
20
+ * `GET …/threads/:thread/events`: the rest of a run after a dropped connection, streamed on
21
+ * `response`, or 204 when there is nothing to send.
22
+ */
23
+ export declare function reattachRun(ctx: TenantContext, scope: AuthScope, access: SessionAccess | undefined, thread: {
24
+ threadId: string;
25
+ id: string;
26
+ }, resume: {
27
+ lastEventId: string | undefined;
28
+ cursor: string | undefined;
29
+ runId: string | undefined;
30
+ }, response: ServerResponse): Promise<void>;
31
+ /** `POST …/threads/:thread/cancel`: cancel the running turn. */
32
+ export declare function cancelRun(ctx: TenantContext, id: string, scope: AuthScope): Promise<void>;
@@ -0,0 +1,305 @@
1
+ /**
2
+ * The Runtime's AG-UI endpoint (Host feature `ag-ui-endpoint`): the Tenant's agents served to
3
+ * AG-UI clients, for a person named by a subject token (a browser or app) or by subject headers
4
+ * (an app server's `createAgUiHandler`, which forwards here). One implementation for both, so
5
+ * a thread is the same session whichever way it is reached.
6
+ *
7
+ * Routes, under `/v1/ag-ui/agents/:agent`:
8
+ * - `POST` — run: a `RunAgentInput` in, the run's events out as server-sent events;
9
+ * - `GET …/threads/:thread/messages` — the thread's messages as AG-UI `Message[]`;
10
+ * - `GET …/threads/:thread/events` — the rest of a run after a dropped connection
11
+ * (`Last-Event-ID` or `?cursor=`, optional `?runId=`);
12
+ * - `POST …/threads/:thread/cancel` — cancel the running turn.
13
+ *
14
+ * Each thread maps to one session per subject, agent and thread. The message id is the
15
+ * idempotency key, approvals become interrupts resumed through `resume`, and the last AG-UI
16
+ * event made from each Runtime event carries its cursor as the SSE id. A run's session is
17
+ * created on its first run with the options in `forwardedProps.nylorun.session` and never
18
+ * changed afterwards. A stream opened with a subject token ends at the token's expiry or
19
+ * revocation with `CUSTOM nylorun.stream_closed`, and the client reattaches with a new token.
20
+ */
21
+ import { randomUUID } from "node:crypto";
22
+ import { EventType, } from "@ag-ui/core";
23
+ import { RunAgentInputSchema } from "@ag-ui/core/schemas";
24
+ import { PutSessionRequestSchema, } from "@nylorun/core/contracts";
25
+ import { messagesFromEvents } from "./history.js";
26
+ import { sessionIdFor } from "./session-id.js";
27
+ import { SSE_CONTENT_TYPE, SSE_HEARTBEAT, sseFrame } from "./sse.js";
28
+ import { RunTranslator } from "./translate.js";
29
+ import { accessOf } from "../../tenant/auth.js";
30
+ import { command } from "../../tenant/commands.js";
31
+ import { loadSession, sessionOf, } from "../../tenant/context.js";
32
+ import { fail, HttpError } from "../../tenant/http.js";
33
+ import { observeSession, readHistory, StreamClosed, } from "../../tenant/session-streams.js";
34
+ import { putSession, sessionView } from "../../tenant/sessions.js";
35
+ import { mayUseAgent } from "../../tenant/tokens.js";
36
+ const HEARTBEAT_MS = 15_000;
37
+ const TERMINAL = new Set([
38
+ "turn.completed",
39
+ "turn.paused",
40
+ "turn.failed",
41
+ "turn.cancelled",
42
+ ]);
43
+ /** The person a request acts for: a subject token's subject, or `Nylorun-Subject`. */
44
+ function personOf(scope) {
45
+ if (scope.kind === "token" || scope.kind === "subject")
46
+ return scope.subject;
47
+ return fail(400, "AG-UI runs act for a person: send a subject token, or Nylorun-Subject with the application key", { code: "invalid_request" });
48
+ }
49
+ function holderOf(scope) {
50
+ return scope.kind === "token"
51
+ ? { subject: scope.subject, epoch: scope.epoch, expiresAt: scope.expiresAt }
52
+ : undefined;
53
+ }
54
+ /** A refusal a client can act on, streamed as `RUN_ERROR` rather than an HTTP error. */
55
+ function runError(error) {
56
+ const code = error.status === 409
57
+ ? "session_busy"
58
+ : (error.rejection.code ?? "request_rejected");
59
+ const message = error.status === 409
60
+ ? "The agent is busy or waiting for an answer to an open interrupt"
61
+ : error.message;
62
+ return { type: EventType.RUN_ERROR, message, code };
63
+ }
64
+ /** The trailing user message: the Runtime keeps the transcript, AG-UI resends all of it. */
65
+ function lastUserMessage(messages) {
66
+ const last = messages.at(-1);
67
+ if (!last || last.role !== "user")
68
+ return fail(400, "The run's last message must be a user message");
69
+ const text = typeof last.content === "string"
70
+ ? last.content
71
+ : last.content
72
+ .filter((part) => part.type === "text")
73
+ .map((part) => part.text)
74
+ .join("\n");
75
+ if (!text.trim())
76
+ return fail(400, "The user message has no text");
77
+ return { id: last.id, text };
78
+ }
79
+ /** `forwardedProps.nylorun.session`: the options a thread's session is created with. */
80
+ function sessionOptions(input, scope) {
81
+ const raw = input.forwardedProps
82
+ ?.nylorun?.session;
83
+ if (raw === undefined)
84
+ return {};
85
+ const parsed = PutSessionRequestSchema.pick({
86
+ info: true,
87
+ vaultIds: true,
88
+ credentialSelections: true,
89
+ })
90
+ .strict()
91
+ .safeParse(raw);
92
+ if (!parsed.success)
93
+ return fail(400, "forwardedProps.nylorun.session is invalid", {
94
+ code: "invalid_request",
95
+ });
96
+ // Agent code may trust `info`: only an app server sets it.
97
+ if (scope.kind === "token" && parsed.data.info !== undefined)
98
+ return fail(403, "A subject token cannot set session info", {
99
+ code: "scope_required",
100
+ });
101
+ return parsed.data;
102
+ }
103
+ async function accepted(ctx, id, body, scope) {
104
+ const result = (await command(ctx, id, body, scope));
105
+ return result?.cursor ?? null;
106
+ }
107
+ async function submit(ctx, id, input, scope) {
108
+ if (input.resume?.length) {
109
+ // The interaction's kind is on the session's pending view, not in its id.
110
+ const view = (await ctx.store.tx(async (t) => sessionView(t, await sessionOf(t, id, accessOf(scope)))));
111
+ const waits = Array.isArray(view.waits) ? view.waits : [];
112
+ let cursor = null;
113
+ for (const entry of input.resume) {
114
+ const wait = waits.find((w) => w?.interaction?.id ===
115
+ entry.interruptId);
116
+ const key = `${input.runId}:${entry.interruptId}`;
117
+ const payload = entry.payload;
118
+ const base = { requestId: randomUUID(), idempotencyKey: key };
119
+ const next = (wait?.interaction?.kind ?? "approval") === "approval"
120
+ ? await accepted(ctx, id, {
121
+ ...base,
122
+ type: "approve",
123
+ interactionId: entry.interruptId,
124
+ approved: entry.status === "resolved" &&
125
+ (payload === true ||
126
+ (typeof payload === "object" && payload?.approved === true)),
127
+ }, scope)
128
+ : entry.status === "cancelled"
129
+ ? await accepted(ctx, id, { ...base, type: "cancel", reason: "interrupt cancelled" }, scope)
130
+ : await accepted(ctx, id, {
131
+ ...base,
132
+ type: "respond",
133
+ interactionId: entry.interruptId,
134
+ value: (entry.payload ?? null),
135
+ }, scope);
136
+ cursor ??= next;
137
+ }
138
+ return cursor;
139
+ }
140
+ const message = lastUserMessage(input.messages);
141
+ // The AG-UI message id is the idempotency key: a retried run replays the same turn, and
142
+ // history gives the message back its client id.
143
+ return accepted(ctx, id, {
144
+ type: "message",
145
+ requestId: randomUUID(),
146
+ idempotencyKey: message.id,
147
+ content: message.text,
148
+ }, scope);
149
+ }
150
+ /**
151
+ * Streams translated events until the run ends. Closing the connection stops the read, never
152
+ * the turn.
153
+ */
154
+ async function stream(response, translator, source, preface = []) {
155
+ const reading = new AbortController();
156
+ response.on("close", () => reading.abort());
157
+ response.writeHead(200, {
158
+ "content-type": SSE_CONTENT_TYPE,
159
+ "cache-control": "no-cache, no-transform",
160
+ "x-accel-buffering": "no",
161
+ });
162
+ response.flushHeaders();
163
+ const write = (text) => {
164
+ if (!response.writableEnded && !response.write(text))
165
+ response.destroy();
166
+ };
167
+ const heartbeat = setInterval(() => write(SSE_HEARTBEAT), HEARTBEAT_MS);
168
+ heartbeat.unref();
169
+ try {
170
+ write(sseFrame(translator.started()));
171
+ for (const event of preface)
172
+ write(sseFrame(event));
173
+ if (source)
174
+ for await (const event of source(reading.signal)) {
175
+ const step = translator.translate(event);
176
+ step.events.forEach((out, index) => write(sseFrame(out, index === step.events.length - 1 ? event.cursor : undefined)));
177
+ if (step.finished || reading.signal.aborted)
178
+ break;
179
+ }
180
+ }
181
+ catch (error) {
182
+ if (!reading.signal.aborted) {
183
+ if (error instanceof StreamClosed)
184
+ write(sseFrame({
185
+ type: EventType.CUSTOM,
186
+ name: "nylorun.stream_closed",
187
+ value: { reason: error.reason },
188
+ }, error.cursor));
189
+ else
190
+ write(sseFrame(error instanceof HttpError
191
+ ? runError(error)
192
+ : {
193
+ type: EventType.RUN_ERROR,
194
+ message: "The agent service failed",
195
+ code: "runtime_error",
196
+ }));
197
+ }
198
+ }
199
+ finally {
200
+ clearInterval(heartbeat);
201
+ reading.abort();
202
+ response.end();
203
+ }
204
+ }
205
+ async function* iterate(items) {
206
+ yield* items;
207
+ }
208
+ /** Every event of a session from `cursor`, following history pages. */
209
+ async function historyFrom(ctx, id, cursor) {
210
+ const items = [];
211
+ let from = cursor;
212
+ for (;;) {
213
+ const page = await readHistory(ctx, id, from, undefined);
214
+ items.push(...page.items);
215
+ if (!page.cursor || page.cursor === from || page.items.length === 0)
216
+ break;
217
+ from = page.cursor;
218
+ }
219
+ return items;
220
+ }
221
+ /**
222
+ * Who an AG-UI request acts for, once its agent is known: a person (a subject token's subject,
223
+ * or `Nylorun-Subject`), who may use the agent; else 400, or the 404 of a missing agent.
224
+ */
225
+ export function agUiCaller(scope, agentId) {
226
+ const subject = personOf(scope);
227
+ // An agent the token may not use is the 404 of a missing one.
228
+ if (!mayUseAgent(scope, agentId))
229
+ return fail(404, "Not found");
230
+ return { subject, access: accessOf(scope) };
231
+ }
232
+ /** `POST /v1/ag-ui/agents/:agent`: start a run, and stream it on `response`. */
233
+ export async function startRun(ctx, scope, agentId, caller, body, response) {
234
+ const parsed = RunAgentInputSchema.safeParse(body);
235
+ if (!parsed.success)
236
+ return fail(400, "Invalid RunAgentInput", { code: "invalid_request" });
237
+ const input = parsed.data;
238
+ // Nylorun tools run in the Runtime or the app's Action endpoint; the browser runs none.
239
+ if (input.tools?.length)
240
+ return fail(400, "Frontend tools are not supported", { code: "invalid_request" });
241
+ const id = sessionIdFor(caller.subject, agentId, input.threadId);
242
+ await putSession(ctx, id, {
243
+ requestId: randomUUID(),
244
+ agentId,
245
+ ownerUserId: caller.subject,
246
+ ...sessionOptions(input, scope),
247
+ }, caller.access, { createOnly: true });
248
+ const translator = new RunTranslator(input.threadId, input.runId);
249
+ let cursor;
250
+ try {
251
+ cursor = await submit(ctx, id, input, scope);
252
+ }
253
+ catch (error) {
254
+ // A busy session or a limit is a run that could not start, not a transport failure.
255
+ if (error instanceof HttpError && (error.status === 409 || error.status === 429))
256
+ return stream(response, translator, undefined, [runError(error)]);
257
+ throw error;
258
+ }
259
+ const holder = holderOf(scope);
260
+ return stream(response, translator, (signal) => observeSession(ctx, id, cursor ?? undefined, {
261
+ signal,
262
+ ...(holder ? { holder } : {}),
263
+ }));
264
+ }
265
+ /** `GET …/threads/:thread/messages`: the thread's messages; none before its first run. */
266
+ export async function threadMessages(ctx, id, access) {
267
+ try {
268
+ await loadSession(ctx, id, access);
269
+ }
270
+ catch (error) {
271
+ // A thread that never ran has no session yet.
272
+ if (error instanceof HttpError && error.status === 404)
273
+ return [];
274
+ throw error;
275
+ }
276
+ return messagesFromEvents(await historyFrom(ctx, id, undefined));
277
+ }
278
+ /**
279
+ * `GET …/threads/:thread/events`: the rest of a run after a dropped connection, streamed on
280
+ * `response`, or 204 when there is nothing to send.
281
+ */
282
+ export async function reattachRun(ctx, scope, access, thread, resume, response) {
283
+ const cursor = resume.lastEventId ??
284
+ resume.cursor ??
285
+ fail(400, "Last-Event-ID or ?cursor= is required", { code: "invalid_request" });
286
+ const session = await loadSession(ctx, thread.id, access);
287
+ const runId = resume.runId ?? session.activeTurnId ?? session.lastTurnId ?? randomUUID();
288
+ const translator = new RunTranslator(thread.threadId, runId, { reattached: true });
289
+ if (session.activeTurnId) {
290
+ const holder = holderOf(scope);
291
+ return stream(response, translator, (signal) => observeSession(ctx, thread.id, cursor, { signal, ...(holder ? { holder } : {}) }));
292
+ }
293
+ // No turn is running: send what the client missed, if the run ended after the cursor.
294
+ const missed = await historyFrom(ctx, thread.id, cursor);
295
+ if (!missed.some((event) => TERMINAL.has(event.type))) {
296
+ response.writeHead(204);
297
+ response.end();
298
+ return;
299
+ }
300
+ return stream(response, translator, () => iterate(missed));
301
+ }
302
+ /** `POST …/threads/:thread/cancel`: cancel the running turn. */
303
+ export async function cancelRun(ctx, id, scope) {
304
+ await accepted(ctx, id, { type: "cancel", requestId: randomUUID(), idempotencyKey: `cancel:${randomUUID()}` }, scope);
305
+ }
@@ -0,0 +1 @@
1
+ export declare function sessionIdFor(subject: string, agentId: string, threadId: string): string;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The session of an AG-UI thread: one per subject, agent and thread, so one subject can never
3
+ * name another's session. The same on every path (an app server's handler or a browser), so a
4
+ * thread continues whichever way it is reached.
5
+ */
6
+ import { createHash } from "node:crypto";
7
+ export function sessionIdFor(subject, agentId, threadId) {
8
+ return createHash("sha256")
9
+ .update(`${subject}\u0000${agentId}\u0000${threadId}`)
10
+ .digest("hex")
11
+ .slice(0, 32);
12
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * AG-UI over server-sent events: one `data:` line of JSON per event. The last AG-UI event
3
+ * made from a Runtime event carries that event's cursor as `id:`, so a client that resumes
4
+ * from `Last-Event-ID` never lands inside a group.
5
+ */
6
+ import type { BaseEvent } from "@ag-ui/core";
7
+ export declare const SSE_CONTENT_TYPE = "text/event-stream";
8
+ export declare function sseFrame(event: BaseEvent, id?: string): string;
9
+ export declare const SSE_HEARTBEAT = ": keep-alive\n\n";
@@ -0,0 +1,7 @@
1
+ export const SSE_CONTENT_TYPE = "text/event-stream";
2
+ export function sseFrame(event, id) {
3
+ // Cursors are opaque base64; a line break would end the field early.
4
+ const idLine = id && !/[\r\n]/.test(id) ? `id: ${id}\n` : "";
5
+ return `${idLine}data: ${JSON.stringify(event)}\n\n`;
6
+ }
7
+ export const SSE_HEARTBEAT = ": keep-alive\n\n";
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Runtime transcript events → AG-UI 1.0 events for one run. Ids: an assistant message is its
3
+ * model call's `invocationId`, a tool call is the model's `callId`, a tool result is
4
+ * `<callId>:result`, so live runs and `messagesFromEvents` agree.
5
+ */
6
+ import { type BaseEvent } from "@ag-ui/core";
7
+ import { type LiveEvent, type TranscriptEvent } from "@nylorun/core/contracts";
8
+ /** The text a tool result shows; `undefined` while there is no result yet. */
9
+ export declare function toolResultText(result: unknown): string | undefined;
10
+ export declare function outputText(output: unknown): string;
11
+ /** One Runtime event's result for a run: AG-UI events, and whether the run ended. */
12
+ export interface Step {
13
+ readonly events: BaseEvent[];
14
+ readonly finished: boolean;
15
+ }
16
+ /** A tool result from any of the three places a call can end. */
17
+ export declare function toolResultOf(event: TranscriptEvent): {
18
+ callId: string;
19
+ content: string;
20
+ } | undefined;
21
+ export declare class RunTranslator {
22
+ private readonly threadId;
23
+ private readonly runId;
24
+ /** Tool invocation → the model's call id, for interrupts that name the invocation. */
25
+ private readonly callIds;
26
+ private readonly closed;
27
+ private sawAssistant;
28
+ /**
29
+ * `reattached`: the run continues from a cursor, so earlier assistant messages already
30
+ * reached the client and a workflow's output is not repeated as text.
31
+ */
32
+ constructor(threadId: string, runId: string, options?: {
33
+ reattached?: boolean;
34
+ });
35
+ translate(raw: LiveEvent): Step;
36
+ started(): BaseEvent;
37
+ private finished;
38
+ }