@nylorun/runtime 0.1.2-beta → 0.2.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.
@@ -3,348 +3,373 @@ import { Hono } from "hono";
3
3
  import { HTTPException } from "hono/http-exception";
4
4
  import { agUiEvents, sse } from "./ag-ui.js";
5
5
  import { observedPayload } from "./digests.js";
6
- import { memoryHistory, scrub, } from "../adapters/journal.js";
6
+ import { localJsonl, scrub } from "../adapters/journal.js";
7
+ import { jsonlObserver } from "../adapters/observe.js";
7
8
  import { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, } from "../adapters/media.js";
8
9
  import { projectSecrets } from "../model/settings.js";
9
- export async function createRuntime(options) {
10
- const agents = [...options.agents];
11
- const byId = new Map(agents.map((agent) => [agent.id, agent]));
12
- if (byId.size !== agents.length)
13
- throw new Error("Agent IDs must be unique.");
14
- for (const agent of agents) {
15
- if (!/^[a-zA-Z0-9_-]+$/.test(agent.id))
16
- throw new Error(`Invalid agent ID: ${agent.id}`);
17
- if (agent.id !== agent.manifest.id || agent.name !== agent.manifest.name)
18
- throw new Error("Agent identity must match its manifest.");
10
+ import { piModel } from "../model/pi-model.js";
11
+ const kServe = Symbol("serve");
12
+ export class Runtime {
13
+ #config;
14
+ #served = false;
15
+ #closing;
16
+ #shutdown;
17
+ constructor(options = {}) {
18
+ this.#config = options;
19
19
  }
20
- const media = options.media;
21
- const journal = options.persistence ?? memoryHistory();
22
- const redact = (value) => scrub(value, projectSecrets());
23
- const live = new Map();
24
- const app = new Hono();
25
- app.onError((error, context) => context.json({ error: String(redact(error.message)) }, error instanceof HTTPException ? error.status : 500));
26
- app.use("*", async (context, next) => {
27
- await next();
28
- if (context.res.headers.get("content-type")?.includes("application/json")) {
29
- const value = await context.res.json();
30
- context.res = new Response(JSON.stringify(redact(value)), {
31
- status: context.res.status,
32
- headers: context.res.headers,
33
- });
34
- }
35
- });
36
- app.use("*", async (context, next) => {
37
- const origin = context.req.header("origin");
38
- if (origin && permitted(origin, options.origins ?? [])) {
39
- context.header("access-control-allow-origin", origin);
40
- context.header("access-control-allow-headers", "content-type");
41
- context.header("access-control-allow-methods", "GET, POST, OPTIONS");
42
- context.header("vary", "origin");
20
+ close = () => (this.#closing ??= this.#shutdown?.() ?? Promise.resolve());
21
+ [kServe](options) {
22
+ if (this.#served)
23
+ throw new Error("A Runtime may only be served once.");
24
+ this.#served = true;
25
+ const agents = [...options.agents];
26
+ const byId = new Map(agents.map((agent) => [agent.id, agent]));
27
+ if (byId.size !== agents.length)
28
+ throw new Error("Agent IDs must be unique.");
29
+ for (const agent of agents) {
30
+ if (!/^[a-zA-Z0-9_-]+$/.test(agent.id))
31
+ throw new Error(`Invalid agent ID: ${agent.id}`);
32
+ if (agent.id !== agent.manifest.id || agent.name !== agent.manifest.name)
33
+ throw new Error("Agent identity must match its manifest.");
43
34
  }
44
- if (context.req.method === "OPTIONS")
45
- return context.body(null, 204);
46
- await next();
47
- });
48
- app.get("/v1/agents", (context) => context.json({
49
- protocolVersion: 2,
50
- agents: agents.map((agent) => ({
51
- id: agent.id,
52
- manifestUrl: `/agents/${agent.id}/manifest.json`,
53
- })),
54
- }));
55
- app.get("/agents/:agentId/manifest.json", (context) => {
56
- const agent = byId.get(context.req.param("agentId"));
57
- return agent === undefined
58
- ? context.json({ error: "unknown agent" }, 404)
59
- : context.json(manifest(agent, media !== undefined));
60
- });
61
- app.get("/agents/:agentId/v1/media/:session/:assetId", async (context) => {
62
- const agent = requireAgent(context.req.param("agentId"));
63
- if (!agent)
64
- return context.json({ error: "unknown agent" }, 404);
65
- const asset = await media?.read(agent.id, context.req.param("session"), context.req.param("assetId"));
66
- if (!asset)
67
- return context.json({ error: "unknown media asset" }, 404);
68
- context.header("cache-control", "no-store");
69
- return context.body(asset.bytes, 200, {
70
- "content-type": asset.asset.mediaType,
35
+ const media = this.#config.media;
36
+ const onModelCall = this.#config.onModelCall ?? piModel({ media });
37
+ const journal = this.#config.durability ?? localJsonl();
38
+ const configuredObserver = this.#config.observer;
39
+ const redact = (value) => scrub(value, projectSecrets());
40
+ const live = new Map();
41
+ let publicPath = (path) => path;
42
+ const routerOptions = options;
43
+ const app = new Hono();
44
+ app.onError((error, context) => context.json({ error: String(redact(error.message)) }, error instanceof HTTPException ? error.status : 500));
45
+ app.use("*", async (context, next) => {
46
+ await next();
47
+ if (context.res.headers.get("content-type")?.includes("application/json")) {
48
+ const value = await context.res.json();
49
+ context.res = new Response(JSON.stringify(redact(value)), {
50
+ status: context.res.status,
51
+ headers: context.res.headers,
52
+ });
53
+ }
71
54
  });
72
- });
73
- app.get("/agents/:agentId/v1/sessions", async (context) => {
74
- const agent = requireAgent(context.req.param("agentId"));
75
- if (agent === undefined)
76
- return context.json({ error: "unknown agent" }, 404);
77
- const listed = await journal.list(agent.id);
78
- return context.json({
79
- sessions: listed.map((summary) => {
80
- const found = live.get(keyOf(agent.id, summary.session));
81
- if (found?.status === "running")
82
- return { ...summary, status: "running" };
83
- if (found?.status === "waiting")
84
- return { ...summary, status: "waiting" };
85
- return summary;
86
- }),
55
+ publicPath = (path) => `${normalizeBasePath(routerOptions.basePath)}${path}`;
56
+ app.get("/v1/agents", (context) => context.json({
57
+ protocolVersion: 2,
58
+ agents: agents.map((agent) => ({
59
+ id: agent.id,
60
+ manifestUrl: publicPath(`/agents/${agent.id}/manifest.json`),
61
+ })),
62
+ }));
63
+ app.get("/agents/:agentId/manifest.json", (context) => {
64
+ const agent = byId.get(context.req.param("agentId"));
65
+ return agent === undefined
66
+ ? context.json({ error: "unknown agent" }, 404)
67
+ : context.json(manifest(agent, media !== undefined, publicPath));
87
68
  });
88
- });
89
- app.get("/agents/:agentId/v1/sessions/:session", async (context) => {
90
- const agent = requireAgent(context.req.param("agentId"));
91
- if (!agent)
92
- return context.json({ error: "unknown agent" }, 404);
93
- const key = keyOf(agent.id, context.req.param("session"));
94
- const found = live.get(key);
95
- const events = found?.events ??
96
- (await journal.events(agent.id, context.req.param("session")));
97
- if (!found && events.length === 0)
98
- return context.json({ error: "unknown session" }, 404);
99
- return context.json({
100
- id: context.req.param("session"),
101
- state: found?.status ?? status(events),
102
- pending_interaction: pending(events),
69
+ app.get("/agents/:agentId/v1/media/:session/:assetId", async (context) => {
70
+ const agent = requireAgent(context.req.param("agentId"));
71
+ if (!agent)
72
+ return context.json({ error: "unknown agent" }, 404);
73
+ const asset = await media?.read(agent.id, context.req.param("session"), context.req.param("assetId"));
74
+ if (!asset)
75
+ return context.json({ error: "unknown media asset" }, 404);
76
+ context.header("cache-control", "no-store");
77
+ return context.body(asset.bytes, 200, {
78
+ "content-type": asset.asset.mediaType,
79
+ });
103
80
  });
104
- });
105
- app.post("/agents/:agentId/v1/sessions/:session", async (context) => {
106
- const agent = requireAgent(context.req.param("agentId"));
107
- if (!agent)
108
- return context.json({ error: "unknown agent" }, 404);
109
- const found = live.get(keyOf(agent.id, context.req.param("session")));
110
- if (!found)
111
- return context.json({ error: "session is no longer live" }, 409);
112
- const payload = await context.req
113
- .json()
114
- .catch(() => undefined);
115
- const interaction = payload?.interaction;
116
- if (!interaction || typeof interaction.id !== "string")
117
- return context.json({ error: "expected interaction" }, 400);
118
- const waiting = pending(found.events);
119
- if (found.status !== "waiting" || !waiting || waiting.id !== interaction.id)
120
- return context.json({ error: "interaction is no longer pending" }, 409);
121
- if (interaction.kind === "approval") {
122
- if (typeof interaction.approved !== "boolean")
123
- return context.json({ error: "expected approval interaction" }, 400);
124
- found.status = "running";
125
- await submit(found, {
126
- kind: "approve",
127
- interactionId: interaction.id,
128
- approved: interaction.approved,
81
+ app.get("/agents/:agentId/v1/sessions", async (context) => {
82
+ const agent = requireAgent(context.req.param("agentId"));
83
+ if (agent === undefined)
84
+ return context.json({ error: "unknown agent" }, 404);
85
+ const listed = await journal.list(agent.id);
86
+ return context.json({
87
+ sessions: listed.map((summary) => {
88
+ const found = live.get(keyOf(agent.id, summary.session));
89
+ if (found?.status === "running")
90
+ return { ...summary, status: "running" };
91
+ if (found?.status === "waiting")
92
+ return { ...summary, status: "waiting" };
93
+ return summary;
94
+ }),
129
95
  });
130
- return context.json({ session_id: context.req.param("session"), state: found.status }, 202);
131
- }
132
- if (interaction.kind === "respond") {
133
- if (!("value" in interaction))
134
- return context.json({ error: "expected respond interaction" }, 400);
135
- found.status = "running";
136
- await submit(found, {
137
- kind: "respond",
138
- interactionId: interaction.id,
139
- value: interaction.value,
96
+ });
97
+ app.get("/agents/:agentId/v1/sessions/:session", async (context) => {
98
+ const agent = requireAgent(context.req.param("agentId"));
99
+ if (!agent)
100
+ return context.json({ error: "unknown agent" }, 404);
101
+ const key = keyOf(agent.id, context.req.param("session"));
102
+ const found = live.get(key);
103
+ const events = found?.events ??
104
+ (await journal.events(agent.id, context.req.param("session")));
105
+ if (!found && events.length === 0)
106
+ return context.json({ error: "unknown session" }, 404);
107
+ return context.json({
108
+ id: context.req.param("session"),
109
+ state: found?.status ?? status(events),
110
+ pending_interaction: pending(events),
140
111
  });
141
- return context.json({ session_id: context.req.param("session"), state: found.status }, 202);
142
- }
143
- return context.json({ error: "expected approval or respond interaction" }, 400);
144
- });
145
- app.get("/agents/:agentId/v1/sessions/:session/events", async (context) => {
146
- const agent = requireAgent(context.req.param("agentId"));
147
- if (!agent)
148
- return context.json({ error: "unknown agent" }, 404);
149
- const after = Number(context.req.query("after") ?? "0");
150
- const session = context.req.param("session");
151
- const events = live.get(keyOf(agent.id, session))?.events ??
152
- (await journal.events(agent.id, session));
153
- return context.json({
154
- events: events.filter((event) => event.seq > after),
155
- next_cursor: events.at(-1)?.seq ?? after,
156
112
  });
157
- });
158
- app.get("/agents/:agentId/v1/ag-ui/sessions/:session", async (context) => {
159
- const agent = requireAgent(context.req.param("agentId"));
160
- if (!agent)
161
- return context.json({ error: "unknown agent" }, 404);
162
- const found = live.get(keyOf(agent.id, context.req.param("session")));
163
- return context.json({
164
- messages: found?.messages ??
165
- messages(agent.id, await journal.events(agent.id, context.req.param("session"))),
113
+ app.post("/agents/:agentId/v1/sessions/:session", async (context) => {
114
+ const agent = requireAgent(context.req.param("agentId"));
115
+ if (!agent)
116
+ return context.json({ error: "unknown agent" }, 404);
117
+ const found = live.get(keyOf(agent.id, context.req.param("session")));
118
+ if (!found)
119
+ return context.json({ error: "session is no longer live" }, 409);
120
+ const payload = await context.req
121
+ .json()
122
+ .catch(() => undefined);
123
+ const interaction = payload?.interaction;
124
+ if (!interaction || typeof interaction.id !== "string")
125
+ return context.json({ error: "expected interaction" }, 400);
126
+ const waiting = pending(found.events);
127
+ if (found.status !== "waiting" ||
128
+ !waiting ||
129
+ waiting.id !== interaction.id)
130
+ return context.json({ error: "interaction is no longer pending" }, 409);
131
+ if (interaction.kind === "approval") {
132
+ if (typeof interaction.approved !== "boolean")
133
+ return context.json({ error: "expected approval interaction" }, 400);
134
+ found.status = "running";
135
+ await submit(found, {
136
+ kind: "approve",
137
+ interactionId: interaction.id,
138
+ approved: interaction.approved,
139
+ });
140
+ return context.json({ session_id: context.req.param("session"), state: found.status }, 202);
141
+ }
142
+ if (interaction.kind === "respond") {
143
+ if (!("value" in interaction))
144
+ return context.json({ error: "expected respond interaction" }, 400);
145
+ found.status = "running";
146
+ await submit(found, {
147
+ kind: "respond",
148
+ interactionId: interaction.id,
149
+ value: interaction.value,
150
+ });
151
+ return context.json({ session_id: context.req.param("session"), state: found.status }, 202);
152
+ }
153
+ return context.json({ error: "expected approval or respond interaction" }, 400);
166
154
  });
167
- });
168
- app.post("/agents/:agentId/v1/ag-ui", async (context) => {
169
- const agent = requireAgent(context.req.param("agentId"));
170
- if (!agent)
171
- return context.json({ error: "unknown agent" }, 404);
172
- const payload = await context.req
173
- .json()
174
- .catch(() => undefined);
175
- const threadId = typeof payload?.threadId === "string" && payload.threadId
176
- ? payload.threadId
177
- : randomUUID();
178
- if (!isSessionId(threadId))
155
+ app.get("/agents/:agentId/v1/sessions/:session/events", async (context) => {
156
+ const agent = requireAgent(context.req.param("agentId"));
157
+ if (!agent)
158
+ return context.json({ error: "unknown agent" }, 404);
159
+ const after = Number(context.req.query("after") ?? "0");
160
+ const session = context.req.param("session");
161
+ const events = live.get(keyOf(agent.id, session))?.events ??
162
+ (await journal.events(agent.id, session));
179
163
  return context.json({
180
- error: "threadId may only contain letters, digits, '.', '_' and '-'",
181
- }, 400);
182
- const runId = typeof payload?.runId === "string" && payload.runId
183
- ? payload.runId
184
- : randomUUID();
185
- let message;
186
- try {
187
- message = await latestMessage(payload?.messages, media, agent.id, threadId);
188
- }
189
- catch (error) {
164
+ events: events.filter((event) => event.seq > after),
165
+ next_cursor: events.at(-1)?.seq ?? after,
166
+ });
167
+ });
168
+ app.get("/agents/:agentId/v1/ag-ui/sessions/:session", async (context) => {
169
+ const agent = requireAgent(context.req.param("agentId"));
170
+ if (!agent)
171
+ return context.json({ error: "unknown agent" }, 404);
172
+ const found = live.get(keyOf(agent.id, context.req.param("session")));
190
173
  return context.json({
191
- error: error instanceof Error
192
- ? error.message
193
- : "Invalid AG-UI user message",
194
- }, 400);
174
+ messages: found?.messages ??
175
+ messages(agent.id, await journal.events(agent.id, context.req.param("session"))),
176
+ });
177
+ });
178
+ app.post("/agents/:agentId/v1/ag-ui", async (context) => {
179
+ const agent = requireAgent(context.req.param("agentId"));
180
+ if (!agent)
181
+ return context.json({ error: "unknown agent" }, 404);
182
+ const payload = await context.req
183
+ .json()
184
+ .catch(() => undefined);
185
+ const threadId = typeof payload?.threadId === "string" && payload.threadId
186
+ ? payload.threadId
187
+ : randomUUID();
188
+ if (!isSessionId(threadId))
189
+ return context.json({
190
+ error: "threadId may only contain letters, digits, '.', '_' and '-'",
191
+ }, 400);
192
+ const runId = typeof payload?.runId === "string" && payload.runId
193
+ ? payload.runId
194
+ : randomUUID();
195
+ let message;
196
+ let actor;
197
+ try {
198
+ actor = await routerOptions.getActor?.(context);
199
+ const metadata = await routerOptions.getRequestMetadata?.(context);
200
+ message = await latestMessage(payload?.messages, media, agent.id, threadId, metadata);
201
+ }
202
+ catch (error) {
203
+ return context.json({
204
+ error: error instanceof Error
205
+ ? error.message
206
+ : "Invalid AG-UI user message",
207
+ }, 400);
208
+ }
209
+ const found = await begin(agent, threadId, message, actor);
210
+ const start = found.events.length;
211
+ await submit(found, message.input);
212
+ return sse(agUiEvents(found.events.slice(start), threadId, runId), context.res.headers);
213
+ });
214
+ function requireAgent(id) {
215
+ return byId.get(id);
195
216
  }
196
- const found = await begin(agent, threadId, message);
197
- const start = found.events.length;
198
- await submit(found, message.input);
199
- return sse(agUiEvents(found.events.slice(start), threadId, runId), context.res.headers);
200
- });
201
- function requireAgent(id) {
202
- return byId.get(id);
203
- }
204
- async function begin(agent, sessionId, message) {
205
- const key = keyOf(agent.id, sessionId);
206
- const existing = live.get(key);
207
- if (existing) {
208
- existing.messages.push(message.chat);
209
- return existing;
217
+ async function begin(agent, sessionId, message, actor) {
218
+ const key = keyOf(agent.id, sessionId);
219
+ const existing = live.get(key);
220
+ if (existing) {
221
+ existing.messages.push(message.chat);
222
+ return existing;
223
+ }
224
+ const archived = await journal.events(agent.id, sessionId);
225
+ const concurrent = live.get(key);
226
+ if (concurrent) {
227
+ concurrent.messages.push(message.chat);
228
+ return concurrent;
229
+ }
230
+ if (archived.length)
231
+ throw new HTTPException(409, {
232
+ message: "This session is archived. Start a new conversation.",
233
+ });
234
+ let entry;
235
+ const observer = configuredObserver ?? jsonlObserver({ agentId: agent.id, sessionId });
236
+ const session = agent.run({
237
+ id: sessionId,
238
+ onModelCall,
239
+ observer: (event) => {
240
+ const image = generatedImageMessage(event, agent.id, sessionId);
241
+ if (image)
242
+ entry.messages.push(image);
243
+ add(entry, agent.id, event.type, observedPayload(event));
244
+ void Promise.resolve(observer(event)).catch(() => { });
245
+ },
246
+ ...(actor === undefined ? {} : { userId: actor.id }),
247
+ ...(actor?.context === undefined ? {} : { context: actor.context }),
248
+ });
249
+ entry = {
250
+ session,
251
+ events: [],
252
+ messages: [message.chat],
253
+ status: "running",
254
+ sequence: 0,
255
+ writes: Promise.resolve(),
256
+ };
257
+ live.set(key, entry);
258
+ return entry;
210
259
  }
211
- const archived = await journal.events(agent.id, sessionId);
212
- const concurrent = live.get(key);
213
- if (concurrent) {
214
- concurrent.messages.push(message.chat);
215
- return concurrent;
260
+ function add(entry, agentId, type, payload) {
261
+ const event = {
262
+ session: entry.session.id,
263
+ seq: ++entry.sequence,
264
+ ts: new Date().toISOString(),
265
+ type,
266
+ payload: redact(payload),
267
+ };
268
+ entry.events.push(event);
269
+ entry.writes = entry.writes.then(() => journal.append(agentId, event));
270
+ // The submit/close paths observe durability errors; avoid an unhandled rejection meanwhile.
271
+ void entry.writes.catch(() => { });
216
272
  }
217
- if (archived.length)
218
- throw new HTTPException(409, {
219
- message: "This session is archived. Start a new conversation.",
273
+ async function submit(entry, input) {
274
+ const agentId = [...live]
275
+ .find(([, value]) => value === entry)[0]
276
+ .split(":")[0];
277
+ entry.status = "running";
278
+ const inputEvent = typeof input === "string"
279
+ ? { kind: "user-message", text: input }
280
+ : "kind" in input
281
+ ? input
282
+ : { kind: "user-message", ...input };
283
+ const message = chatFromInput(inputEvent, agentId, entry.session.id);
284
+ add(entry, agentId, "session.run.started", {
285
+ input_kind: inputEvent.kind,
286
+ input: message ? firstText(message) : undefined,
287
+ ...(message ? { message } : {}),
288
+ ...("approved" in inputEvent ? { approved: inputEvent.approved } : {}),
289
+ ...("value" in inputEvent ? { value: inputEvent.value } : {}),
220
290
  });
221
- const session = agent.run({ id: sessionId });
222
- const entry = {
223
- session,
224
- events: [],
225
- messages: [message.chat],
226
- status: "running",
227
- sequence: 0,
228
- writes: Promise.resolve(),
229
- unsubscribe: () => { },
230
- };
231
- live.set(key, entry);
232
- entry.unsubscribe = session.observe((event) => {
233
- const image = generatedImageMessage(event, agent.id, sessionId);
234
- if (image)
235
- entry.messages.push(image);
236
- add(entry, agent.id, event.type, observedPayload(event));
237
- });
238
- return entry;
239
- }
240
- function add(entry, agentId, type, payload) {
241
- const event = {
242
- session: entry.session.id,
243
- seq: ++entry.sequence,
244
- ts: new Date().toISOString(),
245
- type,
246
- payload: redact(payload),
247
- };
248
- entry.events.push(event);
249
- entry.writes = entry.writes.then(() => journal.append(agentId, event));
250
- // The submit/close paths observe persistence errors; avoid an unhandled rejection meanwhile.
251
- void entry.writes.catch(() => { });
252
- }
253
- async function submit(entry, input) {
254
- const agentId = [...live]
255
- .find(([, value]) => value === entry)[0]
256
- .split(":")[0];
257
- entry.status = "running";
258
- const inputEvent = typeof input === "string"
259
- ? { kind: "user-message", text: input }
260
- : "kind" in input
261
- ? input
262
- : { kind: "user-message", ...input };
263
- const message = chatFromInput(inputEvent, agentId, entry.session.id);
264
- add(entry, agentId, "session.run.started", {
265
- input_kind: inputEvent.kind,
266
- input: message ? firstText(message) : undefined,
267
- ...(message ? { message } : {}),
268
- ...("approved" in inputEvent ? { approved: inputEvent.approved } : {}),
269
- ...("value" in inputEvent ? { value: inputEvent.value } : {}),
270
- });
271
- const start = entry.events.length;
272
- try {
273
- const result = await entry.session.input(input).completed;
274
- for (const event of result.events) {
275
- if (event.type === "final" && event.output !== undefined) {
276
- entry.messages.push({
277
- id: randomUUID(),
278
- role: "assistant",
279
- content: finalContent(event.output),
280
- });
281
- add(entry, agentId, "final", { output: event.output });
291
+ const start = entry.events.length;
292
+ try {
293
+ const result = await entry.session.input(input).completed;
294
+ for (const event of result.events) {
295
+ if (event.type === "final" && event.output !== undefined) {
296
+ entry.messages.push({
297
+ id: randomUUID(),
298
+ role: "assistant",
299
+ content: finalContent(event.output),
300
+ });
301
+ add(entry, agentId, "final", { output: event.output });
302
+ }
303
+ else if (event.type === "interaction.required") {
304
+ add(entry, agentId, event.type, { interaction: event.interaction });
305
+ }
282
306
  }
283
- else if (event.type === "interaction.required") {
284
- add(entry, agentId, event.type, { interaction: event.interaction });
307
+ const observedFailure = entry.events
308
+ .slice(start)
309
+ .some((event) => event.type === "error" || event.type === "tripwire");
310
+ const completionFailure = result.events.find((event) => event.type === "error" || event.type === "tripwire");
311
+ const failed = observedFailure ||
312
+ completionFailure !== undefined ||
313
+ (result.status !== "completed" && result.status !== "waiting");
314
+ if (!observedFailure && completionFailure) {
315
+ add(entry, agentId, completionFailure.type, observedPayload(completionFailure));
285
316
  }
317
+ else if (!observedFailure && failed) {
318
+ add(entry, agentId, "error", {
319
+ message: `Agent run ${result.status}.`,
320
+ });
321
+ }
322
+ entry.status = failed
323
+ ? "failed"
324
+ : result.status === "waiting"
325
+ ? "waiting"
326
+ : "completed";
327
+ await entry.writes;
286
328
  }
287
- const observedFailure = entry.events
288
- .slice(start)
289
- .some((event) => event.type === "error" || event.type === "tripwire");
290
- const completionFailure = result.events.find((event) => event.type === "error" || event.type === "tripwire");
291
- const failed = observedFailure ||
292
- completionFailure !== undefined ||
293
- (result.status !== "completed" && result.status !== "waiting");
294
- if (!observedFailure && completionFailure) {
295
- add(entry, agentId, completionFailure.type, observedPayload(completionFailure));
296
- }
297
- else if (!observedFailure && failed) {
329
+ catch (error) {
330
+ entry.status = "failed";
298
331
  add(entry, agentId, "error", {
299
- message: `Agent run ${result.status}.`,
332
+ message: error instanceof Error ? error.message : String(error),
300
333
  });
334
+ await entry.writes;
335
+ throw error;
301
336
  }
302
- entry.status = failed
303
- ? "failed"
304
- : result.status === "waiting"
305
- ? "waiting"
306
- : "completed";
307
- await entry.writes;
308
- }
309
- catch (error) {
310
- entry.status = "failed";
311
- add(entry, agentId, "error", {
312
- message: error instanceof Error ? error.message : String(error),
313
- });
314
- await entry.writes;
315
- throw error;
316
337
  }
317
- }
318
- let closing;
319
- return Object.freeze({
320
- app,
321
- hasSession: (agentId, sessionId) => live.has(keyOf(agentId, sessionId)),
322
- close: () => (closing ??= (async () => {
338
+ this.#shutdown = async () => {
323
339
  const sessions = await Promise.allSettled([...live.values()].map(async (entry) => {
324
340
  try {
325
341
  await entry.session.stop();
326
342
  await entry.writes;
327
343
  }
328
344
  finally {
329
- entry.unsubscribe();
330
345
  }
331
346
  }));
332
347
  const resources = await Promise.allSettled(agents.map((agent) => agent.close?.()));
333
348
  const failure = [...sessions, ...resources].find((result) => result.status === "rejected");
334
349
  if (failure?.status === "rejected")
335
350
  throw failure.reason;
336
- })()),
337
- });
351
+ };
352
+ return app;
353
+ }
354
+ }
355
+ /** Create the Hono protocol mount for a runtime. Applications may close it during graceful shutdown. */
356
+ // `any` avoids leaking a second physical Hono installation through a peer boundary.
357
+ // The returned value is the caller's Hono router at runtime.
358
+ export function serveAgents(options) {
359
+ if (!(options.runtime instanceof Runtime))
360
+ throw new Error("serveAgents requires a Runtime.");
361
+ const { runtime, ...rest } = options;
362
+ return runtime[kServe](rest);
338
363
  }
339
- function manifest(agent, media) {
364
+ function manifest(agent, media, publicPath) {
340
365
  return {
341
366
  protocolVersion: 2,
342
367
  id: agent.manifest.id,
343
368
  name: agent.manifest.name,
344
369
  manifest: agent.manifest,
345
370
  endpoints: {
346
- agUi: `/agents/${agent.id}/v1/ag-ui`,
347
- sessions: `/agents/${agent.id}/v1/sessions`,
371
+ agUi: publicPath(`/agents/${agent.id}/v1/ag-ui`),
372
+ sessions: publicPath(`/agents/${agent.id}/v1/sessions`),
348
373
  },
349
374
  ...(media
350
375
  ? {
@@ -359,14 +384,14 @@ function manifest(agent, media) {
359
384
  function keyOf(agent, session) {
360
385
  return `${agent}:${session}`;
361
386
  }
362
- async function latestMessage(value, media, agentId, sessionId) {
387
+ async function latestMessage(value, media, agentId, sessionId, metadata) {
363
388
  if (!Array.isArray(value))
364
389
  throw new Error("AG-UI requires a user message.");
365
390
  for (let i = value.length - 1; i >= 0; i -= 1) {
366
391
  const item = value[i];
367
392
  if (item?.role !== "user")
368
393
  continue;
369
- const content = await incomingContent(item.content, media, agentId, sessionId);
394
+ const content = await incomingContent(item.content, media, agentId, sessionId, metadata);
370
395
  if (content)
371
396
  return content;
372
397
  }
@@ -409,7 +434,7 @@ function messages(agentId, events) {
409
434
  ? [generatedImageFromEvent(event, agentId)]
410
435
  : []);
411
436
  }
412
- async function incomingContent(value, media, agentId, sessionId) {
437
+ async function incomingContent(value, media, agentId, sessionId, metadata) {
413
438
  const parts = [];
414
439
  const chat = [];
415
440
  if (typeof value === "string") {
@@ -459,7 +484,10 @@ async function incomingContent(value, media, agentId, sessionId) {
459
484
  if (parts.length === 0)
460
485
  return undefined;
461
486
  return Object.freeze({
462
- input: { content: Object.freeze(parts) },
487
+ input: {
488
+ content: Object.freeze(parts),
489
+ ...(metadata === undefined ? {} : { metadata }),
490
+ },
463
491
  chat: { id: randomUUID(), role: "user", content: Object.freeze(chat) },
464
492
  });
465
493
  }
@@ -565,42 +593,12 @@ function isSessionId(value) {
565
593
  return value !== "." && value !== ".." && /^[a-zA-Z0-9._-]+$/u.test(value);
566
594
  }
567
595
  function mediaUrl(agentId, sessionId, assetId) {
568
- return `/agents/${encodeURIComponent(agentId)}/v1/media/${encodeURIComponent(sessionId)}/${encodeURIComponent(assetId)}`;
569
- }
570
- /** Host headers a loopback bind answers to, on the port actually in use. */
571
- export function loopbackHosts(port) {
572
- return [`localhost:${port}`, `127.0.0.1:${port}`, `[::1]:${port}`];
596
+ return `media/${encodeURIComponent(sessionId)}/${encodeURIComponent(assetId)}`;
573
597
  }
574
- /**
575
- * Decide whether a request's Host header is served. Entries are exact
576
- * `host:port` values or bare host names that accept any port; `*` accepts all.
577
- */
578
- export function allowedHost(header, allowed) {
579
- if (allowed.includes("*"))
580
- return true;
581
- if (!header)
582
- return false;
583
- let hostname;
584
- try {
585
- hostname = new URL(`http://${header}`).hostname;
586
- }
587
- catch {
588
- return false;
589
- }
590
- const bare = hostname.replace(/^\[(.*)\]$/u, "$1");
591
- return allowed.some((entry) => entry.toLowerCase() === header.toLowerCase() ||
592
- entry.toLowerCase() === hostname ||
593
- entry.toLowerCase() === bare);
594
- }
595
- function permitted(origin, configured) {
596
- try {
597
- const url = new URL(origin);
598
- return (configured.includes(origin) ||
599
- url.hostname === "localhost" ||
600
- url.hostname.endsWith(".localhost") ||
601
- url.hostname === "127.0.0.1");
602
- }
603
- catch {
604
- return false;
605
- }
598
+ function normalizeBasePath(value) {
599
+ if (!value || value === "/")
600
+ return "";
601
+ if (!value.startsWith("/") || value.endsWith("/"))
602
+ throw new Error("basePath must start with / and must not end with /.");
603
+ return value;
606
604
  }