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