@aexhq/sdk 0.84.1 → 0.86.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
@@ -8,14 +8,14 @@ tools, send messages, and read saved conversations without operating a Brain ser
8
8
  Create a key in the [dashboard](https://aex.dev/dashboard). With Node.js 22 or newer, install:
9
9
 
10
10
  ```sh
11
- npm install @aexhq/sdk@0.84.1 @aexhq/agentloop-pi@7.2.3 zod@4.4.3
11
+ npm install @aexhq/sdk@0.86.0 @aexhq/agentloop-pi@7.3.0 zod@4.4.3
12
12
  ```
13
13
 
14
14
  Set `AEX_API_KEY` and `OPENAI_API_KEY` in your server environment. Save as `order.mjs` and
15
15
  run `node order.mjs`:
16
16
 
17
17
  ```js
18
- import { Aex, brainEnv, tool } from "@aexhq/sdk";
18
+ import { Aex, tool } from "@aexhq/sdk";
19
19
  import { pi } from "@aexhq/agentloop-pi";
20
20
  import { z } from "zod";
21
21
 
@@ -27,22 +27,22 @@ const lookupOrder = tool({
27
27
  });
28
28
 
29
29
  const aex = new Aex({ apiKey: process.env.AEX_API_KEY });
30
- try {
31
- const session = await aex.sessions.create({
32
- model: { provider: "openai", name: "gpt-4.1-mini", apiKey: process.env.OPENAI_API_KEY },
33
- agentloop: pi({ env: brainEnv({ name: "brain" }) }),
34
- tools: [lookupOrder()],
35
- });
36
- try {
37
- await session.send("Look up order A-1001. Has it shipped?");
38
- console.log(JSON.stringify(await session.transcript(), null, 2));
39
- console.log("Session:", session.id);
40
- } finally {
41
- await session.end();
30
+ aex.sessions.create({
31
+ model: { provider: "openai", name: "gpt-4.1-mini", apiKey: process.env.OPENAI_API_KEY },
32
+ agentloop: pi(),
33
+ tools: [lookupOrder()],
34
+ }).then(async session => {
35
+ const after = session.state.lastSequence;
36
+ await session.send("Look up order A-1001. Has it shipped?");
37
+ for await (const event of session.events(after)) {
38
+ if (event.type === "turn_failed") throw new Error(JSON.stringify(event.data));
42
39
  }
43
- } finally {
44
- await aex.close();
45
- }
40
+ console.log(JSON.stringify(await session.transcript(), null, 2));
41
+ console.log("Session:", session.id);
42
+ }).catch(error => {
43
+ console.error(error);
44
+ process.exitCode = 1;
45
+ });
46
46
  ```
47
47
 
48
48
  The transcript includes an order lookup and the agent's answer. The lookup function runs in your
@@ -60,13 +60,16 @@ The SDK uses Brain's session API and extension helpers. You can import `tool`, `
60
60
  - [Managed environments](https://github.com/aexhq/aex/blob/main/docs/environments.md): hosted tools and workspaces.
61
61
  - [Attachments](https://github.com/aexhq/aex/blob/main/docs/attachments.md): upload images and PDFs.
62
62
 
63
- Use `ctx.finish(value)` to complete a tool. Use `aex.close()` in `finally` to close client
64
- connections. Closing the client keeps stored sessions; tools in your process still require it.
63
+ Use `ctx.finish(value)` to complete a tool. The shared tool connection suspends after five
64
+ idle seconds and reconnects before the same live client starts more work. Configure
65
+ `connectionIdleTimeoutMs` on `new Aex(...)`; use `0` when other callers or future autonomous
66
+ events must reach these tools. History reads do not reconnect, and event subscriptions remain
67
+ caller-owned. Explicit `aex.close()` permanently disposes of the client while keeping stored sessions.
65
68
 
66
69
  ## Structured output
67
70
 
68
- Ask for a typed application answer on one send. Before ending the session, pass a Zod
69
- schema alongside the message:
71
+ Ask for a typed application answer on one send. Inside the callback, pass a Zod schema
72
+ alongside the message:
70
73
 
71
74
  ```ts
72
75
  const answer = await session.send("Return the order status", {
@@ -119,7 +122,7 @@ correction. It supports JSON Schema, not arbitrary local Zod refinements or tran
119
122
  Brain SDK 0.34 removes typed sends and the corresponding types and errors. Aex SDK 0.84
120
123
  owns them. Existing Aex `send(..., { output })` calls keep the same syntax and behavior.
121
124
  Import `StructuredSendOptions` and `StructuredOutputError` from `@aexhq/sdk`.
122
- Use Pi/Codex 7.2.3 and extension packages pinned to Brain 0.34 with this SDK.
125
+ Use Pi 7.3.0, Codex 7.2.5 and extension packages pinned to Brain 0.36.0 with this SDK.
123
126
  Mixing exact Brain dependency versions creates different TypeScript extension brands.
124
127
 
125
128
  An existing standalone Brain handle can use Aex's policy without changing servers:
@@ -159,7 +162,9 @@ independent request within the invocation's lifetime and the session's model aut
159
162
  usage and spending controls include those requests. The Agentloop owns the shared conversation.
160
163
 
161
164
  Connect short application functions through an authenticated HTTP route with
162
- [HTTP tools](https://github.com/aexhq/aex/blob/main/docs/http-tools.md). Submit a turn, close the
165
+ [HTTP tools](https://aex.dev/docs/application). Submit a turn, close the
163
166
  request and read its committed outcome later. Desktop apps can use
164
167
  [upload-only grants](https://github.com/aexhq/aex/blob/main/docs/attachments.md#upload-from-a-desktop-without-an-account-key)
165
168
  without receiving an account credential.
169
+
170
+ Choose [clientBrowser](https://aex.dev/docs/client-browser) for the user's connected tab, or [Application](https://aex.dev/docs/application) for a deployed backend. Both use `aex.sessions.create()` and ordinary Tool declarations.
@@ -0,0 +1,2 @@
1
+ import { type CreateSessionOptions } from "@aexhq/brain";
2
+ export declare function composeApplication(options: CreateSessionOptions, baseUrl: string): CreateSessionOptions;
@@ -0,0 +1,12 @@
1
+ import { inspectEnvironment, inspectTool } from "@aexhq/brain";
2
+ import { applicationTool } from "@aexhq/env-http";
3
+ export function composeApplication(options, baseUrl) {
4
+ const url = new URL("/environments/application", baseUrl).href;
5
+ return { ...options, tools: options.tools?.map(placed => {
6
+ const tool = inspectTool(placed);
7
+ const environment = inspectEnvironment(tool.environment);
8
+ if (environment.driver.driver !== "http" || environment.driver.url !== url)
9
+ return placed;
10
+ return applicationTool(placed, { env: tool.environment });
11
+ }) };
12
+ }
@@ -29,6 +29,38 @@ export type SyncPayment = {
29
29
  };
30
30
  export interface AexContracts {
31
31
  }
32
+ /**
33
+ * This interface was referenced by `AexContracts`'s JSON-Schema
34
+ * via the `definition` "ClientHost".
35
+ */
36
+ export interface ClientHost {
37
+ hostId: string;
38
+ token: string;
39
+ }
40
+ /**
41
+ * This interface was referenced by `AexContracts`'s JSON-Schema
42
+ * via the `definition` "ClientAccess".
43
+ */
44
+ export interface ClientAccess {
45
+ credentials: ClientHost;
46
+ expires_at: number;
47
+ id: string;
48
+ token: string;
49
+ }
50
+ /**
51
+ * This interface was referenced by `AexContracts`'s JSON-Schema
52
+ * via the `definition` "ClientGrantInput".
53
+ */
54
+ export interface ClientGrantInput {
55
+ expires_at: number;
56
+ origin: string;
57
+ /**
58
+ * The prepared Brain request. Credentials remain on the server.
59
+ */
60
+ session: {
61
+ [k: string]: unknown;
62
+ };
63
+ }
32
64
  /**
33
65
  * This interface was referenced by `AexContracts`'s JSON-Schema
34
66
  * via the `definition` "HttpCatalog".
package/dist/index.d.ts CHANGED
@@ -3,9 +3,10 @@ export type * from "./generated.js";
3
3
  export { AexSessionHandle } from "./session.js";
4
4
  export { StructuredOutputError } from "./structured-output.js";
5
5
  export type { StructuredSendOptions } from "./structured-output.js";
6
- import { Brain, type BrainOptions } from "@aexhq/brain";
6
+ import { application } from "@aexhq/env-http";
7
+ import { Brain, type BrainOptions, type CreateSessionOptions, type OperationOptions, type ModelSelection } from "@aexhq/brain";
7
8
  import { AexSessionHandle } from "./session.js";
8
- import type { EnvironmentCatalog, TokenUsage } from "./generated.js";
9
+ import type { ClientAccess, EnvironmentCatalog, TokenUsage } from "./generated.js";
9
10
  import type { Attachment, Account, Usage, ApiKey, IssuedKey, KeyInput, LoginGrantInput, LoginGrant, LoginExchange, AccountSession, Wallet, BillingSettings, LedgerPage, Topup, TopupInput, Refund, RefundInput, SyncPayment } from "./generated.js";
10
11
  export type AexConnection = Omit<BrainOptions, "token" | "baseUrl"> & {
11
12
  baseUrl?: string;
@@ -15,16 +16,27 @@ export type AexOptions = AexConnection & {
15
16
  } & ({
16
17
  apiKey: string;
17
18
  accountToken?: never;
19
+ clientAccess?: never;
18
20
  } | {
19
21
  accountToken: string;
20
22
  apiKey?: never;
23
+ clientAccess?: never;
24
+ } | {
25
+ clientAccess: ClientAccess;
26
+ apiKey?: never;
27
+ accountToken?: never;
21
28
  });
29
+ export type AexSessionOptions = Omit<CreateSessionOptions, "model"> & {
30
+ model: ModelSelection | Omit<ModelSelection, "apiKey">;
31
+ };
22
32
  export declare class Aex {
23
33
  private readonly client;
24
- constructor({ apiKey, accountToken, maxCostMicroUsd, ...options }: AexOptions);
34
+ private readonly clientAccess?;
35
+ constructor({ apiKey, accountToken, clientAccess, maxCostMicroUsd, ...options }: AexOptions);
36
+ private composition;
25
37
  get baseUrl(): string;
26
38
  readonly sessions: Readonly<{
27
- create: (options: import("@aexhq/brain").CreateSessionOptions, operation?: import("@aexhq/brain").OperationOptions | undefined) => Promise<AexSessionHandle>;
39
+ create: (options: AexSessionOptions, operation?: OperationOptions) => Promise<AexSessionHandle>;
28
40
  get: (sessionId: string, options?: {
29
41
  tools?: readonly import("@aexhq/brain").PlacedTool[];
30
42
  } | undefined) => Promise<AexSessionHandle>;
@@ -77,7 +89,18 @@ export declare class Aex {
77
89
  refund: (input: RefundInput, idempotencyKey: string) => Promise<Refund>;
78
90
  sync: (input: SyncPayment) => Promise<Wallet>;
79
91
  };
92
+ readonly clients: {
93
+ grant: ({ session, origin, expiresAt }: {
94
+ session: CreateSessionOptions;
95
+ origin: string;
96
+ expiresAt?: number;
97
+ }) => Promise<ClientAccess>;
98
+ revoke: (id: string) => Promise<void>;
99
+ };
80
100
  readonly environments: {
101
+ application: (options: Omit<Parameters<typeof application>[0], "url" | "credential"> & {
102
+ credential?: string;
103
+ }) => import("@aexhq/brain").Environment;
81
104
  http: () => Promise<import("./generated.js").HttpCatalog>;
82
105
  list: () => Promise<EnvironmentCatalog>;
83
106
  };
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  export * from "@aexhq/brain";
2
2
  export { AexSessionHandle } from "./session.js";
3
3
  export { StructuredOutputError } from "./structured-output.js";
4
+ import { application } from "@aexhq/env-http";
5
+ import { composeApplication } from "./application.js";
4
6
  import { Brain } from "@aexhq/brain";
5
7
  import { AexSessionHandle } from "./session.js";
6
8
  function nonnegativeInteger(value, name) {
@@ -25,14 +27,25 @@ class HostedBrain extends Brain {
25
27
  }
26
28
  export class Aex {
27
29
  client;
28
- constructor({ apiKey, accountToken, maxCostMicroUsd, ...options }) {
29
- if (!(apiKey || accountToken) || (apiKey && accountToken))
30
- throw new TypeError("one apiKey or accountToken is required");
31
- this.client = new HostedBrain({ baseUrl: "https://api.aex.dev", ...options, token: apiKey ?? accountToken, maxCostMicroUsd });
30
+ clientAccess;
31
+ constructor({ apiKey, accountToken, clientAccess, maxCostMicroUsd, ...options }) {
32
+ if ([apiKey, accountToken, clientAccess].filter(Boolean).length !== 1)
33
+ throw new TypeError("one apiKey, accountToken or clientAccess is required");
34
+ this.clientAccess = clientAccess;
35
+ this.client = new HostedBrain({ baseUrl: "https://api.aex.dev", ...options, token: apiKey ?? accountToken ?? clientAccess?.token,
36
+ ...(clientAccess && { credentials: clientAccess.credentials }), maxCostMicroUsd });
37
+ }
38
+ composition(options) {
39
+ if (this.clientAccess && "apiKey" in options.model)
40
+ throw new TypeError("client access uses server-side model credentials; omit apiKey");
41
+ const model = this.clientAccess ? { ...options.model, apiKey: this.clientAccess.token } : options.model;
42
+ if (!("apiKey" in model) || typeof model.apiKey !== "string")
43
+ throw new TypeError("server-side sessions require a model apiKey");
44
+ return composeApplication({ ...options, model: { ...model, apiKey: model.apiKey } }, this.baseUrl);
32
45
  }
33
46
  get baseUrl() { return this.client.baseUrl; }
34
47
  sessions = Object.freeze({
35
- create: async (...args) => new AexSessionHandle(await this.client.sessions.create(...args)),
48
+ create: async (options, operation) => new AexSessionHandle(await this.client.sessions.create(this.composition(options), operation)),
36
49
  get: async (...args) => new AexSessionHandle(await this.client.sessions.get(...args)),
37
50
  list: () => this.client.sessions.list(),
38
51
  });
@@ -104,7 +117,20 @@ export class Aex {
104
117
  refund: (input, idempotencyKey) => this.request("POST", "/v1/billing/refunds", input, idempotencyKey),
105
118
  sync: (input) => this.request("POST", "/v1/billing/sync", input),
106
119
  };
120
+ clients = {
121
+ grant: async ({ session, origin, expiresAt = Math.floor(Date.now() / 1000) + 3600 }) => {
122
+ if (this.clientAccess)
123
+ throw new TypeError("client grants require a backend API key");
124
+ const prepared = await this.client.sessions.prepare(this.composition(session), "authorized-client");
125
+ return this.request("POST", "/v1/clients", { origin, expires_at: expiresAt, session: prepared });
126
+ },
127
+ revoke: (id) => this.request("DELETE", `/v1/clients/${encodeURIComponent(id)}`),
128
+ };
107
129
  environments = {
130
+ application: (options) => application({
131
+ ...options, credential: options.credential ?? this.clientAccess?.token ?? "",
132
+ url: new URL("/environments/application", this.baseUrl).href,
133
+ }),
108
134
  http: () => this.request("GET", "/v1/environments/http"),
109
135
  list: () => this.request("GET", "/v1/environments"),
110
136
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexhq/sdk",
3
- "version": "0.84.1",
3
+ "version": "0.86.0",
4
4
  "description": "Aex accounts and hosted sessions using the Brain SDK",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -28,8 +28,9 @@
28
28
  "tag": "next"
29
29
  },
30
30
  "dependencies": {
31
- "@aexhq/brain": "0.34.1",
32
- "zod": "4.4.3"
31
+ "@aexhq/brain": "0.36.0",
32
+ "zod": "4.4.3",
33
+ "@aexhq/env-http": "0.3.1"
33
34
  },
34
35
  "scripts": {
35
36
  "gen": "node ../../tools/sdk-contracts.mjs",