@zackbart/connecta 0.16.1 → 0.18.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +180 -0
  2. package/README.md +4 -0
  3. package/dist/catalog-service.d.ts +10 -0
  4. package/dist/catalog-service.js +77 -5
  5. package/dist/catalog.js +114 -12
  6. package/dist/errors.d.ts +4 -6
  7. package/dist/execute.d.ts +7 -0
  8. package/dist/execute.js +262 -168
  9. package/dist/invocation.js +3 -1
  10. package/dist/meta-tools.d.ts +4 -0
  11. package/dist/meta-tools.js +55 -23
  12. package/dist/operator-ui/generated.d.ts +1 -1
  13. package/dist/operator-ui/generated.js +1 -1
  14. package/dist/operator-ui/model.d.ts +3 -1
  15. package/dist/providers/mixpanel.d.ts +3 -5
  16. package/dist/providers/mixpanel.js +73 -5
  17. package/dist/providers/stripe.d.ts +25 -24
  18. package/dist/providers/stripe.js +64 -35
  19. package/dist/registry.d.ts +32 -9
  20. package/dist/registry.js +217 -33
  21. package/dist/routes/mcp.js +6 -0
  22. package/dist/routes/ui.js +1 -1
  23. package/dist/skills.d.ts +5 -1
  24. package/dist/skills.js +206 -30
  25. package/dist/types.d.ts +14 -2
  26. package/dist/ui.js +4 -1
  27. package/dist/version.d.ts +1 -1
  28. package/dist/version.js +1 -1
  29. package/documentation/architecture.md +8 -5
  30. package/documentation/code-mode.md +68 -68
  31. package/documentation/connector-guides.md +29 -27
  32. package/documentation/connectors.md +13 -1
  33. package/documentation/meta-tools.md +53 -19
  34. package/documentation/mixpanel.md +20 -0
  35. package/documentation/notion.md +17 -0
  36. package/documentation/operations.md +24 -21
  37. package/documentation/operator-ui.md +12 -2
  38. package/documentation/provider-audit.md +15 -7
  39. package/documentation/provider-conventions.md +26 -13
  40. package/documentation/stripe.md +66 -59
  41. package/documentation/upgrading.md +46 -4
  42. package/ethos.md +7 -7
  43. package/examples/worker/README.md +4 -3
  44. package/package.json +2 -2
  45. package/templates/node/README.md +7 -0
  46. package/templates/node/package.json +5 -2
package/dist/registry.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { closeConnectorScope, } from "./connector-scope.js";
1
2
  import { recordCatalogDriftActivity, } from "./activity.js";
2
3
  import { storedCredentialShape, } from "./credentials.js";
3
4
  import { ConnectorCallError } from "./errors.js";
@@ -6,6 +7,8 @@ import { boundedCatalogDrift } from "./catalog-drift.js";
6
7
  import { fingerprintSerializedCatalog, snapshotCatalog, } from "./catalog-fingerprint.js";
7
8
  import { MAX_CATALOG_CHUNK_BYTES, MAX_CATALOG_TOOLS, MAX_SERIALIZED_CATALOG_BYTES, } from "./catalog-limits.js";
8
9
  import { mapSettledWithConcurrency } from "./concurrency.js";
10
+ import { GUIDE_SUMMARY_LENGTH, normalizeGuideSummary, } from "./skills.js";
11
+ import { withAbortableTimeout } from "./timeout.js";
9
12
  const ID_RE = /^[a-z0-9_-]+$/;
10
13
  const DEFAULT_TTL_SECONDS = 300;
11
14
  const DEFAULT_STALE_SECONDS = 3600;
@@ -122,6 +125,10 @@ export class Registry {
122
125
  catalogMutations = new Map();
123
126
  /** Same-request cold loads share one promise without retaining the request. */
124
127
  requestCatalogLoads = new WeakMap();
128
+ /** One live refresh per connector across agent and operator requests. */
129
+ catalogRefreshes = new Map();
130
+ /** Last payload-free agent catalog access in this runtime. */
131
+ catalogAccess = new Map();
125
132
  /** Deployment-wide observations — every call, whatever view made it. */
126
133
  health = new HealthLog();
127
134
  /** Last drift counts reported to activity, per connector, in this runtime. */
@@ -146,6 +153,16 @@ export class Registry {
146
153
  if (this.connectors.has(c.id)) {
147
154
  throw new Error(`Duplicate connector id "${c.id}"`);
148
155
  }
156
+ const configuredGuideSummary = typeof c.usageGuide === "object"
157
+ ? normalizeGuideSummary(c.usageGuide.summary ?? "")
158
+ : undefined;
159
+ if (configuredGuideSummary !== undefined &&
160
+ configuredGuideSummary.length > GUIDE_SUMMARY_LENGTH) {
161
+ throw new Error(`Connector "${c.id}" usageGuide.summary is ` +
162
+ `${configuredGuideSummary.length} characters after whitespace ` +
163
+ `normalization; the discovery bound is ${GUIDE_SUMMARY_LENGTH}. ` +
164
+ "Shorten it or omit it to derive one.");
165
+ }
149
166
  this.connectors.set(c.id, c);
150
167
  if (c.callAdmission) {
151
168
  this.callAdmission.set(c.id, new ConnectorCallAdmissionController(c.id, c.callAdmission));
@@ -592,19 +609,20 @@ export class Registry {
592
609
  .catch(() => { });
593
610
  return next;
594
611
  }
595
- /** Force a live listTools refresh and replace both catalog cache layers. */
596
- async refreshTools(id, baseUrl, requestScope, callOptions = {}) {
597
- const connector = this.connectors.get(id);
598
- if (!connector)
599
- throw new Error(`Unknown connector "${id}"`);
600
- if (connector.staticTools)
601
- return connector.staticTools;
612
+ async refreshToolsWithContext(id, connector, ctx, skipPublicationWhenAborted = false) {
602
613
  const generation = this.catalogGeneration(id);
603
- const tools = await connector.listTools(this.contextFor(id, baseUrl, requestScope, callOptions));
614
+ const tools = await connector.listTools(ctx);
604
615
  // The listing a maintained proxy just served is also the only catalog
605
616
  // comparison connecta ever makes. It rides this refresh whether or not the
606
617
  // result reaches a cache, because what drifted drifted.
607
618
  this.observeCatalogDrift(connector);
619
+ // A deferred deadline may close the owned scope while a connector that
620
+ // ignores abort is still listing. The completed list remains a valid drift
621
+ // observation, but must not overwrite a newer same-generation refresh.
622
+ // Blocking callers do not set this flag and retain their prior publication
623
+ // behavior when an inbound abort races a completed listing.
624
+ if (skipPublicationWhenAborted && ctx.signal?.aborted)
625
+ return tools;
608
626
  // The caller that began this refresh may still use its result, but a
609
627
  // credential/OAuth change that landed while listTools was in flight means
610
628
  // the listing must not enter either shared cache layer.
@@ -618,8 +636,10 @@ export class Registry {
618
636
  }
619
637
  const previous = this.cache.get(id);
620
638
  const snapshot = await snapshotCatalog(tools);
621
- if (generation !== this.catalogGeneration(id))
639
+ if (generation !== this.catalogGeneration(id) ||
640
+ (skipPublicationWhenAborted && ctx.signal?.aborted)) {
622
641
  return tools;
642
+ }
623
643
  if (snapshot.serializedBytes.byteLength > MAX_SERIALIZED_CATALOG_BYTES) {
624
644
  const message = `Connector "${id}" returned a ${snapshot.serializedBytes.byteLength}-byte ` +
625
645
  `serialized catalog, over the ${MAX_SERIALIZED_CATALOG_BYTES}-byte ceiling; ` +
@@ -653,8 +673,106 @@ export class Registry {
653
673
  }
654
674
  return tools;
655
675
  }
676
+ /**
677
+ * Publish one shared refresh promise before starting its connector work.
678
+ * The first caller owns the scope and deadline; every later caller joins the
679
+ * result without gaining access to that context.
680
+ */
681
+ startCatalogRefresh(id, generation, start, finish) {
682
+ const existing = this.catalogRefreshes.get(id);
683
+ if (existing?.generation === generation)
684
+ return existing;
685
+ let resolveWork;
686
+ let rejectWork;
687
+ const work = new Promise((resolve, reject) => {
688
+ resolveWork = resolve;
689
+ rejectWork = reject;
690
+ });
691
+ let flight;
692
+ const promise = work
693
+ .finally(async () => {
694
+ if (finish)
695
+ await finish();
696
+ })
697
+ .finally(() => {
698
+ if (this.catalogRefreshes.get(id) === flight) {
699
+ this.catalogRefreshes.delete(id);
700
+ }
701
+ });
702
+ flight = { generation, promise };
703
+ this.catalogRefreshes.set(id, flight);
704
+ try {
705
+ Promise.resolve(start()).then(resolveWork, rejectWork);
706
+ }
707
+ catch (err) {
708
+ rejectWork(err);
709
+ }
710
+ return flight;
711
+ }
712
+ /** Force a live listTools refresh and replace both catalog cache layers. */
713
+ async refreshTools(id, baseUrl, requestScope, callOptions = {}) {
714
+ const connector = this.connectors.get(id);
715
+ if (!connector)
716
+ throw new Error(`Unknown connector "${id}"`);
717
+ if (connector.staticTools)
718
+ return connector.staticTools;
719
+ const ctx = this.contextFor(id, baseUrl, requestScope, callOptions);
720
+ return this.startCatalogRefresh(id, this.catalogGeneration(id), () => this.refreshToolsWithContext(id, connector, ctx)).promise;
721
+ }
722
+ observeCatalogAccess(id, state) {
723
+ this.catalogAccess.set(id, {
724
+ state,
725
+ observedAt: new Date().toISOString(),
726
+ });
727
+ }
728
+ /**
729
+ * Start or join one shared refresh. A newly deferred task owns its scope,
730
+ * deadline, and teardown; no inbound signal or request scope crosses into it.
731
+ */
732
+ deferCatalogRefresh(id, baseUrl, expectedGeneration, options) {
733
+ const connector = this.connectors.get(id);
734
+ if (!connector || connector.staticTools || !options.defer)
735
+ return;
736
+ let flight = this.catalogRefreshes.get(id);
737
+ if (flight?.generation !== expectedGeneration)
738
+ flight = undefined;
739
+ if (!flight) {
740
+ const requestScope = {};
741
+ let ctx;
742
+ flight = this.startCatalogRefresh(id, expectedGeneration, () => withAbortableTimeout((signal) => {
743
+ const current = this.cache.get(id);
744
+ if (current && current.exp > Date.now()) {
745
+ return Promise.resolve(current.tools);
746
+ }
747
+ if (expectedGeneration !== this.catalogGeneration(id) ||
748
+ this.invalidated.has(id)) {
749
+ return Promise.reject(new Error(`Deferred catalog refresh of "${id}" was invalidated before it started.`));
750
+ }
751
+ ctx = this.contextFor(id, baseUrl, requestScope, {
752
+ signal,
753
+ timeoutMs: options.refreshTimeoutMs,
754
+ });
755
+ return this.refreshToolsWithContext(id, connector, ctx, true);
756
+ }, options.refreshTimeoutMs, `deferred catalog refresh of "${id}"`), async () => {
757
+ if (ctx)
758
+ await closeConnectorScope(connector, ctx, options.defer);
759
+ });
760
+ }
761
+ if (!flight.deferredTail) {
762
+ // Attach the rejection handler before handing the tail to waitUntil.
763
+ flight.deferredTail = flight.promise.then(() => { }, (err) => {
764
+ this.opts.logger.warn(`[connecta] connector "${id}" deferred catalog refresh failed: ${msg(err)}`);
765
+ });
766
+ }
767
+ try {
768
+ options.defer(flight.deferredTail);
769
+ }
770
+ catch (err) {
771
+ this.opts.logger.warn(`[connecta] connector "${id}" deferred catalog refresh could not attach to the runtime: ${msg(err)}`);
772
+ }
773
+ }
656
774
  /** Cached listTools with in-memory + persisted serializable catalog layers. */
657
- async loadTools(id, baseUrl, requestScope, callOptions = {}) {
775
+ async loadTools(id, baseUrl, requestScope, callOptions = {}, readOptions) {
658
776
  const connector = this.connectors.get(id);
659
777
  if (!connector)
660
778
  throw new Error(`Unknown connector "${id}"`);
@@ -663,9 +781,14 @@ export class Registry {
663
781
  const now = Date.now();
664
782
  const requestGeneration = this.catalogGeneration(id);
665
783
  const hit = this.cache.get(id);
666
- if (hit && hit.exp > now)
784
+ if (hit && hit.exp > now) {
785
+ if (readOptions?.defer)
786
+ this.observeCatalogAccess(id, "fresh");
667
787
  return hit.tools;
668
- let stale = hit && hit.staleUntil > now ? hit.tools : undefined;
788
+ }
789
+ let stale = hit && hit.staleUntil > now
790
+ ? { tools: hit.tools, staleUntil: hit.staleUntil }
791
+ : undefined;
669
792
  if (this.persistToolCatalog && !this.invalidated.has(id)) {
670
793
  const generation = this.catalogGeneration(id);
671
794
  let persisted = null;
@@ -679,49 +802,95 @@ export class Registry {
679
802
  persisted = null;
680
803
  stale = undefined;
681
804
  }
682
- if (persisted && persisted.staleUntil > now) {
805
+ // Storage can yield while another request finishes a live refresh. Read
806
+ // the shared cache again before an older manifest gets any authority.
807
+ // The candidate with the later fresh deadline wins; both candidates have
808
+ // already passed their own fingerprint and completeness checks.
809
+ const reconciledAt = Date.now();
810
+ const current = this.cache.get(id);
811
+ const usableCurrent = current && current.staleUntil > reconciledAt ? current : undefined;
812
+ if (usableCurrent) {
813
+ stale = {
814
+ tools: usableCurrent.tools,
815
+ staleUntil: usableCurrent.staleUntil,
816
+ };
817
+ if (usableCurrent.exp > reconciledAt) {
818
+ if (readOptions?.defer)
819
+ this.observeCatalogAccess(id, "fresh");
820
+ return usableCurrent.tools;
821
+ }
822
+ }
823
+ if (persisted &&
824
+ persisted.staleUntil > reconciledAt &&
825
+ (!usableCurrent || persisted.expiresAt > usableCurrent.exp)) {
683
826
  this.cache.set(id, {
684
827
  tools: persisted.tools,
685
828
  fingerprint: persisted.fingerprint,
686
829
  exp: persisted.expiresAt,
687
830
  staleUntil: persisted.staleUntil,
688
831
  });
689
- if (persisted.expiresAt > now)
832
+ if (persisted.expiresAt > reconciledAt) {
833
+ if (readOptions?.defer)
834
+ this.observeCatalogAccess(id, "fresh");
690
835
  return persisted.tools;
691
- stale = persisted.tools;
836
+ }
837
+ stale = {
838
+ tools: persisted.tools,
839
+ staleUntil: persisted.staleUntil,
840
+ };
841
+ }
842
+ }
843
+ if (stale &&
844
+ stale.staleUntil > Date.now() &&
845
+ readOptions?.defer &&
846
+ requestGeneration === this.catalogGeneration(id) &&
847
+ !this.invalidated.has(id)) {
848
+ this.deferCatalogRefresh(id, baseUrl, requestGeneration, readOptions);
849
+ // Invalidation can land synchronously while the refresh is attached.
850
+ // Repeat the authority check at the exact stale publication point.
851
+ if (stale.staleUntil > Date.now() &&
852
+ requestGeneration === this.catalogGeneration(id) &&
853
+ !this.invalidated.has(id)) {
854
+ this.observeCatalogAccess(id, "stale");
855
+ return stale.tools;
692
856
  }
693
857
  }
694
858
  try {
695
- return await this.refreshTools(id, baseUrl, requestScope, callOptions);
859
+ const tools = await this.refreshTools(id, baseUrl, requestScope, callOptions);
860
+ if (readOptions?.defer)
861
+ this.observeCatalogAccess(id, "fresh");
862
+ return tools;
696
863
  }
697
864
  catch (err) {
698
865
  if (stale &&
866
+ stale.staleUntil > Date.now() &&
699
867
  requestGeneration === this.catalogGeneration(id) &&
700
868
  !this.invalidated.has(id)) {
869
+ if (readOptions?.defer)
870
+ this.observeCatalogAccess(id, "stale");
701
871
  this.opts.logger.warn(`[connecta] connector "${id}" catalog refresh failed; serving stale catalog: ${msg(err)}`);
702
- return stale;
872
+ return stale.tools;
703
873
  }
704
874
  throw err;
705
875
  }
706
876
  }
707
877
  /**
708
- * Coalesce one connector's cold load inside one inbound request. The WeakMap
709
- * neither roots the request scope nor lets its connector context escape into
710
- * another request; settled entries are also removed eagerly.
878
+ * Coalesce one connector's catalog traversal inside one inbound request. The
879
+ * WeakMap neither roots the request scope nor lets its connector context
880
+ * escape into another request; settled entries are also removed eagerly.
711
881
  *
712
- * The first caller's `callOptions` govern the shared load: a later caller's
713
- * signal or timeout neither cancels nor extends it, and an abort by the
714
- * first caller rejects every coalesced caller. Within one request that is
715
- * the deal being made — one fetch, one deadline.
882
+ * The deployment-wide flight below this layer coalesces the actual live
883
+ * refresh across requests. Its first caller's context and deadline govern;
884
+ * later callers join only its result, never its request scope.
716
885
  */
717
- async getTools(id, baseUrl, requestScope, callOptions = {}) {
886
+ async getTools(id, baseUrl, requestScope, callOptions = {}, readOptions) {
718
887
  const connector = this.connectors.get(id);
719
888
  if (!connector)
720
889
  throw new Error(`Unknown connector "${id}"`);
721
890
  if (connector.staticTools)
722
891
  return connector.staticTools;
723
892
  if (!requestScope) {
724
- return this.loadTools(id, baseUrl, requestScope, callOptions);
893
+ return this.loadTools(id, baseUrl, requestScope, callOptions, readOptions);
725
894
  }
726
895
  let loads = this.requestCatalogLoads.get(requestScope);
727
896
  if (!loads) {
@@ -731,7 +900,7 @@ export class Registry {
731
900
  const existing = loads.get(id);
732
901
  if (existing)
733
902
  return existing;
734
- const loading = this.loadTools(id, baseUrl, requestScope, callOptions);
903
+ const loading = this.loadTools(id, baseUrl, requestScope, callOptions, readOptions);
735
904
  loads.set(id, loading);
736
905
  try {
737
906
  return await loading;
@@ -796,24 +965,39 @@ export class Registry {
796
965
  // The report is rebuilt rather than spread through: what the seam returned
797
966
  // is third-party output, and status is read by the operator UI and copied
798
967
  // into responses.
799
- const withDrift = (status) => {
968
+ const withObservations = (status) => {
800
969
  const report = boundedCatalogDrift(connector.catalogDrift?.());
801
- return report ? { ...status, catalogDrift: report } : status;
970
+ const access = this.catalogAccess.get(id);
971
+ // Connector.status is an open plugin seam. Rebuild its public fields so
972
+ // a connector cannot smuggle payload through either registry-owned
973
+ // observation when this runtime has not made one.
974
+ const boundedStatus = {
975
+ state: status.state,
976
+ ...(status.authorizationUrl !== undefined
977
+ ? { authorizationUrl: status.authorizationUrl }
978
+ : {}),
979
+ ...(status.message !== undefined ? { message: status.message } : {}),
980
+ };
981
+ return {
982
+ ...boundedStatus,
983
+ ...(report ? { catalogDrift: report } : {}),
984
+ ...(access ? { catalogAccess: { ...access } } : {}),
985
+ };
802
986
  };
803
987
  if (connector.status) {
804
988
  try {
805
- return withDrift(await connector.status(ctx));
989
+ return withObservations(await connector.status(ctx));
806
990
  }
807
991
  catch (err) {
808
- return withDrift({ state: "error", message: msg(err) });
992
+ return withObservations({ state: "error", message: msg(err) });
809
993
  }
810
994
  }
811
995
  try {
812
996
  await this.getTools(id, baseUrl, requestScope, callOptions);
813
- return withDrift({ state: "ok" });
997
+ return withObservations({ state: "ok" });
814
998
  }
815
999
  catch (err) {
816
- return withDrift({ state: "error", message: msg(err) });
1000
+ return withObservations({ state: "error", message: msg(err) });
817
1001
  }
818
1002
  }
819
1003
  markCatalogInvalid(id) {
@@ -234,6 +234,9 @@ async function serveMcp(request, opts, baseUrl, actor, registry, runtimeContext)
234
234
  ? { discoveryConcurrency: opts.discoveryConcurrency }
235
235
  : {}),
236
236
  requestSignal: request.signal,
237
+ ...(runtimeContext?.waitUntil
238
+ ? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
239
+ : {}),
237
240
  });
238
241
  registerExecuteTool(server, registry, {
239
242
  baseUrl,
@@ -241,6 +244,9 @@ async function serveMcp(request, opts, baseUrl, actor, registry, runtimeContext)
241
244
  logger: opts.logger,
242
245
  ...(activity ? { activity } : {}),
243
246
  requestSignal: request.signal,
247
+ ...(runtimeContext?.waitUntil
248
+ ? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
249
+ : {}),
244
250
  ...(opts.discoveryConcurrency !== undefined
245
251
  ? { discoveryConcurrency: opts.discoveryConcurrency }
246
252
  : {}),
package/dist/routes/ui.js CHANGED
@@ -28,7 +28,7 @@ const INERT_ICON_HEADERS = {
28
28
  "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; sandbox",
29
29
  "X-Content-Type-Options": "nosniff",
30
30
  };
31
- /** Per-request base64 nonce for an operator shell's scripts (Node 20+ and Workers). */
31
+ /** Per-request base64 nonce for an operator shell's scripts (Node 22+ and Workers). */
32
32
  function uiScriptNonce() {
33
33
  const bytes = crypto.getRandomValues(new Uint8Array(16));
34
34
  let binary = "";
package/dist/skills.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Connector } from "./types.js";
2
- export declare const CONNECTA_INSTRUCTIONS = "Connecta exposes seven meta-tools. For one read at an unknown address, search_tools with 2\u20134 distinctive action/object terms and includeSchemas=\"compact\", then one call_tool \u2014 a lone cold call is cheaper direct than a program. For read-only reduction, multiple or dependent calls, loops, joins, or branches, do not call top-level search_tools: make one execute_code call whose program searches, selects, calls, and reduces; never return discovery for another call. connecta.ui(html) is a guest function inside execute_code, never a connector address or search_tools result; pass one HTML string for display-only, or bind named read-only refresh/drill-down calls in its optional reads argument, and return the same initial summary data the HTML renders. Unannotated, write-capable, or destructive tools stay top level: search_tools, then call_destructive_tool; authorize_connector follows auth_required; get_result follows truncation. If this routing is unfamiliar, fetch skills({ name: \"usage\" }).";
2
+ export declare const CONNECTA_INSTRUCTIONS = "Choose a route before discovery. For one read at an unknown address, use search_tools then call_tool; a known address needs only call_tool. For read-only reduction, multiple or dependent calls, loops, joins, or branches, use one execute_code program that discovers, calls, and returns the reduced answer. Only readOnlyHint: true tools run there. Keep unannotated, write-capable, or destructive work top level: search_tools then call_destructive_tool. After auth_required use authorize_connector. After a truncated direct result use fields or get_result. connecta.ui(html) exists only inside execute_code, not in connector search; return the same summary data the HTML renders. Fetch skills({ name: \"usage\" }) once for program syntax, selection, repair, examples, and runtime details.";
3
3
  /** Shared Connecta routing guidance, byte-identical across deployments. */
4
4
  export declare const USAGE_SKILL: string;
5
5
  /** The always-loaded MCP `instructions` string. */
@@ -10,6 +10,10 @@ export declare function hasConnectorGuides(connectors: readonly Connector[]): bo
10
10
  export declare function connectorSkillName(connectorId: string): string;
11
11
  /** The connector's guide, or undefined when it declares none (or a blank one). */
12
12
  export declare function connectorGuide(connector: Connector): string | undefined;
13
+ /** Discovery budget for one connector-guide summary, including an ellipsis. */
14
+ export declare const GUIDE_SUMMARY_LENGTH = 120;
15
+ /** Normalize authored and derived summaries under one construction contract. */
16
+ export declare function normalizeGuideSummary(summary: string): string | undefined;
13
17
  /** Bounded, decision-useful discovery summary for a connector guide. */
14
18
  export declare function connectorGuideSummary(connector: Connector): string | undefined;
15
19
  /** Whether correct use always depends on conventions outside the tool schema. */