@zackbart/connecta 0.7.8 → 0.7.9
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/CHANGELOG.md +46 -0
- package/dist/activity.d.ts +1 -1
- package/dist/activity.d.ts.map +1 -1
- package/dist/activity.js.map +1 -1
- package/dist/call-admission.d.ts +81 -0
- package/dist/call-admission.d.ts.map +1 -0
- package/dist/call-admission.js +339 -0
- package/dist/call-admission.js.map +1 -0
- package/dist/connectors/api.d.ts +3 -1
- package/dist/connectors/api.d.ts.map +1 -1
- package/dist/connectors/api.js +1 -0
- package/dist/connectors/api.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts +3 -1
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +1 -0
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +45 -15
- package/dist/execute.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/meta-tools.d.ts +3 -0
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +127 -29
- package/dist/meta-tools.js.map +1 -1
- package/dist/registry.d.ts +25 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +28 -0
- package/dist/registry.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +5 -0
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +55 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +8 -4
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/activity.ts +5 -1
- package/src/call-admission.ts +519 -0
- package/src/connectors/api.ts +4 -0
- package/src/connectors/remote-mcp.ts +4 -0
- package/src/execute.ts +54 -22
- package/src/index.ts +5 -0
- package/src/meta-tools.ts +157 -45
- package/src/registry.ts +55 -0
- package/src/server.ts +5 -0
- package/src/types.ts +61 -0
- package/src/ui.ts +8 -4
- package/src/version.ts +1 -1
package/src/index.ts
CHANGED
|
@@ -648,6 +648,7 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
648
648
|
closePromise ??= Promise.resolve().then(async () => {
|
|
649
649
|
requestAdmission.close();
|
|
650
650
|
codeAdmission?.close();
|
|
651
|
+
registry.closeCallAdmission();
|
|
651
652
|
await config.executor?.close?.();
|
|
652
653
|
});
|
|
653
654
|
await closePromise;
|
|
@@ -698,6 +699,10 @@ export type {
|
|
|
698
699
|
export type { ApiOptions, ApiTool } from "./connectors/api.js";
|
|
699
700
|
export type {
|
|
700
701
|
Connector,
|
|
702
|
+
ConnectorCallAdmissionInput,
|
|
703
|
+
ConnectorCallAdmissionPolicy,
|
|
704
|
+
ConnectorCallAdmissionRule,
|
|
705
|
+
ConnectorRollingWindowBudget,
|
|
701
706
|
ConnectaBranding,
|
|
702
707
|
ConnectorCredentialAccess,
|
|
703
708
|
ConnectorCredentialConfig,
|
package/src/meta-tools.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
messageLooksRetryable,
|
|
18
18
|
type CallErrorDetails,
|
|
19
19
|
} from "./errors.js";
|
|
20
|
+
import { isCallAdmissionError } from "./call-admission.js";
|
|
20
21
|
import {
|
|
21
22
|
isValidMaxResultBytes,
|
|
22
23
|
MIN_MAX_RESULT_BYTES,
|
|
@@ -228,6 +229,24 @@ function errorDetails(code: string, message: string): ErrorDetails {
|
|
|
228
229
|
return { code, message, retryable: messageLooksRetryable(message) };
|
|
229
230
|
}
|
|
230
231
|
|
|
232
|
+
function callerCancelledDetails(): ErrorDetails {
|
|
233
|
+
return {
|
|
234
|
+
code: "cancelled",
|
|
235
|
+
message: "Tool call was cancelled by the caller.",
|
|
236
|
+
retryable: false,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function isCallerCancellation(
|
|
241
|
+
error: unknown,
|
|
242
|
+
signal: AbortSignal | undefined,
|
|
243
|
+
): boolean {
|
|
244
|
+
return (
|
|
245
|
+
signal?.aborted === true ||
|
|
246
|
+
(isCallAdmissionError(error) && error.admissionKind === "cancelled")
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
231
250
|
/** True if `b` is a UTF-8 continuation byte (0b10xxxxxx). */
|
|
232
251
|
function isContinuationByte(b: number): boolean {
|
|
233
252
|
return (b & 0xc0) === 0x80;
|
|
@@ -598,6 +617,8 @@ export function createMetaTools(
|
|
|
598
617
|
/** Per-connector deadline for the list/search/describe probe fan-out. Default 30_000. */
|
|
599
618
|
probeTimeoutMs?: number;
|
|
600
619
|
activity?: ActivityRequestContext;
|
|
620
|
+
/** Inbound request cancellation shared by direct and batch child calls. */
|
|
621
|
+
requestSignal?: AbortSignal;
|
|
601
622
|
/** Runtime continuation for the bounded tail of probe-owned teardown. */
|
|
602
623
|
defer?: DeferredWork;
|
|
603
624
|
} = {},
|
|
@@ -631,6 +652,7 @@ export function createMetaTools(
|
|
|
631
652
|
attempts: number;
|
|
632
653
|
timing: {
|
|
633
654
|
catalogMs: number;
|
|
655
|
+
admissionMs: number;
|
|
634
656
|
connectorMs: number;
|
|
635
657
|
backoffMs: number;
|
|
636
658
|
resultProcessingMs: number;
|
|
@@ -648,12 +670,14 @@ export function createMetaTools(
|
|
|
648
670
|
): Promise<RunCallOutcome> {
|
|
649
671
|
const started = Date.now();
|
|
650
672
|
let catalogMs = 0;
|
|
673
|
+
let admissionMs = 0;
|
|
651
674
|
let connectorMs = 0;
|
|
652
675
|
let backoffMs = 0;
|
|
653
676
|
let resultProcessingMs = 0;
|
|
654
677
|
let attempts = 0;
|
|
655
678
|
const timing = () => ({
|
|
656
679
|
catalogMs,
|
|
680
|
+
admissionMs,
|
|
657
681
|
connectorMs,
|
|
658
682
|
backoffMs,
|
|
659
683
|
resultProcessingMs,
|
|
@@ -661,7 +685,7 @@ export function createMetaTools(
|
|
|
661
685
|
});
|
|
662
686
|
const resolved = registry.resolveAddress(call.address);
|
|
663
687
|
const record = (
|
|
664
|
-
outcome: "success" | "error" | "timeout",
|
|
688
|
+
outcome: "success" | "error" | "timeout" | "cancelled",
|
|
665
689
|
errorCode?: string,
|
|
666
690
|
) => {
|
|
667
691
|
if (!resolved) return;
|
|
@@ -679,7 +703,14 @@ export function createMetaTools(
|
|
|
679
703
|
const failed = (error: ErrorDetails): RunCallOutcome => {
|
|
680
704
|
const durationMs = Date.now() - started;
|
|
681
705
|
const diagnostics = timing();
|
|
682
|
-
record(
|
|
706
|
+
record(
|
|
707
|
+
error.code === "timeout"
|
|
708
|
+
? "timeout"
|
|
709
|
+
: error.code === "cancelled"
|
|
710
|
+
? "cancelled"
|
|
711
|
+
: "error",
|
|
712
|
+
error.code,
|
|
713
|
+
);
|
|
683
714
|
return {
|
|
684
715
|
toolResult:
|
|
685
716
|
call.resultMode === "value"
|
|
@@ -729,6 +760,9 @@ export function createMetaTools(
|
|
|
729
760
|
).find((tool) => tool.name === resolved.toolName);
|
|
730
761
|
} catch (err) {
|
|
731
762
|
catalogMs += Date.now() - catalogStarted;
|
|
763
|
+
if (opts.requestSignal?.aborted) {
|
|
764
|
+
return failed(callerCancelledDetails());
|
|
765
|
+
}
|
|
732
766
|
// A connector whose catalog cannot be fetched is as unusable as one whose
|
|
733
767
|
// execution fails, so it feeds health accounting the same way the
|
|
734
768
|
// execution catch below does — otherwise a connector every call_tool
|
|
@@ -774,38 +808,76 @@ export function createMetaTools(
|
|
|
774
808
|
let result: unknown;
|
|
775
809
|
while (true) {
|
|
776
810
|
attempts++;
|
|
777
|
-
|
|
811
|
+
let permit: Awaited<ReturnType<RegistryView["admitCall"]>> | undefined;
|
|
812
|
+
const controller =
|
|
813
|
+
timeoutMs || opts.requestSignal ? new AbortController() : undefined;
|
|
814
|
+
const forwardAbort = () =>
|
|
815
|
+
controller?.abort(opts.requestSignal?.reason);
|
|
816
|
+
if (opts.requestSignal?.aborted) forwardAbort();
|
|
817
|
+
else {
|
|
818
|
+
opts.requestSignal?.addEventListener("abort", forwardAbort, {
|
|
819
|
+
once: true,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
778
822
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
requestScope,
|
|
783
|
-
{ signal: controller?.signal, timeoutMs },
|
|
784
|
-
);
|
|
785
|
-
const connectorStarted = Date.now();
|
|
823
|
+
let onAbort: (() => void) | undefined;
|
|
824
|
+
let attemptFailed = false;
|
|
825
|
+
let attemptError: unknown;
|
|
786
826
|
try {
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
827
|
+
const admissionStarted = Date.now();
|
|
828
|
+
try {
|
|
829
|
+
permit = await registry.admitCall(resolved.connector.id, {
|
|
830
|
+
toolName: resolved.toolName,
|
|
831
|
+
args: call.args ?? {},
|
|
832
|
+
signal: opts.requestSignal,
|
|
833
|
+
});
|
|
834
|
+
} finally {
|
|
835
|
+
admissionMs += Date.now() - admissionStarted;
|
|
836
|
+
}
|
|
837
|
+
const ctx = registry.contextFor(
|
|
838
|
+
resolved.connector.id,
|
|
839
|
+
baseUrl,
|
|
840
|
+
requestScope,
|
|
841
|
+
{ signal: controller?.signal, timeoutMs },
|
|
791
842
|
);
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
843
|
+
let rejectCancelled!: (reason: unknown) => void;
|
|
844
|
+
const cancelled = controller
|
|
845
|
+
? new Promise<never>((_, reject) => {
|
|
846
|
+
rejectCancelled = reject;
|
|
847
|
+
})
|
|
848
|
+
: undefined;
|
|
849
|
+
onAbort = () => {
|
|
850
|
+
rejectCancelled(
|
|
851
|
+
controller?.signal.reason ??
|
|
852
|
+
new ConnectorCallError("timeout", "Tool call was cancelled"),
|
|
853
|
+
);
|
|
854
|
+
};
|
|
855
|
+
controller?.signal.addEventListener("abort", onAbort, { once: true });
|
|
856
|
+
if (controller?.signal.aborted) onAbort();
|
|
857
|
+
if (controller?.signal.aborted) await cancelled;
|
|
858
|
+
if (timeoutMs) {
|
|
859
|
+
timer = setTimeout(() => {
|
|
860
|
+
controller?.abort(
|
|
861
|
+
new ConnectorCallError(
|
|
862
|
+
"timeout",
|
|
863
|
+
`Tool call timed out after ${timeoutMs}ms`,
|
|
864
|
+
),
|
|
865
|
+
);
|
|
866
|
+
}, timeoutMs);
|
|
867
|
+
}
|
|
868
|
+
const connectorStarted = Date.now();
|
|
869
|
+
try {
|
|
870
|
+
const pending = resolved.connector.callTool(
|
|
871
|
+
resolved.toolName,
|
|
872
|
+
call.args ?? {},
|
|
873
|
+
ctx,
|
|
874
|
+
);
|
|
875
|
+
result = cancelled
|
|
876
|
+
? await Promise.race([pending, cancelled])
|
|
877
|
+
: await pending;
|
|
878
|
+
} finally {
|
|
879
|
+
connectorMs += Date.now() - connectorStarted;
|
|
880
|
+
}
|
|
809
881
|
const mcpResult = result as {
|
|
810
882
|
content?: TextContent[];
|
|
811
883
|
isError?: boolean;
|
|
@@ -816,34 +888,72 @@ export function createMetaTools(
|
|
|
816
888
|
"Downstream tool call failed",
|
|
817
889
|
);
|
|
818
890
|
}
|
|
819
|
-
break;
|
|
820
891
|
} catch (err) {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
if (
|
|
892
|
+
attemptFailed = true;
|
|
893
|
+
attemptError = err;
|
|
894
|
+
} finally {
|
|
895
|
+
if (timer) clearTimeout(timer);
|
|
896
|
+
if (onAbort) {
|
|
897
|
+
controller?.signal.removeEventListener("abort", onAbort);
|
|
898
|
+
}
|
|
899
|
+
opts.requestSignal?.removeEventListener("abort", forwardAbort);
|
|
900
|
+
permit?.release();
|
|
901
|
+
}
|
|
902
|
+
if (attemptFailed) {
|
|
903
|
+
const callerCancelled = isCallerCancellation(
|
|
904
|
+
attemptError,
|
|
905
|
+
opts.requestSignal,
|
|
906
|
+
);
|
|
907
|
+
const details = callerCancelled
|
|
908
|
+
? callerCancelledDetails()
|
|
909
|
+
: classifyCallError(attemptError);
|
|
910
|
+
if (
|
|
911
|
+
!callerCancelled &&
|
|
912
|
+
attempts <= maxRetries &&
|
|
913
|
+
retrySafe &&
|
|
914
|
+
details.retryable
|
|
915
|
+
) {
|
|
825
916
|
const wait = retryBackoffMs(attempts, details.retryAfterMs);
|
|
826
917
|
if (wait !== undefined) {
|
|
827
918
|
const backoffStarted = Date.now();
|
|
828
919
|
if (wait > 0) {
|
|
829
|
-
await new Promise((resolve) =>
|
|
920
|
+
const completed = await new Promise<boolean>((resolve) => {
|
|
921
|
+
let settled = false;
|
|
922
|
+
const finish = (value: boolean) => {
|
|
923
|
+
if (settled) return;
|
|
924
|
+
settled = true;
|
|
925
|
+
clearTimeout(timer);
|
|
926
|
+
opts.requestSignal?.removeEventListener("abort", cancel);
|
|
927
|
+
resolve(value);
|
|
928
|
+
};
|
|
929
|
+
const timer = setTimeout(() => finish(true), wait);
|
|
930
|
+
const cancel = () => finish(false);
|
|
931
|
+
opts.requestSignal?.addEventListener("abort", cancel, {
|
|
932
|
+
once: true,
|
|
933
|
+
});
|
|
934
|
+
if (opts.requestSignal?.aborted) cancel();
|
|
935
|
+
});
|
|
936
|
+
backoffMs += Date.now() - backoffStarted;
|
|
937
|
+
if (!completed) return failed(callerCancelledDetails());
|
|
938
|
+
} else {
|
|
939
|
+
backoffMs += Date.now() - backoffStarted;
|
|
830
940
|
}
|
|
831
|
-
backoffMs += Date.now() - backoffStarted;
|
|
832
941
|
continue;
|
|
833
942
|
}
|
|
834
943
|
// The reported window is longer than the engine will park a
|
|
835
944
|
// synchronous request for. Fall through to failure with
|
|
836
945
|
// retryAfterMs reported verbatim so the agent can re-issue.
|
|
837
946
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
947
|
+
if (!callerCancelled && !isCallAdmissionError(attemptError)) {
|
|
948
|
+
registry.recordFailure(
|
|
949
|
+
resolved.connector.id,
|
|
950
|
+
Date.now() - started,
|
|
951
|
+
attemptError,
|
|
952
|
+
);
|
|
953
|
+
}
|
|
843
954
|
return failed(details);
|
|
844
|
-
} finally {
|
|
845
|
-
if (timer) clearTimeout(timer);
|
|
846
955
|
}
|
|
956
|
+
break;
|
|
847
957
|
}
|
|
848
958
|
|
|
849
959
|
registry.recordSuccess(resolved.connector.id, Date.now() - started);
|
|
@@ -1657,6 +1767,7 @@ export function registerMetaTools(
|
|
|
1657
1767
|
defaultToolTimeoutMs?: number;
|
|
1658
1768
|
probeTimeoutMs?: number;
|
|
1659
1769
|
activity?: ActivityRequestContext;
|
|
1770
|
+
requestSignal?: AbortSignal;
|
|
1660
1771
|
defer?: DeferredWork;
|
|
1661
1772
|
},
|
|
1662
1773
|
): void {
|
|
@@ -1664,6 +1775,7 @@ export function registerMetaTools(
|
|
|
1664
1775
|
defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
|
|
1665
1776
|
probeTimeoutMs: ctx.probeTimeoutMs,
|
|
1666
1777
|
activity: ctx.activity,
|
|
1778
|
+
requestSignal: ctx.requestSignal,
|
|
1667
1779
|
defer: ctx.defer,
|
|
1668
1780
|
});
|
|
1669
1781
|
|
package/src/registry.ts
CHANGED
|
@@ -18,6 +18,11 @@ import {
|
|
|
18
18
|
type CredentialHealthConfig,
|
|
19
19
|
type CredentialHealthRecord,
|
|
20
20
|
} from "./credential-health.js";
|
|
21
|
+
import {
|
|
22
|
+
ConnectorCallAdmissionController,
|
|
23
|
+
type CallAdmissionPermit,
|
|
24
|
+
type ConnectorCallAdmissionSnapshot,
|
|
25
|
+
} from "./call-admission.js";
|
|
21
26
|
import type { DeferredWork } from "./connector-scope.js";
|
|
22
27
|
import { splitAddress, type Toolkit } from "./toolkits.js";
|
|
23
28
|
|
|
@@ -212,6 +217,14 @@ export interface RegistryView {
|
|
|
212
217
|
requestScope?: object,
|
|
213
218
|
callOptions?: ConnectorOperationOptions,
|
|
214
219
|
): ConnectorContext;
|
|
220
|
+
/**
|
|
221
|
+
* Acquire the connector's shared downstream-call permit. Scoped views
|
|
222
|
+
* delegate to the base registry so every toolkit contends on the same pool.
|
|
223
|
+
*/
|
|
224
|
+
admitCall(
|
|
225
|
+
id: string,
|
|
226
|
+
input: { toolName: string; args: unknown; signal?: AbortSignal },
|
|
227
|
+
): Promise<CallAdmissionPermit>;
|
|
215
228
|
resultsStorage(): KVStorage;
|
|
216
229
|
recordSuccess(id: string, latencyMs: number): void;
|
|
217
230
|
recordFailure(id: string, latencyMs: number, error: unknown): void;
|
|
@@ -242,6 +255,10 @@ export interface RegistryView {
|
|
|
242
255
|
*/
|
|
243
256
|
export class Registry implements RegistryView {
|
|
244
257
|
private readonly connectors = new Map<string, Connector>();
|
|
258
|
+
private readonly callAdmission = new Map<
|
|
259
|
+
string,
|
|
260
|
+
ConnectorCallAdmissionController
|
|
261
|
+
>();
|
|
245
262
|
private readonly cache = new Map<string, CacheEntry>();
|
|
246
263
|
private readonly invalidated = new Set<string>();
|
|
247
264
|
/** Per-connector epoch preventing a pre-invalidation refresh from publishing. */
|
|
@@ -287,6 +304,12 @@ export class Registry implements RegistryView {
|
|
|
287
304
|
throw new Error(`Duplicate connector id "${c.id}"`);
|
|
288
305
|
}
|
|
289
306
|
this.connectors.set(c.id, c);
|
|
307
|
+
if (c.callAdmission) {
|
|
308
|
+
this.callAdmission.set(
|
|
309
|
+
c.id,
|
|
310
|
+
new ConnectorCallAdmissionController(c.id, c.callAdmission),
|
|
311
|
+
);
|
|
312
|
+
}
|
|
290
313
|
}
|
|
291
314
|
this.checkConventions(opts.logger);
|
|
292
315
|
this.checkResultCaps(
|
|
@@ -430,6 +453,30 @@ export class Registry implements RegistryView {
|
|
|
430
453
|
};
|
|
431
454
|
}
|
|
432
455
|
|
|
456
|
+
admitCall(
|
|
457
|
+
id: string,
|
|
458
|
+
input: { toolName: string; args: unknown; signal?: AbortSignal },
|
|
459
|
+
): Promise<CallAdmissionPermit> {
|
|
460
|
+
const admission = this.callAdmission.get(id);
|
|
461
|
+
if (admission) return admission.acquire(input);
|
|
462
|
+
return Promise.resolve({ waitMs: 0, release() {} });
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Payload-free aggregate state for the open health endpoint. */
|
|
466
|
+
callAdmissionSnapshot(): Record<string, ConnectorCallAdmissionSnapshot> {
|
|
467
|
+
return Object.fromEntries(
|
|
468
|
+
[...this.callAdmission].map(([id, admission]) => [
|
|
469
|
+
id,
|
|
470
|
+
admission.snapshot(),
|
|
471
|
+
]),
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Reject queued/future downstream admission; active permits release safely. */
|
|
476
|
+
closeCallAdmission(): void {
|
|
477
|
+
for (const admission of this.callAdmission.values()) admission.close();
|
|
478
|
+
}
|
|
479
|
+
|
|
433
480
|
/**
|
|
434
481
|
* Storage namespaced to the meta-tool result store (`results:` prefix), kept
|
|
435
482
|
* separate from any connector's `conn:<id>:` namespace. Backs get_result.
|
|
@@ -946,6 +993,14 @@ export class ScopedRegistry implements RegistryView {
|
|
|
946
993
|
return this.base.contextFor(id, baseUrl, requestScope, callOptions);
|
|
947
994
|
}
|
|
948
995
|
|
|
996
|
+
admitCall(
|
|
997
|
+
id: string,
|
|
998
|
+
input: { toolName: string; args: unknown; signal?: AbortSignal },
|
|
999
|
+
): Promise<CallAdmissionPermit> {
|
|
1000
|
+
if (!this.visible(id)) return Promise.reject(this.unknownConnector(id));
|
|
1001
|
+
return this.base.admitCall(id, input);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
949
1004
|
/**
|
|
950
1005
|
* Stashed oversized results are bound to the scope that produced them: a
|
|
951
1006
|
* scoped session cannot page a result it could not have produced, and an id
|
package/src/server.ts
CHANGED
|
@@ -1273,6 +1273,7 @@ async function serveMcp(
|
|
|
1273
1273
|
activity,
|
|
1274
1274
|
defaultToolTimeoutMs: opts.defaultToolTimeoutMs,
|
|
1275
1275
|
probeTimeoutMs: opts.probeTimeoutMs,
|
|
1276
|
+
requestSignal: request.signal,
|
|
1276
1277
|
...(runtimeContext
|
|
1277
1278
|
? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
|
|
1278
1279
|
: {}),
|
|
@@ -1577,6 +1578,10 @@ export function createFetchHandler(
|
|
|
1577
1578
|
code: opts.executor
|
|
1578
1579
|
? (codeAdmission ?? { managedByExecutor: true })
|
|
1579
1580
|
: null,
|
|
1581
|
+
downstreamCalls: {
|
|
1582
|
+
policy: "connector-partitioned-per-runtime",
|
|
1583
|
+
connectors: registry.callAdmissionSnapshot(),
|
|
1584
|
+
},
|
|
1580
1585
|
reservedRoutes: [
|
|
1581
1586
|
"/health",
|
|
1582
1587
|
"/",
|
package/src/types.ts
CHANGED
|
@@ -44,6 +44,61 @@ export interface ToolAnnotations extends Record<string, unknown> {
|
|
|
44
44
|
openWorldHint?: boolean;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/** Inputs a connector may reduce to a non-secret admission partition key. */
|
|
48
|
+
export interface ConnectorCallAdmissionInput {
|
|
49
|
+
toolName: string;
|
|
50
|
+
args: unknown;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Exact sliding-window budget for one connector-call partition. */
|
|
54
|
+
export interface ConnectorRollingWindowBudget {
|
|
55
|
+
kind: "rolling-window";
|
|
56
|
+
/** Calls admitted during `windowMs` before another is proactively refused. */
|
|
57
|
+
maxCalls: number;
|
|
58
|
+
/** Width of the rolling window in milliseconds. */
|
|
59
|
+
windowMs: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* One connector-level downstream call-admission rule.
|
|
64
|
+
*
|
|
65
|
+
* This release accepts the plural `rules` container below but enforces exactly
|
|
66
|
+
* one rule. That keeps the public shape ready for providers whose concurrency
|
|
67
|
+
* and budget limits eventually need different partition dimensions without
|
|
68
|
+
* pretending multi-rule admission is already atomic.
|
|
69
|
+
*/
|
|
70
|
+
export interface ConnectorCallAdmissionRule {
|
|
71
|
+
/** Maximum simultaneous Connector.callTool attempts in one partition. */
|
|
72
|
+
maxConcurrency?: number;
|
|
73
|
+
/** Callers allowed to wait behind the concurrency bound. Default 32. */
|
|
74
|
+
maxQueueSize?: number;
|
|
75
|
+
/** Maximum concurrency-queue wait in milliseconds. Default 5,000. */
|
|
76
|
+
queueTimeoutMs?: number;
|
|
77
|
+
/** Retry hint for concurrency overloads. Default 1,000. */
|
|
78
|
+
retryAfterMs?: number;
|
|
79
|
+
/** Optional exact rolling-window call-start budget. */
|
|
80
|
+
budget?: ConnectorRollingWindowBudget;
|
|
81
|
+
/**
|
|
82
|
+
* Derive a bounded, non-secret partition key from the tool call. Omit for
|
|
83
|
+
* one connector-wide partition. Connecta retains the returned key only; it
|
|
84
|
+
* never copies arguments into limiter state.
|
|
85
|
+
*/
|
|
86
|
+
partitionKey?(
|
|
87
|
+
input: Readonly<ConnectorCallAdmissionInput>,
|
|
88
|
+
): string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Optional downstream call-admission policy declared by one connector. */
|
|
92
|
+
export interface ConnectorCallAdmissionPolicy {
|
|
93
|
+
/**
|
|
94
|
+
* Plural-ready policy container. Exactly one rule is supported in this
|
|
95
|
+
* release; empty or multi-rule policies fail construction.
|
|
96
|
+
*/
|
|
97
|
+
rules: readonly ConnectorCallAdmissionRule[];
|
|
98
|
+
/** Maximum simultaneously retained partition states. Default 1,024. */
|
|
99
|
+
maxPartitions?: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
47
102
|
export type ConnectorCredentialValues = Record<string, string>;
|
|
48
103
|
|
|
49
104
|
/** Read-only access to the credentials assigned to one connector. */
|
|
@@ -145,6 +200,12 @@ export interface Connector {
|
|
|
145
200
|
* the connector inherits the deployment-wide cap.
|
|
146
201
|
*/
|
|
147
202
|
maxResultBytes?: number;
|
|
203
|
+
/**
|
|
204
|
+
* Optional per-runtime admission policy for downstream tool calls. It covers
|
|
205
|
+
* call_tool, every batch_call child, and execute_code host calls, but not
|
|
206
|
+
* catalog/status/auth operations.
|
|
207
|
+
*/
|
|
208
|
+
callAdmission?: ConnectorCallAdmissionPolicy;
|
|
148
209
|
/**
|
|
149
210
|
* Optional agent-facing usage guide (markdown) for this connector — preferred
|
|
150
211
|
* tools, address quirks, pagination conventions, rate-limit etiquette, good
|
package/src/ui.ts
CHANGED
|
@@ -1064,7 +1064,8 @@ ${clerkScript}
|
|
|
1064
1064
|
.activity-address { font-family: var(--mono); font-size: .78rem; overflow-wrap: anywhere; }
|
|
1065
1065
|
.activity-outcome { font-size: .9em; }
|
|
1066
1066
|
.activity-item.error .activity-outcome,
|
|
1067
|
-
.activity-item.timeout .activity-outcome
|
|
1067
|
+
.activity-item.timeout .activity-outcome,
|
|
1068
|
+
.activity-item.cancelled .activity-outcome { text-decoration: underline; }
|
|
1068
1069
|
.activity-empty { border-top: 1px solid var(--rule); padding: .75rem 0; }
|
|
1069
1070
|
.activity-more { margin-top: .75rem; }
|
|
1070
1071
|
.unavailable {
|
|
@@ -1498,9 +1499,12 @@ function renderActivity() {
|
|
|
1498
1499
|
}
|
|
1499
1500
|
for (const event of visible) {
|
|
1500
1501
|
const item = document.createElement("article");
|
|
1501
|
-
const outcomeClass = [
|
|
1502
|
-
|
|
1503
|
-
|
|
1502
|
+
const outcomeClass = [
|
|
1503
|
+
"success",
|
|
1504
|
+
"error",
|
|
1505
|
+
"timeout",
|
|
1506
|
+
"cancelled",
|
|
1507
|
+
].includes(event.outcome) ? event.outcome : "error";
|
|
1504
1508
|
item.className = "activity-item " + outcomeClass;
|
|
1505
1509
|
const retryCopy = event.attempts > 1
|
|
1506
1510
|
? " · " + esc(event.attempts) + " attempts"
|
package/src/version.ts
CHANGED