@nylorun/runtime 0.13.0-beta → 0.14.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 (223) hide show
  1. package/CHANGELOG.md +330 -0
  2. package/README.md +69 -15
  3. package/dist/adapters/execution/restate.d.ts +7 -2
  4. package/dist/adapters/execution/restate.js +38 -8
  5. package/dist/adapters/replication/pgoutput.d.ts +16 -0
  6. package/dist/adapters/replication/pgoutput.js +169 -0
  7. package/dist/adapters/streams/s2.js +37 -20
  8. package/dist/admin-openapi.json +1071 -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 +7825 -0
  32. package/dist/api/components.js +83 -0
  33. package/dist/api/http/app.d.ts +18 -0
  34. package/dist/api/http/app.js +58 -0
  35. package/dist/api/http/body.d.ts +4 -0
  36. package/dist/api/http/body.js +37 -0
  37. package/dist/api/http/define.d.ts +39 -0
  38. package/dist/api/http/define.js +130 -0
  39. package/dist/api/http/respond.d.ts +9 -0
  40. package/dist/api/http/respond.js +44 -0
  41. package/dist/api/http/routes/access.d.ts +24 -0
  42. package/dist/api/http/routes/access.js +259 -0
  43. package/dist/api/http/routes/actions.d.ts +8 -0
  44. package/dist/api/http/routes/actions.js +76 -0
  45. package/dist/api/http/routes/endpoints.d.ts +7 -0
  46. package/dist/api/http/routes/endpoints.js +100 -0
  47. package/dist/api/http/routes/sessions.d.ts +3 -0
  48. package/dist/api/http/routes/sessions.js +205 -0
  49. package/dist/api/http/routes/tenant.d.ts +10 -0
  50. package/dist/api/http/routes/tenant.js +172 -0
  51. package/dist/api/http/routes/vaults.d.ts +8 -0
  52. package/dist/api/http/routes/vaults.js +167 -0
  53. package/dist/api/openapi.d.ts +19 -0
  54. package/dist/api/openapi.js +138 -0
  55. package/dist/api/route.d.ts +12 -0
  56. package/dist/api/route.js +4 -0
  57. package/dist/core/bearer.d.ts +8 -0
  58. package/dist/core/bearer.js +11 -0
  59. package/dist/core/flow-host.d.ts +7 -15
  60. package/dist/core/flow-host.js +16 -41
  61. package/dist/core/sandbox-routes.d.ts +12 -7
  62. package/dist/core/sandbox-routes.js +24 -25
  63. package/dist/execution/memory.d.ts +8 -5
  64. package/dist/execution/memory.js +33 -13
  65. package/dist/execution/types.d.ts +38 -6
  66. package/dist/execution/types.js +12 -3
  67. package/dist/host/admin-api.d.ts +22 -0
  68. package/dist/host/admin-api.js +178 -0
  69. package/dist/host/app.d.ts +34 -0
  70. package/dist/host/app.js +185 -0
  71. package/dist/host/config-for.d.ts +2 -0
  72. package/dist/host/config-for.js +1 -0
  73. package/dist/host/config.d.ts +11 -0
  74. package/dist/host/cors.d.ts +27 -0
  75. package/dist/host/cors.js +64 -0
  76. package/dist/host/create-host.d.ts +31 -3
  77. package/dist/host/create-host.js +168 -327
  78. package/dist/host/http.d.ts +14 -3
  79. package/dist/host/http.js +64 -23
  80. package/dist/host/main.js +55 -4
  81. package/dist/host/stack-config.d.ts +20 -0
  82. package/dist/host/stack-config.js +55 -0
  83. package/dist/mcp/pool.d.ts +19 -1
  84. package/dist/mcp/pool.js +60 -10
  85. package/dist/model/catalog.d.ts +2 -10
  86. package/dist/model/classify.d.ts +13 -0
  87. package/dist/model/classify.js +61 -0
  88. package/dist/model/models.d.ts +10 -1
  89. package/dist/model/models.js +14 -6
  90. package/dist/model/pi-model.d.ts +17 -2
  91. package/dist/model/pi-model.js +366 -222
  92. package/dist/openapi.json +25696 -0
  93. package/dist/sandbox/manager.d.ts +13 -1
  94. package/dist/sandbox/manager.js +8 -11
  95. package/dist/sandbox/resolve.d.ts +33 -0
  96. package/dist/sandbox/resolve.js +81 -0
  97. package/dist/sandbox/session-sandbox.d.ts +11 -0
  98. package/dist/sandbox/session-sandbox.js +51 -0
  99. package/dist/sandbox/share.d.ts +14 -2
  100. package/dist/sandbox/share.js +33 -3
  101. package/dist/sandbox/tenant-config.d.ts +37 -0
  102. package/dist/sandbox/tenant-config.js +51 -0
  103. package/dist/sandbox/tools.d.ts +7 -9
  104. package/dist/sandbox/tools.js +11 -3
  105. package/dist/sandbox/types.d.ts +8 -2
  106. package/dist/store/event.d.ts +19 -0
  107. package/dist/store/event.js +49 -0
  108. package/dist/store/memory.d.ts +20 -8
  109. package/dist/store/memory.js +265 -100
  110. package/dist/store/postgres/connect.d.ts +13 -0
  111. package/dist/store/postgres/connect.js +5 -0
  112. package/dist/store/postgres/migrations/003_subject_tokens.d.ts +8 -0
  113. package/dist/store/postgres/migrations/003_subject_tokens.js +42 -0
  114. package/dist/store/postgres/migrations/004_publishable_keys.d.ts +6 -0
  115. package/dist/store/postgres/migrations/004_publishable_keys.js +18 -0
  116. package/dist/store/postgres/migrations/005_action_endpoints.d.ts +6 -0
  117. package/dist/store/postgres/migrations/005_action_endpoints.js +32 -0
  118. package/dist/store/postgres/migrations/006_drop_executors.d.ts +11 -0
  119. package/dist/store/postgres/migrations/006_drop_executors.js +32 -0
  120. package/dist/store/postgres/migrations/007_event_time.d.ts +7 -0
  121. package/dist/store/postgres/migrations/007_event_time.js +14 -0
  122. package/dist/store/postgres/migrations/008_durable_streams.d.ts +13 -0
  123. package/dist/store/postgres/migrations/008_durable_streams.js +35 -0
  124. package/dist/store/postgres/migrations/index.d.ts +5 -1
  125. package/dist/store/postgres/migrations/index.js +22 -2
  126. package/dist/store/postgres/migrations/shared/index.d.ts +31 -0
  127. package/dist/store/postgres/migrations/shared/index.js +92 -0
  128. package/dist/store/postgres/record.d.ts +11 -0
  129. package/dist/store/postgres/record.js +64 -0
  130. package/dist/store/postgres/store.js +337 -95
  131. package/dist/store/postgres/tenants.js +14 -0
  132. package/dist/store/types.d.ts +176 -81
  133. package/dist/streams/basin.d.ts +18 -1
  134. package/dist/streams/basin.js +34 -2
  135. package/dist/streams/control.d.ts +18 -0
  136. package/dist/streams/control.js +29 -0
  137. package/dist/streams/memory.d.ts +3 -1
  138. package/dist/streams/memory.js +14 -8
  139. package/dist/streams/relay/core.d.ts +35 -0
  140. package/dist/streams/relay/core.js +366 -0
  141. package/dist/streams/relay/memory.d.ts +69 -0
  142. package/dist/streams/relay/memory.js +176 -0
  143. package/dist/streams/relay/types.d.ts +77 -0
  144. package/dist/streams/relay/types.js +7 -0
  145. package/dist/streams/types.d.ts +66 -36
  146. package/dist/streams/types.js +18 -53
  147. package/dist/tenant/access-policy.d.ts +30 -0
  148. package/dist/tenant/access-policy.js +52 -0
  149. package/dist/tenant/actions.d.ts +10 -149
  150. package/dist/tenant/actions.js +5 -167
  151. package/dist/tenant/advance.js +32 -0
  152. package/dist/tenant/auth.d.ts +19 -17
  153. package/dist/tenant/auth.js +90 -93
  154. package/dist/tenant/browser.d.ts +15 -0
  155. package/dist/tenant/browser.js +39 -0
  156. package/dist/tenant/commands.d.ts +18 -2
  157. package/dist/tenant/commands.js +107 -113
  158. package/dist/tenant/context.d.ts +75 -18
  159. package/dist/tenant/context.js +14 -10
  160. package/dist/tenant/delivery-token.d.ts +41 -0
  161. package/dist/tenant/delivery-token.js +105 -0
  162. package/dist/tenant/delivery.d.ts +62 -0
  163. package/dist/tenant/delivery.js +476 -0
  164. package/dist/tenant/effects.d.ts +5 -0
  165. package/dist/tenant/effects.js +78 -37
  166. package/dist/tenant/endpoints.d.ts +67 -0
  167. package/dist/tenant/endpoints.js +90 -0
  168. package/dist/tenant/ephemeral.d.ts +6 -0
  169. package/dist/tenant/ephemeral.js +60 -36
  170. package/dist/tenant/http.d.ts +8 -9
  171. package/dist/tenant/http.js +13 -27
  172. package/dist/tenant/jwt.d.ts +16 -0
  173. package/dist/tenant/jwt.js +50 -0
  174. package/dist/tenant/module.d.ts +3 -0
  175. package/dist/tenant/module.js +6 -12
  176. package/dist/tenant/outbound.d.ts +44 -0
  177. package/dist/tenant/outbound.js +127 -0
  178. package/dist/tenant/pool.d.ts +0 -2
  179. package/dist/tenant/pool.js +0 -18
  180. package/dist/tenant/reset.d.ts +0 -4
  181. package/dist/tenant/reset.js +0 -5
  182. package/dist/tenant/runtime.d.ts +10 -20
  183. package/dist/tenant/runtime.js +59 -42
  184. package/dist/tenant/scheduler.d.ts +7 -2
  185. package/dist/tenant/scheduler.js +7 -3
  186. package/dist/tenant/session-streams.d.ts +125 -0
  187. package/dist/tenant/session-streams.js +399 -0
  188. package/dist/tenant/sessions.d.ts +28 -4
  189. package/dist/tenant/sessions.js +118 -24
  190. package/dist/tenant/signing-keys.d.ts +55 -0
  191. package/dist/tenant/signing-keys.js +172 -0
  192. package/dist/tenant/slim.d.ts +8 -0
  193. package/dist/tenant/slim.js +21 -0
  194. package/dist/tenant/status.d.ts +0 -3
  195. package/dist/tenant/status.js +19 -12
  196. package/dist/tenant/streams.d.ts +60 -67
  197. package/dist/tenant/streams.js +140 -208
  198. package/dist/tenant/subject-limits.d.ts +16 -0
  199. package/dist/tenant/subject-limits.js +45 -0
  200. package/dist/tenant/sweep.d.ts +1 -10
  201. package/dist/tenant/sweep.js +29 -69
  202. package/dist/tenant/tokens.d.ts +14 -0
  203. package/dist/tenant/tokens.js +178 -0
  204. package/dist/tenant/transcript.d.ts +48 -5
  205. package/dist/tenant/transcript.js +58 -1
  206. package/dist/tenant/types.d.ts +29 -7
  207. package/dist/tenant/worker.d.ts +3 -1
  208. package/dist/tenant/worker.js +9 -3
  209. package/dist/vault/service.d.ts +18 -2
  210. package/dist/vault/service.js +57 -13
  211. package/dist/version.d.ts +1 -1
  212. package/dist/version.js +1 -1
  213. package/package.json +20 -8
  214. package/dist/core/executors.d.ts +0 -44
  215. package/dist/core/executors.js +0 -95
  216. package/dist/streams/relay.d.ts +0 -37
  217. package/dist/streams/relay.js +0 -254
  218. package/dist/tenant/live.d.ts +0 -92
  219. package/dist/tenant/live.js +0 -290
  220. package/dist/tenant/routes-tenant.d.ts +0 -12
  221. package/dist/tenant/routes-tenant.js +0 -137
  222. package/dist/tenant/routes.d.ts +0 -9
  223. package/dist/tenant/routes.js +0 -151
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Live delivery over Durable Streams (architecture §12.4, "Reading"): session history
3
+ * (`GET /v1/sessions/:id/items`) and session SSE (`GET /v1/sessions/:id/events`).
4
+ *
5
+ * - **Streams.** A session's events are in `sessions/<id>` in the Tenant's current basin
6
+ * generation (`currentBasin`); cursors carry only the session id and sequence.
7
+ * - **History** reads the session's stream from the cursor up to the tail seen when the read
8
+ * starts. The `agent` filter runs here, and the response cursor is the last record read. If
9
+ * the streams fail, history answers `503`.
10
+ * - **SSE** shares one stream read per observed session in this process (a `SessionStream`)
11
+ * among that session's observers. Each observer keeps the next sequence it needs and skips
12
+ * what it already has, so a client resuming from `Last-Event-ID` sees no gap and no
13
+ * duplicate. An observer behind the feed restarts the shared read from its own position.
14
+ * A feed follows one basin generation: it ends once its session is gone or the Tenant moved
15
+ * to a new generation (a reset, possibly on another node), checked when a client joins, when
16
+ * its read fails, on a `sessions.reset` signal, and periodically (`checkSessionStreams`).
17
+ *
18
+ * Business code never writes here: events reach streams only through the stream relay, from
19
+ * the record, after commit.
20
+ */
21
+ import type { IncomingMessage, ServerResponse } from "node:http";
22
+ import type { LiveEvent } from "@nylorun/core/contracts";
23
+ import type { TenantContext } from "./context.js";
24
+ import type { StreamsWiring } from "./streams.js";
25
+ /** Why the Runtime ended a stream a subject token opened. */
26
+ export type StreamEndReason = "token_expired" | "revoked";
27
+ /** Where an observer's events go: an SSE response, or an in-process reader. */
28
+ export interface ObserverSink {
29
+ write(event: LiveEvent): void;
30
+ /** Ends the stream; with a reason, the client is told why before it ends. */
31
+ end(reason?: StreamEndReason): void;
32
+ }
33
+ /** The subject token a stream was opened with: it ends at expiry or revocation. */
34
+ export interface StreamHolder {
35
+ readonly subject: string;
36
+ readonly epoch: number;
37
+ /** Epoch ms when the token expires. */
38
+ readonly expiresAt: number;
39
+ }
40
+ /** One client of a session: the next sequence it needs. */
41
+ interface Observer {
42
+ readonly sink: ObserverSink;
43
+ next: number;
44
+ readonly holder?: StreamHolder;
45
+ /** Ends the observer when its token expires. */
46
+ deadline?: NodeJS.Timeout;
47
+ }
48
+ /** The shared stream read of one observed session. */
49
+ interface SessionStream {
50
+ readonly sessionId: string;
51
+ /** The basin the feed reads (the Tenant's generation when it started). */
52
+ readonly basin: string;
53
+ readonly stream: string;
54
+ /** The sequence the shared read yields next. Every observer's `next` is at or past it. */
55
+ next: number;
56
+ readonly observers: Set<Observer>;
57
+ /** Aborts the current read; replaced when the read restarts from an earlier sequence. */
58
+ read: AbortController;
59
+ }
60
+ export interface SessionStreams {
61
+ /** Set once by `wireStreams`. */
62
+ wiring: StreamsWiring | undefined;
63
+ /** The Tenant's basin generation as this process last saw it (`wireStreams` keeps it). */
64
+ generation: number;
65
+ /** Session id → its shared read and observers. */
66
+ readonly sessions: Map<string, SessionStream>;
67
+ }
68
+ export declare function createSessionStreams(): SessionStreams;
69
+ export declare function send(response: ServerResponse, data: string): void;
70
+ /** Start an SSE response with keepalives; `onClose` runs once the client goes away. */
71
+ export declare function openSse(request: IncomingMessage, response: ServerResponse, onClose: () => void): void;
72
+ /** The resume cursor: `?cursor=` wins over `Last-Event-ID`. */
73
+ export declare function requestCursor(request: IncomingMessage, url: URL): string | undefined;
74
+ /** The basin the Tenant's session streams are in now. */
75
+ export declare function currentBasin(ctx: TenantContext): string;
76
+ /**
77
+ * `GET /v1/sessions/:id/items`: the session stream after the cursor, up to its tail when the
78
+ * read starts. The cursor returned is the last record read (the request's cursor when none
79
+ * was), whatever the `agent` filter kept.
80
+ */
81
+ export declare function readHistory(ctx: TenantContext, sessionId: string, cursor: string | undefined, agent: string | undefined): Promise<{
82
+ items: LiveEvent[];
83
+ cursor: string | null;
84
+ }>;
85
+ /**
86
+ * `GET /v1/sessions/:id/events`: joins the session's shared read at the client's cursor and
87
+ * follows it until the client leaves, the session is reset or deleted, or the Tenant closes.
88
+ */
89
+ export declare function streamSessionEvents(ctx: TenantContext, request: IncomingMessage, response: ServerResponse, sessionId: string, cursor: string | undefined, holder?: StreamHolder): Promise<void>;
90
+ /** Thrown by `observeSession` when the Runtime ended the stream for its token. */
91
+ export declare class StreamClosed extends Error {
92
+ readonly reason: StreamEndReason;
93
+ /** The cursor of the last event delivered, if any. */
94
+ readonly cursor: string | undefined;
95
+ constructor(reason: StreamEndReason,
96
+ /** The cursor of the last event delivered, if any. */
97
+ cursor: string | undefined);
98
+ }
99
+ /**
100
+ * A session's events from `cursor`, in process: the same shared read and deadlines as an SSE
101
+ * client (`streamSessionEvents`). Ends when `signal` aborts or the feed ends; throws
102
+ * `StreamClosed` when the Runtime ends it for its token (expiry or revocation).
103
+ */
104
+ export declare function observeSession(ctx: TenantContext, sessionId: string, cursor: string | undefined, options: {
105
+ holder?: StreamHolder;
106
+ signal: AbortSignal;
107
+ }): AsyncGenerator<LiveEvent>;
108
+ /**
109
+ * Ends every feed in this process whose session is gone or whose Tenant moved to a new basin
110
+ * generation, and moves this process to the Tenant's current generation. Run on a
111
+ * `sessions.reset` signal and periodically, so feeds on nodes other than the one that reset
112
+ * the Tenant end too.
113
+ */
114
+ export declare function checkSessionStreams(ctx: TenantContext): Promise<void>;
115
+ /**
116
+ * Ends the streams of `subject` opened with a token older than `epoch` (a revocation), on
117
+ * this process. Other processes do the same on the `subject.revoked` signal.
118
+ */
119
+ export declare function endSubjectStreams(hub: SessionStreams, subject: string, epoch: number): void;
120
+ /** Reset or close: end every session observer and stop every shared read. */
121
+ export declare function clearObservers(hub: SessionStreams): void;
122
+ /** Close: end every session observer. */
123
+ export declare function endAllStreams(hub: SessionStreams): void;
124
+ export declare function sleep(ms: number, signal: AbortSignal): Promise<void>;
125
+ export {};
@@ -0,0 +1,399 @@
1
+ import { decodeCursor, encodeCursor } from "../store/cursor.js";
2
+ import { basinOf } from "../streams/basin.js";
3
+ import { sessionStream, } from "../streams/types.js";
4
+ import { fail } from "./http.js";
5
+ export function createSessionStreams() {
6
+ return { wiring: undefined, generation: 0, sessions: new Map() };
7
+ }
8
+ const RETRY_MIN_MS = 100;
9
+ const RETRY_MAX_MS = 2000;
10
+ function frame(event) {
11
+ return `id: ${event.cursor}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`;
12
+ }
13
+ export function send(response, data) {
14
+ if (!response.write(data))
15
+ response.destroy();
16
+ }
17
+ /** The frame that tells a client why the Runtime ended its stream (no `id:`: not an event). */
18
+ function closedFrame(reason) {
19
+ return `event: nylorun.closed\ndata: ${JSON.stringify({ reason })}\n\n`;
20
+ }
21
+ /** An observer sink writing SSE frames to `response`. */
22
+ function sseSink(response) {
23
+ return {
24
+ write: (event) => send(response, frame(event)),
25
+ end: (reason) => {
26
+ if (reason && !response.writableEnded)
27
+ send(response, closedFrame(reason));
28
+ response.end();
29
+ },
30
+ };
31
+ }
32
+ /** Start an SSE response with keepalives; `onClose` runs once the client goes away. */
33
+ export function openSse(request, response, onClose) {
34
+ response.writeHead(200, {
35
+ "content-type": "text/event-stream",
36
+ "cache-control": "no-cache",
37
+ connection: "keep-alive",
38
+ "x-accel-buffering": "no",
39
+ });
40
+ response.flushHeaders();
41
+ const timer = setInterval(() => send(response, ": keepalive\n\n"), 15000);
42
+ timer.unref();
43
+ request.on("close", () => {
44
+ clearInterval(timer);
45
+ onClose();
46
+ });
47
+ }
48
+ /** The resume cursor: `?cursor=` wins over `Last-Event-ID`. */
49
+ export function requestCursor(request, url) {
50
+ return (url.searchParams.get("cursor") ??
51
+ (typeof request.headers["last-event-id"] === "string"
52
+ ? request.headers["last-event-id"]
53
+ : undefined));
54
+ }
55
+ /** The first sequence to read: after the cursor's, or the session's first event. */
56
+ function startSeq(sessionId, cursor) {
57
+ return cursor ? decodeCursor(sessionId, cursor) + 1 : 0;
58
+ }
59
+ /** `agent` keeps only events of one agent used as a tool, by delegationId or path. */
60
+ function belongsTo(event, agent) {
61
+ const ref = event.payload?.agent;
62
+ return ref?.delegationId === agent || ref?.path === agent;
63
+ }
64
+ /** The basin the Tenant's session streams are in now. */
65
+ export function currentBasin(ctx) {
66
+ return basinOf(ctx.config.tenantId, ctx.sessionStreams.generation);
67
+ }
68
+ /** The session's stream now, or undefined when the session is gone. */
69
+ async function currentStream(ctx, sessionId) {
70
+ const session = await ctx.store.tx((t) => t.get("sessions", sessionId));
71
+ return session && { basin: currentBasin(ctx), stream: sessionStream(sessionId) };
72
+ }
73
+ const sameStream = (feed, ref) => ref !== undefined && ref.basin === feed.basin && ref.stream === feed.stream;
74
+ function streamsOf(ctx) {
75
+ return (ctx.sessionStreams.wiring?.streams ?? fail(503, "Session streams are unavailable"));
76
+ }
77
+ /**
78
+ * `GET /v1/sessions/:id/items`: the session stream after the cursor, up to its tail when the
79
+ * read starts. The cursor returned is the last record read (the request's cursor when none
80
+ * was), whatever the `agent` filter kept.
81
+ */
82
+ export async function readHistory(ctx, sessionId, cursor, agent) {
83
+ const from = startSeq(sessionId, cursor);
84
+ const streams = streamsOf(ctx);
85
+ const ref = (await currentStream(ctx, sessionId)) ?? fail(404, "Session not found");
86
+ const items = [];
87
+ let last;
88
+ try {
89
+ for await (const record of streams.read(ref.basin, ref.stream, from, { follow: false })) {
90
+ last = record.seq;
91
+ if (agent === undefined || belongsTo(record.body, agent))
92
+ items.push(record.body);
93
+ }
94
+ }
95
+ catch (error) {
96
+ ctx.config.logger.warn("session history read failed", {
97
+ sessionId,
98
+ message: error instanceof Error ? error.message : String(error),
99
+ });
100
+ fail(503, "Session history is unavailable");
101
+ }
102
+ return {
103
+ items,
104
+ cursor: last !== undefined ? encodeCursor(sessionId, last) : cursor ?? null,
105
+ };
106
+ }
107
+ /**
108
+ * `GET /v1/sessions/:id/events`: joins the session's shared read at the client's cursor and
109
+ * follows it until the client leaves, the session is reset or deleted, or the Tenant closes.
110
+ */
111
+ export async function streamSessionEvents(ctx, request, response, sessionId, cursor, holder) {
112
+ const observer = {
113
+ sink: sseSink(response),
114
+ next: startSeq(sessionId, cursor),
115
+ ...(holder ? { holder } : {}),
116
+ };
117
+ const joined = await join(ctx, sessionId, observer);
118
+ openSse(request, response, () => leave(ctx.sessionStreams, joined, observer));
119
+ armDeadline(ctx.sessionStreams, joined, observer);
120
+ }
121
+ /** Thrown by `observeSession` when the Runtime ended the stream for its token. */
122
+ export class StreamClosed extends Error {
123
+ reason;
124
+ cursor;
125
+ constructor(reason,
126
+ /** The cursor of the last event delivered, if any. */
127
+ cursor) {
128
+ super(`The stream ended: ${reason}`);
129
+ this.reason = reason;
130
+ this.cursor = cursor;
131
+ this.name = "StreamClosed";
132
+ }
133
+ }
134
+ /**
135
+ * A session's events from `cursor`, in process: the same shared read and deadlines as an SSE
136
+ * client (`streamSessionEvents`). Ends when `signal` aborts or the feed ends; throws
137
+ * `StreamClosed` when the Runtime ends it for its token (expiry or revocation).
138
+ */
139
+ export async function* observeSession(ctx, sessionId, cursor, options) {
140
+ const queue = [];
141
+ let ended;
142
+ let wake;
143
+ const notify = () => {
144
+ const resolve = wake;
145
+ wake = undefined;
146
+ resolve?.();
147
+ };
148
+ const observer = {
149
+ sink: {
150
+ write: (event) => {
151
+ queue.push(event);
152
+ notify();
153
+ },
154
+ end: (reason) => {
155
+ ended = reason ? { reason } : {};
156
+ notify();
157
+ },
158
+ },
159
+ next: startSeq(sessionId, cursor),
160
+ ...(options.holder ? { holder: options.holder } : {}),
161
+ };
162
+ const joined = await join(ctx, sessionId, observer);
163
+ armDeadline(ctx.sessionStreams, joined, observer);
164
+ const stop = () => {
165
+ leave(ctx.sessionStreams, joined, observer);
166
+ notify();
167
+ };
168
+ options.signal.addEventListener("abort", stop, { once: true });
169
+ let last = cursor;
170
+ try {
171
+ for (;;) {
172
+ while (queue.length > 0) {
173
+ const event = queue.shift();
174
+ last = event.cursor;
175
+ yield event;
176
+ }
177
+ if (options.signal.aborted)
178
+ return;
179
+ if (ended) {
180
+ if (ended.reason)
181
+ throw new StreamClosed(ended.reason, last);
182
+ return;
183
+ }
184
+ await new Promise((resolve) => (wake = resolve));
185
+ }
186
+ }
187
+ finally {
188
+ options.signal.removeEventListener("abort", stop);
189
+ leave(ctx.sessionStreams, joined, observer);
190
+ }
191
+ }
192
+ /** Ends the observer when its token expires. */
193
+ function armDeadline(hub, feed, observer) {
194
+ if (!observer.holder)
195
+ return;
196
+ observer.deadline = setTimeout(() => endObserver(hub, feed, observer, "token_expired"), Math.max(0, observer.holder.expiresAt - Date.now()));
197
+ observer.deadline.unref();
198
+ }
199
+ /**
200
+ * Adds `observer` to the session's shared read, starting or restarting it from the
201
+ * observer's position when needed. 404 when the session has no stream.
202
+ */
203
+ async function join(ctx, sessionId, observer) {
204
+ const from = observer.next;
205
+ streamsOf(ctx);
206
+ const ref = (await currentStream(ctx, sessionId)) ?? fail(404, "Session not found");
207
+ const hub = ctx.sessionStreams;
208
+ let feed = hub.sessions.get(sessionId);
209
+ if (feed && !sameStream(feed, ref)) {
210
+ // The Tenant moved to a new basin since that feed started: it follows an abandoned stream.
211
+ endFeed(hub, feed);
212
+ feed = undefined;
213
+ }
214
+ if (!feed) {
215
+ feed = {
216
+ sessionId,
217
+ basin: ref.basin,
218
+ stream: ref.stream,
219
+ next: from,
220
+ observers: new Set(),
221
+ read: new AbortController(),
222
+ };
223
+ hub.sessions.set(sessionId, feed);
224
+ runFeed(ctx, feed);
225
+ }
226
+ else if (from < feed.next) {
227
+ // Behind the shared read: restart it here; observers ahead skip what they have.
228
+ feed.read.abort();
229
+ feed.read = new AbortController();
230
+ feed.next = from;
231
+ runFeed(ctx, feed);
232
+ }
233
+ feed.observers.add(observer);
234
+ return feed;
235
+ }
236
+ /** Ends one observer early (its token expired or was revoked). */
237
+ function endObserver(hub, feed, observer, reason) {
238
+ if (!feed.observers.has(observer))
239
+ return;
240
+ leave(hub, feed, observer);
241
+ observer.sink.end(reason);
242
+ }
243
+ function leave(hub, feed, observer) {
244
+ if (observer.deadline)
245
+ clearTimeout(observer.deadline);
246
+ if (!feed.observers.delete(observer))
247
+ return;
248
+ if (feed.observers.size > 0)
249
+ return;
250
+ feed.read.abort();
251
+ if (hub.sessions.get(feed.sessionId) === feed)
252
+ hub.sessions.delete(feed.sessionId);
253
+ }
254
+ function deliver(observer, record) {
255
+ if (record.seq < observer.next)
256
+ return;
257
+ observer.next = record.seq + 1;
258
+ observer.sink.write(record.body);
259
+ }
260
+ /**
261
+ * Follows the feed's stream from `feed.next` for the current `feed.read`, retrying failed
262
+ * reads from where it stopped while the session still has that stream. A read that ends by
263
+ * itself (the stream's Tenant is gone or the streams closed), or a session that is gone or
264
+ * whose Tenant moved to a new basin, ends the observers.
265
+ */
266
+ function runFeed(ctx, feed) {
267
+ const read = feed.read;
268
+ const signal = read.signal;
269
+ void (async () => {
270
+ let delay = RETRY_MIN_MS;
271
+ while (!signal.aborted) {
272
+ const streams = ctx.sessionStreams.wiring?.streams;
273
+ if (!streams)
274
+ break;
275
+ try {
276
+ for await (const record of streams.read(feed.basin, feed.stream, feed.next, { signal })) {
277
+ // A restart replaced this read: the new one owns `feed.next`.
278
+ if (signal.aborted || feed.read !== read)
279
+ return;
280
+ if (record.seq < feed.next)
281
+ continue;
282
+ feed.next = record.seq + 1;
283
+ delay = RETRY_MIN_MS;
284
+ for (const observer of feed.observers)
285
+ deliver(observer, record);
286
+ }
287
+ if (signal.aborted)
288
+ return;
289
+ break;
290
+ }
291
+ catch (error) {
292
+ if (signal.aborted)
293
+ return;
294
+ ctx.config.logger.warn("session stream read failed; retrying", {
295
+ sessionId: feed.sessionId,
296
+ message: error instanceof Error ? error.message : String(error),
297
+ });
298
+ }
299
+ if (!(await stillCurrent(ctx, feed)))
300
+ break;
301
+ await sleep(delay, signal);
302
+ delay = Math.min(delay * 2, RETRY_MAX_MS);
303
+ }
304
+ if (signal.aborted || feed.read !== read)
305
+ return;
306
+ endFeed(ctx.sessionStreams, feed);
307
+ })();
308
+ }
309
+ /** False when the feed's session is gone or the Tenant moved to a new basin. True when unsure. */
310
+ async function stillCurrent(ctx, feed) {
311
+ try {
312
+ return sameStream(feed, await currentStream(ctx, feed.sessionId));
313
+ }
314
+ catch {
315
+ return true;
316
+ }
317
+ }
318
+ /**
319
+ * Ends every feed in this process whose session is gone or whose Tenant moved to a new basin
320
+ * generation, and moves this process to the Tenant's current generation. Run on a
321
+ * `sessions.reset` signal and periodically, so feeds on nodes other than the one that reset
322
+ * the Tenant end too.
323
+ */
324
+ export async function checkSessionStreams(ctx) {
325
+ const feeds = [...ctx.sessionStreams.sessions.values()];
326
+ // Also a backstop for lost `subject.revoked` signals: the holders' current epochs.
327
+ const subjects = new Set();
328
+ for (const feed of feeds)
329
+ for (const observer of feed.observers)
330
+ if (observer.holder)
331
+ subjects.add(observer.holder.subject);
332
+ const { exists, epochs, generation } = await ctx.store.tx(async (t) => {
333
+ const exists = new Set();
334
+ for (const feed of feeds)
335
+ if (await t.get("sessions", feed.sessionId))
336
+ exists.add(feed.sessionId);
337
+ return {
338
+ exists,
339
+ generation: (await t.basinGenerations()).current,
340
+ epochs: subjects.size === 0
341
+ ? new Map()
342
+ : await t.subjectEpochs([...subjects]),
343
+ };
344
+ });
345
+ if (generation !== ctx.sessionStreams.generation)
346
+ ctx.sessionStreams.wiring?.moveTo(generation);
347
+ for (const feed of feeds)
348
+ if (ctx.sessionStreams.sessions.get(feed.sessionId) === feed &&
349
+ !sameStream(feed, exists.has(feed.sessionId)
350
+ ? { basin: currentBasin(ctx), stream: sessionStream(feed.sessionId) }
351
+ : undefined))
352
+ endFeed(ctx.sessionStreams, feed);
353
+ for (const [subject, epoch] of epochs)
354
+ endSubjectStreams(ctx.sessionStreams, subject, epoch);
355
+ }
356
+ /**
357
+ * Ends the streams of `subject` opened with a token older than `epoch` (a revocation), on
358
+ * this process. Other processes do the same on the `subject.revoked` signal.
359
+ */
360
+ export function endSubjectStreams(hub, subject, epoch) {
361
+ for (const feed of [...hub.sessions.values()])
362
+ for (const observer of [...feed.observers])
363
+ if (observer.holder?.subject === subject && observer.holder.epoch < epoch)
364
+ endObserver(hub, feed, observer, "revoked");
365
+ }
366
+ function endFeed(hub, feed) {
367
+ feed.read.abort();
368
+ if (hub.sessions.get(feed.sessionId) === feed)
369
+ hub.sessions.delete(feed.sessionId);
370
+ for (const observer of feed.observers) {
371
+ if (observer.deadline)
372
+ clearTimeout(observer.deadline);
373
+ observer.sink.end();
374
+ }
375
+ feed.observers.clear();
376
+ }
377
+ /** Reset or close: end every session observer and stop every shared read. */
378
+ export function clearObservers(hub) {
379
+ for (const feed of [...hub.sessions.values()])
380
+ endFeed(hub, feed);
381
+ hub.sessions.clear();
382
+ }
383
+ /** Close: end every session observer. */
384
+ export function endAllStreams(hub) {
385
+ clearObservers(hub);
386
+ }
387
+ export function sleep(ms, signal) {
388
+ return new Promise((resolve) => {
389
+ if (signal.aborted)
390
+ return resolve();
391
+ const done = () => {
392
+ clearTimeout(timer);
393
+ signal.removeEventListener("abort", done);
394
+ resolve();
395
+ };
396
+ const timer = setTimeout(done, ms);
397
+ signal.addEventListener("abort", done, { once: true });
398
+ });
399
+ }
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { type PutAgentRequest, type PutSessionRequest } from "@nylorun/core/contracts";
7
7
  import type { Tx } from "../store/types.js";
8
- import { type Session, type TenantContext } from "./context.js";
8
+ import { type Session, type SessionAccess, type TenantContext } from "./context.js";
9
9
  export declare function listDefinitions(ctx: TenantContext): Promise<{
10
10
  agents: {
11
11
  agentId: any;
@@ -14,13 +14,27 @@ export declare function listDefinitions(ctx: TenantContext): Promise<{
14
14
  implementationVersion: string;
15
15
  }[];
16
16
  }>;
17
+ /**
18
+ * The agents a subject token or publishable key may see: id, name and description only, never
19
+ * instructions, tools or MCP servers, and only the agents allowed.
20
+ */
21
+ export declare function listAgentsPublic(ctx: TenantContext, agents: ReadonlySet<string> | "*"): Promise<{
22
+ agents: {
23
+ agentId: string;
24
+ name?: any;
25
+ description?: any;
26
+ }[];
27
+ }>;
17
28
  export declare function putDefinition(ctx: TenantContext, agentId: string, body: PutAgentRequest): Promise<{
18
29
  agentId: string;
19
30
  manifestHash: string;
20
31
  implementationVersion: string;
21
32
  }>;
22
- /** `owner`, when the request acts for a subject, limits the list to that subject's sessions. */
23
- export declare function listSessions(ctx: TenantContext, agentId: string | null, owner?: string): Promise<{
33
+ /**
34
+ * `access`, when the request acts for a person, limits the list to that person's sessions of
35
+ * the agents they may use.
36
+ */
37
+ export declare function listSessions(ctx: TenantContext, agentId: string | null, access?: SessionAccess): Promise<{
24
38
  sessions: {
25
39
  id: string;
26
40
  agentId: string;
@@ -34,7 +48,17 @@ export declare function listSessions(ctx: TenantContext, agentId: string | null,
34
48
  * the request acts for a subject, must be the session's owner: another owner's session id is
35
49
  * the 404 of a missing one (not the 409 of a mismatch), and so are another owner's vaults and
36
50
  * sandboxes.
51
+ *
52
+ * The session's sandbox is decided here and fixed for its life (§5 of Sandboxes v3): shared
53
+ * with `{ session }`, none with `false`, inline, or the Tenant default when omitted. A definition
54
+ * that declares its own sandbox (`.sandbox()`) keeps it and takes no sandbox request.
37
55
  */
38
- export declare function putSession(ctx: TenantContext, id: string, body: PutSessionRequest, owner?: string): Promise<Session>;
56
+ export declare function putSession(ctx: TenantContext, id: string, body: PutSessionRequest, access?: SessionAccess, options?: {
57
+ /**
58
+ * Create the session if it is missing and otherwise leave it as it is (an AG-UI thread's
59
+ * session): an existing one must have the same owner and agent, or it is a 404.
60
+ */
61
+ createOnly?: boolean;
62
+ }): Promise<Session>;
39
63
  /** The session resource returned by `GET`/`PUT /v1/sessions/:id`. */
40
64
  export declare function sessionView(t: Tx, s: Session): Promise<unknown>;