@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.
- package/README.md +19 -27
- package/dist/_contracts/operations.d.ts +2 -54
- package/dist/_contracts/operations.js +2 -87
- package/dist/_contracts/run-config.d.ts +19 -13
- package/dist/_contracts/run-config.js +6 -33
- package/dist/_contracts/run-unit.d.ts +1 -33
- package/dist/_contracts/run-unit.js +2 -21
- package/dist/_contracts/runtime-sizes.d.ts +2 -2
- package/dist/_contracts/runtime-sizes.js +2 -2
- package/dist/_contracts/status.d.ts +2 -2
- package/dist/_contracts/status.js +3 -0
- package/dist/_contracts/submission.d.ts +80 -41
- package/dist/_contracts/submission.js +114 -52
- package/dist/agents-md.d.ts +5 -5
- package/dist/agents-md.js +7 -7
- package/dist/agents-md.js.map +1 -1
- package/dist/asset-upload.d.ts +4 -4
- package/dist/asset-upload.js +4 -4
- package/dist/bundle.d.ts +2 -2
- package/dist/bundle.js +2 -2
- package/dist/cli.mjs +369 -12918
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +234 -383
- package/dist/client.js +436 -648
- package/dist/client.js.map +1 -1
- package/dist/data-tools.d.ts +25 -22
- package/dist/data-tools.js +75 -62
- package/dist/data-tools.js.map +1 -1
- package/dist/fetch-archive.js +16 -16
- package/dist/fetch-archive.js.map +1 -1
- package/dist/file.d.ts +5 -5
- package/dist/file.js +7 -7
- package/dist/file.js.map +1 -1
- package/dist/index.d.ts +11 -9
- package/dist/index.js +20 -13
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.d.ts +4 -4
- package/dist/mcp-server.js +4 -4
- package/dist/proxy-endpoint.d.ts +4 -4
- package/dist/proxy-endpoint.js +1 -1
- package/dist/retry.d.ts +162 -0
- package/dist/retry.js +320 -0
- package/dist/retry.js.map +1 -0
- package/dist/secret.d.ts +8 -8
- package/dist/secret.js +8 -8
- package/dist/secret.js.map +1 -1
- package/dist/skill-tool.d.ts +102 -0
- package/dist/skill-tool.js +190 -0
- package/dist/skill-tool.js.map +1 -0
- package/dist/tool.d.ts +1 -1
- package/dist/tool.js +3 -3
- package/dist/tool.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/cleanup.md +3 -3
- package/docs/concepts/agent-tools.md +6 -25
- package/docs/concepts/composition.md +15 -12
- package/docs/concepts/providers-and-runtimes.md +3 -3
- package/docs/concepts/runs.md +27 -22
- package/docs/credentials.md +52 -84
- package/docs/defaults.md +6 -6
- package/docs/events.md +65 -44
- package/docs/limits-and-quotas.md +3 -4
- package/docs/mcp.md +3 -3
- package/docs/networking.md +8 -8
- package/docs/outputs.md +44 -40
- package/docs/provider-runtime-capabilities.md +1 -1
- package/docs/public-surface.json +2 -2
- package/docs/quickstart.md +20 -10
- package/docs/retries.md +129 -0
- package/docs/run-config.md +12 -14
- package/docs/run-record.md +8 -8
- package/docs/secrets.md +16 -26
- package/docs/skills.md +55 -110
- package/docs/vision-skills.md +29 -40
- package/examples/chat-corpus.ts +8 -9
- package/examples/feature-tour.ts +301 -0
- package/package.json +1 -1
- package/dist/skill.d.ts +0 -149
- package/dist/skill.js +0 -198
- 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
|
|
8
|
-
the
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- **
|
|
21
|
-
|
|
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 {
|
|
25
|
+
import { Aex, Models, Tools } from "@aexhq/sdk";
|
|
58
26
|
|
|
59
|
-
const aex = new
|
|
27
|
+
const aex = new Aex({ apiToken });
|
|
60
28
|
|
|
61
|
-
await aex.
|
|
29
|
+
await aex.run({
|
|
62
30
|
model: Models.CLAUDE_HAIKU_4_5,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
31
|
+
message,
|
|
32
|
+
tools: [await Tools.fromSkillDir("./skills/rules", { name: "rules" })],
|
|
33
|
+
apiKeys: { anthropic: apiKey }
|
|
66
34
|
});
|
|
67
35
|
```
|
|
68
36
|
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
##
|
|
61
|
+
## Loading and materialization
|
|
118
62
|
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
the catalog.
|
|
79
|
+
Run-scoped asset copies are part of the run record and are removed by run deletion
|
|
80
|
+
or retention cleanup.
|
package/docs/vision-skills.md
CHANGED
|
@@ -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
|
|
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`, `
|
|
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
|
|
28
|
-
|
|
29
|
-
settings are `responseMode: "full"` (so the skill gets the upstream
|
|
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 {
|
|
34
|
-
|
|
35
|
-
const aex = new
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
+
await aex.run({
|
|
56
49
|
model: Models.CLAUDE_HAIKU_4_5,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
proxyEndpoints,
|
|
60
|
-
|
|
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
|
-
`
|
|
68
|
-
run 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 `
|
|
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.
|
package/examples/chat-corpus.ts
CHANGED
|
@@ -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
|
|
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=…
|
|
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
|
|
18
|
-
if (
|
|
19
|
-
console.error("usage: bun examples/chat-corpus.ts <
|
|
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.
|
|
25
|
-
...(process.env.
|
|
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, {
|
|
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.
|
|
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": {
|