@nylorun/agents 0.9.0-beta → 0.11.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 +165 -0
- package/README.md +296 -77
- 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 +63 -0
- package/dist/action-handler.js +279 -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 +3 -361
- package/dist/connection.d.ts +4 -4
- package/dist/connection.js +16 -25
- package/dist/delivery-token.d.ts +62 -0
- package/dist/delivery-token.js +159 -0
- package/dist/execute-action.d.ts +6 -0
- package/dist/execute-action.js +24 -0
- package/dist/http.d.ts +34 -3
- package/dist/http.js +68 -16
- package/dist/index.d.ts +10 -8
- package/dist/index.js +4 -4
- 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
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { SANDBOX_TOOL_NAMES, delegateOf, isBuiltWorkflow, isSandboxToolName, } from "@nylorun/core/define";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { SCOPES_HEADER, SUBJECT_HEADER } from "@nylorun/core/compatibility";
|
|
4
|
+
import { parseSessionEvent, parseSubjectHeaders, } from "@nylorun/core/contracts";
|
|
5
|
+
import { AccessClient, TokensClient } from "./access.js";
|
|
6
|
+
import { Transport, id, segment } from "./http.js";
|
|
7
|
+
import { observeSSE } from "./sse.js";
|
|
8
|
+
function sessionSandboxOf(transport, sessionId) {
|
|
9
|
+
const call = (tool, args) => transport.json(`/v1/sessions/${segment(sessionId)}/sandbox/${segment(tool)}`, "POST", args);
|
|
10
|
+
const sandbox = {};
|
|
11
|
+
for (const tool of SANDBOX_TOOL_NAMES) {
|
|
12
|
+
if (!isSandboxToolName(tool))
|
|
13
|
+
continue;
|
|
14
|
+
sandbox[tool] = (args) => call(tool, args);
|
|
15
|
+
}
|
|
16
|
+
return sandbox;
|
|
17
|
+
}
|
|
18
|
+
function linkedSessionId(event) {
|
|
19
|
+
if (event.type !== "node.agent")
|
|
20
|
+
return undefined;
|
|
21
|
+
const payload = event.payload;
|
|
22
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
23
|
+
return undefined;
|
|
24
|
+
const sessionId = payload.sessionId;
|
|
25
|
+
return typeof sessionId === "string" && sessionId.length > 0
|
|
26
|
+
? sessionId
|
|
27
|
+
: undefined;
|
|
28
|
+
}
|
|
29
|
+
const middlewareClosure = "Hosted definitions support declarative capabilities and before/after hooks, not middleware closures";
|
|
30
|
+
/** The agent's declarations, then those of each agent it uses as a tool, keyed `<child>/<capability>`. */
|
|
31
|
+
function declarationsOf(agent) {
|
|
32
|
+
if (isBuiltWorkflow(agent) || agent.manifest.kind === "workflow")
|
|
33
|
+
return [];
|
|
34
|
+
const built = agent.build?.() ?? agent;
|
|
35
|
+
if (isBuiltWorkflow(built))
|
|
36
|
+
return [];
|
|
37
|
+
const binding = built.getBinding?.();
|
|
38
|
+
if (!binding || !("declarations" in binding))
|
|
39
|
+
return [];
|
|
40
|
+
const found = (binding.declarations ?? []).map((item) => ({ key: item.id, item }));
|
|
41
|
+
for (const tool of binding.tools ?? []) {
|
|
42
|
+
const delegate = delegateOf(tool);
|
|
43
|
+
const child = delegate?.agent;
|
|
44
|
+
for (const item of child?.getBinding().declarations ?? [])
|
|
45
|
+
found.push({ key: `${child.id}/${item.id}`, item });
|
|
46
|
+
// A flow agent used as a tool: its agents' declarations, keyed `<flow>/<agent>/<capability>`.
|
|
47
|
+
const flow = delegate?.workflow;
|
|
48
|
+
for (const leaf of Object.values(flow?.getBinding().agents ?? {}))
|
|
49
|
+
for (const item of leaf.declarations ?? [])
|
|
50
|
+
found.push({ key: `${flow.id}/${leaf.manifest.id}/${item.id}`, item });
|
|
51
|
+
}
|
|
52
|
+
return found;
|
|
53
|
+
}
|
|
54
|
+
function pluginRootsOf(agent) {
|
|
55
|
+
const roots = {};
|
|
56
|
+
for (const { key, item } of declarationsOf(agent))
|
|
57
|
+
if (item.pluginRoot)
|
|
58
|
+
roots[key] = item.pluginRoot;
|
|
59
|
+
return Object.keys(roots).length === 0 ? undefined : roots;
|
|
60
|
+
}
|
|
61
|
+
/** Closures stay on the live binding. The published manifest cannot name them. */
|
|
62
|
+
export function assertNoMiddlewareClosures(agent) {
|
|
63
|
+
if (declarationsOf(agent).some(({ item }) => item.hasMiddleware))
|
|
64
|
+
throw new Error(middlewareClosure);
|
|
65
|
+
}
|
|
66
|
+
export class AgentsClient {
|
|
67
|
+
transport;
|
|
68
|
+
/** The subject this client acts for (`as`), or `undefined` for the principal itself. */
|
|
69
|
+
subject;
|
|
70
|
+
constructor(destination = {}) {
|
|
71
|
+
this.transport = new Transport(destination);
|
|
72
|
+
this.subject = undefined;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A client that acts for `subject`: every call sends `Nylorun-Subject` and
|
|
76
|
+
* `Nylorun-Scopes`, and the Runtime limits it to the scopes and to the subject's own sessions
|
|
77
|
+
* and vaults (Host feature `subject-headers`). For app servers, which authenticate the person
|
|
78
|
+
* themselves and hold the key; nothing is minted, cached or refreshed.
|
|
79
|
+
*/
|
|
80
|
+
as(subject, options = {}) {
|
|
81
|
+
if (this.subject !== undefined)
|
|
82
|
+
throw new Error(`This client already acts for ${this.subject}`);
|
|
83
|
+
const scopes = [...(options.scopes ?? ["sessions:own"])].join(" ");
|
|
84
|
+
const parsed = parseSubjectHeaders(subject, scopes);
|
|
85
|
+
if (!parsed.ok)
|
|
86
|
+
throw new TypeError(parsed.message);
|
|
87
|
+
const client = Object.create(AgentsClient.prototype);
|
|
88
|
+
return Object.assign(client, {
|
|
89
|
+
transport: this.transport.withHeaders({
|
|
90
|
+
[SUBJECT_HEADER]: subject,
|
|
91
|
+
[SCOPES_HEADER]: [...parsed.scopes].join(" "),
|
|
92
|
+
}),
|
|
93
|
+
subject,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Mints subject tokens for signed-in people (Host feature `subject-tokens`), so their
|
|
98
|
+
* browser or app calls the Runtime directly. Application key only.
|
|
99
|
+
*/
|
|
100
|
+
get tokens() {
|
|
101
|
+
return new TokensClient(this.transport);
|
|
102
|
+
}
|
|
103
|
+
/** The access policy, signing keys and revocations (Host feature `subject-tokens`). */
|
|
104
|
+
get access() {
|
|
105
|
+
return new AccessClient(this.transport);
|
|
106
|
+
}
|
|
107
|
+
/** The Runtime's protocol features, including optional ones such as `transcript-events`. */
|
|
108
|
+
hostFeatures(options = {}) {
|
|
109
|
+
return this.transport.hostFeatures(options.signal);
|
|
110
|
+
}
|
|
111
|
+
listAgents(options = {}) {
|
|
112
|
+
return this.transport.json("/v1/agents", "GET", undefined, options.signal);
|
|
113
|
+
}
|
|
114
|
+
listSessions(options = {}) {
|
|
115
|
+
const query = options.agentId
|
|
116
|
+
? `?agentId=${encodeURIComponent(options.agentId)}`
|
|
117
|
+
: "";
|
|
118
|
+
return this.transport.json(`/v1/sessions${query}`, "GET", undefined, options.signal);
|
|
119
|
+
}
|
|
120
|
+
async saveAgent(agent, options) {
|
|
121
|
+
if (isBuiltWorkflow(agent) && agent.manifest.workflowSchemaVersion === 2) {
|
|
122
|
+
// A v2 workflow embeds its agents: one document, carrying their plugin roots.
|
|
123
|
+
const pluginRoots = {};
|
|
124
|
+
for (const binding of Object.values(agent.getBinding().agents)) {
|
|
125
|
+
const leaf = { id: binding.manifest.id, manifest: binding.manifest, getBinding: () => binding };
|
|
126
|
+
assertNoMiddlewareClosures(leaf);
|
|
127
|
+
for (const [key, root] of Object.entries(pluginRootsOf(leaf) ?? {}))
|
|
128
|
+
pluginRoots[`${leaf.id}/${key}`] = root;
|
|
129
|
+
}
|
|
130
|
+
return this.transport.json(`/v1/agents/${segment(agent.id)}`, "PUT", {
|
|
131
|
+
requestId: options.requestId ?? id(),
|
|
132
|
+
manifest: agent.manifest,
|
|
133
|
+
implementationVersion: options.implementationVersion,
|
|
134
|
+
...(Object.keys(pluginRoots).length === 0 ? {} : { pluginRoots }),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (isBuiltWorkflow(agent)) {
|
|
138
|
+
for (const binding of Object.values(agent.getBinding().agents)) {
|
|
139
|
+
await this.saveAgent({
|
|
140
|
+
id: binding.manifest.id,
|
|
141
|
+
manifest: binding.manifest,
|
|
142
|
+
getBinding: () => binding,
|
|
143
|
+
}, options);
|
|
144
|
+
}
|
|
145
|
+
return this.transport.json(`/v1/agents/${segment(agent.id)}`, "PUT", {
|
|
146
|
+
requestId: options.requestId ?? id(),
|
|
147
|
+
manifest: agent.manifest,
|
|
148
|
+
implementationVersion: options.implementationVersion,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
assertNoMiddlewareClosures(agent);
|
|
152
|
+
const pluginRoots = pluginRootsOf(agent);
|
|
153
|
+
return this.transport.json(`/v1/agents/${segment(agent.id)}`, "PUT", {
|
|
154
|
+
requestId: options.requestId ?? id(),
|
|
155
|
+
manifest: agent.manifest,
|
|
156
|
+
implementationVersion: options.implementationVersion,
|
|
157
|
+
...(pluginRoots === undefined ? {} : { pluginRoots }),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async createSession(options) {
|
|
161
|
+
const sessionId = options.id ?? id();
|
|
162
|
+
await this.transport.json(`/v1/sessions/${segment(sessionId)}`, "PUT", {
|
|
163
|
+
requestId: options.requestId ?? id(),
|
|
164
|
+
agentId: options.agentId,
|
|
165
|
+
ownerUserId: options.ownerUserId,
|
|
166
|
+
...(options.info ? { info: options.info } : {}),
|
|
167
|
+
...(options.vaultIds ? { vaultIds: options.vaultIds } : {}),
|
|
168
|
+
...(options.credentialSelections
|
|
169
|
+
? { credentialSelections: options.credentialSelections }
|
|
170
|
+
: {}),
|
|
171
|
+
...(options.sandbox === undefined ? {} : { sandbox: options.sandbox }),
|
|
172
|
+
});
|
|
173
|
+
return this.session(sessionId);
|
|
174
|
+
}
|
|
175
|
+
createVault(options) {
|
|
176
|
+
return this.transport.json("/v1/vaults", "POST", {
|
|
177
|
+
requestId: options.requestId ?? id(),
|
|
178
|
+
idempotencyKey: options.idempotencyKey,
|
|
179
|
+
name: options.name,
|
|
180
|
+
ownerUserId: options.ownerUserId,
|
|
181
|
+
...(options.metadata ? { metadata: options.metadata } : {}),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
listVaults(ownerUserId, signal) {
|
|
185
|
+
return this.transport.json(`/v1/vaults?ownerUserId=${encodeURIComponent(ownerUserId)}`, "GET", undefined, signal);
|
|
186
|
+
}
|
|
187
|
+
getVault(vaultId, signal) {
|
|
188
|
+
return this.transport.json(`/v1/vaults/${segment(vaultId)}`, "GET", undefined, signal);
|
|
189
|
+
}
|
|
190
|
+
deleteVault(vaultId) {
|
|
191
|
+
return this.transport.json(`/v1/vaults/${segment(vaultId)}`, "DELETE");
|
|
192
|
+
}
|
|
193
|
+
createCredential(vaultId, options) {
|
|
194
|
+
return this.transport.json(`/v1/vaults/${segment(vaultId)}/credentials`, "POST", {
|
|
195
|
+
requestId: options.requestId ?? id(),
|
|
196
|
+
idempotencyKey: options.idempotencyKey,
|
|
197
|
+
name: options.name,
|
|
198
|
+
auth: options.auth,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
listCredentials(vaultId, signal) {
|
|
202
|
+
return this.transport.json(`/v1/vaults/${segment(vaultId)}/credentials`, "GET", undefined, signal);
|
|
203
|
+
}
|
|
204
|
+
getCredential(vaultId, credentialId, signal) {
|
|
205
|
+
return this.transport.json(`/v1/vaults/${segment(vaultId)}/credentials/${segment(credentialId)}`, "GET", undefined, signal);
|
|
206
|
+
}
|
|
207
|
+
rotateCredential(vaultId, credentialId, options) {
|
|
208
|
+
return this.transport.json(`/v1/vaults/${segment(vaultId)}/credentials/${segment(credentialId)}`, "POST", {
|
|
209
|
+
requestId: options.requestId ?? id(),
|
|
210
|
+
idempotencyKey: options.idempotencyKey,
|
|
211
|
+
auth: options.auth,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
deleteCredential(vaultId, credentialId) {
|
|
215
|
+
return this.transport.json(`/v1/vaults/${segment(vaultId)}/credentials/${segment(credentialId)}`, "DELETE");
|
|
216
|
+
}
|
|
217
|
+
session(sessionId) {
|
|
218
|
+
return new SessionClient(this.transport, sessionId);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
export class SessionClient {
|
|
222
|
+
transport;
|
|
223
|
+
id;
|
|
224
|
+
sandbox;
|
|
225
|
+
constructor(transport, id) {
|
|
226
|
+
this.transport = transport;
|
|
227
|
+
this.id = id;
|
|
228
|
+
this.sandbox = sessionSandboxOf(transport, id);
|
|
229
|
+
}
|
|
230
|
+
get path() {
|
|
231
|
+
return `/v1/sessions/${segment(this.id)}`;
|
|
232
|
+
}
|
|
233
|
+
inspect(signal) {
|
|
234
|
+
return this.transport.json(this.path, "GET", undefined, signal);
|
|
235
|
+
}
|
|
236
|
+
/** Pending waits / interactions from the session inspect view. */
|
|
237
|
+
async pending(signal) {
|
|
238
|
+
const view = await this.inspect(signal);
|
|
239
|
+
return view.waits ?? [];
|
|
240
|
+
}
|
|
241
|
+
command(command, signal) {
|
|
242
|
+
return this.transport.json(`${this.path}/commands`, "POST", command, signal);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Start a turn. Strings become `message.content`; any other JSON value becomes
|
|
246
|
+
* `message.data` (workflows.md §5).
|
|
247
|
+
*/
|
|
248
|
+
input(value, options) {
|
|
249
|
+
const base = {
|
|
250
|
+
type: "message",
|
|
251
|
+
requestId: options.requestId ?? id(),
|
|
252
|
+
idempotencyKey: options.idempotencyKey,
|
|
253
|
+
};
|
|
254
|
+
return this.command(typeof value === "string"
|
|
255
|
+
? { ...base, content: value }
|
|
256
|
+
: { ...base, data: value }, options.signal);
|
|
257
|
+
}
|
|
258
|
+
approve(interactionId, approved, options) {
|
|
259
|
+
return this.command({
|
|
260
|
+
type: "approve",
|
|
261
|
+
interactionId,
|
|
262
|
+
approved,
|
|
263
|
+
requestId: options.requestId ?? id(),
|
|
264
|
+
idempotencyKey: options.idempotencyKey,
|
|
265
|
+
}, options.signal);
|
|
266
|
+
}
|
|
267
|
+
respond(interactionId, value, options) {
|
|
268
|
+
return this.command({
|
|
269
|
+
type: "respond",
|
|
270
|
+
interactionId,
|
|
271
|
+
value,
|
|
272
|
+
requestId: options.requestId ?? id(),
|
|
273
|
+
idempotencyKey: options.idempotencyKey,
|
|
274
|
+
}, options.signal);
|
|
275
|
+
}
|
|
276
|
+
cancel(options) {
|
|
277
|
+
return this.command({
|
|
278
|
+
type: "cancel",
|
|
279
|
+
requestId: options.requestId ?? id(),
|
|
280
|
+
idempotencyKey: options.idempotencyKey,
|
|
281
|
+
...(options.reason ? { reason: options.reason } : {}),
|
|
282
|
+
}, options.signal);
|
|
283
|
+
}
|
|
284
|
+
/** `agent` narrows to one agent used as a tool: its delegationId or its path. */
|
|
285
|
+
async history(options = {}) {
|
|
286
|
+
const query = new URLSearchParams();
|
|
287
|
+
if (options.cursor)
|
|
288
|
+
query.set("cursor", options.cursor);
|
|
289
|
+
if (options.agent)
|
|
290
|
+
query.set("agent", options.agent);
|
|
291
|
+
const search = query.toString();
|
|
292
|
+
const page = z
|
|
293
|
+
.object({ items: z.array(z.unknown()), cursor: z.string().nullable() })
|
|
294
|
+
.parse(await this.transport.json(`${this.path}/items${search ? `?${search}` : ""}`, "GET", undefined, options.signal));
|
|
295
|
+
// Typed when the catalog knows the type; a newer Runtime's type stays a bare envelope.
|
|
296
|
+
return { items: page.items.map(parseSessionEvent), cursor: page.cursor };
|
|
297
|
+
}
|
|
298
|
+
async *observe(options = {}) {
|
|
299
|
+
if (!options.follow) {
|
|
300
|
+
yield* this.observeOne(this.id, options);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
yield* this.observeFollowing(options);
|
|
304
|
+
}
|
|
305
|
+
async *observeOne(sessionId, options) {
|
|
306
|
+
for await (const frame of observeSSE(this.transport, `/v1/sessions/${segment(sessionId)}/events`, options)) {
|
|
307
|
+
if (frame.event === "heartbeat" || frame.event === "ready")
|
|
308
|
+
continue;
|
|
309
|
+
yield parseSessionEvent(JSON.parse(frame.data));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Own session stream plus linked agent sessions announced in `node.agent`
|
|
314
|
+
* events, merged by `time` then arrival order (workflows.md §11).
|
|
315
|
+
*/
|
|
316
|
+
async *observeFollowing(options) {
|
|
317
|
+
const signal = options.signal;
|
|
318
|
+
const linked = new Set();
|
|
319
|
+
const queue = [];
|
|
320
|
+
let wait;
|
|
321
|
+
let open = 1;
|
|
322
|
+
let failed;
|
|
323
|
+
const wake = () => {
|
|
324
|
+
wait?.();
|
|
325
|
+
wait = undefined;
|
|
326
|
+
};
|
|
327
|
+
const push = (event) => {
|
|
328
|
+
queue.push(event);
|
|
329
|
+
queue.sort((a, b) => {
|
|
330
|
+
const byTime = a.time.localeCompare(b.time);
|
|
331
|
+
return byTime !== 0 ? byTime : a.cursor.localeCompare(b.cursor);
|
|
332
|
+
});
|
|
333
|
+
wake();
|
|
334
|
+
};
|
|
335
|
+
const pump = async (sessionId, cursor) => {
|
|
336
|
+
try {
|
|
337
|
+
for await (const event of this.observeOne(sessionId, { cursor, signal })) {
|
|
338
|
+
push(event);
|
|
339
|
+
const link = linkedSessionId(event);
|
|
340
|
+
if (link && link !== this.id && !linked.has(link)) {
|
|
341
|
+
linked.add(link);
|
|
342
|
+
open += 1;
|
|
343
|
+
void pump(link).finally(() => {
|
|
344
|
+
open -= 1;
|
|
345
|
+
wake();
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
if (!signal?.aborted)
|
|
352
|
+
failed = error;
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
void pump(this.id, options.cursor).finally(() => {
|
|
356
|
+
open -= 1;
|
|
357
|
+
wake();
|
|
358
|
+
});
|
|
359
|
+
while (!signal?.aborted) {
|
|
360
|
+
if (failed)
|
|
361
|
+
throw failed;
|
|
362
|
+
if (queue.length) {
|
|
363
|
+
yield queue.shift();
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (open <= 0)
|
|
367
|
+
return;
|
|
368
|
+
await new Promise((resolve) => {
|
|
369
|
+
wait = resolve;
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
export { RuntimeError, IncompatibleRuntimeError } from "./http.js";
|
|
375
|
+
export { AccessClient, PublishableKeysClient, SigningKeysClient, TokensClient, } from "./access.js";
|
package/dist/sse.js
CHANGED
|
@@ -65,7 +65,11 @@ export async function* observeSSE(transport, path, options = {}) {
|
|
|
65
65
|
const signal = options.signal ?? controller.signal;
|
|
66
66
|
let cursor = options.cursor;
|
|
67
67
|
let retry = 250;
|
|
68
|
+
// The Runtime ended the stream because its subject token expired or was revoked: reconnect
|
|
69
|
+
// at once (the transport fetches a new token), but only once in a row.
|
|
70
|
+
let closedByRuntime = 0;
|
|
68
71
|
while (!signal.aborted) {
|
|
72
|
+
let reconnectNow = false;
|
|
69
73
|
try {
|
|
70
74
|
const response = await transport.request(path, {
|
|
71
75
|
signal,
|
|
@@ -75,7 +79,13 @@ export async function* observeSSE(transport, path, options = {}) {
|
|
|
75
79
|
},
|
|
76
80
|
});
|
|
77
81
|
for await (const event of readSSE(response, signal)) {
|
|
82
|
+
if (event.event === "nylorun.closed") {
|
|
83
|
+
closedByRuntime += 1;
|
|
84
|
+
reconnectNow = closedByRuntime === 1;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
78
87
|
retry = 250;
|
|
88
|
+
closedByRuntime = 0;
|
|
79
89
|
if (event.id)
|
|
80
90
|
cursor = event.id;
|
|
81
91
|
yield event;
|
|
@@ -90,6 +100,8 @@ export async function* observeSSE(transport, path, options = {}) {
|
|
|
90
100
|
![408, 429].includes(error.status))
|
|
91
101
|
throw error;
|
|
92
102
|
}
|
|
103
|
+
if (reconnectNow)
|
|
104
|
+
continue;
|
|
93
105
|
if (!signal.aborted)
|
|
94
106
|
await delay(retry, signal).catch(() => { });
|
|
95
107
|
retry = Math.min(30000, retry * 2);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nylorun/agents/studio-embed`: the contract between Studio and an app that embeds it in an
|
|
3
|
+
* iframe (Studio design §8). Studio's web app and embedders such as Babai import it from here,
|
|
4
|
+
* because Studio may not depend on `@nylorun/core` directly.
|
|
5
|
+
*
|
|
6
|
+
* No Node-only module is imported here or below.
|
|
7
|
+
*/
|
|
8
|
+
export { STUDIO_EMBED_MESSAGE_TYPE, STUDIO_EMBED_PROTOCOLS, StudioEmbedMessageSchema, StudioLoginTokenRequestSchema, StudioLoginTokenResponseSchema, StudioSessionRequestSchema, StudioSessionResponseSchema, StudioThemeSchema, isFrameAncestor, parseFrameAncestors, } from "@nylorun/core/contracts";
|
|
9
|
+
export type { StudioEmbedKind, StudioEmbedMessage, StudioLoginTokenRequest, StudioLoginTokenResponse, StudioSessionRequest, StudioSessionResponse, StudioTheme, } from "@nylorun/core/contracts";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nylorun/agents/studio-embed`: the contract between Studio and an app that embeds it in an
|
|
3
|
+
* iframe (Studio design §8). Studio's web app and embedders such as Babai import it from here,
|
|
4
|
+
* because Studio may not depend on `@nylorun/core` directly.
|
|
5
|
+
*
|
|
6
|
+
* No Node-only module is imported here or below.
|
|
7
|
+
*/
|
|
8
|
+
export { STUDIO_EMBED_MESSAGE_TYPE, STUDIO_EMBED_PROTOCOLS, StudioEmbedMessageSchema, StudioLoginTokenRequestSchema, StudioLoginTokenResponseSchema, StudioSessionRequestSchema, StudioSessionResponseSchema, StudioThemeSchema, isFrameAncestor, parseFrameAncestors, } from "@nylorun/core/contracts";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A route for app servers that mints a subject token for the signed-in person (Host feature
|
|
3
|
+
* `subject-tokens`). The browser client calls it whenever it needs a token. Mount it on your
|
|
4
|
+
* app's own origin behind your sign-in: `POST` only, no CORS, never cached.
|
|
5
|
+
*
|
|
6
|
+
* export const POST = createTokenEndpoint({
|
|
7
|
+
* client, role: "user", subject: (request) => userFromCookie(request)?.id,
|
|
8
|
+
* });
|
|
9
|
+
*/
|
|
10
|
+
import type { TokenScope } from "@nylorun/core/contracts";
|
|
11
|
+
import type { AgentsClient } from "./session-client.js";
|
|
12
|
+
export interface TokenEndpointOptions {
|
|
13
|
+
/** The application client (the Tenant key stays on the server). */
|
|
14
|
+
client: AgentsClient | Promise<AgentsClient>;
|
|
15
|
+
/** The signed-in person the token is for; `undefined` answers 401. */
|
|
16
|
+
subject(request: Request): string | undefined | Promise<string | undefined>;
|
|
17
|
+
/** The access policy role to mint for, or a function of the request and subject. */
|
|
18
|
+
role: string | ((request: Request, subject: string) => string | Promise<string>);
|
|
19
|
+
scopes?: readonly TokenScope[];
|
|
20
|
+
agents?: readonly string[];
|
|
21
|
+
ttlSeconds?: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function createTokenEndpoint(options: TokenEndpointOptions): (request: Request) => Promise<Response>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { RuntimeError } from "./http.js";
|
|
2
|
+
const NO_STORE = { "cache-control": "no-store" };
|
|
3
|
+
export function createTokenEndpoint(options) {
|
|
4
|
+
return async (request) => {
|
|
5
|
+
if (request.method !== "POST")
|
|
6
|
+
return new Response(null, { status: 405, headers: { allow: "POST", ...NO_STORE } });
|
|
7
|
+
const subject = await options.subject(request);
|
|
8
|
+
if (!subject)
|
|
9
|
+
return Response.json({ error: "Sign in required" }, { status: 401, headers: NO_STORE });
|
|
10
|
+
const role = typeof options.role === "string"
|
|
11
|
+
? options.role
|
|
12
|
+
: await options.role(request, subject);
|
|
13
|
+
try {
|
|
14
|
+
const client = await options.client;
|
|
15
|
+
const minted = await client.tokens.create({
|
|
16
|
+
subject,
|
|
17
|
+
role,
|
|
18
|
+
...(options.scopes ? { scopes: options.scopes } : {}),
|
|
19
|
+
...(options.agents ? { agents: options.agents } : {}),
|
|
20
|
+
...(options.ttlSeconds === undefined ? {} : { ttlSeconds: options.ttlSeconds }),
|
|
21
|
+
});
|
|
22
|
+
return Response.json({ token: minted.token, expiresAt: minted.expiresAt }, { headers: NO_STORE });
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
// The person sees that a token could not be issued, never the Runtime's details.
|
|
26
|
+
const status = error instanceof RuntimeError && error.status === 400 ? 403 : 502;
|
|
27
|
+
return Response.json({ error: "Could not issue a token" }, { status, headers: NO_STORE });
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nylorun/agents",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Nylorun agent definitions, session client and
|
|
3
|
+
"version": "0.11.0-beta",
|
|
4
|
+
"description": "Nylorun agent definitions, session client and Action endpoint",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"files": [
|
|
@@ -24,13 +24,21 @@
|
|
|
24
24
|
"types": "./dist/client.d.ts",
|
|
25
25
|
"import": "./dist/client.js"
|
|
26
26
|
},
|
|
27
|
-
"./executor": {
|
|
28
|
-
"types": "./dist/executor.d.ts",
|
|
29
|
-
"import": "./dist/executor.js"
|
|
30
|
-
},
|
|
31
27
|
"./ag-ui": {
|
|
32
28
|
"types": "./dist/ag-ui/index.d.ts",
|
|
33
29
|
"import": "./dist/ag-ui/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./browser": {
|
|
32
|
+
"types": "./dist/browser.d.ts",
|
|
33
|
+
"import": "./dist/browser.js"
|
|
34
|
+
},
|
|
35
|
+
"./a2a": {
|
|
36
|
+
"types": "./dist/a2a/index.d.ts",
|
|
37
|
+
"import": "./dist/a2a/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./studio-embed": {
|
|
40
|
+
"types": "./dist/studio-embed.d.ts",
|
|
41
|
+
"import": "./dist/studio-embed.js"
|
|
34
42
|
}
|
|
35
43
|
},
|
|
36
44
|
"scripts": {
|
|
@@ -39,8 +47,7 @@
|
|
|
39
47
|
"test": "vitest run --passWithNoTests"
|
|
40
48
|
},
|
|
41
49
|
"dependencies": {
|
|
42
|
-
"@
|
|
43
|
-
"@nylorun/core": "0.8.0-beta",
|
|
50
|
+
"@nylorun/core": "0.10.0-beta",
|
|
44
51
|
"gray-matter": "^4.0.3"
|
|
45
52
|
},
|
|
46
53
|
"peerDependencies": {
|
package/dist/ag-ui/history.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* An AG-UI transcript rebuilt from a session's log, with the ids a live run gave each
|
|
3
|
-
* message (see `translate.ts`), usable as `HttpAgent`'s `initialMessages`.
|
|
4
|
-
*/
|
|
5
|
-
import type { Message } from "@ag-ui/core";
|
|
6
|
-
import { type LiveEvent } from "@nylorun/core/contracts";
|
|
7
|
-
export declare function messagesFromEvents(items: readonly LiveEvent[]): Message[];
|
package/dist/ag-ui/history.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { parseTranscriptEvent } from "@nylorun/core/contracts";
|
|
2
|
-
import { outputText, toolResultOf } from "./translate.js";
|
|
3
|
-
export function messagesFromEvents(items) {
|
|
4
|
-
const messages = [];
|
|
5
|
-
const results = new Set();
|
|
6
|
-
let sawAssistant = false;
|
|
7
|
-
for (const raw of items) {
|
|
8
|
-
if (raw.type === "command.message") {
|
|
9
|
-
const p = (raw.payload ?? {});
|
|
10
|
-
messages.push({
|
|
11
|
-
// The handler sends the client's message id as the idempotency key.
|
|
12
|
-
id: p.idempotencyKey ?? raw.eventId,
|
|
13
|
-
role: "user",
|
|
14
|
-
content: typeof p.content === "string" ? p.content : JSON.stringify(p.data),
|
|
15
|
-
});
|
|
16
|
-
sawAssistant = false;
|
|
17
|
-
continue;
|
|
18
|
-
}
|
|
19
|
-
const event = parseTranscriptEvent(raw);
|
|
20
|
-
if (!event)
|
|
21
|
-
continue;
|
|
22
|
-
const result = toolResultOf(event);
|
|
23
|
-
if (result) {
|
|
24
|
-
if (results.has(result.callId))
|
|
25
|
-
continue;
|
|
26
|
-
results.add(result.callId);
|
|
27
|
-
messages.push({
|
|
28
|
-
id: `${result.callId}:result`,
|
|
29
|
-
role: "tool",
|
|
30
|
-
toolCallId: result.callId,
|
|
31
|
-
content: result.content,
|
|
32
|
-
});
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
if (event.type === "message.assistant" && !event.payload.agent) {
|
|
36
|
-
const p = event.payload;
|
|
37
|
-
sawAssistant = true;
|
|
38
|
-
const message = { id: p.invocationId, role: "assistant" };
|
|
39
|
-
if (p.text)
|
|
40
|
-
message.content = p.text;
|
|
41
|
-
if (p.toolCalls.length)
|
|
42
|
-
message.toolCalls = p.toolCalls.map((call) => ({
|
|
43
|
-
id: call.callId,
|
|
44
|
-
type: "function",
|
|
45
|
-
function: { name: call.name, arguments: JSON.stringify(call.input ?? {}) },
|
|
46
|
-
}));
|
|
47
|
-
messages.push(message);
|
|
48
|
-
}
|
|
49
|
-
else if (event.type === "turn.completed" && !sawAssistant) {
|
|
50
|
-
const output = event.payload.output;
|
|
51
|
-
if (output !== undefined && output !== null && outputText(output))
|
|
52
|
-
messages.push({
|
|
53
|
-
id: raw.eventId,
|
|
54
|
-
role: "assistant",
|
|
55
|
-
content: outputText(output),
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return messages;
|
|
60
|
-
}
|
package/dist/ag-ui/sse.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AG-UI over server-sent events: one `data:` line of JSON per event. The last AG-UI event
|
|
3
|
-
* made from a Runtime event carries that event's cursor as `id:`, so a client that resumes
|
|
4
|
-
* from `Last-Event-ID` never lands inside a group.
|
|
5
|
-
*/
|
|
6
|
-
import type { BaseEvent } from "@ag-ui/core";
|
|
7
|
-
export declare const SSE_CONTENT_TYPE = "text/event-stream";
|
|
8
|
-
export declare function sseFrame(event: BaseEvent, id?: string): string;
|
|
9
|
-
export declare const SSE_HEARTBEAT = ": keep-alive\n\n";
|
package/dist/ag-ui/sse.js
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export const SSE_CONTENT_TYPE = "text/event-stream";
|
|
2
|
-
export function sseFrame(event, id) {
|
|
3
|
-
// Cursors are opaque base64; a line break would end the field early.
|
|
4
|
-
const idLine = id && !/[\r\n]/.test(id) ? `id: ${id}\n` : "";
|
|
5
|
-
return `${idLine}data: ${JSON.stringify(event)}\n\n`;
|
|
6
|
-
}
|
|
7
|
-
export const SSE_HEARTBEAT = ": keep-alive\n\n";
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runtime transcript events → AG-UI 1.0 events for one run. Ids: an assistant message is its
|
|
3
|
-
* model call's `invocationId`, a tool call is the model's `callId`, a tool result is
|
|
4
|
-
* `<callId>:result`, so live runs and `messagesFromEvents` agree.
|
|
5
|
-
*/
|
|
6
|
-
import { type BaseEvent } from "@ag-ui/core";
|
|
7
|
-
import { type LiveEvent, type TranscriptEvent } from "@nylorun/core/contracts";
|
|
8
|
-
/** The text a tool result shows; `undefined` while there is no result yet. */
|
|
9
|
-
export declare function toolResultText(result: unknown): string | undefined;
|
|
10
|
-
export declare function outputText(output: unknown): string;
|
|
11
|
-
/** One Runtime event's result for a run: AG-UI events, and whether the run ended. */
|
|
12
|
-
export interface Step {
|
|
13
|
-
readonly events: BaseEvent[];
|
|
14
|
-
readonly finished: boolean;
|
|
15
|
-
}
|
|
16
|
-
/** A tool result from any of the three places a call can end. */
|
|
17
|
-
export declare function toolResultOf(event: TranscriptEvent): {
|
|
18
|
-
callId: string;
|
|
19
|
-
content: string;
|
|
20
|
-
} | undefined;
|
|
21
|
-
export declare class RunTranslator {
|
|
22
|
-
private readonly threadId;
|
|
23
|
-
private readonly runId;
|
|
24
|
-
/** Tool invocation → the model's call id, for interrupts that name the invocation. */
|
|
25
|
-
private readonly callIds;
|
|
26
|
-
private readonly closed;
|
|
27
|
-
private sawAssistant;
|
|
28
|
-
/**
|
|
29
|
-
* `reattached`: the run continues from a cursor, so earlier assistant messages already
|
|
30
|
-
* reached the client and a workflow's output is not repeated as text.
|
|
31
|
-
*/
|
|
32
|
-
constructor(threadId: string, runId: string, options?: {
|
|
33
|
-
reattached?: boolean;
|
|
34
|
-
});
|
|
35
|
-
translate(raw: LiveEvent): Step;
|
|
36
|
-
started(): BaseEvent;
|
|
37
|
-
private finished;
|
|
38
|
-
}
|