@aexhq/sdk 0.40.6 → 0.40.8

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 (53) hide show
  1. package/README.md +159 -159
  2. package/dist/_contracts/asset-upload-helper.d.ts +38 -0
  3. package/dist/_contracts/asset-upload-helper.js +111 -0
  4. package/dist/_contracts/internal.d.ts +5 -9
  5. package/dist/_contracts/internal.js +5 -79
  6. package/dist/_contracts/operations.d.ts +9 -2
  7. package/dist/_contracts/operations.js +141 -16
  8. package/dist/_contracts/retry-core.d.ts +27 -0
  9. package/dist/_contracts/retry-core.js +75 -0
  10. package/dist/_contracts/runtime-types.d.ts +6 -0
  11. package/dist/_contracts/stable.d.ts +10 -5
  12. package/dist/_contracts/stable.js +10 -5
  13. package/dist/asset-upload.d.ts +2 -1
  14. package/dist/asset-upload.js +28 -61
  15. package/dist/asset-upload.js.map +1 -1
  16. package/dist/cli.mjs +297 -80
  17. package/dist/cli.mjs.sha256 +1 -1
  18. package/dist/client.d.ts +5 -0
  19. package/dist/client.js +8 -13
  20. package/dist/client.js.map +1 -1
  21. package/dist/retry.js +13 -41
  22. package/dist/retry.js.map +1 -1
  23. package/dist/version.d.ts +1 -1
  24. package/dist/version.js +1 -1
  25. package/docs/authentication.md +125 -125
  26. package/docs/billing.md +118 -118
  27. package/docs/concepts/agent-tools.md +66 -66
  28. package/docs/concepts/composition.md +37 -37
  29. package/docs/concepts/providers-and-runtimes.md +54 -54
  30. package/docs/concepts/runs.md +49 -49
  31. package/docs/concepts/subagents.md +88 -88
  32. package/docs/credentials.md +125 -125
  33. package/docs/defaults.md +64 -64
  34. package/docs/errors.md +196 -196
  35. package/docs/events.md +243 -243
  36. package/docs/limits-and-quotas.md +144 -144
  37. package/docs/limits.md +50 -50
  38. package/docs/mcp.md +44 -44
  39. package/docs/networking.md +163 -163
  40. package/docs/outputs.md +267 -267
  41. package/docs/public-surface.json +77 -77
  42. package/docs/quickstart.md +119 -119
  43. package/docs/release.md +38 -38
  44. package/docs/retries.md +129 -129
  45. package/docs/run-config.md +58 -58
  46. package/docs/run-record.md +55 -55
  47. package/docs/secrets.md +125 -125
  48. package/docs/testing.md +35 -35
  49. package/docs/vision-skills.md +91 -91
  50. package/docs/webhooks.md +127 -127
  51. package/examples/feature-tour.ts +282 -282
  52. package/examples/spike-settle-latency.ts +125 -125
  53. package/package.json +1 -1
package/README.md CHANGED
@@ -1,159 +1,159 @@
1
- ---
2
- title: aex
3
- ---
4
-
5
- # @aexhq/sdk
6
-
7
- aex is an agent execution platform for launching autonomous agents from a simple TypeScript SDK and CLI.
8
-
9
- The package ships:
10
-
11
- - `Aex` for sessions, one-shot runs, inspect, download, cancel, and delete.
12
- - `sessions` / `openSession()` for durable, resumable agent sessions.
13
- - Typed run primitives: `Models`, `Providers`, `Sizes`, `Skill`, `Tool` / `Tools`, `AgentsMd`, `File`, `McpServer`, and `Secret`.
14
- - A bundled `aex` CLI with the same run, status, events, outputs, download, cancel, delete, and whoami operations.
15
-
16
- ## Install
17
-
18
- ```bash
19
- npm i @aexhq/sdk
20
- ```
21
-
22
- This installs the TypeScript SDK exports and the bundled `aex` CLI. The CLI
23
- ships inside the package — invoke it with `npx aex …` after a local install, or
24
- `npm i -g @aexhq/sdk` to put a bare `aex` on your PATH. Set both credentials
25
- before running the examples: `AEX_API_KEY` authenticates to aex, and
26
- `ANTHROPIC_API_KEY` is your BYOK provider key for Claude.
27
-
28
- ```bash
29
- export AEX_API_KEY="<your-aex-api-key>"
30
- export ANTHROPIC_API_KEY="<your-anthropic-api-key>"
31
- ```
32
-
33
- ## First Session
34
-
35
- ```ts
36
- import { Aex, Models, Sizes } from "@aexhq/sdk";
37
-
38
- const aex = new Aex(process.env.AEX_API_KEY!);
39
-
40
- const session = await aex.openSession({
41
- model: Models.CLAUDE_HAIKU_4_5,
42
- system: "You are a concise engineering assistant.",
43
- runtime: Sizes.SHARED_0_25X_1GB,
44
- // Default is "3m"; set it explicitly when you want a different idle window.
45
- overrides: { idleTtl: "3m" },
46
- apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! }
47
- });
48
-
49
- // done() awaits settle by default, so status is the terminal outcome
50
- // ("succeeded"), and costUsd/usage are populated.
51
- const first = await session.send("Summarize this repo.").done();
52
- console.log(first.status, first.costUsd, first.text);
53
-
54
- const resumed = await aex.openSession(session.id);
55
- await resumed.send("Continue with the follow-up validation.").done();
56
- ```
57
-
58
- Need a one-shot convenience? `run()` opens a session, sends `message` as one
59
- turn, and returns the collected result. The returned `runId` is the session id,
60
- so it can be resumed later with `openSession(runId)`.
61
-
62
- ```ts
63
- const result = await aex.run({
64
- model: Models.CLAUDE_HAIKU_4_5,
65
- apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! },
66
- message: "Write the report and save outputs."
67
- });
68
-
69
- console.log(result.runId, result.status, result.text);
70
- ```
71
-
72
- For multiple providers, include each BYOK key in `apiKeys`:
73
-
74
- ```ts
75
- await aex.run({
76
- model: Models.CLAUDE_HAIKU_4_5,
77
- apiKeys: {
78
- anthropic: process.env.ANTHROPIC_API_KEY!,
79
- openai: process.env.OPENAI_API_KEY!
80
- },
81
- message: "Delegate research to a subagent."
82
- });
83
- ```
84
-
85
- The same request can run from the bundled CLI (`npx aex` on a local install):
86
-
87
- ```bash
88
- npx aex run \
89
- --api-key "$AEX_API_KEY" \
90
- --anthropic-api-key "$ANTHROPIC_API_KEY" \
91
- --model claude-haiku-4-5 \
92
- --prompt "Write the report and save outputs." \
93
- --follow
94
- ```
95
-
96
- ## CLI: login, discovery, typed errors
97
-
98
- Stop re-passing `--api-key` on every command — log in once and the token (plus
99
- your default `--aex-url`) is persisted to a `0600` config file
100
- (`$XDG_CONFIG_HOME/aex/config.json` or `~/.config/aex/config.json`; `%APPDATA%\aex\config.json`
101
- on Windows). An explicit `--api-key` flag always overrides the stored one.
102
-
103
- ```bash
104
- npx aex login --api-key "$AEX_API_KEY" [--aex-url https://api.aex.dev]
105
- npx aex whoami # no --api-key needed after login
106
- npx aex whoami --json # machine-readable workspace + scopes + limits
107
- npx aex auth status # show the resolved config (the token value is never printed)
108
- npx aex logout # clear the stored token
109
- ```
110
-
111
- Discover the closed sets the platform accepts — no token, no network (human table
112
- by default, machine JSON under `--json`). Per-verb `--help` is also key-free:
113
-
114
- ```bash
115
- npx aex models list # canonical models + their default provider
116
- npx aex providers list # providers + the models each serves
117
- npx aex tools list # complete builtin tool set
118
- npx aex runtime-sizes list # managed runtime presets (cpus / memory / default)
119
- npx aex run --help # flags for one verb (no API key required)
120
- ```
121
-
122
- Errors are typed and actionable. Every `openSession()` / `run()` config-validation
123
- failure throws a `RunConfigValidationError` (`err.code === "RUN_CONFIG_INVALID"`)
124
- you can `catch` by code; CLI failures print a JSON envelope carrying the HTTP
125
- `status`, a one-line `remedy`, and the `runId` where known, and a wrong `--model`,
126
- `--provider`, or `--runtime-size` gets a "did you mean?" suggestion.
127
-
128
- ## Continue with sessions
129
-
130
- Use sessions for conversational flows. `run()` is the one-shot convenience; a
131
- `SessionHandle` is the lower-level surface when you want multiple turns,
132
- streaming, messages, events, outputs, or downloads.
133
-
134
- ```ts
135
- const session = await aex.openSession(result.runId);
136
- const next = await session.send("Turn this into a checklist.").done();
137
- console.log(next.text);
138
-
139
- const messages = await session.messages().list();
140
- const outputs = await aex.sessions.outputs(session.id).list();
141
- ```
142
-
143
- ## Feature Areas
144
-
145
- - **Agent runtime:** managed autonomous runs with filesystem read/edit, grep/glob/head/tail, open web fetch/search, background commands, code execution, git, and subagents.
146
- - **Durable infrastructure:** run records, status, wait/cancel/delete, idempotency, typed events, output capture, downloads, timeouts, and runtime sizes.
147
- - **Agent composition:** skills, files, AGENTS.md, remote MCP servers, environment variables, packages, and networking controls.
148
- - **Subagents:** typed parent/child lineage for async child runs, output handoff, and bounded agent delegation.
149
- - **Models and providers:** Anthropic, DeepSeek, OpenAI, Gemini, Mistral, OpenRouter, Doubao, and Doubao China behind one submission shape.
150
- - **Typed control surface:** strongly typed SDK inputs, CLI parity, BYOK provider keys, workspace secrets, redaction, and output modes.
151
-
152
- ## Docs
153
-
154
- - [Quickstart](docs/quickstart.md)
155
- - [Run configuration](docs/run-config.md)
156
- - [Composition](docs/concepts/composition.md)
157
- - [Secrets](docs/secrets.md)
158
- - [Limits](docs/limits.md)
159
- - [Provider/runtime capabilities](docs/provider-runtime-capabilities.md)
1
+ ---
2
+ title: aex
3
+ ---
4
+
5
+ # @aexhq/sdk
6
+
7
+ aex is an agent execution platform for launching autonomous agents from a simple TypeScript SDK and CLI.
8
+
9
+ The package ships:
10
+
11
+ - `Aex` for sessions, one-shot runs, inspect, download, cancel, and delete.
12
+ - `sessions` / `openSession()` for durable, resumable agent sessions.
13
+ - Typed run primitives: `Models`, `Providers`, `Sizes`, `Skill`, `Tool` / `Tools`, `AgentsMd`, `File`, `McpServer`, and `Secret`.
14
+ - A bundled `aex` CLI with the same run, status, events, outputs, download, cancel, delete, and whoami operations.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm i @aexhq/sdk
20
+ ```
21
+
22
+ This installs the TypeScript SDK exports and the bundled `aex` CLI. The CLI
23
+ ships inside the package — invoke it with `npx aex …` after a local install, or
24
+ `npm i -g @aexhq/sdk` to put a bare `aex` on your PATH. Set both credentials
25
+ before running the examples: `AEX_API_KEY` authenticates to aex, and
26
+ `ANTHROPIC_API_KEY` is your BYOK provider key for Claude.
27
+
28
+ ```bash
29
+ export AEX_API_KEY="<your-aex-api-key>"
30
+ export ANTHROPIC_API_KEY="<your-anthropic-api-key>"
31
+ ```
32
+
33
+ ## First Session
34
+
35
+ ```ts
36
+ import { Aex, Models, Sizes } from "@aexhq/sdk";
37
+
38
+ const aex = new Aex(process.env.AEX_API_KEY!);
39
+
40
+ const session = await aex.openSession({
41
+ model: Models.CLAUDE_HAIKU_4_5,
42
+ system: "You are a concise engineering assistant.",
43
+ runtime: Sizes.SHARED_0_25X_1GB,
44
+ // Default is "3m"; set it explicitly when you want a different idle window.
45
+ overrides: { idleTtl: "3m" },
46
+ apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! }
47
+ });
48
+
49
+ // done() awaits settle by default, so status is the terminal outcome
50
+ // ("succeeded"), and costUsd/usage are populated.
51
+ const first = await session.send("Summarize this repo.").done();
52
+ console.log(first.status, first.costUsd, first.text);
53
+
54
+ const resumed = await aex.openSession(session.id);
55
+ await resumed.send("Continue with the follow-up validation.").done();
56
+ ```
57
+
58
+ Need a one-shot convenience? `run()` opens a session, sends `message` as one
59
+ turn, and returns the collected result. The returned `runId` is the session id,
60
+ so it can be resumed later with `openSession(runId)`.
61
+
62
+ ```ts
63
+ const result = await aex.run({
64
+ model: Models.CLAUDE_HAIKU_4_5,
65
+ apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! },
66
+ message: "Write the report and save outputs."
67
+ });
68
+
69
+ console.log(result.runId, result.status, result.text);
70
+ ```
71
+
72
+ For multiple providers, include each BYOK key in `apiKeys`:
73
+
74
+ ```ts
75
+ await aex.run({
76
+ model: Models.CLAUDE_HAIKU_4_5,
77
+ apiKeys: {
78
+ anthropic: process.env.ANTHROPIC_API_KEY!,
79
+ openai: process.env.OPENAI_API_KEY!
80
+ },
81
+ message: "Delegate research to a subagent."
82
+ });
83
+ ```
84
+
85
+ The same request can run from the bundled CLI (`npx aex` on a local install):
86
+
87
+ ```bash
88
+ npx aex run \
89
+ --api-key "$AEX_API_KEY" \
90
+ --anthropic-api-key "$ANTHROPIC_API_KEY" \
91
+ --model claude-haiku-4-5 \
92
+ --prompt "Write the report and save outputs." \
93
+ --follow
94
+ ```
95
+
96
+ ## CLI: login, discovery, typed errors
97
+
98
+ Stop re-passing `--api-key` on every command — log in once and the token (plus
99
+ your default `--aex-url`) is persisted to a `0600` config file
100
+ (`$XDG_CONFIG_HOME/aex/config.json` or `~/.config/aex/config.json`; `%APPDATA%\aex\config.json`
101
+ on Windows). An explicit `--api-key` flag always overrides the stored one.
102
+
103
+ ```bash
104
+ npx aex login --api-key "$AEX_API_KEY" [--aex-url https://api.aex.dev]
105
+ npx aex whoami # no --api-key needed after login
106
+ npx aex whoami --json # machine-readable workspace + scopes + limits
107
+ npx aex auth status # show the resolved config (the token value is never printed)
108
+ npx aex logout # clear the stored token
109
+ ```
110
+
111
+ Discover the closed sets the platform accepts — no token, no network (human table
112
+ by default, machine JSON under `--json`). Per-verb `--help` is also key-free:
113
+
114
+ ```bash
115
+ npx aex models list # canonical models + their default provider
116
+ npx aex providers list # providers + the models each serves
117
+ npx aex tools list # complete builtin tool set
118
+ npx aex runtime-sizes list # managed runtime presets (cpus / memory / default)
119
+ npx aex run --help # flags for one verb (no API key required)
120
+ ```
121
+
122
+ Errors are typed and actionable. Every `openSession()` / `run()` config-validation
123
+ failure throws a `RunConfigValidationError` (`err.code === "RUN_CONFIG_INVALID"`)
124
+ you can `catch` by code; CLI failures print a JSON envelope carrying the HTTP
125
+ `status`, a one-line `remedy`, and the `runId` where known, and a wrong `--model`,
126
+ `--provider`, or `--runtime-size` gets a "did you mean?" suggestion.
127
+
128
+ ## Continue with sessions
129
+
130
+ Use sessions for conversational flows. `run()` is the one-shot convenience; a
131
+ `SessionHandle` is the lower-level surface when you want multiple turns,
132
+ streaming, messages, events, outputs, or downloads.
133
+
134
+ ```ts
135
+ const session = await aex.openSession(result.runId);
136
+ const next = await session.send("Turn this into a checklist.").done();
137
+ console.log(next.text);
138
+
139
+ const messages = await session.messages().list();
140
+ const outputs = await aex.sessions.outputs(session.id).list();
141
+ ```
142
+
143
+ ## Feature Areas
144
+
145
+ - **Agent runtime:** managed autonomous runs with filesystem read/edit, grep/glob/head/tail, open web fetch/search, background commands, code execution, git, and subagents.
146
+ - **Durable infrastructure:** run records, status, wait/cancel/delete, idempotency, typed events, output capture, downloads, timeouts, and runtime sizes.
147
+ - **Agent composition:** skills, files, AGENTS.md, remote MCP servers, environment variables, packages, and networking controls.
148
+ - **Subagents:** typed parent/child lineage for async child runs, output handoff, and bounded agent delegation.
149
+ - **Models and providers:** Anthropic, DeepSeek, OpenAI, Gemini, Mistral, OpenRouter, Doubao, and Doubao China behind one submission shape.
150
+ - **Typed control surface:** strongly typed SDK inputs, CLI parity, BYOK provider keys, workspace secrets, redaction, and output modes.
151
+
152
+ ## Docs
153
+
154
+ - [Quickstart](docs/quickstart.md)
155
+ - [Run configuration](docs/run-config.md)
156
+ - [Composition](docs/concepts/composition.md)
157
+ - [Secrets](docs/secrets.md)
158
+ - [Limits](docs/limits.md)
159
+ - [Provider/runtime capabilities](docs/provider-runtime-capabilities.md)
@@ -0,0 +1,38 @@
1
+ import { type RetryBackoffConfig } from "./retry-core.js";
2
+ export interface AssetUploadResponse {
3
+ readonly ok: boolean;
4
+ readonly status: number;
5
+ text(): Promise<string>;
6
+ readonly headers?: {
7
+ get(name: string): string | null;
8
+ };
9
+ }
10
+ /** Minimal fetch shape for direct-to-storage PUT requests. */
11
+ export type AssetFetch = (input: string, init?: RequestInit) => Promise<AssetUploadResponse>;
12
+ export declare const DIRECT_UPLOAD_MAX_ATTEMPTS = 3;
13
+ export declare const DIRECT_UPLOAD_INITIAL_DELAY_MS = 500;
14
+ export declare const DIRECT_UPLOAD_MAX_DELAY_MS = 5000;
15
+ export declare const DIRECT_UPLOAD_MAX_ELAPSED_MS = 30000;
16
+ export type AssetUploadSleep = (ms: number, signal?: AbortSignal) => Promise<void>;
17
+ export interface AssetUploadRetryOptions {
18
+ readonly maxAttempts?: number;
19
+ readonly initialDelayMs?: number;
20
+ readonly maxDelayMs?: number;
21
+ readonly maxElapsedMs?: number;
22
+ readonly sleep?: AssetUploadSleep;
23
+ readonly random?: () => number;
24
+ readonly now?: () => number;
25
+ }
26
+ export interface ResolvedAssetUploadRetryConfig extends RetryBackoffConfig {
27
+ readonly maxAttempts: number;
28
+ readonly maxElapsedMs: number;
29
+ }
30
+ export declare function putDirectUploadWithRetry(fetchImpl: AssetFetch, uploadUrl: string, init: RequestInit, options?: AssetUploadRetryOptions): Promise<void>;
31
+ export declare function resolveAssetUploadRetryConfig(options?: AssetUploadRetryOptions): ResolvedAssetUploadRetryConfig;
32
+ export declare function directUploadRetryDelayMs(config: RetryBackoffConfig, attempt: number, random: () => number, retryAfterMs: number | undefined): number;
33
+ export declare function withinDirectUploadRetryBudget(config: Pick<ResolvedAssetUploadRetryConfig, "maxElapsedMs">, startedAt: number, delayMs: number, now: () => number): boolean;
34
+ export declare function isRetryableUploadStatus(status: number): boolean;
35
+ export declare function isRetryableUploadError(err: unknown): boolean;
36
+ export declare function directUploadNetworkError(uploadUrl: string, err: unknown, attempts: number): Error;
37
+ export declare function directUploadResponseError(uploadUrl: string, status: number, detail: string, attempts: number): Error;
38
+ export declare function sanitizeUploadText(text: string): string;
@@ -0,0 +1,111 @@
1
+ import { extractErrorCode, redactUrl } from "./sdk-errors.js";
2
+ import { abortableSleep, computeRetryDelayMs, parseRetryAfterMs } from "./retry-core.js";
3
+ export const DIRECT_UPLOAD_MAX_ATTEMPTS = 3;
4
+ export const DIRECT_UPLOAD_INITIAL_DELAY_MS = 500;
5
+ export const DIRECT_UPLOAD_MAX_DELAY_MS = 5_000;
6
+ export const DIRECT_UPLOAD_MAX_ELAPSED_MS = 30_000;
7
+ export async function putDirectUploadWithRetry(fetchImpl, uploadUrl, init, options = {}) {
8
+ const config = resolveAssetUploadRetryConfig(options);
9
+ const sleep = options.sleep ?? abortableSleep;
10
+ const random = options.random ?? Math.random;
11
+ const now = options.now ?? Date.now;
12
+ const startedAt = now();
13
+ for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
14
+ let response;
15
+ try {
16
+ response = await fetchImpl(uploadUrl, init);
17
+ }
18
+ catch (err) {
19
+ if (attempt < config.maxAttempts && isRetryableUploadError(err)) {
20
+ const delay = directUploadRetryDelayMs(config, attempt, random, undefined);
21
+ if (!withinDirectUploadRetryBudget(config, startedAt, delay, now)) {
22
+ throw directUploadNetworkError(uploadUrl, err, attempt);
23
+ }
24
+ await sleep(delay, init.signal ?? undefined);
25
+ continue;
26
+ }
27
+ throw directUploadNetworkError(uploadUrl, err, attempt);
28
+ }
29
+ if (response.ok)
30
+ return;
31
+ const retryAfterMs = parseRetryAfterMs(response.headers?.get("retry-after"), now());
32
+ if (attempt < config.maxAttempts && isRetryableUploadStatus(response.status)) {
33
+ const delay = directUploadRetryDelayMs(config, attempt, random, retryAfterMs);
34
+ if (!withinDirectUploadRetryBudget(config, startedAt, delay, now)) {
35
+ const detail = await response.text().catch(() => "");
36
+ throw directUploadResponseError(uploadUrl, response.status, detail, attempt);
37
+ }
38
+ await response.text().catch(() => "");
39
+ await sleep(delay, init.signal ?? undefined);
40
+ continue;
41
+ }
42
+ const detail = await response.text().catch(() => "");
43
+ throw directUploadResponseError(uploadUrl, response.status, detail, attempt);
44
+ }
45
+ }
46
+ export function resolveAssetUploadRetryConfig(options = {}) {
47
+ const maxAttempts = Math.max(1, Math.floor(options.maxAttempts ?? DIRECT_UPLOAD_MAX_ATTEMPTS));
48
+ const initialDelayMs = Math.max(0, options.initialDelayMs ?? DIRECT_UPLOAD_INITIAL_DELAY_MS);
49
+ const maxDelayMs = Math.max(initialDelayMs, options.maxDelayMs ?? DIRECT_UPLOAD_MAX_DELAY_MS);
50
+ const maxElapsedMs = Math.max(0, options.maxElapsedMs ?? DIRECT_UPLOAD_MAX_ELAPSED_MS);
51
+ return { maxAttempts, initialDelayMs, maxDelayMs, maxElapsedMs };
52
+ }
53
+ export function directUploadRetryDelayMs(config, attempt, random, retryAfterMs) {
54
+ return computeRetryDelayMs(config, attempt, random, retryAfterMs);
55
+ }
56
+ export function withinDirectUploadRetryBudget(config, startedAt, delayMs, now) {
57
+ return now() - startedAt + delayMs <= config.maxElapsedMs;
58
+ }
59
+ export function isRetryableUploadStatus(status) {
60
+ return status === 408 || status === 425 || status === 429 || (status >= 500 && status <= 599);
61
+ }
62
+ export function isRetryableUploadError(err) {
63
+ if (isNamedError(err, "AbortError"))
64
+ return false;
65
+ return true;
66
+ }
67
+ export function directUploadNetworkError(uploadUrl, err, attempts) {
68
+ const safeUrl = redactUrl(uploadUrl);
69
+ const code = extractErrorCode(err);
70
+ const detail = sanitizeUploadText(errorMessage(err)).slice(0, 500);
71
+ return new Error(`uploadAsset: direct upload PUT failed for ${safeUrl} after ${attemptsLabel(attempts)}` +
72
+ (code ? ` (${code})` : "") +
73
+ (detail ? `: ${detail}` : ""));
74
+ }
75
+ export function directUploadResponseError(uploadUrl, status, detail, attempts) {
76
+ const safeUrl = redactUrl(uploadUrl);
77
+ const safeDetail = sanitizeUploadText(detail).slice(0, 500);
78
+ return new Error(`uploadAsset: direct upload PUT failed for ${safeUrl} with status ${status}` +
79
+ (attempts > 1 ? ` after ${attemptsLabel(attempts)}` : "") +
80
+ (safeDetail ? `: ${safeDetail}` : ""));
81
+ }
82
+ export function sanitizeUploadText(text) {
83
+ return text
84
+ .replace(/https?:\/\/[^\s<>"'`]+/g, (raw) => redactUrlPreservingTrailingPunctuation(raw))
85
+ .replace(/\b(?:X-Amz-(?:Algorithm|Credential|Date|Expires|Security-Token|Signature|SignedHeaders)|AWSAccessKeyId|Signature|Credential|Security-Token|AccessKeyId|SecretAccessKey|SessionToken)=([^&\s<>"'`]+)/gi, "[redacted]")
86
+ .replace(/\bAKIA[0-9A-Z]{8,}\b/g, "[redacted]");
87
+ }
88
+ function attemptsLabel(attempts) {
89
+ return attempts === 1 ? "1 attempt" : `${attempts} attempts`;
90
+ }
91
+ function errorMessage(err) {
92
+ if (err instanceof Error)
93
+ return err.message || err.name;
94
+ if (typeof err === "string")
95
+ return err;
96
+ return String(err);
97
+ }
98
+ function isNamedError(err, name) {
99
+ return stringProperty(err, "name") === name;
100
+ }
101
+ function stringProperty(value, key) {
102
+ if (!value || typeof value !== "object")
103
+ return undefined;
104
+ const prop = value[key];
105
+ return typeof prop === "string" && prop.length > 0 ? prop : undefined;
106
+ }
107
+ function redactUrlPreservingTrailingPunctuation(raw) {
108
+ const trailing = raw.match(/[),.;:!?]+$/)?.[0] ?? "";
109
+ const candidate = trailing ? raw.slice(0, -trailing.length) : raw;
110
+ return `${redactUrl(candidate)}${trailing}`;
111
+ }
@@ -1,6 +1,10 @@
1
+ import { type AssetFetch, type AssetUploadRetryOptions } from "./asset-upload-helper.js";
1
2
  export * from "./models.js";
2
3
  export * from "./post-hook.js";
4
+ export * from "./retry-core.js";
3
5
  export * from "./submission.js";
6
+ export { DIRECT_UPLOAD_MAX_ATTEMPTS, DIRECT_UPLOAD_INITIAL_DELAY_MS, DIRECT_UPLOAD_MAX_DELAY_MS, DIRECT_UPLOAD_MAX_ELAPSED_MS, directUploadRetryDelayMs, directUploadNetworkError, directUploadResponseError, isRetryableUploadError, isRetryableUploadStatus, putDirectUploadWithRetry, resolveAssetUploadRetryConfig, withinDirectUploadRetryBudget, sanitizeUploadText } from "./asset-upload-helper.js";
7
+ export type { AssetFetch, AssetUploadResponse, AssetUploadRetryOptions, AssetUploadSleep, ResolvedAssetUploadRetryConfig } from "./asset-upload-helper.js";
4
8
  /**
5
9
  * Subset of `HttpClient` needed by the asset uploader. Defined structurally so
6
10
  * SDK and CLI tests can supply thin stubs without dragging in the full client.
@@ -8,15 +12,6 @@ export * from "./submission.js";
8
12
  export interface AssetsHttpClient {
9
13
  request<T>(path: string, init?: RequestInit, query?: Record<string, string>): Promise<T>;
10
14
  }
11
- /** Minimal fetch shape for the direct-to-storage PUT. */
12
- export type AssetFetch = (input: string, init?: RequestInit) => Promise<{
13
- readonly ok: boolean;
14
- readonly status: number;
15
- text(): Promise<string>;
16
- readonly headers?: {
17
- get(name: string): string | null;
18
- };
19
- }>;
20
15
  export interface UploadAssetArgs {
21
16
  readonly http: AssetsHttpClient;
22
17
  readonly bytes: Uint8Array;
@@ -24,6 +19,7 @@ export interface UploadAssetArgs {
24
19
  readonly hash: string;
25
20
  readonly contentType?: string;
26
21
  readonly fetch?: AssetFetch;
22
+ readonly retry?: AssetUploadRetryOptions;
27
23
  }
28
24
  export interface UploadedAsset {
29
25
  readonly assetId: string;
@@ -1,4 +1,4 @@
1
- import { extractErrorCode, redactUrl } from "./sdk-errors.js";
1
+ import { putDirectUploadWithRetry } from "./asset-upload-helper.js";
2
2
  // Workspace-internal entry point. Re-exports submission building blocks and
3
3
  // hosts small shared helpers so public packages can avoid hand-mirroring
4
4
  // behavior. NOT part of the public `@aexhq/contracts` surface; do NOT add this
@@ -6,8 +6,9 @@ import { extractErrorCode, redactUrl } from "./sdk-errors.js";
6
6
  // `@aexhq/contracts/internal` subpath.
7
7
  export * from "./models.js";
8
8
  export * from "./post-hook.js";
9
+ export * from "./retry-core.js";
9
10
  export * from "./submission.js";
10
- const DIRECT_UPLOAD_MAX_ATTEMPTS = 3;
11
+ export { DIRECT_UPLOAD_MAX_ATTEMPTS, DIRECT_UPLOAD_INITIAL_DELAY_MS, DIRECT_UPLOAD_MAX_DELAY_MS, DIRECT_UPLOAD_MAX_ELAPSED_MS, directUploadRetryDelayMs, directUploadNetworkError, directUploadResponseError, isRetryableUploadError, isRetryableUploadStatus, putDirectUploadWithRetry, resolveAssetUploadRetryConfig, withinDirectUploadRetryBudget, sanitizeUploadText } from "./asset-upload-helper.js";
11
12
  /**
12
13
  * Upload `bytes` to the hosted API's content-addressable asset store via the
13
14
  * direct-to-storage presign flow shared by SDK and CLI callers.
@@ -42,11 +43,11 @@ export async function uploadAsset(args) {
42
43
  "content-type": args.contentType ?? "application/zip",
43
44
  ...(presign.requiredHeaders ?? {})
44
45
  };
45
- await putWithRetry(doFetch, presign.uploadUrl, {
46
+ await putDirectUploadWithRetry(doFetch, presign.uploadUrl, {
46
47
  method: "PUT",
47
48
  headers: putHeaders,
48
49
  body: args.bytes
49
- });
50
+ }, args.retry);
50
51
  const fin = await args.http.request("/assets/finalize", {
51
52
  method: "POST",
52
53
  headers: { "content-type": "application/json" },
@@ -73,81 +74,6 @@ function assetIdFromContentHash(contentHash) {
73
74
  const hex = contentHash.startsWith("sha256:") ? contentHash.slice("sha256:".length) : contentHash;
74
75
  return `asset_${hex}`;
75
76
  }
76
- async function putWithRetry(fetchImpl, uploadUrl, init) {
77
- for (let attempt = 1; attempt <= DIRECT_UPLOAD_MAX_ATTEMPTS; attempt++) {
78
- let response;
79
- try {
80
- response = await fetchImpl(uploadUrl, init);
81
- }
82
- catch (err) {
83
- if (attempt < DIRECT_UPLOAD_MAX_ATTEMPTS && isRetryableUploadError(err)) {
84
- continue;
85
- }
86
- throw directUploadNetworkError(uploadUrl, err, attempt);
87
- }
88
- if (response.ok)
89
- return;
90
- if (attempt < DIRECT_UPLOAD_MAX_ATTEMPTS && isRetryableUploadStatus(response.status)) {
91
- await response.text().catch(() => "");
92
- continue;
93
- }
94
- const detail = await response.text().catch(() => "");
95
- throw directUploadResponseError(uploadUrl, response.status, detail, attempt);
96
- }
97
- }
98
- function isRetryableUploadStatus(status) {
99
- return status === 408 || status === 425 || status === 429 || (status >= 500 && status <= 599);
100
- }
101
- function isRetryableUploadError(err) {
102
- if (isNamedError(err, "AbortError"))
103
- return false;
104
- return true;
105
- }
106
- function directUploadNetworkError(uploadUrl, err, attempts) {
107
- const safeUrl = redactUrl(uploadUrl);
108
- const code = extractErrorCode(err);
109
- const detail = sanitizeUploadText(errorMessage(err)).slice(0, 500);
110
- return new Error(`uploadAsset: direct upload PUT failed for ${safeUrl} after ${attemptsLabel(attempts)}` +
111
- (code ? ` (${code})` : "") +
112
- (detail ? `: ${detail}` : ""));
113
- }
114
- function directUploadResponseError(uploadUrl, status, detail, attempts) {
115
- const safeUrl = redactUrl(uploadUrl);
116
- const safeDetail = sanitizeUploadText(detail).slice(0, 500);
117
- return new Error(`uploadAsset: direct upload PUT failed for ${safeUrl} with status ${status}` +
118
- (attempts > 1 ? ` after ${attemptsLabel(attempts)}` : "") +
119
- (safeDetail ? `: ${safeDetail}` : ""));
120
- }
121
- function attemptsLabel(attempts) {
122
- return attempts === 1 ? "1 attempt" : `${attempts} attempts`;
123
- }
124
- function errorMessage(err) {
125
- if (err instanceof Error)
126
- return err.message || err.name;
127
- if (typeof err === "string")
128
- return err;
129
- return String(err);
130
- }
131
- function isNamedError(err, name) {
132
- return stringProperty(err, "name") === name;
133
- }
134
- function stringProperty(value, key) {
135
- if (!value || typeof value !== "object")
136
- return undefined;
137
- const prop = value[key];
138
- return typeof prop === "string" && prop.length > 0 ? prop : undefined;
139
- }
140
- function sanitizeUploadText(text) {
141
- return text
142
- .replace(/https?:\/\/[^\s<>"'`]+/g, (raw) => redactUrlPreservingTrailingPunctuation(raw))
143
- .replace(/\b(?:X-Amz-(?:Algorithm|Credential|Date|Expires|Security-Token|Signature|SignedHeaders)|AWSAccessKeyId|Signature|Credential|Security-Token|AccessKeyId|SecretAccessKey|SessionToken)=([^&\s<>"'`]+)/gi, "[redacted]")
144
- .replace(/\bAKIA[0-9A-Z]{8,}\b/g, "[redacted]");
145
- }
146
- function redactUrlPreservingTrailingPunctuation(raw) {
147
- const trailing = raw.match(/[),.;:!?]+$/)?.[0] ?? "";
148
- const candidate = trailing ? raw.slice(0, -trailing.length) : raw;
149
- return `${redactUrl(candidate)}${trailing}`;
150
- }
151
77
  function bufferToHex(buffer) {
152
78
  const view = new Uint8Array(buffer);
153
79
  let out = "";
@@ -115,11 +115,18 @@ export declare function outputLink(http: HttpClient, runId: string, selectorOrQu
115
115
  export declare function createOutputLink(http: HttpClient, runId: string, selectorOrQuery: OutputLinkSelector, options?: OutputLinkOptions): Promise<OutputLink>;
116
116
  export declare function eventArchiveLink(http: HttpClient, runId: string, options?: OutputLinkOptions): Promise<OutputLink>;
117
117
  export declare function resolveOutputFileSelector(outputs: readonly Output[], selector: OutputFileSelector, runId?: string): Output;
118
- export declare function downloadOutput(http: HttpClient, runId: string, selector: OutputFileSelector): Promise<OutputFileDownload>;
118
+ export declare function downloadOutput(http: HttpClient, runId: string, selector: OutputFileSelector, options?: OutputTransferOptions): Promise<OutputFileDownload>;
119
119
  /** Byte ceiling for {@link readOutputText} — a hard cap even if a caller asks for more. */
120
120
  export declare const READ_OUTPUT_TEXT_MAX_BYTES = 10000000;
121
121
  /** Default `maxBytes` for {@link readOutputText} — a chat-sized preview. */
122
122
  export declare const READ_OUTPUT_TEXT_DEFAULT_BYTES = 50000;
123
+ /** Default per-attempt timeout while fetching or reading one output body. */
124
+ export declare const OUTPUT_FILE_TRANSFER_DEFAULT_TIMEOUT_MS = 15000;
125
+ /** Idempotent output GETs retry once on a transfer timeout. */
126
+ export declare const OUTPUT_FILE_TRANSFER_ATTEMPTS = 2;
127
+ export interface OutputTransferOptions {
128
+ readonly timeoutMs?: number;
129
+ }
123
130
  /**
124
131
  * Read ONE output file as byte-capped, decoded UTF-8 text. Built for handing a run
125
132
  * deliverable to an LLM tool: it streams the file body and STOPS at `maxBytes`, so
@@ -214,7 +221,7 @@ export declare function download(http: HttpClient, runId: string): Promise<Uint8
214
221
  * layout: `<rel>` per file plus a `manifest.json`
215
222
  * (`{ runId, namespace: "outputs", outputs[], errors[] }`).
216
223
  */
217
- export declare function downloadOutputs(http: HttpClient, runId: string): Promise<Uint8Array>;
224
+ export declare function downloadOutputs(http: HttpClient, runId: string, options?: OutputTransferOptions): Promise<Uint8Array>;
218
225
  /**
219
226
  * Download only the event archive (the `events` namespace). Always includes
220
227
  * typed `events.jsonl`.