@zackbart/connecta 0.10.5 → 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 +110 -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 +39 -0
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/catalog-service.js +72 -11
- 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 +162 -9
- 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 +5 -1
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +171 -26
- 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/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/code-mode.md +119 -31
- package/documentation/meta-tools.md +79 -3
- 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 +135 -22
- package/src/errors.ts +102 -1
- package/src/execute.ts +238 -9
- package/src/index.ts +22 -0
- package/src/invocation.ts +59 -17
- package/src/meta-tools.ts +211 -30
- 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/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";
|
|
@@ -786,6 +788,10 @@ async function stashResult(
|
|
|
786
788
|
resultId: string;
|
|
787
789
|
totalBytes: number;
|
|
788
790
|
hint: string;
|
|
791
|
+
nextAction: {
|
|
792
|
+
tool: "get_result";
|
|
793
|
+
arguments: { id: string; offset: 0 };
|
|
794
|
+
};
|
|
789
795
|
}> {
|
|
790
796
|
const id = crypto.randomUUID();
|
|
791
797
|
await results.set(`result:${id}`, text, { ttlSeconds: RESULT_TTL_SECONDS });
|
|
@@ -794,16 +800,108 @@ async function stashResult(
|
|
|
794
800
|
resultId: id,
|
|
795
801
|
totalBytes,
|
|
796
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
|
+
},
|
|
797
807
|
};
|
|
798
808
|
}
|
|
799
809
|
|
|
800
|
-
|
|
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
|
+
*/
|
|
801
820
|
function batchSummaryString(value: string): string {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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
|
+
};
|
|
807
905
|
}
|
|
808
906
|
|
|
809
907
|
/**
|
|
@@ -817,16 +915,22 @@ async function guardEncoded(
|
|
|
817
915
|
bytes: Uint8Array,
|
|
818
916
|
results: KVStorage,
|
|
819
917
|
cap: number,
|
|
820
|
-
): Promise<ToolResult
|
|
918
|
+
): Promise<GuardedResult<ToolResult>> {
|
|
821
919
|
if (bytes.length <= cap) {
|
|
822
|
-
return {
|
|
920
|
+
return {
|
|
921
|
+
result: { content: [{ type: "text", text }] },
|
|
922
|
+
truncated: false,
|
|
923
|
+
};
|
|
823
924
|
}
|
|
824
925
|
const notice = await stashResult(text, results, bytes.length);
|
|
825
926
|
const head = dec.decode(
|
|
826
927
|
bytes.slice(0, alignEndToCharBoundary(bytes, 0, cap, bytes.length)),
|
|
827
928
|
);
|
|
828
929
|
return {
|
|
829
|
-
|
|
930
|
+
result: {
|
|
931
|
+
content: [{ type: "text", text: `${head}\n${JSON.stringify(notice)}` }],
|
|
932
|
+
},
|
|
933
|
+
truncated: true,
|
|
830
934
|
};
|
|
831
935
|
}
|
|
832
936
|
|
|
@@ -835,7 +939,7 @@ async function guardText(
|
|
|
835
939
|
text: string,
|
|
836
940
|
results: KVStorage,
|
|
837
941
|
cap: number,
|
|
838
|
-
): Promise<ToolResult
|
|
942
|
+
): Promise<GuardedResult<ToolResult>> {
|
|
839
943
|
// `JSON.stringify`'s type says `string` where its behavior says `string |
|
|
840
944
|
// undefined`, so TypeScript alone does not keep a non-string out of here.
|
|
841
945
|
// Normalizing at the door means the size check below always measures exactly
|
|
@@ -851,11 +955,14 @@ async function guardValue(
|
|
|
851
955
|
value: unknown,
|
|
852
956
|
results: KVStorage,
|
|
853
957
|
cap: number,
|
|
854
|
-
): Promise<unknown
|
|
958
|
+
): Promise<GuardedResult<unknown>> {
|
|
855
959
|
const text = serializeResultText(value);
|
|
856
960
|
const bytes = enc.encode(text);
|
|
857
|
-
if (bytes.length <= cap) return value;
|
|
858
|
-
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
|
+
};
|
|
859
966
|
}
|
|
860
967
|
|
|
861
968
|
/**
|
|
@@ -880,7 +987,7 @@ async function guardContent(
|
|
|
880
987
|
content: TextContent[],
|
|
881
988
|
results: KVStorage,
|
|
882
989
|
cap: number,
|
|
883
|
-
): Promise<ToolResult
|
|
990
|
+
): Promise<GuardedResult<ToolResult>> {
|
|
884
991
|
let text: string;
|
|
885
992
|
try {
|
|
886
993
|
text = JSON.stringify(content);
|
|
@@ -889,17 +996,22 @@ async function guardContent(
|
|
|
889
996
|
// be measured, stashed, or paged either — there is nothing this guard could
|
|
890
997
|
// do with it. Pass it through as the old text-only measure did, rather than
|
|
891
998
|
// turning a call that used to succeed into result_processing_failed.
|
|
892
|
-
return { content };
|
|
999
|
+
return { result: { content }, truncated: false };
|
|
893
1000
|
}
|
|
894
1001
|
const bytes = enc.encode(text);
|
|
895
1002
|
// Under the cap the downstream blocks pass through untouched, non-text ones
|
|
896
1003
|
// included, in their original order.
|
|
897
|
-
if (bytes.length <= cap)
|
|
1004
|
+
if (bytes.length <= cap) {
|
|
1005
|
+
return { result: { content }, truncated: false };
|
|
1006
|
+
}
|
|
898
1007
|
if (content.every((b) => b.type === "text")) {
|
|
899
1008
|
return guardEncoded(text, bytes, results, cap);
|
|
900
1009
|
}
|
|
901
1010
|
const notice = await stashResult(text, results, bytes.length);
|
|
902
|
-
return {
|
|
1011
|
+
return {
|
|
1012
|
+
result: { content: [{ type: "text", text: JSON.stringify(notice) }] },
|
|
1013
|
+
truncated: true,
|
|
1014
|
+
};
|
|
903
1015
|
}
|
|
904
1016
|
|
|
905
1017
|
// --- compact schema rendering (feature 3a) --------------------------------
|
|
@@ -938,6 +1050,10 @@ export interface CallArgs {
|
|
|
938
1050
|
/** Include connector/catalog/result-processing timing segments. */
|
|
939
1051
|
diagnostics?: boolean;
|
|
940
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
|
+
}
|
|
941
1057
|
export interface GetResultArgs {
|
|
942
1058
|
id: string;
|
|
943
1059
|
/**
|
|
@@ -964,6 +1080,21 @@ export interface SkillArgs {
|
|
|
964
1080
|
name?: string;
|
|
965
1081
|
}
|
|
966
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
|
+
|
|
967
1098
|
/**
|
|
968
1099
|
* Every base meta-tool handler over a registry — all nine, whichever surface is
|
|
969
1100
|
* advertised, since folding a tool away only skips its registration and never
|
|
@@ -1018,6 +1149,14 @@ export function createMetaTools(
|
|
|
1018
1149
|
requestScope,
|
|
1019
1150
|
probeTimeoutMs,
|
|
1020
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.
|
|
1021
1160
|
});
|
|
1022
1161
|
const invocation = new InvocationService(registry, catalog, opts.activity);
|
|
1023
1162
|
const withProbeDeadline = <T>(
|
|
@@ -1045,6 +1184,12 @@ export function createMetaTools(
|
|
|
1045
1184
|
interface ProcessedCallResult {
|
|
1046
1185
|
toolResult: ToolResult;
|
|
1047
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";
|
|
1048
1193
|
}
|
|
1049
1194
|
|
|
1050
1195
|
/** MCP adapter: shared invocation semantics plus MCP-only result shaping. */
|
|
@@ -1091,10 +1236,14 @@ export function createMetaTools(
|
|
|
1091
1236
|
resolved.definition.outputSchema,
|
|
1092
1237
|
)
|
|
1093
1238
|
: result;
|
|
1094
|
-
|
|
1239
|
+
const guarded = await guardValue(value, results, cap);
|
|
1240
|
+
value = guarded.result;
|
|
1095
1241
|
return {
|
|
1096
1242
|
toolResult: jsonResult({ ok: true, data: value }),
|
|
1097
1243
|
value,
|
|
1244
|
+
...(guarded.truncated
|
|
1245
|
+
? { friction: "result_too_large" as const }
|
|
1246
|
+
: {}),
|
|
1098
1247
|
};
|
|
1099
1248
|
}
|
|
1100
1249
|
if (resolved.connector.kind === "mcp") {
|
|
@@ -1107,7 +1256,13 @@ export function createMetaTools(
|
|
|
1107
1256
|
resolved.definition.outputSchema,
|
|
1108
1257
|
);
|
|
1109
1258
|
}
|
|
1110
|
-
|
|
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
|
+
};
|
|
1111
1266
|
}
|
|
1112
1267
|
const value = fields
|
|
1113
1268
|
? projectionValue(
|
|
@@ -1116,19 +1271,26 @@ export function createMetaTools(
|
|
|
1116
1271
|
resolved.definition.outputSchema,
|
|
1117
1272
|
)
|
|
1118
1273
|
: result;
|
|
1274
|
+
const guarded = await guardText(
|
|
1275
|
+
serializeResultText(value),
|
|
1276
|
+
results,
|
|
1277
|
+
cap,
|
|
1278
|
+
);
|
|
1119
1279
|
return {
|
|
1120
|
-
toolResult:
|
|
1121
|
-
serializeResultText(value),
|
|
1122
|
-
results,
|
|
1123
|
-
cap,
|
|
1124
|
-
),
|
|
1280
|
+
toolResult: guarded.result,
|
|
1125
1281
|
value,
|
|
1282
|
+
...(guarded.truncated
|
|
1283
|
+
? { friction: "result_too_large" as const }
|
|
1284
|
+
: {}),
|
|
1126
1285
|
};
|
|
1127
1286
|
},
|
|
1287
|
+
activityFriction: (processed) => processed.friction,
|
|
1128
1288
|
},
|
|
1129
1289
|
);
|
|
1130
1290
|
if (!outcome.ok) {
|
|
1291
|
+
const structuredRecovery = outcome.error.nextAction !== undefined;
|
|
1131
1292
|
const failedResult =
|
|
1293
|
+
structuredRecovery ||
|
|
1132
1294
|
outcome.error.code === "auth_required" ||
|
|
1133
1295
|
outcome.error.code === "invalid_args" ||
|
|
1134
1296
|
outcome.error.code === "input_required_unsupported" ||
|
|
@@ -1142,6 +1304,7 @@ export function createMetaTools(
|
|
|
1142
1304
|
})
|
|
1143
1305
|
: errorResult(outcome.error.message);
|
|
1144
1306
|
if (
|
|
1307
|
+
structuredRecovery ||
|
|
1145
1308
|
outcome.error.code === "auth_required" ||
|
|
1146
1309
|
outcome.error.code === "invalid_args" ||
|
|
1147
1310
|
outcome.error.code === "input_required_unsupported"
|
|
@@ -1348,7 +1511,9 @@ export function createMetaTools(
|
|
|
1348
1511
|
return (await runCall(args, "call_tool")).toolResult;
|
|
1349
1512
|
},
|
|
1350
1513
|
|
|
1351
|
-
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.
|
|
1352
1517
|
return (
|
|
1353
1518
|
await runCall(args, "call_destructive_tool", { allowDestructive: true })
|
|
1354
1519
|
).toolResult;
|
|
@@ -1537,7 +1702,7 @@ export function createMetaTools(
|
|
|
1537
1702
|
? { recovery: details.recovery }
|
|
1538
1703
|
: {}),
|
|
1539
1704
|
...(details.nextAction !== undefined
|
|
1540
|
-
? { nextAction: details.nextAction }
|
|
1705
|
+
? { nextAction: batchSummaryNextAction(details.nextAction) }
|
|
1541
1706
|
: {}),
|
|
1542
1707
|
...(details.retry !== undefined
|
|
1543
1708
|
? { retry: batchSummaryString(details.retry) }
|
|
@@ -1626,7 +1791,8 @@ export function createMetaTools(
|
|
|
1626
1791
|
? {
|
|
1627
1792
|
authorizationUrl: status.authorizationUrl,
|
|
1628
1793
|
instructions:
|
|
1629
|
-
"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),
|
|
1630
1796
|
}
|
|
1631
1797
|
: {}),
|
|
1632
1798
|
...(status.message ? { message: status.message } : {}),
|
|
@@ -1649,7 +1815,7 @@ const DESCRIBE_DESC = `Only when search_tools omitted schemas, a compact shape i
|
|
|
1649
1815
|
const CALL_DESC =
|
|
1650
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.';
|
|
1651
1817
|
const CALL_DESTRUCTIVE_DESC =
|
|
1652
|
-
"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.";
|
|
1653
1819
|
const GET_RESULT_DESC =
|
|
1654
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.";
|
|
1655
1821
|
const BATCH_DESC =
|
|
@@ -1864,14 +2030,29 @@ export function registerMetaTools(
|
|
|
1864
2030
|
"call_destructive_tool",
|
|
1865
2031
|
{
|
|
1866
2032
|
description: CALL_DESTRUCTIVE_DESC,
|
|
1867
|
-
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
|
+
}),
|
|
1868
2041
|
annotations: {
|
|
1869
2042
|
destructiveHint: true,
|
|
1870
2043
|
readOnlyHint: false,
|
|
1871
2044
|
openWorldHint: true,
|
|
1872
2045
|
},
|
|
1873
2046
|
},
|
|
1874
|
-
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
|
+
},
|
|
1875
2056
|
);
|
|
1876
2057
|
|
|
1877
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);
|