@aexhq/sdk 0.39.0 → 0.40.1
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 +22 -16
- package/dist/_contracts/api-key.d.ts +49 -0
- package/dist/_contracts/api-key.js +87 -0
- package/dist/_contracts/bundle-manifest.d.ts +86 -0
- package/dist/_contracts/bundle-manifest.js +157 -0
- package/dist/_contracts/error-codes.d.ts +26 -0
- package/dist/_contracts/error-codes.js +79 -0
- package/dist/_contracts/error-factory.d.ts +32 -0
- package/dist/_contracts/error-factory.js +142 -0
- package/dist/_contracts/event-envelope.d.ts +33 -10
- package/dist/_contracts/event-envelope.js +46 -10
- package/dist/_contracts/event-view.d.ts +123 -0
- package/dist/_contracts/event-view.js +120 -0
- package/dist/_contracts/http.js +12 -3
- package/dist/_contracts/index.d.ts +8 -4
- package/dist/_contracts/index.js +11 -7
- package/dist/_contracts/models.d.ts +15 -0
- package/dist/_contracts/models.js +33 -0
- package/dist/_contracts/operations.d.ts +53 -2
- package/dist/_contracts/operations.js +119 -7
- package/dist/_contracts/run-record.d.ts +3 -2
- package/dist/_contracts/runtime-types.d.ts +126 -36
- package/dist/_contracts/runtime-types.js +33 -1
- package/dist/_contracts/sdk-errors.d.ts +61 -2
- package/dist/_contracts/sdk-errors.js +83 -3
- package/dist/_contracts/sdk-secrets.js +31 -6
- package/dist/_contracts/stable.d.ts +14 -0
- package/dist/_contracts/stable.js +14 -0
- package/dist/_contracts/status.d.ts +28 -1
- package/dist/_contracts/status.js +48 -3
- package/dist/_contracts/submission.d.ts +79 -2
- package/dist/_contracts/submission.js +148 -5
- package/dist/_contracts/suggest.d.ts +15 -0
- package/dist/_contracts/suggest.js +54 -0
- package/dist/asset-upload.d.ts +26 -0
- package/dist/asset-upload.js +218 -3
- package/dist/asset-upload.js.map +1 -1
- package/dist/bundle.d.ts +14 -3
- package/dist/bundle.js +34 -14
- package/dist/bundle.js.map +1 -1
- package/dist/canonical-zip.d.ts +68 -0
- package/dist/canonical-zip.js +307 -0
- package/dist/canonical-zip.js.map +1 -0
- package/dist/cli.mjs +1923 -338
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +217 -58
- package/dist/client.js +749 -261
- package/dist/client.js.map +1 -1
- package/dist/file.d.ts +33 -6
- package/dist/file.js +120 -54
- package/dist/file.js.map +1 -1
- package/dist/index.d.ts +17 -8
- package/dist/index.js +26 -9
- package/dist/index.js.map +1 -1
- package/dist/node-fs.d.ts +26 -9
- package/dist/node-fs.js +13 -38
- package/dist/node-fs.js.map +1 -1
- package/dist/node-walk.d.ts +69 -0
- package/dist/node-walk.js +146 -0
- package/dist/node-walk.js.map +1 -0
- package/dist/retry.d.ts +9 -13
- package/dist/retry.js +18 -17
- package/dist/retry.js.map +1 -1
- package/dist/skill.d.ts +16 -4
- package/dist/skill.js +18 -9
- package/dist/skill.js.map +1 -1
- package/dist/tool.d.ts +14 -2
- package/dist/tool.js +33 -7
- package/dist/tool.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/authentication.md +29 -5
- package/docs/billing.md +6 -0
- package/docs/concepts/agent-tools.md +11 -0
- package/docs/defaults.md +1 -0
- package/docs/errors.md +64 -4
- package/docs/events.md +84 -49
- package/docs/limits-and-quotas.md +24 -0
- package/docs/networking.md +7 -1
- package/docs/outputs.md +36 -7
- package/docs/quickstart.md +17 -7
- package/docs/run-config.md +3 -3
- package/docs/secrets.md +14 -0
- package/examples/feature-tour.ts +4 -6
- package/examples/spike-settle-latency.ts +125 -0
- package/package.json +4 -3
- package/dist/_contracts/event-guards.d.ts +0 -67
- package/dist/_contracts/event-guards.js +0 -36
package/docs/events.md
CHANGED
|
@@ -23,10 +23,12 @@ const events = await session.events().list();
|
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
```ts
|
|
26
|
-
// Stream
|
|
27
|
-
// session parks. Backed by polling the aex events endpoint.
|
|
26
|
+
// Stream events as guarded `AexEventView`s: yields each event once, stops when
|
|
27
|
+
// the session parks. Backed by polling the aex events endpoint. Every surface —
|
|
28
|
+
// list(), stream(), streamEnvelopes() — yields the same `AexEventView` with a
|
|
29
|
+
// populated `sequence` and the `is*()` guard methods.
|
|
28
30
|
for await (const event of session.events().stream({ intervalMs: 1000 })) {
|
|
29
|
-
if (event.
|
|
31
|
+
if (event.isTextMessage()) {
|
|
30
32
|
// ...
|
|
31
33
|
}
|
|
32
34
|
}
|
|
@@ -79,8 +81,9 @@ They need a global `WebSocket` (Bun or Node ≥ 22).
|
|
|
79
81
|
|
|
80
82
|
`aex wait` is the host mirror of `session.wait()`:
|
|
81
83
|
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
|
|
83
|
-
|
|
84
|
+
when the session parked cleanly (`idle`/`suspended`/`succeeded`), `1` for any
|
|
85
|
+
non-clean terminal outcome (`failed`/`timed_out`/`cancelled`), and `3` when
|
|
86
|
+
`--timeout` elapses first
|
|
84
87
|
(a `--timeout` on `events --follow` / `run --follow` uses the same exit-`3`
|
|
85
88
|
convention). Durations accept `ms`/`s`/`m`/`h` suffixes or a bare millisecond integer.
|
|
86
89
|
|
|
@@ -100,13 +103,34 @@ for await (const event of turn) {
|
|
|
100
103
|
console.log(event.sequence, event.type);
|
|
101
104
|
}
|
|
102
105
|
|
|
103
|
-
const result = await turn.done(); // status is
|
|
106
|
+
const result = await turn.done(); // status is the terminal outcome, e.g. "succeeded"
|
|
104
107
|
```
|
|
105
108
|
|
|
106
|
-
The turn stream ends
|
|
107
|
-
`aex.session.
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
The turn stream ends on the turn's park terminal: a resumable
|
|
110
|
+
`aex.session.idle` / `aex.session.suspended`, a terminal outcome
|
|
111
|
+
`aex.session.succeeded` / `.failed` / `.timed_out` / `.cancelled`, or the held
|
|
112
|
+
`aex.session.awaiting_approval`. (The bare `aex.session.error` park is retired —
|
|
113
|
+
a failed turn is `failed`, a wall-clock kill `timed_out`, a cancel `cancelled`.)
|
|
114
|
+
`aex.run(config)` is a convenience wrapper over the same flow: it opens a
|
|
115
|
+
session, sends `message` once, and returns the collected session turn. The
|
|
116
|
+
returned `runId` is the session id.
|
|
117
|
+
|
|
118
|
+
## Per-token streaming (`outputMode: 'stream'`)
|
|
119
|
+
|
|
120
|
+
Assistant-text granularity is controlled by `outputMode` on `openSession` / `run`:
|
|
121
|
+
|
|
122
|
+
- **`buffered`** (the default) — the assistant text arrives coalesced as one
|
|
123
|
+
`TEXT_MESSAGE_CONTENT` event per message.
|
|
124
|
+
- **`stream`** — on a **streamable** provider you also receive per-token
|
|
125
|
+
`TEXT_MESSAGE_CONTENT` **deltas** as the model produces them, and a coalesced
|
|
126
|
+
final `TEXT_MESSAGE_CONTENT` (the archived twin) always follows. Streamable
|
|
127
|
+
providers today are Anthropic and the OpenAI-chat-shaped providers (DeepSeek,
|
|
128
|
+
OpenAI, Mistral, OpenRouter, Doubao / Doubao China).
|
|
129
|
+
|
|
130
|
+
Streaming is **capability-gated and fail-closed**: requesting `outputMode: 'stream'`
|
|
131
|
+
on a provider that has no streaming producer (currently `gemini`) is a typed
|
|
132
|
+
rejection at submission parse — **not** a silent downgrade to buffered. Choose a
|
|
133
|
+
streamable provider or drop back to `buffered` explicitly.
|
|
110
134
|
|
|
111
135
|
## Terminal events vs. the run record
|
|
112
136
|
|
|
@@ -119,21 +143,30 @@ on how the turn ends:
|
|
|
119
143
|
instead (see below). Expect `RUN_ERROR` on stream-level failures, and treat
|
|
120
144
|
`RUN_FINISHED` — when it does appear — as a low-latency "stop the spinner"
|
|
121
145
|
hint, not a read-consistency barrier.
|
|
122
|
-
- **`aex.session.*` park terminals** — `CUSTOM` events
|
|
123
|
-
`aex.session.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
146
|
+
- **`aex.session.*` park terminals** — `CUSTOM` events carrying the turn's
|
|
147
|
+
OUTCOME: `aex.session.succeeded` / `aex.session.failed` / `aex.session.timed_out`
|
|
148
|
+
/ `aex.session.cancelled`, or a resumable/held park `aex.session.idle` /
|
|
149
|
+
`aex.session.suspended` / `aex.session.awaiting_approval`. On the managed plane
|
|
150
|
+
these are what actually end a turn. The park event ends the RENDER; the settle
|
|
151
|
+
commit (cost + usage + the authoritative outcome) lands shortly after — which
|
|
152
|
+
is why `run()`/`done()` await settle by DEFAULT and return the outcome as
|
|
153
|
+
`result.status` with `result.costUsd`/`result.usage` always populated (never a
|
|
154
|
+
bare `idle`). Pass `await: 'park'` (on `run(...)` or `session.send(msg, {...})`)
|
|
155
|
+
to return early at the park event when you don't need cost/usage. `result.costUsd`
|
|
156
|
+
is aex runtime/storage spend and **excludes** your BYOK provider charges — price
|
|
157
|
+
those from `result.usage` token counts. Ending a raw stream on the park event is
|
|
158
|
+
fine for live UI; read the settled record (or the default await-settle result)
|
|
159
|
+
when you need cost/usage/outcome.
|
|
160
|
+
|
|
161
|
+
Each event the stream yields carries method guards that cover both families so
|
|
162
|
+
you never have to switch on the plane:
|
|
163
|
+
|
|
164
|
+
- `event.isRunTerminal()` — true for the AG-UI `RUN_FINISHED` / `RUN_ERROR` pair.
|
|
165
|
+
- `event.isRunSettled()` — true for the `aex.run.settled` settle barrier **and**
|
|
133
166
|
for any `aex.session.*` park terminal. The managed plane does not broadcast a
|
|
134
167
|
separate `aex.run.settled` barrier — the park event plays that role — so
|
|
135
|
-
`isRunSettled` is the one
|
|
136
|
-
the record is authoritative".
|
|
168
|
+
`event.isRunSettled()` is the one check that reliably means "this stream is done
|
|
169
|
+
and the record is authoritative".
|
|
137
170
|
|
|
138
171
|
To read the authoritative status consistently, use one of:
|
|
139
172
|
|
|
@@ -154,8 +187,8 @@ for await (const event of session.events().streamEnvelopes({ settleConsistent: t
|
|
|
154
187
|
const settled = await aex.sessions.get(session.id); // parked/terminal here
|
|
155
188
|
```
|
|
156
189
|
|
|
157
|
-
`settleConsistent: true` makes the iterator end exactly when `isRunSettled(
|
|
158
|
-
first fires; on a raw stream,
|
|
190
|
+
`settleConsistent: true` makes the iterator end exactly when `event.isRunSettled()`
|
|
191
|
+
first fires; on a raw stream, call `event.isRunSettled()` yourself. What it
|
|
159
192
|
guarantees: when the stream ends, a subsequent `aex.sessions.get(id)` reads a
|
|
160
193
|
parked/terminal status and `session.outputs().list()` is complete. Outputs are
|
|
161
194
|
uploaded before the terminal is broadcast, so they are readable the moment the
|
|
@@ -175,34 +208,36 @@ const jsonl = await response.text();
|
|
|
175
208
|
|
|
176
209
|
## Event shape
|
|
177
210
|
|
|
178
|
-
|
|
211
|
+
Every read and stream surface yields one canonical shape — the versioned coordinator envelope, wrapped as an `AexEventView` with a populated non-optional `sequence` and the `is*()` guard methods. `sequence` is **sparse by design** (it encodes journal position, not a dense 0..N counter), so resume by cursor VALUE (`from: lastSequence`), never by integer adjacency. Tool-call START and RESULT events share a `data.id` join key, exposed as `event.toolCallId()` on those views. aex records raw runtime/provider payloads **after** secret redaction and structural sanitization, so the bytes you see never contain provider keys, MCP credentials, or runtime secrets supplied when the session was opened.
|
|
179
212
|
|
|
180
213
|
## Typed helpers
|
|
181
214
|
|
|
182
|
-
|
|
215
|
+
Every event the SDK yields — the turn stream (`session.send()`),
|
|
216
|
+
`session.events().list()`, `session.events().streamEnvelopes()`, and
|
|
217
|
+
`RunResult.events` — carries a type-guard **method** for each standardized event
|
|
218
|
+
type, so you branch on the event without importing a free function or writing a
|
|
219
|
+
raw `event.type === "…"` compare:
|
|
183
220
|
|
|
184
221
|
```ts
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
isCustom,
|
|
195
|
-
isLog,
|
|
196
|
-
isEventChannel
|
|
197
|
-
} from "@aexhq/sdk";
|
|
222
|
+
for await (const event of session.send("Continue the task.")) {
|
|
223
|
+
if (event.isTextMessage()) {
|
|
224
|
+
process.stdout.write(event.data.text); // `data.text` is typed `string` here
|
|
225
|
+
} else if (event.isToolCallStart()) {
|
|
226
|
+
console.log("tool:", event.data.name); // `data.name` is typed `string` here
|
|
227
|
+
} else if (event.isRunError()) {
|
|
228
|
+
console.error("run error");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
198
231
|
```
|
|
199
232
|
|
|
200
|
-
|
|
201
|
-
`
|
|
202
|
-
`
|
|
203
|
-
`
|
|
204
|
-
`
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
event
|
|
233
|
+
The full method set: `isRunStarted()`, `isRunFinished()`, `isRunError()`,
|
|
234
|
+
`isRunTerminal()`, `isTextMessage()`, `isToolCallStart()`, `isToolCallResult()`,
|
|
235
|
+
`isCustom()`, `isLog()`, `isEventChannel()`, `isRunSettled()`, and
|
|
236
|
+
`isFromSource(source)`. All test the `type` (or `channel`/`source`) discriminant
|
|
237
|
+
at runtime. `isTextMessage()`, `isToolCallStart()`, and `isToolCallResult()` are
|
|
238
|
+
TS type predicates: inside the guarded branch `event.data` NARROWS to that event
|
|
239
|
+
type's payload fields (e.g. `event.data.text` is typed `string`). Annotate a
|
|
240
|
+
narrowed event with the exported `TextMessageEventView` / `ToolCallStartEventView`
|
|
241
|
+
/ `ToolCallResultEventView` types, or a raw event with `AexEventView`. Use
|
|
242
|
+
`result.text` or `session.messages.all()` when you need assistant text without
|
|
243
|
+
inspecting the event stream directly.
|
|
@@ -31,6 +31,7 @@ And whether you can **raise** it: per-run option, per-plan, or no.
|
|
|
31
31
|
| MCP connect timeout (default) | 30 seconds | aex policy | Per-port via `connectTimeoutMs` | `RUN_DEFAULT_MCP_CONNECT_TIMEOUT_MS` |
|
|
32
32
|
| MCP call timeout (default) | 30 minutes | aex policy | Per-port via `callTimeoutMs` | `RUN_DEFAULT_MCP_CALL_TIMEOUT_MS` |
|
|
33
33
|
| 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) | — |
|
|
34
|
+
| Max agent iterations (turns) per run | 20 by default; hard ceiling 200 | aex policy | Per-session via `overrides.maxTurns` (a positive integer, clamped to the ceiling) | `RUN_DEFAULT_MAX_TURNS` / `RUN_MAX_TURNS_CEILING` |
|
|
34
35
|
|
|
35
36
|
### Output capture (per run)
|
|
36
37
|
|
|
@@ -69,6 +70,29 @@ silently lost.
|
|
|
69
70
|
| Per-run metadata record TTL | 24 hours | aex policy | No (hard ceiling) | `RUN_KV_RECORD_TTL_SECONDS` |
|
|
70
71
|
| Per-run secret-envelope TTL | 24 hours | aex policy | No (hard ceiling) | `RUN_KV_SECRET_TTL_SECONDS` |
|
|
71
72
|
|
|
73
|
+
## Sandbox (managed runtime)
|
|
74
|
+
|
|
75
|
+
Each run executes in an ephemeral Linux container sized by the `runtime` preset.
|
|
76
|
+
A few behaviours are worth knowing before you rely on the filesystem or RAM:
|
|
77
|
+
|
|
78
|
+
- **Only `/workspace` persists across turns** of the same session. Everything
|
|
79
|
+
outside `/workspace` is reset between turns and is gone when the session ends —
|
|
80
|
+
write deliverables and any state you want to survive a turn under `/workspace`.
|
|
81
|
+
- **The image is minimal.** Declare OS/language packages with
|
|
82
|
+
`environment.packages` so they are present deterministically at boot. A runtime
|
|
83
|
+
install inside the agent (`pip install`, `apt-get`) is best-effort and
|
|
84
|
+
**non-persistent** (reset next turn), and `pip` is subject to PEP 668
|
|
85
|
+
(`externally-managed-environment`) — prefer `environment.packages`, or a
|
|
86
|
+
virtualenv you create under `/workspace`.
|
|
87
|
+
- **`/proc/meminfo` reports the HOST's RAM, not your preset's.** Your actual
|
|
88
|
+
memory ceiling is the preset's `memoryMb` (e.g. `shared-0.25x-1gb` = 1 GB, the
|
|
89
|
+
default; `shared-4x-12gb` = 12 GB). The presets are the SSoT in
|
|
90
|
+
[`packages/contracts/src/runtime-sizes.ts`](https://github.com/aexhq/aex/blob/main/packages/contracts/src/runtime-sizes.ts);
|
|
91
|
+
see [Defaults](defaults.md).
|
|
92
|
+
- **The agent loop is bounded** by `maxTurns` (default 20, ceiling 200) — a
|
|
93
|
+
documented, per-run-overridable limit (see the Run scope table above and
|
|
94
|
+
`overrides.maxTurns`), not a silent cutoff.
|
|
95
|
+
|
|
72
96
|
## Workspace scope
|
|
73
97
|
|
|
74
98
|
| Limit | Value | Source | Raisable? | Constant |
|
package/docs/networking.md
CHANGED
|
@@ -15,7 +15,13 @@ internet**. Outbound traffic is governed by **two layers**:
|
|
|
15
15
|
(model providers, built-in tool endpoints, package registries, and related
|
|
16
16
|
well-known development hosts such as `github.com`) and enforces a fixed SSRF
|
|
17
17
|
deny-list: loopback, link-local, cloud-metadata, and other private ranges are
|
|
18
|
-
|
|
18
|
+
blocked on the standard proxy path every normal HTTP client uses, including
|
|
19
|
+
hostnames that resolve to those ranges. Note: on the current managed (Fargate)
|
|
20
|
+
plane a subprocess that deliberately bypasses the proxy with a **raw socket**
|
|
21
|
+
can still reach the on-link task-metadata IP (`169.254.170.2`), which exposes
|
|
22
|
+
non-secret task identity (AWS account id via the task ARN, cluster/image ref)
|
|
23
|
+
— but **never IAM credentials** (the run's task role is unset, so the metadata
|
|
24
|
+
credential endpoint serves nothing) and never another tenant's data.
|
|
19
25
|
|
|
20
26
|
Honest boundary statement: the per-run `allowedHosts` policy is enforced by the
|
|
21
27
|
run's own runtime on the standard proxy path — it is **not** yet enforced at
|
package/docs/outputs.md
CHANGED
|
@@ -4,7 +4,7 @@ title: Outputs
|
|
|
4
4
|
|
|
5
5
|
# Outputs
|
|
6
6
|
|
|
7
|
-
Every session produces durable metadata (status, events,
|
|
7
|
+
Every session produces durable metadata (status, events, cleanup state) and an outputs namespace. By default, managed runs capture the regular files present in the container when the agent exits, EXCLUDING the inputs the platform itself materialized for you (your mounted `files`/`skills`) — those are excluded by IDENTITY (their exact destination paths and skill-dir prefixes), not by any before/after timing comparison. There is no default or official output directory. Use `outputs.allowedDirs` only when you want to narrow capture to specific roots, and `outputs.deniedDirs` to subtract noise. `session.download()` returns the public session record — metadata, typed events, and captured output bytes — as a zip; the per-namespace verbs (`session.outputs().download()` / `session.events().download()` / `session.downloadMetadata()`) return one slice each.
|
|
8
8
|
|
|
9
9
|
The output verbs below hang off the session's `outputs()` accessor
|
|
10
10
|
(`session.outputs().list()`, `.read()`, `.download()`, …). Reach a handle from a
|
|
@@ -28,7 +28,7 @@ await session.download({ to: "./session.zip" });
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
aex download <session-id> --out ./session.zip --api-key …
|
|
31
|
+
npx aex download <session-id> --out ./session.zip --api-key …
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
## The three namespaces
|
|
@@ -133,6 +133,35 @@ Query fields compose with AND semantics:
|
|
|
133
133
|
|
|
134
134
|
`session.outputs().findOne(query)` returns `null` when nothing matches and throws `RunStateError` when the query matches more than one output.
|
|
135
135
|
|
|
136
|
+
## Searching outputs
|
|
137
|
+
|
|
138
|
+
Search is metadata-only (reference hits — filename / extension / content type — no bytes). `filename` accepts a `string` (case-insensitive substring) or a `RegExp`. A content-shaped query (`content`/`text`/…) throws a typed "content search unsupported" rather than silently returning zero hits.
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// One session's outputs:
|
|
142
|
+
const hits = await session.outputs().search({ filename: /report/i });
|
|
143
|
+
|
|
144
|
+
// Across every run in the workspace (or scope with runIds):
|
|
145
|
+
const all = await aex.outputs.search({ extension: "md", runIds: ["run-a", "run-b"] });
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Each hit is `{ runId, outputId, filename?, sizeBytes?, contentType? }`; read the bytes with `session.outputs().read(...)` / `.download(...)`.
|
|
149
|
+
|
|
150
|
+
## CLI
|
|
151
|
+
|
|
152
|
+
The `aex outputs` verb is a thin pass-through over the same SDK accessor, so every per-file operation has a subcommand (`npx aex` on a local install):
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npx aex outputs <session-id> # list captured outputs (NDJSON)
|
|
156
|
+
npx aex outputs read <session-id> <path> # read one file as capped text (JSON)
|
|
157
|
+
npx aex outputs download <session-id> <path> --out f # download one file's raw bytes
|
|
158
|
+
npx aex outputs link <session-id> <path> # mint a temporary download URL (JSON)
|
|
159
|
+
npx aex outputs find <session-id> --name S --ext E --type T
|
|
160
|
+
npx aex outputs search --query S --ext E --run-id ID # cross-run metadata search
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`aex outputs search` (no session id) is the cross-run search (`aex.outputs.search`); the whole-namespace zip stays `aex download <session-id>`.
|
|
164
|
+
|
|
136
165
|
## Temporary output links
|
|
137
166
|
|
|
138
167
|
Use `session.outputs().link(selectorOrQuery, options?)` when another process, browser, media tag, or downloader needs a direct artifact URL instead of bytes buffered through the SDK.
|
|
@@ -190,7 +219,7 @@ Validation:
|
|
|
190
219
|
|
|
191
220
|
Runtime notes:
|
|
192
221
|
|
|
193
|
-
- The managed runtime captures files
|
|
222
|
+
- The managed runtime captures the regular files under the capture roots at terminal time, EXCLUDING the inputs the platform itself materialized (your mounted `files`/`skills`) by IDENTITY — their exact destination paths and skill-dir prefixes are threaded into the capture filter, so an untouched mounted input is never re-emitted as an output regardless of path policy or timing.
|
|
194
223
|
- If you pass an explicit root that does not exist by terminal time, that root contributes no files.
|
|
195
224
|
|
|
196
225
|
## `outputs.deniedDirs` — subtract noise
|
|
@@ -208,10 +237,10 @@ aex.openSession({
|
|
|
208
237
|
|
|
209
238
|
Mechanism (no platform-magical paths — this is honest):
|
|
210
239
|
|
|
211
|
-
1. The hosted platform materializes the workspace
|
|
240
|
+
1. The hosted platform materializes the workspace (your mounted `files`/`skills`) and records the exact destination paths + skill-dir prefixes it wrote.
|
|
212
241
|
2. The agent runs normally. There is no extra model turn and no synthetic sync instruction.
|
|
213
|
-
3. When the agent exits, the runner
|
|
214
|
-
4. The runner uploads
|
|
242
|
+
3. When the agent exits, the runner scans the capture roots and drops any file whose path is a materialized INPUT (exact path or under a materialized skill dir) — inputs are excluded by WHO PUT THEM THERE (the platform), not by timing.
|
|
243
|
+
4. The runner uploads the remaining regular files to durable run artifact storage. Diagnostic log paths are routed to internal diagnostics under `runs/<runId>/internal/logs/`; other paths are routed to `outputs`.
|
|
215
244
|
|
|
216
245
|
Cost: output capture does not add a model turn. The runner pays a filesystem scan and upload cost near the end of the run.
|
|
217
246
|
|
|
@@ -228,7 +257,7 @@ Metadata still gets the full treatment. aex captures every regular file the run
|
|
|
228
257
|
|
|
229
258
|
## Mid-session download semantics
|
|
230
259
|
|
|
231
|
-
Mid-session calls are **best-effort and side-effect-free**: they expose whatever artifacts have already been uploaded. Files written by the agent are normally uploaded near terminal, after the
|
|
260
|
+
Mid-session calls are **best-effort and side-effect-free**: they expose whatever artifacts have already been uploaded. Files written by the agent are normally uploaded near terminal, after the runner scans the capture roots. If you need the full output set, wait for the session to park and call `session.download()` again.
|
|
232
261
|
|
|
233
262
|
## Safety
|
|
234
263
|
|
package/docs/quickstart.md
CHANGED
|
@@ -16,9 +16,10 @@ This installs the TypeScript SDK exports and the bundled `aex` CLI.
|
|
|
16
16
|
|
|
17
17
|
aex is currently in **invite-only beta**: workspaces and API keys are issued
|
|
18
18
|
by the aex team — contact <support@aex.dev> for beta access. Once you have
|
|
19
|
-
access, create a quickstart SDK token with `runs:read`, `runs:write`,
|
|
20
|
-
`outputs:read` in the dashboard at <https://aex.dev>. The
|
|
21
|
-
your BYOK provider key for the model you choose. For the
|
|
19
|
+
access, create a quickstart SDK token with `runs:read`, `runs:write`,
|
|
20
|
+
`outputs:read`, and `billing:read` in the dashboard at <https://aex.dev>. The
|
|
21
|
+
examples also need your BYOK provider key for the model you choose. For the
|
|
22
|
+
Claude examples below:
|
|
22
23
|
|
|
23
24
|
```bash
|
|
24
25
|
export AEX_API_KEY="<your-aex-api-key>"
|
|
@@ -41,9 +42,16 @@ const session = await aex.openSession({
|
|
|
41
42
|
});
|
|
42
43
|
|
|
43
44
|
const first = await session.send("Write a short report and save it as a file.").done();
|
|
44
|
-
console.log(first.status, first.text);
|
|
45
|
+
console.log(first.status, first.costUsd, first.text);
|
|
45
46
|
```
|
|
46
47
|
|
|
48
|
+
`send().done()` (and `run()`) **await settle by default**, so the result always
|
|
49
|
+
carries a terminal `status` (`succeeded` / `failed` / `timed_out` / `cancelled`
|
|
50
|
+
— never a bare `idle`) plus `costUsd` and `usage`. Pass `await: 'park'` to
|
|
51
|
+
return early at the render-complete park event when you don't need cost/usage.
|
|
52
|
+
`costUsd` is aex runtime/storage spend and **excludes** your BYOK provider
|
|
53
|
+
charges — price those from `usage` token counts against your provider's rates.
|
|
54
|
+
|
|
47
55
|
The session parks as `idle` between turns and automatically moves to
|
|
48
56
|
`suspended` after the idle window. Keep the session id and resume later:
|
|
49
57
|
|
|
@@ -88,7 +96,8 @@ await turn.done();
|
|
|
88
96
|
const messages = await session.messages().list();
|
|
89
97
|
console.log(messages.at(-1)?.text);
|
|
90
98
|
|
|
91
|
-
// Poll the record until the session parks
|
|
99
|
+
// Poll the record until the session parks: a resumable `idle` / `suspended`,
|
|
100
|
+
// or a terminal outcome (`succeeded` / `failed` / `timed_out` / `cancelled`).
|
|
92
101
|
const record = await session.wait();
|
|
93
102
|
console.log(record.status);
|
|
94
103
|
|
|
@@ -96,10 +105,11 @@ console.log(record.status);
|
|
|
96
105
|
await session.download({ to: "./session.zip" });
|
|
97
106
|
```
|
|
98
107
|
|
|
99
|
-
The same run from the bundled CLI
|
|
108
|
+
The same run from the bundled CLI (`npx aex` on a local install; or
|
|
109
|
+
`npm i -g @aexhq/sdk` for a bare `aex`):
|
|
100
110
|
|
|
101
111
|
```bash
|
|
102
|
-
aex run \
|
|
112
|
+
npx aex run \
|
|
103
113
|
--api-key "$AEX_API_KEY" \
|
|
104
114
|
--anthropic-api-key "$ANTHROPIC_API_KEY" \
|
|
105
115
|
--model claude-haiku-4-5 \
|
package/docs/run-config.md
CHANGED
|
@@ -14,9 +14,9 @@ Allowed fields:
|
|
|
14
14
|
- `environment` - `{ networking?, packages?, variables? }`. Networking is open by default; set `networking.mode` to `limited` only when you want an allowlist. `variables` are merged into the in-container `RUNTIME.env` / `RUNTIME.json` mounts. (Run secrets go in `environment.secrets`, which carries live `Secret` instances and is not part of a shareable config.)
|
|
15
15
|
- `runtime` - optional managed-runtime preset. Prefer `Sizes` in TypeScript.
|
|
16
16
|
- `metadata` - non-secret structured metadata.
|
|
17
|
-
- `overrides` - `{ idleTtl?, timeout?, maxSpendUsd? }`. `timeout` is an optional session deadline (e.g. `"30m"`, `"2h"`); `maxSpendUsd` stops the session once its spend would exceed the cap (
|
|
17
|
+
- `overrides` - `{ idleTtl?, timeout?, maxSpendUsd?, maxTurns? }`. `timeout` is an optional session deadline (e.g. `"30m"`, `"2h"`); `maxSpendUsd` stops the session once its spend would exceed the cap; `maxTurns` caps the agent's iteration count for the turn (a positive integer — omit to accept the platform default of 20, clamped to the ceiling). See [Limits & quotas](limits-and-quotas.md).
|
|
18
18
|
|
|
19
|
-
`message` (the one-shot `run` input), `agentsMd`, `files`, `outputs`, `tools`, `skills`, `includeBuiltinTools`, and `outputMode` are `openSession` / `run` options, not reusable run-config fields. They carry the turn input, bytes, capture behavior, or agent tool/output controls that belong on a concrete call. Skill bundles are `skills` entries built with `Skill.fromDir(...)`, `Skill.fromUrl(...)`, or the other `Skill.from*` factories, so they too are SDK-code options rather than config fields. Subagents are session-internal (the in-run `subagent` tool — see [Subagents](concepts/subagents.md)); there is no `parentRunId` option. The wire contract carries a per-run `limits` object (the exported `RunLimits` type: `maxConcurrentChildRuns`, `maxSubagentDepth`, `maxSpendUsd`),
|
|
19
|
+
`message` (the one-shot `run` input), `agentsMd`, `files`, `outputs`, `tools`, `skills`, `includeBuiltinTools`, and `outputMode` are `openSession` / `run` options, not reusable run-config fields. They carry the turn input, bytes, capture behavior, or agent tool/output controls that belong on a concrete call. Skill bundles are `skills` entries built with `Skill.fromDir(...)`, `Skill.fromUrl(...)`, or the other `Skill.from*` factories, so they too are SDK-code options rather than config fields. Subagents are session-internal (the in-run `subagent` tool — see [Subagents](concepts/subagents.md)); there is no `parentRunId` option. The wire contract carries a per-run `limits` object (the exported `RunLimits` type: `maxConcurrentChildRuns`, `maxSubagentDepth`, `maxSpendUsd`, `maxTurns`), and the session surface exposes its spend and iteration dials via `overrides.maxSpendUsd` and `overrides.maxTurns`; the subagent depth/breadth dials are not settable per-session today and take the platform defaults.
|
|
20
20
|
|
|
21
21
|
Secrets never live in run config. Pass provider keys through the top-level
|
|
22
22
|
`apiKeys` map and runtime secrets through `environment.secrets` in the SDK, or
|
|
@@ -55,4 +55,4 @@ aex run --config ./run.json \
|
|
|
55
55
|
--anthropic-api-key "$ANTHROPIC_API_KEY"
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
...or as explicit flags (`--model`, `--system`, `--prompt`, `--mcp`, `--mcp-auth`, `--runtime-size`, `--run-timeout`, `--metadata`). The two modes are mutually exclusive.
|
|
58
|
+
...or as explicit flags (`--model`, `--system`, `--prompt`, `--mcp`, `--mcp-auth`, `--runtime-size`, `--run-timeout`, `--metadata`). The two modes are mutually exclusive. Composition inputs attach with repeatable flags that map onto the SDK's `Skill`/`Tool`/`AgentsMd`/`File` factories: `--skill @bundle` (workspace skill), `--tool @tool.js` (custom tool module), `--agents-md @AGENTS.md`, and `--file @path` (mount a file into `/workspace`).
|
package/docs/secrets.md
CHANGED
|
@@ -118,3 +118,17 @@ await aex.secrets.delete("serper-api-key");
|
|
|
118
118
|
|
|
119
119
|
The CLI supports per-run provider and MCP credentials. Workspace secret
|
|
120
120
|
administration is exposed through the SDK.
|
|
121
|
+
|
|
122
|
+
## Redaction Scope And Output Files
|
|
123
|
+
|
|
124
|
+
Registered secret values are redacted from the run's **event stream** (both tool
|
|
125
|
+
output and model-authored surfaces) — a value you inject via `environment.secrets`
|
|
126
|
+
is masked regardless of its shape. Two surfaces are intentionally *not* scrubbed:
|
|
127
|
+
|
|
128
|
+
- **Captured output files** (`outputs().download()` / `read()` / the `aex download`
|
|
129
|
+
zip) are returned **verbatim**. They are your run's own artifacts, so the platform
|
|
130
|
+
does not rewrite their bytes — if the agent writes a secret into a deliverable file,
|
|
131
|
+
that file contains it. Treat downloaded outputs as unredacted.
|
|
132
|
+
- An **unregistered** secret (a credential the run produces itself and never declared
|
|
133
|
+
via `environment.secrets`) can only be masked heuristically by shape; register the
|
|
134
|
+
values you care about so they are masked by value.
|
package/examples/feature-tour.ts
CHANGED
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
BuiltinTools,
|
|
23
23
|
File,
|
|
24
24
|
isRateLimited,
|
|
25
|
-
isTextMessage,
|
|
26
25
|
McpServer,
|
|
27
26
|
Models,
|
|
28
27
|
Providers,
|
|
@@ -200,12 +199,11 @@ for (;;) {
|
|
|
200
199
|
break;
|
|
201
200
|
}
|
|
202
201
|
const event = next.value;
|
|
203
|
-
if (isTextMessage(
|
|
202
|
+
if (event.isTextMessage()) {
|
|
204
203
|
process.stdout.write(event.data.text);
|
|
205
|
-
} else if (event.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
} else if (event.type === "TOOL_CALL_RESULT") {
|
|
204
|
+
} else if (event.isToolCallStart()) {
|
|
205
|
+
process.stdout.write(`\n[tool:start] ${event.data.name}\n`);
|
|
206
|
+
} else if (event.isToolCallResult()) {
|
|
209
207
|
process.stdout.write("[tool:result]\n");
|
|
210
208
|
}
|
|
211
209
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPIKE (throwaway): measure the real park-event -> record-idle gap for interactive
|
|
3
|
+
* session turns. This is the extra wall-clock Option B (settle-consistent turn stream)
|
|
4
|
+
* would force onto EVERY turn: the turn stream currently ends on the RUN_FINISHED park
|
|
5
|
+
* event, but the session RECORD flips running->idle later, in the async settle lambda.
|
|
6
|
+
*
|
|
7
|
+
* AEX_API_KEY=... DEEPSEEK_API_KEY=... AEX_API_URL=... bun packages/sdk/examples/spike-settle-latency.ts
|
|
8
|
+
*
|
|
9
|
+
* Optional: SPIKE_TURNS=8 (samples), SPIKE_POLL_MS=120 (record poll interval).
|
|
10
|
+
*
|
|
11
|
+
* Method per turn:
|
|
12
|
+
* 1) send a tiny prompt, stream events, capture t_runFinished at the RUN_FINISHED event
|
|
13
|
+
* (this is where the default turn stream ends), then stop consuming.
|
|
14
|
+
* 2) tight-poll session.refresh() until status === "idle"; capture t_idle.
|
|
15
|
+
* 3) settleGapMs = t_idle - t_runFinished <-- the Option B tax.
|
|
16
|
+
* The next turn is only sent AFTER idle, so each measurement is isolated (no reconcile).
|
|
17
|
+
*/
|
|
18
|
+
import { Aex, Models, Providers, Sizes } from "@aexhq/sdk";
|
|
19
|
+
|
|
20
|
+
const apiKey = required("AEX_API_KEY");
|
|
21
|
+
const deepseekKey = required("DEEPSEEK_API_KEY");
|
|
22
|
+
const apiUrl = process.env.AEX_API_URL;
|
|
23
|
+
const turns = Number(process.env.SPIKE_TURNS ?? "8");
|
|
24
|
+
const pollMs = Number(process.env.SPIKE_POLL_MS ?? "120");
|
|
25
|
+
|
|
26
|
+
const aex = new Aex({
|
|
27
|
+
apiKey,
|
|
28
|
+
...(apiUrl ? { baseUrl: apiUrl } : {}),
|
|
29
|
+
retry: { maxAttempts: 4, initialDelayMs: 500, maxDelayMs: 10_000, maxElapsedMs: 90_000 }
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
console.log(`opening session (dev)...`);
|
|
33
|
+
const session = await aex.openSession({
|
|
34
|
+
provider: Providers.DEEPSEEK,
|
|
35
|
+
model: Models.DEEPSEEK_V4_FLASH,
|
|
36
|
+
system: "Reply with a single short sentence. Never use tools or write files.",
|
|
37
|
+
includeBuiltinTools: false,
|
|
38
|
+
tools: [],
|
|
39
|
+
runtime: Sizes.SHARED_0_25X_1GB,
|
|
40
|
+
overrides: { idleTtl: "10m", timeout: "5m", maxSpendUsd: 1 },
|
|
41
|
+
apiKeys: { deepseek: deepseekKey }
|
|
42
|
+
});
|
|
43
|
+
console.log(`session: ${session.id}`);
|
|
44
|
+
|
|
45
|
+
type Sample = { turn: number; turnMs: number; settleGapMs: number; polls: number; timedOut: boolean };
|
|
46
|
+
const samples: Sample[] = [];
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < turns; i++) {
|
|
49
|
+
const stream = session.send(`Say hello #${i + 1} in one short sentence.`);
|
|
50
|
+
const tStart = performance.now();
|
|
51
|
+
let tRunFinished = 0;
|
|
52
|
+
let errored = false;
|
|
53
|
+
for await (const event of stream) {
|
|
54
|
+
if (event.isRunError()) {
|
|
55
|
+
errored = true;
|
|
56
|
+
tRunFinished = performance.now();
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
if (event.isRunFinished()) {
|
|
60
|
+
tRunFinished = performance.now();
|
|
61
|
+
break; // default turn stream would end HERE
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (tRunFinished === 0) tRunFinished = performance.now();
|
|
65
|
+
const turnMs = tRunFinished - tStart;
|
|
66
|
+
|
|
67
|
+
// Tight-poll the authoritative record until it leaves running.
|
|
68
|
+
let polls = 0;
|
|
69
|
+
let tIdle = 0;
|
|
70
|
+
let timedOut = false;
|
|
71
|
+
const deadline = performance.now() + 90_000;
|
|
72
|
+
for (;;) {
|
|
73
|
+
polls++;
|
|
74
|
+
const rec = await session.refresh().catch(() => undefined);
|
|
75
|
+
const status = rec?.status;
|
|
76
|
+
if (status === "idle" || status === "suspended" || status === "error") {
|
|
77
|
+
tIdle = performance.now();
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
if (performance.now() >= deadline) {
|
|
81
|
+
tIdle = performance.now();
|
|
82
|
+
timedOut = true;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
await sleep(pollMs);
|
|
86
|
+
}
|
|
87
|
+
const settleGapMs = tIdle - tRunFinished;
|
|
88
|
+
samples.push({ turn: i + 1, turnMs, settleGapMs, polls, timedOut });
|
|
89
|
+
console.log(
|
|
90
|
+
`turn ${String(i + 1).padStart(2)} turn=${fmt(turnMs)} settleGap=${fmt(settleGapMs)} polls=${polls}` +
|
|
91
|
+
(errored ? " (RUN_ERROR)" : "") +
|
|
92
|
+
(timedOut ? " (TIMEOUT>90s)" : "")
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const gaps = samples.map((s) => s.settleGapMs).sort((a, b) => a - b);
|
|
97
|
+
console.log("\n=== settle-gap distribution (park event -> record idle) ===");
|
|
98
|
+
console.log(`n=${gaps.length}`);
|
|
99
|
+
console.log(`min ${fmt(gaps[0])}`);
|
|
100
|
+
console.log(`median ${fmt(pct(gaps, 50))}`);
|
|
101
|
+
console.log(`p90 ${fmt(pct(gaps, 90))}`);
|
|
102
|
+
console.log(`max ${fmt(gaps[gaps.length - 1])}`);
|
|
103
|
+
console.log(`mean ${fmt(gaps.reduce((a, b) => a + b, 0) / gaps.length)}`);
|
|
104
|
+
console.log("\nInterpretation: settleGap is the per-turn latency Option B adds before the");
|
|
105
|
+
console.log("caller regains control (the turn stream would block until the record is idle).");
|
|
106
|
+
|
|
107
|
+
function pct(sorted: number[], p: number): number {
|
|
108
|
+
if (sorted.length === 0) return 0;
|
|
109
|
+
const idx = Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length));
|
|
110
|
+
return sorted[idx];
|
|
111
|
+
}
|
|
112
|
+
function fmt(ms: number): string {
|
|
113
|
+
return ms >= 1000 ? `${(ms / 1000).toFixed(2)}s` : `${Math.round(ms)}ms`;
|
|
114
|
+
}
|
|
115
|
+
function sleep(ms: number): Promise<void> {
|
|
116
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
117
|
+
}
|
|
118
|
+
function required(name: string): string {
|
|
119
|
+
const v = process.env[name];
|
|
120
|
+
if (!v) {
|
|
121
|
+
console.error(`Missing env var ${name}`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
return v;
|
|
125
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aexhq/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.1",
|
|
4
4
|
"description": "TypeScript SDK for running autonomous agent sessions across providers (Anthropic, OpenAI, DeepSeek, Gemini, Mistral) behind one interface.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
29
|
"build": "bun ../../scripts/with-generated-dist-lock.mjs bun run build:unlocked",
|
|
30
|
-
"build:unlocked": "bun ./scripts/clean-dist.mjs && bun run --cwd ../.. --filter @aexhq/contracts build && bun run --cwd ../.. --filter @aexhq/cli build &&
|
|
30
|
+
"build:unlocked": "bun ./scripts/clean-dist.mjs && bun run --cwd ../.. --filter @aexhq/contracts build && tsc -p tsconfig.build.json && bun run --cwd ../.. --filter @aexhq/cli build && bun ./scripts/bundle-cli.mjs && bun ./scripts/inline-contracts.mjs",
|
|
31
31
|
"lint": "bun ../../scripts/with-generated-dist-lock.mjs bun run lint:unlocked",
|
|
32
32
|
"lint:unlocked": "tsc --noEmit",
|
|
33
33
|
"test": "bun run test:unit",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"bun": ">=1.3.14"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"fflate": "0.8.2"
|
|
50
|
+
"fflate": "0.8.2",
|
|
51
|
+
"ignore": "7.0.5"
|
|
51
52
|
}
|
|
52
53
|
}
|