@aexhq/sdk 0.38.1 → 0.40.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 +23 -17
- 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 +70 -2
- package/dist/_contracts/operations.js +143 -7
- package/dist/_contracts/run-config.d.ts +35 -10
- package/dist/_contracts/run-config.js +17 -5
- package/dist/_contracts/run-record.d.ts +3 -2
- package/dist/_contracts/runtime-types.d.ts +148 -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 +157 -14
- package/dist/_contracts/submission.js +319 -55
- package/dist/_contracts/suggest.d.ts +15 -0
- package/dist/_contracts/suggest.js +54 -0
- package/dist/asset-upload.d.ts +27 -1
- package/dist/asset-upload.js +219 -4
- package/dist/asset-upload.js.map +1 -1
- package/dist/bundle.d.ts +23 -14
- package/dist/bundle.js +39 -20
- 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 +1927 -326
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +273 -70
- package/dist/client.js +885 -335
- package/dist/client.js.map +1 -1
- package/dist/fetch-archive.js +14 -14
- package/dist/fetch-archive.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 +23 -14
- package/dist/index.js +31 -14
- 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 +151 -0
- package/dist/skill.js +298 -0
- package/dist/skill.js.map +1 -0
- 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/concepts/composition.md +3 -3
- 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/mcp.md +3 -2
- package/docs/networking.md +7 -1
- package/docs/outputs.md +36 -7
- package/docs/provider-runtime-capabilities.md +1 -1
- package/docs/quickstart.md +17 -7
- package/docs/run-config.md +3 -3
- package/docs/secrets.md +14 -0
- package/docs/skills.md +74 -44
- package/docs/vision-skills.md +3 -3
- 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/dist/skill-tool.d.ts +0 -102
- package/dist/skill-tool.js +0 -190
- package/dist/skill-tool.js.map +0 -1
package/docs/errors.md
CHANGED
|
@@ -5,9 +5,56 @@ title: Errors
|
|
|
5
5
|
# Errors
|
|
6
6
|
|
|
7
7
|
Every API error is a JSON body with a machine-readable `error` code; most also
|
|
8
|
-
carry a human `message` and the self-describing fields named below.
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
carry a human `message` and the self-describing fields named below.
|
|
9
|
+
|
|
10
|
+
## Typed errors in the SDK
|
|
11
|
+
|
|
12
|
+
The SDK maps every non-2xx response through one factory to a typed exception. All
|
|
13
|
+
inherit `AexApiError`, which carries the HTTP `status`, the redacted parsed
|
|
14
|
+
`body`, the server's stable `apiCode` (a machine-branchable identity distinct
|
|
15
|
+
from the human `message`), and a `requestId` for support correlation. The
|
|
16
|
+
factory dispatches to a subclass by code/status:
|
|
17
|
+
|
|
18
|
+
| Class | Fires for | Extra fields |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| `AexAuthError` | `401` / `403` (unauthorized, forbidden, insufficient_scope, token_invalid/revoked/expired, malformed_token) | `requiredScope` (on `insufficient_scope`) |
|
|
21
|
+
| `AexIdempotencyConflictError` | `409` `idempotency_conflict` | — |
|
|
22
|
+
| `AexNotFoundError` | `404` `not_found` | — |
|
|
23
|
+
| `AexRateLimitError` | `429` (rate_limited, workspace_concurrency_exceeded, workspace_submit_rate_exceeded) | `retryAfterMs` (when advertised) |
|
|
24
|
+
| `AexApiError` (base) | every other stable code (session_busy, run_not_terminal, insufficient_balance, workspace_spend_cap_exceeded, upstream_error, internal_error, …) | — |
|
|
25
|
+
|
|
26
|
+
Branch with the exported guards instead of parsing bodies or matching status
|
|
27
|
+
codes: `isAuthError`, `isInsufficientScope`, `isIdempotencyConflict`,
|
|
28
|
+
`isNotFound`, and `isRateLimited`.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { isInsufficientScope, isIdempotencyConflict } from "@aexhq/sdk";
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await aex.run(config);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (isInsufficientScope(err)) {
|
|
37
|
+
// err.requiredScope names the missing scope; mint a key that includes it.
|
|
38
|
+
} else if (isIdempotencyConflict(err)) {
|
|
39
|
+
// same key, different body — use a fresh key or resend the byte-identical body.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The **stable** `apiCode` set the SDK types and dispatches on is: `unauthorized`,
|
|
45
|
+
`forbidden`, `insufficient_scope`, `token_invalid`, `token_revoked`,
|
|
46
|
+
`token_expired`, `malformed_token`, `not_found`, `idempotency_conflict`,
|
|
47
|
+
`session_busy`, `run_not_terminal`, `unknown_workspace`,
|
|
48
|
+
`workspace_concurrency_exceeded`, `workspace_submit_rate_exceeded`,
|
|
49
|
+
`workspace_spend_cap_exceeded`, `insufficient_balance`, `rate_limited`,
|
|
50
|
+
`upstream_error`, `internal_error`. A code outside this set (e.g. a validation
|
|
51
|
+
`error` a route reports) still surfaces as an `AexApiError` with the `status` and
|
|
52
|
+
`body.error` preserved; `apiCode` is then `undefined`.
|
|
53
|
+
|
|
54
|
+
Transport failures with no HTTP response (DNS, connection refused, TLS reset)
|
|
55
|
+
surface as `AexNetworkError`; client-side config validation surfaces as
|
|
56
|
+
`RunConfigValidationError` (`err.code === "RUN_CONFIG_INVALID"`) before any
|
|
57
|
+
request is sent.
|
|
11
58
|
|
|
12
59
|
## 401 — authentication
|
|
13
60
|
|
|
@@ -118,7 +165,20 @@ gates enforce. See [Limits & quotas](limits-and-quotas.md).
|
|
|
118
165
|
## 404 — not found
|
|
119
166
|
|
|
120
167
|
`not_found`: the id does not exist **or** belongs to another workspace (aex
|
|
121
|
-
does not distinguish the two).
|
|
168
|
+
does not distinguish the two). The SDK raises `AexNotFoundError` (guard:
|
|
169
|
+
`isNotFound(err)`).
|
|
170
|
+
|
|
171
|
+
## 409 — idempotency conflict
|
|
172
|
+
|
|
173
|
+
`idempotency_conflict`: the `idempotencyKey` was already used with a **different**
|
|
174
|
+
request body. The SDK raises `AexIdempotencyConflictError` (guard:
|
|
175
|
+
`isIdempotencyConflict(err)`).
|
|
176
|
+
|
|
177
|
+
**Remedy:** use a fresh idempotency key for a genuinely new request, or resubmit
|
|
178
|
+
the byte-identical body to replay the original result (a matching retry returns
|
|
179
|
+
the existing session rather than conflicting). Note that the SDK validates the
|
|
180
|
+
key client-side first: an empty or whitespace-only `idempotencyKey` throws
|
|
181
|
+
`RunConfigValidationError` before the request is sent — never pass `""`.
|
|
122
182
|
|
|
123
183
|
## 5xx — server errors
|
|
124
184
|
|
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/mcp.md
CHANGED
|
@@ -29,8 +29,9 @@ For ingestion-style MCP servers that return large JSON blobs (search results,
|
|
|
29
29
|
catalogue dumps, bulk reads), prefer a skill that writes files instead of
|
|
30
30
|
putting the whole response in model context:
|
|
31
31
|
|
|
32
|
-
1. Package the upstream as a
|
|
33
|
-
|
|
32
|
+
1. Package the upstream helper as a Skill (`Skill.fromDir` / `Skill.fromUrl`)
|
|
33
|
+
and pass it via the top-level `skills` option. The skill can include a CLI
|
|
34
|
+
binary or script that the agent invokes with its bash tool.
|
|
34
35
|
2. Keep any upstream HTTPS credentials in `environment.secrets`.
|
|
35
36
|
3. Have the CLI write the full payload to the session filesystem. By default,
|
|
36
37
|
files it creates or modifies are captured automatically; pass
|
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
|
|
|
@@ -40,7 +40,7 @@ All new submissions run on the managed runtime. Public support is expressed as s
|
|
|
40
40
|
|
|
41
41
|
## Skills
|
|
42
42
|
|
|
43
|
-
Skills are supplied
|
|
43
|
+
Skills are supplied through the top-level `skills` option. Build one with `Skill.fromDir`, `Skill.fromUrl`, `Skill.fromFiles`, `Skill.fromContent`, or `Skill.fromBytes`; each normalizes to a named workspace skill that the platform snapshots into durable run asset storage.
|
|
44
44
|
|
|
45
45
|
Notes:
|
|
46
46
|
|
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`, `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 `
|
|
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.
|