@aexhq/sdk 0.62.1 → 0.63.0

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/dist/session.js CHANGED
@@ -1,658 +1,42 @@
1
- import { CustomerEnvironment, prepareComponents, } from "@aexhq/brain";
2
- import * as z from "zod";
3
- import { AbortError, OutputRefusalError, OutputSchemaError, OutputValidationError, SessionError, abortError, errorFromApi, } from "./errors.js";
4
- import { canonicalize, jcsSha256, randomIdempotencyKey } from "./json.js";
5
- import { compileCallbacks } from "./tools.js";
6
- import { SessionChildren, SessionSandbox, SessionStorage } from "./resources.js";
7
1
  export class Sessions {
8
- #transport;
9
- #webSocketFactory;
10
- #customerEnvironments = new Map();
11
- #customerEnvironmentInstances = new Map();
12
- #customerEnvironmentReadinessWaiters = new Map();
13
- #closed = false;
14
- constructor(transport, webSocketFactory) {
15
- this.#transport = transport;
16
- this.#webSocketFactory = webSocketFactory;
2
+ brain;
3
+ constructor(brain) {
4
+ this.brain = brain;
17
5
  }
18
- /** @internal Called by `Aex.close()`. */
19
- close() {
20
- if (this.#closed)
21
- return;
22
- this.#closed = true;
23
- for (const hand of this.#customerEnvironmentInstances.values())
24
- hand.close();
25
- this.#customerEnvironmentInstances.clear();
26
- this.#customerEnvironments.clear();
6
+ async create(request, options = {}) {
7
+ return new Session(await this.brain.createSession(request, key(options)));
27
8
  }
28
- async create(options, request = {}) {
29
- if (this.#closed)
30
- throw new SessionError("Aex client is closed");
31
- if (options.model?.component === undefined) {
32
- throw new TypeError("sessions.create requires an imported Model component");
33
- }
34
- if (options.agentloop === undefined) {
35
- throw new TypeError("sessions.create requires an imported Agentloop component");
36
- }
37
- const model = options.model.component;
38
- const agentloop = options.agentloop;
39
- const tools = [...(options.tools ?? [])];
40
- const componentTools = tools.filter(isComponentTool);
41
- const callbackTools = tools.filter((tool) => !isComponentTool(tool));
42
- const environments = Object.entries(options.environments ?? {});
43
- if (environments.some(([, environment]) => !isEnvironmentComponent(environment))) {
44
- throw new TypeError("component sessions accept only precompiled Environment component values");
45
- }
46
- const environmentComponents = environments.map(([name, environment]) => {
47
- if (!isEnvironmentComponent(environment)) {
48
- throw new TypeError(`Environment ${JSON.stringify(name)} is not a component`);
49
- }
50
- return [name, environment];
51
- });
52
- const applicationEnvironments = environmentComponents.filter(([, environment]) => isApplicationEnvironment(environment));
53
- if (callbackTools.length > 0 && applicationEnvironments.length !== 1) {
54
- throw new TypeError("application callback Tools require exactly one app() Environment");
55
- }
56
- const callbacks = await compileCallbacks(callbackTools);
57
- const allComponentTools = [...componentTools, ...callbacks.components];
58
- const toolDefinitions = allComponentTools.map(componentToolDefinition);
59
- const names = new Set();
60
- for (const definition of toolDefinitions) {
61
- if (names.has(definition.name)) {
62
- throw new TypeError(`Tool ${JSON.stringify(definition.name)} was selected twice`);
63
- }
64
- names.add(definition.name);
65
- }
66
- const callbackEnvironment = applicationEnvironments[0];
67
- if (callbackEnvironment !== undefined) {
68
- await this.#ensureCustomerEnvironment(applicationEnvironmentId(callbackEnvironment[1]), callbacks.registrations, request.signal);
69
- }
70
- const prepared = await prepareComponents([
71
- model,
72
- agentloop,
73
- ...allComponentTools,
74
- ...environmentComponents.map(([, environment]) => environment),
75
- ]);
76
- const modelBinding = prepared.bindings[0];
77
- const agentloopBinding = prepared.bindings[1];
78
- if (modelBinding === undefined || agentloopBinding === undefined) {
79
- throw new TypeError("component session bindings are incomplete");
80
- }
81
- const toolBindings = prepared.bindings.slice(2, 2 + allComponentTools.length);
82
- const environmentBindings = prepared.bindings.slice(2 + allComponentTools.length);
83
- const toolItems = allComponentTools.map((tool, index) => {
84
- const binding = toolBindings[index];
85
- if (binding === undefined)
86
- throw new TypeError("Tool component binding is missing");
87
- const definition = toolDefinitions[index];
88
- if (definition === undefined)
89
- throw new TypeError("Tool component definition is missing");
90
- const needsEnvironment = binding.grants.includes("environment");
91
- const isCallback = index >= componentTools.length;
92
- if (needsEnvironment && !isCallback && environmentComponents.length !== 1) {
93
- throw new TypeError("a Tool with the environment grant requires exactly one declared Environment");
94
- }
95
- const environmentName = isCallback
96
- ? callbackEnvironment?.[0]
97
- : environmentComponents[0]?.[0];
98
- return {
99
- definition,
100
- executor: {
101
- kind: "component",
102
- component_digest: binding.component_digest,
103
- world: binding.world,
104
- config: binding.config,
105
- grants: binding.grants,
106
- ...(needsEnvironment ? { environment: environmentName } : {}),
107
- ...(binding.bundle_digest === undefined ? {} : { bundle_digest: binding.bundle_digest }),
108
- },
109
- };
110
- });
111
- const environmentConfig = Object.fromEntries(environmentComponents.map(([name], index) => {
112
- const binding = environmentBindings[index];
113
- if (binding === undefined) {
114
- throw new TypeError(`Environment component binding ${JSON.stringify(name)} is missing`);
115
- }
116
- return [name, {
117
- component_digest: binding.component_digest,
118
- world: binding.world,
119
- config: binding.config,
120
- }];
121
- }));
122
- const body = {
123
- model: {
124
- component_digest: modelBinding.component_digest,
125
- world: modelBinding.world,
126
- config: modelBinding.config,
127
- provider: options.model.provider,
128
- name: options.model.name,
129
- api_key: options.model.apiKey,
130
- ...(options.model.baseUrl === undefined ? {} : { base_url: options.model.baseUrl }),
131
- ...(options.model.maxOutputTokens === undefined
132
- ? {}
133
- : { max_output_tokens: options.model.maxOutputTokens }),
134
- },
135
- agentloop: {
136
- component_digest: agentloopBinding.component_digest,
137
- world: agentloopBinding.world,
138
- config: agentloopBinding.config,
139
- },
140
- component_artifacts: prepared.artifacts,
141
- tools: { items: toolItems },
142
- ...(prepared.toolArtifactLayers.length === 0
143
- ? {}
144
- : { tool_artifact_layers: prepared.toolArtifactLayers }),
145
- ...(environmentComponents.length === 0 ? {} : { environments: environmentConfig }),
146
- ...(options.secrets === undefined ? {} : { secrets: options.secrets }),
147
- ...(options.metadata === undefined ? {} : { metadata: options.metadata }),
148
- ...(options.retainUntil === undefined
149
- ? {}
150
- : { retain_until: normalizeTimestamp(options.retainUntil, "retainUntil") }),
151
- ...(options.network === undefined ? {} : { network: options.network }),
152
- ...(options.providerRecoveryRetries === undefined
153
- ? {}
154
- : { provider_recovery_retries: options.providerRecoveryRetries }),
155
- ...(options.children === undefined
156
- ? {}
157
- : {
158
- children: {
159
- ...(options.children.maxDepth === undefined
160
- ? {}
161
- : { max_depth: options.children.maxDepth }),
162
- ...(options.children.maxDirectChildren === undefined
163
- ? {}
164
- : { max_direct_children: options.children.maxDirectChildren }),
165
- ...(options.children.maxDescendants === undefined
166
- ? {}
167
- : { max_descendants: options.children.maxDescendants }),
168
- },
169
- }),
170
- };
171
- const data = await this.#transport.json("POST", "/v1/sessions", {
172
- body,
173
- headers: { "Idempotency-Key": request.idempotencyKey ?? randomIdempotencyKey() },
174
- signal: request.signal,
175
- retry: true,
176
- });
177
- return new Session(this.#transport, data);
9
+ async list() {
10
+ const { sessions } = await this.brain.listSessions();
11
+ return Promise.all(sessions.map(async (state) => new Session(await this.brain.getSession(state.session_id))));
178
12
  }
179
- async get(id, options = {}) {
180
- const data = await this.#transport.json("GET", `/v1/sessions/${encodeURIComponent(id)}`, { signal: options.signal });
181
- return new Session(this.#transport, data);
13
+ async get(sessionId) {
14
+ return new Session(await this.brain.getSession(sessionId));
182
15
  }
183
- async list(options = {}) {
184
- const query = new URLSearchParams();
185
- if (options.limit !== undefined)
186
- query.set("limit", String(options.limit));
187
- if (options.cursor !== undefined)
188
- query.set("cursor", options.cursor);
189
- if (options.state !== undefined)
190
- query.set("state", options.state);
191
- const suffix = query.size === 0 ? "" : `?${query}`;
192
- const list = await this.#transport.json("GET", `/v1/sessions${suffix}`, {
193
- signal: options.signal,
194
- });
195
- return {
196
- data: list.data.map((data) => new Session(this.#transport, data)),
197
- hasMore: list.has_more,
198
- ...(list.next_cursor === undefined ? {} : { nextCursor: list.next_cursor }),
199
- };
200
- }
201
- async #ensureCustomerEnvironment(clientId, registrations, signal) {
202
- if (this.#closed)
203
- throw new SessionError("Aex client is closed");
204
- if (registrations.length === 0)
205
- return;
206
- if (clientId === undefined)
207
- throw new TypeError("Callback Tools require an app() environment");
208
- if (this.#webSocketFactory === undefined) {
209
- throw new TypeError("This runtime does not provide WebSocket; pass webSocketFactory to Aex");
210
- }
211
- const existing = this.#customerEnvironments.get(clientId);
212
- if (existing === undefined) {
213
- let partial;
214
- const starting = (async () => {
215
- try {
216
- partial = new CustomerEnvironment(async () => {
217
- const grant = await this.#transport.customerEnvironmentGrant(clientId);
218
- return {
219
- request: { url: grant.url, protocol: grant.protocol },
220
- observe: (observation) => this.#transport.customerEnvironmentObserve(grant.observationUrl, grant.observationToken, observation),
221
- };
222
- }, registrations, this.#webSocketFactory, { clientId });
223
- this.#customerEnvironmentInstances.set(clientId, partial);
224
- await partial.ready;
225
- if (this.#closed) {
226
- partial.close();
227
- throw new SessionError("Aex client is closed");
228
- }
229
- return partial;
230
- }
231
- catch (error) {
232
- partial?.close();
233
- throw error;
234
- }
235
- })();
236
- this.#customerEnvironments.set(clientId, starting);
237
- void starting.catch(() => {
238
- if (this.#customerEnvironments.get(clientId) === starting) {
239
- this.#customerEnvironments.delete(clientId);
240
- if (this.#customerEnvironmentInstances.get(clientId) === partial) {
241
- this.#customerEnvironmentInstances.delete(clientId);
242
- }
243
- }
244
- });
245
- await this.#waitForCustomerEnvironmentReadiness(clientId, starting, signal);
246
- return;
247
- }
248
- const hand = await this.#waitForCustomerEnvironmentReadiness(clientId, existing, signal);
249
- if (this.#closed)
250
- throw new SessionError("Aex client is closed");
251
- await waitWithSignal(hand.register(registrations), signal);
252
- }
253
- async #waitForCustomerEnvironmentReadiness(clientId, starting, signal) {
254
- this.#customerEnvironmentReadinessWaiters.set(clientId, (this.#customerEnvironmentReadinessWaiters.get(clientId) ?? 0) + 1);
255
- let settled = false;
256
- void starting.then(() => { settled = true; }, () => { settled = true; });
257
- try {
258
- return await waitWithSignal(starting, signal);
259
- }
260
- catch (error) {
261
- if (signal?.aborted && !settled
262
- && this.#customerEnvironmentReadinessWaiters.get(clientId) === 1
263
- && this.#customerEnvironments.get(clientId) === starting) {
264
- const partial = this.#customerEnvironmentInstances.get(clientId);
265
- partial?.close();
266
- this.#customerEnvironments.delete(clientId);
267
- if (this.#customerEnvironmentInstances.get(clientId) === partial) {
268
- this.#customerEnvironmentInstances.delete(clientId);
269
- }
270
- }
271
- throw error;
272
- }
273
- finally {
274
- const waiters = this.#customerEnvironmentReadinessWaiters.get(clientId) ?? 1;
275
- if (waiters <= 1)
276
- this.#customerEnvironmentReadinessWaiters.delete(clientId);
277
- else
278
- this.#customerEnvironmentReadinessWaiters.set(clientId, waiters - 1);
279
- }
280
- }
281
- }
282
- function isApplicationEnvironment(value) {
283
- const config = value.config;
284
- return config !== null && typeof config === "object" && !Array.isArray(config) &&
285
- config.driver === "customer";
286
- }
287
- function applicationEnvironmentId(value) {
288
- const config = value.config;
289
- const id = config.configuration?.registration;
290
- if (typeof id !== "string" || !/^[A-Za-z0-9_.:-]{1,128}$/u.test(id)) {
291
- throw new TypeError("app() Environment registration is invalid");
292
- }
293
- return id;
294
- }
295
- function isComponentTool(value) {
296
- return value !== null
297
- && typeof value === "object"
298
- && value.kind === "brain.component"
299
- && value.extension === "tool";
300
- }
301
- function isEnvironmentComponent(value) {
302
- return value !== null
303
- && typeof value === "object"
304
- && value.kind === "brain.component"
305
- && value.extension === "environment";
306
- }
307
- function componentToolDefinition(value) {
308
- const config = value.config;
309
- if (config === null || typeof config !== "object" || Array.isArray(config)) {
310
- throw new TypeError("Tool component config must be an object containing definition");
311
- }
312
- const definition = config.definition;
313
- if (definition === null || typeof definition !== "object" || Array.isArray(definition)) {
314
- throw new TypeError("Tool component config.definition is required");
315
- }
316
- return definition;
317
- }
318
- function waitWithSignal(promise, signal) {
319
- if (signal === undefined)
320
- return promise;
321
- if (signal.aborted)
322
- return Promise.reject(abortError(signal.reason));
323
- return new Promise((resolve, reject) => {
324
- const cleanup = () => signal.removeEventListener("abort", onAbort);
325
- const onAbort = () => {
326
- cleanup();
327
- reject(abortError(signal.reason));
328
- };
329
- signal.addEventListener("abort", onAbort, { once: true });
330
- promise.then((value) => { cleanup(); resolve(value); }, (error) => { cleanup(); reject(error); });
331
- });
332
16
  }
333
17
  export class Session {
334
- #transport;
335
- #data;
336
- storage;
337
- children;
338
- constructor(transport, data) {
339
- this.#transport = transport;
340
- this.#data = data;
341
- this.storage = new SessionStorage(transport, data.id);
342
- this.children = new SessionChildren(transport, data.id);
343
- }
344
- /** The Environment names this session sealed. Only these can be addressed. */
345
- get environments() {
346
- return this.#data.environments;
347
- }
348
- /** One sealed Environment addressed by the name the session declared it under. */
349
- environment(name) {
350
- if (!this.#data.environments.includes(name)) {
351
- throw new TypeError(`Session ${this.#data.id} did not declare Environment ${JSON.stringify(name)}`);
352
- }
353
- return new SessionSandbox(this.#transport, this.#data.id, name);
354
- }
355
- /** The session's default sandbox: the one Environment it declared. */
356
- get sandbox() {
357
- const [only, ...rest] = this.#data.environments;
358
- if (only === undefined || rest.length > 0) {
359
- throw new TypeError(`Session ${this.#data.id} has no default sandbox because it declared ${this.#data.environments.length} Environments; use session.environment(name)`);
360
- }
361
- return this.environment(only);
362
- }
363
- get id() {
364
- return this.#data.id;
365
- }
366
- get state() {
367
- return this.#data.state;
368
- }
369
- get turnState() {
370
- return this.#data.turn_state;
371
- }
372
- get parentId() {
373
- return this.#data.parent_id;
374
- }
375
- get rootId() {
376
- return this.#data.root_id;
377
- }
378
- get depth() {
379
- return this.#data.depth;
18
+ handle;
19
+ constructor(handle) {
20
+ this.handle = handle;
380
21
  }
381
- get model() {
382
- return {
383
- provider: this.#data.model.provider,
384
- name: this.#data.model.name,
385
- ...(this.#data.model.base_url === undefined ? {} : { baseUrl: this.#data.model.base_url }),
386
- contextWindowTokens: this.#data.model.context_window_tokens,
387
- };
22
+ get state() { return this.handle.state; }
23
+ get id() { return this.handle.id; }
24
+ async send(content, options = {}) {
25
+ return this.handle.send(content, key(options));
388
26
  }
389
- get createdAt() {
390
- return this.#data.created_at;
391
- }
392
- get updatedAt() {
393
- return this.#data.updated_at;
394
- }
395
- get retainUntil() {
396
- return this.#data.retain_until;
397
- }
398
- get metadata() {
399
- return this.#data.metadata;
400
- }
401
- async refresh(options = {}) {
402
- this.#data = await this.#transport.json("GET", `/v1/sessions/${encodeURIComponent(this.id)}`, { signal: options.signal });
403
- return this;
404
- }
405
- async send(input, options = {}) {
406
- const outputOptions = isOutputOptions(options) ? options : undefined;
407
- const compiled = outputOptions === undefined
408
- ? undefined
409
- : await compileOutputSchema(outputOptions.output, outputOptions.outputRetries);
410
- const accepted = await this.#transport.json("POST", `/v1/sessions/${encodeURIComponent(this.id)}/messages`, {
411
- body: {
412
- content: input,
413
- ...(options.metadata === undefined ? {} : { metadata: options.metadata }),
414
- ...(compiled === undefined
415
- ? {}
416
- : {
417
- output: {
418
- schema: compiled.jsonSchema,
419
- schema_hash: compiled.schemaHash,
420
- ...(compiled.retries === undefined ? {} : { retries: compiled.retries }),
421
- },
422
- }),
423
- },
424
- headers: { "Idempotency-Key": options.idempotencyKey ?? randomIdempotencyKey() },
425
- signal: options.signal,
426
- retry: true,
427
- });
428
- if (compiled !== undefined) {
429
- if (accepted.session_id !== this.id ||
430
- accepted.output_id === undefined ||
431
- accepted.schema_hash !== compiled.schemaHash) {
432
- throw new SessionError("Aex returned an inconsistent typed-output admission");
433
- }
434
- }
435
- let answer = "";
436
- try {
437
- for await (const event of this.events({ after: Math.max(0, accepted.seq - 1), signal: options.signal })) {
438
- if (event.type === "assistant.message" && event.turn_id === accepted.turn_id && event.agent_id === "root") {
439
- answer = event.text;
440
- }
441
- else if (event.type === "turn.failed" && event.turn_id === accepted.turn_id) {
442
- throw errorFromApi(event.error, undefined, outputIssues(event.error));
443
- }
444
- else if (event.type === "turn.completed" && event.turn_id === accepted.turn_id) {
445
- this.markIdle();
446
- if (event.stop_reason === "cancelled")
447
- throw new AbortError();
448
- if (compiled !== undefined && outputOptions !== undefined) {
449
- if (event.result === undefined) {
450
- if (event.stop_reason === "refusal") {
451
- throw new OutputRefusalError("The model refused the structured-output request");
452
- }
453
- throw new OutputValidationError("The model ended the turn without submitting the requested structured output", [{
454
- path: "",
455
- message: "The model did not call aex_submit_output",
456
- keyword: "missing_output",
457
- }]);
458
- }
459
- if (event.result.name !== "aex_submit_output" ||
460
- event.result.metadata?.output_id !== accepted.output_id ||
461
- event.result.metadata?.schema_hash !== compiled.schemaHash) {
462
- throw new SessionError("Aex returned a typed result for a different request");
463
- }
464
- const parsed = await outputOptions.output.safeParseAsync(event.result.value);
465
- if (!parsed.success) {
466
- throw new OutputValidationError("The output passed the wire schema but failed the original Zod schema", parsed.error.issues.map((issue) => ({
467
- path: jsonPointer(issue.path),
468
- message: issue.message,
469
- keyword: issue.code,
470
- })));
471
- }
472
- return parsed.data;
473
- }
474
- return answer;
475
- }
476
- }
477
- }
478
- catch (error) {
479
- if (options.signal?.aborted === true) {
480
- await this.cancel().catch(() => undefined);
481
- throw abortError(error);
482
- }
483
- throw error;
484
- }
485
- throw new SessionError("The Aex event stream ended before the session finished its work");
486
- }
487
- /**
488
- * Raw, attempt-aware event stream. Provisional frames have no durable cursor and may later be
489
- * superseded; consumers rendering them must key by `attempt_id` and process
490
- * `model.attempt_superseded`. Use `send()` when only the durable winning answer is needed.
491
- */
492
- events(options = {}) {
493
- return this.#transport.events(this.id, options);
27
+ events(after = 0) {
28
+ return this.handle.events(after);
494
29
  }
495
30
  async cancel(options = {}) {
496
- this.#data = await this.#transport.json("POST", `/v1/sessions/${encodeURIComponent(this.id)}/cancel`, { signal: options.signal, retry: true });
497
- return this;
498
- }
499
- async suspend(options = {}) {
500
- this.#data = await this.#transport.json("POST", `/v1/sessions/${encodeURIComponent(this.id)}/suspend`, { signal: options.signal, retry: true });
501
- return this;
502
- }
503
- async resume(options = {}) {
504
- this.#data = await this.#transport.json("POST", `/v1/sessions/${encodeURIComponent(this.id)}/resume`, { signal: options.signal, retry: true });
505
- return this;
506
- }
507
- async setRetention(value, options = {}) {
508
- this.#data = await this.#transport.json("POST", `/v1/sessions/${encodeURIComponent(this.id)}/retention`, {
509
- body: {
510
- retain_until: normalizeTimestamp(value, "retainUntil"),
511
- allow_shorten: options.allowShorten ?? false,
512
- },
513
- signal: options.signal,
514
- retry: true,
515
- });
516
- return this;
31
+ await this.handle.cancel(key(options));
517
32
  }
518
33
  async end(options = {}) {
519
- this.#data = await this.#transport.json("POST", `/v1/sessions/${encodeURIComponent(this.id)}/end`, { signal: options.signal, retry: true });
520
- return this;
34
+ return this.handle.end(key(options));
521
35
  }
522
36
  async delete(options = {}) {
523
- await this.#transport.deleteSession(this.id, options.queue !== true, options.signal);
524
- this.#data = {
525
- ...this.#data,
526
- state: options.queue === true ? "deleting" : "deleted",
527
- };
37
+ await this.handle.delete(key(options));
528
38
  }
529
- markIdle() {
530
- this.#data = { ...this.#data, turn_state: "idle" };
531
- }
532
- }
533
- function isOutputOptions(options) {
534
- return "output" in options;
535
- }
536
- function normalizeTimestamp(value, field) {
537
- const parsed = value instanceof Date ? value : new Date(value);
538
- if (!Number.isFinite(parsed.getTime()))
539
- throw new TypeError(`${field} must be a valid timestamp`);
540
- return parsed.toISOString();
541
- }
542
- async function compileOutputSchema(schema, retries) {
543
- let jsonSchema;
544
- try {
545
- assertPortableOutputSchema(schema);
546
- jsonSchema = z.toJSONSchema(schema, {
547
- target: "draft-2020-12",
548
- unrepresentable: "throw",
549
- });
550
- }
551
- catch (cause) {
552
- throw new OutputSchemaError(messageOf(cause, "The Zod schema cannot be represented as JSON Schema"), {
553
- cause,
554
- });
555
- }
556
- if (jsonSchema.type !== "object") {
557
- throw new OutputSchemaError("session.send() output requires a Zod object schema");
558
- }
559
- assertSupportedJsonSchema(jsonSchema);
560
- return { jsonSchema, schemaHash: await jcsSha256(jsonSchema), retries };
561
- }
562
- function assertSupportedJsonSchema(schema) {
563
- const bytes = new TextEncoder().encode(canonicalize(schema));
564
- if (bytes.byteLength > 64 * 1024) {
565
- throw new OutputSchemaError("The output schema exceeds the 65536-byte service limit");
566
- }
567
- let nodes = 0;
568
- const visit = (value, depth) => {
569
- if (depth > 64)
570
- throw new OutputSchemaError("The output schema exceeds the service depth limit");
571
- nodes += 1;
572
- if (nodes > 4096)
573
- throw new OutputSchemaError("The output schema exceeds the service node limit");
574
- if (Array.isArray(value)) {
575
- for (const child of value)
576
- visit(child, depth + 1);
577
- return;
578
- }
579
- if (value === null || typeof value !== "object")
580
- return;
581
- const object = value;
582
- if ("pattern" in object || "patternProperties" in object) {
583
- throw new OutputSchemaError("Regular-expression JSON Schema keywords are not supported for Aex structured output");
584
- }
585
- for (const keyword of ["$ref", "$dynamicRef", "$recursiveRef"]) {
586
- const reference = object[keyword];
587
- if (typeof reference === "string" && !reference.startsWith("#")) {
588
- throw new OutputSchemaError("Remote JSON Schema references are not supported for Aex structured output");
589
- }
590
- }
591
- for (const child of Object.values(object))
592
- visit(child, depth + 1);
593
- };
594
- visit(schema, 0);
595
- }
596
- function outputIssues(error) {
597
- const details = error.details;
598
- if (details === undefined || details === null || typeof details !== "object")
599
- return [];
600
- const issues = details.issues;
601
- if (!Array.isArray(issues))
602
- return [];
603
- return issues.flatMap((issue) => {
604
- if (issue === null || typeof issue !== "object")
605
- return [];
606
- const value = issue;
607
- if (typeof value.path !== "string" || typeof value.message !== "string")
608
- return [];
609
- return [{
610
- path: value.path,
611
- message: value.message,
612
- ...(typeof value.keyword === "string" ? { keyword: value.keyword } : {}),
613
- }];
614
- });
615
- }
616
- function jsonPointer(path) {
617
- if (path.length === 0)
618
- return "";
619
- return `/${path
620
- .map((part) => String(part).replaceAll("~", "~0").replaceAll("/", "~1"))
621
- .join("/")}`;
622
- }
623
- function messageOf(error, fallback) {
624
- return error instanceof Error && error.message !== "" ? error.message : fallback;
625
39
  }
626
- /**
627
- * A server cannot execute user-defined Zod functions. Reject them before admission instead of
628
- * committing a value that only the calling process can later discover was invalid.
629
- */
630
- function assertPortableOutputSchema(schema) {
631
- const seen = new WeakSet();
632
- const visit = (value) => {
633
- if (value === null || typeof value !== "object" || seen.has(value))
634
- return;
635
- seen.add(value);
636
- const internal = value;
637
- if (internal._zod?.def !== undefined) {
638
- const definition = internal._zod.def;
639
- const unsupported = definition.type === "custom" ||
640
- definition.type === "transform" ||
641
- definition.type === "file" ||
642
- definition.check === "overwrite";
643
- if (unsupported) {
644
- throw new OutputSchemaError(`This Zod schema contains ${String(definition.type ?? definition.check)} behavior that cannot be enforced by the Aex output service`);
645
- }
646
- visit(definition);
647
- return;
648
- }
649
- if (Array.isArray(value)) {
650
- for (const item of value)
651
- visit(item);
652
- return;
653
- }
654
- for (const child of Object.values(value))
655
- visit(child);
656
- };
657
- visit(schema);
40
+ function key(options) {
41
+ return options.idempotencyKey ?? crypto.randomUUID();
658
42
  }