@nylorun/runtime 0.4.0-beta → 0.6.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 (64) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +29 -47
  3. package/dist/adapters/media.d.ts +2 -18
  4. package/dist/adapters/media.js +2 -52
  5. package/dist/adapters/observe.js +1 -1
  6. package/dist/config.d.ts +17 -10
  7. package/dist/configuration.d.ts +3 -0
  8. package/dist/configuration.js +3 -0
  9. package/dist/contracts.d.ts +5 -166
  10. package/dist/core/main.js +40 -0
  11. package/dist/core/provider.d.ts +12 -0
  12. package/dist/core/provider.js +60 -0
  13. package/dist/core/runtime.d.ts +50 -0
  14. package/dist/core/runtime.js +877 -0
  15. package/dist/core/store.d.ts +16 -0
  16. package/dist/core/store.js +101 -0
  17. package/dist/index.d.ts +6 -11
  18. package/dist/index.js +4 -6
  19. package/dist/media.d.ts +29 -0
  20. package/dist/media.js +53 -0
  21. package/dist/model/defaults.d.ts +17 -0
  22. package/dist/model/defaults.js +21 -0
  23. package/dist/model/http-model.d.ts +12 -0
  24. package/dist/model/http-model.js +299 -0
  25. package/dist/model/pi-model.d.ts +2 -1
  26. package/dist/model/pi-model.js +52 -7
  27. package/dist/node/index.d.ts +5 -0
  28. package/dist/node/index.js +5 -0
  29. package/dist/node/local-sessions.d.ts +5 -0
  30. package/dist/node/local-sessions.js +174 -0
  31. package/dist/redact.d.ts +1 -0
  32. package/dist/redact.js +14 -0
  33. package/dist/server/ag-ui.d.ts +1 -1
  34. package/dist/server/delivery.d.ts +24 -0
  35. package/dist/server/delivery.js +107 -0
  36. package/dist/server/host.d.ts +50 -7
  37. package/dist/server/host.js +304 -307
  38. package/dist/session/api.d.ts +10 -0
  39. package/dist/session/api.js +15 -0
  40. package/dist/session/default.d.ts +5 -0
  41. package/dist/session/default.js +30 -0
  42. package/dist/session/handle.d.ts +27 -0
  43. package/dist/session/handle.js +199 -0
  44. package/dist/session/index.d.ts +2 -0
  45. package/dist/session/index.js +2 -0
  46. package/dist/sessions/host.d.ts +39 -0
  47. package/dist/sessions/host.js +359 -0
  48. package/dist/sessions/store.d.ts +41 -0
  49. package/dist/sessions/store.js +33 -0
  50. package/package.json +23 -12
  51. package/dist/adapters/journal.d.ts +0 -35
  52. package/dist/adapters/journal.js +0 -130
  53. package/dist/cli.d.ts +0 -2
  54. package/dist/cli.js +0 -100
  55. package/dist/dev-entry.js +0 -2
  56. package/dist/dev.d.ts +0 -2
  57. package/dist/dev.js +0 -126
  58. package/dist/environment.d.ts +0 -2
  59. package/dist/environment.js +0 -64
  60. package/dist/launcher.d.ts +0 -1
  61. package/dist/launcher.js +0 -28
  62. package/dist/model/configure.d.ts +0 -12
  63. package/dist/model/configure.js +0 -155
  64. /package/dist/{dev-entry.d.ts → core/main.d.ts} +0 -0
@@ -1,28 +1,52 @@
1
- import { randomUUID } from "node:crypto";
2
1
  import { Hono } from "hono";
3
2
  import { cors } from "hono/cors";
4
3
  import { HTTPException } from "hono/http-exception";
5
- import { agUiEvents, sse } from "./ag-ui.js";
6
- import { observedPayload } from "./digests.js";
7
- import { localJsonl, scrub } from "../adapters/journal.js";
8
- import { jsonlObserver } from "../adapters/observe.js";
9
- import { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, } from "../adapters/media.js";
10
- import { projectSecrets } from "../model/settings.js";
11
- import { piModel } from "../model/pi-model.js";
4
+ import { agUiEvents } from "./ag-ui.js";
5
+ import { EventDelivery } from "./delivery.js";
6
+ import { scrub } from "../redact.js";
7
+ import { memorySessions } from "../sessions/store.js";
8
+ import { SessionHost } from "../sessions/host.js";
9
+ import { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, } from "../media.js";
10
+ import { defaultModel, processEnvironment, registerRuntimeLifecycle, } from "../model/defaults.js";
11
+ import { createSessionHandle, } from "../session/handle.js";
12
+ import { installDefaultRuntimeFactory } from "../session/default.js";
13
+ const randomUUID = () => crypto.randomUUID();
12
14
  const kServe = Symbol("serve");
13
15
  export class Runtime {
14
- #config;
15
- #served = false;
16
- #closing;
17
- #shutdown;
18
- constructor(options = {}) {
19
- this.#config = options;
16
+ config;
17
+ host;
18
+ served = false;
19
+ closing;
20
+ constructor(config = {}) {
21
+ this.config = config;
22
+ this.host = new SessionHost(config.sessions ?? memorySessions());
23
+ registerRuntimeLifecycle(this.close);
24
+ }
25
+ close = () => (this.closing ??= this.host.close());
26
+ resolveModel(onPreview) {
27
+ return (this.config.onModelCall ??
28
+ (this.config.createModel ?? defaultModel)({
29
+ environment: this.config.environment ?? processEnvironment(),
30
+ onPreview,
31
+ media: this.config.media,
32
+ }));
33
+ }
34
+ openSession(agent, options) {
35
+ return createSessionHandle(this, agent, options);
36
+ }
37
+ async listSessions(agentId) {
38
+ return this.host.list(agentId);
39
+ }
40
+ async getSession(agentId, sessionId) {
41
+ return this.host.read(agentId, sessionId);
42
+ }
43
+ async deleteSession(agentId, sessionId) {
44
+ return this.host.delete(agentId, sessionId);
20
45
  }
21
- close = () => (this.#closing ??= this.#shutdown?.() ?? Promise.resolve());
22
46
  [kServe](options) {
23
- if (this.#served)
47
+ if (this.served)
24
48
  throw new Error("A Runtime may only be served once.");
25
- this.#served = true;
49
+ this.served = true;
26
50
  const agents = [...options.agents];
27
51
  const byId = new Map(agents.map((agent) => [agent.id, agent]));
28
52
  if (byId.size !== agents.length)
@@ -33,15 +57,9 @@ export class Runtime {
33
57
  if (agent.id !== agent.manifest.id || agent.name !== agent.manifest.name)
34
58
  throw new Error("Agent identity must match its manifest.");
35
59
  }
36
- const media = this.#config.media;
37
- const onModelCall = this.#config.onModelCall ?? piModel({ media });
38
- const journal = this.#config.durability ?? localJsonl();
39
- const configuredObserver = this.#config.observer;
40
- const redact = (value) => scrub(value, projectSecrets());
41
- const live = new Map();
42
- const routerOptions = options;
60
+ const { media } = this.config;
43
61
  const app = new Hono();
44
- if (process.env.NYLORUN_DEV === "1") {
62
+ if (processEnvironment().NYLORUN_DEV === "1")
45
63
  app.use("*", cors({
46
64
  origin: (origin) => {
47
65
  try {
@@ -56,339 +74,299 @@ export class Runtime {
56
74
  return undefined;
57
75
  }
58
76
  },
59
- allowMethods: [
60
- "GET",
61
- "HEAD",
62
- "POST",
63
- "PUT",
64
- "PATCH",
65
- "DELETE",
66
- "OPTIONS",
67
- ],
68
77
  }));
69
- }
70
- app.onError((error, context) => context.json({ error: String(redact(error.message)) }, error instanceof HTTPException ? error.status : 500));
78
+ app.onError((error, context) => context.json({
79
+ error: String(scrub(error.message, secretValues(environment(context)))),
80
+ }, error instanceof HTTPException ? error.status : 500));
81
+ // Application authorization/info resolution precedes all store and media access.
71
82
  app.use("*", async (context, next) => {
83
+ const actor = await options.getActor?.(context);
84
+ const info = options.getInfo
85
+ ? await options.getInfo(context)
86
+ : actor
87
+ ? { ...actor.context, userId: actor.id }
88
+ : undefined;
89
+ context.set("info", info);
90
+ if (options.getEnvironment)
91
+ context.set("modelEnvironment", await options.getEnvironment(context));
72
92
  await next();
73
93
  if (context.res.headers.get("content-type")?.includes("application/json")) {
74
94
  const value = await context.res.json();
75
- context.res = new Response(JSON.stringify(redact(value)), {
76
- status: context.res.status,
77
- headers: context.res.headers,
78
- });
95
+ context.res = new Response(JSON.stringify(scrub(value, secretValues(environment(context)))), { status: context.res.status, headers: context.res.headers });
79
96
  }
80
97
  });
81
- // Infer the public mount from this request URL and the local route path.
82
- // Do not use hono/route basePath: consumers often install a separate `hono`
83
- // copy, and that helper's match-result Symbol then misses the parent's match.
84
- const publicPath = (context, routePath, path) => `${normalizeBasePath(routerOptions.basePath ?? inferMountPath(context, routePath))}${path}`;
98
+ const environment = (context) => context.get("modelEnvironment") ??
99
+ this.config.environment ??
100
+ bindingsEnvironment(context) ??
101
+ processEnvironment();
102
+ const path = (context, route, suffix) => `${normalizeBasePath(options.basePath ?? inferMountPath(context, route))}${suffix}`;
103
+ const agentFor = (context) => {
104
+ const agent = byId.get(context.req.param("agentId"));
105
+ if (!agent)
106
+ throw new HTTPException(404, { message: "unknown agent" });
107
+ const session = context.req.param("session");
108
+ if (session !== undefined && !isSessionId(session))
109
+ throw new HTTPException(400, { message: "Invalid session identifier" });
110
+ return agent;
111
+ };
112
+ const submitOptions = (context, sessionId, onPreview) => {
113
+ // Leave undefined on Node so defaultModel can use the installed piModel
114
+ // factory. Hono's Node adapter puts stream bindings on context.env; those
115
+ // must not be treated as Workers-style provider bindings.
116
+ const explicitEnvironment = context.get("modelEnvironment") ??
117
+ this.config.environment ??
118
+ bindingsEnvironment(context);
119
+ return {
120
+ info: { ...(context.get("info") ?? {}), sessionId },
121
+ onEvent: (event) => {
122
+ try {
123
+ void Promise.resolve(this.config.observer?.({ type: event.type, ...event.payload })).catch(() => { });
124
+ }
125
+ catch {
126
+ /* Observation cannot acknowledge persistence. */
127
+ }
128
+ },
129
+ onModelCall: this.config.onModelCall ??
130
+ (this.config.createModel ?? defaultModel)({
131
+ environment: explicitEnvironment,
132
+ onPreview,
133
+ media,
134
+ }),
135
+ secrets: secretValues(environment(context)),
136
+ signal: context.req.raw.signal,
137
+ };
138
+ };
85
139
  app.get("/v1/agents", (context) => context.json({
86
140
  protocolVersion: 2,
87
141
  agents: agents.map((agent) => ({
88
142
  id: agent.id,
89
- manifestUrl: publicPath(context, "/v1/agents", `/${agent.id}/manifest.json`),
143
+ manifestUrl: path(context, "/v1/agents", `/${agent.id}/manifest.json`),
90
144
  })),
91
145
  }));
92
- app.get("/:agentId/manifest.json", (context) => {
93
- const agent = byId.get(context.req.param("agentId"));
94
- return agent === undefined
95
- ? context.json({ error: "unknown agent" }, 404)
96
- : context.json(manifest(agent, media !== undefined, (path) => publicPath(context, "/:agentId/manifest.json", path)));
97
- });
146
+ app.get("/:agentId/manifest.json", (context) => context.json(manifest(agentFor(context), media !== undefined, (suffix) => path(context, "/:agentId/manifest.json", suffix))));
98
147
  app.get("/:agentId/v1/media/:session/:assetId", async (context) => {
99
- const agent = requireAgent(context.req.param("agentId"));
100
- if (!agent)
101
- return context.json({ error: "unknown agent" }, 404);
148
+ const agent = agentFor(context);
102
149
  const asset = await media?.read(agent.id, context.req.param("session"), context.req.param("assetId"));
103
150
  if (!asset)
104
151
  return context.json({ error: "unknown media asset" }, 404);
105
- context.header("cache-control", "no-store");
106
152
  return context.body(asset.bytes, 200, {
107
153
  "content-type": asset.asset.mediaType,
154
+ "cache-control": "no-store",
108
155
  });
109
156
  });
110
- app.get("/:agentId/v1/sessions", async (context) => {
111
- const agent = requireAgent(context.req.param("agentId"));
112
- if (agent === undefined)
113
- return context.json({ error: "unknown agent" }, 404);
114
- const listed = await journal.list(agent.id);
115
- return context.json({
116
- sessions: listed.map((summary) => {
117
- const found = live.get(keyOf(agent.id, summary.session));
118
- if (found?.status === "running")
119
- return { ...summary, status: "running" };
120
- if (found?.status === "waiting")
121
- return { ...summary, status: "waiting" };
122
- return summary;
123
- }),
124
- });
125
- });
157
+ app.get("/:agentId/v1/sessions", async (context) => context.json({ sessions: await this.host.list(agentFor(context).id) }));
126
158
  app.get("/:agentId/v1/sessions/:session", async (context) => {
127
- const agent = requireAgent(context.req.param("agentId"));
128
- if (!agent)
129
- return context.json({ error: "unknown agent" }, 404);
130
- const key = keyOf(agent.id, context.req.param("session"));
131
- const found = live.get(key);
132
- const events = found?.events ??
133
- (await journal.events(agent.id, context.req.param("session")));
134
- if (!found && events.length === 0)
159
+ const document = await this.host.read(agentFor(context).id, context.req.param("session"));
160
+ if (!document)
135
161
  return context.json({ error: "unknown session" }, 404);
136
162
  return context.json({
137
- id: context.req.param("session"),
138
- state: found?.status ?? status(events),
139
- pending_interaction: pending(events),
163
+ id: document.id,
164
+ state: document.status,
165
+ pending_interaction: document.state?.plan?.calls.find((call) => call.status === "interaction")?.interaction,
166
+ pending_waits: document.state?.plan?.calls
167
+ .filter((call) => (call.status === "interaction" || call.status === "deferred") &&
168
+ call.wait)
169
+ .map((call) => call.wait),
140
170
  });
141
171
  });
172
+ app.delete("/:agentId/v1/sessions/:session", async (context) => {
173
+ const agent = agentFor(context);
174
+ const sessionId = context.req.param("session");
175
+ const deleted = await this.host.delete(agent.id, sessionId);
176
+ if (!deleted)
177
+ return context.json({ error: "unknown session" }, 404);
178
+ return context.json({ session_id: sessionId, deleted: true });
179
+ });
142
180
  app.post("/:agentId/v1/sessions/:session", async (context) => {
143
- const agent = requireAgent(context.req.param("agentId"));
144
- if (!agent)
145
- return context.json({ error: "unknown agent" }, 404);
146
- const found = live.get(keyOf(agent.id, context.req.param("session")));
147
- if (!found)
148
- return context.json({ error: "session is no longer live" }, 409);
149
- const payload = await context.req
150
- .json()
151
- .catch(() => undefined);
152
- const interaction = payload?.interaction;
153
- if (!interaction || typeof interaction.id !== "string")
154
- return context.json({ error: "expected interaction" }, 400);
155
- const waiting = pending(found.events);
156
- if (found.status !== "waiting" ||
157
- !waiting ||
158
- waiting.id !== interaction.id)
159
- return context.json({ error: "interaction is no longer pending" }, 409);
160
- if (interaction.kind === "approval") {
161
- if (typeof interaction.approved !== "boolean")
162
- return context.json({ error: "expected approval interaction" }, 400);
163
- found.status = "running";
164
- await submit(found, {
181
+ const agent = agentFor(context), sessionId = context.req.param("session");
182
+ const payload = await context.req.json();
183
+ const interaction = payload.interaction;
184
+ let input;
185
+ if (payload.action === "cancel") {
186
+ await this.host.cancel(agent, sessionId);
187
+ return context.json({ session_id: sessionId, state: "cancelled" });
188
+ }
189
+ if (payload.action === "interrupt") {
190
+ const result = await this.host.interrupt(agent, sessionId, payload.input, submitOptions(context, sessionId));
191
+ return context.json({
192
+ session_id: sessionId,
193
+ state: result.status === "paused" ? "waiting" : result.status,
194
+ }, 202);
195
+ }
196
+ if (payload.settlement)
197
+ input = { kind: "settle", ...payload.settlement };
198
+ else if (payload.action === "wait-resolve" ||
199
+ payload.waitResolve ||
200
+ (interaction?.kind === "wait-resolve" &&
201
+ typeof interaction.waitId === "string")) {
202
+ const wait = payload.waitResolve ??
203
+ (interaction?.kind === "wait-resolve" ? interaction : payload);
204
+ if (typeof wait.waitId !== "string")
205
+ return context.json({ error: "wait-resolve requires waitId" }, 400);
206
+ input = {
207
+ kind: "wait-resolve",
208
+ waitId: wait.waitId,
209
+ ...("value" in wait ? { value: wait.value } : {}),
210
+ };
211
+ }
212
+ else if (interaction?.kind === "approval" &&
213
+ typeof interaction.id === "string" &&
214
+ typeof interaction.approved === "boolean")
215
+ input = {
165
216
  kind: "approve",
166
217
  interactionId: interaction.id,
167
218
  approved: interaction.approved,
168
- });
169
- return context.json({ session_id: context.req.param("session"), state: found.status }, 202);
170
- }
171
- if (interaction.kind === "respond") {
172
- if (!("value" in interaction))
173
- return context.json({ error: "expected respond interaction" }, 400);
174
- found.status = "running";
175
- await submit(found, {
219
+ };
220
+ else if (interaction?.kind === "respond" &&
221
+ typeof interaction.id === "string" &&
222
+ "value" in interaction)
223
+ input = {
176
224
  kind: "respond",
177
225
  interactionId: interaction.id,
178
226
  value: interaction.value,
179
- });
180
- return context.json({ session_id: context.req.param("session"), state: found.status }, 202);
181
- }
182
- return context.json({ error: "expected approval or respond interaction" }, 400);
227
+ };
228
+ else
229
+ return context.json({
230
+ error: "expected a correlated interaction, settlement, or wait-resolve",
231
+ }, 400);
232
+ const document = await this.host.read(agent.id, sessionId);
233
+ if (!document || document.status !== "waiting")
234
+ return context.json({ error: "interaction is no longer pending" }, 409);
235
+ const result = await this.host.submit(agent, sessionId, input, submitOptions(context, sessionId));
236
+ return context.json({
237
+ session_id: sessionId,
238
+ state: result.status === "paused" ? "waiting" : result.status,
239
+ }, 202);
183
240
  });
184
241
  app.get("/:agentId/v1/sessions/:session/events", async (context) => {
185
- const agent = requireAgent(context.req.param("agentId"));
186
- if (!agent)
187
- return context.json({ error: "unknown agent" }, 404);
242
+ const document = await this.host.read(agentFor(context).id, context.req.param("session"));
188
243
  const after = Number(context.req.query("after") ?? "0");
189
- const session = context.req.param("session");
190
- const events = live.get(keyOf(agent.id, session))?.events ??
191
- (await journal.events(agent.id, session));
244
+ if (!Number.isSafeInteger(after) || after < 0)
245
+ return context.json({ error: "Invalid event cursor" }, 400);
246
+ const events = document?.events ?? [];
192
247
  return context.json({
193
248
  events: events.filter((event) => event.seq > after),
194
249
  next_cursor: events.at(-1)?.seq ?? after,
195
250
  });
196
251
  });
197
252
  app.get("/:agentId/v1/ag-ui/sessions/:session", async (context) => {
198
- const agent = requireAgent(context.req.param("agentId"));
199
- if (!agent)
200
- return context.json({ error: "unknown agent" }, 404);
201
- const found = live.get(keyOf(agent.id, context.req.param("session")));
253
+ const agent = agentFor(context);
202
254
  return context.json({
203
- messages: found?.messages ??
204
- messages(agent.id, await journal.events(agent.id, context.req.param("session"))),
255
+ messages: messages(agent.id, (await this.host.read(agent.id, context.req.param("session")))
256
+ ?.events ?? []),
205
257
  });
206
258
  });
207
259
  app.post("/:agentId/v1/ag-ui", async (context) => {
208
- const agent = requireAgent(context.req.param("agentId"));
209
- if (!agent)
210
- return context.json({ error: "unknown agent" }, 404);
211
- const payload = await context.req
212
- .json()
213
- .catch(() => undefined);
214
- const threadId = typeof payload?.threadId === "string" && payload.threadId
260
+ const agent = agentFor(context);
261
+ const payload = await context.req.json();
262
+ const threadId = typeof payload.threadId === "string" && payload.threadId
215
263
  ? payload.threadId
216
264
  : randomUUID();
217
265
  if (!isSessionId(threadId))
218
- return context.json({
219
- error: "threadId may only contain letters, digits, '.', '_' and '-'",
220
- }, 400);
221
- const runId = typeof payload?.runId === "string" && payload.runId
266
+ return context.json({ error: "Invalid threadId" }, 400);
267
+ const runId = typeof payload.runId === "string" && payload.runId
222
268
  ? payload.runId
223
269
  : randomUUID();
224
- let message;
225
- let actor;
226
- try {
227
- actor = await routerOptions.getActor?.(context);
228
- const metadata = await routerOptions.getRequestMetadata?.(context);
229
- message = await latestMessage(payload?.messages, media, agent.id, threadId, metadata);
230
- }
231
- catch (error) {
232
- return context.json({
233
- error: error instanceof Error
234
- ? error.message
235
- : "Invalid AG-UI user message",
236
- }, 400);
237
- }
238
- const found = await begin(agent, threadId, message, actor);
239
- const start = found.events.length;
240
- await submit(found, message.input);
241
- return sse(agUiEvents(found.events.slice(start), threadId, runId), context.res.headers);
242
- });
243
- function requireAgent(id) {
244
- return byId.get(id);
245
- }
246
- async function begin(agent, sessionId, message, actor) {
247
- const key = keyOf(agent.id, sessionId);
248
- const existing = live.get(key);
249
- if (existing) {
250
- existing.messages.push(message.chat);
251
- return existing;
252
- }
253
- const archived = await journal.events(agent.id, sessionId);
254
- const concurrent = live.get(key);
255
- if (concurrent) {
256
- concurrent.messages.push(message.chat);
257
- return concurrent;
258
- }
259
- if (archived.length)
260
- throw new HTTPException(409, {
261
- message: "This session is archived. Start a new conversation.",
262
- });
263
- let entry;
264
- const observer = configuredObserver ?? jsonlObserver({ agentId: agent.id, sessionId });
265
- const session = agent.run({
266
- id: sessionId,
267
- onModelCall,
268
- observer: (event) => {
269
- const image = generatedImageMessage(event, agent.id, sessionId);
270
- if (image)
271
- entry.messages.push(image);
272
- add(entry, agent.id, event.type, observedPayload(event));
273
- void Promise.resolve(observer(event)).catch(() => { });
270
+ const message = await latestMessage(payload.messages, media, agent.id, threadId, await options.getRequestMetadata?.(context));
271
+ const controller = new AbortController();
272
+ const abort = () => controller.abort(context.req.raw.signal.reason);
273
+ context.req.raw.signal.addEventListener("abort", abort, { once: true });
274
+ if (context.req.raw.signal.aborted)
275
+ abort();
276
+ const delivery = new EventDelivery(() => controller.abort(new Error("Delivery connection ended")), this.config.delivery, context.res.headers);
277
+ delivery.push({ type: "RUN_STARTED", threadId, runId });
278
+ const modelOptions = submitOptions(context, threadId, this.config.tokens
279
+ ? (preview) => delivery.push({
280
+ type: "CUSTOM",
281
+ name: "nylorun.preview",
282
+ value: { ...preview, runId },
283
+ }, preview.invocationId)
284
+ : undefined);
285
+ const task = this.host.submit(agent, threadId, message.input, {
286
+ ...modelOptions,
287
+ runId,
288
+ signal: controller.signal,
289
+ started: {
290
+ input_kind: "user-message",
291
+ message: message.chat,
292
+ ...(firstText(message.chat) === undefined
293
+ ? {}
294
+ : { input: firstText(message.chat) }),
274
295
  },
275
- ...(actor === undefined ? {} : { userId: actor.id }),
276
- ...(actor?.context === undefined ? {} : { context: actor.context }),
277
- });
278
- entry = {
279
- session,
280
- events: [],
281
- messages: [message.chat],
282
- status: "running",
283
- sequence: 0,
284
- writes: Promise.resolve(),
285
- };
286
- live.set(key, entry);
287
- return entry;
288
- }
289
- function add(entry, agentId, type, payload) {
290
- const event = {
291
- session: entry.session.id,
292
- seq: ++entry.sequence,
293
- ts: new Date().toISOString(),
294
- type,
295
- payload: redact(payload),
296
- };
297
- entry.events.push(event);
298
- entry.writes = entry.writes.then(() => journal.append(agentId, event));
299
- // The submit/close paths observe durability errors; avoid an unhandled rejection meanwhile.
300
- void entry.writes.catch(() => { });
301
- }
302
- async function submit(entry, input) {
303
- const agentId = [...live]
304
- .find(([, value]) => value === entry)[0]
305
- .split(":")[0];
306
- entry.status = "running";
307
- const inputEvent = typeof input === "string"
308
- ? { kind: "user-message", text: input }
309
- : "kind" in input
310
- ? input
311
- : { kind: "user-message", ...input };
312
- const message = chatFromInput(inputEvent, agentId, entry.session.id);
313
- add(entry, agentId, "session.run.started", {
314
- input_kind: inputEvent.kind,
315
- input: message ? firstText(message) : undefined,
316
- ...(message ? { message } : {}),
317
- ...("approved" in inputEvent ? { approved: inputEvent.approved } : {}),
318
- ...("value" in inputEvent ? { value: inputEvent.value } : {}),
319
- });
320
- const start = entry.events.length;
321
- try {
322
- const result = await entry.session.input(input).completed;
323
- for (const event of result.events) {
324
- if (event.type === "final" && event.output !== undefined) {
325
- entry.messages.push({
326
- id: randomUUID(),
327
- role: "assistant",
328
- content: finalContent(event.output),
329
- });
330
- add(entry, agentId, "final", { output: event.output });
331
- }
332
- else if (event.type === "interaction.required") {
333
- add(entry, agentId, event.type, { interaction: event.interaction });
334
- }
335
- }
336
- const observedFailure = entry.events
337
- .slice(start)
338
- .some((event) => event.type === "error" || event.type === "tripwire");
339
- const completionFailure = result.events.find((event) => event.type === "error" || event.type === "tripwire");
340
- const failed = observedFailure ||
341
- completionFailure !== undefined ||
342
- (result.status !== "completed" && result.status !== "waiting");
343
- if (!observedFailure && completionFailure) {
344
- add(entry, agentId, completionFailure.type, observedPayload(completionFailure));
345
- }
346
- else if (!observedFailure && failed) {
347
- add(entry, agentId, "error", {
348
- message: `Agent run ${result.status}.`,
296
+ onEvent: (event) => {
297
+ delivery.push({
298
+ type: "CUSTOM",
299
+ name: "nylorun.execution",
300
+ value: event,
349
301
  });
350
- }
351
- entry.status = failed
352
- ? "failed"
353
- : result.status === "waiting"
354
- ? "waiting"
355
- : "completed";
356
- await entry.writes;
357
- }
358
- catch (error) {
359
- entry.status = "failed";
360
- add(entry, agentId, "error", {
361
- message: error instanceof Error ? error.message : String(error),
302
+ for (const projected of agUiEvents([event], threadId, runId))
303
+ if (!["RUN_STARTED", "RUN_FINISHED"].includes(String(projected.type)))
304
+ delivery.push(projected);
305
+ modelOptions.onEvent(event);
306
+ },
307
+ });
308
+ void task
309
+ .then((result) => {
310
+ delivery.push({
311
+ type: "CUSTOM",
312
+ name: "nylorun.preview.settled",
313
+ value: { runId, status: result.status },
362
314
  });
363
- await entry.writes;
364
- throw error;
365
- }
366
- }
367
- this.#shutdown = async () => {
368
- const sessions = await Promise.allSettled([...live.values()].map(async (entry) => {
369
- try {
370
- await entry.session.stop();
371
- await entry.writes;
372
- }
373
- finally {
374
- }
375
- }));
376
- const resources = await Promise.allSettled(agents.map((agent) => agent.close?.()));
377
- const failure = [...sessions, ...resources].find((result) => result.status === "rejected");
378
- if (failure?.status === "rejected")
379
- throw failure.reason;
380
- };
315
+ if (result.status !== "failed")
316
+ delivery.push({ type: "RUN_FINISHED", threadId, runId });
317
+ }, (error) => {
318
+ delivery.push({
319
+ type: "RUN_ERROR",
320
+ message: String(scrub(error instanceof Error ? error.message : String(error), modelOptions.secrets)),
321
+ });
322
+ })
323
+ .finally(() => {
324
+ context.req.raw.signal.removeEventListener("abort", abort);
325
+ delivery.end();
326
+ });
327
+ return delivery.response;
328
+ });
381
329
  return app;
382
330
  }
383
331
  }
384
- /** Create the Hono protocol mount for a runtime. Applications may close it during graceful shutdown. */
385
- // `any` avoids leaking a second physical Hono installation through a peer boundary.
386
- // The returned value is the caller's Hono router at runtime.
387
332
  export function serveAgents(options) {
388
- if (!(options.runtime instanceof Runtime))
389
- throw new Error("serveAgents requires a Runtime.");
390
- const { runtime, ...rest } = options;
391
- return runtime[kServe](rest);
333
+ if ("runtime" in options && options.runtime) {
334
+ const { runtime, ...rest } = options;
335
+ return runtime[kServe]({
336
+ ...rest,
337
+ agents: options.agents ?? [],
338
+ });
339
+ }
340
+ const runtime = new Runtime({});
341
+ const getInfo = options.getInfo ?? ("info" in options ? options.info : undefined);
342
+ const app = runtime[kServe]({
343
+ ...options,
344
+ agents: options.agents ?? [],
345
+ ...(getInfo ? { getInfo } : {}),
346
+ });
347
+ return {
348
+ fetch: (request) => app.fetch(request),
349
+ };
350
+ }
351
+ installDefaultRuntimeFactory(() => new Runtime({}));
352
+ /** Workers-style provider bindings on context.env; ignore Hono Node stream slots. */
353
+ function bindingsEnvironment(context) {
354
+ const env = context.env;
355
+ if (!env || typeof env !== "object")
356
+ return undefined;
357
+ const keys = Object.keys(env);
358
+ if (!keys.length)
359
+ return undefined;
360
+ if (keys.every((key) => key === "incoming" || key === "outgoing"))
361
+ return undefined;
362
+ return env;
363
+ }
364
+ function secretValues(environment) {
365
+ return Object.entries(environment).flatMap(([key, value]) => /key|token|secret|password|credential/i.test(key) &&
366
+ typeof value === "string" &&
367
+ value
368
+ ? [value]
369
+ : []);
392
370
  }
393
371
  function manifest(agent, media, publicPath) {
394
372
  return {
@@ -415,7 +393,7 @@ function keyOf(agent, session) {
415
393
  }
416
394
  async function latestMessage(value, media, agentId, sessionId, metadata) {
417
395
  if (!Array.isArray(value))
418
- throw new Error("AG-UI requires a user message.");
396
+ throw new HTTPException(400, { message: "AG-UI requires a user message." });
419
397
  for (let i = value.length - 1; i >= 0; i -= 1) {
420
398
  const item = value[i];
421
399
  if (item?.role !== "user")
@@ -424,7 +402,9 @@ async function latestMessage(value, media, agentId, sessionId, metadata) {
424
402
  if (content)
425
403
  return content;
426
404
  }
427
- throw new Error("AG-UI requires a non-empty user message.");
405
+ throw new HTTPException(400, {
406
+ message: "AG-UI requires a non-empty user message.",
407
+ });
428
408
  }
429
409
  function pending(events) {
430
410
  for (let i = events.length - 1; i >= 0; i -= 1) {
@@ -484,22 +464,39 @@ async function incomingContent(value, media, agentId, sessionId, metadata) {
484
464
  continue;
485
465
  }
486
466
  if (part?.type !== "image")
487
- throw new Error("Only text and image inputs are supported.");
467
+ throw new HTTPException(400, {
468
+ message: "Only text and image inputs are supported.",
469
+ });
488
470
  if (++images > 1)
489
- throw new Error("Attach only one image per message.");
471
+ throw new HTTPException(400, {
472
+ message: "Attach only one image per message.",
473
+ });
490
474
  const source = part.source;
491
475
  if (!source ||
492
476
  source.type !== "data" ||
493
477
  typeof source.value !== "string" ||
494
478
  typeof source.mimeType !== "string")
495
- throw new Error("Image input must contain base64 data and a media type.");
479
+ throw new HTTPException(400, {
480
+ message: "Image input must contain base64 data and a media type.",
481
+ });
496
482
  if (!media)
497
- throw new Error("This runtime does not support image input.");
483
+ throw new HTTPException(400, {
484
+ message: "This runtime does not support image input.",
485
+ });
486
+ try {
487
+ decodeImageBase64(source.mimeType, source.value);
488
+ }
489
+ catch (cause) {
490
+ throw new HTTPException(400, {
491
+ message: cause instanceof Error ? cause.message : "Invalid image input",
492
+ cause,
493
+ });
494
+ }
498
495
  const asset = await media.saveInput(agentId, sessionId, source.mimeType, source.value);
499
496
  parts.push({
500
497
  type: "media",
501
498
  mediaType: asset.mediaType,
502
- reference: { agentId, assetId: asset.id },
499
+ reference: { agentId, sessionId, assetId: asset.id },
503
500
  });
504
501
  chat.push({
505
502
  type: "image",