@aexhq/sdk 0.13.7 → 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 +5 -6
- package/dist/_contracts/submission.js +1 -1
- package/dist/cli.mjs +127 -127
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +7 -57
- package/dist/client.js +302 -167
- 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
|
|
@@ -322,13 +324,27 @@ export class AexClient {
|
|
|
322
324
|
};
|
|
323
325
|
if (options.runtime !== undefined &&
|
|
324
326
|
!RUNTIME_KINDS.includes(options.runtime)) {
|
|
325
|
-
throw new AexError("RUNTIME_UNSUPPORTED", `
|
|
327
|
+
throw new AexError("RUNTIME_UNSUPPORTED", `AgentExecutor.submitRun: runtime must be one of: ${RUNTIME_KINDS.join(", ")} ` +
|
|
326
328
|
`(got ${JSON.stringify(options.runtime)})`);
|
|
327
329
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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;
|
|
332
348
|
}
|
|
333
349
|
getRun(runId) {
|
|
334
350
|
return operations.getRun(this.#http, runId);
|
|
@@ -431,11 +447,11 @@ export class AexClient {
|
|
|
431
447
|
if (isTerminal(run.status))
|
|
432
448
|
return run;
|
|
433
449
|
if (Date.now() >= deadline) {
|
|
434
|
-
throw new Error(`
|
|
450
|
+
throw new Error(`AgentExecutor.waitForRun: timeout after ${timeoutMs}ms`);
|
|
435
451
|
}
|
|
436
452
|
await sleep(intervalMs, signal);
|
|
437
453
|
}
|
|
438
|
-
throw new Error("
|
|
454
|
+
throw new Error("AgentExecutor.waitForRun: aborted");
|
|
439
455
|
}
|
|
440
456
|
/** Short alias for `waitForRun`. */
|
|
441
457
|
wait(runId, options) {
|
|
@@ -468,57 +484,6 @@ export class AexClient {
|
|
|
468
484
|
}
|
|
469
485
|
return writeOptionalFile(bytes, to);
|
|
470
486
|
}
|
|
471
|
-
/**
|
|
472
|
-
* Bundle the per-run debug artifacts aex captures automatically:
|
|
473
|
-
*
|
|
474
|
-
* - `runtime/{stdout,stderr,args}.log` — runtime process diagnostics.
|
|
475
|
-
* - `host/...` — managed host logs when the platform includes them.
|
|
476
|
-
* These all live in the run's `logs` namespace (`runs/<id>/logs/`).
|
|
477
|
-
* Each is downloaded through the gated `/logs/:id/download` endpoint,
|
|
478
|
-
* decoded as UTF-8 text when the content type looks textual, and
|
|
479
|
-
* surfaced as raw bytes (base64) otherwise. The call is best-effort: a
|
|
480
|
-
* download failure for one file does not block the others; the failing
|
|
481
|
-
* entry lands in `errors` with the underlying message.
|
|
482
|
-
*
|
|
483
|
-
* Use this when a run failed or behaved oddly and you want all the
|
|
484
|
-
* post-mortem material in one round-trip — no need to wire
|
|
485
|
-
* `listOutputs` + `createOutputLink` by hand.
|
|
486
|
-
*/
|
|
487
|
-
async getRunDebugLogs(runId) {
|
|
488
|
-
// The `logs` namespace IS the diagnostics surface — everything it
|
|
489
|
-
// lists is a debug artifact, so no client-side prefix filter.
|
|
490
|
-
const matches = await operations.listLogs(this.#http, runId);
|
|
491
|
-
const logs = [];
|
|
492
|
-
const errors = [];
|
|
493
|
-
for (const out of matches) {
|
|
494
|
-
const filename = out.filename ?? "(unnamed)";
|
|
495
|
-
try {
|
|
496
|
-
const { response } = await this.#http.download(`/api/runs/${runId}/logs/${out.id}/download`);
|
|
497
|
-
const buf = await response.arrayBuffer();
|
|
498
|
-
const bytes = new Uint8Array(buf);
|
|
499
|
-
const contentType = out.contentType ?? "application/octet-stream";
|
|
500
|
-
const isText = /^(text\/|application\/json)/.test(contentType);
|
|
501
|
-
const bytesBase64 = bytesToBase64(bytes);
|
|
502
|
-
logs.push({
|
|
503
|
-
filename,
|
|
504
|
-
sizeBytes: out.sizeBytes ?? bytes.byteLength,
|
|
505
|
-
contentType,
|
|
506
|
-
createdAt: out.createdAt ?? new Date(0).toISOString(),
|
|
507
|
-
...(isText ? { text: new TextDecoder().decode(bytes) } : {}),
|
|
508
|
-
bytesBase64
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
catch (err) {
|
|
512
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
513
|
-
errors.push({ filename, message });
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
return { runId, logs, errors };
|
|
517
|
-
}
|
|
518
|
-
/** Short alias for `getRunDebugLogs`. */
|
|
519
|
-
debugLogs(runId) {
|
|
520
|
-
return this.getRunDebugLogs(runId);
|
|
521
|
-
}
|
|
522
487
|
cancelRun(runId) {
|
|
523
488
|
return operations.cancelRun(this.#http, runId);
|
|
524
489
|
}
|
|
@@ -545,12 +510,11 @@ export class AexClient {
|
|
|
545
510
|
return operations.whoami(this.#http);
|
|
546
511
|
}
|
|
547
512
|
/**
|
|
548
|
-
* Download EVERYTHING about a run as one zip, assembled client-side
|
|
513
|
+
* Download EVERYTHING public about a run as one zip, assembled client-side
|
|
549
514
|
* from the public read endpoints (`getRun` + `listEvents` +
|
|
550
|
-
* `listOutputs` + per-output `/download`). Organised into the
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
* 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
|
|
554
518
|
* bytes to a file path while still returning the bytes.
|
|
555
519
|
*/
|
|
556
520
|
async download(runId, options) {
|
|
@@ -560,10 +524,6 @@ export class AexClient {
|
|
|
560
524
|
async downloadOutputs(runId, options) {
|
|
561
525
|
return writeOptionalFile(await operations.downloadOutputs(this.#http, runId), options?.to);
|
|
562
526
|
}
|
|
563
|
-
/** Download only the platform diagnostics (the `logs` namespace) as a zip. */
|
|
564
|
-
async downloadLogs(runId, options) {
|
|
565
|
-
return writeOptionalFile(await operations.downloadLogs(this.#http, runId), options?.to);
|
|
566
|
-
}
|
|
567
527
|
/** Download only the indexed event archive (the `events` namespace) as a zip. */
|
|
568
528
|
async downloadEvents(runId, options) {
|
|
569
529
|
return writeOptionalFile(await operations.downloadEvents(this.#http, runId), options?.to);
|
|
@@ -594,7 +554,7 @@ function resolveOutputFileSelector(outputs, selector, runId) {
|
|
|
594
554
|
if (isOutputPathSelector(selector)) {
|
|
595
555
|
const target = normalizeOutputLookupPath(selector.path);
|
|
596
556
|
if (!target) {
|
|
597
|
-
throw new RunStateError("
|
|
557
|
+
throw new RunStateError("AgentExecutor.downloadOutput: output path must be non-empty", {
|
|
598
558
|
runId,
|
|
599
559
|
path: selector.path
|
|
600
560
|
});
|
|
@@ -611,15 +571,15 @@ function resolveOutputFileSelector(outputs, selector, runId) {
|
|
|
611
571
|
if (matches.length === 1)
|
|
612
572
|
return matches[0];
|
|
613
573
|
if (matches.length > 1) {
|
|
614
|
-
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) });
|
|
615
575
|
}
|
|
616
|
-
throw new RunStateError(`
|
|
576
|
+
throw new RunStateError(`AgentExecutor.downloadOutput: output path "${selector.path}" was not found`, {
|
|
617
577
|
runId,
|
|
618
578
|
path: selector.path
|
|
619
579
|
});
|
|
620
580
|
}
|
|
621
581
|
if (typeof selector.id !== "string" || selector.id.length === 0) {
|
|
622
|
-
throw new RunStateError("
|
|
582
|
+
throw new RunStateError("AgentExecutor.downloadOutput: selector must include an output id or path", { runId });
|
|
623
583
|
}
|
|
624
584
|
return { ...selector, id: selector.id };
|
|
625
585
|
}
|
|
@@ -650,23 +610,6 @@ function sleep(ms, signal) {
|
|
|
650
610
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
651
611
|
});
|
|
652
612
|
}
|
|
653
|
-
/**
|
|
654
|
-
* Encode a byte array as base64. Uses Node's `Buffer` when available
|
|
655
|
-
* (the SDK ships as a Node tarball; this is the hot path), and falls
|
|
656
|
-
* back to `btoa` for browser or edge runtimes that pull the SDK in
|
|
657
|
-
* without Buffer.
|
|
658
|
-
*/
|
|
659
|
-
function bytesToBase64(bytes) {
|
|
660
|
-
const BufferCtor = globalThis.Buffer;
|
|
661
|
-
if (BufferCtor) {
|
|
662
|
-
return BufferCtor.from(bytes).toString("base64");
|
|
663
|
-
}
|
|
664
|
-
let binary = "";
|
|
665
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
666
|
-
binary += String.fromCharCode(bytes[i]);
|
|
667
|
-
}
|
|
668
|
-
return globalThis.btoa(binary);
|
|
669
|
-
}
|
|
670
613
|
function generateIdempotencyKey() {
|
|
671
614
|
const cryptoObj = globalThis.crypto;
|
|
672
615
|
if (cryptoObj?.randomUUID)
|
|
@@ -676,133 +619,325 @@ function generateIdempotencyKey() {
|
|
|
676
619
|
function normalisePrompt(input) {
|
|
677
620
|
if (typeof input === "string") {
|
|
678
621
|
if (!input) {
|
|
679
|
-
throw new Error("
|
|
622
|
+
throw new Error("AgentExecutor.submitRun: prompt must be a non-empty string");
|
|
680
623
|
}
|
|
681
624
|
return [input];
|
|
682
625
|
}
|
|
683
626
|
if (!Array.isArray(input) || input.length === 0) {
|
|
684
|
-
throw new Error("
|
|
627
|
+
throw new Error("AgentExecutor.submitRun: prompt must be a non-empty string or string array");
|
|
685
628
|
}
|
|
686
629
|
for (const segment of input) {
|
|
687
630
|
if (typeof segment !== "string" || !segment) {
|
|
688
|
-
throw new Error("
|
|
631
|
+
throw new Error("AgentExecutor.submitRun: prompt segments must be non-empty strings");
|
|
689
632
|
}
|
|
690
633
|
}
|
|
691
634
|
return [...input];
|
|
692
635
|
}
|
|
693
|
-
/**
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
* inline refs assigned positional slot ids (`transient-0`, …).
|
|
698
|
-
* - `inlineBundles[]` — the bytes for each inline skill,
|
|
699
|
-
* parallel-indexed by slot.
|
|
700
|
-
*
|
|
701
|
-
* Throws on consumed Skills (the user reused a draft after a prior
|
|
702
|
-
* `submitRun` call) so that mistake is loud, not silent.
|
|
703
|
-
*/
|
|
704
|
-
/**
|
|
705
|
-
* Walk the user-provided Skill[], materialize every draft to assets, and return
|
|
706
|
-
* the wire-shape refs.
|
|
707
|
-
*/
|
|
708
|
-
async function materializeSkills(http, skills, fetch) {
|
|
709
|
-
const out = [];
|
|
636
|
+
/** Walk Skill[] and turn drafts into direct-bootstrap descriptors. */
|
|
637
|
+
function prepareSkills(skills) {
|
|
638
|
+
const refs = [];
|
|
639
|
+
const directInputs = [];
|
|
710
640
|
for (let i = 0; i < skills.length; i++) {
|
|
711
641
|
const entry = skills[i];
|
|
712
642
|
if (!(entry instanceof Skill)) {
|
|
713
|
-
throw new Error(`
|
|
643
|
+
throw new Error(`AgentExecutor.submitRun: skills[${i}] must be a Skill instance`);
|
|
714
644
|
}
|
|
715
645
|
if (entry.isConsumed) {
|
|
716
|
-
throw new Error(`
|
|
646
|
+
throw new Error(`AgentExecutor.submitRun: skills[${i}] was already consumed by a prior submitRun`);
|
|
717
647
|
}
|
|
718
648
|
const ref = entry.ref;
|
|
719
649
|
if (ref.kind === "draft") {
|
|
720
650
|
const bundle = entry._takeDraftBundle();
|
|
721
651
|
if (!bundle) {
|
|
722
|
-
throw new Error(`
|
|
652
|
+
throw new Error(`AgentExecutor.submitRun: skills[${i}] is draft but has no bytes`);
|
|
723
653
|
}
|
|
724
|
-
const
|
|
725
|
-
|
|
654
|
+
const input = directInputFor({
|
|
655
|
+
role: "skill",
|
|
656
|
+
index: i,
|
|
657
|
+
name: bundle.name,
|
|
658
|
+
contentHash: bundle.contentHash,
|
|
726
659
|
bytes: bundle.bytes,
|
|
727
|
-
|
|
728
|
-
...(fetch ? { fetch } : {})
|
|
660
|
+
contentType: "application/zip"
|
|
729
661
|
});
|
|
730
|
-
|
|
662
|
+
directInputs.push(input);
|
|
663
|
+
refs.push({
|
|
731
664
|
kind: "asset",
|
|
732
|
-
assetId:
|
|
665
|
+
assetId: input.assetId,
|
|
733
666
|
name: bundle.name
|
|
734
667
|
});
|
|
735
668
|
continue;
|
|
736
669
|
}
|
|
737
670
|
// Already-materialized asset ref.
|
|
738
|
-
|
|
671
|
+
refs.push(ref);
|
|
739
672
|
}
|
|
740
|
-
return
|
|
673
|
+
return { refs, directInputs };
|
|
741
674
|
}
|
|
742
|
-
/**
|
|
743
|
-
|
|
744
|
-
const
|
|
675
|
+
/** Walk AgentsMd[] and turn drafts into direct-bootstrap descriptors. */
|
|
676
|
+
function prepareAgentsMd(agentsMds) {
|
|
677
|
+
const refs = [];
|
|
678
|
+
const directInputs = [];
|
|
745
679
|
for (let i = 0; i < agentsMds.length; i++) {
|
|
746
680
|
const entry = agentsMds[i];
|
|
747
681
|
if (!(entry instanceof AgentsMd)) {
|
|
748
|
-
throw new Error(`
|
|
682
|
+
throw new Error(`AgentExecutor.submitRun: agentsMd[${i}] must be an AgentsMd instance`);
|
|
749
683
|
}
|
|
750
684
|
if (entry.isConsumed) {
|
|
751
|
-
throw new Error(`
|
|
685
|
+
throw new Error(`AgentExecutor.submitRun: agentsMd[${i}] was already consumed by a prior submitRun`);
|
|
752
686
|
}
|
|
753
687
|
const ref = entry.ref;
|
|
754
688
|
if (ref.kind === "draft") {
|
|
755
689
|
const bundle = entry._takeDraftBundle();
|
|
756
690
|
if (!bundle) {
|
|
757
|
-
throw new Error(`
|
|
691
|
+
throw new Error(`AgentExecutor.submitRun: agentsMd[${i}] is draft but has no bytes`);
|
|
758
692
|
}
|
|
759
|
-
const
|
|
760
|
-
|
|
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({
|
|
761
703
|
kind: "asset",
|
|
762
|
-
assetId:
|
|
704
|
+
assetId: input.assetId,
|
|
763
705
|
name: bundle.name
|
|
764
706
|
});
|
|
765
707
|
continue;
|
|
766
708
|
}
|
|
767
|
-
|
|
709
|
+
refs.push(ref);
|
|
768
710
|
}
|
|
769
|
-
return
|
|
711
|
+
return { refs, directInputs };
|
|
770
712
|
}
|
|
771
|
-
/**
|
|
772
|
-
|
|
773
|
-
const
|
|
713
|
+
/** Walk File[] and turn drafts into direct-bootstrap descriptors. */
|
|
714
|
+
function prepareFiles(files) {
|
|
715
|
+
const refs = [];
|
|
716
|
+
const directInputs = [];
|
|
774
717
|
for (let i = 0; i < files.length; i++) {
|
|
775
718
|
const entry = files[i];
|
|
776
719
|
if (!(entry instanceof File)) {
|
|
777
|
-
throw new Error(`
|
|
720
|
+
throw new Error(`AgentExecutor.submitRun: files[${i}] must be a File instance`);
|
|
778
721
|
}
|
|
779
722
|
if (entry.isConsumed) {
|
|
780
|
-
throw new Error(`
|
|
723
|
+
throw new Error(`AgentExecutor.submitRun: files[${i}] was already consumed by a prior submitRun`);
|
|
781
724
|
}
|
|
782
725
|
const ref = entry.ref;
|
|
783
726
|
if (ref.kind === "draft") {
|
|
784
727
|
const bundle = entry._takeDraftBundle();
|
|
785
728
|
if (!bundle) {
|
|
786
|
-
throw new Error(`
|
|
729
|
+
throw new Error(`AgentExecutor.submitRun: files[${i}] is draft but has no bytes`);
|
|
787
730
|
}
|
|
788
|
-
const
|
|
789
|
-
|
|
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
|
|
790
742
|
? {
|
|
791
743
|
kind: "asset",
|
|
792
|
-
assetId:
|
|
744
|
+
assetId: input.assetId,
|
|
793
745
|
name: bundle.name,
|
|
794
746
|
mountPath: bundle.mountPath
|
|
795
747
|
}
|
|
796
748
|
: {
|
|
797
749
|
kind: "asset",
|
|
798
|
-
assetId:
|
|
750
|
+
assetId: input.assetId,
|
|
799
751
|
name: bundle.name
|
|
800
752
|
});
|
|
801
753
|
continue;
|
|
802
754
|
}
|
|
803
|
-
|
|
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
|
+
});
|
|
804
812
|
}
|
|
805
|
-
|
|
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;
|
|
806
941
|
}
|
|
807
942
|
function mergeMcpServers(inputs, explicitSecrets) {
|
|
808
943
|
const submissionMcpServers = [];
|
|
@@ -813,14 +948,14 @@ function mergeMcpServers(inputs, explicitSecrets) {
|
|
|
813
948
|
for (let i = 0; i < inputs.length; i++) {
|
|
814
949
|
const entry = inputs[i];
|
|
815
950
|
if (!(entry instanceof McpServer)) {
|
|
816
|
-
throw new Error(`
|
|
951
|
+
throw new Error(`AgentExecutor.submitRun: mcpServers[${i}] must be an McpServer instance`);
|
|
817
952
|
}
|
|
818
953
|
submissionMcpServers.push(entry.toSubmissionEntry());
|
|
819
954
|
const secret = entry.toSecretEntry();
|
|
820
955
|
if (secret) {
|
|
821
956
|
const existing = secretByName.get(secret.name);
|
|
822
957
|
if (existing && existing.url !== secret.url) {
|
|
823
|
-
throw new Error(`
|
|
958
|
+
throw new Error(`AgentExecutor.submitRun: mcpServers[${i}].url conflicts with secrets.mcpServers["${secret.name}"]`);
|
|
824
959
|
}
|
|
825
960
|
secretByName.set(secret.name, secret);
|
|
826
961
|
}
|
|
@@ -848,7 +983,7 @@ function mergeProxyEndpointAuth(fromInstances, fromExplicitSecrets) {
|
|
|
848
983
|
for (const entry of fromInstances) {
|
|
849
984
|
const existing = byName.get(entry.name);
|
|
850
985
|
if (existing && existing.value.type !== entry.value.type) {
|
|
851
|
-
throw new Error(`
|
|
986
|
+
throw new Error(`AgentExecutor.submitRun: proxyEndpoint "${entry.name}" auth type conflicts ` +
|
|
852
987
|
`with secrets.proxyEndpointAuth (instance=${entry.value.type}, secrets=${existing.value.type})`);
|
|
853
988
|
}
|
|
854
989
|
byName.set(entry.name, entry);
|