@aexhq/sdk 0.29.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -6
- package/dist/_contracts/event-guards.d.ts +67 -0
- package/dist/_contracts/event-guards.js +36 -0
- package/dist/_contracts/index.d.ts +2 -0
- package/dist/_contracts/index.js +6 -0
- package/dist/_contracts/run-trace.d.ts +7 -0
- package/dist/_contracts/run-trace.js +9 -0
- package/dist/_contracts/runtime-types.d.ts +31 -0
- package/dist/_contracts/submission.d.ts +16 -2
- package/dist/_contracts/submission.js +23 -5
- package/dist/agents-md.d.ts +4 -1
- package/dist/agents-md.js +10 -9
- package/dist/agents-md.js.map +1 -1
- package/dist/cli.mjs +17785 -3914
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +91 -12
- package/dist/client.js +236 -77
- package/dist/client.js.map +1 -1
- package/dist/data-tools.d.ts +23 -0
- package/dist/data-tools.js +102 -13
- package/dist/data-tools.js.map +1 -1
- package/dist/file.d.ts +4 -1
- package/dist/file.js +10 -9
- package/dist/file.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/skill.d.ts +8 -6
- package/dist/skill.js +14 -14
- package/dist/skill.js.map +1 -1
- package/dist/tool.d.ts +4 -1
- package/dist/tool.js +10 -8
- package/dist/tool.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/concepts/agent-tools.md +7 -3
- package/docs/events.md +32 -9
- package/docs/networking.md +141 -0
- package/docs/quickstart.md +19 -10
- package/examples/chat-corpus.ts +85 -0
- package/package.json +2 -2
package/dist/cli.mjs.sha256
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
88682d1f64442fabb34e7f3645f950c2a109b192183b100cd75dbaf92fd8f385 cli.mjs
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HttpClient, SecretString, type AexEvent, type AgentsMdRecord, type CredentialMode, type DebugSink, type FetchLike, type FileRecord, type Output, type OutputFileType, type OutputLink, type OutputLinkOptions, type OutputQuery, type OutputText, type OutputMode, type ReadOutputTextOptions, type RunListPage, type RunListQuery, type PlatformEnvironmentInput, type PlatformSubmission, type PlatformInlineSecrets, type PlatformProxyEndpoint, type PlatformProxyEndpointAuth, type PlatformPostHookInput, type Run, type RunModel, type RunEvent, type RunLimits, type RunWebhookDelivery, type RunProvider, type Region, type SecretRecord, type SecretReveal, type RunUnit, type BuiltinToolName, type RuntimeSize, type RuntimeKind, type Skill as SkillRecord, type WhoAmI } from "./_contracts/index.js";
|
|
1
|
+
import { HttpClient, SecretString, type AexEvent, type AgentsMdRecord, type CredentialMode, type DebugSink, type FetchLike, type FileRecord, type Output, type OutputFileType, type OutputLink, type OutputLinkOptions, type OutputQuery, type OutputText, type OutputMode, type ReadOutputTextOptions, type RunListPage, type RunListQuery, type OutputSearchQuery, type OutputSearchPage, type PlatformEnvironmentInput, type PlatformSubmission, type PlatformInlineSecrets, type PlatformProxyEndpoint, type PlatformProxyEndpointAuth, type PlatformPostHookInput, type Run, type RunModel, type RunEvent, type RunTrace, type UsageSummary, type RunLimits, type RunWebhookDelivery, type RunProvider, type Region, type SecretRecord, type SecretReveal, type RunUnit, type BuiltinToolName, type RuntimeSize, type RuntimeKind, type Skill as SkillRecord, type WhoAmI } from "./_contracts/index.js";
|
|
2
2
|
import { AgentsMd } from "./agents-md.js";
|
|
3
3
|
import { type UploadedAsset } from "./asset-upload.js";
|
|
4
4
|
import { File } from "./file.js";
|
|
@@ -40,8 +40,9 @@ export interface AgentExecutorOptions {
|
|
|
40
40
|
* secret is bundled into the constructor and split into
|
|
41
41
|
* `secrets.proxyEndpointAuth` server-side; the public submission
|
|
42
42
|
* only carries the declaration (`{ name, baseUrl, authShape, … }`).
|
|
43
|
-
* - `
|
|
44
|
-
*
|
|
43
|
+
* - `apiKey` / `credentials` / `secrets` — the BYOK provider key(s). A key for
|
|
44
|
+
* the selected provider is REQUIRED; the simplest call passes just `apiKey`.
|
|
45
|
+
* Use `credentials` (or `secrets.apiKeys`) to carry keys for additional
|
|
45
46
|
* providers so subagents spawned with a different-family model can use them
|
|
46
47
|
* (the child inherits the parent's keys server-side). The platform never
|
|
47
48
|
* holds a long-lived provider key on your behalf.
|
|
@@ -171,7 +172,26 @@ export interface SubmitOptions {
|
|
|
171
172
|
* typing UIs.
|
|
172
173
|
*/
|
|
173
174
|
readonly outputMode?: OutputMode;
|
|
174
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Single-provider BYOK key sugar — the key for the run's selected `provider`.
|
|
177
|
+
* The simplest call passes just `apiKey` (no nested `secrets` envelope). When
|
|
178
|
+
* several sources name a key for the same provider they must agree (else submit
|
|
179
|
+
* throws); resolution precedence is
|
|
180
|
+
* `secrets.apiKeys[provider] ?? secrets.apiKey ?? credentials[provider] ?? apiKey`.
|
|
181
|
+
*/
|
|
182
|
+
readonly apiKey?: string;
|
|
183
|
+
/**
|
|
184
|
+
* Multi-provider BYOK key map — the clean way to supply keys for more than one
|
|
185
|
+
* provider (e.g. so a subagent spawned with a different-family model inherits a
|
|
186
|
+
* key server-side). Folded into the `secrets.apiKeys` wire shape.
|
|
187
|
+
*/
|
|
188
|
+
readonly credentials?: Partial<Record<RunProvider, string>>;
|
|
189
|
+
/**
|
|
190
|
+
* Advanced inline secrets bundle (per-provider `apiKeys`, MCP headers, proxy
|
|
191
|
+
* auth, env secrets). OPTIONAL — the common case uses `apiKey` / `credentials`.
|
|
192
|
+
* `secrets.apiKey` / `secrets.apiKeys` keep working unchanged.
|
|
193
|
+
*/
|
|
194
|
+
readonly secrets?: PlatformInlineSecrets;
|
|
175
195
|
readonly idempotencyKey?: string;
|
|
176
196
|
/**
|
|
177
197
|
* Lineage parent (agent-session §9). When set, the server admits this run as
|
|
@@ -205,6 +225,42 @@ export interface SubmitOptions {
|
|
|
205
225
|
}
|
|
206
226
|
/** @deprecated Renamed to {@link SubmitOptions}. Kept for one release. */
|
|
207
227
|
export type SubmitRunOptions = SubmitOptions;
|
|
228
|
+
/**
|
|
229
|
+
* The settle-consistent result of {@link AgentExecutor.run} / `runAndCollect`:
|
|
230
|
+
* the terminal run record plus its settle-bracketed events, decoded trace,
|
|
231
|
+
* assistant text, and captured outputs — everything a "do it and give me the
|
|
232
|
+
* result" caller needs without hand-rolling a poll loop.
|
|
233
|
+
*/
|
|
234
|
+
export interface RunResult {
|
|
235
|
+
readonly runId: string;
|
|
236
|
+
/** The full terminal run record (status, costTelemetry, timings). */
|
|
237
|
+
readonly run: Run;
|
|
238
|
+
readonly status: string;
|
|
239
|
+
/** `true` when `status === "succeeded"`. */
|
|
240
|
+
readonly ok: boolean;
|
|
241
|
+
/** The assistant's final text (decoded over the settled events). */
|
|
242
|
+
readonly text: string;
|
|
243
|
+
/** The settle-bracketed event stream (RUN_STARTED … terminal). */
|
|
244
|
+
readonly events: readonly RunEvent[];
|
|
245
|
+
/** Decoded view of the events: tool calls + usage + assistant text. */
|
|
246
|
+
readonly trace: RunTrace;
|
|
247
|
+
/** The run's captured output files. */
|
|
248
|
+
readonly outputs: readonly Output[];
|
|
249
|
+
/** Aggregate token usage when the deployment exposes it on the record. */
|
|
250
|
+
readonly usage?: UsageSummary;
|
|
251
|
+
/** Settle-time showback estimate (USD), from `run.costTelemetry`. */
|
|
252
|
+
readonly costUsd?: number;
|
|
253
|
+
/** The run's error message when `!ok`. */
|
|
254
|
+
readonly error?: string;
|
|
255
|
+
}
|
|
256
|
+
/** Options for {@link AgentExecutor.run} / `runAndCollect`. */
|
|
257
|
+
export interface RunCollectOptions {
|
|
258
|
+
/** Overall wait budget (ms) for the run to reach a terminal record. */
|
|
259
|
+
readonly timeoutMs?: number;
|
|
260
|
+
readonly signal?: AbortSignal;
|
|
261
|
+
/** Throw a {@link RunStateError} when the run does not succeed. Default false. */
|
|
262
|
+
readonly throwOnFailure?: boolean;
|
|
263
|
+
}
|
|
208
264
|
export interface StreamEventsOptions {
|
|
209
265
|
/** Poll interval in ms for the `RunEvent` snapshot loop. Default 1000. */
|
|
210
266
|
readonly intervalMs?: number;
|
|
@@ -463,15 +519,26 @@ export declare class AgentExecutor {
|
|
|
463
519
|
readonly contentType?: string;
|
|
464
520
|
}): Promise<UploadedAsset>;
|
|
465
521
|
/**
|
|
466
|
-
* Submit a run
|
|
467
|
-
* "do it and give me the result"
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
522
|
+
* Submit a run, wait until its RECORD is terminal, and collect the full
|
|
523
|
+
* {@link RunResult} — the settle-consistent "do it and give me the result"
|
|
524
|
+
* primitive. Folds the poll loop every consumer hand-rolled into one call:
|
|
525
|
+
* submit → {@link waitForRun} (polls `getRun`, NOT the earlier RUN_FINISHED
|
|
526
|
+
* event) → poll `listEvents` until the snapshot is settle-bracketed
|
|
527
|
+
* (RUN_STARTED + a terminal event present) → `listOutputs` → decode the trace
|
|
528
|
+
* and assistant text. On resolve, `getRun`/`listOutputs` are guaranteed
|
|
529
|
+
* consistent.
|
|
530
|
+
*
|
|
531
|
+
* Uses polling (portable across backends), NOT the coordinator WebSocket. By
|
|
532
|
+
* default a failed run resolves with `ok: false` and a populated `error`; pass
|
|
533
|
+
* `{ throwOnFailure: true }` to throw instead. For live events prefer `submit`
|
|
534
|
+
* + `streamEnvelopes(runId, { settleConsistent: true })`.
|
|
535
|
+
*/
|
|
536
|
+
run(options: SubmitOptions, opts?: RunCollectOptions): Promise<RunResult>;
|
|
537
|
+
/**
|
|
538
|
+
* Explicit, discoverable alias for {@link run}: submit, wait, and collect the
|
|
539
|
+
* full {@link RunResult} in one call.
|
|
473
540
|
*/
|
|
474
|
-
|
|
541
|
+
runAndCollect(options: SubmitOptions, opts?: RunCollectOptions): Promise<RunResult>;
|
|
475
542
|
/**
|
|
476
543
|
* Submit a run and return its run id immediately. Use that id with
|
|
477
544
|
* `wait`, `stream`, `outputs`, `download`, `cancel`, or `delete`.
|
|
@@ -503,6 +570,18 @@ export declare class AgentExecutor {
|
|
|
503
570
|
* `listOutputs` / `readOutputText` to reach any run's deliverables.
|
|
504
571
|
*/
|
|
505
572
|
listRuns(query?: RunListQuery): Promise<RunListPage>;
|
|
573
|
+
/**
|
|
574
|
+
* Find output files across runs by filename / extension / content type.
|
|
575
|
+
* Returns lean REFERENCE hits (runId, outputId, filename, size, content type)
|
|
576
|
+
* — never bytes; fetch content with {@link readOutputText}. Scope the search
|
|
577
|
+
* to a corpus with `query.runIds`; omit it to scan the whole workspace (needs
|
|
578
|
+
* the owner-gated `listRuns`). Composed client-side for the MVP (per-run
|
|
579
|
+
* `listOutputs` + the contracts output filter), so it works against any
|
|
580
|
+
* already-terminal run with no new server endpoint. Bounded by
|
|
581
|
+
* `query.limit` (default 100) — for very large corpora prefer the deferred
|
|
582
|
+
* server-side index.
|
|
583
|
+
*/
|
|
584
|
+
searchOutputs(query?: OutputSearchQuery): Promise<OutputSearchPage>;
|
|
506
585
|
/**
|
|
507
586
|
* Fetch the self-contained `RunUnit`: parsed submission inputs,
|
|
508
587
|
* attempts, indexed events (inline + cursor for the tail), raw
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AexError, DEFAULT_CREDENTIAL_MODE, DEFAULT_RUN_PROVIDER, HttpClient, REGIONS, RUNTIME_KINDS, RunStateError, SecretString, isRunSettled, operations, parseCredentialMode, providersForModel, streamCoordinatorEvents, parseRunLimits, BUILTIN_TOOL_NAMES, TERMINAL_RUN_STATUSES } from "./_contracts/index.js";
|
|
1
|
+
import { AexError, DEFAULT_CREDENTIAL_MODE, DEFAULT_RUN_PROVIDER, HttpClient, REGIONS, RUNTIME_KINDS, RunConfigValidationError, RunStateError, SecretString, isRunSettled, operations, parseCredentialMode, providersForModel, streamCoordinatorEvents, summarizeRunTrace, textOf, parseRunLimits, BUILTIN_TOOL_NAMES, TERMINAL_RUN_STATUSES } from "./_contracts/index.js";
|
|
2
2
|
import { AgentsMd } from "./agents-md.js";
|
|
3
3
|
import { uploadAsset } from "./asset-upload.js";
|
|
4
4
|
import { File } from "./file.js";
|
|
@@ -286,17 +286,86 @@ export class AgentExecutor {
|
|
|
286
286
|
});
|
|
287
287
|
}
|
|
288
288
|
/**
|
|
289
|
-
* Submit a run
|
|
290
|
-
* "do it and give me the result"
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
289
|
+
* Submit a run, wait until its RECORD is terminal, and collect the full
|
|
290
|
+
* {@link RunResult} — the settle-consistent "do it and give me the result"
|
|
291
|
+
* primitive. Folds the poll loop every consumer hand-rolled into one call:
|
|
292
|
+
* submit → {@link waitForRun} (polls `getRun`, NOT the earlier RUN_FINISHED
|
|
293
|
+
* event) → poll `listEvents` until the snapshot is settle-bracketed
|
|
294
|
+
* (RUN_STARTED + a terminal event present) → `listOutputs` → decode the trace
|
|
295
|
+
* and assistant text. On resolve, `getRun`/`listOutputs` are guaranteed
|
|
296
|
+
* consistent.
|
|
297
|
+
*
|
|
298
|
+
* Uses polling (portable across backends), NOT the coordinator WebSocket. By
|
|
299
|
+
* default a failed run resolves with `ok: false` and a populated `error`; pass
|
|
300
|
+
* `{ throwOnFailure: true }` to throw instead. For live events prefer `submit`
|
|
301
|
+
* + `streamEnvelopes(runId, { settleConsistent: true })`.
|
|
296
302
|
*/
|
|
297
|
-
async run(options) {
|
|
303
|
+
async run(options, opts = {}) {
|
|
304
|
+
const signal = opts.signal ?? options.signal;
|
|
298
305
|
const runId = await this.submit(options);
|
|
299
|
-
|
|
306
|
+
const run = await this.waitForRun(runId, {
|
|
307
|
+
...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
|
|
308
|
+
...(signal ? { signal } : {})
|
|
309
|
+
});
|
|
310
|
+
const events = await this.#collectSettledEvents(runId, signal);
|
|
311
|
+
const outputs = await this.listOutputs(runId);
|
|
312
|
+
const ok = run.status === "succeeded";
|
|
313
|
+
const costUsd = run.costTelemetry?.billedCostUsd;
|
|
314
|
+
const errorMessage = typeof run.errorMessage === "string" && run.errorMessage ? run.errorMessage : undefined;
|
|
315
|
+
const result = {
|
|
316
|
+
runId,
|
|
317
|
+
run,
|
|
318
|
+
status: run.status,
|
|
319
|
+
ok,
|
|
320
|
+
text: textOf(events),
|
|
321
|
+
events,
|
|
322
|
+
trace: summarizeRunTrace(events),
|
|
323
|
+
outputs,
|
|
324
|
+
...(run.usage ? { usage: run.usage } : {}),
|
|
325
|
+
...(typeof costUsd === "number" ? { costUsd } : {}),
|
|
326
|
+
...(!ok && errorMessage ? { error: errorMessage } : {})
|
|
327
|
+
};
|
|
328
|
+
if (opts.throwOnFailure && !ok) {
|
|
329
|
+
throw new RunStateError(`AgentExecutor.run: run ${runId} ended ${run.status}${errorMessage ? `: ${errorMessage}` : ""}`, { runId, status: run.status });
|
|
330
|
+
}
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Explicit, discoverable alias for {@link run}: submit, wait, and collect the
|
|
335
|
+
* full {@link RunResult} in one call.
|
|
336
|
+
*/
|
|
337
|
+
runAndCollect(options, opts) {
|
|
338
|
+
return this.run(options, opts);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Poll `listEvents` until the snapshot is settle-bracketed — both a
|
|
342
|
+
* RUN_STARTED and a terminal (RUN_FINISHED / RUN_ERROR) event present — then
|
|
343
|
+
* return it. The runner emits the terminal AG-UI event BEFORE the platform
|
|
344
|
+
* commits the record, and the `listEvents` snapshot can lag the terminal
|
|
345
|
+
* record by a beat; this closes that race so the decoded trace/text/outputs
|
|
346
|
+
* are complete. Bounded so an older runtime that never emits one of the
|
|
347
|
+
* brackets still returns the best snapshot available.
|
|
348
|
+
*/
|
|
349
|
+
async #collectSettledEvents(runId, signal) {
|
|
350
|
+
const intervalMs = 500;
|
|
351
|
+
const maxAttempts = 20;
|
|
352
|
+
let latest = [];
|
|
353
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
354
|
+
if (signal?.aborted)
|
|
355
|
+
return latest;
|
|
356
|
+
latest = await this.listEvents(runId);
|
|
357
|
+
const hasStart = latest.some((event) => event.type === "RUN_STARTED");
|
|
358
|
+
const hasTerminal = latest.some((event) => event.type === "RUN_FINISHED" || event.type === "RUN_ERROR");
|
|
359
|
+
if (hasStart && hasTerminal)
|
|
360
|
+
return latest;
|
|
361
|
+
try {
|
|
362
|
+
await sleep(intervalMs, signal);
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
return latest;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return latest;
|
|
300
369
|
}
|
|
301
370
|
/**
|
|
302
371
|
* Submit a run and return its run id immediately. Use that id with
|
|
@@ -316,7 +385,7 @@ export class AgentExecutor {
|
|
|
316
385
|
*/
|
|
317
386
|
async submit(options) {
|
|
318
387
|
if (!options || typeof options !== "object") {
|
|
319
|
-
throw new
|
|
388
|
+
throw new RunConfigValidationError("AgentExecutor.submit: options is required");
|
|
320
389
|
}
|
|
321
390
|
// A model maps to one or more upstream providers (see MODEL_PROVIDER_IDS).
|
|
322
391
|
// `providersForModel` returns the supported providers in priority order, or
|
|
@@ -327,27 +396,23 @@ export class AgentExecutor {
|
|
|
327
396
|
if (options.provider &&
|
|
328
397
|
supportedProviders.length > 0 &&
|
|
329
398
|
!supportedProviders.includes(options.provider)) {
|
|
330
|
-
throw new
|
|
399
|
+
throw new RunConfigValidationError(`AgentExecutor.submit: provider ${JSON.stringify(options.provider)} is not available for ` +
|
|
331
400
|
`model ${JSON.stringify(options.model)} (supported: ${supportedProviders.join(", ")})`);
|
|
332
401
|
}
|
|
333
402
|
const provider = options.provider ?? supportedProviders[0] ?? DEFAULT_RUN_PROVIDER;
|
|
334
403
|
const credentialMode = parseCredentialMode(options.credentialMode);
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
const selectedProviderKey = options.secrets.apiKeys?.[provider] ?? options.secrets.apiKey;
|
|
342
|
-
if (typeof selectedProviderKey !== "string" || !selectedProviderKey) {
|
|
343
|
-
throw new Error("AgentExecutor.submit: secrets.apiKey is required");
|
|
344
|
-
}
|
|
404
|
+
// Resolve the BYOK key(s) across the top-level sugar (`apiKey`,
|
|
405
|
+
// `credentials`) and the advanced `secrets` bundle, folding everything into a
|
|
406
|
+
// single `apiKeys` map (the wire shape the platform reads). Validates the
|
|
407
|
+
// selected provider's key is present and that the sources don't disagree,
|
|
408
|
+
// failing synchronously before any network call.
|
|
409
|
+
const { foldedApiKeys } = resolveSubmitCredentials(options, provider);
|
|
345
410
|
if (typeof options.model !== "string" || !options.model) {
|
|
346
|
-
throw new
|
|
411
|
+
throw new RunConfigValidationError("AgentExecutor.submit: model is required");
|
|
347
412
|
}
|
|
348
413
|
const prompt = normalisePrompt(options.prompt);
|
|
349
414
|
const { endpoints: proxyEndpointDeclarations, auth: proxyEndpointAuthFromInstances } = splitProxyEndpoints(options.proxyEndpoints ?? []);
|
|
350
|
-
const mergedProxyAuth = mergeProxyEndpointAuth(proxyEndpointAuthFromInstances, options.secrets
|
|
415
|
+
const mergedProxyAuth = mergeProxyEndpointAuth(proxyEndpointAuthFromInstances, options.secrets?.proxyEndpointAuth ?? []);
|
|
351
416
|
// Split secretEnv into value-free declarations (hashed submission) and
|
|
352
417
|
// ephemeral values (vaulted secrets channel), mirroring the proxy split.
|
|
353
418
|
const { declarations: secretEnvDeclarations, values: envSecretValues } = splitSecretEnv(options.secretEnv);
|
|
@@ -383,7 +448,7 @@ export class AgentExecutor {
|
|
|
383
448
|
const preparedTools = await prepareTools(options.tools ?? [], uploader);
|
|
384
449
|
const preparedAgentsMd = await prepareAgentsMd(options.agentsMd ?? [], uploader);
|
|
385
450
|
const preparedFiles = await prepareFiles(options.files ?? [], uploader);
|
|
386
|
-
const { submissionMcpServers, mergedMcpSecrets } = mergeMcpServers(options.mcpServers ?? [], options.secrets
|
|
451
|
+
const { submissionMcpServers, mergedMcpSecrets } = mergeMcpServers(options.mcpServers ?? [], options.secrets?.mcpServers ?? []);
|
|
387
452
|
const outputCapture = outputsForWire(options.outputs);
|
|
388
453
|
const submission = {
|
|
389
454
|
model: options.model,
|
|
@@ -424,6 +489,9 @@ export class AgentExecutor {
|
|
|
424
489
|
};
|
|
425
490
|
const secrets = {
|
|
426
491
|
...options.secrets,
|
|
492
|
+
// Folded BYOK keys (sugar + secrets.apiKeys) override; omitted entirely
|
|
493
|
+
// when empty so a pure legacy `secrets.apiKey` submission stays unchanged.
|
|
494
|
+
...(Object.keys(foldedApiKeys).length > 0 ? { apiKeys: foldedApiKeys } : {}),
|
|
427
495
|
...(mergedMcpSecrets.length > 0 ? { mcpServers: mergedMcpSecrets } : {}),
|
|
428
496
|
...(mergedProxyAuth.length > 0 ? { proxyEndpointAuth: mergedProxyAuth } : {}),
|
|
429
497
|
...(Object.keys(envSecretValues).length > 0 ? { envSecrets: envSecretValues } : {})
|
|
@@ -483,6 +551,60 @@ export class AgentExecutor {
|
|
|
483
551
|
listRuns(query) {
|
|
484
552
|
return operations.listRuns(this.#http, query);
|
|
485
553
|
}
|
|
554
|
+
/**
|
|
555
|
+
* Find output files across runs by filename / extension / content type.
|
|
556
|
+
* Returns lean REFERENCE hits (runId, outputId, filename, size, content type)
|
|
557
|
+
* — never bytes; fetch content with {@link readOutputText}. Scope the search
|
|
558
|
+
* to a corpus with `query.runIds`; omit it to scan the whole workspace (needs
|
|
559
|
+
* the owner-gated `listRuns`). Composed client-side for the MVP (per-run
|
|
560
|
+
* `listOutputs` + the contracts output filter), so it works against any
|
|
561
|
+
* already-terminal run with no new server endpoint. Bounded by
|
|
562
|
+
* `query.limit` (default 100) — for very large corpora prefer the deferred
|
|
563
|
+
* server-side index.
|
|
564
|
+
*/
|
|
565
|
+
async searchOutputs(query = {}) {
|
|
566
|
+
const runIds = query.runIds ?? (await this.#allWorkspaceRunIds());
|
|
567
|
+
const limit = query.limit ?? 100;
|
|
568
|
+
// Translate the search query to an OutputQuery so the contracts output
|
|
569
|
+
// filter (classifyOutput / basename match / contentType wildcard) does the
|
|
570
|
+
// matching — no re-derived filter logic here.
|
|
571
|
+
const outputQuery = {
|
|
572
|
+
...(query.filename ? { filename: new RegExp(escapeRegExp(query.filename), "i") } : {}),
|
|
573
|
+
...(query.extension ? { extension: query.extension } : {}),
|
|
574
|
+
...(query.contentType ? { contentType: query.contentType } : {})
|
|
575
|
+
};
|
|
576
|
+
const hasFilter = Object.keys(outputQuery).length > 0;
|
|
577
|
+
const hits = [];
|
|
578
|
+
for (const runId of runIds) {
|
|
579
|
+
const outputs = hasFilter
|
|
580
|
+
? await this.listOutputs(runId, outputQuery)
|
|
581
|
+
: await this.listOutputs(runId);
|
|
582
|
+
for (const o of outputs) {
|
|
583
|
+
hits.push({
|
|
584
|
+
runId,
|
|
585
|
+
outputId: o.id,
|
|
586
|
+
...(o.filename !== undefined ? { filename: o.filename } : {}),
|
|
587
|
+
...(o.sizeBytes !== undefined ? { sizeBytes: o.sizeBytes } : {}),
|
|
588
|
+
...(o.contentType !== undefined ? { contentType: o.contentType } : {})
|
|
589
|
+
});
|
|
590
|
+
if (hits.length >= limit)
|
|
591
|
+
return { hits };
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return { hits };
|
|
595
|
+
}
|
|
596
|
+
/** Enumerate every run id in the workspace by paging `listRuns`. */
|
|
597
|
+
async #allWorkspaceRunIds() {
|
|
598
|
+
const ids = [];
|
|
599
|
+
let cursor;
|
|
600
|
+
do {
|
|
601
|
+
const page = await this.listRuns(cursor ? { cursor } : {});
|
|
602
|
+
for (const run of page.runs)
|
|
603
|
+
ids.push(run.id);
|
|
604
|
+
cursor = page.nextCursor;
|
|
605
|
+
} while (cursor);
|
|
606
|
+
return ids;
|
|
607
|
+
}
|
|
486
608
|
/**
|
|
487
609
|
* Fetch the self-contained `RunUnit`: parsed submission inputs,
|
|
488
610
|
* attempts, indexed events (inline + cursor for the tail), raw
|
|
@@ -714,6 +836,10 @@ const TERMINAL_STATUSES = new Set(TERMINAL_RUN_STATUSES);
|
|
|
714
836
|
function isTerminal(status) {
|
|
715
837
|
return typeof status === "string" && TERMINAL_STATUSES.has(status);
|
|
716
838
|
}
|
|
839
|
+
/** Escape a literal string for safe interpolation into a RegExp. */
|
|
840
|
+
function escapeRegExp(input) {
|
|
841
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
842
|
+
}
|
|
717
843
|
function isOutputPathSelector(selector) {
|
|
718
844
|
return Boolean(selector && typeof selector === "object" && "path" in selector);
|
|
719
845
|
}
|
|
@@ -793,20 +919,64 @@ function generateIdempotencyKey() {
|
|
|
793
919
|
function normalisePrompt(input) {
|
|
794
920
|
if (typeof input === "string") {
|
|
795
921
|
if (!input) {
|
|
796
|
-
throw new
|
|
922
|
+
throw new RunConfigValidationError("AgentExecutor.submit: prompt must be a non-empty string");
|
|
797
923
|
}
|
|
798
924
|
return [input];
|
|
799
925
|
}
|
|
800
926
|
if (!Array.isArray(input) || input.length === 0) {
|
|
801
|
-
throw new
|
|
927
|
+
throw new RunConfigValidationError("AgentExecutor.submit: prompt must be a non-empty string or string array");
|
|
802
928
|
}
|
|
803
929
|
for (const segment of input) {
|
|
804
930
|
if (typeof segment !== "string" || !segment) {
|
|
805
|
-
throw new
|
|
931
|
+
throw new RunConfigValidationError("AgentExecutor.submit: prompt segments must be non-empty strings");
|
|
806
932
|
}
|
|
807
933
|
}
|
|
808
934
|
return [...input];
|
|
809
935
|
}
|
|
936
|
+
/**
|
|
937
|
+
* Resolve the BYOK provider key(s) for a submission across the top-level sugar
|
|
938
|
+
* (`apiKey`, `credentials`) and the advanced `secrets` bundle, folding them into
|
|
939
|
+
* one `apiKeys` map (the wire shape the platform reads). Per-provider precedence:
|
|
940
|
+
* `secrets.apiKeys[p] ?? secrets.apiKey ?? credentials[p] ?? apiKey`. Legacy
|
|
941
|
+
* `secrets.apiKey` is left in place (not folded) so a pure-legacy submission's
|
|
942
|
+
* wire shape is unchanged; the platform falls back to it for the selected
|
|
943
|
+
* provider. Throws synchronously when the selected provider has no key, or when
|
|
944
|
+
* the sources name DIFFERENT keys for it (a call-site mistake).
|
|
945
|
+
*/
|
|
946
|
+
function resolveSubmitCredentials(options, provider) {
|
|
947
|
+
const secrets = options.secrets;
|
|
948
|
+
const selectedCandidates = [
|
|
949
|
+
secrets?.apiKeys?.[provider],
|
|
950
|
+
secrets?.apiKey,
|
|
951
|
+
options.credentials?.[provider],
|
|
952
|
+
options.apiKey
|
|
953
|
+
].filter((value) => typeof value === "string" && value.length > 0);
|
|
954
|
+
if (new Set(selectedCandidates).size > 1) {
|
|
955
|
+
throw new RunConfigValidationError(`AgentExecutor.submit: conflicting API keys for provider ${JSON.stringify(provider)} ` +
|
|
956
|
+
"(secrets.apiKeys / secrets.apiKey / credentials / apiKey disagree). Supply exactly one.");
|
|
957
|
+
}
|
|
958
|
+
if (selectedCandidates.length === 0) {
|
|
959
|
+
throw new RunConfigValidationError("AgentExecutor.submit: a provider API key is required — pass `apiKey`, " +
|
|
960
|
+
"`credentials[provider]`, `secrets.apiKey`, or `secrets.apiKeys[provider]`.");
|
|
961
|
+
}
|
|
962
|
+
// Fold in precedence order (lowest first; later writes win):
|
|
963
|
+
// apiKey (selected provider) < credentials < secrets.apiKeys.
|
|
964
|
+
const foldedApiKeys = {};
|
|
965
|
+
if (typeof options.apiKey === "string" && options.apiKey.length > 0) {
|
|
966
|
+
foldedApiKeys[provider] = options.apiKey;
|
|
967
|
+
}
|
|
968
|
+
for (const [p, key] of Object.entries(options.credentials ?? {})) {
|
|
969
|
+
if (typeof key === "string" && key.length > 0) {
|
|
970
|
+
foldedApiKeys[p] = key;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
for (const [p, key] of Object.entries(secrets?.apiKeys ?? {})) {
|
|
974
|
+
if (typeof key === "string" && key.length > 0) {
|
|
975
|
+
foldedApiKeys[p] = key;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return { foldedApiKeys };
|
|
979
|
+
}
|
|
810
980
|
function postHookForWire(input) {
|
|
811
981
|
if (input === undefined || typeof input.command !== "string" || input.command.trim().length === 0) {
|
|
812
982
|
return undefined;
|
|
@@ -835,31 +1005,41 @@ function outputsForWire(outputs) {
|
|
|
835
1005
|
...(outputs.maxFiles !== undefined ? { maxFiles: outputs.maxFiles } : {})
|
|
836
1006
|
};
|
|
837
1007
|
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Resolve a draft's asset id: reuse the cached id from a prior submit, otherwise
|
|
1010
|
+
* upload the bytes and cache the result on the instance.
|
|
1011
|
+
*/
|
|
1012
|
+
async function resolveAssetId(entry, bundle, uploader) {
|
|
1013
|
+
const cached = entry._cachedAssetId;
|
|
1014
|
+
if (cached !== undefined) {
|
|
1015
|
+
return cached;
|
|
1016
|
+
}
|
|
1017
|
+
const uploaded = await uploader({
|
|
1018
|
+
bytes: bundle.bytes,
|
|
1019
|
+
hash: bundle.contentHash,
|
|
1020
|
+
contentType: "application/zip"
|
|
1021
|
+
});
|
|
1022
|
+
entry._rememberAsset(uploaded.assetId);
|
|
1023
|
+
return uploaded.assetId;
|
|
1024
|
+
}
|
|
838
1025
|
/** Walk Skill[], eagerly upload drafts as assets, and return plain asset refs. */
|
|
839
1026
|
async function prepareSkills(skills, uploader) {
|
|
840
1027
|
const refs = [];
|
|
841
1028
|
for (let i = 0; i < skills.length; i++) {
|
|
842
1029
|
const entry = skills[i];
|
|
843
1030
|
if (!(entry instanceof Skill)) {
|
|
844
|
-
throw new
|
|
845
|
-
}
|
|
846
|
-
if (entry.isConsumed) {
|
|
847
|
-
throw new Error(`AgentExecutor.submit: skills[${i}] was already consumed by a prior submit`);
|
|
1031
|
+
throw new RunConfigValidationError(`AgentExecutor.submit: skills[${i}] must be a Skill instance`);
|
|
848
1032
|
}
|
|
849
1033
|
const ref = entry.ref;
|
|
850
1034
|
if (ref.kind === "draft") {
|
|
851
1035
|
const bundle = entry._takeDraftBundle();
|
|
852
1036
|
if (!bundle) {
|
|
853
|
-
throw new
|
|
1037
|
+
throw new RunConfigValidationError(`AgentExecutor.submit: skills[${i}] is draft but has no bytes`);
|
|
854
1038
|
}
|
|
855
|
-
const
|
|
856
|
-
bytes: bundle.bytes,
|
|
857
|
-
hash: bundle.contentHash,
|
|
858
|
-
contentType: "application/zip"
|
|
859
|
-
});
|
|
1039
|
+
const assetId = await resolveAssetId(entry, bundle, uploader);
|
|
860
1040
|
refs.push({
|
|
861
1041
|
kind: "asset",
|
|
862
|
-
assetId
|
|
1042
|
+
assetId,
|
|
863
1043
|
name: bundle.name
|
|
864
1044
|
});
|
|
865
1045
|
continue;
|
|
@@ -885,7 +1065,7 @@ async function prepareTools(tools, uploader) {
|
|
|
885
1065
|
// A bare string is a builtin tool reference.
|
|
886
1066
|
if (typeof entry === "string") {
|
|
887
1067
|
if (!BUILTIN_TOOL_NAMES.includes(entry)) {
|
|
888
|
-
throw new
|
|
1068
|
+
throw new RunConfigValidationError(`AgentExecutor.submit: tools[${i}] (${JSON.stringify(entry)}) is not a builtin tool name; ` +
|
|
889
1069
|
`expected a Tool instance or one of: ${BUILTIN_TOOL_NAMES.join(", ")}`);
|
|
890
1070
|
}
|
|
891
1071
|
if (!seenBuiltins.has(entry)) {
|
|
@@ -895,23 +1075,16 @@ async function prepareTools(tools, uploader) {
|
|
|
895
1075
|
continue;
|
|
896
1076
|
}
|
|
897
1077
|
if (!(entry instanceof Tool)) {
|
|
898
|
-
throw new
|
|
899
|
-
}
|
|
900
|
-
if (entry.isConsumed) {
|
|
901
|
-
throw new Error(`AgentExecutor.submit: tools[${i}] was already consumed by a prior submit`);
|
|
1078
|
+
throw new RunConfigValidationError(`AgentExecutor.submit: tools[${i}] must be a Tool instance or a builtin tool name`);
|
|
902
1079
|
}
|
|
903
1080
|
const ref = entry.ref;
|
|
904
1081
|
if (ref.kind === "draft") {
|
|
905
1082
|
const bundle = entry._takeDraftBundle();
|
|
906
1083
|
if (!bundle) {
|
|
907
|
-
throw new
|
|
1084
|
+
throw new RunConfigValidationError(`AgentExecutor.submit: tools[${i}] is draft but has no bytes`);
|
|
908
1085
|
}
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
hash: bundle.contentHash,
|
|
912
|
-
contentType: "application/zip"
|
|
913
|
-
});
|
|
914
|
-
refs.push({ ...bundle.ref, assetId: uploaded.assetId });
|
|
1086
|
+
const assetId = await resolveAssetId(entry, bundle, uploader);
|
|
1087
|
+
refs.push({ ...bundle.ref, assetId });
|
|
915
1088
|
continue;
|
|
916
1089
|
}
|
|
917
1090
|
refs.push(ref);
|
|
@@ -924,25 +1097,18 @@ async function prepareAgentsMd(agentsMds, uploader) {
|
|
|
924
1097
|
for (let i = 0; i < agentsMds.length; i++) {
|
|
925
1098
|
const entry = agentsMds[i];
|
|
926
1099
|
if (!(entry instanceof AgentsMd)) {
|
|
927
|
-
throw new
|
|
928
|
-
}
|
|
929
|
-
if (entry.isConsumed) {
|
|
930
|
-
throw new Error(`AgentExecutor.submit: agentsMd[${i}] was already consumed by a prior submit`);
|
|
1100
|
+
throw new RunConfigValidationError(`AgentExecutor.submit: agentsMd[${i}] must be an AgentsMd instance`);
|
|
931
1101
|
}
|
|
932
1102
|
const ref = entry.ref;
|
|
933
1103
|
if (ref.kind === "draft") {
|
|
934
1104
|
const bundle = entry._takeDraftBundle();
|
|
935
1105
|
if (!bundle) {
|
|
936
|
-
throw new
|
|
1106
|
+
throw new RunConfigValidationError(`AgentExecutor.submit: agentsMd[${i}] is draft but has no bytes`);
|
|
937
1107
|
}
|
|
938
|
-
const
|
|
939
|
-
bytes: bundle.bytes,
|
|
940
|
-
hash: bundle.contentHash,
|
|
941
|
-
contentType: "application/zip"
|
|
942
|
-
});
|
|
1108
|
+
const assetId = await resolveAssetId(entry, bundle, uploader);
|
|
943
1109
|
refs.push({
|
|
944
1110
|
kind: "asset",
|
|
945
|
-
assetId
|
|
1111
|
+
assetId,
|
|
946
1112
|
name: bundle.name
|
|
947
1113
|
});
|
|
948
1114
|
continue;
|
|
@@ -957,25 +1123,18 @@ async function prepareFiles(files, uploader) {
|
|
|
957
1123
|
for (let i = 0; i < files.length; i++) {
|
|
958
1124
|
const entry = files[i];
|
|
959
1125
|
if (!(entry instanceof File)) {
|
|
960
|
-
throw new
|
|
961
|
-
}
|
|
962
|
-
if (entry.isConsumed) {
|
|
963
|
-
throw new Error(`AgentExecutor.submit: files[${i}] was already consumed by a prior submit`);
|
|
1126
|
+
throw new RunConfigValidationError(`AgentExecutor.submit: files[${i}] must be a File instance`);
|
|
964
1127
|
}
|
|
965
1128
|
const ref = entry.ref;
|
|
966
1129
|
if (ref.kind === "draft") {
|
|
967
1130
|
const bundle = entry._takeDraftBundle();
|
|
968
1131
|
if (!bundle) {
|
|
969
|
-
throw new
|
|
1132
|
+
throw new RunConfigValidationError(`AgentExecutor.submit: files[${i}] is draft but has no bytes`);
|
|
970
1133
|
}
|
|
971
|
-
const
|
|
972
|
-
bytes: bundle.bytes,
|
|
973
|
-
hash: bundle.contentHash,
|
|
974
|
-
contentType: "application/zip"
|
|
975
|
-
});
|
|
1134
|
+
const assetId = await resolveAssetId(entry, bundle, uploader);
|
|
976
1135
|
refs.push({
|
|
977
1136
|
kind: "asset",
|
|
978
|
-
assetId
|
|
1137
|
+
assetId,
|
|
979
1138
|
name: bundle.name,
|
|
980
1139
|
mountPath: bundle.mountPath
|
|
981
1140
|
});
|
|
@@ -988,7 +1147,7 @@ async function prepareFiles(files, uploader) {
|
|
|
988
1147
|
function getSubmittedRunId(response) {
|
|
989
1148
|
const id = response.id ?? response.runId;
|
|
990
1149
|
if (typeof id !== "string" || id.length === 0) {
|
|
991
|
-
throw new
|
|
1150
|
+
throw new RunStateError("AgentExecutor.submit: submit response did not include a run id");
|
|
992
1151
|
}
|
|
993
1152
|
return id;
|
|
994
1153
|
}
|
|
@@ -1001,14 +1160,14 @@ function mergeMcpServers(inputs, explicitSecrets) {
|
|
|
1001
1160
|
for (let i = 0; i < inputs.length; i++) {
|
|
1002
1161
|
const entry = inputs[i];
|
|
1003
1162
|
if (!(entry instanceof McpServer)) {
|
|
1004
|
-
throw new
|
|
1163
|
+
throw new RunConfigValidationError(`AgentExecutor.submit: mcpServers[${i}] must be an McpServer instance`);
|
|
1005
1164
|
}
|
|
1006
1165
|
submissionMcpServers.push(entry.toSubmissionEntry());
|
|
1007
1166
|
const secret = entry.toSecretEntry();
|
|
1008
1167
|
if (secret) {
|
|
1009
1168
|
const existing = secretByName.get(secret.name);
|
|
1010
1169
|
if (existing && existing.url !== secret.url) {
|
|
1011
|
-
throw new
|
|
1170
|
+
throw new RunConfigValidationError(`AgentExecutor.submit: mcpServers[${i}].url conflicts with secrets.mcpServers["${secret.name}"]`);
|
|
1012
1171
|
}
|
|
1013
1172
|
secretByName.set(secret.name, secret);
|
|
1014
1173
|
}
|
|
@@ -1036,7 +1195,7 @@ function mergeProxyEndpointAuth(fromInstances, fromExplicitSecrets) {
|
|
|
1036
1195
|
for (const entry of fromInstances) {
|
|
1037
1196
|
const existing = byName.get(entry.name);
|
|
1038
1197
|
if (existing && existing.value.type !== entry.value.type) {
|
|
1039
|
-
throw new
|
|
1198
|
+
throw new RunConfigValidationError(`AgentExecutor.submit: proxyEndpoint "${entry.name}" auth type conflicts ` +
|
|
1040
1199
|
`with secrets.proxyEndpointAuth (instance=${entry.value.type}, secrets=${existing.value.type})`);
|
|
1041
1200
|
}
|
|
1042
1201
|
byName.set(entry.name, entry);
|