@nylorun/runtime 0.1.2-beta → 0.2.1-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.
- package/CHANGELOG.md +12 -0
- package/README.md +23 -38
- package/dist/adapters/journal.d.ts +10 -10
- package/dist/adapters/journal.js +20 -20
- package/dist/adapters/observe.d.ts +10 -0
- package/dist/adapters/observe.js +23 -0
- package/dist/cli.js +58 -293
- package/dist/config.d.ts +9 -6
- package/dist/config.js +1 -15
- package/dist/contracts.d.ts +12 -6
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/server/digests.d.ts +4 -2
- package/dist/server/host.d.ts +25 -12
- package/dist/server/host.js +338 -340
- package/package.json +9 -8
package/dist/server/host.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}))
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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(`/${agent.id}/manifest.json`),
|
|
61
|
+
})),
|
|
62
|
+
}));
|
|
63
|
+
app.get("/: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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return context.json({
|
|
100
|
-
id: context.req.param("session"),
|
|
101
|
-
state: found?.status ?? status(events),
|
|
102
|
-
pending_interaction: pending(events),
|
|
69
|
+
app.get("/: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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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("/: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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (!
|
|
134
|
-
return context.json({ error: "
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
96
|
+
});
|
|
97
|
+
app.get("/: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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
113
|
+
app.post("/: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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
.
|
|
174
|
-
|
|
175
|
-
const threadId = typeof payload?.threadId === "string" && payload.threadId
|
|
176
|
-
? payload.threadId
|
|
177
|
-
: randomUUID();
|
|
178
|
-
if (!isSessionId(threadId))
|
|
155
|
+
app.get("/: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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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("/: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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
174
|
+
messages: found?.messages ??
|
|
175
|
+
messages(agent.id, await journal.events(agent.id, context.req.param("session"))),
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
app.post("/: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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
284
|
-
|
|
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
|
-
|
|
288
|
-
.
|
|
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:
|
|
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:
|
|
347
|
-
sessions:
|
|
371
|
+
agUi: publicPath(`/${agent.id}/v1/ag-ui`),
|
|
372
|
+
sessions: publicPath(`/${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: {
|
|
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
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
}
|