@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 +45 -87
- package/dist/index.d.ts +6 -9
- package/dist/index.js +3 -7
- package/dist/resources.d.ts +2 -0
- package/dist/resources.js +4 -4
- package/dist/session.d.ts +26 -35
- package/dist/session.js +193 -63
- package/dist/tools.d.ts +48 -2
- package/dist/tools.js +119 -1
- package/dist/transport.d.ts +2 -2
- package/dist/transport.js +16 -16
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,105 +1,63 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @aexhq/sdk
|
|
2
2
|
|
|
3
|
-
TypeScript
|
|
3
|
+
TypeScript SDK for Aex sessions and tool extensions.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
|
-
import { Aex
|
|
7
|
-
import {
|
|
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
|
|
10
|
-
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
28
|
-
output: z.object({ answer: z.string() }),
|
|
29
|
-
});
|
|
27
|
+
console.log(await session.send("Inspect the workspace."));
|
|
30
28
|
```
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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 {
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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 {
|
|
8
|
-
export {
|
|
9
|
-
export type { BinarySource, ChildSummary, IdempotentOperationOptions, OperationOptions, PageOptions,
|
|
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 "
|
|
12
|
-
export type {
|
|
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 {
|
|
6
|
-
export { tool } from "
|
|
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
|
|
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() {
|
package/dist/resources.d.ts
CHANGED
|
@@ -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.
|
|
162
|
+
return storageObject(await this.#inner.copyFromEnvironment(input.environment, {
|
|
163
163
|
key: input.key,
|
|
164
164
|
path: input.path,
|
|
165
|
-
|
|
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.
|
|
170
|
+
return file(await this.#inner.copyToEnvironment(input.environment, {
|
|
171
171
|
key: input.key,
|
|
172
172
|
path: input.path,
|
|
173
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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:
|
|
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
|
|
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 {
|
|
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 {
|
|
6
|
-
import {
|
|
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
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#customerHandInstance;
|
|
10
|
+
#customerEnvironments = new Map();
|
|
11
|
+
#customerEnvironmentInstances = new Map();
|
|
13
12
|
#closed = false;
|
|
14
|
-
constructor(transport, webSocketFactory
|
|
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.#
|
|
25
|
-
|
|
26
|
-
this.#
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
133
|
+
agentloop: {
|
|
134
|
+
component_digest: agentloopBinding.component_digest,
|
|
135
|
+
world: agentloopBinding.world,
|
|
136
|
+
config: agentloopBinding.config,
|
|
55
137
|
},
|
|
56
|
-
|
|
57
|
-
|
|
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.
|
|
143
|
+
...(options.retainUntil === undefined
|
|
70
144
|
? {}
|
|
71
|
-
: {
|
|
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 #
|
|
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 (
|
|
137
|
-
throw new TypeError("
|
|
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
|
-
|
|
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
|
|
147
|
-
const grant = await this.#transport.
|
|
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.
|
|
215
|
+
observe: (observation) => this.#transport.customerEnvironmentObserve(grant.observationUrl, grant.observationToken, observation),
|
|
151
216
|
};
|
|
152
|
-
}, registrations, this.#webSocketFactory, { clientId
|
|
153
|
-
this.#
|
|
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.#
|
|
231
|
+
this.#customerEnvironments.set(clientId, starting);
|
|
167
232
|
void starting.catch(() => {
|
|
168
|
-
if (this.#
|
|
169
|
-
this.#
|
|
170
|
-
if (this.#
|
|
171
|
-
this.#
|
|
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(
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/transport.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
19
|
-
const grant = await this.json("POST", "/v1/customer-
|
|
20
|
-
const observationUrl =
|
|
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
|
|
30
|
-
const body = encodeJsonOnce(observation, MAX_CUSTOMER_OBSERVATION_BYTES, "Customer
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
37
|
+
"@aexhq/brain": "0.3.1",
|
|
38
|
+
"@aexhq/env-app": "0.2.0"
|
|
38
39
|
},
|
|
39
40
|
"peerDependencies": {
|
|
40
41
|
"zod": "4.4.3"
|