@aexhq/sdk 0.13.6
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/LICENSE +201 -0
- package/README.md +160 -0
- package/dist/_contracts/connection-ticket.d.ts +21 -0
- package/dist/_contracts/connection-ticket.js +49 -0
- package/dist/_contracts/event-envelope.d.ts +276 -0
- package/dist/_contracts/event-envelope.js +324 -0
- package/dist/_contracts/event-stream-client.d.ts +47 -0
- package/dist/_contracts/event-stream-client.js +141 -0
- package/dist/_contracts/http.d.ts +35 -0
- package/dist/_contracts/http.js +114 -0
- package/dist/_contracts/index.d.ts +28 -0
- package/dist/_contracts/index.js +29 -0
- package/dist/_contracts/managed-key.d.ts +74 -0
- package/dist/_contracts/managed-key.js +110 -0
- package/dist/_contracts/operations.d.ts +237 -0
- package/dist/_contracts/operations.js +632 -0
- package/dist/_contracts/provider-support.d.ts +220 -0
- package/dist/_contracts/provider-support.js +90 -0
- package/dist/_contracts/proxy-protocol.d.ts +257 -0
- package/dist/_contracts/proxy-protocol.js +234 -0
- package/dist/_contracts/proxy-validation.d.ts +19 -0
- package/dist/_contracts/proxy-validation.js +51 -0
- package/dist/_contracts/run-artifacts.d.ts +47 -0
- package/dist/_contracts/run-artifacts.js +101 -0
- package/dist/_contracts/run-config.d.ts +304 -0
- package/dist/_contracts/run-config.js +659 -0
- package/dist/_contracts/run-cost.d.ts +125 -0
- package/dist/_contracts/run-cost.js +616 -0
- package/dist/_contracts/run-custody.d.ts +226 -0
- package/dist/_contracts/run-custody.js +465 -0
- package/dist/_contracts/run-record.d.ts +127 -0
- package/dist/_contracts/run-record.js +177 -0
- package/dist/_contracts/run-retention.d.ts +213 -0
- package/dist/_contracts/run-retention.js +484 -0
- package/dist/_contracts/run-unit.d.ts +194 -0
- package/dist/_contracts/run-unit.js +215 -0
- package/dist/_contracts/runner-event.d.ts +114 -0
- package/dist/_contracts/runner-event.js +187 -0
- package/dist/_contracts/runtime-manifest.d.ts +106 -0
- package/dist/_contracts/runtime-manifest.js +98 -0
- package/dist/_contracts/runtime-security-profile.d.ts +27 -0
- package/dist/_contracts/runtime-security-profile.js +82 -0
- package/dist/_contracts/runtime-sizes.d.ts +144 -0
- package/dist/_contracts/runtime-sizes.js +136 -0
- package/dist/_contracts/runtime-types.d.ts +212 -0
- package/dist/_contracts/runtime-types.js +2 -0
- package/dist/_contracts/sdk-errors.d.ts +34 -0
- package/dist/_contracts/sdk-errors.js +52 -0
- package/dist/_contracts/sdk-secrets.d.ts +31 -0
- package/dist/_contracts/sdk-secrets.js +220 -0
- package/dist/_contracts/side-effect-audit.d.ts +129 -0
- package/dist/_contracts/side-effect-audit.js +494 -0
- package/dist/_contracts/sse.d.ts +74 -0
- package/dist/_contracts/sse.js +0 -0
- package/dist/_contracts/stable.d.ts +26 -0
- package/dist/_contracts/stable.js +44 -0
- package/dist/_contracts/status.d.ts +19 -0
- package/dist/_contracts/status.js +61 -0
- package/dist/_contracts/submission.d.ts +383 -0
- package/dist/_contracts/submission.js +1380 -0
- package/dist/agents-md.d.ts +46 -0
- package/dist/agents-md.js +83 -0
- package/dist/agents-md.js.map +1 -0
- package/dist/asset-upload.d.ts +66 -0
- package/dist/asset-upload.js +168 -0
- package/dist/asset-upload.js.map +1 -0
- package/dist/bundle.d.ts +33 -0
- package/dist/bundle.js +89 -0
- package/dist/bundle.js.map +1 -0
- package/dist/cli.mjs +4140 -0
- package/dist/cli.mjs.sha256 +1 -0
- package/dist/client.d.ts +460 -0
- package/dist/client.js +857 -0
- package/dist/client.js.map +1 -0
- package/dist/fetch-archive.d.ts +16 -0
- package/dist/fetch-archive.js +170 -0
- package/dist/fetch-archive.js.map +1 -0
- package/dist/file.d.ts +57 -0
- package/dist/file.js +153 -0
- package/dist/file.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server.d.ts +84 -0
- package/dist/mcp-server.js +114 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/node-fs.d.ts +12 -0
- package/dist/node-fs.js +44 -0
- package/dist/node-fs.js.map +1 -0
- package/dist/proxy-endpoint.d.ts +131 -0
- package/dist/proxy-endpoint.js +147 -0
- package/dist/proxy-endpoint.js.map +1 -0
- package/dist/skill.d.ts +117 -0
- package/dist/skill.js +169 -0
- package/dist/skill.js.map +1 -0
- package/dist/version.d.ts +9 -0
- package/dist/version.js +10 -0
- package/dist/version.js.map +1 -0
- package/docs/cleanup.md +38 -0
- package/docs/credentials.md +153 -0
- package/docs/events.md +76 -0
- package/docs/mcp.md +47 -0
- package/docs/outputs.md +157 -0
- package/docs/product-boundaries.md +57 -0
- package/docs/provider-runtime-capabilities.md +103 -0
- package/docs/quickstart.md +110 -0
- package/docs/release.md +99 -0
- package/docs/run-config.md +53 -0
- package/docs/run-record.md +39 -0
- package/docs/skills.md +139 -0
- package/docs/testing.md +29 -0
- package/package.json +47 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
import { AexError, DEFAULT_CREDENTIAL_MODE, DEFAULT_RUN_PROVIDER, HttpClient, RUNTIME_KINDS, RunStateError, operations, parseCredentialMode, streamCoordinatorEvents, TERMINAL_RUN_STATUSES } from "./_contracts/index.js";
|
|
2
|
+
import { uploadAsset } from "./asset-upload.js";
|
|
3
|
+
import { AgentsMd } from "./agents-md.js";
|
|
4
|
+
import { File } from "./file.js";
|
|
5
|
+
import { McpServer } from "./mcp-server.js";
|
|
6
|
+
import { splitProxyEndpoints } from "./proxy-endpoint.js";
|
|
7
|
+
import { Skill } from "./skill.js";
|
|
8
|
+
/**
|
|
9
|
+
* Workspace skill admin operations exposed under `client.skills`.
|
|
10
|
+
*
|
|
11
|
+
* New run submissions usually use `Skill.fromFiles(...)` or
|
|
12
|
+
* `Skill.fromPath(...)` directly inside `submitRun`; the SDK materializes
|
|
13
|
+
* those bytes to the hosted asset store before the run lands. This namespace is the read/delete
|
|
14
|
+
* surface for workspace skill records and the internal transport used by the
|
|
15
|
+
* legacy CLI upload command.
|
|
16
|
+
*/
|
|
17
|
+
export class SkillsClient {
|
|
18
|
+
#http;
|
|
19
|
+
constructor(http) {
|
|
20
|
+
this.#http = http;
|
|
21
|
+
}
|
|
22
|
+
list() {
|
|
23
|
+
return operations.listSkills(this.#http);
|
|
24
|
+
}
|
|
25
|
+
get(skillId) {
|
|
26
|
+
return operations.getSkill(this.#http, skillId);
|
|
27
|
+
}
|
|
28
|
+
delete(skillId) {
|
|
29
|
+
return operations.deleteSkill(this.#http, skillId);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Lookup a live workspace skill by `(name, contentHash)`.
|
|
33
|
+
*
|
|
34
|
+
* Returns the matching `Skill` record or `null` when no live row
|
|
35
|
+
* carries that hash. The `contentHash` is the wire format
|
|
36
|
+
* `sha256:<hex>` returned by `hashSkillBundle` (and stored verbatim
|
|
37
|
+
* on every skill row). The hash space is unique enough that one
|
|
38
|
+
* row at most can match, so this is a single keyed lookup.
|
|
39
|
+
*
|
|
40
|
+
* Consumers can call this directly when they already have a hash in hand
|
|
41
|
+
* and want to know whether the skill is already persisted.
|
|
42
|
+
*/
|
|
43
|
+
findByHash(args) {
|
|
44
|
+
return operations.findSkillByHash(this.#http, args);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Lookup a live workspace skill by `name`. Returns the matching
|
|
48
|
+
* `Skill` record or `null` when no live row carries that name.
|
|
49
|
+
* Implemented as a list-and-filter against the existing `/api/skills`
|
|
50
|
+
* endpoint — typical workspace skill counts are small enough that
|
|
51
|
+
* the cost is negligible.
|
|
52
|
+
*/
|
|
53
|
+
findByName(name) {
|
|
54
|
+
return operations.findSkillByName(this.#http, name);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Internal: post a pre-bundled skill zip to the BFF. Only
|
|
58
|
+
* `Skill.upload` calls this. NOT part of the public API.
|
|
59
|
+
*/
|
|
60
|
+
async _uploadSkillBundle(args) {
|
|
61
|
+
return operations.createSkillBundle(this.#http, {
|
|
62
|
+
name: args.name,
|
|
63
|
+
body: args.body,
|
|
64
|
+
contentType: "application/zip",
|
|
65
|
+
filename: `${args.name}.zip`
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Workspace AgentsMd admin operations exposed under `client.agentsMd`.
|
|
71
|
+
*
|
|
72
|
+
* New run submissions usually use `AgentsMd.fromContent(...)` or
|
|
73
|
+
* `AgentsMd.fromPath(...)` directly inside `submitRun`; the SDK
|
|
74
|
+
* materializes those bytes to the hosted asset store before the run lands. This namespace is
|
|
75
|
+
* the read/delete surface for persisted AgentsMd records plus an internal
|
|
76
|
+
* upload transport retained for legacy callers.
|
|
77
|
+
*/
|
|
78
|
+
export class AgentsMdClient {
|
|
79
|
+
#http;
|
|
80
|
+
constructor(http) {
|
|
81
|
+
this.#http = http;
|
|
82
|
+
}
|
|
83
|
+
list() {
|
|
84
|
+
return operations.listAgentsMd(this.#http);
|
|
85
|
+
}
|
|
86
|
+
get(agentsMdId) {
|
|
87
|
+
return operations.getAgentsMd(this.#http, agentsMdId);
|
|
88
|
+
}
|
|
89
|
+
delete(agentsMdId) {
|
|
90
|
+
return operations.deleteAgentsMd(this.#http, agentsMdId);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Internal: post an AgentsMd markdown string to the BFF.
|
|
94
|
+
* NOT part of the public API.
|
|
95
|
+
*/
|
|
96
|
+
async _uploadAgentsMd(args) {
|
|
97
|
+
return operations.createAgentsMd(this.#http, args);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Workspace File admin operations exposed under `client.files`.
|
|
102
|
+
*
|
|
103
|
+
* New run submissions usually use `File.fromPath(...)` or
|
|
104
|
+
* `File.fromBytes(...)` directly inside `submitRun`; the SDK materializes
|
|
105
|
+
* those bytes to the hosted asset store before the run lands. This namespace is the read/delete
|
|
106
|
+
* surface for persisted file records plus an internal upload transport
|
|
107
|
+
* retained for legacy callers.
|
|
108
|
+
*/
|
|
109
|
+
export class FilesClient {
|
|
110
|
+
#http;
|
|
111
|
+
constructor(http) {
|
|
112
|
+
this.#http = http;
|
|
113
|
+
}
|
|
114
|
+
list() {
|
|
115
|
+
return operations.listFiles(this.#http);
|
|
116
|
+
}
|
|
117
|
+
get(fileId) {
|
|
118
|
+
return operations.getFile(this.#http, fileId);
|
|
119
|
+
}
|
|
120
|
+
delete(fileId) {
|
|
121
|
+
return operations.deleteFile(this.#http, fileId);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Internal: post a pre-bundled file zip to the BFF.
|
|
125
|
+
* NOT part of the public API.
|
|
126
|
+
*/
|
|
127
|
+
async _uploadFile(args) {
|
|
128
|
+
return operations.createFile(this.#http, args);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Unified user-facing client for the aex platform. The same class
|
|
133
|
+
* powers the published `@aexhq/sdk` SDK and (under the hood) every host-side
|
|
134
|
+
* subcommand of the in-container `aex` CLI. All operations talk to
|
|
135
|
+
* the dashboard BFF and operate on durable run records.
|
|
136
|
+
*
|
|
137
|
+
* The SDK never asks the caller for a workspace id — workspace identity
|
|
138
|
+
* is derived server-side from the API token on every request. Use
|
|
139
|
+
* `client.whoami()` if you want to introspect which workspace the
|
|
140
|
+
* token resolves to.
|
|
141
|
+
*/
|
|
142
|
+
export class AexClient {
|
|
143
|
+
#http;
|
|
144
|
+
/**
|
|
145
|
+
* The same fetch the HttpClient uses, kept so the asset materializer can
|
|
146
|
+
* PUT bytes DIRECTLY to the presigned upload URL (a non-aex origin) with the
|
|
147
|
+
* caller's fetch (tests inject one; prod uses the global).
|
|
148
|
+
*/
|
|
149
|
+
#fetch;
|
|
150
|
+
skills;
|
|
151
|
+
agentsMd;
|
|
152
|
+
files;
|
|
153
|
+
constructor(options) {
|
|
154
|
+
if (!options.apiToken) {
|
|
155
|
+
throw new Error("AexClient: apiToken is required");
|
|
156
|
+
}
|
|
157
|
+
this.#http = new HttpClient({
|
|
158
|
+
...(options.baseUrl ? { baseUrl: options.baseUrl } : {}),
|
|
159
|
+
apiToken: options.apiToken,
|
|
160
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
161
|
+
// Opt-in local diagnostics: emit a redacted per-request trace to
|
|
162
|
+
// stderr. Uploads nothing. A caller wanting a custom sink can pass
|
|
163
|
+
// a function instead of `true`.
|
|
164
|
+
...(options.debug
|
|
165
|
+
? { debug: typeof options.debug === "function" ? options.debug : (line) => console.error(line) }
|
|
166
|
+
: {})
|
|
167
|
+
});
|
|
168
|
+
this.#fetch = options.fetch;
|
|
169
|
+
this.skills = new SkillsClient(this.#http);
|
|
170
|
+
this.agentsMd = new AgentsMdClient(this.#http);
|
|
171
|
+
this.files = new FilesClient(this.#http);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Internal: forwards to `SkillsClient._uploadSkillBundle`. NOT part of
|
|
175
|
+
* the public API.
|
|
176
|
+
*
|
|
177
|
+
* NOTE (tech-debt): this is part of the legacy workspace-skill upload
|
|
178
|
+
* surface (`SkillsClient` + `operations.createSkillBundle` + the TUS
|
|
179
|
+
* chunked path in asset-upload.ts). The live submit path materializes
|
|
180
|
+
* inline skills via `uploadAsset` instead; `Skill` no longer
|
|
181
|
+
* exposes `.upload()`/`.fromId()`. This surface is retained pending a
|
|
182
|
+
* deliberate deprecation pass (it still threads into the CLI host
|
|
183
|
+
* commands), tracked in the remediation plan as item 4a.
|
|
184
|
+
*/
|
|
185
|
+
async _uploadSkillBundle(args) {
|
|
186
|
+
return this.skills._uploadSkillBundle(args);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Internal: an `AgentsMd.upload(this)` shortcut that bypasses
|
|
190
|
+
* `client.agentsMd` indirection. Forwarded to
|
|
191
|
+
* `AgentsMdClient._uploadAgentsMd`. NOT part of the public API.
|
|
192
|
+
*/
|
|
193
|
+
async _uploadAgentsMd(args) {
|
|
194
|
+
return this.agentsMd._uploadAgentsMd(args);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Internal: a `File.upload(this)` shortcut that bypasses
|
|
198
|
+
* `client.files` indirection. Forwarded to
|
|
199
|
+
* `FilesClient._uploadFile`. NOT part of the public API.
|
|
200
|
+
*/
|
|
201
|
+
async _uploadFile(args) {
|
|
202
|
+
return this.files._uploadFile(args);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Submit a run and wait for it to reach a terminal state. Returns the
|
|
206
|
+
* final `Run` record. For long-running flows, prefer `submitRun` +
|
|
207
|
+
* `stream(runId)` + `wait(runId)`.
|
|
208
|
+
*/
|
|
209
|
+
async run(options) {
|
|
210
|
+
const runId = await this.submitRun(options);
|
|
211
|
+
return this.waitForRun(runId, options.signal ? { signal: options.signal } : {});
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Submit a run and return its run id immediately. Use that id with
|
|
215
|
+
* `wait`, `stream`, `outputs`, `download`, `cancel`, or `delete`.
|
|
216
|
+
*
|
|
217
|
+
* The SDK splits `mcpServers[i].headers` into `secrets.mcpServers`
|
|
218
|
+
* and `proxyEndpoints[i]` auth values into `secrets.proxyEndpointAuth`
|
|
219
|
+
* before sending so credentials never enter the hashed submission or
|
|
220
|
+
* the run snapshot.
|
|
221
|
+
*
|
|
222
|
+
* Unstaged inline skills (`Skill.fromFiles` / `Skill.fromPath`
|
|
223
|
+
* without a prior `.upload`) are accepted: the SDK switches to a
|
|
224
|
+
* multipart body that carries the canonical zip bytes alongside the
|
|
225
|
+
* JSON submission. The dashboard BFF ingests each one through the
|
|
226
|
+
* standard workspace-skill upload pipeline (dedup by content hash;
|
|
227
|
+
* upload via the existing two-phase pending → ready flow) and
|
|
228
|
+
* rewrites the run's `skills[]` to reference the resulting `skl_*`
|
|
229
|
+
* ids. The bytes persist on aex; the user can browse and
|
|
230
|
+
* download the resulting workspace skill from the dashboard.
|
|
231
|
+
*/
|
|
232
|
+
async submitRun(options) {
|
|
233
|
+
if (!options || typeof options !== "object") {
|
|
234
|
+
throw new Error("AexClient.submitRun: options is required");
|
|
235
|
+
}
|
|
236
|
+
const provider = options.provider ?? DEFAULT_RUN_PROVIDER;
|
|
237
|
+
const credentialMode = parseCredentialMode(options.credentialMode);
|
|
238
|
+
if (credentialMode === "managed") {
|
|
239
|
+
throw new AexError("CREDENTIAL_INVALID", "AexClient.submitRun: credentialMode \"managed\" is not available without a private managed-key implementation");
|
|
240
|
+
}
|
|
241
|
+
if (!options.secrets) {
|
|
242
|
+
throw new Error("AexClient.submitRun: secrets is required");
|
|
243
|
+
}
|
|
244
|
+
// The matching provider's apiKey is required; every OTHER provider's
|
|
245
|
+
// secret block must be absent. The shared parser re-runs this check
|
|
246
|
+
// on the server; failing early here gives the caller a synchronous
|
|
247
|
+
// error before any network call.
|
|
248
|
+
const providerSecret = options.secrets[provider];
|
|
249
|
+
if (!providerSecret?.apiKey) {
|
|
250
|
+
throw new Error(`AexClient.submitRun: secrets.${provider}.apiKey is required`);
|
|
251
|
+
}
|
|
252
|
+
for (const other of ["anthropic", "deepseek", "openai", "gemini", "mistral"]) {
|
|
253
|
+
if (other === provider)
|
|
254
|
+
continue;
|
|
255
|
+
if (options.secrets[other] !== undefined) {
|
|
256
|
+
throw new Error(`AexClient.submitRun: secrets.${other} is not allowed when provider is ${provider}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (typeof options.model !== "string" || !options.model) {
|
|
260
|
+
throw new Error("AexClient.submitRun: model is required");
|
|
261
|
+
}
|
|
262
|
+
const prompt = normalisePrompt(options.prompt);
|
|
263
|
+
const { endpoints: proxyEndpointDeclarations, auth: proxyEndpointAuthFromInstances } = splitProxyEndpoints(options.proxyEndpoints ?? []);
|
|
264
|
+
const mergedProxyAuth = mergeProxyEndpointAuth(proxyEndpointAuthFromInstances, options.secrets.proxyEndpointAuth ?? []);
|
|
265
|
+
// Walk Skill / AgentsMd / File instances and materialize every draft before
|
|
266
|
+
// the submit round-trip. The wire shape carries only kind:"asset" refs.
|
|
267
|
+
const assetSkills = await materializeSkills(this.#http, options.skills ?? [], this.#fetch);
|
|
268
|
+
const assetAgentsMd = await materializeAgentsMd(this.#http, options.agentsMd ?? [], this.#fetch);
|
|
269
|
+
const assetFiles = await materializeFiles(this.#http, options.files ?? [], this.#fetch);
|
|
270
|
+
const { submissionMcpServers, mergedMcpSecrets } = mergeMcpServers(options.mcpServers ?? [], options.secrets.mcpServers ?? []);
|
|
271
|
+
const submission = {
|
|
272
|
+
model: options.model,
|
|
273
|
+
...(options.system ? { system: options.system } : {}),
|
|
274
|
+
prompt,
|
|
275
|
+
skills: assetSkills,
|
|
276
|
+
agentsMd: assetAgentsMd,
|
|
277
|
+
files: assetFiles,
|
|
278
|
+
// submissionMcpServers may contain workspace refs of the shape
|
|
279
|
+
// {kind:"workspace", id:"mcp_..."}. The BFF runs
|
|
280
|
+
// `resolveWorkspaceMcpRefsInSubmission` BEFORE the shared parser
|
|
281
|
+
// and replaces them with the resolved {name, url}, so by the
|
|
282
|
+
// time anything reads PlatformSubmission post-parse the
|
|
283
|
+
// shape matches McpServerRef. The cast acknowledges that the
|
|
284
|
+
// SDK is producing pre-resolution wire input here.
|
|
285
|
+
mcpServers: submissionMcpServers,
|
|
286
|
+
...(options.environment ? { environment: options.environment } : {}),
|
|
287
|
+
...(options.metadata ? { metadata: options.metadata } : {}),
|
|
288
|
+
...(options.outputs &&
|
|
289
|
+
((options.outputs.allowedDirs?.length ?? 0) > 0 || (options.outputs.deniedDirs?.length ?? 0) > 0)
|
|
290
|
+
? { outputs: options.outputs }
|
|
291
|
+
: {}),
|
|
292
|
+
// Pass-through `builtins` verbatim — including an empty array,
|
|
293
|
+
// which is the "disable all builtins" signal. Distinguish from
|
|
294
|
+
// omitted (default applies) via `!== undefined`.
|
|
295
|
+
...(options.builtins !== undefined ? { builtins: options.builtins } : {})
|
|
296
|
+
};
|
|
297
|
+
const secrets = {
|
|
298
|
+
...options.secrets,
|
|
299
|
+
...(mergedMcpSecrets.length > 0 ? { mcpServers: mergedMcpSecrets } : {}),
|
|
300
|
+
...(mergedProxyAuth.length > 0 ? { proxyEndpointAuth: mergedProxyAuth } : {})
|
|
301
|
+
};
|
|
302
|
+
const request = {
|
|
303
|
+
idempotencyKey: options.idempotencyKey ?? generateIdempotencyKey(),
|
|
304
|
+
// Always include `provider` on the wire so dashboard / proxy
|
|
305
|
+
// tooling never has to second-guess what the runtime saw. The
|
|
306
|
+
// shared parser still defaults to `anthropic` when callers omit
|
|
307
|
+
// the field entirely, but the SDK has resolved it by here.
|
|
308
|
+
provider,
|
|
309
|
+
...(credentialMode !== DEFAULT_CREDENTIAL_MODE ? { credentialMode } : {}),
|
|
310
|
+
// `runtime` is optional on the wire — absent means let the
|
|
311
|
+
// dispatcher auto-route. Only emit it when the caller asked for
|
|
312
|
+
// a specific runtime so the wire shape stays minimal.
|
|
313
|
+
...(options.runtime ? { runtime: options.runtime } : {}),
|
|
314
|
+
submission,
|
|
315
|
+
...(options.runtimeSize ? { runtimeSize: options.runtimeSize } : {}),
|
|
316
|
+
...(options.timeout ? { timeout: options.timeout } : {}),
|
|
317
|
+
secrets,
|
|
318
|
+
...(proxyEndpointDeclarations.length > 0
|
|
319
|
+
? { proxyEndpoints: proxyEndpointDeclarations }
|
|
320
|
+
: {})
|
|
321
|
+
};
|
|
322
|
+
if (options.runtime !== undefined &&
|
|
323
|
+
!RUNTIME_KINDS.includes(options.runtime)) {
|
|
324
|
+
throw new AexError("RUNTIME_UNSUPPORTED", `AexClient.submitRun: runtime must be one of: ${RUNTIME_KINDS.join(", ")} ` +
|
|
325
|
+
`(got ${JSON.stringify(options.runtime)})`);
|
|
326
|
+
}
|
|
327
|
+
// All inline refs were materialized above, so submitRun is
|
|
328
|
+
// always a plain JSON post. The multipart code path is gone.
|
|
329
|
+
const run = await operations.submitRun(this.#http, request);
|
|
330
|
+
return run.id;
|
|
331
|
+
}
|
|
332
|
+
getRun(runId) {
|
|
333
|
+
return operations.getRun(this.#http, runId);
|
|
334
|
+
}
|
|
335
|
+
/** Short alias for `getRun`. */
|
|
336
|
+
get(runId) {
|
|
337
|
+
return this.getRun(runId);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Fetch the self-contained `RunUnit`: parsed submission inputs,
|
|
341
|
+
* attempts, indexed events (inline + cursor for the tail), raw
|
|
342
|
+
* provider-event Storage manifest, outputs, capture failures,
|
|
343
|
+
* proxy-call audit, pinned workspace skills, provider skills,
|
|
344
|
+
* inline skills. Backed by the same endpoint as `getRun` but
|
|
345
|
+
* typed against the full wire shape — use this when you need
|
|
346
|
+
* fields beyond `{id, status, timestamps, usage}`.
|
|
347
|
+
*/
|
|
348
|
+
getRunUnit(runId) {
|
|
349
|
+
return operations.getRunUnit(this.#http, runId);
|
|
350
|
+
}
|
|
351
|
+
/** Short alias for `getRunUnit`. */
|
|
352
|
+
getUnit(runId) {
|
|
353
|
+
return this.getRunUnit(runId);
|
|
354
|
+
}
|
|
355
|
+
listEvents(runId) {
|
|
356
|
+
return operations.listRunEvents(this.#http, runId);
|
|
357
|
+
}
|
|
358
|
+
/** Short alias for `listEvents`. */
|
|
359
|
+
events(runId) {
|
|
360
|
+
return this.listEvents(runId);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Yield run events (the `RunEvent` snapshot shape) as they arrive, by
|
|
364
|
+
* polling the coordinator-backed `/events` endpoint until the run reaches
|
|
365
|
+
* a terminal state, the signal aborts, or the caller breaks the iterator.
|
|
366
|
+
*
|
|
367
|
+
* For the live, low-latency envelope stream prefer {@link streamEnvelopes}
|
|
368
|
+
* (coordinator WebSocket). This polling form stays for consumers that want
|
|
369
|
+
* the loose `RunEvent` shape without a WS.
|
|
370
|
+
*/
|
|
371
|
+
async *streamEvents(runId, options = {}) {
|
|
372
|
+
if (options.signal?.aborted)
|
|
373
|
+
return;
|
|
374
|
+
yield* this.#streamEventsPolling(runId, { ...options, seenIds: new Set() });
|
|
375
|
+
}
|
|
376
|
+
/** Short alias for `streamEvents`. */
|
|
377
|
+
stream(runId, options) {
|
|
378
|
+
return this.streamEvents(runId, options);
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Stream the unified {@link AexEvent} envelope live over the coordinator
|
|
382
|
+
* WebSocket. The Worker's ticket broker authorizes the connection (workspace
|
|
383
|
+
* token → short-lived coordinator ticket); the shared client replays from
|
|
384
|
+
* the cursor, tails live, and resumes exactly-once across reconnects. The
|
|
385
|
+
* ticket is re-minted on each (re)connect so a long run never outlives it.
|
|
386
|
+
*/
|
|
387
|
+
async *streamEnvelopes(runId, options = {}) {
|
|
388
|
+
const first = await operations.getCoordinatorTicket(this.#http, runId);
|
|
389
|
+
yield* streamCoordinatorEvents({
|
|
390
|
+
wsUrl: first.wsUrl,
|
|
391
|
+
from: options.from ?? 0,
|
|
392
|
+
fetchTicket: async () => (await operations.getCoordinatorTicket(this.#http, runId)).ticket,
|
|
393
|
+
...(options.signal ? { signal: options.signal } : {})
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
async *#streamEventsPolling(runId, options) {
|
|
397
|
+
const intervalMs = options.intervalMs ?? 1_000;
|
|
398
|
+
const signal = options.signal;
|
|
399
|
+
while (!signal?.aborted) {
|
|
400
|
+
const events = await this.listEvents(runId);
|
|
401
|
+
for (const event of events) {
|
|
402
|
+
if (!options.seenIds.has(event.id)) {
|
|
403
|
+
options.seenIds.add(event.id);
|
|
404
|
+
yield event;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const run = await this.getRun(runId);
|
|
408
|
+
if (isTerminal(run.status))
|
|
409
|
+
return;
|
|
410
|
+
// `sleep` rejects on abort — treat that as a graceful stop.
|
|
411
|
+
try {
|
|
412
|
+
await sleep(intervalMs, signal);
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Poll the run record until it reaches a terminal status (succeeded,
|
|
421
|
+
* failed, terminated). Throws if `timeoutMs` elapses first.
|
|
422
|
+
*/
|
|
423
|
+
async waitForRun(runId, options = {}) {
|
|
424
|
+
const intervalMs = options.intervalMs ?? 1_500;
|
|
425
|
+
const timeoutMs = options.timeoutMs;
|
|
426
|
+
const signal = options.signal;
|
|
427
|
+
const deadline = typeof timeoutMs === "number" ? Date.now() + timeoutMs : Number.POSITIVE_INFINITY;
|
|
428
|
+
while (!signal?.aborted) {
|
|
429
|
+
const run = await this.getRun(runId);
|
|
430
|
+
if (isTerminal(run.status))
|
|
431
|
+
return run;
|
|
432
|
+
if (Date.now() >= deadline) {
|
|
433
|
+
throw new Error(`AexClient.waitForRun: timeout after ${timeoutMs}ms`);
|
|
434
|
+
}
|
|
435
|
+
await sleep(intervalMs, signal);
|
|
436
|
+
}
|
|
437
|
+
throw new Error("AexClient.waitForRun: aborted");
|
|
438
|
+
}
|
|
439
|
+
/** Short alias for `waitForRun`. */
|
|
440
|
+
wait(runId, options) {
|
|
441
|
+
return this.waitForRun(runId, options);
|
|
442
|
+
}
|
|
443
|
+
listOutputs(runId) {
|
|
444
|
+
return operations.listOutputs(this.#http, runId);
|
|
445
|
+
}
|
|
446
|
+
/** Short alias for `listOutputs`. */
|
|
447
|
+
outputs(runId) {
|
|
448
|
+
return this.listOutputs(runId);
|
|
449
|
+
}
|
|
450
|
+
createOutputLink(runId, outputId) {
|
|
451
|
+
return operations.createOutputLink(this.#http, runId, outputId);
|
|
452
|
+
}
|
|
453
|
+
async downloadOutput(runId, selectorOrOptions, options) {
|
|
454
|
+
const hasSelector = selectorOrOptions !== undefined && !isOutputDownloadOptionsOnly(selectorOrOptions);
|
|
455
|
+
const selector = hasSelector ? selectorOrOptions : undefined;
|
|
456
|
+
const to = hasSelector ? options?.to : selectorOrOptions?.to ?? options?.to;
|
|
457
|
+
let bytes;
|
|
458
|
+
if (selector === undefined) {
|
|
459
|
+
bytes = await operations.downloadOutputs(this.#http, runId);
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
const output = isOutputPathSelector(selector)
|
|
463
|
+
? resolveOutputFileSelector(await operations.listOutputs(this.#http, runId), selector, runId)
|
|
464
|
+
: resolveOutputFileSelector([], selector, runId);
|
|
465
|
+
const { response } = await this.#http.download(`/api/runs/${encodeURIComponent(runId)}/outputs/${encodeURIComponent(output.id)}/download`);
|
|
466
|
+
bytes = new Uint8Array(await response.arrayBuffer());
|
|
467
|
+
}
|
|
468
|
+
return writeOptionalFile(bytes, to);
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Bundle the per-run debug artifacts aex captures automatically:
|
|
472
|
+
*
|
|
473
|
+
* - `runtime/{stdout,stderr,args}.log` — runtime process diagnostics.
|
|
474
|
+
* - `host/...` — managed host logs when the platform includes them.
|
|
475
|
+
* These all live in the run's `logs` namespace (`runs/<id>/logs/`).
|
|
476
|
+
* Each is downloaded through the gated `/logs/:id/download` endpoint,
|
|
477
|
+
* decoded as UTF-8 text when the content type looks textual, and
|
|
478
|
+
* surfaced as raw bytes (base64) otherwise. The call is best-effort: a
|
|
479
|
+
* download failure for one file does not block the others; the failing
|
|
480
|
+
* entry lands in `errors` with the underlying message.
|
|
481
|
+
*
|
|
482
|
+
* Use this when a run failed or behaved oddly and you want all the
|
|
483
|
+
* post-mortem material in one round-trip — no need to wire
|
|
484
|
+
* `listOutputs` + `createOutputLink` by hand.
|
|
485
|
+
*/
|
|
486
|
+
async getRunDebugLogs(runId) {
|
|
487
|
+
// The `logs` namespace IS the diagnostics surface — everything it
|
|
488
|
+
// lists is a debug artifact, so no client-side prefix filter.
|
|
489
|
+
const matches = await operations.listLogs(this.#http, runId);
|
|
490
|
+
const logs = [];
|
|
491
|
+
const errors = [];
|
|
492
|
+
for (const out of matches) {
|
|
493
|
+
const filename = out.filename ?? "(unnamed)";
|
|
494
|
+
try {
|
|
495
|
+
const { response } = await this.#http.download(`/api/runs/${runId}/logs/${out.id}/download`);
|
|
496
|
+
const buf = await response.arrayBuffer();
|
|
497
|
+
const bytes = new Uint8Array(buf);
|
|
498
|
+
const contentType = out.contentType ?? "application/octet-stream";
|
|
499
|
+
const isText = /^(text\/|application\/json)/.test(contentType);
|
|
500
|
+
const bytesBase64 = bytesToBase64(bytes);
|
|
501
|
+
logs.push({
|
|
502
|
+
filename,
|
|
503
|
+
sizeBytes: out.sizeBytes ?? bytes.byteLength,
|
|
504
|
+
contentType,
|
|
505
|
+
createdAt: out.createdAt ?? new Date(0).toISOString(),
|
|
506
|
+
...(isText ? { text: new TextDecoder().decode(bytes) } : {}),
|
|
507
|
+
bytesBase64
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
catch (err) {
|
|
511
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
512
|
+
errors.push({ filename, message });
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return { runId, logs, errors };
|
|
516
|
+
}
|
|
517
|
+
/** Short alias for `getRunDebugLogs`. */
|
|
518
|
+
debugLogs(runId) {
|
|
519
|
+
return this.getRunDebugLogs(runId);
|
|
520
|
+
}
|
|
521
|
+
cancelRun(runId) {
|
|
522
|
+
return operations.cancelRun(this.#http, runId);
|
|
523
|
+
}
|
|
524
|
+
/** Short alias for `cancelRun`. */
|
|
525
|
+
cancel(runId) {
|
|
526
|
+
return this.cancelRun(runId);
|
|
527
|
+
}
|
|
528
|
+
deleteRun(runId) {
|
|
529
|
+
return operations.deleteRun(this.#http, runId);
|
|
530
|
+
}
|
|
531
|
+
/** Short alias for `deleteRun`. */
|
|
532
|
+
delete(runId) {
|
|
533
|
+
return this.deleteRun(runId);
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Delete a workspace asset blob from the shared content-addressed store
|
|
537
|
+
* (`assets/<workspaceId>/<hash>`). Accepts `sha256:<hex>` or a bare
|
|
538
|
+
* 64-hex digest. Runs that already snapshotted the asset are unaffected.
|
|
539
|
+
*/
|
|
540
|
+
deleteWorkspaceAsset(hash) {
|
|
541
|
+
return operations.deleteWorkspaceAsset(this.#http, hash);
|
|
542
|
+
}
|
|
543
|
+
whoami() {
|
|
544
|
+
return operations.whoami(this.#http);
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Download EVERYTHING about a run as one zip, assembled client-side
|
|
548
|
+
* from the public read endpoints (`getRun` + `listEvents` +
|
|
549
|
+
* `listOutputs` + per-output `/download`). Organised into the four
|
|
550
|
+
* namespace folders: `metadata/`, `events/`, `outputs/` (deliverables),
|
|
551
|
+
* `logs/` (`runtime/`, `host/`, `provider-proxy/`, `control-plane/`
|
|
552
|
+
* diagnostics), plus a `manifest.json`. Pass `to` to also write the
|
|
553
|
+
* bytes to a file path while still returning the bytes.
|
|
554
|
+
*/
|
|
555
|
+
async download(runId, options) {
|
|
556
|
+
return writeOptionalFile(await operations.download(this.#http, runId), options?.to);
|
|
557
|
+
}
|
|
558
|
+
/** Download only the run's deliverables (the `outputs` namespace) as a zip. */
|
|
559
|
+
async downloadOutputs(runId, options) {
|
|
560
|
+
return writeOptionalFile(await operations.downloadOutputs(this.#http, runId), options?.to);
|
|
561
|
+
}
|
|
562
|
+
/** Download only the platform diagnostics (the `logs` namespace) as a zip. */
|
|
563
|
+
async downloadLogs(runId, options) {
|
|
564
|
+
return writeOptionalFile(await operations.downloadLogs(this.#http, runId), options?.to);
|
|
565
|
+
}
|
|
566
|
+
/** Download only the indexed event archive (the `events` namespace) as a zip. */
|
|
567
|
+
async downloadEvents(runId, options) {
|
|
568
|
+
return writeOptionalFile(await operations.downloadEvents(this.#http, runId), options?.to);
|
|
569
|
+
}
|
|
570
|
+
/** Download only the run record (the `metadata` namespace) as a zip. */
|
|
571
|
+
async downloadMetadata(runId, options) {
|
|
572
|
+
return writeOptionalFile(await operations.downloadMetadata(this.#http, runId), options?.to);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
// `Run.status` is a loose `string` on the wire shape, so we membership-test
|
|
576
|
+
// against the canonical terminal set rather than re-deriving one (which is how
|
|
577
|
+
// `timed_out` got dropped from the old hardcoded list).
|
|
578
|
+
const TERMINAL_STATUSES = new Set(TERMINAL_RUN_STATUSES);
|
|
579
|
+
function isTerminal(status) {
|
|
580
|
+
return typeof status === "string" && TERMINAL_STATUSES.has(status);
|
|
581
|
+
}
|
|
582
|
+
function isOutputPathSelector(selector) {
|
|
583
|
+
return Boolean(selector && typeof selector === "object" && "path" in selector);
|
|
584
|
+
}
|
|
585
|
+
function isOutputDownloadOptionsOnly(value) {
|
|
586
|
+
return Boolean(value &&
|
|
587
|
+
typeof value === "object" &&
|
|
588
|
+
"to" in value &&
|
|
589
|
+
!("id" in value) &&
|
|
590
|
+
!("path" in value));
|
|
591
|
+
}
|
|
592
|
+
function resolveOutputFileSelector(outputs, selector, runId) {
|
|
593
|
+
if (isOutputPathSelector(selector)) {
|
|
594
|
+
const target = normalizeOutputLookupPath(selector.path);
|
|
595
|
+
if (!target) {
|
|
596
|
+
throw new RunStateError("AexClient.downloadOutput: output path must be non-empty", {
|
|
597
|
+
runId,
|
|
598
|
+
path: selector.path
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
const matches = outputs.filter((output) => {
|
|
602
|
+
if (typeof output.filename !== "string")
|
|
603
|
+
return false;
|
|
604
|
+
const filename = normalizeOutputLookupPath(output.filename);
|
|
605
|
+
if (selector.match === "suffix") {
|
|
606
|
+
return filename === target || filename.endsWith(`/${target}`);
|
|
607
|
+
}
|
|
608
|
+
return filename === target;
|
|
609
|
+
});
|
|
610
|
+
if (matches.length === 1)
|
|
611
|
+
return matches[0];
|
|
612
|
+
if (matches.length > 1) {
|
|
613
|
+
throw new RunStateError(`AexClient.downloadOutput: output path "${selector.path}" matched multiple files`, { runId, path: selector.path, matches: matches.map((output) => output.filename ?? output.id) });
|
|
614
|
+
}
|
|
615
|
+
throw new RunStateError(`AexClient.downloadOutput: output path "${selector.path}" was not found`, {
|
|
616
|
+
runId,
|
|
617
|
+
path: selector.path
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
if (typeof selector.id !== "string" || selector.id.length === 0) {
|
|
621
|
+
throw new RunStateError("AexClient.downloadOutput: selector must include an output id or path", { runId });
|
|
622
|
+
}
|
|
623
|
+
return { ...selector, id: selector.id };
|
|
624
|
+
}
|
|
625
|
+
function normalizeOutputLookupPath(path) {
|
|
626
|
+
return path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
627
|
+
}
|
|
628
|
+
async function writeOptionalFile(bytes, to) {
|
|
629
|
+
if (to !== undefined) {
|
|
630
|
+
const { writeFile } = await import("node:fs/promises");
|
|
631
|
+
await writeFile(to, bytes);
|
|
632
|
+
}
|
|
633
|
+
return bytes;
|
|
634
|
+
}
|
|
635
|
+
function sleep(ms, signal) {
|
|
636
|
+
return new Promise((resolve, reject) => {
|
|
637
|
+
if (signal?.aborted) {
|
|
638
|
+
reject(new Error("aborted"));
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const timer = setTimeout(() => {
|
|
642
|
+
signal?.removeEventListener("abort", onAbort);
|
|
643
|
+
resolve();
|
|
644
|
+
}, ms);
|
|
645
|
+
const onAbort = () => {
|
|
646
|
+
clearTimeout(timer);
|
|
647
|
+
reject(new Error("aborted"));
|
|
648
|
+
};
|
|
649
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Encode a byte array as base64. Uses Node's `Buffer` when available
|
|
654
|
+
* (the SDK ships as a Node tarball; this is the hot path), and falls
|
|
655
|
+
* back to `btoa` for browser or edge runtimes that pull the SDK in
|
|
656
|
+
* without Buffer.
|
|
657
|
+
*/
|
|
658
|
+
function bytesToBase64(bytes) {
|
|
659
|
+
const BufferCtor = globalThis.Buffer;
|
|
660
|
+
if (BufferCtor) {
|
|
661
|
+
return BufferCtor.from(bytes).toString("base64");
|
|
662
|
+
}
|
|
663
|
+
let binary = "";
|
|
664
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
665
|
+
binary += String.fromCharCode(bytes[i]);
|
|
666
|
+
}
|
|
667
|
+
return globalThis.btoa(binary);
|
|
668
|
+
}
|
|
669
|
+
function generateIdempotencyKey() {
|
|
670
|
+
const cryptoObj = globalThis.crypto;
|
|
671
|
+
if (cryptoObj?.randomUUID)
|
|
672
|
+
return cryptoObj.randomUUID();
|
|
673
|
+
return `idem-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
674
|
+
}
|
|
675
|
+
function normalisePrompt(input) {
|
|
676
|
+
if (typeof input === "string") {
|
|
677
|
+
if (!input) {
|
|
678
|
+
throw new Error("AexClient.submitRun: prompt must be a non-empty string");
|
|
679
|
+
}
|
|
680
|
+
return [input];
|
|
681
|
+
}
|
|
682
|
+
if (!Array.isArray(input) || input.length === 0) {
|
|
683
|
+
throw new Error("AexClient.submitRun: prompt must be a non-empty string or string array");
|
|
684
|
+
}
|
|
685
|
+
for (const segment of input) {
|
|
686
|
+
if (typeof segment !== "string" || !segment) {
|
|
687
|
+
throw new Error("AexClient.submitRun: prompt segments must be non-empty strings");
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return [...input];
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Walk the user-provided `Skill[]`, validating each instance and
|
|
694
|
+
* producing:
|
|
695
|
+
* - `skillRefs[]` — the wire entries for `submission.skills[]`, with
|
|
696
|
+
* inline refs assigned positional slot ids (`transient-0`, …).
|
|
697
|
+
* - `inlineBundles[]` — the bytes for each inline skill,
|
|
698
|
+
* parallel-indexed by slot.
|
|
699
|
+
*
|
|
700
|
+
* Throws on consumed Skills (the user reused a draft after a prior
|
|
701
|
+
* `submitRun` call) so that mistake is loud, not silent.
|
|
702
|
+
*/
|
|
703
|
+
/**
|
|
704
|
+
* Walk the user-provided Skill[], materialize every draft to assets, and return
|
|
705
|
+
* the wire-shape refs.
|
|
706
|
+
*/
|
|
707
|
+
async function materializeSkills(http, skills, fetch) {
|
|
708
|
+
const out = [];
|
|
709
|
+
for (let i = 0; i < skills.length; i++) {
|
|
710
|
+
const entry = skills[i];
|
|
711
|
+
if (!(entry instanceof Skill)) {
|
|
712
|
+
throw new Error(`AexClient.submitRun: skills[${i}] must be a Skill instance`);
|
|
713
|
+
}
|
|
714
|
+
if (entry.isConsumed) {
|
|
715
|
+
throw new Error(`AexClient.submitRun: skills[${i}] was already consumed by a prior submitRun`);
|
|
716
|
+
}
|
|
717
|
+
const ref = entry.ref;
|
|
718
|
+
if (ref.kind === "draft") {
|
|
719
|
+
const bundle = entry._takeDraftBundle();
|
|
720
|
+
if (!bundle) {
|
|
721
|
+
throw new Error(`AexClient.submitRun: skills[${i}] is draft but has no bytes`);
|
|
722
|
+
}
|
|
723
|
+
const uploaded = await uploadAsset({
|
|
724
|
+
http,
|
|
725
|
+
bytes: bundle.bytes,
|
|
726
|
+
hash: bundle.contentHash,
|
|
727
|
+
...(fetch ? { fetch } : {})
|
|
728
|
+
});
|
|
729
|
+
out.push({
|
|
730
|
+
kind: "asset",
|
|
731
|
+
assetId: uploaded.assetId,
|
|
732
|
+
name: bundle.name
|
|
733
|
+
});
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
// Already-materialized asset ref.
|
|
737
|
+
out.push(ref);
|
|
738
|
+
}
|
|
739
|
+
return out;
|
|
740
|
+
}
|
|
741
|
+
/** Materialize draft AgentsMd[] to assets; pass-through any already-materialized refs. */
|
|
742
|
+
async function materializeAgentsMd(http, agentsMds, fetch) {
|
|
743
|
+
const out = [];
|
|
744
|
+
for (let i = 0; i < agentsMds.length; i++) {
|
|
745
|
+
const entry = agentsMds[i];
|
|
746
|
+
if (!(entry instanceof AgentsMd)) {
|
|
747
|
+
throw new Error(`AexClient.submitRun: agentsMd[${i}] must be an AgentsMd instance`);
|
|
748
|
+
}
|
|
749
|
+
if (entry.isConsumed) {
|
|
750
|
+
throw new Error(`AexClient.submitRun: agentsMd[${i}] was already consumed by a prior submitRun`);
|
|
751
|
+
}
|
|
752
|
+
const ref = entry.ref;
|
|
753
|
+
if (ref.kind === "draft") {
|
|
754
|
+
const bundle = entry._takeDraftBundle();
|
|
755
|
+
if (!bundle) {
|
|
756
|
+
throw new Error(`AexClient.submitRun: agentsMd[${i}] is draft but has no bytes`);
|
|
757
|
+
}
|
|
758
|
+
const uploaded = await uploadAsset({ http, bytes: bundle.bytes, hash: bundle.contentHash, ...(fetch ? { fetch } : {}) });
|
|
759
|
+
out.push({
|
|
760
|
+
kind: "asset",
|
|
761
|
+
assetId: uploaded.assetId,
|
|
762
|
+
name: bundle.name
|
|
763
|
+
});
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
out.push(ref);
|
|
767
|
+
}
|
|
768
|
+
return out;
|
|
769
|
+
}
|
|
770
|
+
/** Materialize draft File[] to assets; pass-through any already-materialized refs. */
|
|
771
|
+
async function materializeFiles(http, files, fetch) {
|
|
772
|
+
const out = [];
|
|
773
|
+
for (let i = 0; i < files.length; i++) {
|
|
774
|
+
const entry = files[i];
|
|
775
|
+
if (!(entry instanceof File)) {
|
|
776
|
+
throw new Error(`AexClient.submitRun: files[${i}] must be a File instance`);
|
|
777
|
+
}
|
|
778
|
+
if (entry.isConsumed) {
|
|
779
|
+
throw new Error(`AexClient.submitRun: files[${i}] was already consumed by a prior submitRun`);
|
|
780
|
+
}
|
|
781
|
+
const ref = entry.ref;
|
|
782
|
+
if (ref.kind === "draft") {
|
|
783
|
+
const bundle = entry._takeDraftBundle();
|
|
784
|
+
if (!bundle) {
|
|
785
|
+
throw new Error(`AexClient.submitRun: files[${i}] is draft but has no bytes`);
|
|
786
|
+
}
|
|
787
|
+
const uploaded = await uploadAsset({ http, bytes: bundle.bytes, hash: bundle.contentHash, ...(fetch ? { fetch } : {}) });
|
|
788
|
+
out.push(bundle.mountPath !== undefined
|
|
789
|
+
? {
|
|
790
|
+
kind: "asset",
|
|
791
|
+
assetId: uploaded.assetId,
|
|
792
|
+
name: bundle.name,
|
|
793
|
+
mountPath: bundle.mountPath
|
|
794
|
+
}
|
|
795
|
+
: {
|
|
796
|
+
kind: "asset",
|
|
797
|
+
assetId: uploaded.assetId,
|
|
798
|
+
name: bundle.name
|
|
799
|
+
});
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
out.push(ref);
|
|
803
|
+
}
|
|
804
|
+
return out;
|
|
805
|
+
}
|
|
806
|
+
function mergeMcpServers(inputs, explicitSecrets) {
|
|
807
|
+
const submissionMcpServers = [];
|
|
808
|
+
const secretByName = new Map();
|
|
809
|
+
for (const secret of explicitSecrets) {
|
|
810
|
+
secretByName.set(secret.name, secret);
|
|
811
|
+
}
|
|
812
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
813
|
+
const entry = inputs[i];
|
|
814
|
+
if (!(entry instanceof McpServer)) {
|
|
815
|
+
throw new Error(`AexClient.submitRun: mcpServers[${i}] must be an McpServer instance`);
|
|
816
|
+
}
|
|
817
|
+
submissionMcpServers.push(entry.toSubmissionEntry());
|
|
818
|
+
const secret = entry.toSecretEntry();
|
|
819
|
+
if (secret) {
|
|
820
|
+
const existing = secretByName.get(secret.name);
|
|
821
|
+
if (existing && existing.url !== secret.url) {
|
|
822
|
+
throw new Error(`AexClient.submitRun: mcpServers[${i}].url conflicts with secrets.mcpServers["${secret.name}"]`);
|
|
823
|
+
}
|
|
824
|
+
secretByName.set(secret.name, secret);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
submissionMcpServers,
|
|
829
|
+
mergedMcpSecrets: Array.from(secretByName.values())
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Merge `ProxyEndpoint`-derived auth entries with any
|
|
834
|
+
* `secrets.proxyEndpointAuth` the caller passed explicitly. Per-instance
|
|
835
|
+
* auth values win on the same `name`; a type mismatch (e.g. instance
|
|
836
|
+
* declares `bearer` but secrets carry `header` for the same name) is a
|
|
837
|
+
* call-site error and we throw at the SDK boundary instead of letting
|
|
838
|
+
* the BFF reject the submission an HTTP request later.
|
|
839
|
+
*/
|
|
840
|
+
function mergeProxyEndpointAuth(fromInstances, fromExplicitSecrets) {
|
|
841
|
+
if (fromInstances.length === 0 && fromExplicitSecrets.length === 0)
|
|
842
|
+
return [];
|
|
843
|
+
const byName = new Map();
|
|
844
|
+
for (const entry of fromExplicitSecrets) {
|
|
845
|
+
byName.set(entry.name, entry);
|
|
846
|
+
}
|
|
847
|
+
for (const entry of fromInstances) {
|
|
848
|
+
const existing = byName.get(entry.name);
|
|
849
|
+
if (existing && existing.value.type !== entry.value.type) {
|
|
850
|
+
throw new Error(`AexClient.submitRun: proxyEndpoint "${entry.name}" auth type conflicts ` +
|
|
851
|
+
`with secrets.proxyEndpointAuth (instance=${entry.value.type}, secrets=${existing.value.type})`);
|
|
852
|
+
}
|
|
853
|
+
byName.set(entry.name, entry);
|
|
854
|
+
}
|
|
855
|
+
return Array.from(byName.values());
|
|
856
|
+
}
|
|
857
|
+
//# sourceMappingURL=client.js.map
|