@aexhq/sdk 0.33.1 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +19 -27
  2. package/dist/_contracts/operations.d.ts +2 -54
  3. package/dist/_contracts/operations.js +2 -87
  4. package/dist/_contracts/run-config.d.ts +19 -13
  5. package/dist/_contracts/run-config.js +6 -33
  6. package/dist/_contracts/run-unit.d.ts +1 -33
  7. package/dist/_contracts/run-unit.js +2 -21
  8. package/dist/_contracts/runtime-sizes.d.ts +2 -2
  9. package/dist/_contracts/runtime-sizes.js +2 -2
  10. package/dist/_contracts/status.d.ts +2 -2
  11. package/dist/_contracts/status.js +3 -0
  12. package/dist/_contracts/submission.d.ts +80 -41
  13. package/dist/_contracts/submission.js +114 -52
  14. package/dist/agents-md.d.ts +5 -5
  15. package/dist/agents-md.js +7 -7
  16. package/dist/agents-md.js.map +1 -1
  17. package/dist/asset-upload.d.ts +4 -4
  18. package/dist/asset-upload.js +4 -4
  19. package/dist/bundle.d.ts +2 -2
  20. package/dist/bundle.js +2 -2
  21. package/dist/cli.mjs +369 -12918
  22. package/dist/cli.mjs.sha256 +1 -1
  23. package/dist/client.d.ts +234 -383
  24. package/dist/client.js +436 -648
  25. package/dist/client.js.map +1 -1
  26. package/dist/data-tools.d.ts +25 -22
  27. package/dist/data-tools.js +75 -62
  28. package/dist/data-tools.js.map +1 -1
  29. package/dist/fetch-archive.js +16 -16
  30. package/dist/fetch-archive.js.map +1 -1
  31. package/dist/file.d.ts +5 -5
  32. package/dist/file.js +7 -7
  33. package/dist/file.js.map +1 -1
  34. package/dist/index.d.ts +11 -9
  35. package/dist/index.js +20 -13
  36. package/dist/index.js.map +1 -1
  37. package/dist/mcp-server.d.ts +4 -4
  38. package/dist/mcp-server.js +4 -4
  39. package/dist/proxy-endpoint.d.ts +4 -4
  40. package/dist/proxy-endpoint.js +1 -1
  41. package/dist/retry.d.ts +162 -0
  42. package/dist/retry.js +320 -0
  43. package/dist/retry.js.map +1 -0
  44. package/dist/secret.d.ts +8 -8
  45. package/dist/secret.js +8 -8
  46. package/dist/secret.js.map +1 -1
  47. package/dist/skill-tool.d.ts +102 -0
  48. package/dist/skill-tool.js +190 -0
  49. package/dist/skill-tool.js.map +1 -0
  50. package/dist/tool.d.ts +1 -1
  51. package/dist/tool.js +3 -3
  52. package/dist/tool.js.map +1 -1
  53. package/dist/version.d.ts +1 -1
  54. package/dist/version.js +1 -1
  55. package/docs/cleanup.md +3 -3
  56. package/docs/concepts/agent-tools.md +6 -25
  57. package/docs/concepts/composition.md +15 -12
  58. package/docs/concepts/providers-and-runtimes.md +3 -3
  59. package/docs/concepts/runs.md +27 -22
  60. package/docs/credentials.md +52 -84
  61. package/docs/defaults.md +6 -6
  62. package/docs/events.md +65 -44
  63. package/docs/limits-and-quotas.md +3 -4
  64. package/docs/mcp.md +3 -3
  65. package/docs/networking.md +8 -8
  66. package/docs/outputs.md +44 -40
  67. package/docs/provider-runtime-capabilities.md +1 -1
  68. package/docs/public-surface.json +2 -2
  69. package/docs/quickstart.md +20 -10
  70. package/docs/retries.md +129 -0
  71. package/docs/run-config.md +12 -14
  72. package/docs/run-record.md +8 -8
  73. package/docs/secrets.md +16 -26
  74. package/docs/skills.md +55 -110
  75. package/docs/vision-skills.md +29 -40
  76. package/examples/chat-corpus.ts +8 -9
  77. package/examples/feature-tour.ts +301 -0
  78. package/package.json +1 -1
  79. package/dist/skill.d.ts +0 -149
  80. package/dist/skill.js +0 -198
  81. package/dist/skill.js.map +0 -1
package/docs/skills.md CHANGED
@@ -4,132 +4,77 @@ title: Skills
4
4
 
5
5
  # Skills
6
6
 
7
- A skill is executable or instructional content that is mounted into a run before
8
- the first agent turn. Every accepted skill ends up as a storage-neutral
9
- `kind:"asset"` reference in the run submission, and the hosted platform snapshots
10
- that asset into durable run asset storage before dispatch.
11
-
12
- There are three sources for skill bytes:
13
-
14
- - **Inline/local draft:** `Skill.fromFiles(...)`, `Skill.fromPath(...)`, or
15
- `Skill.fromUrl(...)` builds a draft in the SDK process. `submit` uploads
16
- it before posting `/runs`.
17
- - **Pre-uploaded workspace asset:** call `await draft.upload(aex)` and reuse the
18
- returned materialized `Skill`, or pass an existing `kind:"asset"` ref from a
19
- config file.
20
- - **Workspace skill catalog:** upload with `aex skills upload` or the dashboard,
21
- then pass the returned record to `Skill.fromCatalog(record)`.
22
-
23
- All three sources normalize to the same content-addressed asset. Identical bytes
24
- dedup by hash, so repeated submissions of the same bundle are no-op uploads.
25
- There is no per-run auto-suffixed `skl_*` row for inline skills.
26
-
27
- ## Materialization
28
-
29
- For each run, the platform copies referenced skill assets into durable run asset
30
- storage (`runs/<runId>/assets/<hash>`) and the runner downloads them into the
31
- workspace under `skills/<name>/`.
32
-
33
- A bundle's `SKILL.md` is mounted at `skills/<name>/SKILL.md`, and the agent's
34
- instructions list each mounted skill with that path. The full skill body stays
35
- on disk, so prompts or `AGENTS.md` guidance that rely on a skill should tell the
36
- agent when to read or use it. Bundles without `SKILL.md` are still mounted as
37
- files at `skills/<name>/`, but nothing prompts the agent to read them; reference
38
- them explicitly from the prompt or your `AGENTS.md`.
39
-
40
- The platform also mounts the `aex` CLI and a per-run manifest into every run.
41
- Skills call managed HTTP proxy endpoints through the mounted CLI
42
- (`aex proxy ...`); see `credentials.md` for the policy and auth model.
43
-
44
- Run-scoped asset copies are part of the run record and are removed by run deletion
45
- or retention cleanup. Catalog assets are separate workspace records: deleting a
46
- catalog skill hard-deletes its metadata and removes the shared asset object only
47
- when no other catalog row still references those bytes. Existing run snapshots
48
- keep their run-scoped copy.
49
-
50
- ## Inline And Local Drafts
51
-
52
- `Skill.fromFiles({ name, files })`, `Skill.fromPath(rootDir, { name })`, and
53
- `Skill.fromUrl(url, { name })` build an unstaged `Skill`. The instance carries
54
- canonical zip bytes and a `sha256:<hex>` content hash.
7
+ A skill is a bundle of instructional or executable content (`SKILL.md` plus any
8
+ supporting files) that the agent can pull into context on demand. In the SDK a
9
+ skill is expressed as a per-skill **load-tool**: it rides in the session's
10
+ `tools` array next to builtin tool names and custom `Tool` bundles, and the model
11
+ loads it by calling it.
12
+
13
+ Build a skill-tool with the `Tools.fromSkill*` factories. Each factory reads a
14
+ skill bundle, lifts the tool `name` and `description` from the `SKILL.md` YAML
15
+ frontmatter (an explicit `{ name }` overrides the frontmatter), canonically
16
+ zips + hashes the bytes, and returns a `SkillTool`:
17
+
18
+ - **Local directory:** `Tools.fromSkillDir(rootDir, { name? })` reads a folder
19
+ that has `SKILL.md` at its root (Bun/Node filesystem runtimes).
20
+ - **Signed URL:** `Tools.fromSkillUrl(url, { name?, sha256?, timeoutMs?, fetch? })`
21
+ fetches a zip archive with `SKILL.md` at the archive root (universal — needs a
22
+ global `fetch`, or pass one).
55
23
 
56
24
  ```ts
57
- import { AgentExecutor, Models, Skill } from "@aexhq/sdk";
25
+ import { Aex, Models, Tools } from "@aexhq/sdk";
58
26
 
59
- const aex = new AgentExecutor({ apiToken });
27
+ const aex = new Aex({ apiToken });
60
28
 
61
- await aex.submit({
29
+ await aex.run({
62
30
  model: Models.CLAUDE_HAIKU_4_5,
63
- prompt,
64
- skills: [await Skill.fromPath("./skills/rules", { name: "rules" })],
65
- secrets: { apiKeys: { anthropic: apiKey } }
31
+ message,
32
+ tools: [await Tools.fromSkillDir("./skills/rules", { name: "rules" })],
33
+ apiKeys: { anthropic: apiKey }
66
34
  });
67
35
  ```
68
36
 
69
- Before it posts `/runs`, the SDK uploads each draft through the asset upload
70
- flow:
37
+ `Tools.fromSkillDir("./skills/rules", …)` resolves relative to the process CWD,
38
+ so run the script from the directory that *contains* `skills/`. The `SKILL.md`
39
+ frontmatter must supply a `description` (max 2048 chars) and, unless you pass
40
+ `{ name }`, a `name`. Names must match the tool-name pattern and must not contain
41
+ `__` (that separator is reserved for MCP tools).
42
+
43
+ ## How a skill-tool rides on the wire
44
+
45
+ Before the session lands, `openSession` / `run` walks the `tools` array and
46
+ uploads each draft skill-tool's bundle through the asset upload flow:
71
47
 
72
48
  1. `POST /assets/presign` checks for a dedup hit and, when needed, returns a
73
49
  signed upload URL.
74
50
  2. The SDK PUTs bytes directly to object storage with the signed checksum headers.
75
51
  3. `POST /assets/finalize` confirms the object exists.
76
52
 
77
- The runner re-verifies the content hash when it downloads the asset.
78
-
79
- ## Pre-Upload For Reuse
80
-
81
- If you want to build a local skill once and reuse the materialized asset across
82
- multiple submissions, upload the draft explicitly:
83
-
84
- ```ts
85
- const draft = await Skill.fromFiles({ name: "rules", files });
86
- const uploaded = await draft.upload(aex);
87
-
88
- await aex.submit({
89
- model: Models.CLAUDE_HAIKU_4_5,
90
- prompt,
91
- skills: [uploaded],
92
- secrets: { apiKeys: { anthropic: apiKey } }
93
- });
94
- ```
95
-
96
- The returned `uploaded` skill carries a plain `kind:"asset"` ref. Submitting it
97
- does not upload bytes again.
98
-
99
- ## Fetch From A Signed URL
100
-
101
- When your app runs in the cloud with limited local storage, host the skill
102
- yourself as a zip archive with `SKILL.md` at the archive root and pass a
103
- temporary signed URL:
104
-
105
- ```ts
106
- const skill = await Skill.fromUrl(signedUrl, {
107
- name: "rules",
108
- sha256: "sha256:<hex>"
109
- });
110
- ```
111
-
112
- `Skill.fromUrl` fetches the archive in the SDK process. The hosted platform does
113
- not fetch the caller-controlled URL. The signed URL only needs to be valid for
114
- this call; the SDK snapshots the bytes into the asset store before the run is
115
- submitted.
53
+ The wire ref then becomes a `{ kind:"skill", assetId, name, description }` entry
54
+ inside `submission.tools`. Identical bytes dedup by content hash, so re-submitting
55
+ the same bundle is a no-op upload; a `SkillTool` instance also caches its resolved
56
+ asset id, so reusing the same instance across sessions skips the re-upload. A URL
57
+ is an ingestion source, not a persistent reference the SDK snapshots the fetched
58
+ bytes into the asset store, and the hosted platform never fetches the
59
+ caller-controlled URL.
116
60
 
117
- ## Workspace Catalog
61
+ ## Loading and materialization
118
62
 
119
- Catalog skills are workspace records backed by the same content-addressed
120
- assets. Use them when a team wants a named, listed skill record:
63
+ The skill-tool's `name` and `description` are what the agent sees in its tool
64
+ list. At run time the model calls the **no-arg load-tool** to pull the skill's
65
+ `SKILL.md` body into context — the description tells the agent when that is worth
66
+ doing.
121
67
 
122
- ```ts
123
- const [record] = await aex.skills.list();
68
+ Independently of whether the model calls the load-tool, the platform copies the
69
+ referenced skill asset into durable run asset storage
70
+ (`runs/<runId>/assets/<hash>`) and the runner **eagerly stages** the bundle's
71
+ files into the workspace under `/workspace/skills/<name>/`. So the `SKILL.md` body
72
+ and every supporting file are on disk from the first turn; the load-tool call is
73
+ how that body enters the model's context, not how the files get written.
124
74
 
125
- await aex.submit({
126
- model: Models.CLAUDE_HAIKU_4_5,
127
- prompt,
128
- skills: [Skill.fromCatalog(record)],
129
- secrets: { apiKeys: { anthropic: apiKey } }
130
- });
131
- ```
75
+ The platform also mounts the `aex` CLI and a per-run manifest into every run.
76
+ Skills call managed HTTP proxy endpoints through the mounted CLI
77
+ (`aex proxy ...`); see [Credentials](credentials.md) for the policy and auth model.
132
78
 
133
- The record must be `ready` and carry a content hash. `Skill.fromCatalog` performs
134
- no upload; it produces a `kind:"asset"` ref directly against bytes already in
135
- the catalog.
79
+ Run-scoped asset copies are part of the run record and are removed by run deletion
80
+ or retention cleanup.
@@ -8,8 +8,8 @@ aex has no built-in vision tool. The agent's `provider`/`model` selects the
8
8
  *reasoning* model — it is not an endpoint a skill can POST an image to mid-run.
9
9
  To give a run image understanding (or to call any other model/HTTP API), ship a
10
10
  **skill** that POSTs to the provider's OpenAI-compatible endpoint **through the
11
- managed proxy**, with the key supplied via `secrets.proxyEndpointAuth`. The raw
12
- key never enters the container.
11
+ managed proxy**, with the key supplied on a `ProxyEndpoint.bearer(...)` instance.
12
+ The raw key never enters the container.
13
13
 
14
14
  This is the same proxy described in `credentials.md` — this page is the worked
15
15
  recipe for the model-API case, which has two wrinkles a plain JSON call does not:
@@ -18,54 +18,44 @@ large enough to need a raised `maxRequestBytes`.
18
18
 
19
19
  The canonical, runnable example lives in the repo at
20
20
  [`examples/vision-skill/`](../../../examples/vision-skill) (`SKILL.md`,
21
- `caption_frame.py`, `verify_frame.py`, `submit_with_vision_skill.mjs`). It
21
+ `caption_frame.py`, `verify_frame.py`, `run_with_vision_skill.mjs`). It
22
22
  captions a frame with ByteDance Doubao Seed Vision (Ark) and returns a per-noun
23
23
  "does the frame depict X?" verdict. Everything below is taken from it.
24
24
 
25
25
  ## 1. Declare the model endpoint as a proxy endpoint
26
26
 
27
- The vision provider's API is just an HTTPS host. Declare it as a `bearer` proxy
28
- endpoint and supply the key in `secrets.proxyEndpointAuth`. The two model-specific
29
- settings are `responseMode: "full"` (so the skill gets the upstream JSON back) and
30
- a raised `maxRequestBytes` (so the base64 image fits):
27
+ The vision provider's API is just an HTTPS host. Declare it with
28
+ `ProxyEndpoint.bearer(...)`, which carries the key on the instance. The two
29
+ model-specific settings are `responseMode: "full"` (so the skill gets the upstream
30
+ JSON back) and a raised `maxRequestBytes` (so the base64 image fits):
31
31
 
32
32
  ```ts
33
- import { AgentExecutor, Models, Skill, ProxyEndpoint, validateProxyAuth } from "@aexhq/sdk";
34
-
35
- const aex = new AgentExecutor({ apiToken: process.env.AEX_API_TOKEN! });
36
-
37
- const proxyEndpoints = [
38
- ProxyEndpoint.bearer({
39
- name: "doubao-ark",
40
- baseUrl: "https://ark.ap-southeast.bytepluses.com", // intl BytePlus gateway
41
- allowMethods: ["POST"],
42
- allowPathPrefixes: ["/api/v3/chat/completions"],
43
- maxRequestBytes: 2_000_000, // base64 image POSTs — see note below
44
- responseMode: "full",
45
- timeoutMs: 60_000
46
- })
47
- ];
48
-
49
- const proxyEndpointAuth = [
50
- { name: "doubao-ark", value: { type: "bearer", token: process.env.DOUBAO_API_KEY! } }
51
- ];
52
-
53
- validateProxyAuth(proxyEndpoints, proxyEndpointAuth); // fail fast at submit time
33
+ import { Aex, Models, Tools, ProxyEndpoint } from "@aexhq/sdk";
34
+
35
+ const aex = new Aex({ apiToken: process.env.AEX_API_TOKEN! });
36
+
37
+ const doubaoArk = ProxyEndpoint.bearer({
38
+ name: "doubao-ark",
39
+ baseUrl: "https://ark.ap-southeast.bytepluses.com", // intl BytePlus gateway
40
+ token: process.env.DOUBAO_API_KEY!,
41
+ allowMethods: ["POST"],
42
+ allowPathPrefixes: ["/api/v3/chat/completions"],
43
+ maxRequestBytes: 2_000_000, // base64 image POSTs — see note below
44
+ responseMode: "full",
45
+ timeoutMs: 60_000
46
+ });
54
47
 
55
- const runId = await aex.submit({
48
+ await aex.run({
56
49
  model: Models.CLAUDE_HAIKU_4_5,
57
- prompt: "…read skills/frame-vision-gate/SKILL.md, then caption + verify the frame…",
58
- skills: [await Skill.fromPath("./vision-skill", { name: "frame-vision-gate" })],
59
- proxyEndpoints,
60
- secrets: {
61
- apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! },
62
- proxyEndpointAuth
63
- }
50
+ message: "…read skills/frame-vision-gate/SKILL.md, then caption + verify the frame…",
51
+ tools: [await Tools.fromSkillDir("./vision-skill", { name: "frame-vision-gate" })],
52
+ proxyEndpoints: [doubaoArk],
53
+ apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! }
64
54
  });
65
55
  ```
66
56
 
67
- `Skill.fromPath("./vision-skill", …)` is resolved relative to the process CWD, so
68
- run the submit script from the directory that *contains* `vision-skill/` (in the
57
+ `Tools.fromSkillDir("./vision-skill", …)` is resolved relative to the process CWD, so
58
+ run the script from the directory that *contains* `vision-skill/` (in the
69
59
  repo, that is `examples/`). The same pattern works for OpenAI, Gemini's
70
60
  OpenAI-compatible endpoint, or any other OpenAI-chat-shaped vision API — only
71
61
  `baseUrl` and the path prefix change.
@@ -147,8 +137,7 @@ so full-res frames do not add payload and model cost without useful signal.
147
137
  - **Host selection.** Use the provider endpoint that matches your account and
148
138
  declare it as the proxy endpoint `baseUrl`.
149
139
  - **Keyless model hosts.** If the upstream takes no credential, declare the
150
- endpoint with `authShape: { type: "none" }` and omit the `proxyEndpointAuth`
151
- entry (see `credentials.md`).
140
+ endpoint with `ProxyEndpoint.none(...)` (see `credentials.md`).
152
141
  - **Response size.** `responseMode: "full"` is required to read the model's reply
153
142
  back. Leave `maxResponseBytes` at its default (`0` = unlimited, streamed) unless
154
143
  you want a truncation cap.
@@ -3,10 +3,9 @@
3
3
  *
4
4
  * Combines the public `@aexhq/sdk` corpus read-tools (`createCorpusTools`) with a
5
5
  * direct Claude chat loop (`@anthropic-ai/sdk`). The importable `@aexhq/sdk` stays
6
- * LLM-vendor-free; the vendor dependency lives only here in the example (and in
7
- * the bundled `aex chat` CLI command).
6
+ * LLM-vendor-free; the vendor dependency lives only here in the example.
8
7
  *
9
- * Run (Bun): ANTHROPIC_API_KEY=… AEX_TOKEN=… bun examples/chat-corpus.ts <runId> [runId…]
8
+ * Run (Bun): ANTHROPIC_API_KEY=… AEX_API_TOKEN=… bun examples/chat-corpus.ts <sessionId> [sessionId…]
10
9
  *
11
10
  * The model answers ONLY from the named runs' outputs (read via the corpus
12
11
  * tools); a run outside the corpus is refused by the tool layer.
@@ -14,17 +13,17 @@
14
13
  import Anthropic from "@anthropic-ai/sdk";
15
14
  import { AgentExecutor, createCorpusTools, DataToolError } from "@aexhq/sdk";
16
15
 
17
- const runIds = process.argv.slice(2);
18
- if (runIds.length === 0) {
19
- console.error("usage: bun examples/chat-corpus.ts <runId> [runId…]");
16
+ const sessionIds = process.argv.slice(2);
17
+ if (sessionIds.length === 0) {
18
+ console.error("usage: bun examples/chat-corpus.ts <sessionId> [sessionId…]");
20
19
  process.exit(2);
21
20
  }
22
21
 
23
22
  const aex = new AgentExecutor({
24
- apiToken: process.env.AEX_TOKEN!,
25
- ...(process.env.AEX_URL ? { baseUrl: process.env.AEX_URL } : {})
23
+ apiToken: process.env.AEX_API_TOKEN!,
24
+ ...(process.env.AEX_API_URL ? { baseUrl: process.env.AEX_API_URL } : {})
26
25
  });
27
- const tools = createCorpusTools(aex, { runIds });
26
+ const tools = createCorpusTools(aex, { sessionIds });
28
27
  const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
29
28
 
30
29
  const SYSTEM =
@@ -0,0 +1,301 @@
1
+ /**
2
+ * SDK feature tour: one managed session that uses typed model/runtime constants,
3
+ * inline AGENTS.md guidance, uploaded files, a custom tool bundle, selected
4
+ * built-in tools, runtime env vars/secrets, streamed events, output reads, and
5
+ * corpus-scoped data tools.
6
+ *
7
+ * Run from the repository root after building the workspace package:
8
+ *
9
+ * AEX_API_TOKEN=... DEEPSEEK_API_KEY=... bun packages/sdk/examples/feature-tour.ts
10
+ *
11
+ * Optional:
12
+ *
13
+ * AEX_API_URL=https://api.aex.dev
14
+ * AEX_FEATURE_TOUR_DOWNLOAD=./feature-tour-session.zip
15
+ * AEX_DEMO_RUNTIME_SECRET=... # mounted as a runtime secret; never printed
16
+ * AEX_DEMO_MCP_URL=https://... # declares an optional remote MCP server
17
+ * AEX_DEMO_MCP_TOKEN=... # optional bearer auth for that MCP server
18
+ */
19
+ import {
20
+ AgentsMd,
21
+ Aex,
22
+ BuiltinTools,
23
+ createCorpusTools,
24
+ File,
25
+ isRateLimited,
26
+ isTextMessage,
27
+ McpServer,
28
+ Models,
29
+ ProxyEndpoint,
30
+ Providers,
31
+ Secret,
32
+ Sizes,
33
+ Tool
34
+ } from "@aexhq/sdk";
35
+
36
+ process.on("uncaughtException", handleFatal);
37
+ process.on("unhandledRejection", handleFatal);
38
+
39
+ const apiToken = required("AEX_API_TOKEN");
40
+ const deepseekKey = required("DEEPSEEK_API_KEY");
41
+ const apiUrl = process.env.AEX_API_URL;
42
+ const demoMcpUrl = process.env.AEX_DEMO_MCP_URL;
43
+ const demoMcpToken = process.env.AEX_DEMO_MCP_TOKEN;
44
+ const demoRuntimeSecret = process.env.AEX_DEMO_RUNTIME_SECRET;
45
+ const downloadPath = process.env.AEX_FEATURE_TOUR_DOWNLOAD;
46
+ const textEncoder = new TextEncoder();
47
+
48
+ const aex = new Aex({
49
+ apiToken,
50
+ ...(apiUrl ? { baseUrl: apiUrl } : {}),
51
+ retry: {
52
+ maxAttempts: 4,
53
+ initialDelayMs: 500,
54
+ maxDelayMs: 10_000,
55
+ maxElapsedMs: 90_000
56
+ }
57
+ });
58
+
59
+ const metricLookup = await Tool.fromFiles({
60
+ name: "metric_lookup",
61
+ description: "Looks up normalized demo metrics for one product line.",
62
+ inputSchema: {
63
+ type: "object",
64
+ additionalProperties: false,
65
+ properties: {
66
+ product: {
67
+ type: "string",
68
+ enum: ["atlas", "beacon", "cinder"],
69
+ description: "Product line to inspect."
70
+ }
71
+ },
72
+ required: ["product"]
73
+ },
74
+ entry: "index.js",
75
+ files: {
76
+ "index.js": `
77
+ const DATA = {
78
+ atlas: { customerCount: 118, activationHealthPct: 72.4, supportTickets: 11 },
79
+ beacon: { customerCount: 74, activationHealthPct: 65.1, supportTickets: 19 },
80
+ cinder: { customerCount: 43, activationHealthPct: 58.8, supportTickets: 7 }
81
+ };
82
+
83
+ export default async function ({ input }) {
84
+ const key = String(input.product ?? "").toLowerCase();
85
+ const row = DATA[key];
86
+ if (!row) {
87
+ return { content: [{ type: "text", text: \`unknown product: \${key}\` }], is_error: true };
88
+ }
89
+ return { content: [{ type: "text", text: JSON.stringify({ product: key, ...row }) }] };
90
+ }
91
+ `
92
+ }
93
+ });
94
+
95
+ const demoCsv = [
96
+ "product,region,q1_revenue_usd,q2_revenue_usd,activation_rate",
97
+ "atlas,na,120000,139500,0.84",
98
+ "beacon,emea,98000,104300,0.79",
99
+ "cinder,apac,67000,81000,0.91"
100
+ ].join("\n");
101
+
102
+ const attachedFile = await File.fromBytes({
103
+ name: "quarterly-metrics.csv",
104
+ bytes: textEncoder.encode(`${demoCsv}\n`),
105
+ mountPath: "/workspace/input"
106
+ });
107
+
108
+ const runRules = await AgentsMd.fromContent(
109
+ [
110
+ "# Feature tour rules",
111
+ "- Use `/workspace/input/quarterly-metrics.csv` as the source table.",
112
+ "- Call `metric_lookup` for atlas, beacon, and cinder before writing conclusions.",
113
+ "- Write final artifacts under `/workspace/outputs`.",
114
+ "- Never print runtime secret values or provider keys."
115
+ ].join("\n"),
116
+ { name: "feature-tour-rules" }
117
+ );
118
+
119
+ const mcpServers = demoMcpUrl
120
+ ? [
121
+ McpServer.remote({
122
+ name: "demo-mcp",
123
+ url: demoMcpUrl,
124
+ ...(demoMcpToken
125
+ ? { headers: { Authorization: `Bearer ${demoMcpToken}` } }
126
+ : {})
127
+ })
128
+ ]
129
+ : [];
130
+
131
+ const environmentSecrets = demoRuntimeSecret
132
+ ? { DEMO_RUNTIME_SECRET: Secret.value(demoRuntimeSecret) }
133
+ : undefined;
134
+
135
+ const proxyEndpoints = [
136
+ ProxyEndpoint.none({
137
+ name: "httpbin",
138
+ baseUrl: "https://httpbin.org",
139
+ allowMethods: ["GET"],
140
+ allowPathPrefixes: ["/json"],
141
+ responseMode: "full",
142
+ timeoutMs: 10_000
143
+ })
144
+ ];
145
+
146
+ console.log("creating feature-tour session...");
147
+ console.log(`optional mcp: ${mcpServers.length > 0 ? "enabled" : "disabled"}`);
148
+ console.log(`optional runtime secret: ${environmentSecrets ? "enabled" : "disabled"}`);
149
+
150
+ const session = await aex.openSession({
151
+ provider: Providers.DEEPSEEK,
152
+ model: Models.DEEPSEEK_V4_FLASH,
153
+ system: [
154
+ "You are a concise analytics agent.",
155
+ "Prefer exact calculations and write durable files for the caller."
156
+ ].join(" "),
157
+ agentsMd: [runRules],
158
+ files: [attachedFile],
159
+ includeBuiltinTools: false,
160
+ tools: [
161
+ BuiltinTools.read_file,
162
+ BuiltinTools.write_file,
163
+ BuiltinTools.bash,
164
+ BuiltinTools.grep,
165
+ BuiltinTools.code_execution,
166
+ metricLookup
167
+ ],
168
+ proxyEndpoints,
169
+ mcpServers,
170
+ environment: {
171
+ networking: { mode: "open" },
172
+ variables: {
173
+ FEATURE_TOUR: "true",
174
+ REPORT_DIR: "/workspace/outputs"
175
+ },
176
+ ...(environmentSecrets ? { secrets: environmentSecrets } : {})
177
+ },
178
+ outputs: {
179
+ allowedDirs: ["/workspace/outputs"],
180
+ deniedDirs: ["*.tmp"],
181
+ maxFiles: 10,
182
+ maxFileBytes: 1_000_000
183
+ },
184
+ outputMode: "stream",
185
+ runtime: Sizes.SHARED_0_25X_1GB,
186
+ metadata: {
187
+ example: "sdk-feature-tour",
188
+ sdkSurface: "public"
189
+ },
190
+ overrides: {
191
+ idleTtl: "5m",
192
+ timeout: "10m",
193
+ maxSpendUsd: 2
194
+ },
195
+ apiKeys: { deepseek: deepseekKey }
196
+ });
197
+
198
+ console.log(`session: ${session.id}`);
199
+
200
+ const prompt = [
201
+ "Analyze the attached quarterly metrics.",
202
+ "Call metric_lookup for atlas, beacon, and cinder.",
203
+ "Create /workspace/outputs/feature-tour-report.md with a short table, a ranking by q2_revenue_usd, and two risks.",
204
+ "Create /workspace/outputs/summary.json with keys topProduct, totalQ2RevenueUsd, highestActivationProduct, and riskCount.",
205
+ "Mention whether the httpbin proxy endpoint is declared, but do not call it unless you need to."
206
+ ].join(" ");
207
+
208
+ const firstTurn = session.send(prompt);
209
+ const firstTurnIterator = firstTurn[Symbol.asyncIterator]();
210
+ let result: Awaited<ReturnType<typeof firstTurn.done>> | undefined;
211
+ for (;;) {
212
+ const next = await firstTurnIterator.next();
213
+ if (next.done) {
214
+ result = next.value as Awaited<ReturnType<typeof firstTurn.done>>;
215
+ break;
216
+ }
217
+ const event = next.value;
218
+ if (isTextMessage(event)) {
219
+ process.stdout.write(event.data.text);
220
+ } else if (event.type === "TOOL_CALL_START") {
221
+ const name = typeof event.data.name === "string" ? event.data.name : "tool";
222
+ process.stdout.write(`\n[tool:start] ${name}\n`);
223
+ } else if (event.type === "TOOL_CALL_RESULT") {
224
+ process.stdout.write("[tool:result]\n");
225
+ }
226
+ }
227
+
228
+ if (!result) {
229
+ throw new Error("first turn stream ended without a result");
230
+ }
231
+ console.log(`\nfirst turn parked with status: ${result.status}`);
232
+
233
+ const followUp = await session
234
+ .send("Read summary.json back and answer with one sentence confirming the top product and total Q2 revenue.")
235
+ .done();
236
+ console.log(`follow-up status: ${followUp.status}`);
237
+ if (followUp.text) {
238
+ console.log(`follow-up text: ${followUp.text.trim()}`);
239
+ }
240
+
241
+ const parked = await session.wait({ timeoutMs: 60_000, intervalMs: 2_000 });
242
+ console.log(`settled session status: ${parked.status}`);
243
+
244
+ const messages = await session.messages().list();
245
+ console.log(`assistant messages: ${messages.length}`);
246
+
247
+ const events = await session.events().list();
248
+ console.log(`captured events: ${events.length}`);
249
+
250
+ const outputs = await session.outputs().list();
251
+ console.log("outputs:");
252
+ for (const output of outputs) {
253
+ console.log(`- ${output.filename ?? output.id} (${output.contentType ?? "unknown"})`);
254
+ }
255
+
256
+ const summary = await session.outputs().read(
257
+ { path: "summary.json", match: "suffix" },
258
+ { maxBytes: 20_000 }
259
+ );
260
+ console.log("summary.json:");
261
+ console.log(summary.text);
262
+
263
+ const report = await session.outputs().findOne({
264
+ filename: "feature-tour-report.md"
265
+ });
266
+ if (report) {
267
+ const reportPreview = await session.outputs().read(report, {
268
+ maxBytes: 4_000,
269
+ grep: "risk"
270
+ });
271
+ console.log("report risk lines:");
272
+ console.log(reportPreview.text || "(no risk lines found)");
273
+ }
274
+
275
+ const corpusTools = createCorpusTools(aex, { sessionIds: [session.id] }, { defaultReadBytes: 4_000 });
276
+ const corpusOutputs = await corpusTools.execute("list_outputs", { session_id: session.id });
277
+ console.log("corpus-scoped list_outputs result:");
278
+ console.log(JSON.stringify(corpusOutputs, null, 2));
279
+
280
+ if (downloadPath) {
281
+ const bytes = await session.download({ to: downloadPath });
282
+ console.log(`downloaded session archive: ${downloadPath} (${bytes.byteLength} bytes)`);
283
+ }
284
+
285
+ function required(name: string): string {
286
+ const value = process.env[name];
287
+ if (!value) {
288
+ console.error(`Missing env var ${name}`);
289
+ process.exit(1);
290
+ }
291
+ return value;
292
+ }
293
+
294
+ function handleFatal(err: unknown): void {
295
+ if (isRateLimited(err)) {
296
+ console.error(`rate limited after ${err.attempts} attempts; retry after ${err.retryAfterMs ?? "unknown"}ms`);
297
+ process.exit(1);
298
+ }
299
+ console.error(err instanceof Error ? err.stack ?? err.message : String(err));
300
+ process.exit(1);
301
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexhq/sdk",
3
- "version": "0.33.1",
3
+ "version": "0.35.0",
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": {