@aexhq/sdk 0.34.0 → 0.36.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 +16 -15
- package/dist/_contracts/index.d.ts +3 -4
- package/dist/_contracts/index.js +1 -4
- package/dist/_contracts/operations.d.ts +2 -1
- package/dist/_contracts/operations.js +10 -0
- package/dist/_contracts/run-config.d.ts +1 -3
- package/dist/_contracts/run-config.js +2 -7
- package/dist/_contracts/run-trace.d.ts +0 -86
- package/dist/_contracts/run-trace.js +1 -184
- package/dist/_contracts/run-unit.d.ts +2 -25
- package/dist/_contracts/run-unit.js +1 -2
- package/dist/_contracts/runtime-manifest.d.ts +1 -1
- package/dist/_contracts/runtime-security-profile.d.ts +0 -2
- package/dist/_contracts/runtime-security-profile.js +0 -9
- package/dist/_contracts/runtime-types.d.ts +25 -4
- package/dist/_contracts/stable.d.ts +1 -1
- package/dist/_contracts/stable.js +1 -1
- package/dist/_contracts/submission.d.ts +62 -95
- package/dist/_contracts/submission.js +59 -482
- package/dist/cli.mjs +99 -442
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +49 -25
- package/dist/client.js +341 -70
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +9 -15
- package/dist/index.js +11 -17
- package/dist/index.js.map +1 -1
- package/dist/retry.d.ts +162 -0
- package/dist/retry.js +320 -0
- package/dist/retry.js.map +1 -0
- package/dist/secret.d.ts +2 -2
- package/dist/secret.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/concepts/composition.md +8 -14
- package/docs/credentials.md +59 -101
- package/docs/defaults.md +0 -8
- package/docs/events.md +8 -9
- package/docs/limits-and-quotas.md +1 -4
- package/docs/limits.md +2 -6
- package/docs/mcp.md +4 -5
- package/docs/networking.md +6 -16
- package/docs/outputs.md +0 -4
- package/docs/public-surface.json +3 -3
- package/docs/quickstart.md +3 -7
- package/docs/retries.md +129 -0
- package/docs/run-config.md +6 -3
- package/docs/secrets.md +1 -1
- package/docs/skills.md +3 -3
- package/docs/vision-skills.md +52 -101
- package/examples/feature-tour.ts +284 -0
- package/package.json +1 -1
- package/dist/_contracts/proxy-protocol.d.ts +0 -305
- package/dist/_contracts/proxy-protocol.js +0 -297
- package/dist/_contracts/proxy-validation.d.ts +0 -19
- package/dist/_contracts/proxy-validation.js +0 -51
- package/dist/data-tools.d.ts +0 -82
- package/dist/data-tools.js +0 -251
- package/dist/data-tools.js.map +0 -1
- package/dist/proxy-endpoint.d.ts +0 -131
- package/dist/proxy-endpoint.js +0 -144
- package/dist/proxy-endpoint.js.map +0 -1
- package/examples/chat-corpus.ts +0 -84
package/dist/client.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { AexError, DEFAULT_RUN_PROVIDER, HttpClient, RunConfigValidationError, RunStateError, SecretString, customName, isRunSettled, operations, providersForModel, streamCoordinatorEvents,
|
|
1
|
+
import { AexApiError, AexError, DEFAULT_RUN_PROVIDER, HttpClient, RunConfigValidationError, RunStateError, SecretString, customName, isRunSettled, operations, providersForModel, streamCoordinatorEvents, 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";
|
|
5
5
|
import { McpServer } from "./mcp-server.js";
|
|
6
|
-
import {
|
|
6
|
+
import { AexRateLimitError, isThrottleFault, parseProviderFault, withRetry } from "./retry.js";
|
|
7
7
|
import { splitSecretEnv } from "./secret.js";
|
|
8
8
|
import { SkillTool } from "./skill-tool.js";
|
|
9
9
|
import { Tool } from "./tool.js";
|
|
@@ -40,6 +40,8 @@ export class SessionHandle {
|
|
|
40
40
|
#http;
|
|
41
41
|
#fetch;
|
|
42
42
|
#session;
|
|
43
|
+
/** The last message sent on this handle, for {@link SessionHandle.replayLast}. */
|
|
44
|
+
#lastSend;
|
|
43
45
|
constructor(http, session, fetch) {
|
|
44
46
|
this.#http = http;
|
|
45
47
|
this.#session = session;
|
|
@@ -56,8 +58,28 @@ export class SessionHandle {
|
|
|
56
58
|
assertNoSessionSendSignal(options, "SessionHandle.send");
|
|
57
59
|
return sendSessionInternal(this, input, options);
|
|
58
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Re-send the last message on this session — the clean way to retry a turn a
|
|
63
|
+
* throttle or transient failure interrupted. By default it REUSES the previous
|
|
64
|
+
* message's idempotency key, so if the original turn actually landed
|
|
65
|
+
* server-side the replay de-duplicates instead of creating a second billable
|
|
66
|
+
* turn; pass a fresh `idempotencyKey` to force a brand-new turn.
|
|
67
|
+
*/
|
|
68
|
+
replayLast(options = {}) {
|
|
69
|
+
assertNoSessionSendSignal(options, "SessionHandle.replayLast");
|
|
70
|
+
const last = this.#lastSend;
|
|
71
|
+
if (last === undefined) {
|
|
72
|
+
throw new RunStateError("SessionHandle.replayLast: no message has been sent on this session yet");
|
|
73
|
+
}
|
|
74
|
+
return sendSessionInternal(this, last.input, {
|
|
75
|
+
...options,
|
|
76
|
+
idempotencyKey: options.idempotencyKey ?? last.idempotencyKey
|
|
77
|
+
});
|
|
78
|
+
}
|
|
59
79
|
async *#send(input, options) {
|
|
60
|
-
const
|
|
80
|
+
const idempotencyKey = options.idempotencyKey ?? generateIdempotencyKey();
|
|
81
|
+
this.#lastSend = { input, idempotencyKey };
|
|
82
|
+
const accepted = await operations.sendSessionMessage(this.#http, this.id, { input }, { idempotencyKey });
|
|
61
83
|
this.#session = accepted.session;
|
|
62
84
|
const turn = accepted.turn;
|
|
63
85
|
const events = [];
|
|
@@ -72,14 +94,16 @@ export class SessionHandle {
|
|
|
72
94
|
const readSession = await operations.getSession(this.#http, this.id).catch(() => this.#session);
|
|
73
95
|
this.#session = withTerminalSessionStatus(readSession, terminalStatus);
|
|
74
96
|
const outputs = await operations.listSessionOutputs(this.#http, this.id).catch(() => []);
|
|
97
|
+
const messages = projectAssistantMessages(events);
|
|
75
98
|
return {
|
|
76
99
|
sessionId: this.id,
|
|
77
100
|
session: this.#session,
|
|
78
101
|
turn,
|
|
79
102
|
status: this.#session.status,
|
|
80
|
-
text:
|
|
103
|
+
text: assistantTextFromEvents(events),
|
|
81
104
|
events,
|
|
82
|
-
outputs
|
|
105
|
+
outputs,
|
|
106
|
+
messages
|
|
83
107
|
};
|
|
84
108
|
}
|
|
85
109
|
async suspend(options = {}) {
|
|
@@ -104,19 +128,39 @@ export class SessionHandle {
|
|
|
104
128
|
}
|
|
105
129
|
}
|
|
106
130
|
/**
|
|
107
|
-
* Accessor for the session's
|
|
108
|
-
*
|
|
109
|
-
*
|
|
131
|
+
* Accessor for the session's assistant messages. `all()` returns them
|
|
132
|
+
* oldest-first; `last()`/`first()` return a single entry or `undefined` when
|
|
133
|
+
* empty. The accessor is callable as a compatibility shim for older
|
|
134
|
+
* `session.messages().list()` callers.
|
|
110
135
|
*/
|
|
111
|
-
messages() {
|
|
136
|
+
get messages() {
|
|
112
137
|
const http = this.#http;
|
|
113
138
|
const id = this.id;
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
139
|
+
const fromEvents = async () => projectAssistantMessages(await operations.listSessionEvents(http, id));
|
|
140
|
+
const all = async () => {
|
|
141
|
+
try {
|
|
142
|
+
const page = await operations.listSessionMessages(http, id);
|
|
143
|
+
if (!Array.isArray(page.messages)) {
|
|
144
|
+
return fromEvents();
|
|
145
|
+
}
|
|
146
|
+
return page.messages.map(messageFromWire);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
if (!isMissingMessagesEndpoint(err)) {
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
return fromEvents();
|
|
153
|
+
}
|
|
119
154
|
};
|
|
155
|
+
let accessor;
|
|
156
|
+
accessor = (() => accessor);
|
|
157
|
+
Object.assign(accessor, {
|
|
158
|
+
all,
|
|
159
|
+
list: all,
|
|
160
|
+
last: async () => (await all()).at(-1),
|
|
161
|
+
first: async () => (await all())[0]
|
|
162
|
+
});
|
|
163
|
+
return accessor;
|
|
120
164
|
}
|
|
121
165
|
/**
|
|
122
166
|
* Accessor for the session's event stream: the buffered `SessionEvent`
|
|
@@ -303,10 +347,15 @@ export class SessionClient {
|
|
|
303
347
|
const { message, deleteAfter, messageIdempotencyKey, stream, ...createOptions } = options;
|
|
304
348
|
assertNoLegacySessionFields(options, "Aex.sessions.run");
|
|
305
349
|
const input = normaliseSessionInput(message, "Aex.sessions.run", "message");
|
|
306
|
-
|
|
350
|
+
// Derive the message key from the create key (like the CLI) so a retried run
|
|
351
|
+
// with the same `idempotencyKey` de-duplicates BOTH the create and the
|
|
352
|
+
// billable turn — never a duplicate billable run.
|
|
353
|
+
const createKey = createOptions.idempotencyKey ?? generateIdempotencyKey();
|
|
354
|
+
const messageKey = messageIdempotencyKey ?? deriveMessageKey(createKey);
|
|
355
|
+
const session = await this.create({ ...createOptions, idempotencyKey: createKey });
|
|
307
356
|
const result = await session.send(input, {
|
|
308
357
|
...(stream ?? {}),
|
|
309
|
-
idempotencyKey:
|
|
358
|
+
idempotencyKey: messageKey
|
|
310
359
|
}).done();
|
|
311
360
|
if (deleteAfter) {
|
|
312
361
|
await session.delete();
|
|
@@ -419,6 +468,199 @@ function sessionOutputs(http, id, fetchLike) {
|
|
|
419
468
|
download: (selector, options) => downloadSessionOutput(http, id, selector, options)
|
|
420
469
|
};
|
|
421
470
|
}
|
|
471
|
+
function messageFromWire(message) {
|
|
472
|
+
return {
|
|
473
|
+
id: message.id,
|
|
474
|
+
sender: message.sender,
|
|
475
|
+
text: message.text,
|
|
476
|
+
...(message.timestamp !== undefined ? { timestamp: message.timestamp } : {}),
|
|
477
|
+
...(message.turnSeq !== undefined ? { turnSeq: message.turnSeq } : {}),
|
|
478
|
+
...(message.sequence !== undefined ? { sequence: message.sequence } : {})
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function isMissingMessagesEndpoint(err) {
|
|
482
|
+
return err instanceof AexApiError && (err.status === 404 || err.status === 405 || err.status === 501);
|
|
483
|
+
}
|
|
484
|
+
function projectAssistantMessages(events) {
|
|
485
|
+
const out = [];
|
|
486
|
+
const byMessageId = new Map();
|
|
487
|
+
for (let i = 0; i < events.length; i++) {
|
|
488
|
+
const event = events[i];
|
|
489
|
+
if (event.type !== "TEXT_MESSAGE_CONTENT")
|
|
490
|
+
continue;
|
|
491
|
+
const data = asRecord(event.data);
|
|
492
|
+
const text = typeof data.text === "string" ? data.text : undefined;
|
|
493
|
+
if (text === undefined)
|
|
494
|
+
continue;
|
|
495
|
+
const messageId = typeof data.messageId === "string" && data.messageId ? data.messageId : undefined;
|
|
496
|
+
const sequence = event.sequence ?? event.seq;
|
|
497
|
+
const timestamp = event.time ?? event.recordedAt ?? timestampFromEpochMs(event.receivedAt);
|
|
498
|
+
const turnSeq = typeof data.turnSeq === "number" ? data.turnSeq : undefined;
|
|
499
|
+
if (messageId !== undefined) {
|
|
500
|
+
const existing = byMessageId.get(messageId);
|
|
501
|
+
if (existing !== undefined) {
|
|
502
|
+
const current = out[existing];
|
|
503
|
+
out[existing] = {
|
|
504
|
+
...current,
|
|
505
|
+
text: `${current.text}${text}`,
|
|
506
|
+
...(timestamp !== undefined ? { timestamp } : {}),
|
|
507
|
+
...(sequence !== undefined ? { sequence } : {}),
|
|
508
|
+
...(turnSeq !== undefined ? { turnSeq } : {})
|
|
509
|
+
};
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
byMessageId.set(messageId, out.length);
|
|
513
|
+
}
|
|
514
|
+
out.push({
|
|
515
|
+
id: messageId ?? (typeof event.id === "string" && event.id ? event.id : `message-${i}`),
|
|
516
|
+
sender: "assistant",
|
|
517
|
+
text,
|
|
518
|
+
...(timestamp !== undefined ? { timestamp } : {}),
|
|
519
|
+
...(sequence !== undefined ? { sequence } : {}),
|
|
520
|
+
...(turnSeq !== undefined ? { turnSeq } : {})
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
return out;
|
|
524
|
+
}
|
|
525
|
+
function assistantTextFromEvents(events) {
|
|
526
|
+
return assistantTextEntriesFromEvents(events).map((entry) => entry.text).join("");
|
|
527
|
+
}
|
|
528
|
+
function runTraceFromEvents(events) {
|
|
529
|
+
return {
|
|
530
|
+
toolCalls: toolCallsFromEvents(events),
|
|
531
|
+
usage: usageFromEvents(events),
|
|
532
|
+
text: assistantTextEntriesFromEvents(events)
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function assistantTextEntriesFromEvents(events) {
|
|
536
|
+
const out = [];
|
|
537
|
+
for (const raw of events) {
|
|
538
|
+
const event = raw;
|
|
539
|
+
if (event.type !== "TEXT_MESSAGE_CONTENT")
|
|
540
|
+
continue;
|
|
541
|
+
const data = asRecord(event.data);
|
|
542
|
+
const text = typeof data.text === "string" ? data.text : undefined;
|
|
543
|
+
if (text === undefined)
|
|
544
|
+
continue;
|
|
545
|
+
const entry = { text };
|
|
546
|
+
const messageId = typeof data.messageId === "string" ? data.messageId : undefined;
|
|
547
|
+
if (messageId !== undefined)
|
|
548
|
+
entry.messageId = messageId;
|
|
549
|
+
if (typeof event.seq === "number")
|
|
550
|
+
entry.seq = event.seq;
|
|
551
|
+
const recordedAt = typeof event.recordedAt === "string" ? event.recordedAt : undefined;
|
|
552
|
+
if (recordedAt !== undefined)
|
|
553
|
+
entry.recordedAt = recordedAt;
|
|
554
|
+
out.push(entry);
|
|
555
|
+
}
|
|
556
|
+
return out;
|
|
557
|
+
}
|
|
558
|
+
function toolCallsFromEvents(events) {
|
|
559
|
+
const order = [];
|
|
560
|
+
const byId = new Map();
|
|
561
|
+
for (const event of events) {
|
|
562
|
+
const data = asRecord(event.data);
|
|
563
|
+
if (event.type === "TOOL_CALL_START") {
|
|
564
|
+
const id = typeof data.id === "string" ? data.id : undefined;
|
|
565
|
+
if (id === undefined)
|
|
566
|
+
continue;
|
|
567
|
+
const trace = {
|
|
568
|
+
id,
|
|
569
|
+
name: typeof data.name === "string" ? data.name : "",
|
|
570
|
+
args: asRecord(data.arguments)
|
|
571
|
+
};
|
|
572
|
+
const messageId = typeof data.messageId === "string" ? data.messageId : undefined;
|
|
573
|
+
if (messageId !== undefined)
|
|
574
|
+
trace.messageId = messageId;
|
|
575
|
+
if (typeof event.seq === "number")
|
|
576
|
+
trace.startSeq = event.seq;
|
|
577
|
+
if (typeof event.recordedAt === "string")
|
|
578
|
+
trace.startedAt = event.recordedAt;
|
|
579
|
+
if (!byId.has(id))
|
|
580
|
+
order.push(id);
|
|
581
|
+
byId.set(id, trace);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (event.type === "TOOL_CALL_RESULT") {
|
|
585
|
+
const id = typeof data.id === "string" ? data.id : undefined;
|
|
586
|
+
if (id === undefined)
|
|
587
|
+
continue;
|
|
588
|
+
const result = {
|
|
589
|
+
isError: data.isError === true,
|
|
590
|
+
content: data.content ?? null
|
|
591
|
+
};
|
|
592
|
+
if (typeof event.seq === "number")
|
|
593
|
+
result.seq = event.seq;
|
|
594
|
+
if (typeof event.recordedAt === "string")
|
|
595
|
+
result.recordedAt = event.recordedAt;
|
|
596
|
+
let trace = byId.get(id);
|
|
597
|
+
if (trace === undefined) {
|
|
598
|
+
trace = { id, name: "", args: {} };
|
|
599
|
+
order.push(id);
|
|
600
|
+
byId.set(id, trace);
|
|
601
|
+
}
|
|
602
|
+
trace.result = result;
|
|
603
|
+
const duration = durationMs(trace.startedAt, result.recordedAt);
|
|
604
|
+
if (duration !== undefined)
|
|
605
|
+
trace.durationMs = duration;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return order.map((id) => byId.get(id));
|
|
609
|
+
}
|
|
610
|
+
function usageFromEvents(events) {
|
|
611
|
+
const totals = { inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0, cacheCreationInputTokens: 0 };
|
|
612
|
+
let seen = false;
|
|
613
|
+
for (const event of events) {
|
|
614
|
+
if (event.type !== "CUSTOM")
|
|
615
|
+
continue;
|
|
616
|
+
const data = asRecord(event.data);
|
|
617
|
+
if (data.name !== "aex.usage")
|
|
618
|
+
continue;
|
|
619
|
+
const value = asRecord(data.value);
|
|
620
|
+
const fields = [
|
|
621
|
+
["input_tokens", "inputTokens"],
|
|
622
|
+
["output_tokens", "outputTokens"],
|
|
623
|
+
["cache_read_input_tokens", "cacheReadInputTokens"],
|
|
624
|
+
["cache_creation_input_tokens", "cacheCreationInputTokens"]
|
|
625
|
+
];
|
|
626
|
+
for (const [wireName, apiName] of fields) {
|
|
627
|
+
const n = value[wireName];
|
|
628
|
+
if (typeof n === "number" && Number.isFinite(n)) {
|
|
629
|
+
totals[apiName] += n;
|
|
630
|
+
seen = true;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (!seen)
|
|
635
|
+
return {};
|
|
636
|
+
return {
|
|
637
|
+
inputTokens: totals.inputTokens,
|
|
638
|
+
outputTokens: totals.outputTokens,
|
|
639
|
+
cacheReadInputTokens: totals.cacheReadInputTokens,
|
|
640
|
+
cacheCreationInputTokens: totals.cacheCreationInputTokens,
|
|
641
|
+
totalTokens: totals.inputTokens + totals.outputTokens
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
function asRecord(value) {
|
|
645
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
646
|
+
? value
|
|
647
|
+
: {};
|
|
648
|
+
}
|
|
649
|
+
function timestampFromEpochMs(value) {
|
|
650
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
651
|
+
? new Date(value).toISOString()
|
|
652
|
+
: undefined;
|
|
653
|
+
}
|
|
654
|
+
function durationMs(start, end) {
|
|
655
|
+
if (start === undefined || end === undefined)
|
|
656
|
+
return undefined;
|
|
657
|
+
const a = Date.parse(start);
|
|
658
|
+
const b = Date.parse(end);
|
|
659
|
+
if (!Number.isFinite(a) || !Number.isFinite(b))
|
|
660
|
+
return undefined;
|
|
661
|
+
const delta = b - a;
|
|
662
|
+
return delta >= 0 ? delta : undefined;
|
|
663
|
+
}
|
|
422
664
|
function isSessionTurnTerminalEvent(event, turnSeq) {
|
|
423
665
|
const name = customName(event);
|
|
424
666
|
if (name !== "aex.session.idle" &&
|
|
@@ -571,7 +813,7 @@ function unwrapSecretValue(value) {
|
|
|
571
813
|
* `client.whoami()` if you want to introspect which workspace the
|
|
572
814
|
* token resolves to.
|
|
573
815
|
*/
|
|
574
|
-
export class
|
|
816
|
+
export class Aex {
|
|
575
817
|
#http;
|
|
576
818
|
/** The same fetch the HttpClient uses, threaded into `_uploadAsset`. */
|
|
577
819
|
#fetch;
|
|
@@ -579,22 +821,30 @@ export class AgentExecutor {
|
|
|
579
821
|
files;
|
|
580
822
|
secrets;
|
|
581
823
|
sessions;
|
|
582
|
-
constructor(options) {
|
|
583
|
-
|
|
584
|
-
|
|
824
|
+
constructor(options, overrides = {}) {
|
|
825
|
+
const resolved = typeof options === "string" ? { ...overrides, apiKey: options } : options;
|
|
826
|
+
const apiKey = resolved.apiKey ?? resolved.apiToken;
|
|
827
|
+
if (!apiKey) {
|
|
828
|
+
throw new Error("Aex: apiKey is required");
|
|
585
829
|
}
|
|
830
|
+
// Wrap the transport fetch (the caller's override, or global `fetch`) with
|
|
831
|
+
// the bounded-retry layer so every BFF request gets default resilience.
|
|
832
|
+
// The raw `#fetch` below stays unwrapped for the direct-to-storage asset PUT
|
|
833
|
+
// and presigned output GETs, which target object storage, not the API plane.
|
|
834
|
+
const baseFetch = resolved.fetch ?? ((input, init) => fetch(input, init));
|
|
835
|
+
const retryingFetch = withRetry(baseFetch, resolved.retry);
|
|
586
836
|
this.#http = new HttpClient({
|
|
587
|
-
...(
|
|
588
|
-
apiToken:
|
|
589
|
-
|
|
837
|
+
...(resolved.baseUrl ? { baseUrl: resolved.baseUrl } : {}),
|
|
838
|
+
apiToken: apiKey,
|
|
839
|
+
fetch: retryingFetch,
|
|
590
840
|
// Opt-in local diagnostics: emit a redacted per-request trace to
|
|
591
841
|
// stderr. Uploads nothing. A caller wanting a custom sink can pass
|
|
592
842
|
// a function instead of `true`.
|
|
593
|
-
...(
|
|
594
|
-
? { debug: typeof
|
|
843
|
+
...(resolved.debug
|
|
844
|
+
? { debug: typeof resolved.debug === "function" ? resolved.debug : (line) => console.error(line) }
|
|
595
845
|
: {})
|
|
596
846
|
});
|
|
597
|
-
this.#fetch =
|
|
847
|
+
this.#fetch = resolved.fetch;
|
|
598
848
|
this.agentsMd = new AgentsMdClient(this.#http);
|
|
599
849
|
this.files = new FilesClient(this.#http);
|
|
600
850
|
this.secrets = new SecretsClient(this.#http);
|
|
@@ -646,10 +896,15 @@ export class AgentExecutor {
|
|
|
646
896
|
...(opts.idleTimeoutMs !== undefined ? { idleTimeoutMs: opts.idleTimeoutMs } : {}),
|
|
647
897
|
...(opts.pingIntervalMs !== undefined ? { pingIntervalMs: opts.pingIntervalMs } : {})
|
|
648
898
|
};
|
|
649
|
-
|
|
899
|
+
// Derive the message key from the create key (like the CLI) so a retried
|
|
900
|
+
// run with the same `idempotencyKey` de-duplicates BOTH the create and the
|
|
901
|
+
// billable turn server-side — never a duplicate billable run (sdk-dx-3).
|
|
902
|
+
const createKey = createOptions.idempotencyKey ?? generateIdempotencyKey();
|
|
903
|
+
const messageKey = messageIdempotencyKey ?? deriveMessageKey(createKey);
|
|
904
|
+
const session = await this.sessions.create({ ...createOptions, idempotencyKey: createKey });
|
|
650
905
|
const turnResult = await sendSessionInternal(session, input, {
|
|
651
906
|
...streamOptions,
|
|
652
|
-
idempotencyKey:
|
|
907
|
+
idempotencyKey: messageKey
|
|
653
908
|
}).done();
|
|
654
909
|
if (deleteAfter) {
|
|
655
910
|
await session.delete();
|
|
@@ -670,15 +925,29 @@ export class AgentExecutor {
|
|
|
670
925
|
status: turnResult.status,
|
|
671
926
|
ok,
|
|
672
927
|
text: turnResult.text,
|
|
928
|
+
messages: turnResult.messages,
|
|
673
929
|
events,
|
|
674
|
-
trace:
|
|
930
|
+
trace: runTraceFromEvents(events),
|
|
675
931
|
outputs,
|
|
676
932
|
...(turnResult.session.usage ? { usage: turnResult.session.usage } : {}),
|
|
677
933
|
...(typeof costUsd === "number" ? { costUsd } : {}),
|
|
678
934
|
...(!ok && errorMessage ? { error: errorMessage } : {})
|
|
679
935
|
};
|
|
680
936
|
if (opts.throwOnFailure && !ok) {
|
|
681
|
-
|
|
937
|
+
// A turn that failed because the upstream provider throttled us surfaces
|
|
938
|
+
// as a structured, non-leaky AexRateLimitError carrying the provider
|
|
939
|
+
// fault, so callers can branch on `isRateLimited(err)` and replay.
|
|
940
|
+
const throttle = throttleFromSession(turnResult.session);
|
|
941
|
+
if (throttle) {
|
|
942
|
+
throw new AexRateLimitError({
|
|
943
|
+
status: throttle.status ?? 429,
|
|
944
|
+
attempts: 1,
|
|
945
|
+
source: "provider",
|
|
946
|
+
providerFault: throttle,
|
|
947
|
+
...(throttle.retryAfterMs !== undefined ? { retryAfterMs: throttle.retryAfterMs } : {})
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
throw new RunStateError(`Aex.run: session ${runId} ended ${turnResult.status}${errorMessage ? `: ${errorMessage}` : ""}`, { runId, status: turnResult.status });
|
|
682
951
|
}
|
|
683
952
|
return result;
|
|
684
953
|
}
|
|
@@ -708,8 +977,6 @@ export class AgentExecutor {
|
|
|
708
977
|
if (typeof options.model !== "string" || !options.model) {
|
|
709
978
|
throw new RunConfigValidationError("Aex.openSession: model is required");
|
|
710
979
|
}
|
|
711
|
-
const { endpoints: proxyEndpointDeclarations, auth: proxyEndpointAuthFromInstances } = splitProxyEndpoints(options.proxyEndpoints ?? []);
|
|
712
|
-
const mergedProxyAuth = mergeProxyEndpointAuth(proxyEndpointAuthFromInstances, []);
|
|
713
980
|
const { declarations: secretEnvDeclarations, values: envSecretValues } = splitSecretEnv(options.environment?.secrets);
|
|
714
981
|
let limits;
|
|
715
982
|
try {
|
|
@@ -752,7 +1019,6 @@ export class AgentExecutor {
|
|
|
752
1019
|
const secrets = {
|
|
753
1020
|
...(options.apiKeys ? { apiKeys: options.apiKeys } : {}),
|
|
754
1021
|
...(mergedMcpSecrets.length > 0 ? { mcpServers: mergedMcpSecrets } : {}),
|
|
755
|
-
...(mergedProxyAuth.length > 0 ? { proxyEndpointAuth: mergedProxyAuth } : {}),
|
|
756
1022
|
...(Object.keys(envSecretValues).length > 0 ? { envSecrets: envSecretValues } : {})
|
|
757
1023
|
};
|
|
758
1024
|
const retention = sessionRetentionForWire(options);
|
|
@@ -766,10 +1032,7 @@ export class AgentExecutor {
|
|
|
766
1032
|
// Operational/delivery concern — sibling of secrets, NOT part of the
|
|
767
1033
|
// hashed submission. Delivered at the settle-consistent barrier.
|
|
768
1034
|
...(options.webhook ? { webhook: options.webhook } : {}),
|
|
769
|
-
secrets
|
|
770
|
-
...(proxyEndpointDeclarations.length > 0
|
|
771
|
-
? { proxyEndpoints: proxyEndpointDeclarations }
|
|
772
|
-
: {})
|
|
1035
|
+
secrets
|
|
773
1036
|
};
|
|
774
1037
|
}
|
|
775
1038
|
/**
|
|
@@ -784,9 +1047,6 @@ export class AgentExecutor {
|
|
|
784
1047
|
return operations.whoami(this.#http);
|
|
785
1048
|
}
|
|
786
1049
|
}
|
|
787
|
-
/** Canonical SDK client name. `AgentExecutor` remains as a compatibility alias. */
|
|
788
|
-
export class Aex extends AgentExecutor {
|
|
789
|
-
}
|
|
790
1050
|
// `Run.status` is a loose `string` on the wire shape, so we membership-test
|
|
791
1051
|
// against the canonical terminal set rather than re-deriving one (which is how
|
|
792
1052
|
// `timed_out` got dropped from the old hardcoded list).
|
|
@@ -843,7 +1103,7 @@ function resolveOutputFileSelector(outputs, selector, runId) {
|
|
|
843
1103
|
if (isOutputPathSelector(selector)) {
|
|
844
1104
|
const target = normalizeOutputLookupPath(selector.path);
|
|
845
1105
|
if (!target) {
|
|
846
|
-
throw new RunStateError("
|
|
1106
|
+
throw new RunStateError("Aex.downloadOutput: output path must be non-empty", {
|
|
847
1107
|
runId,
|
|
848
1108
|
path: selector.path
|
|
849
1109
|
});
|
|
@@ -860,15 +1120,15 @@ function resolveOutputFileSelector(outputs, selector, runId) {
|
|
|
860
1120
|
if (matches.length === 1)
|
|
861
1121
|
return matches[0];
|
|
862
1122
|
if (matches.length > 1) {
|
|
863
|
-
throw new RunStateError(`
|
|
1123
|
+
throw new RunStateError(`Aex.downloadOutput: output path "${selector.path}" matched multiple files`, { runId, path: selector.path, matches: matches.map((output) => output.filename ?? output.id) });
|
|
864
1124
|
}
|
|
865
|
-
throw new RunStateError(`
|
|
1125
|
+
throw new RunStateError(`Aex.downloadOutput: output path "${selector.path}" was not found`, {
|
|
866
1126
|
runId,
|
|
867
1127
|
path: selector.path
|
|
868
1128
|
});
|
|
869
1129
|
}
|
|
870
1130
|
if (typeof selector.id !== "string" || selector.id.length === 0) {
|
|
871
|
-
throw new RunStateError("
|
|
1131
|
+
throw new RunStateError("Aex.downloadOutput: selector must include an output id or path", { runId });
|
|
872
1132
|
}
|
|
873
1133
|
return { ...selector, id: selector.id };
|
|
874
1134
|
}
|
|
@@ -905,6 +1165,40 @@ function generateIdempotencyKey() {
|
|
|
905
1165
|
return cryptoObj.randomUUID();
|
|
906
1166
|
return `idem-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
907
1167
|
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Derive the message idempotency key from the session-create key. Mirrors the
|
|
1170
|
+
* CLI (`<createKey>:message`) so a retried `run` / `sessions.run` that reuses
|
|
1171
|
+
* one `idempotencyKey` de-duplicates BOTH the create and the billable turn.
|
|
1172
|
+
*/
|
|
1173
|
+
function deriveMessageKey(createKey) {
|
|
1174
|
+
return `${createKey}:message`;
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Extract a throttle-class {@link ProviderFault} from a failed session record.
|
|
1178
|
+
* Reads a structured `providerFault` / `error` field first (the shape the
|
|
1179
|
+
* runtime is expected to emit on a throttled turn), then falls back to a
|
|
1180
|
+
* heuristic scan of `errorMessage`. Returns `undefined` when the failure is not
|
|
1181
|
+
* a throttle.
|
|
1182
|
+
*/
|
|
1183
|
+
function throttleFromSession(session) {
|
|
1184
|
+
const fault = parseProviderFault(session.providerFault) ??
|
|
1185
|
+
parseProviderFault(session.error) ??
|
|
1186
|
+
faultFromErrorMessage(typeof session.errorMessage === "string" ? session.errorMessage : undefined);
|
|
1187
|
+
return fault && isThrottleFault(fault) ? fault : undefined;
|
|
1188
|
+
}
|
|
1189
|
+
/** Last-resort throttle detection from a free-text run error message. */
|
|
1190
|
+
function faultFromErrorMessage(message) {
|
|
1191
|
+
if (message === undefined || message.length === 0)
|
|
1192
|
+
return undefined;
|
|
1193
|
+
const lower = message.toLowerCase();
|
|
1194
|
+
if (/\b429\b|rate.?limit|too many requests/.test(lower)) {
|
|
1195
|
+
return { kind: "rate_limit", message };
|
|
1196
|
+
}
|
|
1197
|
+
if (/\b529\b|overloaded/.test(lower)) {
|
|
1198
|
+
return { kind: "overloaded", message };
|
|
1199
|
+
}
|
|
1200
|
+
return undefined;
|
|
1201
|
+
}
|
|
908
1202
|
function normaliseSessionInput(input, surface, field) {
|
|
909
1203
|
if (typeof input === "string") {
|
|
910
1204
|
if (!input) {
|
|
@@ -924,6 +1218,7 @@ function normaliseSessionInput(input, surface, field) {
|
|
|
924
1218
|
}
|
|
925
1219
|
function assertNoLegacySessionFields(options, surface) {
|
|
926
1220
|
const record = options;
|
|
1221
|
+
const removedProxyField = "proxy" + "Endpoints";
|
|
927
1222
|
const messages = {
|
|
928
1223
|
input: "send user messages with session.send(...) or use run({ message }).",
|
|
929
1224
|
prompt: "use message for one-shot run input or session.send(...) for follow-up messages.",
|
|
@@ -939,7 +1234,8 @@ function assertNoLegacySessionFields(options, surface) {
|
|
|
939
1234
|
limits: "use overrides.",
|
|
940
1235
|
timeout: "use overrides.timeout.",
|
|
941
1236
|
signal: "use session.cancel() / session.suspend() for remote control.",
|
|
942
|
-
postHook: "send a follow-up validation message when the session returns idle."
|
|
1237
|
+
postHook: "send a follow-up validation message when the session returns idle.",
|
|
1238
|
+
[removedProxyField]: "proxy endpoints are not part of the public SDK session API."
|
|
943
1239
|
};
|
|
944
1240
|
for (const [field, message] of Object.entries(messages)) {
|
|
945
1241
|
if (Object.prototype.hasOwnProperty.call(record, field)) {
|
|
@@ -1161,29 +1457,4 @@ function mergeMcpServers(inputs, explicitSecrets) {
|
|
|
1161
1457
|
mergedMcpSecrets: Array.from(secretByName.values())
|
|
1162
1458
|
};
|
|
1163
1459
|
}
|
|
1164
|
-
/**
|
|
1165
|
-
* Merge `ProxyEndpoint`-derived auth entries with any
|
|
1166
|
-
* `secrets.proxyEndpointAuth` the caller passed explicitly. Per-instance
|
|
1167
|
-
* auth values win on the same `name`; a type mismatch (e.g. instance
|
|
1168
|
-
* declares `bearer` but secrets carry `header` for the same name) is a
|
|
1169
|
-
* call-site error and we throw at the SDK boundary instead of letting
|
|
1170
|
-
* the BFF reject the submission an HTTP request later.
|
|
1171
|
-
*/
|
|
1172
|
-
function mergeProxyEndpointAuth(fromInstances, fromExplicitSecrets) {
|
|
1173
|
-
if (fromInstances.length === 0 && fromExplicitSecrets.length === 0)
|
|
1174
|
-
return [];
|
|
1175
|
-
const byName = new Map();
|
|
1176
|
-
for (const entry of fromExplicitSecrets) {
|
|
1177
|
-
byName.set(entry.name, entry);
|
|
1178
|
-
}
|
|
1179
|
-
for (const entry of fromInstances) {
|
|
1180
|
-
const existing = byName.get(entry.name);
|
|
1181
|
-
if (existing && existing.value.type !== entry.value.type) {
|
|
1182
|
-
throw new RunConfigValidationError(`aex: proxyEndpoint "${entry.name}" auth type conflicts ` +
|
|
1183
|
-
`with secrets.proxyEndpointAuth (instance=${entry.value.type}, secrets=${existing.value.type})`);
|
|
1184
|
-
}
|
|
1185
|
-
byName.set(entry.name, entry);
|
|
1186
|
-
}
|
|
1187
|
-
return Array.from(byName.values());
|
|
1188
|
-
}
|
|
1189
1460
|
//# sourceMappingURL=client.js.map
|