@aexhq/sdk 0.56.0 → 0.57.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,17 +1,38 @@
1
+ import { CustomerHand } from "@aexhq/brain";
1
2
  import * as z from "zod";
2
3
  import { AbortError, OutputRefusalError, OutputSchemaError, OutputValidationError, SessionError, abortError, errorFromApi, } from "./errors.js";
3
4
  import { canonicalize, jcsSha256, randomIdempotencyKey } from "./json.js";
4
5
  import { compileTools } from "./tools.js";
6
+ import { SessionChildren, SessionSandbox, SessionStorage } from "./resources.js";
5
7
  export class Sessions {
6
8
  #transport;
7
- constructor(transport) {
9
+ #webSocketFactory;
10
+ #clientId;
11
+ #customerHand;
12
+ #customerHandInstance;
13
+ #closed = false;
14
+ constructor(transport, webSocketFactory, clientId) {
8
15
  this.#transport = transport;
16
+ this.#webSocketFactory = webSocketFactory;
17
+ this.#clientId = clientId;
18
+ }
19
+ /** @internal Called by `Aex.close()`. */
20
+ close() {
21
+ if (this.#closed)
22
+ return;
23
+ this.#closed = true;
24
+ this.#customerHandInstance?.close();
25
+ this.#customerHandInstance = undefined;
26
+ this.#customerHand = undefined;
9
27
  }
10
28
  async create(options, request = {}) {
29
+ if (this.#closed)
30
+ throw new SessionError("Aex client is closed");
11
31
  const compiledTools = await compileTools(options.tools);
12
- if (compiledTools.attached.size > 0) {
13
- throw new TypeError("Attached-process Tools are not available through this Aex SDK revision; use the default Hand execution mode");
32
+ if (options.client?.submitRetries !== undefined && this.#clientId === undefined) {
33
+ throw new TypeError("client.submitRetries requires Aex({ client: { id } })");
14
34
  }
35
+ await this.#ensureCustomerHand(compiledTools.clientRegistrations, request.signal);
15
36
  const body = {
16
37
  model: {
17
38
  provider: options.model.provider,
@@ -21,6 +42,9 @@ export class Sessions {
21
42
  ...(options.model.maxOutputTokens === undefined
22
43
  ? {}
23
44
  : { max_output_tokens: options.model.maxOutputTokens }),
45
+ ...(options.model.contextWindowTokens === undefined
46
+ ? {}
47
+ : { context_window_tokens: options.model.contextWindowTokens }),
24
48
  ...(options.model.temperature === undefined ? {} : { temperature: options.model.temperature }),
25
49
  ...(options.model.reasoningEffort === undefined
26
50
  ? {}
@@ -28,31 +52,42 @@ export class Sessions {
28
52
  },
29
53
  tools: {
30
54
  items: compiledTools.items,
31
- ...(options.mcp === undefined
32
- ? {}
33
- : {
34
- mcp: options.mcp.map((server) => ({
35
- name: server.name,
36
- url: server.url,
37
- ...(server.headers === undefined ? {} : { headers: server.headers }),
38
- ...(server.protocol === undefined ? {} : { protocol: server.protocol }),
39
- ...(server.allowedTools === undefined
40
- ? {}
41
- : { allowed_tools: [...server.allowedTools] }),
42
- })),
43
- }),
44
55
  },
45
56
  ...(compiledTools.bundles.length === 0 ? {} : { tool_bundles: compiledTools.bundles }),
46
- ...(options.hand === undefined
57
+ ...(options.secrets === undefined ? {} : { secrets: options.secrets }),
58
+ ...(options.systemPrompt === undefined ? {} : { system_prompt: options.systemPrompt }),
59
+ ...(options.metadata === undefined ? {} : { metadata: options.metadata }),
60
+ ...(options.network === undefined
61
+ ? {}
62
+ : { network: options.network }),
63
+ ...(options.providerRecoveryRetries === undefined
64
+ ? {}
65
+ : { provider_recovery_retries: options.providerRecoveryRetries }),
66
+ ...(this.#clientId === undefined
47
67
  ? {}
48
68
  : {
49
- hand: {
50
- ...(options.hand.enabled === undefined ? {} : { enabled: options.hand.enabled }),
51
- ...(options.hand.env === undefined ? {} : { env: options.hand.env }),
69
+ client: {
70
+ id: this.#clientId,
71
+ ...(options.client?.submitRetries === undefined
72
+ ? {}
73
+ : { submit_retries: options.client.submitRetries }),
74
+ },
75
+ }),
76
+ ...(options.children === undefined
77
+ ? {}
78
+ : {
79
+ children: {
80
+ ...(options.children.maxDepth === undefined
81
+ ? {}
82
+ : { max_depth: options.children.maxDepth }),
83
+ ...(options.children.maxDirectChildren === undefined
84
+ ? {}
85
+ : { max_direct_children: options.children.maxDirectChildren }),
86
+ ...(options.children.maxDescendants === undefined
87
+ ? {}
88
+ : { max_descendants: options.children.maxDescendants }),
52
89
  },
53
90
  }),
54
- ...(options.systemPrompt === undefined ? {} : { system_prompt: options.systemPrompt }),
55
- ...(options.metadata === undefined ? {} : { metadata: options.metadata }),
56
91
  };
57
92
  const data = await this.#transport.json("POST", "/v1/sessions", {
58
93
  body,
@@ -84,13 +119,87 @@ export class Sessions {
84
119
  ...(list.next_cursor === undefined ? {} : { nextCursor: list.next_cursor }),
85
120
  };
86
121
  }
122
+ async #ensureCustomerHand(registrations, signal) {
123
+ if (this.#closed)
124
+ throw new SessionError("Aex client is closed");
125
+ if (registrations.length === 0)
126
+ return;
127
+ if (this.#clientId === undefined) {
128
+ throw new TypeError("Customer-app Tools require Aex({ client: { id } })");
129
+ }
130
+ if (this.#webSocketFactory === undefined) {
131
+ throw new TypeError("This runtime does not provide WebSocket; pass webSocketFactory to Aex");
132
+ }
133
+ if (this.#customerHand === undefined) {
134
+ let partial;
135
+ const starting = (async () => {
136
+ try {
137
+ partial = new CustomerHand(async () => {
138
+ const grant = await this.#transport.customerHandGrant(this.#clientId);
139
+ return {
140
+ request: { url: grant.url, protocol: grant.protocol },
141
+ observe: (observation) => this.#transport.customerHandObserve(grant.observationUrl, grant.observationToken, observation),
142
+ };
143
+ }, registrations, this.#webSocketFactory, { clientId: this.#clientId });
144
+ this.#customerHandInstance = partial;
145
+ await partial.ready;
146
+ if (this.#closed) {
147
+ partial.close();
148
+ throw new SessionError("Aex client is closed");
149
+ }
150
+ return partial;
151
+ }
152
+ catch (error) {
153
+ partial?.close();
154
+ throw error;
155
+ }
156
+ })();
157
+ this.#customerHand = starting;
158
+ void starting.catch(() => {
159
+ if (this.#customerHand === starting) {
160
+ this.#customerHand = undefined;
161
+ if (this.#customerHandInstance === partial)
162
+ this.#customerHandInstance = undefined;
163
+ }
164
+ });
165
+ // The request may stop waiting, but the process-scoped runner remains reconnectable for
166
+ // later sessions. Its grant/reconnect lifetime must never inherit one create signal.
167
+ await waitWithSignal(starting, signal);
168
+ return;
169
+ }
170
+ const hand = await waitWithSignal(this.#customerHand, signal);
171
+ if (this.#closed)
172
+ throw new SessionError("Aex client is closed");
173
+ await waitWithSignal(hand.register(registrations), signal);
174
+ }
175
+ }
176
+ function waitWithSignal(promise, signal) {
177
+ if (signal === undefined)
178
+ return promise;
179
+ if (signal.aborted)
180
+ return Promise.reject(abortError(signal.reason));
181
+ return new Promise((resolve, reject) => {
182
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
183
+ const onAbort = () => {
184
+ cleanup();
185
+ reject(abortError(signal.reason));
186
+ };
187
+ signal.addEventListener("abort", onAbort, { once: true });
188
+ promise.then((value) => { cleanup(); resolve(value); }, (error) => { cleanup(); reject(error); });
189
+ });
87
190
  }
88
191
  export class Session {
89
192
  #transport;
90
193
  #data;
194
+ sandbox;
195
+ storage;
196
+ children;
91
197
  constructor(transport, data) {
92
198
  this.#transport = transport;
93
199
  this.#data = data;
200
+ this.sandbox = new SessionSandbox(transport, data.id);
201
+ this.storage = new SessionStorage(transport, data.id);
202
+ this.children = new SessionChildren(transport, data.id);
94
203
  }
95
204
  get id() {
96
205
  return this.#data.id;
@@ -98,11 +207,24 @@ export class Session {
98
207
  get state() {
99
208
  return this.#data.state;
100
209
  }
210
+ get turnState() {
211
+ return this.#data.turn_state;
212
+ }
213
+ get parentId() {
214
+ return this.#data.parent_id;
215
+ }
216
+ get rootId() {
217
+ return this.#data.root_id;
218
+ }
219
+ get depth() {
220
+ return this.#data.depth;
221
+ }
101
222
  get model() {
102
223
  return {
103
224
  provider: this.#data.model.provider,
104
225
  name: this.#data.model.name,
105
226
  ...(this.#data.model.base_url === undefined ? {} : { baseUrl: this.#data.model.base_url }),
227
+ contextWindowTokens: this.#data.model.context_window_tokens,
106
228
  };
107
229
  }
108
230
  get createdAt() {
@@ -200,21 +322,31 @@ export class Session {
200
322
  }
201
323
  throw new SessionError("The Aex event stream ended before the session finished its work");
202
324
  }
325
+ /**
326
+ * Raw, attempt-aware event stream. Provisional frames have no durable cursor and may later be
327
+ * superseded; consumers rendering them must key by `attempt_id` and process
328
+ * `model.attempt_superseded`. Use `send()` when only the durable winning answer is needed.
329
+ */
203
330
  events(options = {}) {
204
331
  return this.#transport.events(this.id, options);
205
332
  }
206
333
  async cancel(options = {}) {
207
- this.#data = await this.#transport.json("POST", `/v1/sessions/${encodeURIComponent(this.id)}/cancel`, { signal: options.signal });
334
+ this.#data = await this.#transport.json("POST", `/v1/sessions/${encodeURIComponent(this.id)}/cancel`, { signal: options.signal, retry: true });
335
+ return this;
336
+ }
337
+ async end(options = {}) {
338
+ this.#data = await this.#transport.json("POST", `/v1/sessions/${encodeURIComponent(this.id)}/end`, { signal: options.signal, retry: true });
208
339
  return this;
209
340
  }
210
341
  async delete(options = {}) {
211
- await this.#transport.json("DELETE", `/v1/sessions/${encodeURIComponent(this.id)}`, {
212
- signal: options.signal,
213
- });
214
- this.#data = { ...this.#data, state: "deleted" };
342
+ await this.#transport.deleteSession(this.id, options.queue !== true, options.signal);
343
+ this.#data = {
344
+ ...this.#data,
345
+ state: options.queue === true ? "deleting" : "deleted",
346
+ };
215
347
  }
216
348
  markIdle() {
217
- this.#data = { ...this.#data, state: "idle" };
349
+ this.#data = { ...this.#data, turn_state: "idle" };
218
350
  }
219
351
  }
220
352
  function isOutputOptions(options) {
package/dist/tools.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export { compileTools } from "@aexhq/brain";
2
- export type { CompiledTools, Tool } from "@aexhq/brain";
2
+ export type { ClientRegistration, CompiledTools, Tool } from "@aexhq/brain";
@@ -1,11 +1,6 @@
1
1
  import type { Event } from "@aexhq/brain/session";
2
+ import type { JsonRequestOptions, TransferTicket } from "@aexhq/brain";
2
3
  export type Fetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
3
- interface JsonRequestOptions {
4
- body?: unknown;
5
- headers?: Record<string, string>;
6
- signal?: AbortSignal | undefined;
7
- retry?: boolean;
8
- }
9
4
  export interface EventOptions {
10
5
  after?: number;
11
6
  follow?: boolean;
@@ -15,8 +10,22 @@ export declare class Transport {
15
10
  #private;
16
11
  readonly baseUrl: string;
17
12
  constructor(apiKey: string, baseUrl: string, fetchImplementation: Fetch);
13
+ customerHandGrant(clientId: string, signal?: AbortSignal): Promise<{
14
+ url: string;
15
+ protocol: string;
16
+ expiresAt: string;
17
+ observationUrl: string;
18
+ observationToken: string;
19
+ }>;
20
+ customerHandObserve(url: string, token: string, observation: unknown): Promise<void>;
21
+ downloadTransfer(ticket: TransferTicket, signal?: AbortSignal, expectedBytes?: number): Promise<Uint8Array>;
22
+ downloadTransferStream(ticket: TransferTicket, signal?: AbortSignal, expectedBytes?: number): Promise<ReadableStream<Uint8Array>>;
23
+ uploadTransfer(ticket: TransferTicket, content: Uint8Array | (() => ReadableStream<Uint8Array>), bytes: number, signal?: AbortSignal): Promise<void>;
18
24
  json<T>(method: "GET" | "POST" | "DELETE", path: string, options?: JsonRequestOptions): Promise<T>;
25
+ /** Accept one durable deletion job; strict callers poll its short status resource client-side. */
26
+ deleteSession(sessionId: string, waitForCompletion: boolean, signal?: AbortSignal): Promise<void>;
19
27
  events(sessionId: string, options?: EventOptions): AsyncGenerator<Event>;
20
28
  private responseError;
21
29
  }
22
- export {};
30
+ /** @internal Incremental, constant-space decoder for Brain's bounded public SSE events. */
31
+ export declare function parseEventStream(stream: ReadableStream<Uint8Array>): AsyncGenerator<Event>;