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