@aexhq/sdk 0.57.5 → 0.59.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
@@ -1,105 +1,63 @@
1
- # `@aexhq/sdk`
1
+ # @aexhq/sdk
2
2
 
3
- TypeScript client for durable Aex sessions.
3
+ TypeScript SDK for Aex sessions and tool extensions.
4
4
 
5
5
  ```ts
6
- import { Aex, tool } from "@aexhq/sdk";
7
- import { z } from "zod";
6
+ import { Aex } from "@aexhq/sdk";
7
+ import { awsMicrovm } from "@aexhq/env-aws-microvm";
8
+ import { pi } from "@aexhq/loop-pi";
9
+ import { openai } from "@aexhq/model-openai";
10
+ import { bash, read, task, write } from "@aexhq/tools";
8
11
 
9
- const lookup = tool(
10
- z.object({ id: z.string() }),
11
- async function lookup({ id }) {
12
- return database.customers.find(id);
13
- },
14
- ).client();
12
+ const aex = new Aex({ apiKey: process.env.AEX_API_KEY! });
13
+ const workspace = awsMicrovm();
15
14
 
16
- const aex = new Aex({
17
- apiKey: process.env.AEX_API_KEY!,
18
- client: { id: "customer-api" },
15
+ const session = await aex.sessions.create({
16
+ model: {
17
+ component: openai(),
18
+ provider: "openai",
19
+ name: "gpt-5.4",
20
+ apiKey: process.env.OPENAI_API_KEY!,
21
+ },
22
+ agentloop: pi({ instructions: "Work carefully and verify changes." }),
23
+ environments: { workspace },
24
+ tools: [bash(), read(), write(), task()],
19
25
  });
20
- const model = {
21
- provider: "openai" as const,
22
- name: "gpt-5.4",
23
- apiKey: process.env.OPENAI_API_KEY!,
24
- };
25
- const session = await aex.sessions.create({ model, tools: [lookup] });
26
26
 
27
- const result = await session.send("Answer the question.", {
28
- output: z.object({ answer: z.string() }),
29
- });
27
+ console.log(await session.send("Inspect the workspace."));
30
28
  ```
31
29
 
32
- `send()` returns text by default. Passing `output` returns a typed Promise. The client uses
33
- `https://api.aex.dev` by default; the model `baseUrl` is independent of the Aex API origin.
34
- `send()` resolves only the durable winning assistant message after provider recovery. The lower-
35
- level `session.events()` stream is raw and attempt-aware: provisional frames have no durable cursor
36
- and can later be superseded, so live renderers must key them by `attempt_id` and process
37
- `model.attempt_superseded` instead of concatenating attempts.
38
- The SDK always generates and reuses an `Idempotency-Key` for root/child creation and messages; pass
39
- `idempotencyKey` explicitly when the application must recover the same operation across its own
40
- process restart. Raw hosted REST calls must supply that header for those operations.
41
-
42
- Hosted alpha has one managed-compute shape, `1gb` (0.5 vCPU and 1 GiB), so the SDK exposes no shape
43
- selector. Child sessions and managed sandboxes inherit the root's physical seal.
30
+ There is no default Agentloop, Model, Tool, or Environment. A Tool component that requests the
31
+ Environment capability requires the session's one declared Environment in the MVP.
44
32
 
45
- Tools are immutable once the session is created. `.client()` keeps closures in this application;
46
- `.server(import.meta.url)` bundles a module whose default export is the completed Tool value for the
47
- session's shared managed computer. One customer-app socket is shared across sessions; call
48
- `aex.close()` during graceful process shutdown to stop it and interrupt process-local work. A
49
- closed `Aex` instance cannot create another session. The
50
- default `.client()` registration
51
- is derived from its contract. If one application intentionally has different closures with the
52
- same name and schemas, give each a stable `.client({ registration: "..." })`; a live runner rejects
53
- a registration collision instead of invoking the wrong closure. Sessions grant no execution
54
- capabilities by default. Add only the official capabilities needed. Aex's reserved output protocol is
55
- present but inert unless a particular `send({ output })` request arms it:
33
+ ## Application Tools
56
34
 
57
- Hosted `.server()` bindings use distinct generation-lifetime unprivileged users, separate from the
58
- ordinary shell, while sharing the workspace through a group. Declared environment secrets are not
59
- written by Aex to the workspace, arguments, results, or logs. This blocks ordinary sibling reads,
60
- not guest-root compromise or a Tool deliberately writing the value. Use `.client()` or an external
61
- service for that stronger boundary; local mode is intentionally unsandboxed.
35
+ Application callbacks use the same Tool and Environment component ABI while the handler remains in
36
+ your process. Declare one `app()` Environment; Aex registers each selected callback over one
37
+ authenticated, reconnecting connection. Handler source and captured application state are never
38
+ uploaded to Brain.
62
39
 
63
40
  ```ts
64
- import { bash, edit, read, sandbox, storage, subagents, write } from "@aexhq/tools";
65
-
66
- const session = await aex.sessions.create({
67
- model,
68
- tools: [bash(), read(), write(), edit(), storage(), sandbox(), subagents()],
69
- network: { outbound: "none" },
70
- });
71
- ```
72
-
73
- Omitting `tools` and passing `tools: []` are equivalent. Zod schemas must be representable as JSON
74
- Schema; process-local refinements and transforms fail before model work starts. Temporary files are
75
- available through `session.sandbox.files`; durable objects use `session.storage`. Large transfers
76
- automatically bypass the Brain actor. Durable direct children use `session.children`.
77
- Sandbox file operations honor guest permissions; they do not bypass a `.server()` binding's
78
- deliberate mode-0600 files. The binding must explicitly export or relax those permissions.
79
-
80
- Buffered `upload()` and `download()` are convenient for small objects. Large transfers can stay
81
- O(1)-heap with `downloadStream()` and a replayable declared source:
41
+ import { Aex, tool } from "@aexhq/sdk";
42
+ import { app } from "@aexhq/env-app";
43
+ import { z } from "zod";
82
44
 
83
- ```ts
84
- import { createReadStream } from "node:fs";
85
- import { Readable } from "node:stream";
45
+ const lookupOrder = tool(
46
+ z.object({ orderId: z.string() }),
47
+ async function lookupOrder({ orderId }) {
48
+ return database.lookup(orderId);
49
+ },
50
+ )
51
+ .describe("Look up an order")
52
+ .returns(z.object({ status: z.string() }));
86
53
 
87
- await session.storage.upload("inputs/archive.tar", {
88
- bytes: stat.size,
89
- sha256: digest,
90
- stream: () => Readable.toWeb(createReadStream(filename)),
54
+ await new Aex({ apiKey: process.env.AEX_API_KEY! }).sessions.create({
55
+ model,
56
+ agentloop,
57
+ environments: { application: app({ id: "orders-ui" }) },
58
+ tools: [lookupOrder],
91
59
  });
92
- const body = await session.storage.downloadStream("inputs/archive.tar");
93
60
  ```
94
61
 
95
- The same methods are available on `session.sandbox.files`; the SDK enforces upload length, ticket
96
- ceilings, abort signals, and the downloaded object's exact metadata length while bytes bypass
97
- Brain. Direct large sandbox transfers are a happy-path convenience: the SDK does not automatically
98
- retry an ambiguous completion or recover a ticket after Brain restart or expiry. Inspect the
99
- generation and path, then prepare a fresh transfer. Put bytes in `session.storage` and copy them to
100
- or from the sandbox when the transfer itself must be recovery-safe.
101
-
102
- `await session.end()` closes work but retains journal and storage. Both deletion modes first make
103
- the same short, durable deletion-job request. `await session.delete()` then polls the status
104
- resource for confirmed removal; `{ queue: true }` returns after acceptance. No HTTP request stays
105
- open while sandbox and storage cleanup runs.
62
+ Source callback Tools are routed automatically to the single `app()` Environment. Durable objects
63
+ remain available through `session.storage`, independently of Environment lifetime.
package/dist/index.d.ts CHANGED
@@ -4,21 +4,18 @@ import type { WebSocketFactory } from "@aexhq/brain";
4
4
  export { AbortError, AexError, OutputRefusalError, OutputSchemaError, OutputValidationError, SessionError, } from "./errors.js";
5
5
  export type { AexErrorOptions, OutputValidationIssue } from "./errors.js";
6
6
  export { Session, Sessions, } from "./session.js";
7
- export type { AgentloopBundle, CreateSessionOptions, ListSessionsOptions, ModelSummary, ModelOptions, OutputOptions, RequestOptions, SessionInput, SessionList, SessionSummary, } from "./session.js";
8
- export { SandboxFiles, SessionChild, SessionChildren, SessionSandbox, SessionStorage, } from "./resources.js";
9
- export type { BinarySource, ChildSummary, IdempotentOperationOptions, OperationOptions, PageOptions, SandboxFile, SandboxFileOptions, SandboxFilePage, SandboxFilePageOptions, SandboxStatus, StorageObject, StoragePage, StreamingUploadSource, UploadSource, } from "./resources.js";
7
+ export type { CreateSessionOptions, ListSessionsOptions, ModelSummary, ModelOptions, OutputOptions, RequestOptions, SessionInput, SessionList, SessionSummary, } from "./session.js";
8
+ export { SessionChild, SessionChildren, SessionStorage, } from "./resources.js";
9
+ export type { BinarySource, ChildSummary, IdempotentOperationOptions, OperationOptions, PageOptions, StorageObject, StoragePage, StreamingUploadSource, UploadSource, } from "./resources.js";
10
10
  export type { EventOptions } from "./transport.js";
11
- export { tool } from "@aexhq/brain";
12
- export type { ClientToolOptions, Tool, ToolBuilder, ToolContract, ToolContext, ToolHandler, ServerToolOptions, NetworkDestination, NetworkPolicy, WebSocketFactory, } from "@aexhq/brain";
11
+ export { tool } from "./tools.js";
12
+ export type { Tool, ToolContract, ToolContext, ToolHandler, } from "./tools.js";
13
+ export type { NetworkPolicy, WebSocketFactory } from "@aexhq/brain";
13
14
  export interface AexOptions {
14
15
  apiKey: string;
15
16
  baseUrl?: string;
16
17
  fetch?: Fetch;
17
18
  webSocketFactory?: WebSocketFactory;
18
- /** Stable tenant-scoped identity for this exact customer-application runner. */
19
- client?: {
20
- id: string;
21
- };
22
19
  }
23
20
  export declare class Aex {
24
21
  readonly sessions: Sessions;
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@ 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 { SandboxFiles, SessionChild, SessionChildren, SessionSandbox, SessionStorage, } from "./resources.js";
6
- export { tool } from "@aexhq/brain";
5
+ export { SessionChild, SessionChildren, SessionStorage, } from "./resources.js";
6
+ export { tool } from "./tools.js";
7
7
  const DEFAULT_API_URL = "https://api.aex.dev";
8
8
  export class Aex {
9
9
  sessions;
@@ -14,16 +14,12 @@ export class Aex {
14
14
  if (fetchImplementation === undefined) {
15
15
  throw new TypeError("This runtime does not provide fetch; pass a fetch implementation to Aex");
16
16
  }
17
- if (options.client !== undefined &&
18
- !/^[A-Za-z0-9_.:-]{1,128}$/u.test(options.client.id)) {
19
- throw new TypeError("Aex client.id must contain 1 through 128 letters, digits, dots, colons, underscores, or hyphens");
20
- }
21
17
  const transport = new Transport(options.apiKey, options.baseUrl ?? DEFAULT_API_URL, fetchImplementation);
22
18
  const webSocketFactory = options.webSocketFactory ??
23
19
  (globalThis.WebSocket === undefined
24
20
  ? undefined
25
21
  : (request) => new globalThis.WebSocket(request.url, request.protocol));
26
- this.sessions = new Sessions(transport, webSocketFactory, options.client?.id);
22
+ this.sessions = new Sessions(transport, webSocketFactory);
27
23
  }
28
24
  /** Stop customer-app execution permanently; this Aex instance cannot create another session. */
29
25
  close() {
@@ -101,12 +101,14 @@ export declare class SessionStorage {
101
101
  }): Promise<StorageObject>;
102
102
  delete(key: string, options?: OperationOptions): Promise<void>;
103
103
  copyFromSandbox(input: {
104
+ environment: string;
104
105
  key: string;
105
106
  path: string;
106
107
  sandboxGeneration: string;
107
108
  overwrite?: boolean;
108
109
  }, options?: OperationOptions): Promise<StorageObject>;
109
110
  copyToSandbox(input: {
111
+ environment: string;
110
112
  key: string;
111
113
  path: string;
112
114
  sandboxGeneration: string;
package/dist/resources.js CHANGED
@@ -159,18 +159,18 @@ export class SessionStorage {
159
159
  return this.#inner.delete(key, request(options));
160
160
  }
161
161
  async copyFromSandbox(input, options = {}) {
162
- return storageObject(await this.#inner.copyFromSandbox({
162
+ return storageObject(await this.#inner.copyFromEnvironment(input.environment, {
163
163
  key: input.key,
164
164
  path: input.path,
165
- sandbox_generation: input.sandboxGeneration,
165
+ environment_generation: input.sandboxGeneration,
166
166
  ...(input.overwrite === undefined ? {} : { overwrite: input.overwrite }),
167
167
  }, request(options)));
168
168
  }
169
169
  async copyToSandbox(input, options = {}) {
170
- return file(await this.#inner.copyToSandbox({
170
+ return file(await this.#inner.copyToEnvironment(input.environment, {
171
171
  key: input.key,
172
172
  path: input.path,
173
- sandbox_generation: input.sandboxGeneration,
173
+ environment_generation: input.sandboxGeneration,
174
174
  ...(input.overwrite === undefined ? {} : { overwrite: input.overwrite }),
175
175
  }, request(options)));
176
176
  }
package/dist/session.d.ts CHANGED
@@ -1,56 +1,37 @@
1
- import type { Event, Provider, Session as SessionData, SessionState } from "@aexhq/brain/session";
1
+ import type { Event, Session as BrainSessionData, SessionState } from "@aexhq/brain/session";
2
+ import { type ComponentExtension, type SessionTool as ComponentSessionTool } from "@aexhq/brain";
2
3
  import type { NetworkPolicy, WebSocketFactory } from "@aexhq/brain";
3
4
  import * as z from "zod";
4
5
  import type { EventOptions } from "./transport.js";
5
6
  import { Transport } from "./transport.js";
6
- import type { Tool } from "./tools.js";
7
+ import { type Tool } from "./tools.js";
7
8
  import { SessionChildren, SessionSandbox, SessionStorage } from "./resources.js";
9
+ type SessionData = BrainSessionData & {
10
+ retain_until: string;
11
+ };
8
12
  export type SessionInput = string;
9
13
  export interface ModelOptions {
10
- provider: Provider;
14
+ /** Imported Model component. */
15
+ component: ComponentExtension<"model">;
16
+ provider: string;
11
17
  name: string;
12
18
  apiKey: string;
13
19
  baseUrl?: string;
14
20
  maxOutputTokens?: number;
15
- /** Immutable context capacity used for admission and compaction; Brain never guesses by name. */
16
- contextWindowTokens?: number;
17
- temperature?: number;
18
- reasoningEffort?: "low" | "medium" | "high";
19
- }
20
- /**
21
- * A built agentloop implementation, as exported by a loop package or produced by
22
- * `buildLoopBundle` from `@aexhq/agentloop`. Assignment is by import, never by name:
23
- * the sealed identity is the content digest plus the pinned toolchain.
24
- */
25
- export interface AgentloopBundle {
26
- /** The complete deterministic ESM source bundle — the exact bytes sealed and uploaded. */
27
- source: string;
28
- /** SHA-256 hex of the UTF-8 source bytes. */
29
- sha256: string;
30
- /** The pinned loop-toolchain identity the bundle was built for. */
31
- toolchain: string;
32
21
  }
33
22
  export interface CreateSessionOptions {
34
23
  model: ModelOptions;
24
+ /** The imported agent loop that drives this session and every child unless spawn overrides it. */
25
+ agentloop: ComponentExtension<"agentloop">;
35
26
  /** Omitted or empty grants no tools. A non-empty list is the exact grant. */
36
- tools?: readonly Tool[];
37
- /**
38
- * The agentloop driving this session's turns, assigned by importing its implementation.
39
- * Sealed at create for the life of the session; children inherit it. Omission seals the
40
- * official aex loop.
41
- */
42
- agentloop?: AgentloopBundle;
43
- systemPrompt?: string;
44
- /** Write-only values for environment names declared by managed Tools. */
27
+ environments?: Readonly<Record<string, ComponentExtension<"environment">>>;
28
+ tools?: readonly (Tool | ComponentSessionTool)[];
29
+ /** Write-only values for components that explicitly consume session secrets. */
45
30
  secrets?: Record<string, string>;
46
31
  /** Maximum direct outbound network authority sealed for managed sandboxes. Omission is deny-all. */
47
32
  network?: NetworkPolicy;
48
33
  /** Replacement attempts after an unrecoverable provider outcome. Defaults to one. */
49
34
  providerRecoveryRetries?: 0 | 1;
50
- client?: {
51
- /** Replacement sends to the same customer process and operation. Defaults to one. */
52
- submitRetries?: 0 | 1;
53
- };
54
35
  /** Optional ceilings for durable child sessions. Omitted fields use the hosted defaults. */
55
36
  children?: {
56
37
  maxDepth?: number;
@@ -58,6 +39,8 @@ export interface CreateSessionOptions {
58
39
  maxDescendants?: number;
59
40
  };
60
41
  metadata?: Record<string, string>;
42
+ /** Finite durable-history deadline. Omit to use the Brain deployment default. */
43
+ retainUntil?: Date | string;
61
44
  }
62
45
  export interface RequestOptions {
63
46
  signal?: AbortSignal;
@@ -81,7 +64,7 @@ export interface SessionList {
81
64
  nextCursor?: string;
82
65
  }
83
66
  export interface ModelSummary {
84
- provider: Provider;
67
+ provider: string;
85
68
  name: string;
86
69
  baseUrl?: string;
87
70
  contextWindowTokens: number;
@@ -96,11 +79,12 @@ export interface SessionSummary {
96
79
  model: ModelSummary;
97
80
  createdAt: string;
98
81
  updatedAt: string;
82
+ retainUntil: string;
99
83
  metadata: Readonly<Record<string, string | undefined>>;
100
84
  }
101
85
  export declare class Sessions {
102
86
  #private;
103
- constructor(transport: Transport, webSocketFactory?: WebSocketFactory, clientId?: string);
87
+ constructor(transport: Transport, webSocketFactory?: WebSocketFactory);
104
88
  /** @internal Called by `Aex.close()`. */
105
89
  close(): void;
106
90
  create(options: CreateSessionOptions, request?: RequestOptions): Promise<Session>;
@@ -122,6 +106,7 @@ export declare class Session implements SessionSummary {
122
106
  get model(): ModelSummary;
123
107
  get createdAt(): string;
124
108
  get updatedAt(): string;
109
+ get retainUntil(): string;
125
110
  get metadata(): Readonly<Record<string, string | undefined>>;
126
111
  refresh(options?: Pick<RequestOptions, "signal">): Promise<this>;
127
112
  send(input: SessionInput, options?: RequestOptions): Promise<string>;
@@ -133,9 +118,15 @@ export declare class Session implements SessionSummary {
133
118
  */
134
119
  events(options?: EventOptions): AsyncGenerator<Event>;
135
120
  cancel(options?: Pick<RequestOptions, "signal">): Promise<this>;
121
+ suspend(options?: Pick<RequestOptions, "signal">): Promise<this>;
122
+ resume(options?: Pick<RequestOptions, "signal">): Promise<this>;
123
+ setRetention(value: Date | string, options?: Pick<RequestOptions, "signal"> & {
124
+ allowShorten?: boolean;
125
+ }): Promise<this>;
136
126
  end(options?: Pick<RequestOptions, "signal">): Promise<this>;
137
127
  delete(options?: Pick<RequestOptions, "signal"> & {
138
128
  queue?: boolean;
139
129
  }): Promise<void>;
140
130
  private markIdle;
141
131
  }
132
+ export {};
package/dist/session.js CHANGED
@@ -1,40 +1,127 @@
1
- import { CustomerHand } from "@aexhq/brain";
1
+ import { CustomerEnvironment, prepareComponents, } from "@aexhq/brain";
2
2
  import * as z from "zod";
3
3
  import { AbortError, OutputRefusalError, OutputSchemaError, OutputValidationError, SessionError, abortError, errorFromApi, } from "./errors.js";
4
4
  import { canonicalize, jcsSha256, randomIdempotencyKey } from "./json.js";
5
- import { compileTools } from "./tools.js";
6
- import { encodeBase64, SessionChildren, SessionSandbox, SessionStorage } from "./resources.js";
5
+ import { compileCallbacks } from "./tools.js";
6
+ import { SessionChildren, SessionSandbox, SessionStorage } from "./resources.js";
7
7
  export class Sessions {
8
8
  #transport;
9
9
  #webSocketFactory;
10
- #clientId;
11
- #customerHand;
12
- #customerHandInstance;
10
+ #customerEnvironments = new Map();
11
+ #customerEnvironmentInstances = new Map();
13
12
  #closed = false;
14
- constructor(transport, webSocketFactory, clientId) {
13
+ constructor(transport, webSocketFactory) {
15
14
  this.#transport = transport;
16
15
  this.#webSocketFactory = webSocketFactory;
17
- this.#clientId = clientId;
18
16
  }
19
17
  /** @internal Called by `Aex.close()`. */
20
18
  close() {
21
19
  if (this.#closed)
22
20
  return;
23
21
  this.#closed = true;
24
- this.#customerHandInstance?.close();
25
- this.#customerHandInstance = undefined;
26
- this.#customerHand = undefined;
22
+ for (const hand of this.#customerEnvironmentInstances.values())
23
+ hand.close();
24
+ this.#customerEnvironmentInstances.clear();
25
+ this.#customerEnvironments.clear();
27
26
  }
28
27
  async create(options, request = {}) {
29
28
  if (this.#closed)
30
29
  throw new SessionError("Aex client is closed");
31
- const compiledTools = await compileTools(options.tools);
32
- if (options.client?.submitRetries !== undefined && this.#clientId === undefined) {
33
- throw new TypeError("client.submitRetries requires Aex({ client: { id } })");
30
+ if (options.model?.component === undefined) {
31
+ throw new TypeError("sessions.create requires an imported Model component");
34
32
  }
35
- await this.#ensureCustomerHand(compiledTools.clientRegistrations, request.signal);
33
+ if (options.agentloop === undefined) {
34
+ throw new TypeError("sessions.create requires an imported Agentloop component");
35
+ }
36
+ const model = options.model.component;
37
+ const agentloop = options.agentloop;
38
+ const tools = [...(options.tools ?? [])];
39
+ const componentTools = tools.filter(isComponentTool);
40
+ const callbackTools = tools.filter((tool) => !isComponentTool(tool));
41
+ const environments = Object.entries(options.environments ?? {});
42
+ if (environments.some(([, environment]) => !isEnvironmentComponent(environment))) {
43
+ throw new TypeError("component sessions accept only precompiled Environment component values");
44
+ }
45
+ const environmentComponents = environments.map(([name, environment]) => {
46
+ if (!isEnvironmentComponent(environment)) {
47
+ throw new TypeError(`Environment ${JSON.stringify(name)} is not a component`);
48
+ }
49
+ return [name, environment];
50
+ });
51
+ const applicationEnvironments = environmentComponents.filter(([, environment]) => isApplicationEnvironment(environment));
52
+ if (callbackTools.length > 0 && applicationEnvironments.length !== 1) {
53
+ throw new TypeError("application callback Tools require exactly one app() Environment");
54
+ }
55
+ const callbacks = await compileCallbacks(callbackTools);
56
+ const allComponentTools = [...componentTools, ...callbacks.components];
57
+ const toolDefinitions = allComponentTools.map(componentToolDefinition);
58
+ const names = new Set();
59
+ for (const definition of toolDefinitions) {
60
+ if (names.has(definition.name)) {
61
+ throw new TypeError(`Tool ${JSON.stringify(definition.name)} was selected twice`);
62
+ }
63
+ names.add(definition.name);
64
+ }
65
+ const callbackEnvironment = applicationEnvironments[0];
66
+ if (callbackEnvironment !== undefined) {
67
+ await this.#ensureCustomerEnvironment(applicationEnvironmentId(callbackEnvironment[1]), callbacks.registrations, request.signal);
68
+ }
69
+ const prepared = await prepareComponents([
70
+ model,
71
+ agentloop,
72
+ ...allComponentTools,
73
+ ...environmentComponents.map(([, environment]) => environment),
74
+ ]);
75
+ const modelBinding = prepared.bindings[0];
76
+ const agentloopBinding = prepared.bindings[1];
77
+ if (modelBinding === undefined || agentloopBinding === undefined) {
78
+ throw new TypeError("component session bindings are incomplete");
79
+ }
80
+ const toolBindings = prepared.bindings.slice(2, 2 + allComponentTools.length);
81
+ const environmentBindings = prepared.bindings.slice(2 + allComponentTools.length);
82
+ const toolItems = allComponentTools.map((tool, index) => {
83
+ const binding = toolBindings[index];
84
+ if (binding === undefined)
85
+ throw new TypeError("Tool component binding is missing");
86
+ const definition = toolDefinitions[index];
87
+ if (definition === undefined)
88
+ throw new TypeError("Tool component definition is missing");
89
+ const needsEnvironment = binding.grants.includes("environment");
90
+ const isCallback = index >= componentTools.length;
91
+ if (needsEnvironment && !isCallback && environmentComponents.length !== 1) {
92
+ throw new TypeError("a Tool with the environment grant requires exactly one declared Environment");
93
+ }
94
+ const environmentName = isCallback
95
+ ? callbackEnvironment?.[0]
96
+ : environmentComponents[0]?.[0];
97
+ return {
98
+ definition,
99
+ executor: {
100
+ kind: "component",
101
+ component_digest: binding.component_digest,
102
+ world: binding.world,
103
+ config: binding.config,
104
+ grants: binding.grants,
105
+ ...(needsEnvironment ? { environment: environmentName } : {}),
106
+ },
107
+ };
108
+ });
109
+ const environmentConfig = Object.fromEntries(environmentComponents.map(([name], index) => {
110
+ const binding = environmentBindings[index];
111
+ if (binding === undefined) {
112
+ throw new TypeError(`Environment component binding ${JSON.stringify(name)} is missing`);
113
+ }
114
+ return [name, {
115
+ component_digest: binding.component_digest,
116
+ world: binding.world,
117
+ config: binding.config,
118
+ }];
119
+ }));
36
120
  const body = {
37
121
  model: {
122
+ component_digest: modelBinding.component_digest,
123
+ world: modelBinding.world,
124
+ config: modelBinding.config,
38
125
  provider: options.model.provider,
39
126
  name: options.model.name,
40
127
  api_key: options.model.apiKey,
@@ -42,46 +129,24 @@ export class Sessions {
42
129
  ...(options.model.maxOutputTokens === undefined
43
130
  ? {}
44
131
  : { max_output_tokens: options.model.maxOutputTokens }),
45
- ...(options.model.contextWindowTokens === undefined
46
- ? {}
47
- : { context_window_tokens: options.model.contextWindowTokens }),
48
- ...(options.model.temperature === undefined ? {} : { temperature: options.model.temperature }),
49
- ...(options.model.reasoningEffort === undefined
50
- ? {}
51
- : { reasoning_effort: options.model.reasoningEffort }),
52
132
  },
53
- tools: {
54
- items: compiledTools.items,
133
+ agentloop: {
134
+ component_digest: agentloopBinding.component_digest,
135
+ world: agentloopBinding.world,
136
+ config: agentloopBinding.config,
55
137
  },
56
- ...(compiledTools.bundles.length === 0 ? {} : { tool_bundles: compiledTools.bundles }),
57
- ...(options.agentloop === undefined
58
- ? {}
59
- : {
60
- agentloop: {
61
- source_bundle_sha256: options.agentloop.sha256,
62
- toolchain: options.agentloop.toolchain,
63
- bundle_base64: encodeBase64(new TextEncoder().encode(options.agentloop.source)),
64
- },
65
- }),
138
+ component_artifacts: prepared.artifacts,
139
+ tools: { items: toolItems },
140
+ ...(environmentComponents.length === 0 ? {} : { environments: environmentConfig }),
66
141
  ...(options.secrets === undefined ? {} : { secrets: options.secrets }),
67
- ...(options.systemPrompt === undefined ? {} : { system_prompt: options.systemPrompt }),
68
142
  ...(options.metadata === undefined ? {} : { metadata: options.metadata }),
69
- ...(options.network === undefined
143
+ ...(options.retainUntil === undefined
70
144
  ? {}
71
- : { network: options.network }),
145
+ : { retain_until: normalizeTimestamp(options.retainUntil, "retainUntil") }),
146
+ ...(options.network === undefined ? {} : { network: options.network }),
72
147
  ...(options.providerRecoveryRetries === undefined
73
148
  ? {}
74
149
  : { provider_recovery_retries: options.providerRecoveryRetries }),
75
- ...(this.#clientId === undefined
76
- ? {}
77
- : {
78
- client: {
79
- id: this.#clientId,
80
- ...(options.client?.submitRetries === undefined
81
- ? {}
82
- : { submit_retries: options.client.submitRetries }),
83
- },
84
- }),
85
150
  ...(options.children === undefined
86
151
  ? {}
87
152
  : {
@@ -128,29 +193,29 @@ export class Sessions {
128
193
  ...(list.next_cursor === undefined ? {} : { nextCursor: list.next_cursor }),
129
194
  };
130
195
  }
131
- async #ensureCustomerHand(registrations, signal) {
196
+ async #ensureCustomerEnvironment(clientId, registrations, signal) {
132
197
  if (this.#closed)
133
198
  throw new SessionError("Aex client is closed");
134
199
  if (registrations.length === 0)
135
200
  return;
136
- if (this.#clientId === undefined) {
137
- throw new TypeError("Customer-app Tools require Aex({ client: { id } })");
138
- }
201
+ if (clientId === undefined)
202
+ throw new TypeError("Callback Tools require an app() environment");
139
203
  if (this.#webSocketFactory === undefined) {
140
204
  throw new TypeError("This runtime does not provide WebSocket; pass webSocketFactory to Aex");
141
205
  }
142
- if (this.#customerHand === undefined) {
206
+ const existing = this.#customerEnvironments.get(clientId);
207
+ if (existing === undefined) {
143
208
  let partial;
144
209
  const starting = (async () => {
145
210
  try {
146
- partial = new CustomerHand(async () => {
147
- const grant = await this.#transport.customerHandGrant(this.#clientId);
211
+ partial = new CustomerEnvironment(async () => {
212
+ const grant = await this.#transport.customerEnvironmentGrant(clientId);
148
213
  return {
149
214
  request: { url: grant.url, protocol: grant.protocol },
150
- observe: (observation) => this.#transport.customerHandObserve(grant.observationUrl, grant.observationToken, observation),
215
+ observe: (observation) => this.#transport.customerEnvironmentObserve(grant.observationUrl, grant.observationToken, observation),
151
216
  };
152
- }, registrations, this.#webSocketFactory, { clientId: this.#clientId });
153
- this.#customerHandInstance = partial;
217
+ }, registrations, this.#webSocketFactory, { clientId });
218
+ this.#customerEnvironmentInstances.set(clientId, partial);
154
219
  await partial.ready;
155
220
  if (this.#closed) {
156
221
  partial.close();
@@ -163,12 +228,13 @@ export class Sessions {
163
228
  throw error;
164
229
  }
165
230
  })();
166
- this.#customerHand = starting;
231
+ this.#customerEnvironments.set(clientId, starting);
167
232
  void starting.catch(() => {
168
- if (this.#customerHand === starting) {
169
- this.#customerHand = undefined;
170
- if (this.#customerHandInstance === partial)
171
- this.#customerHandInstance = undefined;
233
+ if (this.#customerEnvironments.get(clientId) === starting) {
234
+ this.#customerEnvironments.delete(clientId);
235
+ if (this.#customerEnvironmentInstances.get(clientId) === partial) {
236
+ this.#customerEnvironmentInstances.delete(clientId);
237
+ }
172
238
  }
173
239
  });
174
240
  // The request may stop waiting, but the process-scoped runner remains reconnectable for
@@ -176,12 +242,48 @@ export class Sessions {
176
242
  await waitWithSignal(starting, signal);
177
243
  return;
178
244
  }
179
- const hand = await waitWithSignal(this.#customerHand, signal);
245
+ const hand = await waitWithSignal(existing, signal);
180
246
  if (this.#closed)
181
247
  throw new SessionError("Aex client is closed");
182
248
  await waitWithSignal(hand.register(registrations), signal);
183
249
  }
184
250
  }
251
+ function isApplicationEnvironment(value) {
252
+ const config = value.config;
253
+ return config !== null && typeof config === "object" && !Array.isArray(config) &&
254
+ config.driver === "customer";
255
+ }
256
+ function applicationEnvironmentId(value) {
257
+ const config = value.config;
258
+ const id = config.configuration?.registration;
259
+ if (typeof id !== "string" || !/^[A-Za-z0-9_.:-]{1,128}$/u.test(id)) {
260
+ throw new TypeError("app() Environment registration is invalid");
261
+ }
262
+ return id;
263
+ }
264
+ function isComponentTool(value) {
265
+ return value !== null
266
+ && typeof value === "object"
267
+ && value.kind === "brain.component"
268
+ && value.extension === "tool";
269
+ }
270
+ function isEnvironmentComponent(value) {
271
+ return value !== null
272
+ && typeof value === "object"
273
+ && value.kind === "brain.component"
274
+ && value.extension === "environment";
275
+ }
276
+ function componentToolDefinition(value) {
277
+ const config = value.config;
278
+ if (config === null || typeof config !== "object" || Array.isArray(config)) {
279
+ throw new TypeError("Tool component config must be an object containing definition");
280
+ }
281
+ const definition = config.definition;
282
+ if (definition === null || typeof definition !== "object" || Array.isArray(definition)) {
283
+ throw new TypeError("Tool component config.definition is required");
284
+ }
285
+ return definition;
286
+ }
185
287
  function waitWithSignal(promise, signal) {
186
288
  if (signal === undefined)
187
289
  return promise;
@@ -242,6 +344,9 @@ export class Session {
242
344
  get updatedAt() {
243
345
  return this.#data.updated_at;
244
346
  }
347
+ get retainUntil() {
348
+ return this.#data.retain_until;
349
+ }
245
350
  get metadata() {
246
351
  return this.#data.metadata;
247
352
  }
@@ -343,6 +448,25 @@ export class Session {
343
448
  this.#data = await this.#transport.json("POST", `/v1/sessions/${encodeURIComponent(this.id)}/cancel`, { signal: options.signal, retry: true });
344
449
  return this;
345
450
  }
451
+ async suspend(options = {}) {
452
+ this.#data = await this.#transport.json("POST", `/v1/sessions/${encodeURIComponent(this.id)}/suspend`, { signal: options.signal, retry: true });
453
+ return this;
454
+ }
455
+ async resume(options = {}) {
456
+ this.#data = await this.#transport.json("POST", `/v1/sessions/${encodeURIComponent(this.id)}/resume`, { signal: options.signal, retry: true });
457
+ return this;
458
+ }
459
+ async setRetention(value, options = {}) {
460
+ this.#data = await this.#transport.json("POST", `/v1/sessions/${encodeURIComponent(this.id)}/retention`, {
461
+ body: {
462
+ retain_until: normalizeTimestamp(value, "retainUntil"),
463
+ allow_shorten: options.allowShorten ?? false,
464
+ },
465
+ signal: options.signal,
466
+ retry: true,
467
+ });
468
+ return this;
469
+ }
346
470
  async end(options = {}) {
347
471
  this.#data = await this.#transport.json("POST", `/v1/sessions/${encodeURIComponent(this.id)}/end`, { signal: options.signal, retry: true });
348
472
  return this;
@@ -361,6 +485,12 @@ export class Session {
361
485
  function isOutputOptions(options) {
362
486
  return "output" in options;
363
487
  }
488
+ function normalizeTimestamp(value, field) {
489
+ const parsed = value instanceof Date ? value : new Date(value);
490
+ if (!Number.isFinite(parsed.getTime()))
491
+ throw new TypeError(`${field} must be a valid timestamp`);
492
+ return parsed.toISOString();
493
+ }
364
494
  async function compileOutputSchema(schema, retries) {
365
495
  let jsonSchema;
366
496
  try {
package/dist/tools.d.ts CHANGED
@@ -1,2 +1,48 @@
1
- export { compileTools } from "@aexhq/brain";
2
- export type { ClientRegistration, CompiledTools, Tool } from "@aexhq/brain";
1
+ import type { ComponentExtension } from "@aexhq/brain";
2
+ import type { ToolDefinition } from "@aexhq/brain/session";
3
+ import * as z from "zod";
4
+ export type JsonSchema = Record<string, unknown>;
5
+ export interface ToolContext {
6
+ readonly signal: AbortSignal;
7
+ readonly operationId: string;
8
+ readonly sessionId: string;
9
+ readonly deadlineMs: number;
10
+ }
11
+ export type ToolHandler<Input extends z.ZodType, Output = unknown> = (input: z.output<Input>, context: ToolContext) => Output | Promise<Output>;
12
+ export interface ToolContract {
13
+ readonly name: string;
14
+ readonly description?: string;
15
+ readonly inputSchema: JsonSchema;
16
+ readonly outputSchema?: JsonSchema;
17
+ readonly contractDigest: string;
18
+ }
19
+ export interface Tool<Input extends z.ZodType = z.ZodType, Output = unknown> {
20
+ readonly kind: "aex.tool";
21
+ readonly name?: string;
22
+ readonly description?: string;
23
+ readonly input: Input;
24
+ readonly output?: z.ZodType;
25
+ readonly handler: ToolHandler<Input, Output>;
26
+ describe(description: string): Tool<Input, Output>;
27
+ named(name: string): Tool<Input, Output>;
28
+ returns<Schema extends z.ZodType>(schema: Schema): Tool<Input, z.output<Schema>>;
29
+ }
30
+ declare const EMPTY_INPUT: z.ZodObject<{}, z.core.$strip>;
31
+ export declare function tool<Output>(handler: ToolHandler<typeof EMPTY_INPUT, Output>): Tool<typeof EMPTY_INPUT, Output>;
32
+ export declare function tool<Input extends z.ZodType, Output>(input: Input, handler: ToolHandler<Input, Output>): Tool<Input, Output>;
33
+ export interface ClientRegistration {
34
+ readonly registration: string;
35
+ readonly name: string;
36
+ readonly contractDigest: string;
37
+ readonly input: z.ZodType;
38
+ readonly output?: z.ZodType;
39
+ readonly handler: ToolHandler<z.ZodType>;
40
+ }
41
+ export interface CompiledCallbacks {
42
+ readonly components: readonly ComponentExtension<"tool", Readonly<Record<string, unknown>> & {
43
+ readonly definition: ToolDefinition;
44
+ }>[];
45
+ readonly registrations: readonly ClientRegistration[];
46
+ }
47
+ export declare function compileCallbacks(selections: readonly Tool[]): Promise<CompiledCallbacks>;
48
+ export {};
package/dist/tools.js CHANGED
@@ -1 +1,119 @@
1
- export { compileTools } from "@aexhq/brain";
1
+ import { callback as callbackComponent } from "@aexhq/env-app";
2
+ import * as z from "zod";
3
+ import { jcsSha256 } from "./json.js";
4
+ const EMPTY_INPUT = z.object({});
5
+ const TOOL_NAME = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/u;
6
+ export function tool(inputOrHandler, maybeHandler) {
7
+ if (typeof inputOrHandler === "function") {
8
+ return makeTool({
9
+ ...(inputOrHandler.name === "" ? {} : { name: inputOrHandler.name }),
10
+ input: EMPTY_INPUT,
11
+ handler: inputOrHandler,
12
+ });
13
+ }
14
+ if (typeof maybeHandler !== "function")
15
+ throw new TypeError("tool(schema, handler) requires a function");
16
+ return makeTool({
17
+ ...(maybeHandler.name === "" ? {} : { name: maybeHandler.name }),
18
+ input: inputOrHandler,
19
+ handler: maybeHandler,
20
+ });
21
+ }
22
+ function makeTool(draft) {
23
+ return Object.freeze({
24
+ kind: "aex.tool",
25
+ ...(draft.name === undefined ? {} : { name: draft.name }),
26
+ ...(draft.description === undefined ? {} : { description: draft.description }),
27
+ input: draft.input,
28
+ ...(draft.output === undefined ? {} : { output: draft.output }),
29
+ handler: draft.handler,
30
+ describe(description) {
31
+ if (description.trim() === "" || description.length > 4096) {
32
+ throw new TypeError("Tool description must contain 1 through 4096 characters");
33
+ }
34
+ return makeTool({ ...draft, description });
35
+ },
36
+ named(name) {
37
+ assertToolName(name);
38
+ return makeTool({ ...draft, name });
39
+ },
40
+ returns(output) {
41
+ schemaOf(output, `${draft.name ?? "Tool"} output`);
42
+ return makeTool({ ...draft, output });
43
+ },
44
+ });
45
+ }
46
+ export async function compileCallbacks(selections) {
47
+ const components = [];
48
+ const registrations = [];
49
+ const names = new Set();
50
+ for (const value of selections) {
51
+ assertTool(value);
52
+ const contract = await compileContract(value);
53
+ if (names.has(contract.name)) {
54
+ throw new TypeError(`Tool ${JSON.stringify(contract.name)} was selected twice`);
55
+ }
56
+ names.add(contract.name);
57
+ const definition = {
58
+ name: contract.name,
59
+ ...(contract.description === undefined ? {} : { description: contract.description }),
60
+ input_schema: contract.inputSchema,
61
+ output_schema: contract.outputSchema ?? {},
62
+ contract_digest: contract.contractDigest,
63
+ };
64
+ const registration = `tool:${contract.contractDigest}`;
65
+ components.push(callbackComponent(definition, registration));
66
+ registrations.push(Object.freeze({
67
+ registration,
68
+ name: contract.name,
69
+ contractDigest: contract.contractDigest,
70
+ input: value.input,
71
+ ...(value.output === undefined ? {} : { output: value.output }),
72
+ handler: value.handler,
73
+ }));
74
+ }
75
+ return {
76
+ components: Object.freeze(components),
77
+ registrations: Object.freeze(registrations),
78
+ };
79
+ }
80
+ async function compileContract(value) {
81
+ const name = value.name;
82
+ if (name === undefined)
83
+ throw new TypeError("Tool functions must be named; use .named(name) when build tooling removes the name");
84
+ assertToolName(name);
85
+ const inputSchema = schemaOf(value.input, `${name} input`);
86
+ const outputSchema = value.output === undefined ? undefined : schemaOf(value.output, `${name} output`);
87
+ const canonical = {
88
+ name,
89
+ ...(value.description === undefined ? {} : { description: value.description }),
90
+ input_schema: inputSchema,
91
+ ...(outputSchema === undefined ? {} : { output_schema: outputSchema }),
92
+ };
93
+ return {
94
+ name,
95
+ ...(value.description === undefined ? {} : { description: value.description }),
96
+ inputSchema,
97
+ ...(outputSchema === undefined ? {} : { outputSchema }),
98
+ contractDigest: await jcsSha256(canonical),
99
+ };
100
+ }
101
+ function schemaOf(schema, label) {
102
+ try {
103
+ const value = z.toJSONSchema(schema, { target: "draft-2020-12", unrepresentable: "throw" });
104
+ if (Object.keys(value).length === 0)
105
+ throw new TypeError(`${label} schema is empty`);
106
+ return value;
107
+ }
108
+ catch (cause) {
109
+ throw new TypeError(`${label} cannot be represented as JSON Schema`, { cause });
110
+ }
111
+ }
112
+ function assertTool(value) {
113
+ if (value?.kind !== "aex.tool")
114
+ throw new TypeError("Tool selection is invalid");
115
+ }
116
+ function assertToolName(name) {
117
+ if (!TOOL_NAME.test(name))
118
+ throw new TypeError(`Invalid Tool name ${JSON.stringify(name)}`);
119
+ }
@@ -10,14 +10,14 @@ export declare class Transport {
10
10
  #private;
11
11
  readonly baseUrl: string;
12
12
  constructor(apiKey: string, baseUrl: string, fetchImplementation: Fetch);
13
- customerHandGrant(clientId: string, signal?: AbortSignal): Promise<{
13
+ customerEnvironmentGrant(clientId: string, signal?: AbortSignal): Promise<{
14
14
  url: string;
15
15
  protocol: string;
16
16
  expiresAt: string;
17
17
  observationUrl: string;
18
18
  observationToken: string;
19
19
  }>;
20
- customerHandObserve(url: string, token: string, observation: unknown): Promise<void>;
20
+ customerEnvironmentObserve(url: string, token: string, observation: unknown): Promise<void>;
21
21
  downloadTransfer(ticket: TransferTicket, signal?: AbortSignal, expectedBytes?: number): Promise<Uint8Array>;
22
22
  downloadTransferStream(ticket: TransferTicket, signal?: AbortSignal, expectedBytes?: number): Promise<ReadableStream<Uint8Array>>;
23
23
  uploadTransfer(ticket: TransferTicket, content: Uint8Array | (() => ReadableStream<Uint8Array>), bytes: number, signal?: AbortSignal): Promise<void>;
package/dist/transport.js CHANGED
@@ -2,7 +2,7 @@ import { MAX_CREATE_SESSION_REQUEST_BYTES, MAX_CUSTOMER_OBSERVATION_BYTES, MAX_M
2
2
  import { AbortError, AexError, SessionError, abortError, errorFromApi } from "./errors.js";
3
3
  const MAX_ORDINARY_JSON_BYTES = 2 * 1024 * 1024;
4
4
  const MAX_ERROR_RESPONSE_BYTES = 64 * 1024;
5
- const CUSTOMER_HAND_OBSERVATION_TIMEOUT_MS = 15_000;
5
+ const CUSTOMER_ENVIRONMENT_OBSERVATION_TIMEOUT_MS = 15_000;
6
6
  export class Transport {
7
7
  baseUrl;
8
8
  #apiKey;
@@ -15,9 +15,9 @@ export class Transport {
15
15
  this.baseUrl = baseUrl.slice(0, end);
16
16
  this.#fetch = fetchImplementation;
17
17
  }
18
- async customerHandGrant(clientId, signal) {
19
- const grant = await this.json("POST", "/v1/customer-hand/grants", { body: { client_id: clientId }, signal });
20
- const observationUrl = validateCustomerHandGrant(this.baseUrl, grant);
18
+ async customerEnvironmentGrant(clientId, signal) {
19
+ const grant = await this.json("POST", "/v1/customer-environment/grants", { body: { client_id: clientId }, signal });
20
+ const observationUrl = validateCustomerEnvironmentGrant(this.baseUrl, grant);
21
21
  return {
22
22
  url: grant.url,
23
23
  protocol: grant.protocol,
@@ -26,8 +26,8 @@ export class Transport {
26
26
  observationToken: grant.observation_token,
27
27
  };
28
28
  }
29
- async customerHandObserve(url, token, observation) {
30
- const body = encodeJsonOnce(observation, MAX_CUSTOMER_OBSERVATION_BYTES, "Customer Hand observation");
29
+ async customerEnvironmentObserve(url, token, observation) {
30
+ const body = encodeJsonOnce(observation, MAX_CUSTOMER_OBSERVATION_BYTES, "Customer Environment observation");
31
31
  const response = await this.#fetch(url, {
32
32
  method: "POST",
33
33
  redirect: "error",
@@ -37,13 +37,13 @@ export class Transport {
37
37
  "Content-Type": "application/json",
38
38
  },
39
39
  body,
40
- signal: AbortSignal.timeout(CUSTOMER_HAND_OBSERVATION_TIMEOUT_MS),
40
+ signal: AbortSignal.timeout(CUSTOMER_ENVIRONMENT_OBSERVATION_TIMEOUT_MS),
41
41
  });
42
42
  if (response.ok)
43
43
  return;
44
44
  const preview = await readResponseText(response, 4096)
45
45
  .catch(() => "<response too large or unreadable>");
46
- throw new SessionError(`Customer Hand observation ingress returned HTTP ${response.status}: ${preview}`, { status: response.status, requestId: response.headers.get("x-request-id") ?? undefined });
46
+ throw new SessionError(`Customer Environment observation ingress returned HTTP ${response.status}: ${preview}`, { status: response.status, requestId: response.headers.get("x-request-id") ?? undefined });
47
47
  }
48
48
  async downloadTransfer(ticket, signal, expectedBytes) {
49
49
  const stream = await this.downloadTransferStream(ticket, signal, expectedBytes);
@@ -419,15 +419,15 @@ function assertTransferTicket(ticket, method) {
419
419
  throw new SessionError("Aex file transfers require HTTPS (or loopback HTTP for development)");
420
420
  }
421
421
  }
422
- function validateCustomerHandGrant(baseUrl, grant) {
422
+ function validateCustomerEnvironmentGrant(baseUrl, grant) {
423
423
  if (!/^[A-Za-z0-9_.-]{1,128}$/u.test(grant.grant_id)) {
424
- throw new SessionError("Aex returned an invalid customer Hand grant id");
424
+ throw new SessionError("Aex returned an invalid customer Environment grant id");
425
425
  }
426
426
  if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]{1,2048}$/u.test(grant.protocol)) {
427
- throw new SessionError("Aex returned an invalid customer Hand WebSocket protocol");
427
+ throw new SessionError("Aex returned an invalid customer Environment WebSocket protocol");
428
428
  }
429
429
  if (!/^[\x21-\x7e]{1,2048}$/u.test(grant.observation_token)) {
430
- throw new SessionError("Aex returned an invalid customer Hand observation token");
430
+ throw new SessionError("Aex returned an invalid customer Environment observation token");
431
431
  }
432
432
  let socket;
433
433
  let observation;
@@ -435,19 +435,19 @@ function validateCustomerHandGrant(baseUrl, grant) {
435
435
  try {
436
436
  socket = new URL(grant.url);
437
437
  observation = new URL(grant.observation_url);
438
- expectedObservation = new URL(`${baseUrl}/v1/customer-hand/observations/${encodeURIComponent(grant.grant_id)}`);
438
+ expectedObservation = new URL(`${baseUrl}/v1/customer-environment/observations/${encodeURIComponent(grant.grant_id)}`);
439
439
  }
440
440
  catch (cause) {
441
- throw new SessionError("Aex returned an invalid customer Hand URL", { cause });
441
+ throw new SessionError("Aex returned an invalid customer Environment URL", { cause });
442
442
  }
443
443
  const secureSocket = socket.protocol === "wss:" ||
444
444
  (socket.protocol === "ws:" && isLoopback(socket.hostname));
445
445
  if (!secureSocket || socket.username !== "" || socket.password !== "" ||
446
446
  socket.search !== "" || socket.hash !== "") {
447
- throw new SessionError("Aex customer Hand sockets require credential-free WSS (or loopback WS for development)");
447
+ throw new SessionError("Aex customer Environment sockets require credential-free WSS (or loopback WS for development)");
448
448
  }
449
449
  if (observation.href !== expectedObservation.href) {
450
- throw new SessionError("Aex returned an unsafe customer Hand observation URL");
450
+ throw new SessionError("Aex returned an unsafe customer Environment observation URL");
451
451
  }
452
452
  if (observation.href.includes(grant.observation_token)) {
453
453
  throw new SessionError("Aex returned an observation URL containing its bearer token");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexhq/sdk",
3
- "version": "0.57.5",
3
+ "version": "0.59.0",
4
4
  "description": "TypeScript SDK for Aex sessions",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://aex.dev",
@@ -34,7 +34,8 @@
34
34
  "test": "tsc -p tsconfig.json && node --test"
35
35
  },
36
36
  "dependencies": {
37
- "@aexhq/brain": "0.3.0"
37
+ "@aexhq/brain": "0.3.1",
38
+ "@aexhq/env-app": "0.2.0"
38
39
  },
39
40
  "peerDependencies": {
40
41
  "zod": "4.4.3"