@nylorun/agents 0.9.0-beta → 0.10.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 +131 -0
- package/README.md +287 -69
- package/dist/a2a/handler.d.ts +59 -0
- package/dist/a2a/handler.js +178 -0
- package/dist/a2a/index.d.ts +8 -0
- package/dist/a2a/index.js +7 -0
- package/dist/access.d.ts +101 -0
- package/dist/access.js +111 -0
- package/dist/action-handler.d.ts +64 -0
- package/dist/action-handler.js +283 -0
- package/dist/ag-ui/handler.d.ts +12 -4
- package/dist/ag-ui/handler.js +87 -281
- package/dist/ag-ui/index.d.ts +2 -1
- package/dist/ag-ui/index.js +2 -1
- package/dist/browser.d.ts +97 -0
- package/dist/browser.js +277 -0
- package/dist/client.d.ts +3 -230
- package/dist/client.js +2 -357
- package/dist/connection.d.ts +0 -1
- package/dist/connection.js +3 -9
- package/dist/delivery-token.d.ts +57 -0
- package/dist/delivery-token.js +150 -0
- package/dist/execute-action.d.ts +6 -0
- package/dist/execute-action.js +24 -0
- package/dist/http.d.ts +31 -1
- package/dist/http.js +74 -14
- package/dist/index.d.ts +9 -7
- package/dist/index.js +3 -3
- package/dist/sandbox/client.d.ts +4 -5
- package/dist/sandbox/client.js +4 -8
- package/dist/sandbox/index.d.ts +0 -2
- package/dist/sandbox/index.js +0 -1
- package/dist/served-definitions.d.ts +20 -0
- package/dist/served-definitions.js +100 -0
- package/dist/session-client.d.ts +282 -0
- package/dist/session-client.js +375 -0
- package/dist/sse.js +12 -0
- package/dist/studio-embed.d.ts +9 -0
- package/dist/studio-embed.js +8 -0
- package/dist/token-endpoint.d.ts +23 -0
- package/dist/token-endpoint.js +30 -0
- package/package.json +15 -8
- package/dist/ag-ui/history.d.ts +0 -7
- package/dist/ag-ui/history.js +0 -60
- package/dist/ag-ui/sse.d.ts +0 -9
- package/dist/ag-ui/sse.js +0 -7
- package/dist/ag-ui/translate.d.ts +0 -38
- package/dist/ag-ui/translate.js +0 -222
- package/dist/derived-credentials.d.ts +0 -5
- package/dist/derived-credentials.js +0 -17
- package/dist/executor.d.ts +0 -20
- package/dist/executor.js +0 -417
package/dist/ag-ui/handler.js
CHANGED
|
@@ -1,29 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* handlers. The host
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* AG-UI for the Tenant's agents, served from the app's own server as web-standard
|
|
3
|
+
* `Request` → `Response` handlers. The host signs people in (`subject`); the handler forwards
|
|
4
|
+
* each request to the Runtime's AG-UI endpoint (Host feature `ag-ui-endpoint`) acting for that
|
|
5
|
+
* person, so the Runtime keeps one person out of another's threads and does all the protocol
|
|
6
|
+
* work. The browser never sees the Tenant key, the Runtime's URL or the Tenant id.
|
|
7
|
+
*
|
|
8
|
+
* The same threads are reachable directly from a browser with a subject token: each thread is
|
|
9
|
+
* one session per subject, agent and thread on both paths.
|
|
7
10
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import { EventType } from "@ag-ui/core";
|
|
10
|
-
import { RunAgentInputSchema } from "@ag-ui/core/schemas";
|
|
11
|
-
import { parseSubjectHeaders, } from "@nylorun/core/contracts";
|
|
11
|
+
import { parseSubjectHeaders } from "@nylorun/core/contracts";
|
|
12
12
|
import { createClient, } from "../client.js";
|
|
13
|
-
import { IncompatibleRuntimeError
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import { RunTranslator } from "./translate.js";
|
|
17
|
-
const HEARTBEAT_MS = 15_000;
|
|
18
|
-
/** Host features the handler needs: the chat transcript, and acting for each subject. */
|
|
19
|
-
const REQUIRED_FEATURES = ["transcript-events", "subject-headers"];
|
|
13
|
+
import { IncompatibleRuntimeError } from "../http.js";
|
|
14
|
+
/** The Host feature the handler forwards to. */
|
|
15
|
+
const REQUIRED_FEATURES = ["ag-ui-endpoint"];
|
|
20
16
|
const DEFAULT_SCOPES = ["sessions:own"];
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"turn.paused",
|
|
24
|
-
"turn.failed",
|
|
25
|
-
"turn.cancelled",
|
|
26
|
-
]);
|
|
17
|
+
/** Response headers passed back to the browser; nothing else of the Runtime's. */
|
|
18
|
+
const PASSED_HEADERS = ["content-type", "cache-control", "x-accel-buffering"];
|
|
27
19
|
class Problem extends Error {
|
|
28
20
|
status;
|
|
29
21
|
code;
|
|
@@ -36,49 +28,28 @@ class Problem extends Error {
|
|
|
36
28
|
function problem(status, message, code) {
|
|
37
29
|
return Response.json({ error: message, ...(code ? { code } : {}) }, { status });
|
|
38
30
|
}
|
|
31
|
+
/** The Runtime's refusal, as the browser may see it: no Runtime details. */
|
|
32
|
+
function refusal(status) {
|
|
33
|
+
if (status === 404)
|
|
34
|
+
return new Problem(404, "Not found");
|
|
35
|
+
if (status === 400)
|
|
36
|
+
return new Problem(400, "The request was rejected", "invalid_request");
|
|
37
|
+
if (status === 409)
|
|
38
|
+
return new Problem(409, "The agent is busy or waiting for an answer to an open interrupt", "session_busy");
|
|
39
|
+
return new Problem(502, "The agent service failed", "runtime_error");
|
|
40
|
+
}
|
|
39
41
|
function problemOf(error) {
|
|
40
42
|
if (error instanceof Problem)
|
|
41
43
|
return error;
|
|
42
|
-
if (error instanceof RuntimeError) {
|
|
43
|
-
if (error.status === 404)
|
|
44
|
-
return new Problem(404, "Not found");
|
|
45
|
-
// The Runtime refused what the client sent (e.g. a cursor it cannot read).
|
|
46
|
-
if (error.status === 400)
|
|
47
|
-
return new Problem(400, "The request was rejected", "invalid_request");
|
|
48
|
-
if (error.status === 409)
|
|
49
|
-
return new Problem(409, "The agent is busy or waiting for an answer to an open interrupt", "session_busy");
|
|
50
|
-
return new Problem(502, "The agent service failed", "runtime_error");
|
|
51
|
-
}
|
|
52
44
|
if (error instanceof IncompatibleRuntimeError)
|
|
53
45
|
return new Problem(502, error.message, "runtime_incompatible");
|
|
54
|
-
return new Problem(
|
|
55
|
-
}
|
|
56
|
-
/** A session per subject, agent and thread; one subject can never name another's session. */
|
|
57
|
-
export function sessionIdFor(subject, agentId, threadId) {
|
|
58
|
-
return createHash("sha256")
|
|
59
|
-
.update(`${subject}\u0000${agentId}\u0000${threadId}`)
|
|
60
|
-
.digest("hex")
|
|
61
|
-
.slice(0, 32);
|
|
62
|
-
}
|
|
63
|
-
/** The trailing user message: the Runtime keeps the transcript, AG-UI resends all of it. */
|
|
64
|
-
function lastUserMessage(messages) {
|
|
65
|
-
const last = messages.at(-1);
|
|
66
|
-
if (!last || last.role !== "user")
|
|
67
|
-
throw new Problem(400, "The run's last message must be a user message");
|
|
68
|
-
const text = typeof last.content === "string"
|
|
69
|
-
? last.content
|
|
70
|
-
: last.content
|
|
71
|
-
.filter((part) => part.type === "text")
|
|
72
|
-
.map((part) => part.text)
|
|
73
|
-
.join("\n");
|
|
74
|
-
if (!text.trim())
|
|
75
|
-
throw new Problem(400, "The user message has no text");
|
|
76
|
-
return { id: last.id, text };
|
|
46
|
+
return new Problem(502, "The agent service failed", "runtime_error");
|
|
77
47
|
}
|
|
78
48
|
function normalizeBase(basePath = "/") {
|
|
79
49
|
const trimmed = basePath.replace(/\/+$/, "");
|
|
80
50
|
return trimmed === "" ? "" : trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
81
51
|
}
|
|
52
|
+
const segment = (value) => encodeURIComponent(value);
|
|
82
53
|
export function createAgUiHandler(options) {
|
|
83
54
|
const allowed = new Set(options.agents.map((agent) => (typeof agent === "string" ? agent : agent.id)));
|
|
84
55
|
const base = normalizeBase(options.basePath);
|
|
@@ -87,7 +58,7 @@ export function createAgUiHandler(options) {
|
|
|
87
58
|
if (!checked.ok)
|
|
88
59
|
throw new TypeError(`createAgUiHandler: ${checked.message}`);
|
|
89
60
|
let ready;
|
|
90
|
-
/** The client, once the Runtime is known to
|
|
61
|
+
/** The client, once the Runtime is known to serve AG-UI. Retries after a failure. */
|
|
91
62
|
function connected() {
|
|
92
63
|
ready ??= (async () => {
|
|
93
64
|
const client = await (options.client ?? createClient());
|
|
@@ -102,253 +73,91 @@ export function createAgUiHandler(options) {
|
|
|
102
73
|
});
|
|
103
74
|
return ready;
|
|
104
75
|
}
|
|
105
|
-
/** The client acting for `subject`: the Runtime limits it to the subject's own sessions. */
|
|
106
|
-
async function clientFor(subject) {
|
|
107
|
-
const client = await connected();
|
|
108
|
-
try {
|
|
109
|
-
return client.as(subject, { scopes });
|
|
110
|
-
}
|
|
111
|
-
catch (error) {
|
|
112
|
-
// The host's `subject` returned something the Runtime cannot name (see `as`).
|
|
113
|
-
throw new Problem(500, error.message, "subject_invalid");
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
76
|
async function subjectOf(request) {
|
|
117
77
|
const subject = await options.subject(request);
|
|
118
78
|
if (!subject)
|
|
119
79
|
throw new Problem(401, "Sign in required");
|
|
120
80
|
return subject;
|
|
121
81
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
async function sessionFor(request, agentId, threadId) {
|
|
127
|
-
assertAgent(agentId);
|
|
128
|
-
const subject = await subjectOf(request);
|
|
129
|
-
const client = await clientFor(subject);
|
|
130
|
-
return {
|
|
131
|
-
client,
|
|
132
|
-
subject,
|
|
133
|
-
session: client.session(sessionIdFor(subject, agentId, threadId)),
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
async function submit(session, input) {
|
|
137
|
-
if (input.resume?.length) {
|
|
138
|
-
let cursor = null;
|
|
139
|
-
// The interaction's kind is on the pending view, not in its id.
|
|
140
|
-
const pending = await session.pending();
|
|
141
|
-
const waits = Array.isArray(pending) ? pending : [];
|
|
142
|
-
for (const entry of input.resume) {
|
|
143
|
-
const wait = waits.find((w) => w?.interaction?.id === entry.interruptId);
|
|
144
|
-
const key = `${input.runId}:${entry.interruptId}`;
|
|
145
|
-
const payload = entry.payload;
|
|
146
|
-
const accepted = (wait?.interaction?.kind ?? "approval") === "approval"
|
|
147
|
-
? await session.approve(entry.interruptId, entry.status === "resolved" &&
|
|
148
|
-
(payload === true ||
|
|
149
|
-
(typeof payload === "object" && payload?.approved === true)), { idempotencyKey: key })
|
|
150
|
-
: entry.status === "cancelled"
|
|
151
|
-
? await session.cancel({ idempotencyKey: key, reason: "interrupt cancelled" })
|
|
152
|
-
: await session.respond(entry.interruptId, (entry.payload ?? null), {
|
|
153
|
-
idempotencyKey: key,
|
|
154
|
-
});
|
|
155
|
-
cursor ??= accepted.cursor;
|
|
156
|
-
}
|
|
157
|
-
return cursor;
|
|
158
|
-
}
|
|
159
|
-
const message = lastUserMessage(input.messages);
|
|
160
|
-
// The AG-UI message id is the idempotency key: a retried run replays the same turn,
|
|
161
|
-
// and history gives the message back its client id.
|
|
162
|
-
const accepted = await session.input(message.text, {
|
|
163
|
-
idempotencyKey: message.id,
|
|
164
|
-
});
|
|
165
|
-
return accepted.cursor;
|
|
166
|
-
}
|
|
167
|
-
/** Streams translated events until the run ends. Closing the response stops the read only. */
|
|
168
|
-
function stream(request, translator, source, preface = []) {
|
|
169
|
-
const reading = new AbortController();
|
|
170
|
-
request.signal.addEventListener("abort", () => reading.abort(), { once: true });
|
|
171
|
-
const bytes = new TextEncoder();
|
|
172
|
-
const body = new ReadableStream({
|
|
173
|
-
async start(controller) {
|
|
174
|
-
const write = (text) => {
|
|
175
|
-
try {
|
|
176
|
-
controller.enqueue(bytes.encode(text));
|
|
177
|
-
}
|
|
178
|
-
catch {
|
|
179
|
-
reading.abort();
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
const heartbeat = setInterval(() => write(SSE_HEARTBEAT), HEARTBEAT_MS);
|
|
183
|
-
try {
|
|
184
|
-
write(sseFrame(translator.started()));
|
|
185
|
-
for (const event of preface)
|
|
186
|
-
write(sseFrame(event));
|
|
187
|
-
if (source)
|
|
188
|
-
for await (const event of source(reading.signal)) {
|
|
189
|
-
const step = translator.translate(event);
|
|
190
|
-
step.events.forEach((out, index) => write(sseFrame(out, index === step.events.length - 1 ? event.cursor : undefined)));
|
|
191
|
-
if (step.finished || reading.signal.aborted)
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
catch (error) {
|
|
196
|
-
if (!reading.signal.aborted) {
|
|
197
|
-
const failure = problemOf(error);
|
|
198
|
-
write(sseFrame({
|
|
199
|
-
type: EventType.RUN_ERROR,
|
|
200
|
-
message: failure.message,
|
|
201
|
-
...(failure.code ? { code: failure.code } : {}),
|
|
202
|
-
}));
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
finally {
|
|
206
|
-
clearInterval(heartbeat);
|
|
207
|
-
reading.abort();
|
|
208
|
-
try {
|
|
209
|
-
controller.close();
|
|
210
|
-
}
|
|
211
|
-
catch {
|
|
212
|
-
/* the client is gone */
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
},
|
|
216
|
-
cancel() {
|
|
217
|
-
reading.abort();
|
|
218
|
-
},
|
|
219
|
-
});
|
|
220
|
-
return new Response(body, {
|
|
221
|
-
headers: {
|
|
222
|
-
"content-type": SSE_CONTENT_TYPE,
|
|
223
|
-
"cache-control": "no-cache, no-transform",
|
|
224
|
-
"x-accel-buffering": "no",
|
|
225
|
-
},
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
async function run(request, agentId) {
|
|
82
|
+
/** Forwards to the Runtime acting for `subject`, and passes the answer back. */
|
|
83
|
+
async function forward(request, subject, path, init) {
|
|
84
|
+
const client = await connected();
|
|
85
|
+
let person;
|
|
229
86
|
try {
|
|
230
|
-
|
|
231
|
-
const subject = await subjectOf(request);
|
|
232
|
-
const parsed = RunAgentInputSchema.safeParse(await request.json().catch(() => undefined));
|
|
233
|
-
if (!parsed.success)
|
|
234
|
-
throw new Problem(400, "Invalid RunAgentInput");
|
|
235
|
-
const input = parsed.data;
|
|
236
|
-
// Nylorun tools run in the Runtime or the app's executor; the browser runs none.
|
|
237
|
-
if (input.tools?.length)
|
|
238
|
-
throw new Problem(400, "Frontend tools are not supported");
|
|
239
|
-
const client = await clientFor(subject);
|
|
240
|
-
const extra = (await options.session?.(subject, agentId)) ?? {};
|
|
241
|
-
const translator = new RunTranslator(input.threadId, input.runId);
|
|
242
|
-
let session;
|
|
243
|
-
try {
|
|
244
|
-
// The host's parameters first: they never replace the session's identity.
|
|
245
|
-
session = await client.createSession({
|
|
246
|
-
...extra,
|
|
247
|
-
id: sessionIdFor(subject, agentId, input.threadId),
|
|
248
|
-
agentId,
|
|
249
|
-
ownerUserId: subject,
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
catch (error) {
|
|
253
|
-
if (error instanceof RuntimeError && error.status === 409)
|
|
254
|
-
throw new Problem(409, "The thread's session was opened with other parameters", "session_conflict");
|
|
255
|
-
throw error;
|
|
256
|
-
}
|
|
257
|
-
let cursor;
|
|
258
|
-
try {
|
|
259
|
-
cursor = await submit(session, input);
|
|
260
|
-
}
|
|
261
|
-
catch (error) {
|
|
262
|
-
const failure = problemOf(error);
|
|
263
|
-
// A busy or paused session is a run that could not start, not a transport failure.
|
|
264
|
-
if (failure.status !== 409)
|
|
265
|
-
throw failure;
|
|
266
|
-
return stream(request, translator, undefined, [
|
|
267
|
-
{
|
|
268
|
-
type: EventType.RUN_ERROR,
|
|
269
|
-
message: failure.message,
|
|
270
|
-
code: failure.code,
|
|
271
|
-
},
|
|
272
|
-
]);
|
|
273
|
-
}
|
|
274
|
-
return stream(request, translator, (signal) => session.observe({ ...(cursor ? { cursor } : {}), signal }));
|
|
87
|
+
person = client.as(subject, { scopes });
|
|
275
88
|
}
|
|
276
89
|
catch (error) {
|
|
277
|
-
|
|
278
|
-
|
|
90
|
+
// The host's `subject` returned something the Runtime cannot name (see `as`).
|
|
91
|
+
throw new Problem(500, error.message, "subject_invalid");
|
|
279
92
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
for (;;) {
|
|
288
|
-
const page = await session.history({ ...(cursor ? { cursor } : {}) });
|
|
289
|
-
items.push(...page.items);
|
|
290
|
-
if (!page.cursor || page.cursor === cursor || page.items.length === 0)
|
|
291
|
-
break;
|
|
292
|
-
cursor = page.cursor;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
catch (error) {
|
|
296
|
-
// A thread that never ran has no session yet.
|
|
297
|
-
if (error instanceof RuntimeError && error.status === 404)
|
|
298
|
-
return Response.json([]);
|
|
299
|
-
throw error;
|
|
300
|
-
}
|
|
301
|
-
return Response.json(messagesFromEvents(items));
|
|
93
|
+
const response = await person.transport.forward(`/v1/ag-ui/agents${path}`, {
|
|
94
|
+
...init,
|
|
95
|
+
signal: request.signal,
|
|
96
|
+
});
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
await response.body?.cancel().catch(() => { });
|
|
99
|
+
throw refusal(response.status);
|
|
302
100
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
101
|
+
const headers = new Headers();
|
|
102
|
+
for (const name of PASSED_HEADERS) {
|
|
103
|
+
const value = response.headers.get(name);
|
|
104
|
+
if (value !== null)
|
|
105
|
+
headers.set(name, value);
|
|
306
106
|
}
|
|
107
|
+
return new Response(response.body, { status: response.status, headers });
|
|
307
108
|
}
|
|
308
|
-
async function
|
|
109
|
+
async function run(request, agentId) {
|
|
309
110
|
try {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
break;
|
|
330
|
-
from = page.cursor;
|
|
331
|
-
}
|
|
332
|
-
if (!missed.some((event) => TERMINAL.has(event.type)))
|
|
333
|
-
return new Response(null, { status: 204 });
|
|
334
|
-
return stream(request, translator, () => iterate(missed));
|
|
111
|
+
if (!allowed.has(agentId))
|
|
112
|
+
throw new Problem(404, "Not found");
|
|
113
|
+
const subject = await subjectOf(request);
|
|
114
|
+
const body = (await request.json().catch(() => undefined));
|
|
115
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
116
|
+
throw new Problem(400, "Invalid RunAgentInput", "invalid_request");
|
|
117
|
+
// Only the host names the session's options; whatever the browser sent is replaced.
|
|
118
|
+
const forwarded = body.forwardedProps && typeof body.forwardedProps === "object"
|
|
119
|
+
? { ...body.forwardedProps }
|
|
120
|
+
: {};
|
|
121
|
+
delete forwarded.nylorun;
|
|
122
|
+
const extra = (await options.session?.(subject, agentId)) ?? {};
|
|
123
|
+
if (Object.keys(extra).length > 0)
|
|
124
|
+
forwarded.nylorun = { session: extra };
|
|
125
|
+
return await forward(request, subject, `/${segment(agentId)}`, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: { accept: "text/event-stream" },
|
|
128
|
+
body: JSON.stringify({ ...body, forwardedProps: forwarded }),
|
|
129
|
+
});
|
|
335
130
|
}
|
|
336
131
|
catch (error) {
|
|
337
132
|
const failure = problemOf(error);
|
|
338
133
|
return problem(failure.status, failure.message, failure.code);
|
|
339
134
|
}
|
|
340
135
|
}
|
|
341
|
-
async function
|
|
136
|
+
async function thread(request, agentId, threadId, action, init) {
|
|
342
137
|
try {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
138
|
+
if (!allowed.has(agentId))
|
|
139
|
+
throw new Problem(404, "Not found");
|
|
140
|
+
const subject = await subjectOf(request);
|
|
141
|
+
const query = new URL(request.url).search;
|
|
142
|
+
return await forward(request, subject, `/${segment(agentId)}/threads/${segment(threadId)}/${action}${query}`, init);
|
|
346
143
|
}
|
|
347
144
|
catch (error) {
|
|
348
145
|
const failure = problemOf(error);
|
|
349
146
|
return problem(failure.status, failure.message, failure.code);
|
|
350
147
|
}
|
|
351
148
|
}
|
|
149
|
+
const history = (request, agentId, threadId) => thread(request, agentId, threadId, "messages", { method: "GET" });
|
|
150
|
+
const reattach = (request, agentId, threadId) => {
|
|
151
|
+
const cursor = request.headers.get("last-event-id");
|
|
152
|
+
return thread(request, agentId, threadId, "events", {
|
|
153
|
+
method: "GET",
|
|
154
|
+
headers: {
|
|
155
|
+
accept: "text/event-stream",
|
|
156
|
+
...(cursor ? { "last-event-id": cursor } : {}),
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
const cancel = (request, agentId, threadId) => thread(request, agentId, threadId, "cancel", { method: "POST" });
|
|
352
161
|
async function route(request) {
|
|
353
162
|
const path = new URL(request.url).pathname;
|
|
354
163
|
if (path !== base && !path.startsWith(`${base}/`))
|
|
@@ -359,12 +168,12 @@ export function createAgUiHandler(options) {
|
|
|
359
168
|
.slice(base.length)
|
|
360
169
|
.split("/")
|
|
361
170
|
.slice(1)
|
|
362
|
-
.map((
|
|
171
|
+
.map((part) => decodeURIComponent(part));
|
|
363
172
|
}
|
|
364
173
|
catch {
|
|
365
174
|
return problem(404, "Not found");
|
|
366
175
|
}
|
|
367
|
-
if (segments.some((
|
|
176
|
+
if (segments.some((part) => part === ""))
|
|
368
177
|
return problem(404, "Not found");
|
|
369
178
|
const [agentId, threads, threadId, action] = segments;
|
|
370
179
|
const method = request.method.toUpperCase();
|
|
@@ -392,6 +201,3 @@ export function createAgUiHandler(options) {
|
|
|
392
201
|
}
|
|
393
202
|
return { fetch: route, run, history, reattach, cancel };
|
|
394
203
|
}
|
|
395
|
-
async function* iterate(items) {
|
|
396
|
-
yield* items;
|
|
397
|
-
}
|
package/dist/ag-ui/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@nylorun/agents/ag-ui`: serve the Tenant's agents to AG-UI clients from the app's own
|
|
3
|
-
* server. The
|
|
3
|
+
* server. The handler forwards to the Runtime's AG-UI endpoint for each signed-in person; it
|
|
4
|
+
* loads no AG-UI package itself.
|
|
4
5
|
*/
|
|
5
6
|
export { createAgUiHandler } from "./handler.js";
|
|
6
7
|
export type { AgUiHandler, AgUiHandlerOptions, AgUiSessionOptions, } from "./handler.js";
|
package/dist/ag-ui/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@nylorun/agents/ag-ui`: serve the Tenant's agents to AG-UI clients from the app's own
|
|
3
|
-
* server. The
|
|
3
|
+
* server. The handler forwards to the Runtime's AG-UI endpoint for each signed-in person; it
|
|
4
|
+
* loads no AG-UI package itself.
|
|
4
5
|
*/
|
|
5
6
|
export { createAgUiHandler } from "./handler.js";
|
|
6
7
|
export { toNodeListener } from "./node.js";
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { CredentialSelection, VaultInfo } from "@nylorun/core/contracts";
|
|
2
|
+
import type { TokenSource } from "./http.js";
|
|
3
|
+
import { AgentsClient, type SessionClient } from "./session-client.js";
|
|
4
|
+
/** What the token callback returns: the token, or the app server's `{ token, expiresAt }`. */
|
|
5
|
+
export type TokenReply = string | {
|
|
6
|
+
token: string;
|
|
7
|
+
expiresAt?: string;
|
|
8
|
+
};
|
|
9
|
+
export interface BrowserClientOptions {
|
|
10
|
+
/** The Runtime's URL, as the page reaches it. */
|
|
11
|
+
url: string;
|
|
12
|
+
/** The Tenant's publishable key for this app (`nr_pub_…`). */
|
|
13
|
+
publishableKey: string;
|
|
14
|
+
/** Returns a subject token for the signed-in person, usually from your app server. */
|
|
15
|
+
token(signal?: AbortSignal): Promise<TokenReply>;
|
|
16
|
+
fetch?: typeof fetch;
|
|
17
|
+
}
|
|
18
|
+
interface Claims {
|
|
19
|
+
sub?: string;
|
|
20
|
+
exp?: number;
|
|
21
|
+
}
|
|
22
|
+
/** The token's claims, read without verifying (the Runtime verifies). */
|
|
23
|
+
export declare function readClaims(token: string): Claims;
|
|
24
|
+
/** A token kept in memory, fetched on demand, one fetch at a time. */
|
|
25
|
+
export declare class CachedToken implements TokenSource {
|
|
26
|
+
private readonly fetchToken;
|
|
27
|
+
private readonly now;
|
|
28
|
+
private current;
|
|
29
|
+
private pending;
|
|
30
|
+
constructor(fetchToken: (signal?: AbortSignal) => Promise<TokenReply>, now?: () => number);
|
|
31
|
+
get(signal?: AbortSignal): Promise<string>;
|
|
32
|
+
invalidate(): void;
|
|
33
|
+
/** The subject of the current token, fetching one if needed. */
|
|
34
|
+
subject(signal?: AbortSignal): Promise<string>;
|
|
35
|
+
}
|
|
36
|
+
/** The signed-in person's view of the Runtime: their sessions and vaults, the agents they may use. */
|
|
37
|
+
export declare class BrowserClient {
|
|
38
|
+
/** The underlying Tenant API client, for anything this class does not wrap. */
|
|
39
|
+
readonly client: AgentsClient;
|
|
40
|
+
readonly url: string;
|
|
41
|
+
readonly publishableKey: string;
|
|
42
|
+
private readonly tokens;
|
|
43
|
+
private readonly fetcher;
|
|
44
|
+
constructor(options: BrowserClientOptions);
|
|
45
|
+
/** The person the tokens are for. */
|
|
46
|
+
subject(signal?: AbortSignal): Promise<string>;
|
|
47
|
+
/** A current token, e.g. for a request this client does not make itself. */
|
|
48
|
+
token(signal?: AbortSignal): Promise<string>;
|
|
49
|
+
/** The agents the person may use: id, name and description. */
|
|
50
|
+
listAgents(signal?: AbortSignal): Promise<{
|
|
51
|
+
agents: {
|
|
52
|
+
agentId: string;
|
|
53
|
+
name?: string;
|
|
54
|
+
description?: string;
|
|
55
|
+
}[];
|
|
56
|
+
}>;
|
|
57
|
+
listSessions(agentId?: string, signal?: AbortSignal): Promise<{
|
|
58
|
+
sessions: import("./session-client.js").SessionView[];
|
|
59
|
+
}>;
|
|
60
|
+
session(sessionId: string): SessionClient;
|
|
61
|
+
/** A session of the person's own; the owner is always the token's subject. */
|
|
62
|
+
createSession(options: {
|
|
63
|
+
id?: string;
|
|
64
|
+
agentId: string;
|
|
65
|
+
vaultIds?: readonly string[];
|
|
66
|
+
credentialSelections?: readonly CredentialSelection[];
|
|
67
|
+
}): Promise<SessionClient>;
|
|
68
|
+
createVault(options: {
|
|
69
|
+
name: string;
|
|
70
|
+
idempotencyKey: string;
|
|
71
|
+
metadata?: Record<string, string>;
|
|
72
|
+
}): Promise<VaultInfo>;
|
|
73
|
+
listVaults(signal?: AbortSignal): Promise<{
|
|
74
|
+
vaults: VaultInfo[];
|
|
75
|
+
}>;
|
|
76
|
+
/** `fetch` with this client's credentials, retrying once after `401 token_expired`. */
|
|
77
|
+
private send;
|
|
78
|
+
/**
|
|
79
|
+
* For `@ag-ui/client`'s `HttpAgent`: `new HttpAgent({ url, fetch })`. The `fetch` adds the
|
|
80
|
+
* publishable key and a current token, and when the Runtime ends a run's stream because the
|
|
81
|
+
* token expired or was revoked, it reattaches with a new token from the last event, so the
|
|
82
|
+
* agent sees one run.
|
|
83
|
+
*/
|
|
84
|
+
agUi(agentId: string): {
|
|
85
|
+
url: string;
|
|
86
|
+
fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
87
|
+
};
|
|
88
|
+
/** The thread's messages, for `HttpAgent`'s `initialMessages`. */
|
|
89
|
+
agUiHistory(agentId: string, threadId: string, signal?: AbortSignal): Promise<unknown[]>;
|
|
90
|
+
/**
|
|
91
|
+
* Passes a run's frames through; on `nylorun.stream_closed` before the run finished, fetches
|
|
92
|
+
* the rest from the last event id and continues without repeating `RUN_STARTED`.
|
|
93
|
+
*/
|
|
94
|
+
private stitch;
|
|
95
|
+
}
|
|
96
|
+
export declare function createBrowserClient(options: BrowserClientOptions): BrowserClient;
|
|
97
|
+
export {};
|