@zackbart/connecta 0.10.4 → 0.10.6
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 +135 -0
- package/dist/activity.d.ts +11 -1
- package/dist/activity.d.ts.map +1 -1
- package/dist/activity.js +44 -3
- package/dist/activity.js.map +1 -1
- package/dist/catalog-service.d.ts +40 -0
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/catalog-service.js +97 -15
- package/dist/catalog-service.js.map +1 -1
- package/dist/errors.d.ts +48 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +67 -0
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +72 -0
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +163 -10
- package/dist/execute.js.map +1 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/invocation.d.ts +9 -2
- package/dist/invocation.d.ts.map +1 -1
- package/dist/invocation.js +59 -29
- package/dist/invocation.js.map +1 -1
- package/dist/meta-tools.d.ts +12 -3
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +185 -30
- package/dist/meta-tools.js.map +1 -1
- package/dist/operator-ui/generated.d.ts +1 -1
- package/dist/operator-ui/generated.d.ts.map +1 -1
- package/dist/operator-ui/generated.js +1 -1
- package/dist/operator-ui/generated.js.map +1 -1
- package/dist/registry.d.ts +11 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +5 -2
- package/dist/registry.js.map +1 -1
- package/dist/routes/mcp.d.ts.map +1 -1
- package/dist/routes/mcp.js +9 -0
- package/dist/routes/mcp.js.map +1 -1
- package/dist/routes/shared.d.ts +4 -0
- package/dist/routes/shared.d.ts.map +1 -1
- package/dist/routes/shared.js.map +1 -1
- package/dist/skills.d.ts +1 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/code-mode.md +125 -34
- package/documentation/meta-tools.md +91 -9
- package/documentation/rich-output-design.md +212 -0
- package/ethos.md +17 -19
- package/examples/worker/README.md +11 -3
- package/examples/worker/src/d1-activity-row.ts +40 -0
- package/examples/worker/src/d1-activity.ts +3 -2
- package/package.json +1 -1
- package/src/activity.ts +64 -3
- package/src/catalog-service.ts +166 -26
- package/src/errors.ts +102 -1
- package/src/execute.ts +240 -10
- package/src/index.ts +22 -0
- package/src/invocation.ts +59 -17
- package/src/meta-tools.ts +239 -37
- package/src/operator-ui/browser.ts +10 -2
- package/src/operator-ui/generated.ts +1 -1
- package/src/registry.ts +5 -2
- package/src/routes/mcp.ts +9 -0
- package/src/routes/shared.ts +4 -0
- package/src/skills.ts +1 -1
- package/src/version.ts +1 -1
- package/templates/node/package.json +1 -1
package/src/meta-tools.ts
CHANGED
|
@@ -23,7 +23,9 @@ import {
|
|
|
23
23
|
type DeferredWork,
|
|
24
24
|
} from "./connector-scope.js";
|
|
25
25
|
import {
|
|
26
|
+
boundedEchoText,
|
|
26
27
|
classifyCallError,
|
|
28
|
+
echoedCallArgs,
|
|
27
29
|
messageLooksRetryable,
|
|
28
30
|
type CallErrorDetails,
|
|
29
31
|
} from "./errors.js";
|
|
@@ -280,6 +282,7 @@ interface ProjectionFeedback {
|
|
|
280
282
|
$connecta: {
|
|
281
283
|
type: "field_projection";
|
|
282
284
|
unmatchedFields: string[];
|
|
285
|
+
hint?: string;
|
|
283
286
|
schemaDeclared?: true;
|
|
284
287
|
schemaCoverage?: "complete" | "partial";
|
|
285
288
|
invalidFields?: string[];
|
|
@@ -649,7 +652,20 @@ function schemaProjectionFeedback(
|
|
|
649
652
|
const invalidFields = analysis.complete
|
|
650
653
|
? unmatchedFields.filter((field) => !available.has(field))
|
|
651
654
|
: [];
|
|
655
|
+
const missingArrayMarker = unmatchedFields.some((field) =>
|
|
656
|
+
analysis.paths.some(
|
|
657
|
+
(availableField) =>
|
|
658
|
+
availableField.includes("[]") &&
|
|
659
|
+
availableField.replaceAll("[]", "") === field,
|
|
660
|
+
),
|
|
661
|
+
);
|
|
652
662
|
return {
|
|
663
|
+
...(missingArrayMarker
|
|
664
|
+
? {
|
|
665
|
+
hint:
|
|
666
|
+
'Traverse arrays with [] after the array field name, for example "results[].id".',
|
|
667
|
+
}
|
|
668
|
+
: {}),
|
|
653
669
|
schemaDeclared: true,
|
|
654
670
|
schemaCoverage: analysis.complete ? "complete" : "partial",
|
|
655
671
|
...(invalidFields.length > 0 ? { invalidFields } : {}),
|
|
@@ -772,6 +788,10 @@ async function stashResult(
|
|
|
772
788
|
resultId: string;
|
|
773
789
|
totalBytes: number;
|
|
774
790
|
hint: string;
|
|
791
|
+
nextAction: {
|
|
792
|
+
tool: "get_result";
|
|
793
|
+
arguments: { id: string; offset: 0 };
|
|
794
|
+
};
|
|
775
795
|
}> {
|
|
776
796
|
const id = crypto.randomUUID();
|
|
777
797
|
await results.set(`result:${id}`, text, { ttlSeconds: RESULT_TTL_SECONDS });
|
|
@@ -780,16 +800,108 @@ async function stashResult(
|
|
|
780
800
|
resultId: id,
|
|
781
801
|
totalBytes,
|
|
782
802
|
hint: "use get_result {id, offset} to page, or re-call with fields to select less",
|
|
803
|
+
nextAction: {
|
|
804
|
+
tool: "get_result",
|
|
805
|
+
arguments: { id, offset: 0 },
|
|
806
|
+
},
|
|
783
807
|
};
|
|
784
808
|
}
|
|
785
809
|
|
|
786
|
-
|
|
810
|
+
interface GuardedResult<T> {
|
|
811
|
+
result: T;
|
|
812
|
+
truncated: boolean;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Keep an oversized batch's inline outcome summary at fixed string overhead.
|
|
817
|
+
* The same clamp the error envelopes use — one budget, one marker, defined
|
|
818
|
+
* once in `errors.ts` so the two cannot drift apart.
|
|
819
|
+
*/
|
|
787
820
|
function batchSummaryString(value: string): string {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
821
|
+
return boundedEchoText(value);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/** Candidate addresses kept in an oversized batch's summary of one ambiguity. */
|
|
825
|
+
const MAX_SUMMARY_ADDRESSES = 10;
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* A recovery route rebuilt field by field so the summary above keeps its
|
|
829
|
+
* promise. Spreading `nextAction` through raw would reopen the hole this
|
|
830
|
+
* function closes: every variant carries free-form strings, and one of them
|
|
831
|
+
* carries the caller's arguments, which is exactly the payload an oversized
|
|
832
|
+
* batch was already too large to hold.
|
|
833
|
+
*/
|
|
834
|
+
function batchSummaryNextAction(
|
|
835
|
+
nextAction: NonNullable<CallErrorDetails["nextAction"]>,
|
|
836
|
+
): NonNullable<CallErrorDetails["nextAction"]> {
|
|
837
|
+
if ("function" in nextAction) {
|
|
838
|
+
// A batch runs on the top-level catalog, whose search route is the tool, so
|
|
839
|
+
// the function-keyed discovery variant does not arrive here today. Rebuild
|
|
840
|
+
// it anyway: a guard that silently dropped an unrecognized route would turn
|
|
841
|
+
// a recovery record into nothing at exactly the moment one is needed.
|
|
842
|
+
if (nextAction.function === "connecta.search") {
|
|
843
|
+
return {
|
|
844
|
+
function: "connecta.search",
|
|
845
|
+
arguments: batchSummarySearchArgs(nextAction.arguments),
|
|
846
|
+
purpose: batchSummaryString(nextAction.purpose),
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
// Say so when the candidate list is clipped. The unclipped purpose reads
|
|
850
|
+
// "choose the intended canonical address", which is a lie about a list
|
|
851
|
+
// that no longer contains every candidate — and the caller has no other
|
|
852
|
+
// way to learn that the address it wants was the eleventh.
|
|
853
|
+
const candidates = nextAction.addresses.slice(0, MAX_SUMMARY_ADDRESSES);
|
|
854
|
+
return {
|
|
855
|
+
function: nextAction.function,
|
|
856
|
+
addresses: candidates.map(batchSummaryString),
|
|
857
|
+
purpose: batchSummaryString(
|
|
858
|
+
candidates.length < nextAction.addresses.length
|
|
859
|
+
? `${nextAction.purpose} Showing the first ${candidates.length} of ` +
|
|
860
|
+
`${nextAction.addresses.length} candidates; re-run the call on its ` +
|
|
861
|
+
"own to see them all."
|
|
862
|
+
: nextAction.purpose,
|
|
863
|
+
),
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
if (nextAction.tool === "authorize_connector") {
|
|
867
|
+
return {
|
|
868
|
+
tool: "authorize_connector",
|
|
869
|
+
arguments: {
|
|
870
|
+
connector: batchSummaryString(nextAction.arguments.connector),
|
|
871
|
+
},
|
|
872
|
+
operatorHandoff: batchSummaryString(nextAction.operatorHandoff),
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
if (nextAction.tool === "call_destructive_tool") {
|
|
876
|
+
return {
|
|
877
|
+
tool: "call_destructive_tool",
|
|
878
|
+
arguments: {
|
|
879
|
+
address: batchSummaryString(nextAction.arguments.address),
|
|
880
|
+
...echoedCallArgs(nextAction.arguments.args),
|
|
881
|
+
},
|
|
882
|
+
purpose: batchSummaryString(nextAction.purpose),
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
886
|
+
tool: "search_tools",
|
|
887
|
+
arguments: batchSummarySearchArgs(nextAction.arguments),
|
|
888
|
+
purpose: batchSummaryString(nextAction.purpose),
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/** The scoping arguments both discovery routes carry, bounded the same way. */
|
|
893
|
+
function batchSummarySearchArgs(args: {
|
|
894
|
+
query: string;
|
|
895
|
+
connector?: string;
|
|
896
|
+
includeSchemas: "compact";
|
|
897
|
+
}): { query: string; connector?: string; includeSchemas: "compact" } {
|
|
898
|
+
return {
|
|
899
|
+
query: batchSummaryString(args.query),
|
|
900
|
+
...(args.connector !== undefined
|
|
901
|
+
? { connector: batchSummaryString(args.connector) }
|
|
902
|
+
: {}),
|
|
903
|
+
includeSchemas: "compact",
|
|
904
|
+
};
|
|
793
905
|
}
|
|
794
906
|
|
|
795
907
|
/**
|
|
@@ -803,16 +915,22 @@ async function guardEncoded(
|
|
|
803
915
|
bytes: Uint8Array,
|
|
804
916
|
results: KVStorage,
|
|
805
917
|
cap: number,
|
|
806
|
-
): Promise<ToolResult
|
|
918
|
+
): Promise<GuardedResult<ToolResult>> {
|
|
807
919
|
if (bytes.length <= cap) {
|
|
808
|
-
return {
|
|
920
|
+
return {
|
|
921
|
+
result: { content: [{ type: "text", text }] },
|
|
922
|
+
truncated: false,
|
|
923
|
+
};
|
|
809
924
|
}
|
|
810
925
|
const notice = await stashResult(text, results, bytes.length);
|
|
811
926
|
const head = dec.decode(
|
|
812
927
|
bytes.slice(0, alignEndToCharBoundary(bytes, 0, cap, bytes.length)),
|
|
813
928
|
);
|
|
814
929
|
return {
|
|
815
|
-
|
|
930
|
+
result: {
|
|
931
|
+
content: [{ type: "text", text: `${head}\n${JSON.stringify(notice)}` }],
|
|
932
|
+
},
|
|
933
|
+
truncated: true,
|
|
816
934
|
};
|
|
817
935
|
}
|
|
818
936
|
|
|
@@ -821,7 +939,7 @@ async function guardText(
|
|
|
821
939
|
text: string,
|
|
822
940
|
results: KVStorage,
|
|
823
941
|
cap: number,
|
|
824
|
-
): Promise<ToolResult
|
|
942
|
+
): Promise<GuardedResult<ToolResult>> {
|
|
825
943
|
// `JSON.stringify`'s type says `string` where its behavior says `string |
|
|
826
944
|
// undefined`, so TypeScript alone does not keep a non-string out of here.
|
|
827
945
|
// Normalizing at the door means the size check below always measures exactly
|
|
@@ -837,11 +955,14 @@ async function guardValue(
|
|
|
837
955
|
value: unknown,
|
|
838
956
|
results: KVStorage,
|
|
839
957
|
cap: number,
|
|
840
|
-
): Promise<unknown
|
|
958
|
+
): Promise<GuardedResult<unknown>> {
|
|
841
959
|
const text = serializeResultText(value);
|
|
842
960
|
const bytes = enc.encode(text);
|
|
843
|
-
if (bytes.length <= cap) return value;
|
|
844
|
-
return
|
|
961
|
+
if (bytes.length <= cap) return { result: value, truncated: false };
|
|
962
|
+
return {
|
|
963
|
+
result: await stashResult(text, results, bytes.length),
|
|
964
|
+
truncated: true,
|
|
965
|
+
};
|
|
845
966
|
}
|
|
846
967
|
|
|
847
968
|
/**
|
|
@@ -866,7 +987,7 @@ async function guardContent(
|
|
|
866
987
|
content: TextContent[],
|
|
867
988
|
results: KVStorage,
|
|
868
989
|
cap: number,
|
|
869
|
-
): Promise<ToolResult
|
|
990
|
+
): Promise<GuardedResult<ToolResult>> {
|
|
870
991
|
let text: string;
|
|
871
992
|
try {
|
|
872
993
|
text = JSON.stringify(content);
|
|
@@ -875,17 +996,22 @@ async function guardContent(
|
|
|
875
996
|
// be measured, stashed, or paged either — there is nothing this guard could
|
|
876
997
|
// do with it. Pass it through as the old text-only measure did, rather than
|
|
877
998
|
// turning a call that used to succeed into result_processing_failed.
|
|
878
|
-
return { content };
|
|
999
|
+
return { result: { content }, truncated: false };
|
|
879
1000
|
}
|
|
880
1001
|
const bytes = enc.encode(text);
|
|
881
1002
|
// Under the cap the downstream blocks pass through untouched, non-text ones
|
|
882
1003
|
// included, in their original order.
|
|
883
|
-
if (bytes.length <= cap)
|
|
1004
|
+
if (bytes.length <= cap) {
|
|
1005
|
+
return { result: { content }, truncated: false };
|
|
1006
|
+
}
|
|
884
1007
|
if (content.every((b) => b.type === "text")) {
|
|
885
1008
|
return guardEncoded(text, bytes, results, cap);
|
|
886
1009
|
}
|
|
887
1010
|
const notice = await stashResult(text, results, bytes.length);
|
|
888
|
-
return {
|
|
1011
|
+
return {
|
|
1012
|
+
result: { content: [{ type: "text", text: JSON.stringify(notice) }] },
|
|
1013
|
+
truncated: true,
|
|
1014
|
+
};
|
|
889
1015
|
}
|
|
890
1016
|
|
|
891
1017
|
// --- compact schema rendering (feature 3a) --------------------------------
|
|
@@ -901,11 +1027,13 @@ export interface SearchArgs {
|
|
|
901
1027
|
fullDescriptions?: boolean;
|
|
902
1028
|
includeSchemas?: "compact" | "json";
|
|
903
1029
|
}
|
|
904
|
-
export
|
|
905
|
-
|
|
1030
|
+
export type DescribeArgs = (
|
|
1031
|
+
| { address: string; addresses?: never }
|
|
1032
|
+
| { address?: never; addresses: string[] }
|
|
1033
|
+
) & {
|
|
906
1034
|
format?: "compact" | "json";
|
|
907
1035
|
fullDescriptions?: boolean;
|
|
908
|
-
}
|
|
1036
|
+
};
|
|
909
1037
|
export interface ListArgs {
|
|
910
1038
|
/** When false, return cached/observed health without downstream I/O. */
|
|
911
1039
|
probe?: boolean;
|
|
@@ -922,6 +1050,10 @@ export interface CallArgs {
|
|
|
922
1050
|
/** Include connector/catalog/result-processing timing segments. */
|
|
923
1051
|
diagnostics?: boolean;
|
|
924
1052
|
}
|
|
1053
|
+
export interface DestructiveCallArgs extends CallArgs {
|
|
1054
|
+
/** Short model-authored context for the host's approval UI; never downstream input. */
|
|
1055
|
+
reason?: string;
|
|
1056
|
+
}
|
|
925
1057
|
export interface GetResultArgs {
|
|
926
1058
|
id: string;
|
|
927
1059
|
/**
|
|
@@ -948,6 +1080,21 @@ export interface SkillArgs {
|
|
|
948
1080
|
name?: string;
|
|
949
1081
|
}
|
|
950
1082
|
|
|
1083
|
+
/**
|
|
1084
|
+
* The sentence that closes the OAuth handoff, telling the operator's agent how
|
|
1085
|
+
* to confirm the flow landed. `authorize_connector` is registered on both
|
|
1086
|
+
* surfaces but `list_connectors` is not, so the classic status check cannot be
|
|
1087
|
+
* the only one offered: a code-first agent handed that advice gets an
|
|
1088
|
+
* unknown-tool error at exactly the moment it is trying to recover. It gets the
|
|
1089
|
+
* check its own surface serves instead — the same folded-name defect as the
|
|
1090
|
+
* describe path (#261), one tool result further along.
|
|
1091
|
+
*/
|
|
1092
|
+
function oauthFollowUp(surface: ConnectaSurface, connectorId: string): string {
|
|
1093
|
+
return surface === "code-first"
|
|
1094
|
+
? `Then retry the original call; connecta.search({ connector: ${JSON.stringify(connectorId)} }) inside execute_code confirms the catalog now loads.`
|
|
1095
|
+
: "Re-run list_connectors afterwards to confirm status is ok.";
|
|
1096
|
+
}
|
|
1097
|
+
|
|
951
1098
|
/**
|
|
952
1099
|
* Every base meta-tool handler over a registry — all nine, whichever surface is
|
|
953
1100
|
* advertised, since folding a tool away only skips its registration and never
|
|
@@ -1002,6 +1149,14 @@ export function createMetaTools(
|
|
|
1002
1149
|
requestScope,
|
|
1003
1150
|
probeTimeoutMs,
|
|
1004
1151
|
concurrency: discoveryConcurrency,
|
|
1152
|
+
// These handlers are the top-level tools, so the route is the advertised
|
|
1153
|
+
// one; in-program describes route through connecta.describe instead and
|
|
1154
|
+
// are built with their own CatalogService in execute.ts.
|
|
1155
|
+
describeRoute:
|
|
1156
|
+
surface === "code-first" ? "connecta.describe" : "describe_tools",
|
|
1157
|
+
// searchRoute keeps its default: unlike describe_tools, search_tools is
|
|
1158
|
+
// served by both advertised surfaces, so a top-level handler has nothing to
|
|
1159
|
+
// derive. Only an in-program caller needs to be sent to connecta.search.
|
|
1005
1160
|
});
|
|
1006
1161
|
const invocation = new InvocationService(registry, catalog, opts.activity);
|
|
1007
1162
|
const withProbeDeadline = <T>(
|
|
@@ -1029,6 +1184,12 @@ export function createMetaTools(
|
|
|
1029
1184
|
interface ProcessedCallResult {
|
|
1030
1185
|
toolResult: ToolResult;
|
|
1031
1186
|
value?: unknown;
|
|
1187
|
+
/**
|
|
1188
|
+
* Friction on a call that *succeeded*. It travels as a friction class, not
|
|
1189
|
+
* as an `errorCode`, so persistence keyed on "this row has an error code"
|
|
1190
|
+
* keeps counting failures rather than truncations.
|
|
1191
|
+
*/
|
|
1192
|
+
friction?: "result_too_large";
|
|
1032
1193
|
}
|
|
1033
1194
|
|
|
1034
1195
|
/** MCP adapter: shared invocation semantics plus MCP-only result shaping. */
|
|
@@ -1075,10 +1236,14 @@ export function createMetaTools(
|
|
|
1075
1236
|
resolved.definition.outputSchema,
|
|
1076
1237
|
)
|
|
1077
1238
|
: result;
|
|
1078
|
-
|
|
1239
|
+
const guarded = await guardValue(value, results, cap);
|
|
1240
|
+
value = guarded.result;
|
|
1079
1241
|
return {
|
|
1080
1242
|
toolResult: jsonResult({ ok: true, data: value }),
|
|
1081
1243
|
value,
|
|
1244
|
+
...(guarded.truncated
|
|
1245
|
+
? { friction: "result_too_large" as const }
|
|
1246
|
+
: {}),
|
|
1082
1247
|
};
|
|
1083
1248
|
}
|
|
1084
1249
|
if (resolved.connector.kind === "mcp") {
|
|
@@ -1091,7 +1256,13 @@ export function createMetaTools(
|
|
|
1091
1256
|
resolved.definition.outputSchema,
|
|
1092
1257
|
);
|
|
1093
1258
|
}
|
|
1094
|
-
|
|
1259
|
+
const guarded = await guardContent(content, results, cap);
|
|
1260
|
+
return {
|
|
1261
|
+
toolResult: guarded.result,
|
|
1262
|
+
...(guarded.truncated
|
|
1263
|
+
? { friction: "result_too_large" as const }
|
|
1264
|
+
: {}),
|
|
1265
|
+
};
|
|
1095
1266
|
}
|
|
1096
1267
|
const value = fields
|
|
1097
1268
|
? projectionValue(
|
|
@@ -1100,19 +1271,26 @@ export function createMetaTools(
|
|
|
1100
1271
|
resolved.definition.outputSchema,
|
|
1101
1272
|
)
|
|
1102
1273
|
: result;
|
|
1274
|
+
const guarded = await guardText(
|
|
1275
|
+
serializeResultText(value),
|
|
1276
|
+
results,
|
|
1277
|
+
cap,
|
|
1278
|
+
);
|
|
1103
1279
|
return {
|
|
1104
|
-
toolResult:
|
|
1105
|
-
serializeResultText(value),
|
|
1106
|
-
results,
|
|
1107
|
-
cap,
|
|
1108
|
-
),
|
|
1280
|
+
toolResult: guarded.result,
|
|
1109
1281
|
value,
|
|
1282
|
+
...(guarded.truncated
|
|
1283
|
+
? { friction: "result_too_large" as const }
|
|
1284
|
+
: {}),
|
|
1110
1285
|
};
|
|
1111
1286
|
},
|
|
1287
|
+
activityFriction: (processed) => processed.friction,
|
|
1112
1288
|
},
|
|
1113
1289
|
);
|
|
1114
1290
|
if (!outcome.ok) {
|
|
1291
|
+
const structuredRecovery = outcome.error.nextAction !== undefined;
|
|
1115
1292
|
const failedResult =
|
|
1293
|
+
structuredRecovery ||
|
|
1116
1294
|
outcome.error.code === "auth_required" ||
|
|
1117
1295
|
outcome.error.code === "invalid_args" ||
|
|
1118
1296
|
outcome.error.code === "input_required_unsupported" ||
|
|
@@ -1126,6 +1304,7 @@ export function createMetaTools(
|
|
|
1126
1304
|
})
|
|
1127
1305
|
: errorResult(outcome.error.message);
|
|
1128
1306
|
if (
|
|
1307
|
+
structuredRecovery ||
|
|
1129
1308
|
outcome.error.code === "auth_required" ||
|
|
1130
1309
|
outcome.error.code === "invalid_args" ||
|
|
1131
1310
|
outcome.error.code === "input_required_unsupported"
|
|
@@ -1311,7 +1490,12 @@ export function createMetaTools(
|
|
|
1311
1490
|
async searchTools(args: SearchArgs): Promise<ToolResult> {
|
|
1312
1491
|
return discoveryResult(
|
|
1313
1492
|
async () =>
|
|
1314
|
-
groupedSearchResult(
|
|
1493
|
+
groupedSearchResult(
|
|
1494
|
+
await catalog.search({
|
|
1495
|
+
...args,
|
|
1496
|
+
includeSchemaKeys: args.includeSchemas !== undefined,
|
|
1497
|
+
}),
|
|
1498
|
+
),
|
|
1315
1499
|
"Request a smaller limit, omit fullDescriptions, or use compact schemas.",
|
|
1316
1500
|
);
|
|
1317
1501
|
},
|
|
@@ -1327,7 +1511,9 @@ export function createMetaTools(
|
|
|
1327
1511
|
return (await runCall(args, "call_tool")).toolResult;
|
|
1328
1512
|
},
|
|
1329
1513
|
|
|
1330
|
-
async callDestructiveTool(args:
|
|
1514
|
+
async callDestructiveTool(args: DestructiveCallArgs): Promise<ToolResult> {
|
|
1515
|
+
// `reason` is read by the host's approval view and stops there — runCall
|
|
1516
|
+
// forwards only the call fields, so it never reaches the connector.
|
|
1331
1517
|
return (
|
|
1332
1518
|
await runCall(args, "call_destructive_tool", { allowDestructive: true })
|
|
1333
1519
|
).toolResult;
|
|
@@ -1516,7 +1702,7 @@ export function createMetaTools(
|
|
|
1516
1702
|
? { recovery: details.recovery }
|
|
1517
1703
|
: {}),
|
|
1518
1704
|
...(details.nextAction !== undefined
|
|
1519
|
-
? { nextAction: details.nextAction }
|
|
1705
|
+
? { nextAction: batchSummaryNextAction(details.nextAction) }
|
|
1520
1706
|
: {}),
|
|
1521
1707
|
...(details.retry !== undefined
|
|
1522
1708
|
? { retry: batchSummaryString(details.retry) }
|
|
@@ -1605,7 +1791,8 @@ export function createMetaTools(
|
|
|
1605
1791
|
? {
|
|
1606
1792
|
authorizationUrl: status.authorizationUrl,
|
|
1607
1793
|
instructions:
|
|
1608
|
-
"Have the operator open authorizationUrl in a browser and complete the consent flow. The provider then redirects back to this server's /oauth/callback/<connector> route, which finishes the flow automatically.
|
|
1794
|
+
"Have the operator open authorizationUrl in a browser and complete the consent flow. The provider then redirects back to this server's /oauth/callback/<connector> route, which finishes the flow automatically. " +
|
|
1795
|
+
oauthFollowUp(surface, connector.id),
|
|
1609
1796
|
}
|
|
1610
1797
|
: {}),
|
|
1611
1798
|
...(status.message ? { message: status.message } : {}),
|
|
@@ -1623,12 +1810,12 @@ export function createMetaTools(
|
|
|
1623
1810
|
|
|
1624
1811
|
const LIST_DESC =
|
|
1625
1812
|
"List connectors with status, cached tool count, and recent real-call health. Use probe=false for a fast inventory; use probe=true (default) only to diagnose live health or authorization.";
|
|
1626
|
-
const SEARCH_DESC = `Unknown address: use 2–4 distinctive action/object terms, not the full request; omit limit initially (default ${DEFAULT_SEARCH_LIMIT}) and page only if needed, up to ${MAX_SEARCH_LIMIT}. Partial and no-match searches report term coverage and next-step guidance. safety="readOnly" returns only calls available to call_tool and generated code; "approvalRequired" returns everything else; omitted or "all" preserves the complete catalog. This filters results, not authority. includeSchemas="compact" adds the input and any declared output shape, each bounded; inputSchemaTruncated/outputSchemaTruncated mark shapes that need exact retrieval; matches also carry declared annotations. Call directly when sufficient. Empty query browses all.`;
|
|
1813
|
+
const SEARCH_DESC = `Unknown address: use 2–4 distinctive action/object terms, not the full request; omit limit initially (default ${DEFAULT_SEARCH_LIMIT}) and page only if needed, up to ${MAX_SEARCH_LIMIT}. Partial and no-match searches report term coverage and next-step guidance. safety="readOnly" returns only calls available to call_tool and generated code; "approvalRequired" returns everything else; omitted or "all" preserves the complete catalog. This filters results, not authority. includeSchemas="compact" adds the input and any declared output shape, each bounded; plain-object schemas also expose inputKeys, requiredInputKeys, and outputKeys, while inputSchemaTruncated/outputSchemaTruncated mark shapes that need exact retrieval; matches also carry declared annotations. Call directly when sufficient. Empty query browses all.`;
|
|
1627
1814
|
const DESCRIBE_DESC = `Only when search_tools omitted schemas, a compact shape is ambiguous, or exact JSON constraints are needed. Inspects up to ${MAX_DESCRIBE_ADDRESSES} addresses with schemas and annotations; "compact" is default, while "json" preserves exact constraints.`;
|
|
1628
1815
|
const CALL_DESC =
|
|
1629
|
-
'Use for one tool explicitly annotated readOnlyHint: true. For 2–10 independent read-only calls use batch_call; for dependent steps or data reduction use execute_code when available. Unannotated, write-capable, and destructive tools are refused and require call_destructive_tool. fields selects JSON dot-paths;
|
|
1816
|
+
'Use for one tool explicitly annotated readOnlyHint: true. For 2–10 independent read-only calls use batch_call; for dependent steps or data reduction use execute_code when available. Unannotated, write-capable, and destructive tools are refused and require call_destructive_tool. fields selects JSON dot-paths; traverse arrays with [] (for example results[].id). Misses return data plus `$connecta` feedback. resultMode "value" unwraps results, timeoutMs sets a deadline, safe maxRetries are annotation-gated, diagnostics adds timing, and large results page through get_result.';
|
|
1630
1817
|
const CALL_DESTRUCTIVE_DESC =
|
|
1631
|
-
"Invoke any tool that is not explicitly annotated readOnlyHint: true, including unannotated, write-capable, or destructive tools. The MCP destructiveHint on this meta-tool lets the host request human approval before execution. Use only after reviewing the downstream tool schema and consequences.";
|
|
1818
|
+
"Invoke any tool that is not explicitly annotated readOnlyHint: true, including unannotated, write-capable, or destructive tools. Include a short reason explaining the intended consequence for the human reviewer; it grants no authority and is never passed downstream. The MCP destructiveHint on this meta-tool lets the host request human approval before execution. Use only after reviewing the downstream tool schema and consequences.";
|
|
1632
1819
|
const GET_RESULT_DESC =
|
|
1633
1820
|
"Page a truncated result stashed by call_tool/batch_call. Input { id, offset?, maxBytes? } → { text, offset, nextOffset?, totalBytes } sliced by byte offset. maxBytes is a whole number of bytes >= 1 (omit for the deployment default) and offset a whole number of bytes >= 0; an offset inside a multi-byte character is moved back to that character's first byte and the offset served is returned. Unknown/expired id is an error.";
|
|
1634
1821
|
const BATCH_DESC =
|
|
@@ -1651,7 +1838,7 @@ const SKILLS_DESC =
|
|
|
1651
1838
|
*/
|
|
1652
1839
|
const CODE_FIRST_SEARCH_DESC = `${SEARCH_DESC} Expand an ambiguous compact shape, or read exact JSON constraints, with connecta.describe inside execute_code.`;
|
|
1653
1840
|
const CODE_FIRST_CALL_DESC =
|
|
1654
|
-
'Use for ONE tool explicitly annotated readOnlyHint: true — the cheapest path for a single cold call. For two or more calls, dependent steps, loops, joins, or data reduction use execute_code, whose connecta.call and connecta.batch reach the same tools. Unannotated, write-capable, and destructive tools are refused and require call_destructive_tool. fields selects JSON dot-paths;
|
|
1841
|
+
'Use for ONE tool explicitly annotated readOnlyHint: true — the cheapest path for a single cold call. For two or more calls, dependent steps, loops, joins, or data reduction use execute_code, whose connecta.call and connecta.batch reach the same tools. Unannotated, write-capable, and destructive tools are refused and require call_destructive_tool. fields selects JSON dot-paths; traverse arrays with [] (for example results[].id). Misses return data plus `$connecta` feedback. resultMode "value" unwraps results, timeoutMs sets a deadline, safe maxRetries are annotation-gated, diagnostics adds timing, and large results page through get_result.';
|
|
1655
1842
|
const CODE_FIRST_GET_RESULT_DESC =
|
|
1656
1843
|
"Page a truncated result stashed by call_tool or call_destructive_tool; a program's oversized return is not paged, so reduce it in code instead. Input { id, offset?, maxBytes? } → { text, offset, nextOffset?, totalBytes } sliced by byte offset. maxBytes is a whole number of bytes >= 1 (omit for the deployment default) and offset a whole number of bytes >= 0; an offset inside a multi-byte character is moved back to that character's first byte and the offset served is returned. Unknown/expired id is an error.";
|
|
1657
1844
|
|
|
@@ -1843,14 +2030,29 @@ export function registerMetaTools(
|
|
|
1843
2030
|
"call_destructive_tool",
|
|
1844
2031
|
{
|
|
1845
2032
|
description: CALL_DESTRUCTIVE_DESC,
|
|
1846
|
-
inputSchema: z.object(
|
|
2033
|
+
inputSchema: z.object({
|
|
2034
|
+
...CALL_INPUT_SCHEMA,
|
|
2035
|
+
// Bounded above, but with no lower bound: a model that sends `""` or
|
|
2036
|
+
// whitespace has written no reason, and failing an entire consequential
|
|
2037
|
+
// call over a cosmetic field the host merely displays is the wrong
|
|
2038
|
+
// trade. It is normalized to absent below instead.
|
|
2039
|
+
reason: z.string().max(500).optional(),
|
|
2040
|
+
}),
|
|
1847
2041
|
annotations: {
|
|
1848
2042
|
destructiveHint: true,
|
|
1849
2043
|
readOnlyHint: false,
|
|
1850
2044
|
openWorldHint: true,
|
|
1851
2045
|
},
|
|
1852
2046
|
},
|
|
1853
|
-
async (args) =>
|
|
2047
|
+
async (args) => {
|
|
2048
|
+
// `reason` is the host's to display and connecta's to keep out of the
|
|
2049
|
+
// downstream call, so this destructuring is the whole of its handling:
|
|
2050
|
+
// nothing below reads it. Dropping it is also what makes an empty or
|
|
2051
|
+
// whitespace-only one "absent" rather than a validation failure — there
|
|
2052
|
+
// is no field left for it to be absent from.
|
|
2053
|
+
const { reason: _hostContext, ...call } = args as DestructiveCallArgs;
|
|
2054
|
+
return mt.callDestructiveTool(call);
|
|
2055
|
+
},
|
|
1854
2056
|
);
|
|
1855
2057
|
|
|
1856
2058
|
server.registerTool(
|
|
@@ -58,6 +58,7 @@ interface UiActivityEvent {
|
|
|
58
58
|
durationMs: number;
|
|
59
59
|
attempts: number;
|
|
60
60
|
errorCode?: string;
|
|
61
|
+
friction?: string;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
interface UiActivityResponse {
|
|
@@ -425,6 +426,7 @@ function renderActivity(): void {
|
|
|
425
426
|
event.source,
|
|
426
427
|
event.outcome,
|
|
427
428
|
event.errorCode,
|
|
429
|
+
event.friction,
|
|
428
430
|
actor.kind,
|
|
429
431
|
actor.id,
|
|
430
432
|
actor.namespace,
|
|
@@ -454,7 +456,13 @@ function renderActivity(): void {
|
|
|
454
456
|
const retryCopy = event.attempts > 1
|
|
455
457
|
? " · " + esc(event.attempts) + " attempts"
|
|
456
458
|
: "";
|
|
457
|
-
const
|
|
459
|
+
const frictionCopy = event.friction ? " · " + esc(event.friction) : "";
|
|
460
|
+
// The friction class and the code coincide for auth_required and
|
|
461
|
+
// result_too_large. Printing "· auth_required · auth_required" says nothing
|
|
462
|
+
// twice, so the coarse class stands in for both when they agree.
|
|
463
|
+
const errorCopy = event.errorCode && event.errorCode !== event.friction
|
|
464
|
+
? " · " + esc(event.errorCode)
|
|
465
|
+
: "";
|
|
458
466
|
const actorId = event.actor?.id
|
|
459
467
|
? (event.actor.namespace
|
|
460
468
|
? event.actor.namespace + " · " + event.actor.id
|
|
@@ -474,7 +482,7 @@ function renderActivity(): void {
|
|
|
474
482
|
"</div>" + stableActorId + "</div></div>" +
|
|
475
483
|
'<div><div class="activity-address">' + esc(event.address) +
|
|
476
484
|
'</div><div class="activity-detail">' + esc(event.source) + retryCopy +
|
|
477
|
-
errorCopy + '</div></div>' +
|
|
485
|
+
frictionCopy + errorCopy + '</div></div>' +
|
|
478
486
|
'<div><div class="activity-outcome">' + esc(event.outcome) +
|
|
479
487
|
'</div><div class="activity-detail">' + esc(event.durationMs) + ' ms</div></div>';
|
|
480
488
|
list.appendChild(item);
|