@aexhq/sdk 0.13.6 → 0.13.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -14
- package/dist/_contracts/connection-ticket.d.ts +8 -7
- package/dist/_contracts/connection-ticket.js +20 -14
- package/dist/_contracts/event-envelope.d.ts +17 -18
- package/dist/_contracts/event-envelope.js +10 -11
- package/dist/_contracts/managed-key.d.ts +27 -1
- package/dist/_contracts/managed-key.js +75 -4
- package/dist/_contracts/operations.d.ts +9 -20
- package/dist/_contracts/operations.js +33 -82
- package/dist/_contracts/proxy-protocol.d.ts +35 -2
- package/dist/_contracts/proxy-protocol.js +34 -1
- package/dist/_contracts/run-artifacts.d.ts +12 -10
- package/dist/_contracts/run-artifacts.js +13 -11
- package/dist/_contracts/run-config.d.ts +7 -0
- package/dist/_contracts/run-config.js +93 -24
- package/dist/_contracts/run-custody.d.ts +3 -3
- package/dist/_contracts/run-custody.js +5 -5
- package/dist/_contracts/run-record.d.ts +5 -17
- package/dist/_contracts/run-record.js +4 -15
- package/dist/_contracts/run-retention.d.ts +2 -2
- package/dist/_contracts/run-retention.js +3 -3
- package/dist/_contracts/run-unit.d.ts +4 -5
- package/dist/_contracts/runner-event.d.ts +7 -8
- package/dist/_contracts/runner-event.js +7 -8
- package/dist/_contracts/side-effect-audit.d.ts +2 -2
- package/dist/_contracts/side-effect-audit.js +3 -3
- package/dist/_contracts/stable.d.ts +1 -1
- package/dist/_contracts/stable.js +1 -1
- package/dist/_contracts/submission.d.ts +15 -11
- package/dist/_contracts/submission.js +18 -14
- package/dist/cli.mjs +127 -127
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +14 -58
- package/dist/client.js +304 -168
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/cleanup.md +4 -4
- package/docs/credentials.md +5 -5
- package/docs/events.md +5 -5
- package/docs/outputs.md +23 -25
- package/docs/product-boundaries.md +5 -5
- package/docs/provider-runtime-capabilities.md +1 -1
- package/docs/quickstart.md +12 -12
- package/docs/run-config.md +1 -1
- package/docs/run-record.md +6 -9
- package/docs/skills.md +23 -25
- package/package.json +2 -2
package/dist/client.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
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
2
|
import { AgentsMd } from "./agents-md.js";
|
|
4
3
|
import { File } from "./file.js";
|
|
5
4
|
import { McpServer } from "./mcp-server.js";
|
|
@@ -139,20 +138,16 @@ export class FilesClient {
|
|
|
139
138
|
* `client.whoami()` if you want to introspect which workspace the
|
|
140
139
|
* token resolves to.
|
|
141
140
|
*/
|
|
142
|
-
export class
|
|
141
|
+
export class AgentExecutor {
|
|
143
142
|
#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
|
-
*/
|
|
143
|
+
/** The same fetch the HttpClient uses, kept for direct bootstrap uploads. */
|
|
149
144
|
#fetch;
|
|
150
145
|
skills;
|
|
151
146
|
agentsMd;
|
|
152
147
|
files;
|
|
153
148
|
constructor(options) {
|
|
154
149
|
if (!options.apiToken) {
|
|
155
|
-
throw new Error("
|
|
150
|
+
throw new Error("AgentExecutor: apiToken is required");
|
|
156
151
|
}
|
|
157
152
|
this.#http = new HttpClient({
|
|
158
153
|
...(options.baseUrl ? { baseUrl: options.baseUrl } : {}),
|
|
@@ -231,15 +226,15 @@ export class AexClient {
|
|
|
231
226
|
*/
|
|
232
227
|
async submitRun(options) {
|
|
233
228
|
if (!options || typeof options !== "object") {
|
|
234
|
-
throw new Error("
|
|
229
|
+
throw new Error("AgentExecutor.submitRun: options is required");
|
|
235
230
|
}
|
|
236
231
|
const provider = options.provider ?? DEFAULT_RUN_PROVIDER;
|
|
237
232
|
const credentialMode = parseCredentialMode(options.credentialMode);
|
|
238
233
|
if (credentialMode === "managed") {
|
|
239
|
-
throw new AexError("CREDENTIAL_INVALID", "
|
|
234
|
+
throw new AexError("CREDENTIAL_INVALID", "AgentExecutor.submitRun: credentialMode \"managed\" is not available without a private managed-key implementation");
|
|
240
235
|
}
|
|
241
236
|
if (!options.secrets) {
|
|
242
|
-
throw new Error("
|
|
237
|
+
throw new Error("AgentExecutor.submitRun: secrets is required");
|
|
243
238
|
}
|
|
244
239
|
// The matching provider's apiKey is required; every OTHER provider's
|
|
245
240
|
// secret block must be absent. The shared parser re-runs this check
|
|
@@ -247,34 +242,41 @@ export class AexClient {
|
|
|
247
242
|
// error before any network call.
|
|
248
243
|
const providerSecret = options.secrets[provider];
|
|
249
244
|
if (!providerSecret?.apiKey) {
|
|
250
|
-
throw new Error(`
|
|
245
|
+
throw new Error(`AgentExecutor.submitRun: secrets.${provider}.apiKey is required`);
|
|
251
246
|
}
|
|
252
247
|
for (const other of ["anthropic", "deepseek", "openai", "gemini", "mistral"]) {
|
|
253
248
|
if (other === provider)
|
|
254
249
|
continue;
|
|
255
250
|
if (options.secrets[other] !== undefined) {
|
|
256
|
-
throw new Error(`
|
|
251
|
+
throw new Error(`AgentExecutor.submitRun: secrets.${other} is not allowed when provider is ${provider}`);
|
|
257
252
|
}
|
|
258
253
|
}
|
|
259
254
|
if (typeof options.model !== "string" || !options.model) {
|
|
260
|
-
throw new Error("
|
|
255
|
+
throw new Error("AgentExecutor.submitRun: model is required");
|
|
261
256
|
}
|
|
262
257
|
const prompt = normalisePrompt(options.prompt);
|
|
263
258
|
const { endpoints: proxyEndpointDeclarations, auth: proxyEndpointAuthFromInstances } = splitProxyEndpoints(options.proxyEndpoints ?? []);
|
|
264
259
|
const mergedProxyAuth = mergeProxyEndpointAuth(proxyEndpointAuthFromInstances, options.secrets.proxyEndpointAuth ?? []);
|
|
265
|
-
// Walk Skill / AgentsMd / File instances
|
|
266
|
-
// the submit
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const
|
|
260
|
+
// Walk Skill / AgentsMd / File instances. Drafts are declared as direct
|
|
261
|
+
// inputs on the submit request, then uploaded to the run bootstrap target
|
|
262
|
+
// after the control plane accepts the run. Already-materialized asset refs
|
|
263
|
+
// still pass through unchanged.
|
|
264
|
+
const preparedSkills = prepareSkills(options.skills ?? []);
|
|
265
|
+
const preparedAgentsMd = prepareAgentsMd(options.agentsMd ?? []);
|
|
266
|
+
const preparedFiles = prepareFiles(options.files ?? []);
|
|
267
|
+
const directInputs = [
|
|
268
|
+
...preparedSkills.directInputs,
|
|
269
|
+
...preparedAgentsMd.directInputs,
|
|
270
|
+
...preparedFiles.directInputs
|
|
271
|
+
];
|
|
270
272
|
const { submissionMcpServers, mergedMcpSecrets } = mergeMcpServers(options.mcpServers ?? [], options.secrets.mcpServers ?? []);
|
|
271
273
|
const submission = {
|
|
272
274
|
model: options.model,
|
|
273
275
|
...(options.system ? { system: options.system } : {}),
|
|
274
276
|
prompt,
|
|
275
|
-
skills:
|
|
276
|
-
agentsMd:
|
|
277
|
-
files:
|
|
277
|
+
skills: preparedSkills.refs,
|
|
278
|
+
agentsMd: preparedAgentsMd.refs,
|
|
279
|
+
files: preparedFiles.refs,
|
|
278
280
|
// submissionMcpServers may contain workspace refs of the shape
|
|
279
281
|
// {kind:"workspace", id:"mcp_..."}. The BFF runs
|
|
280
282
|
// `resolveWorkspaceMcpRefsInSubmission` BEFORE the shared parser
|
|
@@ -292,7 +294,8 @@ export class AexClient {
|
|
|
292
294
|
// Pass-through `builtins` verbatim — including an empty array,
|
|
293
295
|
// which is the "disable all builtins" signal. Distinguish from
|
|
294
296
|
// omitted (default applies) via `!== undefined`.
|
|
295
|
-
...(options.builtins !== undefined ? { builtins: options.builtins } : {})
|
|
297
|
+
...(options.builtins !== undefined ? { builtins: options.builtins } : {}),
|
|
298
|
+
...(options.outputMode !== undefined ? { outputMode: options.outputMode } : {})
|
|
296
299
|
};
|
|
297
300
|
const secrets = {
|
|
298
301
|
...options.secrets,
|
|
@@ -321,13 +324,27 @@ export class AexClient {
|
|
|
321
324
|
};
|
|
322
325
|
if (options.runtime !== undefined &&
|
|
323
326
|
!RUNTIME_KINDS.includes(options.runtime)) {
|
|
324
|
-
throw new AexError("RUNTIME_UNSUPPORTED", `
|
|
327
|
+
throw new AexError("RUNTIME_UNSUPPORTED", `AgentExecutor.submitRun: runtime must be one of: ${RUNTIME_KINDS.join(", ")} ` +
|
|
325
328
|
`(got ${JSON.stringify(options.runtime)})`);
|
|
326
329
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
330
|
+
const submitRequest = directInputs.length > 0
|
|
331
|
+
? {
|
|
332
|
+
...request,
|
|
333
|
+
bootstrapMode: "direct",
|
|
334
|
+
directInputs: directInputs.map(({ bytes: _bytes, ...descriptor }) => descriptor)
|
|
335
|
+
}
|
|
336
|
+
: request;
|
|
337
|
+
const run = await operations.submitRun(this.#http, submitRequest);
|
|
338
|
+
const runId = getSubmittedRunId(run);
|
|
339
|
+
if (directInputs.length > 0) {
|
|
340
|
+
await completeDirectBootstrap({
|
|
341
|
+
response: run,
|
|
342
|
+
directInputs,
|
|
343
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
344
|
+
...(this.#fetch ? { fetch: this.#fetch } : {})
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return runId;
|
|
331
348
|
}
|
|
332
349
|
getRun(runId) {
|
|
333
350
|
return operations.getRun(this.#http, runId);
|
|
@@ -430,11 +447,11 @@ export class AexClient {
|
|
|
430
447
|
if (isTerminal(run.status))
|
|
431
448
|
return run;
|
|
432
449
|
if (Date.now() >= deadline) {
|
|
433
|
-
throw new Error(`
|
|
450
|
+
throw new Error(`AgentExecutor.waitForRun: timeout after ${timeoutMs}ms`);
|
|
434
451
|
}
|
|
435
452
|
await sleep(intervalMs, signal);
|
|
436
453
|
}
|
|
437
|
-
throw new Error("
|
|
454
|
+
throw new Error("AgentExecutor.waitForRun: aborted");
|
|
438
455
|
}
|
|
439
456
|
/** Short alias for `waitForRun`. */
|
|
440
457
|
wait(runId, options) {
|
|
@@ -467,57 +484,6 @@ export class AexClient {
|
|
|
467
484
|
}
|
|
468
485
|
return writeOptionalFile(bytes, to);
|
|
469
486
|
}
|
|
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
487
|
cancelRun(runId) {
|
|
522
488
|
return operations.cancelRun(this.#http, runId);
|
|
523
489
|
}
|
|
@@ -544,12 +510,11 @@ export class AexClient {
|
|
|
544
510
|
return operations.whoami(this.#http);
|
|
545
511
|
}
|
|
546
512
|
/**
|
|
547
|
-
* Download EVERYTHING about a run as one zip, assembled client-side
|
|
513
|
+
* Download EVERYTHING public about a run as one zip, assembled client-side
|
|
548
514
|
* from the public read endpoints (`getRun` + `listEvents` +
|
|
549
|
-
* `listOutputs` + per-output `/download`). Organised into the
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
* diagnostics), plus a `manifest.json`. Pass `to` to also write the
|
|
515
|
+
* `listOutputs` + per-output `/download`). Organised into the namespace
|
|
516
|
+
* folders `metadata/`, `events/`, and `outputs/`, plus a `manifest.json`.
|
|
517
|
+
* Pass `to` to also write the
|
|
553
518
|
* bytes to a file path while still returning the bytes.
|
|
554
519
|
*/
|
|
555
520
|
async download(runId, options) {
|
|
@@ -559,10 +524,6 @@ export class AexClient {
|
|
|
559
524
|
async downloadOutputs(runId, options) {
|
|
560
525
|
return writeOptionalFile(await operations.downloadOutputs(this.#http, runId), options?.to);
|
|
561
526
|
}
|
|
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
527
|
/** Download only the indexed event archive (the `events` namespace) as a zip. */
|
|
567
528
|
async downloadEvents(runId, options) {
|
|
568
529
|
return writeOptionalFile(await operations.downloadEvents(this.#http, runId), options?.to);
|
|
@@ -593,7 +554,7 @@ function resolveOutputFileSelector(outputs, selector, runId) {
|
|
|
593
554
|
if (isOutputPathSelector(selector)) {
|
|
594
555
|
const target = normalizeOutputLookupPath(selector.path);
|
|
595
556
|
if (!target) {
|
|
596
|
-
throw new RunStateError("
|
|
557
|
+
throw new RunStateError("AgentExecutor.downloadOutput: output path must be non-empty", {
|
|
597
558
|
runId,
|
|
598
559
|
path: selector.path
|
|
599
560
|
});
|
|
@@ -610,15 +571,15 @@ function resolveOutputFileSelector(outputs, selector, runId) {
|
|
|
610
571
|
if (matches.length === 1)
|
|
611
572
|
return matches[0];
|
|
612
573
|
if (matches.length > 1) {
|
|
613
|
-
throw new RunStateError(`
|
|
574
|
+
throw new RunStateError(`AgentExecutor.downloadOutput: output path "${selector.path}" matched multiple files`, { runId, path: selector.path, matches: matches.map((output) => output.filename ?? output.id) });
|
|
614
575
|
}
|
|
615
|
-
throw new RunStateError(`
|
|
576
|
+
throw new RunStateError(`AgentExecutor.downloadOutput: output path "${selector.path}" was not found`, {
|
|
616
577
|
runId,
|
|
617
578
|
path: selector.path
|
|
618
579
|
});
|
|
619
580
|
}
|
|
620
581
|
if (typeof selector.id !== "string" || selector.id.length === 0) {
|
|
621
|
-
throw new RunStateError("
|
|
582
|
+
throw new RunStateError("AgentExecutor.downloadOutput: selector must include an output id or path", { runId });
|
|
622
583
|
}
|
|
623
584
|
return { ...selector, id: selector.id };
|
|
624
585
|
}
|
|
@@ -649,23 +610,6 @@ function sleep(ms, signal) {
|
|
|
649
610
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
650
611
|
});
|
|
651
612
|
}
|
|
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
613
|
function generateIdempotencyKey() {
|
|
670
614
|
const cryptoObj = globalThis.crypto;
|
|
671
615
|
if (cryptoObj?.randomUUID)
|
|
@@ -675,133 +619,325 @@ function generateIdempotencyKey() {
|
|
|
675
619
|
function normalisePrompt(input) {
|
|
676
620
|
if (typeof input === "string") {
|
|
677
621
|
if (!input) {
|
|
678
|
-
throw new Error("
|
|
622
|
+
throw new Error("AgentExecutor.submitRun: prompt must be a non-empty string");
|
|
679
623
|
}
|
|
680
624
|
return [input];
|
|
681
625
|
}
|
|
682
626
|
if (!Array.isArray(input) || input.length === 0) {
|
|
683
|
-
throw new Error("
|
|
627
|
+
throw new Error("AgentExecutor.submitRun: prompt must be a non-empty string or string array");
|
|
684
628
|
}
|
|
685
629
|
for (const segment of input) {
|
|
686
630
|
if (typeof segment !== "string" || !segment) {
|
|
687
|
-
throw new Error("
|
|
631
|
+
throw new Error("AgentExecutor.submitRun: prompt segments must be non-empty strings");
|
|
688
632
|
}
|
|
689
633
|
}
|
|
690
634
|
return [...input];
|
|
691
635
|
}
|
|
692
|
-
/**
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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 = [];
|
|
636
|
+
/** Walk Skill[] and turn drafts into direct-bootstrap descriptors. */
|
|
637
|
+
function prepareSkills(skills) {
|
|
638
|
+
const refs = [];
|
|
639
|
+
const directInputs = [];
|
|
709
640
|
for (let i = 0; i < skills.length; i++) {
|
|
710
641
|
const entry = skills[i];
|
|
711
642
|
if (!(entry instanceof Skill)) {
|
|
712
|
-
throw new Error(`
|
|
643
|
+
throw new Error(`AgentExecutor.submitRun: skills[${i}] must be a Skill instance`);
|
|
713
644
|
}
|
|
714
645
|
if (entry.isConsumed) {
|
|
715
|
-
throw new Error(`
|
|
646
|
+
throw new Error(`AgentExecutor.submitRun: skills[${i}] was already consumed by a prior submitRun`);
|
|
716
647
|
}
|
|
717
648
|
const ref = entry.ref;
|
|
718
649
|
if (ref.kind === "draft") {
|
|
719
650
|
const bundle = entry._takeDraftBundle();
|
|
720
651
|
if (!bundle) {
|
|
721
|
-
throw new Error(`
|
|
652
|
+
throw new Error(`AgentExecutor.submitRun: skills[${i}] is draft but has no bytes`);
|
|
722
653
|
}
|
|
723
|
-
const
|
|
724
|
-
|
|
654
|
+
const input = directInputFor({
|
|
655
|
+
role: "skill",
|
|
656
|
+
index: i,
|
|
657
|
+
name: bundle.name,
|
|
658
|
+
contentHash: bundle.contentHash,
|
|
725
659
|
bytes: bundle.bytes,
|
|
726
|
-
|
|
727
|
-
...(fetch ? { fetch } : {})
|
|
660
|
+
contentType: "application/zip"
|
|
728
661
|
});
|
|
729
|
-
|
|
662
|
+
directInputs.push(input);
|
|
663
|
+
refs.push({
|
|
730
664
|
kind: "asset",
|
|
731
|
-
assetId:
|
|
665
|
+
assetId: input.assetId,
|
|
732
666
|
name: bundle.name
|
|
733
667
|
});
|
|
734
668
|
continue;
|
|
735
669
|
}
|
|
736
670
|
// Already-materialized asset ref.
|
|
737
|
-
|
|
671
|
+
refs.push(ref);
|
|
738
672
|
}
|
|
739
|
-
return
|
|
673
|
+
return { refs, directInputs };
|
|
740
674
|
}
|
|
741
|
-
/**
|
|
742
|
-
|
|
743
|
-
const
|
|
675
|
+
/** Walk AgentsMd[] and turn drafts into direct-bootstrap descriptors. */
|
|
676
|
+
function prepareAgentsMd(agentsMds) {
|
|
677
|
+
const refs = [];
|
|
678
|
+
const directInputs = [];
|
|
744
679
|
for (let i = 0; i < agentsMds.length; i++) {
|
|
745
680
|
const entry = agentsMds[i];
|
|
746
681
|
if (!(entry instanceof AgentsMd)) {
|
|
747
|
-
throw new Error(`
|
|
682
|
+
throw new Error(`AgentExecutor.submitRun: agentsMd[${i}] must be an AgentsMd instance`);
|
|
748
683
|
}
|
|
749
684
|
if (entry.isConsumed) {
|
|
750
|
-
throw new Error(`
|
|
685
|
+
throw new Error(`AgentExecutor.submitRun: agentsMd[${i}] was already consumed by a prior submitRun`);
|
|
751
686
|
}
|
|
752
687
|
const ref = entry.ref;
|
|
753
688
|
if (ref.kind === "draft") {
|
|
754
689
|
const bundle = entry._takeDraftBundle();
|
|
755
690
|
if (!bundle) {
|
|
756
|
-
throw new Error(`
|
|
691
|
+
throw new Error(`AgentExecutor.submitRun: agentsMd[${i}] is draft but has no bytes`);
|
|
757
692
|
}
|
|
758
|
-
const
|
|
759
|
-
|
|
693
|
+
const input = directInputFor({
|
|
694
|
+
role: "agentsMd",
|
|
695
|
+
index: i,
|
|
696
|
+
name: bundle.name,
|
|
697
|
+
contentHash: bundle.contentHash,
|
|
698
|
+
bytes: bundle.bytes,
|
|
699
|
+
contentType: "application/zip"
|
|
700
|
+
});
|
|
701
|
+
directInputs.push(input);
|
|
702
|
+
refs.push({
|
|
760
703
|
kind: "asset",
|
|
761
|
-
assetId:
|
|
704
|
+
assetId: input.assetId,
|
|
762
705
|
name: bundle.name
|
|
763
706
|
});
|
|
764
707
|
continue;
|
|
765
708
|
}
|
|
766
|
-
|
|
709
|
+
refs.push(ref);
|
|
767
710
|
}
|
|
768
|
-
return
|
|
711
|
+
return { refs, directInputs };
|
|
769
712
|
}
|
|
770
|
-
/**
|
|
771
|
-
|
|
772
|
-
const
|
|
713
|
+
/** Walk File[] and turn drafts into direct-bootstrap descriptors. */
|
|
714
|
+
function prepareFiles(files) {
|
|
715
|
+
const refs = [];
|
|
716
|
+
const directInputs = [];
|
|
773
717
|
for (let i = 0; i < files.length; i++) {
|
|
774
718
|
const entry = files[i];
|
|
775
719
|
if (!(entry instanceof File)) {
|
|
776
|
-
throw new Error(`
|
|
720
|
+
throw new Error(`AgentExecutor.submitRun: files[${i}] must be a File instance`);
|
|
777
721
|
}
|
|
778
722
|
if (entry.isConsumed) {
|
|
779
|
-
throw new Error(`
|
|
723
|
+
throw new Error(`AgentExecutor.submitRun: files[${i}] was already consumed by a prior submitRun`);
|
|
780
724
|
}
|
|
781
725
|
const ref = entry.ref;
|
|
782
726
|
if (ref.kind === "draft") {
|
|
783
727
|
const bundle = entry._takeDraftBundle();
|
|
784
728
|
if (!bundle) {
|
|
785
|
-
throw new Error(`
|
|
729
|
+
throw new Error(`AgentExecutor.submitRun: files[${i}] is draft but has no bytes`);
|
|
786
730
|
}
|
|
787
|
-
const
|
|
788
|
-
|
|
731
|
+
const input = directInputFor({
|
|
732
|
+
role: "file",
|
|
733
|
+
index: i,
|
|
734
|
+
name: bundle.name,
|
|
735
|
+
contentHash: bundle.contentHash,
|
|
736
|
+
bytes: bundle.bytes,
|
|
737
|
+
contentType: "application/zip",
|
|
738
|
+
...(bundle.mountPath ? { mountPath: bundle.mountPath } : {})
|
|
739
|
+
});
|
|
740
|
+
directInputs.push(input);
|
|
741
|
+
refs.push(bundle.mountPath !== undefined
|
|
789
742
|
? {
|
|
790
743
|
kind: "asset",
|
|
791
|
-
assetId:
|
|
744
|
+
assetId: input.assetId,
|
|
792
745
|
name: bundle.name,
|
|
793
746
|
mountPath: bundle.mountPath
|
|
794
747
|
}
|
|
795
748
|
: {
|
|
796
749
|
kind: "asset",
|
|
797
|
-
assetId:
|
|
750
|
+
assetId: input.assetId,
|
|
798
751
|
name: bundle.name
|
|
799
752
|
});
|
|
800
753
|
continue;
|
|
801
754
|
}
|
|
802
|
-
|
|
755
|
+
refs.push(ref);
|
|
756
|
+
}
|
|
757
|
+
return { refs, directInputs };
|
|
758
|
+
}
|
|
759
|
+
function directInputFor(args) {
|
|
760
|
+
const sha256 = args.contentHash.startsWith("sha256:")
|
|
761
|
+
? args.contentHash
|
|
762
|
+
: `sha256:${args.contentHash}`;
|
|
763
|
+
const hashHex = sha256.slice("sha256:".length);
|
|
764
|
+
if (!/^[0-9a-f]{64}$/.test(hashHex)) {
|
|
765
|
+
throw new Error(`AgentExecutor.submitRun: ${args.role}[${args.index}] content hash must be sha256:<64-hex>`);
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
inputId: `input_${args.role}_${args.index}_${hashHex}`,
|
|
769
|
+
role: args.role,
|
|
770
|
+
assetId: `asset_${hashHex}`,
|
|
771
|
+
name: args.name,
|
|
772
|
+
sha256,
|
|
773
|
+
sizeBytes: args.bytes.byteLength,
|
|
774
|
+
contentType: args.contentType,
|
|
775
|
+
...(args.mountPath ? { mountPath: args.mountPath } : {}),
|
|
776
|
+
bytes: args.bytes
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
function getSubmittedRunId(response) {
|
|
780
|
+
const id = response.id ?? response.runId;
|
|
781
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
782
|
+
throw new Error("AgentExecutor.submitRun: submit response did not include a run id");
|
|
783
|
+
}
|
|
784
|
+
return id;
|
|
785
|
+
}
|
|
786
|
+
async function completeDirectBootstrap(args) {
|
|
787
|
+
const token = args.response.bootstrapToken;
|
|
788
|
+
const statusUrl = args.response.bootstrapStatusUrl;
|
|
789
|
+
if (typeof token !== "string" || token.length === 0 || typeof statusUrl !== "string" || statusUrl.length === 0) {
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const fetchImpl = args.fetch ?? globalThis.fetch.bind(globalThis);
|
|
793
|
+
let target;
|
|
794
|
+
try {
|
|
795
|
+
target = resolveBootstrapReady(args.response) ?? await pollBootstrapReady({
|
|
796
|
+
fetchImpl,
|
|
797
|
+
statusUrl,
|
|
798
|
+
token,
|
|
799
|
+
...(args.response.bootstrapExpiresAt ? { expiresAt: args.response.bootstrapExpiresAt } : {}),
|
|
800
|
+
...(args.signal ? { signal: args.signal } : {})
|
|
801
|
+
});
|
|
802
|
+
for (const input of args.directInputs) {
|
|
803
|
+
await uploadDirectInput({ fetchImpl, target, token, input, ...(args.signal ? { signal: args.signal } : {}) });
|
|
804
|
+
}
|
|
805
|
+
await commitDirectInputs({
|
|
806
|
+
fetchImpl,
|
|
807
|
+
target,
|
|
808
|
+
token,
|
|
809
|
+
inputs: args.directInputs,
|
|
810
|
+
...(args.signal ? { signal: args.signal } : {})
|
|
811
|
+
});
|
|
803
812
|
}
|
|
804
|
-
|
|
813
|
+
catch (err) {
|
|
814
|
+
await abortDirectBootstrap({
|
|
815
|
+
fetchImpl,
|
|
816
|
+
token,
|
|
817
|
+
statusUrl,
|
|
818
|
+
...(target ? { target } : {}),
|
|
819
|
+
...(args.signal ? { signal: args.signal } : {})
|
|
820
|
+
}).catch(() => undefined);
|
|
821
|
+
throw err;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
async function pollBootstrapReady(args) {
|
|
825
|
+
const deadline = bootstrapDeadline(args.expiresAt);
|
|
826
|
+
while (!args.signal?.aborted) {
|
|
827
|
+
if (Date.now() >= deadline) {
|
|
828
|
+
throw new Error("AgentExecutor.submitRun: bootstrap target did not become ready before it expired");
|
|
829
|
+
}
|
|
830
|
+
const res = await args.fetchImpl(args.statusUrl, {
|
|
831
|
+
method: "GET",
|
|
832
|
+
headers: { authorization: `Bearer ${args.token}`, accept: "application/json" },
|
|
833
|
+
...(args.signal ? { signal: args.signal } : {})
|
|
834
|
+
});
|
|
835
|
+
if (res.ok) {
|
|
836
|
+
const body = await res.json();
|
|
837
|
+
const ready = resolveBootstrapReady(body);
|
|
838
|
+
if (ready)
|
|
839
|
+
return ready;
|
|
840
|
+
}
|
|
841
|
+
else if (![202, 404, 425].includes(res.status)) {
|
|
842
|
+
const detail = await res.text().catch(() => "");
|
|
843
|
+
throw new Error(`AgentExecutor.submitRun: bootstrap status failed with ${res.status}` +
|
|
844
|
+
(detail ? `: ${detail.slice(0, 300)}` : ""));
|
|
845
|
+
}
|
|
846
|
+
await sleep(250, args.signal);
|
|
847
|
+
}
|
|
848
|
+
throw new Error("AgentExecutor.submitRun: aborted");
|
|
849
|
+
}
|
|
850
|
+
function resolveBootstrapReady(value) {
|
|
851
|
+
if (!value || typeof value !== "object")
|
|
852
|
+
return undefined;
|
|
853
|
+
const record = value;
|
|
854
|
+
const base = typeof record.uploadBaseUrl === "string"
|
|
855
|
+
? record.uploadBaseUrl
|
|
856
|
+
: typeof record.bootstrapUploadBaseUrl === "string"
|
|
857
|
+
? record.bootstrapUploadBaseUrl
|
|
858
|
+
: undefined;
|
|
859
|
+
if (!base)
|
|
860
|
+
return undefined;
|
|
861
|
+
const routingHeaders = isStringRecord(record.routingHeaders) ? record.routingHeaders : undefined;
|
|
862
|
+
return {
|
|
863
|
+
uploadBaseUrl: stripTrailingSlash(base),
|
|
864
|
+
...(routingHeaders ? { routingHeaders } : {}),
|
|
865
|
+
...(typeof record.abortUrl === "string" ? { abortUrl: record.abortUrl } : {}),
|
|
866
|
+
...(typeof record.commitUrl === "string" ? { commitUrl: record.commitUrl } : {})
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
async function uploadDirectInput(args) {
|
|
870
|
+
const res = await args.fetchImpl(`${args.target.uploadBaseUrl}/inputs/${encodeURIComponent(args.input.inputId)}`, {
|
|
871
|
+
method: "PUT",
|
|
872
|
+
headers: {
|
|
873
|
+
authorization: `Bearer ${args.token}`,
|
|
874
|
+
"content-type": args.input.contentType,
|
|
875
|
+
"x-aex-input-sha256": args.input.sha256,
|
|
876
|
+
"x-aex-input-size": String(args.input.sizeBytes),
|
|
877
|
+
...(args.target.routingHeaders ?? {})
|
|
878
|
+
},
|
|
879
|
+
body: args.input.bytes,
|
|
880
|
+
...(args.signal ? { signal: args.signal } : {})
|
|
881
|
+
});
|
|
882
|
+
if (!res.ok) {
|
|
883
|
+
const detail = await res.text().catch(() => "");
|
|
884
|
+
throw new Error(`AgentExecutor.submitRun: bootstrap input upload failed for ${args.input.inputId} ` +
|
|
885
|
+
`(status ${res.status})${detail ? `: ${detail.slice(0, 300)}` : ""}`);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
async function commitDirectInputs(args) {
|
|
889
|
+
const commitUrl = args.target.commitUrl ?? `${args.target.uploadBaseUrl}/commit`;
|
|
890
|
+
const res = await args.fetchImpl(commitUrl, {
|
|
891
|
+
method: "POST",
|
|
892
|
+
headers: {
|
|
893
|
+
authorization: `Bearer ${args.token}`,
|
|
894
|
+
"content-type": "application/json",
|
|
895
|
+
accept: "application/json",
|
|
896
|
+
...(args.target.routingHeaders ?? {})
|
|
897
|
+
},
|
|
898
|
+
body: JSON.stringify({
|
|
899
|
+
inputs: args.inputs.map((input) => ({
|
|
900
|
+
inputId: input.inputId,
|
|
901
|
+
sha256: input.sha256,
|
|
902
|
+
sizeBytes: input.sizeBytes
|
|
903
|
+
}))
|
|
904
|
+
}),
|
|
905
|
+
...(args.signal ? { signal: args.signal } : {})
|
|
906
|
+
});
|
|
907
|
+
if (!res.ok) {
|
|
908
|
+
const detail = await res.text().catch(() => "");
|
|
909
|
+
throw new Error(`AgentExecutor.submitRun: bootstrap commit failed with ${res.status}` +
|
|
910
|
+
(detail ? `: ${detail.slice(0, 300)}` : ""));
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
async function abortDirectBootstrap(args) {
|
|
914
|
+
const abortUrl = args.target?.abortUrl ?? `${args.statusUrl.replace(/\/status$/, "")}/abort`;
|
|
915
|
+
await args.fetchImpl(abortUrl, {
|
|
916
|
+
method: "POST",
|
|
917
|
+
headers: {
|
|
918
|
+
authorization: `Bearer ${args.token}`,
|
|
919
|
+
accept: "application/json",
|
|
920
|
+
...(args.target?.routingHeaders ?? {})
|
|
921
|
+
},
|
|
922
|
+
...(args.signal ? { signal: args.signal } : {})
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
function bootstrapDeadline(expiresAt) {
|
|
926
|
+
if (typeof expiresAt === "string") {
|
|
927
|
+
const parsed = Date.parse(expiresAt);
|
|
928
|
+
if (Number.isFinite(parsed))
|
|
929
|
+
return parsed;
|
|
930
|
+
}
|
|
931
|
+
return Date.now() + 60_000;
|
|
932
|
+
}
|
|
933
|
+
function isStringRecord(value) {
|
|
934
|
+
return Boolean(value &&
|
|
935
|
+
typeof value === "object" &&
|
|
936
|
+
!Array.isArray(value) &&
|
|
937
|
+
Object.values(value).every((entry) => typeof entry === "string"));
|
|
938
|
+
}
|
|
939
|
+
function stripTrailingSlash(s) {
|
|
940
|
+
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
805
941
|
}
|
|
806
942
|
function mergeMcpServers(inputs, explicitSecrets) {
|
|
807
943
|
const submissionMcpServers = [];
|
|
@@ -812,14 +948,14 @@ function mergeMcpServers(inputs, explicitSecrets) {
|
|
|
812
948
|
for (let i = 0; i < inputs.length; i++) {
|
|
813
949
|
const entry = inputs[i];
|
|
814
950
|
if (!(entry instanceof McpServer)) {
|
|
815
|
-
throw new Error(`
|
|
951
|
+
throw new Error(`AgentExecutor.submitRun: mcpServers[${i}] must be an McpServer instance`);
|
|
816
952
|
}
|
|
817
953
|
submissionMcpServers.push(entry.toSubmissionEntry());
|
|
818
954
|
const secret = entry.toSecretEntry();
|
|
819
955
|
if (secret) {
|
|
820
956
|
const existing = secretByName.get(secret.name);
|
|
821
957
|
if (existing && existing.url !== secret.url) {
|
|
822
|
-
throw new Error(`
|
|
958
|
+
throw new Error(`AgentExecutor.submitRun: mcpServers[${i}].url conflicts with secrets.mcpServers["${secret.name}"]`);
|
|
823
959
|
}
|
|
824
960
|
secretByName.set(secret.name, secret);
|
|
825
961
|
}
|
|
@@ -847,7 +983,7 @@ function mergeProxyEndpointAuth(fromInstances, fromExplicitSecrets) {
|
|
|
847
983
|
for (const entry of fromInstances) {
|
|
848
984
|
const existing = byName.get(entry.name);
|
|
849
985
|
if (existing && existing.value.type !== entry.value.type) {
|
|
850
|
-
throw new Error(`
|
|
986
|
+
throw new Error(`AgentExecutor.submitRun: proxyEndpoint "${entry.name}" auth type conflicts ` +
|
|
851
987
|
`with secrets.proxyEndpointAuth (instance=${entry.value.type}, secrets=${existing.value.type})`);
|
|
852
988
|
}
|
|
853
989
|
byName.set(entry.name, entry);
|