@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.
Files changed (52) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/README.md +287 -69
  3. package/dist/a2a/handler.d.ts +59 -0
  4. package/dist/a2a/handler.js +178 -0
  5. package/dist/a2a/index.d.ts +8 -0
  6. package/dist/a2a/index.js +7 -0
  7. package/dist/access.d.ts +101 -0
  8. package/dist/access.js +111 -0
  9. package/dist/action-handler.d.ts +64 -0
  10. package/dist/action-handler.js +283 -0
  11. package/dist/ag-ui/handler.d.ts +12 -4
  12. package/dist/ag-ui/handler.js +87 -281
  13. package/dist/ag-ui/index.d.ts +2 -1
  14. package/dist/ag-ui/index.js +2 -1
  15. package/dist/browser.d.ts +97 -0
  16. package/dist/browser.js +277 -0
  17. package/dist/client.d.ts +3 -230
  18. package/dist/client.js +2 -357
  19. package/dist/connection.d.ts +0 -1
  20. package/dist/connection.js +3 -9
  21. package/dist/delivery-token.d.ts +57 -0
  22. package/dist/delivery-token.js +150 -0
  23. package/dist/execute-action.d.ts +6 -0
  24. package/dist/execute-action.js +24 -0
  25. package/dist/http.d.ts +31 -1
  26. package/dist/http.js +74 -14
  27. package/dist/index.d.ts +9 -7
  28. package/dist/index.js +3 -3
  29. package/dist/sandbox/client.d.ts +4 -5
  30. package/dist/sandbox/client.js +4 -8
  31. package/dist/sandbox/index.d.ts +0 -2
  32. package/dist/sandbox/index.js +0 -1
  33. package/dist/served-definitions.d.ts +20 -0
  34. package/dist/served-definitions.js +100 -0
  35. package/dist/session-client.d.ts +282 -0
  36. package/dist/session-client.js +375 -0
  37. package/dist/sse.js +12 -0
  38. package/dist/studio-embed.d.ts +9 -0
  39. package/dist/studio-embed.js +8 -0
  40. package/dist/token-endpoint.d.ts +23 -0
  41. package/dist/token-endpoint.js +30 -0
  42. package/package.json +15 -8
  43. package/dist/ag-ui/history.d.ts +0 -7
  44. package/dist/ag-ui/history.js +0 -60
  45. package/dist/ag-ui/sse.d.ts +0 -9
  46. package/dist/ag-ui/sse.js +0 -7
  47. package/dist/ag-ui/translate.d.ts +0 -38
  48. package/dist/ag-ui/translate.js +0 -222
  49. package/dist/derived-credentials.d.ts +0 -5
  50. package/dist/derived-credentials.js +0 -17
  51. package/dist/executor.d.ts +0 -20
  52. package/dist/executor.js +0 -417
@@ -0,0 +1,283 @@
1
+ /**
2
+ * An Action endpoint (design: Action endpoints): the application mounts this handler at a URL
3
+ * and registers the URL; the Runtime POSTs each Action for the served agents to it and records
4
+ * the answer as the Action's outcome. No stream, no claim, no key needed to serve.
5
+ *
6
+ * Each request is checked before any code runs: the delivery token must be signed by the
7
+ * Tenant's signing key, be for this Tenant, this URL (once known), this Action and this
8
+ * generation, and cover the exact body. The Action then runs through `executeAction`, with the
9
+ * request's signal as `ctx.signal`.
10
+ */
11
+ import { OUTCOME_HEADER, SIGNATURE_HEADER, } from "@nylorun/core/compatibility";
12
+ import { ActionDeliverySchema, DeliveryHeartbeatResponseSchema, EndpointPingResponseSchema, PutEndpointsRequestSchema, } from "@nylorun/core/contracts";
13
+ import { toNodeListener } from "./ag-ui/node.js";
14
+ import { createClient } from "./client.js";
15
+ import { DeliveryVerificationError, JwksCache, verifyDeliveryToken, } from "./delivery-token.js";
16
+ import { executeAction, runsInBackground, } from "./execute-action.js";
17
+ import { RuntimeError, Transport, delay, segment } from "./http.js";
18
+ import { createActionSandbox } from "./sandbox/client.js";
19
+ import { buildAgents, embeddedIn, flowManifestHash, implementationVersionOf, } from "./served-definitions.js";
20
+ /** The Host feature `register` needs. */
21
+ const REQUIRED_FEATURE = "action-endpoints";
22
+ export function createActionHandler(options) {
23
+ const agents = buildAgents(options.agents, "createActionHandler");
24
+ const version = implementationVersionOf(options);
25
+ let client;
26
+ const clientOf = () => (client ??= Promise.resolve(options.client ?? createClient()));
27
+ let audience = options.url;
28
+ let verification;
29
+ const verificationOf = () => (verification ??= resolveVerification(options, clientOf)).catch((error) => {
30
+ verification = undefined;
31
+ throw error;
32
+ });
33
+ let callbacks;
34
+ const report = (error) => {
35
+ try {
36
+ options.onError?.(error);
37
+ }
38
+ catch { }
39
+ };
40
+ const handle = async (request) => {
41
+ if (request.method !== "POST")
42
+ return answer(405, "method_not_allowed", "An Action endpoint accepts POST only");
43
+ const body = new Uint8Array(await request.arrayBuffer());
44
+ const { tenant, url, fetch, keys } = await verificationOf();
45
+ const token = request.headers.get(SIGNATURE_HEADER);
46
+ let claims;
47
+ try {
48
+ claims = await verifyDeliveryToken(token, {
49
+ keys: keys.lookup,
50
+ tenantId: tenant,
51
+ body,
52
+ ...(audience === undefined ? {} : { audience }),
53
+ });
54
+ }
55
+ catch (error) {
56
+ if (!(error instanceof DeliveryVerificationError))
57
+ throw error;
58
+ // A key this endpoint has not seen yet (a rotation within the refetch interval) is not
59
+ // a refusal: the Runtime retries a 503, while a 401 would fail the Action.
60
+ return answer(error.code === "key_unknown" ? 503 : 401, error.code, error.message);
61
+ }
62
+ let delivery;
63
+ try {
64
+ delivery = ActionDeliverySchema.parse(JSON.parse(new TextDecoder().decode(body)));
65
+ }
66
+ catch {
67
+ return answer(400, "invalid_delivery", "The body is not an Action delivery");
68
+ }
69
+ if (delivery.type === "ping") {
70
+ if (claims.sub !== "ping" || claims.agt !== delivery.agentId)
71
+ return answer(401, "signature_invalid", "The token is for another delivery");
72
+ const agent = agents.get(delivery.agentId);
73
+ if (!agent)
74
+ return notServed(delivery.agentId);
75
+ const manifestHash = flowManifestHash(agent);
76
+ return Response.json({
77
+ agentId: agent.id,
78
+ implementationVersion: version,
79
+ ...(manifestHash === undefined ? {} : { manifestHash }),
80
+ });
81
+ }
82
+ const { action } = delivery;
83
+ if (claims.sub !== action.actionId ||
84
+ claims.gen !== action.generation ||
85
+ claims.agt !== action.agentId)
86
+ return answer(401, "signature_invalid", "The token is for another delivery");
87
+ const agent = agents.get(action.agentId);
88
+ if (!agent)
89
+ return notServed(action.agentId);
90
+ const served = flowManifestHash(agent);
91
+ if (served !== undefined && "key" in action && action.manifestHash !== served)
92
+ return answer(409, "version_mismatch", `Action ${action.actionId} belongs to manifest ${action.manifestHash} of '${action.agentId}'; this endpoint serves ${served}`);
93
+ if (runsInBackground(action, agent)) {
94
+ const work = runInBackground({
95
+ action,
96
+ agent,
97
+ token: token,
98
+ sandbox: delivery.sandbox,
99
+ runtime: { url, tenant, ...(fetch ? { fetch } : {}) },
100
+ report,
101
+ });
102
+ options.waitUntil?.(work);
103
+ return new Response(null, { status: 202 });
104
+ }
105
+ const sandbox = delivery.sandbox
106
+ ? createActionSandbox({
107
+ transport: (callbacks = callbacks
108
+ ? callbacks.withKey(token)
109
+ : new Transport({ url, tenant, key: token, ...(fetch ? { fetch } : {}) })),
110
+ actionId: action.actionId,
111
+ signal: request.signal,
112
+ })
113
+ : undefined;
114
+ try {
115
+ const outcome = await executeAction(action, agent, request.signal, {
116
+ ...(sandbox ? { sandbox } : {}),
117
+ });
118
+ return Response.json(outcome, { headers: { [OUTCOME_HEADER]: "1" } });
119
+ }
120
+ catch (error) {
121
+ if (request.signal.aborted)
122
+ return answer(503, "aborted", "The Runtime closed the delivery");
123
+ // Tool errors are outcomes; a throw means this endpoint cannot run the Action at all.
124
+ report(error);
125
+ return answer(404, "action_not_served", error instanceof Error ? error.message : String(error));
126
+ }
127
+ };
128
+ const fetchHandler = async (request) => {
129
+ try {
130
+ return await handle(request);
131
+ }
132
+ catch (error) {
133
+ report(error);
134
+ // Not run: the Runtime retries a 503 whatever the Action's kind.
135
+ return answer(503, "endpoint_unavailable", error instanceof Error ? error.message : String(error));
136
+ }
137
+ };
138
+ return {
139
+ fetch: fetchHandler,
140
+ node: toNodeListener({ fetch: fetchHandler }),
141
+ async register(register) {
142
+ const application = await clientOf();
143
+ const { transport } = application;
144
+ await transport.requireFeature(REQUIRED_FEATURE, register.signal);
145
+ if (register.saveDefinitions ?? true) {
146
+ const embedded = embeddedIn(agents);
147
+ for (const agent of agents.values())
148
+ if (!embedded.has(agent.id))
149
+ await application.saveAgent(agent, { implementationVersion: version });
150
+ }
151
+ const endpoints = [...agents.values()].map((agent) => {
152
+ const manifestHash = flowManifestHash(agent);
153
+ return {
154
+ agentId: agent.id,
155
+ url: register.url,
156
+ implementationVersion: version,
157
+ ...(manifestHash === undefined ? {} : { manifestHash }),
158
+ ...(register.timeoutMs === undefined ? {} : { timeoutMs: register.timeoutMs }),
159
+ ...(register.maxConcurrent === undefined
160
+ ? {}
161
+ : { maxConcurrent: register.maxConcurrent }),
162
+ };
163
+ });
164
+ await transport.json("/v1/endpoints", "PUT", PutEndpointsRequestSchema.parse({ endpoints }), register.signal);
165
+ audience = register.url;
166
+ const answers = [];
167
+ for (const agent of agents.values())
168
+ answers.push(EndpointPingResponseSchema.parse(await transport.json(`/v1/endpoints/${segment(agent.id)}/ping`, "POST", {}, register.signal)));
169
+ return answers;
170
+ },
171
+ };
172
+ }
173
+ /** How long a background tool waits before its first heartbeat, and after a failed one. */
174
+ const HEARTBEAT_RETRY_MS = 1_000;
175
+ /**
176
+ * A background tool (`ToolDefinition.background`): runs after the `202` answer, heartbeats on
177
+ * the deadline the Runtime returns, stops when a heartbeat answers `409` (cancelled, lost or
178
+ * sent again), and posts its outcome. Every callback uses the newest delivery token.
179
+ */
180
+ async function runInBackground(input) {
181
+ const { action, report } = input;
182
+ let current = new Transport({ ...input.runtime, key: input.token });
183
+ const live = {
184
+ json: (...args) => current.json(...args),
185
+ };
186
+ const path = `/v1/actions/${segment(action.actionId)}`;
187
+ const stop = new AbortController();
188
+ const done = new AbortController();
189
+ const heartbeats = (async () => {
190
+ let wait = HEARTBEAT_RETRY_MS;
191
+ while (!done.signal.aborted && !stop.signal.aborted) {
192
+ try {
193
+ await delay(wait, done.signal);
194
+ }
195
+ catch {
196
+ return;
197
+ }
198
+ try {
199
+ const renewed = DeliveryHeartbeatResponseSchema.parse(await current.json(`${path}/heartbeat`, "POST", undefined, done.signal));
200
+ current = current.withKey(renewed.token);
201
+ wait = Math.max(HEARTBEAT_RETRY_MS, Math.floor((Date.parse(renewed.deadlineAt) - Date.now()) / 3));
202
+ }
203
+ catch (error) {
204
+ if (done.signal.aborted)
205
+ return;
206
+ if (error instanceof RuntimeError && error.status === 409) {
207
+ stop.abort(error);
208
+ return;
209
+ }
210
+ report(error);
211
+ wait = HEARTBEAT_RETRY_MS;
212
+ }
213
+ }
214
+ })();
215
+ let outcome;
216
+ try {
217
+ outcome = await executeAction(action, input.agent, stop.signal, {
218
+ ...(input.sandbox
219
+ ? { sandbox: createActionSandbox({ transport: live, actionId: action.actionId, signal: stop.signal }) }
220
+ : {}),
221
+ });
222
+ }
223
+ catch (error) {
224
+ // Not served here: say so rather than leave the Runtime waiting for the deadline.
225
+ report(error);
226
+ outcome = {
227
+ value: {
228
+ kind: "failed",
229
+ code: "action_not_served",
230
+ message: error instanceof Error ? error.message : String(error),
231
+ },
232
+ };
233
+ }
234
+ finally {
235
+ done.abort();
236
+ }
237
+ await heartbeats;
238
+ if (stop.signal.aborted)
239
+ return;
240
+ for (let attempt = 0;; attempt++) {
241
+ try {
242
+ await current.json(`${path}/result`, "POST", outcome);
243
+ return;
244
+ }
245
+ catch (error) {
246
+ if (attempt >= 4 ||
247
+ (error instanceof RuntimeError && error.status < 500 && error.status !== 429)) {
248
+ report(error);
249
+ return;
250
+ }
251
+ await delay(Math.min(4_000, 250 * 2 ** attempt), new AbortController().signal);
252
+ }
253
+ }
254
+ }
255
+ async function resolveVerification(options, clientOf) {
256
+ if (options.runtime) {
257
+ const { url, tenant, fetch } = options.runtime;
258
+ return {
259
+ tenant,
260
+ url,
261
+ ...(fetch ? { fetch } : {}),
262
+ keys: new JwksCache(options.jwks ?? { url, tenant, ...(fetch ? { fetch } : {}) }),
263
+ };
264
+ }
265
+ const { transport } = await clientOf();
266
+ return {
267
+ tenant: transport.tenant,
268
+ url: transport.url,
269
+ fetch: transport.fetcher,
270
+ keys: new JwksCache(options.jwks ?? {
271
+ url: transport.url,
272
+ tenant: transport.tenant,
273
+ key: transport.key,
274
+ fetch: transport.fetcher,
275
+ }),
276
+ };
277
+ }
278
+ function notServed(agentId) {
279
+ return answer(404, "agent_not_served", `This endpoint does not serve '${agentId}'`);
280
+ }
281
+ function answer(status, code, message) {
282
+ return Response.json({ code, message }, { status });
283
+ }
@@ -1,9 +1,19 @@
1
- import type { BuiltWorkflow } from "@nylorun/core/define";
1
+ /**
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.
10
+ */
2
11
  import { type SubjectScope } from "@nylorun/core/contracts";
12
+ import type { BuiltWorkflow } from "@nylorun/core/define";
3
13
  import { type AgentSource, type AgentsClient } from "../client.js";
4
14
  /** Per-session parameters the host may add, such as the person's vault for MCP servers. */
5
15
  export interface AgUiSessionOptions {
6
- /** Part of the session's identity: keep it stable for a subject and agent, or runs get 409. */
16
+ /** Set when the thread's session is created, on its first run; later runs keep them. */
7
17
  info?: Record<string, unknown>;
8
18
  vaultIds?: readonly string[];
9
19
  credentialSelections?: Parameters<AgentsClient["createSession"]>[0]["credentialSelections"];
@@ -37,6 +47,4 @@ export interface AgUiHandler {
37
47
  /** `POST`: cancels the thread's running turn. */
38
48
  cancel(request: Request, agentId: string, threadId: string): Promise<Response>;
39
49
  }
40
- /** A session per subject, agent and thread; one subject can never name another's session. */
41
- export declare function sessionIdFor(subject: string, agentId: string, threadId: string): string;
42
50
  export declare function createAgUiHandler(options: AgUiHandlerOptions): AgUiHandler;