@aexhq/sdk 0.55.1 → 0.56.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/README.md CHANGED
@@ -13,12 +13,28 @@ const session = await aex.sessions.create({
13
13
  },
14
14
  });
15
15
 
16
- const result = await session.output(
17
- z.object({ answer: z.string() }),
18
- "Answer the question.",
19
- );
16
+ const result = await session.send("Answer the question.", {
17
+ output: z.object({ answer: z.string() }),
18
+ });
19
+ ```
20
+
21
+ `send()` returns text by default. Passing `output` returns a normal typed Promise and uses
22
+ `https://api.aex.dev` by default. Zod schemas must be representable as JSON Schema; process-local
23
+ custom refinements and transforms fail before any model work is admitted.
24
+
25
+ Sessions start with no model tools. Configure the exact capabilities at creation:
26
+
27
+ ```ts
28
+ import { bash, edit, read, subagents, write } from "@aexhq/tools";
29
+
30
+ const session = await aex.sessions.create({
31
+ model: {
32
+ provider: "anthropic",
33
+ name: "claude-sonnet-5",
34
+ apiKey: "sk-ant-...",
35
+ },
36
+ tools: [bash(), read(), write(), edit(), subagents()],
37
+ });
20
38
  ```
21
39
 
22
- `send()` returns text. `output()` returns a normal typed Promise and uses `https://api.aex.dev` by
23
- default. Zod schemas must be representable as JSON Schema; process-local custom refinements and
24
- transforms fail before any model work is admitted.
40
+ Omitting `tools` and passing `tools: []` are equivalent.
package/dist/errors.d.ts CHANGED
@@ -1,4 +1,10 @@
1
- import type { ApiError, ApiErrorCode, OutputValidationIssue } from "@aexhq/contracts/session";
1
+ import type { ApiError, ApiErrorCode } from "@aexhq/brain/session";
2
+ /** One Aex trusted-output validation failure, expressed as a JSON Pointer. */
3
+ export interface OutputValidationIssue {
4
+ path: string;
5
+ message: string;
6
+ keyword?: string;
7
+ }
2
8
  export interface AexErrorOptions {
3
9
  code?: ApiErrorCode | undefined;
4
10
  status?: number | undefined;
package/dist/index.d.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { Sessions } from "./session.js";
2
2
  import type { Fetch } from "./transport.js";
3
3
  export { AbortError, AexError, OutputRefusalError, OutputSchemaError, OutputValidationError, SessionError, } from "./errors.js";
4
- export type { AexErrorOptions } from "./errors.js";
4
+ export type { AexErrorOptions, OutputValidationIssue } from "./errors.js";
5
5
  export { Session, Sessions, } from "./session.js";
6
- export type { CreateSessionOptions, ListSessionsOptions, ModelSummary, ModelOptions, OutputOptions, RequestOptions, SessionInput, SessionList, SessionSummary, } from "./session.js";
6
+ export type { CreateSessionOptions, ListSessionsOptions, McpServerOptions, ModelSummary, ModelOptions, OutputOptions, RequestOptions, SessionInput, SessionList, SessionSummary, } from "./session.js";
7
7
  export type { EventOptions } from "./transport.js";
8
+ export { defineIntrinsicTool, definePreinstalledTool, defineServerTool, defineTool, } from "@aexhq/brain";
9
+ export type { DefineIntrinsicToolOptions, DefinePreinstalledToolOptions, DefineServerToolOptions, DefineToolOptions, Tool, ToolContext, ToolHandler, } from "@aexhq/brain";
8
10
  export interface AexOptions {
9
11
  apiKey: string;
10
12
  baseUrl?: string;
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import { Sessions } from "./session.js";
2
2
  import { Transport } from "./transport.js";
3
3
  export { AbortError, AexError, OutputRefusalError, OutputSchemaError, OutputValidationError, SessionError, } from "./errors.js";
4
4
  export { Session, Sessions, } from "./session.js";
5
+ export { defineIntrinsicTool, definePreinstalledTool, defineServerTool, defineTool, } from "@aexhq/brain";
5
6
  const DEFAULT_API_URL = "https://api.aex.dev";
6
7
  export class Aex {
7
8
  sessions;
package/dist/session.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import type { Event, Provider, Session as SessionData, SessionState } from "@aexhq/contracts/session";
1
+ import type { Event, Provider, Session as SessionData, SessionState } from "@aexhq/brain/session";
2
2
  import * as z from "zod";
3
3
  import type { EventOptions } from "./transport.js";
4
4
  import { Transport } from "./transport.js";
5
+ import type { Tool } from "./tools.js";
5
6
  export type SessionInput = string;
6
7
  export interface ModelOptions {
7
8
  provider: Provider;
@@ -14,15 +15,33 @@ export interface ModelOptions {
14
15
  }
15
16
  export interface CreateSessionOptions {
16
17
  model: ModelOptions;
18
+ /** Omitted or empty grants no tools. A non-empty list is the exact grant. */
19
+ tools?: readonly Tool[];
20
+ /** Optional remote MCP servers discovered and sealed by Brain at session creation. */
21
+ mcp?: readonly McpServerOptions[];
17
22
  systemPrompt?: string;
23
+ hand?: {
24
+ enabled?: boolean;
25
+ env?: Record<string, string>;
26
+ };
18
27
  metadata?: Record<string, string>;
19
28
  }
29
+ export interface McpServerOptions {
30
+ name: string;
31
+ url: string;
32
+ headers?: Record<string, string>;
33
+ protocol?: "auto" | "2026-07" | "legacy";
34
+ allowedTools?: readonly string[];
35
+ }
20
36
  export interface RequestOptions {
21
37
  signal?: AbortSignal;
22
38
  idempotencyKey?: string;
23
39
  metadata?: Record<string, string>;
24
40
  }
25
- export interface OutputOptions extends RequestOptions {
41
+ export interface OutputOptions<Schema extends z.ZodType = z.ZodType> extends RequestOptions {
42
+ output: Schema;
43
+ /** Extra attempts after the first invalid candidate. Defaults to 1; maximum 2. */
44
+ outputRetries?: 0 | 1 | 2;
26
45
  }
27
46
  export interface ListSessionsOptions {
28
47
  limit?: number;
@@ -66,7 +85,7 @@ export declare class Session implements SessionSummary {
66
85
  get metadata(): Readonly<Record<string, string | undefined>>;
67
86
  refresh(options?: Pick<RequestOptions, "signal">): Promise<this>;
68
87
  send(input: SessionInput, options?: RequestOptions): Promise<string>;
69
- output<Schema extends z.ZodType>(schema: Schema, input?: SessionInput, options?: OutputOptions): Promise<z.output<Schema>>;
88
+ send<Schema extends z.ZodType>(input: SessionInput, options: OutputOptions<Schema>): Promise<z.output<Schema>>;
70
89
  events(options?: EventOptions): AsyncGenerator<Event>;
71
90
  cancel(options?: Pick<RequestOptions, "signal">): Promise<this>;
72
91
  delete(options?: Pick<RequestOptions, "signal">): Promise<void>;
package/dist/session.js CHANGED
@@ -1,12 +1,17 @@
1
1
  import * as z from "zod";
2
- import { AbortError, OutputSchemaError, OutputValidationError, SessionError, abortError, errorFromApi, } from "./errors.js";
3
- import { jcsSha256, randomIdempotencyKey } from "./json.js";
2
+ import { AbortError, OutputRefusalError, OutputSchemaError, OutputValidationError, SessionError, abortError, errorFromApi, } from "./errors.js";
3
+ import { canonicalize, jcsSha256, randomIdempotencyKey } from "./json.js";
4
+ import { compileTools } from "./tools.js";
4
5
  export class Sessions {
5
6
  #transport;
6
7
  constructor(transport) {
7
8
  this.#transport = transport;
8
9
  }
9
10
  async create(options, request = {}) {
11
+ 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");
14
+ }
10
15
  const body = {
11
16
  model: {
12
17
  provider: options.model.provider,
@@ -21,6 +26,31 @@ export class Sessions {
21
26
  ? {}
22
27
  : { reasoning_effort: options.model.reasoningEffort }),
23
28
  },
29
+ tools: {
30
+ 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
+ },
45
+ ...(compiledTools.bundles.length === 0 ? {} : { tool_bundles: compiledTools.bundles }),
46
+ ...(options.hand === undefined
47
+ ? {}
48
+ : {
49
+ hand: {
50
+ ...(options.hand.enabled === undefined ? {} : { enabled: options.hand.enabled }),
51
+ ...(options.hand.env === undefined ? {} : { env: options.hand.env }),
52
+ },
53
+ }),
24
54
  ...(options.systemPrompt === undefined ? {} : { system_prompt: options.systemPrompt }),
25
55
  ...(options.metadata === undefined ? {} : { metadata: options.metadata }),
26
56
  };
@@ -89,15 +119,35 @@ export class Session {
89
119
  return this;
90
120
  }
91
121
  async send(input, options = {}) {
122
+ const outputOptions = isOutputOptions(options) ? options : undefined;
123
+ const compiled = outputOptions === undefined
124
+ ? undefined
125
+ : await compileOutputSchema(outputOptions.output, outputOptions.outputRetries);
92
126
  const accepted = await this.#transport.json("POST", `/v1/sessions/${encodeURIComponent(this.id)}/messages`, {
93
127
  body: {
94
128
  content: input,
95
129
  ...(options.metadata === undefined ? {} : { metadata: options.metadata }),
130
+ ...(compiled === undefined
131
+ ? {}
132
+ : {
133
+ output: {
134
+ schema: compiled.jsonSchema,
135
+ schema_hash: compiled.schemaHash,
136
+ ...(compiled.retries === undefined ? {} : { retries: compiled.retries }),
137
+ },
138
+ }),
96
139
  },
97
140
  headers: { "Idempotency-Key": options.idempotencyKey ?? randomIdempotencyKey() },
98
141
  signal: options.signal,
99
142
  retry: true,
100
143
  });
144
+ if (compiled !== undefined) {
145
+ if (accepted.session_id !== this.id ||
146
+ accepted.output_id === undefined ||
147
+ accepted.schema_hash !== compiled.schemaHash) {
148
+ throw new SessionError("Aex returned an inconsistent typed-output admission");
149
+ }
150
+ }
101
151
  let answer = "";
102
152
  try {
103
153
  for await (const event of this.events({ after: Math.max(0, accepted.seq - 1), signal: options.signal })) {
@@ -105,79 +155,39 @@ export class Session {
105
155
  answer = event.text;
106
156
  }
107
157
  else if (event.type === "turn.failed" && event.turn_id === accepted.turn_id) {
108
- throw errorFromApi(event.error);
158
+ throw errorFromApi(event.error, undefined, outputIssues(event.error));
109
159
  }
110
160
  else if (event.type === "turn.completed" && event.turn_id === accepted.turn_id) {
111
161
  this.markIdle();
112
162
  if (event.stop_reason === "cancelled")
113
163
  throw new AbortError();
114
- return answer;
115
- }
116
- }
117
- }
118
- catch (error) {
119
- if (options.signal?.aborted === true)
120
- throw abortError(error);
121
- throw error;
122
- }
123
- throw new SessionError("The Aex event stream ended before the session finished its work");
124
- }
125
- async output(schema, input, options = {}) {
126
- let jsonSchema;
127
- try {
128
- assertPortableOutputSchema(schema);
129
- jsonSchema = z.toJSONSchema(schema, {
130
- target: "draft-2020-12",
131
- unrepresentable: "throw",
132
- });
133
- }
134
- catch (cause) {
135
- throw new OutputSchemaError(messageOf(cause, "The Zod schema cannot be represented as JSON Schema"), {
136
- cause,
137
- });
138
- }
139
- if (jsonSchema.type !== "object") {
140
- throw new OutputSchemaError("session.output() requires a Zod object schema");
141
- }
142
- const schemaHash = await jcsSha256(jsonSchema);
143
- const body = {
144
- schema: jsonSchema,
145
- schema_hash: schemaHash,
146
- ...(input === undefined ? {} : { input }),
147
- ...(options.metadata === undefined ? {} : { metadata: options.metadata }),
148
- };
149
- const accepted = await this.#transport.json("POST", `/v1/sessions/${encodeURIComponent(this.id)}/output`, {
150
- body,
151
- headers: { "Idempotency-Key": options.idempotencyKey ?? randomIdempotencyKey() },
152
- signal: options.signal,
153
- retry: true,
154
- });
155
- if (accepted.session_id !== this.id || accepted.schema_hash !== schemaHash) {
156
- throw new SessionError("Aex returned an inconsistent output admission");
157
- }
158
- try {
159
- for await (const event of this.events({ after: Math.max(0, accepted.seq - 1), signal: options.signal })) {
160
- if (event.type === "output.failed" && event.output_id === accepted.output_id) {
161
- if (event.schema_hash !== schemaHash) {
162
- throw new SessionError("Aex returned an output failure for a different schema");
163
- }
164
- this.markIdle();
165
- throw outputEventError(event.error, event.issues);
166
- }
167
- if (event.type === "output.completed" && event.output_id === accepted.output_id) {
168
- if (event.output.schema_hash !== schemaHash) {
169
- throw new SessionError("Aex returned an output value for a different schema");
170
- }
171
- this.markIdle();
172
- const parsed = await schema.safeParseAsync(event.output.value);
173
- if (!parsed.success) {
174
- throw new OutputValidationError("The output passed the wire schema but failed the original Zod schema", parsed.error.issues.map((issue) => ({
175
- path: jsonPointer(issue.path),
176
- message: issue.message,
177
- keyword: issue.code,
178
- })));
164
+ if (compiled !== undefined && outputOptions !== undefined) {
165
+ if (event.result === undefined) {
166
+ if (event.stop_reason === "refusal") {
167
+ throw new OutputRefusalError("The model refused the structured-output request");
168
+ }
169
+ throw new OutputValidationError("The model ended the turn without submitting the requested structured output", [{
170
+ path: "",
171
+ message: "The model did not call aex_submit_output",
172
+ keyword: "missing_output",
173
+ }]);
174
+ }
175
+ if (event.result.name !== "aex_submit_output" ||
176
+ event.result.metadata?.output_id !== accepted.output_id ||
177
+ event.result.metadata?.schema_hash !== compiled.schemaHash) {
178
+ throw new SessionError("Aex returned a typed result for a different request");
179
+ }
180
+ const parsed = await outputOptions.output.safeParseAsync(event.result.value);
181
+ if (!parsed.success) {
182
+ throw new OutputValidationError("The output passed the wire schema but failed the original Zod schema", parsed.error.issues.map((issue) => ({
183
+ path: jsonPointer(issue.path),
184
+ message: issue.message,
185
+ keyword: issue.code,
186
+ })));
187
+ }
188
+ return parsed.data;
179
189
  }
180
- return parsed.data;
190
+ return answer;
181
191
  }
182
192
  }
183
193
  }
@@ -188,7 +198,7 @@ export class Session {
188
198
  }
189
199
  throw error;
190
200
  }
191
- throw new SessionError("The Aex event stream ended before the typed output completed");
201
+ throw new SessionError("The Aex event stream ended before the session finished its work");
192
202
  }
193
203
  events(options = {}) {
194
204
  return this.#transport.events(this.id, options);
@@ -207,8 +217,82 @@ export class Session {
207
217
  this.#data = { ...this.#data, state: "idle" };
208
218
  }
209
219
  }
210
- function outputEventError(error, issues) {
211
- return errorFromApi(error, undefined, issues ?? []);
220
+ function isOutputOptions(options) {
221
+ return "output" in options;
222
+ }
223
+ async function compileOutputSchema(schema, retries) {
224
+ let jsonSchema;
225
+ try {
226
+ assertPortableOutputSchema(schema);
227
+ jsonSchema = z.toJSONSchema(schema, {
228
+ target: "draft-2020-12",
229
+ unrepresentable: "throw",
230
+ });
231
+ }
232
+ catch (cause) {
233
+ throw new OutputSchemaError(messageOf(cause, "The Zod schema cannot be represented as JSON Schema"), {
234
+ cause,
235
+ });
236
+ }
237
+ if (jsonSchema.type !== "object") {
238
+ throw new OutputSchemaError("session.send() output requires a Zod object schema");
239
+ }
240
+ assertSupportedJsonSchema(jsonSchema);
241
+ return { jsonSchema, schemaHash: await jcsSha256(jsonSchema), retries };
242
+ }
243
+ function assertSupportedJsonSchema(schema) {
244
+ const bytes = new TextEncoder().encode(canonicalize(schema));
245
+ if (bytes.byteLength > 64 * 1024) {
246
+ throw new OutputSchemaError("The output schema exceeds the 65536-byte service limit");
247
+ }
248
+ let nodes = 0;
249
+ const visit = (value, depth) => {
250
+ if (depth > 64)
251
+ throw new OutputSchemaError("The output schema exceeds the service depth limit");
252
+ nodes += 1;
253
+ if (nodes > 4096)
254
+ throw new OutputSchemaError("The output schema exceeds the service node limit");
255
+ if (Array.isArray(value)) {
256
+ for (const child of value)
257
+ visit(child, depth + 1);
258
+ return;
259
+ }
260
+ if (value === null || typeof value !== "object")
261
+ return;
262
+ const object = value;
263
+ if ("pattern" in object || "patternProperties" in object) {
264
+ throw new OutputSchemaError("Regular-expression JSON Schema keywords are not supported for Aex structured output");
265
+ }
266
+ for (const keyword of ["$ref", "$dynamicRef", "$recursiveRef"]) {
267
+ const reference = object[keyword];
268
+ if (typeof reference === "string" && !reference.startsWith("#")) {
269
+ throw new OutputSchemaError("Remote JSON Schema references are not supported for Aex structured output");
270
+ }
271
+ }
272
+ for (const child of Object.values(object))
273
+ visit(child, depth + 1);
274
+ };
275
+ visit(schema, 0);
276
+ }
277
+ function outputIssues(error) {
278
+ const details = error.details;
279
+ if (details === undefined || details === null || typeof details !== "object")
280
+ return [];
281
+ const issues = details.issues;
282
+ if (!Array.isArray(issues))
283
+ return [];
284
+ return issues.flatMap((issue) => {
285
+ if (issue === null || typeof issue !== "object")
286
+ return [];
287
+ const value = issue;
288
+ if (typeof value.path !== "string" || typeof value.message !== "string")
289
+ return [];
290
+ return [{
291
+ path: value.path,
292
+ message: value.message,
293
+ ...(typeof value.keyword === "string" ? { keyword: value.keyword } : {}),
294
+ }];
295
+ });
212
296
  }
213
297
  function jsonPointer(path) {
214
298
  if (path.length === 0)
@@ -0,0 +1,2 @@
1
+ export { compileTools } from "@aexhq/brain";
2
+ export type { CompiledTools, Tool } from "@aexhq/brain";
package/dist/tools.js ADDED
@@ -0,0 +1 @@
1
+ export { compileTools } from "@aexhq/brain";
@@ -1,4 +1,4 @@
1
- import type { Event } from "@aexhq/contracts/session";
1
+ import type { Event } from "@aexhq/brain/session";
2
2
  export type Fetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
3
3
  interface JsonRequestOptions {
4
4
  body?: unknown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexhq/sdk",
3
- "version": "0.55.1",
3
+ "version": "0.56.0",
4
4
  "description": "TypeScript SDK for Aex sessions",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://aex.dev",
@@ -33,13 +33,13 @@
33
33
  "test": "tsc -p tsconfig.json && node --test"
34
34
  },
35
35
  "dependencies": {
36
- "@aexhq/contracts": "0.26.1"
36
+ "@aexhq/brain": "0.1.0"
37
37
  },
38
38
  "peerDependencies": {
39
- "zod": "^4.0.0"
39
+ "zod": "4.4.3"
40
40
  },
41
41
  "devDependencies": {
42
42
  "typescript": "^5.9.2",
43
- "zod": "^4.0.0"
43
+ "zod": "4.4.3"
44
44
  }
45
45
  }