@aexhq/sdk 0.33.1 → 0.35.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.
Files changed (81) hide show
  1. package/README.md +19 -27
  2. package/dist/_contracts/operations.d.ts +2 -54
  3. package/dist/_contracts/operations.js +2 -87
  4. package/dist/_contracts/run-config.d.ts +19 -13
  5. package/dist/_contracts/run-config.js +6 -33
  6. package/dist/_contracts/run-unit.d.ts +1 -33
  7. package/dist/_contracts/run-unit.js +2 -21
  8. package/dist/_contracts/runtime-sizes.d.ts +2 -2
  9. package/dist/_contracts/runtime-sizes.js +2 -2
  10. package/dist/_contracts/status.d.ts +2 -2
  11. package/dist/_contracts/status.js +3 -0
  12. package/dist/_contracts/submission.d.ts +80 -41
  13. package/dist/_contracts/submission.js +114 -52
  14. package/dist/agents-md.d.ts +5 -5
  15. package/dist/agents-md.js +7 -7
  16. package/dist/agents-md.js.map +1 -1
  17. package/dist/asset-upload.d.ts +4 -4
  18. package/dist/asset-upload.js +4 -4
  19. package/dist/bundle.d.ts +2 -2
  20. package/dist/bundle.js +2 -2
  21. package/dist/cli.mjs +369 -12918
  22. package/dist/cli.mjs.sha256 +1 -1
  23. package/dist/client.d.ts +234 -383
  24. package/dist/client.js +436 -648
  25. package/dist/client.js.map +1 -1
  26. package/dist/data-tools.d.ts +25 -22
  27. package/dist/data-tools.js +75 -62
  28. package/dist/data-tools.js.map +1 -1
  29. package/dist/fetch-archive.js +16 -16
  30. package/dist/fetch-archive.js.map +1 -1
  31. package/dist/file.d.ts +5 -5
  32. package/dist/file.js +7 -7
  33. package/dist/file.js.map +1 -1
  34. package/dist/index.d.ts +11 -9
  35. package/dist/index.js +20 -13
  36. package/dist/index.js.map +1 -1
  37. package/dist/mcp-server.d.ts +4 -4
  38. package/dist/mcp-server.js +4 -4
  39. package/dist/proxy-endpoint.d.ts +4 -4
  40. package/dist/proxy-endpoint.js +1 -1
  41. package/dist/retry.d.ts +162 -0
  42. package/dist/retry.js +320 -0
  43. package/dist/retry.js.map +1 -0
  44. package/dist/secret.d.ts +8 -8
  45. package/dist/secret.js +8 -8
  46. package/dist/secret.js.map +1 -1
  47. package/dist/skill-tool.d.ts +102 -0
  48. package/dist/skill-tool.js +190 -0
  49. package/dist/skill-tool.js.map +1 -0
  50. package/dist/tool.d.ts +1 -1
  51. package/dist/tool.js +3 -3
  52. package/dist/tool.js.map +1 -1
  53. package/dist/version.d.ts +1 -1
  54. package/dist/version.js +1 -1
  55. package/docs/cleanup.md +3 -3
  56. package/docs/concepts/agent-tools.md +6 -25
  57. package/docs/concepts/composition.md +15 -12
  58. package/docs/concepts/providers-and-runtimes.md +3 -3
  59. package/docs/concepts/runs.md +27 -22
  60. package/docs/credentials.md +52 -84
  61. package/docs/defaults.md +6 -6
  62. package/docs/events.md +65 -44
  63. package/docs/limits-and-quotas.md +3 -4
  64. package/docs/mcp.md +3 -3
  65. package/docs/networking.md +8 -8
  66. package/docs/outputs.md +44 -40
  67. package/docs/provider-runtime-capabilities.md +1 -1
  68. package/docs/public-surface.json +2 -2
  69. package/docs/quickstart.md +20 -10
  70. package/docs/retries.md +129 -0
  71. package/docs/run-config.md +12 -14
  72. package/docs/run-record.md +8 -8
  73. package/docs/secrets.md +16 -26
  74. package/docs/skills.md +55 -110
  75. package/docs/vision-skills.md +29 -40
  76. package/examples/chat-corpus.ts +8 -9
  77. package/examples/feature-tour.ts +301 -0
  78. package/package.json +1 -1
  79. package/dist/skill.d.ts +0 -149
  80. package/dist/skill.js +0 -198
  81. package/dist/skill.js.map +0 -1
@@ -8,30 +8,28 @@ aex treats provider keys, MCP credentials, and proxy endpoint auth as per-run
8
8
  credentials. Reusable env secrets are documented separately in
9
9
  [Secrets](secrets.md).
10
10
 
11
- The caller passes a workspace-scoped SDK token and the provider key inline on every `submit` call. aex holds the bundle in run-scoped custody for the run lifecycle and attempts terminal cleanup/revocation for the aex-controlled references. MCP credentials and proxy endpoint auth values travel the same way.
11
+ The caller passes a workspace-scoped SDK token and the provider key inline on every `openSession` / `run` call. aex holds the bundle in run-scoped custody for the session lifecycle and attempts terminal cleanup/revocation for the aex-controlled references. MCP credentials and proxy endpoint auth values travel the same way.
12
12
 
13
- A run selects one upstream `provider` (default `anthropic`) and must carry a BYOK
14
- key for it. Keys are supplied per-provider so a run can also hold keys for the
13
+ A session selects one upstream `provider` (default `anthropic`) and must carry a BYOK
14
+ key for it. Keys are supplied per-provider so a session can also hold keys for the
15
15
  **other** providers its subagents may use:
16
16
 
17
17
  | Field | Required secret |
18
18
  | --- | --- |
19
- | Provider API keys | `secrets.apiKeys` (keyed by provider) |
19
+ | Provider API keys | `apiKeys` (top-level, keyed by provider) |
20
20
 
21
21
  ```ts
22
- // The run's own provider key, plus extra keys its subagents can use.
23
- secrets: {
24
- apiKeys: {
25
- anthropic: process.env.ANTHROPIC_API_KEY!, // the run's provider
26
- deepseek: process.env.DEEPSEEK_API_KEY! // for a cross-provider subagent
27
- }
22
+ // The session's own provider key, plus extra keys its subagents can use.
23
+ apiKeys: {
24
+ anthropic: process.env.ANTHROPIC_API_KEY!, // the session's provider
25
+ deepseek: process.env.DEEPSEEK_API_KEY! // for a cross-provider subagent
28
26
  }
29
27
  ```
30
28
 
31
29
  A `subagent` spawned with a different-family model **inherits the parent's keys
32
- server-side** from the run's vaulted bundle — the keys never transit the
33
- container. If the parent holds no key for the child's provider, the child submit
34
- is rejected with `parent_missing_provider_key`.
30
+ server-side** from the session's vaulted bundle — the keys never transit the
31
+ container. If the parent holds no key for the child's provider, the child is
32
+ rejected with `parent_missing_provider_key`.
35
33
 
36
34
  MCP credential types:
37
35
 
@@ -50,64 +48,46 @@ For managed-runtime runs, aex injects the matching BYOK provider key at the host
50
48
 
51
49
  Some skills need to call non-MCP HTTP services (e.g. Stripe, internal APIs). Embedding the credential in the skill content puts the raw secret on disk in the agent container and in the model's context — both prompt-injection-readable.
52
50
 
53
- The platform's managed HTTP proxy is the agent-first alternative. The caller declares **policy** at the top level of the submission (hashed for idempotency) and supplies the matching **auth value** inside `secrets` (not hashed, so key rotation does not collapse onto a stale run). The raw credential value never enters the container.
51
+ The platform's managed HTTP proxy is the agent-first alternative. Declare each endpoint with a `ProxyEndpoint.*` constructor: the instance carries the non-secret **policy** (hashed for idempotency) and its **auth token** together at the call site. The SDK splits the token into the vaulted secrets channel server-side (not hashed, so key rotation does not collapse onto a stale run), and the raw credential value never enters the container.
54
52
 
55
53
  ```ts
56
- import {
57
- AgentExecutor,
58
- Models,
59
- validateProxyAuth,
60
- buildPlatformAllowedHosts
61
- } from "@aexhq/sdk";
62
-
63
- const aex = new AgentExecutor({
54
+ import { Aex, Models, ProxyEndpoint } from "@aexhq/sdk";
55
+
56
+ const aex = new Aex({
64
57
  apiToken: "ant_..."
65
58
  });
66
59
 
67
- const proxyEndpoints = [
68
- {
69
- name: "stripe",
70
- baseUrl: "https://api.stripe.com",
71
- authShape: { type: "bearer" },
72
- allowMethods: ["GET", "POST"],
73
- allowPathPrefixes: ["/v1/charges", "/v1/refunds"],
74
- maxRequestBytes: 65_536,
75
- maxResponseBytes: 65_536,
76
- timeoutMs: 10_000,
77
- responseMode: "headers_only",
78
- retry: {
79
- maxAttempts: 3,
80
- initialDelayMs: 250,
81
- maxDelayMs: 5000,
82
- jitter: "full",
83
- retryOnStatuses: [408, 425, 429, 500, 502, 503, 504],
84
- retryOnMethods: ["GET", "HEAD"],
85
- respectRetryAfter: true
86
- }
60
+ const stripe = ProxyEndpoint.bearer({
61
+ name: "stripe",
62
+ baseUrl: "https://api.stripe.com",
63
+ token: process.env.STRIPE_API_KEY!,
64
+ allowMethods: ["GET", "POST"],
65
+ allowPathPrefixes: ["/v1/charges", "/v1/refunds"],
66
+ maxRequestBytes: 65_536,
67
+ maxResponseBytes: 65_536,
68
+ timeoutMs: 10_000,
69
+ responseMode: "headers_only",
70
+ retry: {
71
+ maxAttempts: 3,
72
+ initialDelayMs: 250,
73
+ maxDelayMs: 5000,
74
+ jitter: "full",
75
+ retryOnStatuses: [408, 425, 429, 500, 502, 503, 504],
76
+ retryOnMethods: ["GET", "HEAD"],
77
+ respectRetryAfter: true
87
78
  }
88
- ] as const;
89
-
90
- const proxyEndpointAuth = [
91
- {
92
- name: "stripe",
93
- value: { type: "bearer", token: process.env.STRIPE_API_KEY! }
94
- }
95
- ] as const;
96
-
97
- // Fail fast at submission time when policy and auth disagree.
98
- validateProxyAuth(proxyEndpoints, proxyEndpointAuth);
79
+ });
99
80
 
100
- const runId = await aex.submit({
81
+ await aex.run({
101
82
  model: Models.CLAUDE_HAIKU_4_5,
102
- prompt: "…",
103
- proxyEndpoints,
104
- secrets: {
105
- apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! },
106
- proxyEndpointAuth
107
- }
83
+ message: "…",
84
+ proxyEndpoints: [stripe],
85
+ apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! }
108
86
  });
109
87
  ```
110
88
 
89
+ The five constructors — `ProxyEndpoint.none` / `bearer` / `header` / `basic` / `query` — put the auth secret on the same call as the policy, so any drift (wrong `responseMode`, misnamed auth field) is a TypeScript error at the call site instead of an HTTP 400 a round-trip later.
90
+
111
91
  Inside the run container, every session has the platform CLI mounted at `/mnt/session/uploads/aex/aex` (a Bun-compatible ESM bundle) and a manifest at `/mnt/session/uploads/aex/index.json` describing the declared endpoints. The skill invokes the CLI through `bun` (the mount has no execute permission so direct invocation fails with `bad interpreter: Permission denied`):
112
92
 
113
93
  ```bash
@@ -123,40 +103,28 @@ Retries are declaration-based. Add `retry` to the endpoint policy when safe for
123
103
 
124
104
  #### Keyless upstreams (`authShape: { type: "none" }`)
125
105
 
126
- For public APIs that take no credential (Wikimedia Commons, Internet Archive, Library of Congress, NASA Images, NARA, GDELT, etc.), declare the endpoint with `authShape: { type: "none" }` and omit the matching `proxyEndpointAuth[]` entry entirely:
127
-
128
- ```ts
129
- const proxyEndpoints = [
130
- {
131
- name: "wikimedia",
132
- baseUrl: "https://commons.wikimedia.org",
133
- authShape: { type: "none" },
134
- allowMethods: ["GET"],
135
- allowPathPrefixes: ["/wiki/", "/w/api.php"]
136
- }
137
- ] as const;
138
-
139
- const runId = await aex.submit({
140
- model: Models.CLAUDE_HAIKU_4_5,
141
- prompt: "…",
142
- proxyEndpoints,
143
- secrets: { apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! } }
144
- });
145
- ```
146
-
147
- The keyless endpoint still routes through the aex managed proxy: every call is allow-listed, audited, and redacted. The hosted proxy injects no `Authorization` header and no query-string credential. Shipping a `proxyEndpointAuth` entry for a `none`-shape endpoint is rejected at submission time. Equivalent class-based form:
106
+ For public APIs that take no credential (Wikimedia Commons, Internet Archive, Library of Congress, NASA Images, NARA, GDELT, etc.), declare the endpoint with `ProxyEndpoint.none(...)` it produces only a declaration, no auth token:
148
107
 
149
108
  ```ts
150
109
  import { ProxyEndpoint } from "@aexhq/sdk";
151
110
 
152
- ProxyEndpoint.none({
111
+ const wikimedia = ProxyEndpoint.none({
153
112
  name: "wikimedia",
154
113
  baseUrl: "https://commons.wikimedia.org",
155
114
  allowMethods: ["GET"],
156
115
  allowPathPrefixes: ["/wiki/", "/w/api.php"]
157
116
  });
117
+
118
+ await aex.run({
119
+ model: Models.CLAUDE_HAIKU_4_5,
120
+ message: "…",
121
+ proxyEndpoints: [wikimedia],
122
+ apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! }
123
+ });
158
124
  ```
159
125
 
126
+ The keyless endpoint still routes through the aex managed proxy: every call is allow-listed, audited, and redacted. The hosted proxy injects no `Authorization` header and no query-string credential.
127
+
160
128
  `bun /mnt/session/uploads/aex/aex --help` reads endpoint details from `/mnt/session/uploads/aex/index.json`. Runs that do not declare any `proxyEndpoints` still have the CLI and an empty manifest mounted, so agents never need to introspect whether the surface exists.
161
129
 
162
130
  ### Networking
@@ -174,4 +142,4 @@ const allowedHosts = buildPlatformAllowedHosts({
174
142
 
175
143
  ### Secrets are always explicit at the call site
176
144
 
177
- There is no `defaultSecrets` and no client-held secret state. Every `submit` call carries its full `secrets` bundle (one provider key + optional MCP credentials + optional `proxyEndpointAuth`). This is the agent-first invariant: the credentials being used on any given call are visible in the same line of code that submits the run.
145
+ There is no `defaultSecrets` and no client-held secret state. Every `openSession` / `run` call carries its own credentials at the call site: the top-level `apiKeys` map (one provider key, plus any subagent keys), MCP auth on each `McpServer` instance, and proxy auth on each `ProxyEndpoint` instance. This is the agent-first invariant: the credentials being used on any given call are visible in the same code that opens the session.
package/docs/defaults.md CHANGED
@@ -21,9 +21,9 @@ For the hard ceilings and who can raise them, see
21
21
 
22
22
  | Option | Default | How to override | Source |
23
23
  | --- | --- | --- | --- |
24
- | `timeout` (run deadline) | 1 hour | Per-run via `options.timeout` (e.g. `"30m"`, `"2h"`), clamped to the run-timeout floor/ceiling. | `RUN_DEFAULT_TIMEOUT_MS` |
25
- | `runtimeSize` (machine size) | `shared-0.25x-1gb` — 0.25 vCPU, 1 GB | Per-run via `options.runtimeSize` (use `RuntimeSizes.*` in TypeScript). | `RUN_DEFAULT_RUNTIME_SIZE` |
26
- | `limits.maxSpendUsd` (per-run spend cap) | None — no per-run spend cap (the run is still bounded by its `timeout` and any workspace-level cap) | Per-run via `options.limits.maxSpendUsd` (a positive USD amount); the run is stopped once its spend would exceed the cap. | — |
24
+ | `timeout` (run deadline) | 1 hour | Per-session via `overrides.timeout` (e.g. `"30m"`, `"2h"`), clamped to the run-timeout floor/ceiling. | `RUN_DEFAULT_TIMEOUT_MS` |
25
+ | `runtime` (machine size) | `shared-0.25x-1gb` — 0.25 vCPU, 1 GB | Per-session via `runtime` (use `Sizes.*` in TypeScript). | `RUN_DEFAULT_RUNTIME_SIZE` |
26
+ | `overrides.maxSpendUsd` (per-session spend cap) | None — no spend cap (the session is still bounded by its `timeout` and any workspace-level cap) | Per-session via `overrides.maxSpendUsd` (a positive USD amount); the session is stopped once its spend would exceed the cap. | — |
27
27
 
28
28
  ## Tools
29
29
 
@@ -51,15 +51,15 @@ For the hard ceilings and who can raise them, see
51
51
 
52
52
  | Option | Default | How to override | Source |
53
53
  | --- | --- | --- | --- |
54
- | Output link / signed-URL TTL | 300 seconds (5 minutes) at the storage layer; `outputLink(...)` defaults to `"1h"` | Per-call via `expiresSeconds` (storage) or `expiresIn` on `outputLink` / `fetchOutput`. | `REQUEST_PRESIGN_URL_DEFAULT_TTL_SECONDS` |
54
+ | Output link / signed-URL TTL | 300 seconds (5 minutes) at the storage layer; `session.outputs().link(...)` defaults to `"1h"` | Per-call via `expiresSeconds` (storage) or `expiresIn` on `session.outputs().link` / `session.outputs().fetch`. | `REQUEST_PRESIGN_URL_DEFAULT_TTL_SECONDS` |
55
55
  | Event-stream connection ticket TTL | 60 seconds | Per-mint via the `ttlMs` argument. | `REQUEST_TICKET_DEFAULT_TTL_MS` |
56
56
 
57
57
  ## Subagents
58
58
 
59
59
  | Option | Default | How to override | Source |
60
60
  | --- | --- | --- | --- |
61
- | Concurrent child runs per lineage root | 1000 (live, non-terminal child runs) | Per-run via `options.limits.maxConcurrentChildRuns`, clamped to the 4096 platform ceiling. | `RUN_DEFAULT_MAX_CONCURRENT_CHILD_RUNS` |
62
- | Max subagent depth | 5 | Per-run via `options.limits.maxSubagentDepth`, clamped to the same hard ceiling. | `RUN_MAX_PUBLIC_SUBAGENT_DEPTH` |
61
+ | Concurrent child runs per lineage root | 1000 (live, non-terminal child runs) | Platform default (subagents run in-process; no public per-run override). Hard ceiling 4096. | `RUN_DEFAULT_MAX_CONCURRENT_CHILD_RUNS` |
62
+ | Max subagent depth | 5 | Platform default (subagents run in-process; no public per-run override). | `RUN_MAX_PUBLIC_SUBAGENT_DEPTH` |
63
63
 
64
64
  ## Workspace
65
65
 
package/docs/events.md CHANGED
@@ -12,15 +12,20 @@ tool-approval hook.
12
12
 
13
13
  ## Two ways to consume events
14
14
 
15
+ A session's reads and streams are grouped under accessor sub-resources:
16
+ `session.events()` owns the event timeline, `session.messages()` owns the decoded
17
+ assistant text, and `session.outputs()` owns the captured files. Reach a verb by
18
+ chaining it off the accessor.
19
+
15
20
  ```ts
16
21
  // Pull a snapshot of every event captured so far.
17
- const events = await aex.events(runId);
22
+ const events = await session.events().list();
18
23
  ```
19
24
 
20
25
  ```ts
21
26
  // Stream the RunEvent snapshot shape: yields each event once, stops when the
22
- // run reaches a terminal status. Backed by polling the aex events endpoint.
23
- for await (const event of aex.stream(runId, { intervalMs: 1000 })) {
27
+ // session parks. Backed by polling the aex events endpoint.
28
+ for await (const event of session.events().stream({ intervalMs: 1000 })) {
24
29
  if (event.type === "TEXT_MESSAGE_CONTENT") {
25
30
  // ...
26
31
  }
@@ -30,42 +35,57 @@ for await (const event of aex.stream(runId, { intervalMs: 1000 })) {
30
35
  For the canonical event envelope, use the coordinator WebSocket stream:
31
36
 
32
37
  ```ts
33
- for await (const event of aex.streamEnvelopes(runId, { from: 0 })) {
38
+ for await (const event of session.events().streamEnvelopes({ from: 0 })) {
34
39
  console.log(event.sequence, event.type, event.source);
35
40
  }
36
41
  ```
37
42
 
38
- `streamEnvelopes()` uses a short-lived ticket minted by the hosted API, then subscribes directly to the per-run coordinator. Subscribe means read-from-cursor plus tail: reconnects resume from the last sequence.
43
+ `session.events().streamEnvelopes()` uses a short-lived ticket minted by the hosted API, then subscribes directly to the per-session coordinator. Subscribe means read-from-cursor plus tail: reconnects resume from the last sequence.
44
+
45
+ ## Assistant text
46
+
47
+ To collect just the agent's assistant messages, use the `messages()` accessor —
48
+ `list()` returns every decoded `AssistantTextEntry` oldest-first, and
49
+ `last()`/`first()` return one entry (or `undefined` when empty). Read `.text`
50
+ for the string:
51
+
52
+ ```ts
53
+ const lastText = (await session.messages().last())?.text;
54
+ ```
55
+
56
+ `decodeAssistantText`, `textOf`, and `summarizeRunTrace` remain exported as the
57
+ power-user escape hatch over a raw `RunEvent` list, but "get the last message"
58
+ is now `await session.messages().last()`.
39
59
 
40
60
  The CLI mirrors the same surface:
41
61
 
42
62
  ```bash
43
- aex events <run-id> --api-token … [--aex-url …] # snapshot (polling)
44
- aex events <run-id> --follow [--timeout 8m] --api-token … [--aex-url …] # stream until terminal (polling)
45
- aex tail <run-id> [--json] [--filter <type|source>] [--logs] [--settle] [--timeout 8m] --api-token … # live, human-readable, over the WS envelope stream
46
- aex inspect <run-id> [--json] [--filter <type|source>] [--logs] [--timeout 8m] --api-token … # one-shot full timeline + jump-to-failure + cost/usage
47
- aex wait <run-id> [--timeout 8m] [--interval 2s] --api-token … # block, print final run
63
+ aex events <session-id> --api-token … [--aex-url …] # snapshot (polling)
64
+ aex events <session-id> --follow [--timeout 8m] --api-token … [--aex-url …] # stream until the session parks (polling)
65
+ aex tail <session-id> [--json] [--filter <type|source>] [--logs] [--settle] [--timeout 8m] --api-token … # live, human-readable, over the WS envelope stream
66
+ aex inspect <session-id> [--json] [--filter <type|source>] [--logs] [--timeout 8m] --api-token … # one-shot full timeline + jump-to-failure + cost/usage
67
+ aex wait <session-id> [--timeout 8m] [--interval 2s] --api-token … # block, print final session
48
68
  ```
49
69
 
50
70
  `aex tail` and `aex inspect` consume the same coordinator WebSocket envelope
51
- stream as `streamEnvelopes()` (replay-from-cursor + tail + exactly-once resume),
52
- so they are the low-latency equivalents of `events --follow`'s polling. `--json`
53
- is the raw-NDJSON escape hatch; `--filter` keeps only the named AG-UI types
54
- (`TEXT_MESSAGE_CONTENT`, `TOOL_CALL_START`, …) or sources (`agent`/`runtime`/…);
55
- a `RUN_ERROR` is surfaced as a jump-to-failure line. `aex inspect` adds a header,
56
- a settle-consistent full timeline, and a cost/usage footer. Both exit `0`
57
- succeeded / `1` other terminal / `3` timeout. They need a global `WebSocket`
58
- (Bun or Node ≥ 22).
59
-
60
- `aex wait` is the host mirror of `aex.wait(runId)` / `aex.waitForRun(runId)`:
61
- it polls until the run reaches a terminal status and prints the final `Run`
62
- record. Exit `0` when the run `succeeded`, `1` for any other terminal status,
63
- and `3` when `--timeout` elapses first (a `--timeout` on `events --follow` /
64
- `run --follow` uses the same exit-`3` convention). Durations accept `ms`/`s`/`m`/`h`
65
- suffixes or a bare millisecond integer.
66
-
67
- Both surfaces observe the same events. A subscriber attached after `submit()` or
68
- a session message is accepted replays the events it missed, then continues live.
71
+ stream as `session.events().streamEnvelopes()` (replay-from-cursor + tail +
72
+ exactly-once resume), so they are the low-latency equivalents of
73
+ `events --follow`'s polling. `--json` is the raw-NDJSON escape hatch; `--filter`
74
+ keeps only the named AG-UI types (`TEXT_MESSAGE_CONTENT`, `TOOL_CALL_START`, …)
75
+ or sources (`agent`/`runtime`/…); a `RUN_ERROR` is surfaced as a jump-to-failure
76
+ line. `aex inspect` adds a header, a settle-consistent full timeline, and a
77
+ cost/usage footer. Both exit `0` parked cleanly / `1` error park / `3` timeout.
78
+ They need a global `WebSocket` (Bun or Node ≥ 22).
79
+
80
+ `aex wait` is the host mirror of `session.wait()`:
81
+ it polls until the session parks and prints the final `Session` record. Exit `0`
82
+ when the session parked cleanly (`idle`/`suspended`), `1` for any other park
83
+ (`error` / a non-clean terminal status), and `3` when `--timeout` elapses first
84
+ (a `--timeout` on `events --follow` / `run --follow` uses the same exit-`3`
85
+ convention). Durations accept `ms`/`s`/`m`/`h` suffixes or a bare millisecond integer.
86
+
87
+ Both surfaces observe the same events. A subscriber attached after a session
88
+ message is accepted replays the events it missed, then continues live.
69
89
 
70
90
  ## Session turn events
71
91
 
@@ -90,36 +110,37 @@ collected session turn. The returned `runId` is the session id.
90
110
 
91
111
  ## Terminal events vs. the run record
92
112
 
93
- The low-level `submit()` run path emits a terminal **event** — `RUN_FINISHED`
113
+ A session turn emits a terminal **event** — `RUN_FINISHED`
94
114
  (success) or `RUN_ERROR` — when
95
115
  the agent's stream ends. This is an AG-UI *render-complete* signal: the runner
96
- emits it **before** aex commits the authoritative run record, so a `getRun(runId)`
97
- issued the instant you observe `RUN_FINISHED` can still read `status: "running"`
98
- for a moment. Treat the terminal event as the lowest-latency "stop the spinner"
99
- signal — **not** a read-consistency barrier.
116
+ emits it **before** aex commits the authoritative session record, so an
117
+ `aex.sessions.get(id)` issued the instant you observe `RUN_FINISHED` can still
118
+ read a non-parked status for a moment. Treat the terminal event as the
119
+ lowest-latency "stop the spinner" signal — **not** a read-consistency barrier.
100
120
 
101
121
  Two facts make this easy to work with:
102
122
 
103
123
  - **Outputs are already durable at the terminal event.** The runner uploads every
104
- output before it emits the terminal event, and `listOutputs(runId)` / downloads
124
+ output before it emits the terminal event, and `session.outputs().list()` / downloads
105
125
  read object storage directly — so the moment you see `RUN_FINISHED` the outputs
106
126
  are complete and readable.
107
- - **The run _record_ settles a beat later.** To read the authoritative status
127
+ - **The session _record_ settles a beat later.** To read the authoritative status
108
128
  consistently, don't key off the terminal event — use one of:
109
129
 
110
130
  ```ts
111
- // Low-level run record path: submit + wait.
112
- const runId = await aex.submit(runConfig);
113
- const sameRun = await aex.waitForRun(runId); // or wait on an already-submitted run for the bare Run record
131
+ // Session record path: send a turn, then wait for the session to park.
132
+ const session = await aex.openSession(config);
133
+ await session.send("Continue the task.").done();
134
+ const record = await session.wait(); // the parked session record
114
135
  ```
115
136
 
116
137
  ```ts
117
138
  // Live events AND a settle-consistent end: the iterator keeps reading past
118
139
  // RUN_FINISHED until the post-mirror barrier, so the record is terminal when it ends.
119
- for await (const event of aex.streamEnvelopes(runId, { settleConsistent: true })) {
140
+ for await (const event of session.events().streamEnvelopes({ settleConsistent: true })) {
120
141
  // render events live…
121
142
  }
122
- const run = await aex.getRun(runId); // guaranteed terminal here
143
+ const settled = await aex.sessions.get(session.id); // guaranteed terminal here
123
144
  ```
124
145
 
125
146
  Under the hood the coordinator broadcasts one `aex.run.settled` CUSTOM event as a
@@ -129,10 +150,10 @@ run's last stream event, immediately after the durable record commits.
129
150
 
130
151
  ## Temporary event archive links
131
152
 
132
- For terminal runs, `eventArchiveLink(runId, options?)` returns a temporary direct URL to `events.jsonl`, the same redacted customer-visible event export used by `downloadEvents(runId)`.
153
+ For terminal runs, `session.events().archiveLink(options?)` returns a temporary direct URL to `events.jsonl`, the same redacted customer-visible event export used by `session.events().download()`.
133
154
 
134
155
  ```ts
135
- const link = await aex.eventArchiveLink(runId, { expiresIn: "1h" });
156
+ const link = await session.events().archiveLink({ expiresIn: "1h" });
136
157
  const response = await fetch(link.url);
137
158
  const jsonl = await response.text();
138
159
  ```
@@ -141,7 +162,7 @@ const jsonl = await response.text();
141
162
 
142
163
  ## Event shape
143
164
 
144
- Events are typed as the discriminated `RunEvent` union for compatibility and as the versioned coordinator envelope for live consumers. aex records raw runtime/provider payloads **after** secret redaction and structural sanitization, so the bytes you see never contain the provider key, MCP credentials, or proxy bearer that were supplied to `submit`.
165
+ Events are typed as the discriminated `RunEvent` union for compatibility and as the versioned coordinator envelope for live consumers. aex records raw runtime/provider payloads **after** secret redaction and structural sanitization, so the bytes you see never contain the provider key, MCP credentials, or proxy bearer that were supplied when the session was opened.
145
166
 
146
167
  ## Typed helpers
147
168
 
@@ -166,7 +187,7 @@ import {
166
187
 
167
188
  All guards test the `type` discriminant at runtime. `isTextMessage`,
168
189
  `isToolCallStart`, `isToolCallResult`, and `isRunFinished` operate on the loose
169
- `RunEvent` snapshot (`listEvents` / `RunResult.events`) and additionally NARROW
190
+ `RunEvent` snapshot (`session.events().list()` / `RunResult.events`) and additionally NARROW
170
191
  `event.data` to the fields that event type carries — e.g. inside
171
192
  `if (isTextMessage(e))`, `e.data.text` is typed `string`. The lifecycle/channel
172
193
  guards (`isRunStarted`, `isRunError`, `isCustom`, `isLog`, …) operate on the
@@ -29,10 +29,9 @@ And whether you can **raise** it: per-run option, per-plan, or no.
29
29
  | Maximum run timeout | 6 hours | aex policy | Per plan (billing-driven) | `RUN_MAX_TIMEOUT_MS` |
30
30
  | Minimum run timeout | 1 minute | aex policy | No (floor) | `RUN_MIN_TIMEOUT_MS` |
31
31
  | Per-call exec timeout (default) | 30 minutes | aex policy | Per-call via the tool call's `timeoutMs` | `RUN_DEFAULT_EXEC_TIMEOUT_MS` |
32
- | Post-hook timeout (default) | 60 minutes | aex policy | Per-run via the hook's `timeoutMs` | `RUN_DEFAULT_POST_HOOK_TIMEOUT_MS` |
33
32
  | MCP connect timeout (default) | 30 seconds | aex policy | Per-port via `connectTimeoutMs` | `RUN_DEFAULT_MCP_CONNECT_TIMEOUT_MS` |
34
33
  | MCP call timeout (default) | 30 minutes | aex policy | Per-port via `callTimeoutMs` | `RUN_DEFAULT_MCP_CALL_TIMEOUT_MS` |
35
- | Per-run spend cap | None by default; when set, the run is stopped once its spend would exceed the cap | aex policy | Per-run via `options.limits.maxSpendUsd` (a positive USD amount) | — |
34
+ | Per-session spend cap | None by default; when set, the session is stopped once its spend would exceed the cap | aex policy | Per-session via `overrides.maxSpendUsd` (a positive USD amount) | — |
36
35
 
37
36
  ### Output capture (per run)
38
37
 
@@ -61,8 +60,8 @@ silently lost.
61
60
 
62
61
  | Limit | Value | Source | Raisable? | Constant |
63
62
  | --- | --- | --- | --- | --- |
64
- | Max subagent depth (public submit / `subagent` tool) | 5 (a depth-5 lineage may not spawn deeper) | aex policy | Per-run via `options.limits.maxSubagentDepth` (clamped to this hard ceiling) | `RUN_MAX_PUBLIC_SUBAGENT_DEPTH` |
65
- | Concurrent child runs per lineage root | 1000 live (non-terminal); hard ceiling 4096 | aex policy | Per-run via `options.limits.maxConcurrentChildRuns` (clamped to the 4096 ceiling) | `RUN_DEFAULT_MAX_CONCURRENT_CHILD_RUNS` |
63
+ | Max subagent depth (`subagent` tool) | 5 (a depth-5 lineage may not spawn deeper) | aex policy | No public per-run override (subagents run in-process) | `RUN_MAX_PUBLIC_SUBAGENT_DEPTH` |
64
+ | Concurrent child runs per lineage root | 1000 live (non-terminal); hard ceiling 4096 | aex policy | No public per-run override (subagents run in-process) | `RUN_DEFAULT_MAX_CONCURRENT_CHILD_RUNS` |
66
65
 
67
66
  ### Retention (per run)
68
67
 
package/docs/mcp.md CHANGED
@@ -13,7 +13,7 @@ Rules:
13
13
  - Tool policy must be configured before session start.
14
14
  - Enabled MCP tools use `always_allow` provider permissions.
15
15
  - `always_ask` is not used by aex MVP.
16
- - Bearer/OAuth-style auth is passed in the per-run `secrets.mcpServers` bundle.
16
+ - Bearer/OAuth-style auth is carried by the `McpServer` instance (its `headers`); the SDK splits it into the vaulted secrets channel server-side.
17
17
 
18
18
  Use allowlists for sensitive servers whenever possible.
19
19
 
@@ -29,8 +29,8 @@ For ingestion-style tools that return large JSON blobs (search results,
29
29
  catalogue dumps, bulk reads), use the **CLI-as-skill + managed proxy**
30
30
  pattern instead of MCP:
31
31
 
32
- 1. Package the upstream as a `Skill` — a CLI binary the agent invokes
33
- with its bash tool.
32
+ 1. Package the upstream as a skill-tool (`Tools.fromSkillDir` /
33
+ `Tools.fromSkillUrl`) — a CLI binary the agent invokes with its bash tool.
34
34
  2. Route every upstream HTTPS call through a per-run `ProxyEndpoint`
35
35
  (audit, byte caps, budget enforcement).
36
36
  3. Have the CLI write the full payload to the session filesystem. By default,
@@ -47,21 +47,21 @@ without you listing them.
47
47
  ### TypeScript
48
48
 
49
49
  ```ts
50
- import { AgentExecutor, Models, Providers } from "@aexhq/sdk";
50
+ import { Aex, Models, Providers } from "@aexhq/sdk";
51
51
 
52
- const aex = new AgentExecutor({ apiToken: process.env.AEX_API_TOKEN! });
52
+ const aex = new Aex({ apiToken: process.env.AEX_API_TOKEN! });
53
53
 
54
- await aex.submit({
54
+ await aex.run({
55
55
  provider: Providers.ANTHROPIC,
56
56
  model: Models.CLAUDE_HAIKU_4_5,
57
- prompt: "Fetch the public status page and summarize it.",
57
+ message: "Fetch the public status page and summarize it.",
58
58
  environment: {
59
59
  networking: {
60
60
  mode: "limited",
61
61
  allowedHosts: ["api.example.com", "status.example.com"]
62
62
  }
63
63
  },
64
- secrets: { apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! } }
64
+ apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! }
65
65
  });
66
66
  ```
67
67
 
@@ -92,11 +92,11 @@ whenever you can name the hosts — it gives the run a stable, auditable, least-
92
92
  privilege egress surface (it is the tighter posture, not the default).
93
93
 
94
94
  ```ts
95
- await aex.submit({
95
+ await aex.run({
96
96
  model: Models.CLAUDE_HAIKU_4_5,
97
- prompt: "Research the topic across the open web.",
97
+ message: "Research the topic across the open web.",
98
98
  environment: { networking: { mode: "open" } },
99
- secrets: { apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! } }
99
+ apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! }
100
100
  });
101
101
  ```
102
102