@nylorun/runtime 0.1.1-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.
- package/CHANGELOG.md +13 -0
- package/README.md +20 -30
- 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 +26 -7
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/model/pi-model.js +64 -4
- package/dist/server/ag-ui.js +13 -4
- package/dist/server/digests.d.ts +4 -2
- package/dist/server/host.d.ts +25 -12
- package/dist/server/host.js +342 -327
- package/package.json +9 -8
package/dist/server/host.js
CHANGED
|
@@ -3,331 +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(`/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
|
-
|
|
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("/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
|
-
|
|
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("/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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (!
|
|
134
|
-
return context.json({ error: "
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
message = await latestMessage(payload?.messages, media, agent.id, threadId);
|
|
186
|
-
}
|
|
187
|
-
catch (error) {
|
|
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));
|
|
163
|
+
return context.json({
|
|
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")));
|
|
188
173
|
return context.json({
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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);
|
|
193
216
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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;
|
|
208
259
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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(() => { });
|
|
214
272
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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 } : {}),
|
|
218
290
|
});
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
payload: redact(payload),
|
|
245
|
-
};
|
|
246
|
-
entry.events.push(event);
|
|
247
|
-
entry.writes = entry.writes.then(() => journal.append(agentId, event));
|
|
248
|
-
// The submit/close paths observe persistence errors; avoid an unhandled rejection meanwhile.
|
|
249
|
-
void entry.writes.catch(() => { });
|
|
250
|
-
}
|
|
251
|
-
async function submit(entry, input) {
|
|
252
|
-
const agentId = [...live]
|
|
253
|
-
.find(([, value]) => value === entry)[0]
|
|
254
|
-
.split(":")[0];
|
|
255
|
-
entry.status = "running";
|
|
256
|
-
const inputEvent = typeof input === "string"
|
|
257
|
-
? { kind: "user-message", text: input }
|
|
258
|
-
: "kind" in input
|
|
259
|
-
? input
|
|
260
|
-
: { kind: "user-message", ...input };
|
|
261
|
-
const message = chatFromInput(inputEvent, agentId, entry.session.id);
|
|
262
|
-
add(entry, agentId, "session.run.started", {
|
|
263
|
-
input_kind: inputEvent.kind,
|
|
264
|
-
input: message ? firstText(message) : undefined,
|
|
265
|
-
...(message ? { message } : {}),
|
|
266
|
-
...("approved" in inputEvent ? { approved: inputEvent.approved } : {}),
|
|
267
|
-
...("value" in inputEvent ? { value: inputEvent.value } : {}),
|
|
268
|
-
});
|
|
269
|
-
try {
|
|
270
|
-
const result = await entry.session.input(input).completed;
|
|
271
|
-
for (const event of result.events) {
|
|
272
|
-
if (event.type === "final" && event.output !== undefined) {
|
|
273
|
-
entry.messages.push({
|
|
274
|
-
id: randomUUID(),
|
|
275
|
-
role: "assistant",
|
|
276
|
-
content: finalContent(event.output),
|
|
277
|
-
});
|
|
278
|
-
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
|
+
}
|
|
306
|
+
}
|
|
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));
|
|
279
316
|
}
|
|
280
|
-
else if (
|
|
281
|
-
add(entry, agentId,
|
|
317
|
+
else if (!observedFailure && failed) {
|
|
318
|
+
add(entry, agentId, "error", {
|
|
319
|
+
message: `Agent run ${result.status}.`,
|
|
320
|
+
});
|
|
282
321
|
}
|
|
322
|
+
entry.status = failed
|
|
323
|
+
? "failed"
|
|
324
|
+
: result.status === "waiting"
|
|
325
|
+
? "waiting"
|
|
326
|
+
: "completed";
|
|
327
|
+
await entry.writes;
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
entry.status = "failed";
|
|
331
|
+
add(entry, agentId, "error", {
|
|
332
|
+
message: error instanceof Error ? error.message : String(error),
|
|
333
|
+
});
|
|
334
|
+
await entry.writes;
|
|
335
|
+
throw error;
|
|
283
336
|
}
|
|
284
|
-
entry.status =
|
|
285
|
-
result.status === "waiting"
|
|
286
|
-
? "waiting"
|
|
287
|
-
: result.status === "completed"
|
|
288
|
-
? "completed"
|
|
289
|
-
: "failed";
|
|
290
|
-
await entry.writes;
|
|
291
|
-
}
|
|
292
|
-
catch (error) {
|
|
293
|
-
entry.status = "failed";
|
|
294
|
-
add(entry, agentId, "error", {
|
|
295
|
-
message: error instanceof Error ? error.message : String(error),
|
|
296
|
-
});
|
|
297
|
-
await entry.writes;
|
|
298
|
-
throw error;
|
|
299
337
|
}
|
|
300
|
-
|
|
301
|
-
let closing;
|
|
302
|
-
return Object.freeze({
|
|
303
|
-
app,
|
|
304
|
-
hasSession: (agentId, sessionId) => live.has(keyOf(agentId, sessionId)),
|
|
305
|
-
close: () => (closing ??= (async () => {
|
|
338
|
+
this.#shutdown = async () => {
|
|
306
339
|
const sessions = await Promise.allSettled([...live.values()].map(async (entry) => {
|
|
307
340
|
try {
|
|
308
341
|
await entry.session.stop();
|
|
309
342
|
await entry.writes;
|
|
310
343
|
}
|
|
311
344
|
finally {
|
|
312
|
-
entry.unsubscribe();
|
|
313
345
|
}
|
|
314
346
|
}));
|
|
315
347
|
const resources = await Promise.allSettled(agents.map((agent) => agent.close?.()));
|
|
316
348
|
const failure = [...sessions, ...resources].find((result) => result.status === "rejected");
|
|
317
349
|
if (failure?.status === "rejected")
|
|
318
350
|
throw failure.reason;
|
|
319
|
-
}
|
|
320
|
-
|
|
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);
|
|
321
363
|
}
|
|
322
|
-
function manifest(agent, media) {
|
|
364
|
+
function manifest(agent, media, publicPath) {
|
|
323
365
|
return {
|
|
324
366
|
protocolVersion: 2,
|
|
325
367
|
id: agent.manifest.id,
|
|
326
368
|
name: agent.manifest.name,
|
|
327
369
|
manifest: agent.manifest,
|
|
328
370
|
endpoints: {
|
|
329
|
-
agUi: `/agents/${agent.id}/v1/ag-ui
|
|
330
|
-
sessions: `/agents/${agent.id}/v1/sessions
|
|
371
|
+
agUi: publicPath(`/agents/${agent.id}/v1/ag-ui`),
|
|
372
|
+
sessions: publicPath(`/agents/${agent.id}/v1/sessions`),
|
|
331
373
|
},
|
|
332
374
|
...(media
|
|
333
375
|
? {
|
|
@@ -342,14 +384,14 @@ function manifest(agent, media) {
|
|
|
342
384
|
function keyOf(agent, session) {
|
|
343
385
|
return `${agent}:${session}`;
|
|
344
386
|
}
|
|
345
|
-
async function latestMessage(value, media, agentId, sessionId) {
|
|
387
|
+
async function latestMessage(value, media, agentId, sessionId, metadata) {
|
|
346
388
|
if (!Array.isArray(value))
|
|
347
389
|
throw new Error("AG-UI requires a user message.");
|
|
348
390
|
for (let i = value.length - 1; i >= 0; i -= 1) {
|
|
349
391
|
const item = value[i];
|
|
350
392
|
if (item?.role !== "user")
|
|
351
393
|
continue;
|
|
352
|
-
const content = await incomingContent(item.content, media, agentId, sessionId);
|
|
394
|
+
const content = await incomingContent(item.content, media, agentId, sessionId, metadata);
|
|
353
395
|
if (content)
|
|
354
396
|
return content;
|
|
355
397
|
}
|
|
@@ -392,7 +434,7 @@ function messages(agentId, events) {
|
|
|
392
434
|
? [generatedImageFromEvent(event, agentId)]
|
|
393
435
|
: []);
|
|
394
436
|
}
|
|
395
|
-
async function incomingContent(value, media, agentId, sessionId) {
|
|
437
|
+
async function incomingContent(value, media, agentId, sessionId, metadata) {
|
|
396
438
|
const parts = [];
|
|
397
439
|
const chat = [];
|
|
398
440
|
if (typeof value === "string") {
|
|
@@ -442,7 +484,10 @@ async function incomingContent(value, media, agentId, sessionId) {
|
|
|
442
484
|
if (parts.length === 0)
|
|
443
485
|
return undefined;
|
|
444
486
|
return Object.freeze({
|
|
445
|
-
input: {
|
|
487
|
+
input: {
|
|
488
|
+
content: Object.freeze(parts),
|
|
489
|
+
...(metadata === undefined ? {} : { metadata }),
|
|
490
|
+
},
|
|
446
491
|
chat: { id: randomUUID(), role: "user", content: Object.freeze(chat) },
|
|
447
492
|
});
|
|
448
493
|
}
|
|
@@ -548,42 +593,12 @@ function isSessionId(value) {
|
|
|
548
593
|
return value !== "." && value !== ".." && /^[a-zA-Z0-9._-]+$/u.test(value);
|
|
549
594
|
}
|
|
550
595
|
function mediaUrl(agentId, sessionId, assetId) {
|
|
551
|
-
return
|
|
552
|
-
}
|
|
553
|
-
/** Host headers a loopback bind answers to, on the port actually in use. */
|
|
554
|
-
export function loopbackHosts(port) {
|
|
555
|
-
return [`localhost:${port}`, `127.0.0.1:${port}`, `[::1]:${port}`];
|
|
596
|
+
return `media/${encodeURIComponent(sessionId)}/${encodeURIComponent(assetId)}`;
|
|
556
597
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
return true;
|
|
564
|
-
if (!header)
|
|
565
|
-
return false;
|
|
566
|
-
let hostname;
|
|
567
|
-
try {
|
|
568
|
-
hostname = new URL(`http://${header}`).hostname;
|
|
569
|
-
}
|
|
570
|
-
catch {
|
|
571
|
-
return false;
|
|
572
|
-
}
|
|
573
|
-
const bare = hostname.replace(/^\[(.*)\]$/u, "$1");
|
|
574
|
-
return allowed.some((entry) => entry.toLowerCase() === header.toLowerCase() ||
|
|
575
|
-
entry.toLowerCase() === hostname ||
|
|
576
|
-
entry.toLowerCase() === bare);
|
|
577
|
-
}
|
|
578
|
-
function permitted(origin, configured) {
|
|
579
|
-
try {
|
|
580
|
-
const url = new URL(origin);
|
|
581
|
-
return (configured.includes(origin) ||
|
|
582
|
-
url.hostname === "localhost" ||
|
|
583
|
-
url.hostname.endsWith(".localhost") ||
|
|
584
|
-
url.hostname === "127.0.0.1");
|
|
585
|
-
}
|
|
586
|
-
catch {
|
|
587
|
-
return false;
|
|
588
|
-
}
|
|
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;
|
|
589
604
|
}
|