@zackbart/connecta 0.24.4 → 0.25.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 (107) hide show
  1. package/AGENTS.md +40 -10
  2. package/CHANGELOG.md +253 -0
  3. package/dist/advertised-schema.js +53 -0
  4. package/dist/auth/downstream-oauth.js +490 -106
  5. package/dist/call-admission.js +139 -113
  6. package/dist/catalog-service.js +141 -105
  7. package/dist/catalog.js +258 -33
  8. package/dist/concurrency.js +24 -23
  9. package/dist/connector-scope.js +6 -37
  10. package/dist/connectors/api.d.ts +2 -2
  11. package/dist/connectors/remote-mcp.d.ts +6 -5
  12. package/dist/connectors/remote-mcp.js +317 -241
  13. package/dist/credential-contract.d.ts +9 -0
  14. package/dist/credential-rules.js +2 -2
  15. package/dist/credentials.d.ts +6 -1
  16. package/dist/credentials.js +45 -3
  17. package/dist/execute.js +363 -257
  18. package/dist/executor-admission.js +67 -50
  19. package/dist/executors/quickjs-runtime.js +4 -2
  20. package/dist/executors/quickjs.js +251 -210
  21. package/dist/fix-prompt.js +37 -0
  22. package/dist/index.d.ts +10 -1
  23. package/dist/index.js +51 -20
  24. package/dist/invocation.js +324 -285
  25. package/dist/meta-tools.js +206 -92
  26. package/dist/oauth-callback-outcome.js +94 -0
  27. package/dist/oauth-sealing.js +29 -0
  28. package/dist/operator-ui/fix-prompts.js +87 -0
  29. package/dist/operator-ui/generated.js +2 -2
  30. package/dist/operator-ui/model.d.ts +46 -1
  31. package/dist/operator-ui/setup-commands.js +53 -0
  32. package/dist/operator-ui/view.js +38 -2
  33. package/dist/program-source.js +205 -0
  34. package/dist/providers/notion.js +2 -2
  35. package/dist/providers/revenuecat.js +32 -5
  36. package/dist/providers/stripe.d.ts +3 -2
  37. package/dist/providers/vercel.js +1 -1
  38. package/dist/registry.d.ts +1 -1
  39. package/dist/registry.js +305 -260
  40. package/dist/routes/activity.js +120 -137
  41. package/dist/routes/credentials.js +174 -163
  42. package/dist/routes/mcp.js +129 -70
  43. package/dist/routes/oauth-management.js +60 -71
  44. package/dist/routes/oauth.js +44 -16
  45. package/dist/routes/operator.js +82 -0
  46. package/dist/routes/shared.d.ts +2 -0
  47. package/dist/routes/ui-data.js +210 -0
  48. package/dist/routes/ui.js +64 -32
  49. package/dist/runtime/admission.js +43 -0
  50. package/dist/runtime/call-admission.js +67 -0
  51. package/dist/runtime/connector-scope.js +72 -0
  52. package/dist/runtime/run.js +174 -0
  53. package/dist/runtime/services.js +130 -0
  54. package/dist/runtime/storage.js +66 -0
  55. package/dist/server.js +111 -89
  56. package/dist/skills.js +1 -1
  57. package/dist/storage/file.js +39 -2
  58. package/dist/storage/memory.js +28 -16
  59. package/dist/timeout.js +17 -47
  60. package/dist/types.d.ts +27 -7
  61. package/dist/ui.d.ts +16 -2
  62. package/dist/ui.js +50 -166
  63. package/dist/url-safety.js +157 -0
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.js +1 -1
  66. package/documentation/architecture.md +184 -10
  67. package/documentation/auth.md +80 -0
  68. package/documentation/code-mode.md +69 -24
  69. package/documentation/meta-tools.md +164 -27
  70. package/ethos.md +44 -47
  71. package/examples/worker/README.md +51 -5
  72. package/examples/worker/src/d1-storage.ts +116 -0
  73. package/examples/worker/src/index.ts +6 -6
  74. package/examples/worker/wrangler.jsonc +16 -4
  75. package/package.json +11 -3
  76. package/templates/node/package.json +1 -1
  77. package/dist/auth/downstream-oauth.d.ts +0 -200
  78. package/dist/catalog-fingerprint.d.ts +0 -19
  79. package/dist/catalog-limits.d.ts +0 -9
  80. package/dist/catalog-service.d.ts +0 -263
  81. package/dist/catalog.d.ts +0 -87
  82. package/dist/concurrency.d.ts +0 -3
  83. package/dist/connectors/guarded-fetch.d.ts +0 -96
  84. package/dist/credential-rules.d.ts +0 -85
  85. package/dist/execute.d.ts +0 -154
  86. package/dist/executor-result.d.ts +0 -10
  87. package/dist/executors/quickjs-child.d.ts +0 -1
  88. package/dist/executors/quickjs-protocol.d.ts +0 -67
  89. package/dist/favicon.d.ts +0 -1
  90. package/dist/identity.d.ts +0 -4
  91. package/dist/invocation.d.ts +0 -74
  92. package/dist/mcp-result.d.ts +0 -8
  93. package/dist/meta-tools.d.ts +0 -129
  94. package/dist/operator-ui/generated.d.ts +0 -2
  95. package/dist/operator-ui/view.d.ts +0 -188
  96. package/dist/result-shapes.d.ts +0 -13
  97. package/dist/retired-toolkits.d.ts +0 -2
  98. package/dist/routes/activity.d.ts +0 -2
  99. package/dist/routes/credentials.d.ts +0 -2
  100. package/dist/routes/mcp.d.ts +0 -10
  101. package/dist/routes/oauth-management.d.ts +0 -2
  102. package/dist/routes/oauth.d.ts +0 -2
  103. package/dist/routes/ui.d.ts +0 -2
  104. package/dist/server.d.ts +0 -9
  105. package/dist/skills.d.ts +0 -44
  106. package/dist/timeout.d.ts +0 -21
  107. package/dist/tool-safety.d.ts +0 -9
@@ -1,8 +1,10 @@
1
- import { compactDiscoverySchema, compactDescriptionSchema, lexicalCorpusStatistics, lexicalQueryTerms, lexicalSearchQuery, matchesLexicalTerm, rankTools, schemaObjectKeys, summarizeDiscoveryDescription, summarizeDescription, } from "./catalog.js";
2
- import { mapSettledWithConcurrency, resolveDiscoveryConcurrency, } from "./concurrency.js";
1
+ import { Deferred, Effect, Exit, Result } from "effect";
2
+ import { compactDiscoverySchema, compactDescriptionSchema, lexicalCorpusStatistics, lexicalQueryTerms, lexicalSearchQuery, matchesLexicalTerm, rankTools, schemaObjectKeys, summarizeDiscoveryDescription, summarizeDescription, typescriptSignature, } from "./catalog.js";
3
+ import { resolveDiscoveryConcurrency } from "./concurrency.js";
3
4
  import { boundedEchoText, classifyCallError, framingError, } from "./errors.js";
4
5
  import { connectorGuide, connectorGuideRequired, connectorGuideSummary, connectorSkillName, } from "./skills.js";
5
- import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs, withDeadline, } from "./timeout.js";
6
+ import { runEdge, withDeadlineEffect } from "./runtime/run.js";
7
+ import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs, } from "./timeout.js";
6
8
  import { isExplicitlyReadOnly } from "./tool-safety.js";
7
9
  export const DEFAULT_SEARCH_LIMIT = 8;
8
10
  export const MAX_SEARCH_LIMIT = 100;
@@ -199,8 +201,20 @@ export class CatalogService {
199
201
  concurrency;
200
202
  searchRoute;
201
203
  readOptions;
202
- loaded = new Map();
203
- loading = new Map();
204
+ requestSignal;
205
+ // The request-scoped catalog cache: one Deferred per connector asked about,
206
+ // completed by the first asker's registry read and joined by every later
207
+ // one. A success stays for the rest of the request, so a search and the call
208
+ // after it see one catalog; a failure is dropped so the next ask reads
209
+ // again. The service lives and dies with one request, so no Deferred here is
210
+ // ever awaited from another.
211
+ //
212
+ // Registry.getTools also joins concurrent reads in one request scope, which
213
+ // the operator UI and status reads rely on. This map overlaps it only for
214
+ // reads still in flight and is kept for what the registry's cannot do: hold
215
+ // the success for the rest of the request, recorded by the read itself
216
+ // rather than by an asker, whose probe deadline may have ended its wait.
217
+ catalogs = new Map();
204
218
  constructor(registry, baseUrl, options = {}) {
205
219
  this.registry = registry;
206
220
  this.baseUrl = baseUrl;
@@ -209,6 +223,7 @@ export class CatalogService {
209
223
  normalizeTimeoutMs(options.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
210
224
  this.concurrency = resolveDiscoveryConcurrency(options.concurrency);
211
225
  this.searchRoute = options.searchRoute ?? "search_tools";
226
+ this.requestSignal = options.requestSignal;
212
227
  this.readOptions = options.defer
213
228
  ? {
214
229
  defer: options.defer,
@@ -237,34 +252,45 @@ export class CatalogService {
237
252
  ? { function: "connecta.search", arguments: searchArgs, purpose }
238
253
  : { tool: "search_tools", arguments: searchArgs, purpose };
239
254
  }
240
- async loadConnector(id, callOptions = {}) {
241
- const cached = this.loaded.get(id);
242
- if (cached)
243
- return cached;
244
- const inFlight = this.loading.get(id);
245
- if (inFlight)
246
- return inFlight;
247
- const loading = this.registry
248
- .getTools(id, this.baseUrl, this.requestScope, callOptions, this.readOptions)
249
- .then((tools) => {
250
- this.loaded.set(id, tools);
251
- return tools;
252
- })
253
- .finally(() => {
254
- if (this.loading.get(id) === loading)
255
- this.loading.delete(id);
255
+ loadConnector(id, callOptions = {}) {
256
+ return runEdge(this.catalog(id, callOptions));
257
+ }
258
+ // One connector's catalog from the request-scoped cache, reading it through
259
+ // the registry when this request has not yet. The first asker's options
260
+ // govern the read and a joiner shares its outcome, as the registry's own
261
+ // flight below does. The read settles the Deferred itself, not the asker's
262
+ // fiber, so a probe deadline ends one asker's wait and never the read.
263
+ catalog(id, callOptions) {
264
+ return Effect.suspend(() => {
265
+ const cached = this.catalogs.get(id);
266
+ if (cached)
267
+ return Deferred.await(cached);
268
+ const read = Deferred.makeUnsafe();
269
+ this.catalogs.set(id, read);
270
+ // The executor turns a synchronous throw from getTools into a rejection.
271
+ new Promise((resolve) => resolve(this.registry.getTools(id, this.baseUrl, this.requestScope, callOptions, this.readOptions))).then((tools) => {
272
+ Deferred.doneUnsafe(read, Exit.succeed(tools));
273
+ }, (cause) => {
274
+ // Evicted before the Deferred resumes anyone, so an asker that
275
+ // retries on hearing of the failure starts a fresh read.
276
+ if (this.catalogs.get(id) === read)
277
+ this.catalogs.delete(id);
278
+ Deferred.doneUnsafe(read, Exit.fail(cause));
279
+ });
280
+ return Deferred.await(read);
256
281
  });
257
- this.loading.set(id, loading);
258
- return loading;
259
282
  }
260
- loadForDiscovery(id, label) {
261
- return withDeadline((signal) => this.loadConnector(id, {
262
- signal,
263
- timeoutMs: this.probeTimeoutMs,
264
- }), {
283
+ // Every named catalog under its own probe deadline, `concurrency` at a time,
284
+ // each settling in its input slot: an unavailable catalog is a Failure
285
+ // there, not a failed discovery. `route` names the surface in a timeout.
286
+ // A probe also ends with its request, so no listing outlives the caller
287
+ // that asked for it.
288
+ discoveryCatalogs(ids, route) {
289
+ return Effect.forEach(ids, (id) => Effect.result(withDeadlineEffect((signal) => this.catalog(id, { signal, timeoutMs: this.probeTimeoutMs }), {
290
+ ...(this.requestSignal ? { signal: this.requestSignal } : {}),
265
291
  timeoutMs: this.probeTimeoutMs,
266
- timeoutError: new Error(`${label} timed out after ${this.probeTimeoutMs}ms`),
267
- });
292
+ timeoutError: new Error(`${route} probe of "${id}" timed out after ${this.probeTimeoutMs}ms`),
293
+ })), { concurrency: this.concurrency });
268
294
  }
269
295
  outputSchema(connectorId, tool) {
270
296
  if (tool.outputSchema)
@@ -306,32 +332,35 @@ export class CatalogService {
306
332
  toolName,
307
333
  };
308
334
  }
309
- async resolveTool(address, callOptions = {}) {
310
- const resolved = this.registry.resolveAddress(address);
311
- if (!resolved) {
312
- return this.unknownAddressFailure(address, address);
313
- }
314
- const started = Date.now();
315
- let tools;
316
- try {
317
- tools = await this.loadConnector(resolved.connector.id, callOptions);
318
- }
319
- catch (cause) {
320
- return this.catalogLoadFailure(cause, started, resolved.connector, resolved.toolName);
321
- }
322
- const definition = tools.find((tool) => tool.name === resolved.toolName);
323
- if (!definition) {
324
- return this.unknownToolFailure(resolved.toolName, resolved.connector, started);
325
- }
326
- return {
327
- ok: true,
328
- resolved: {
329
- connector: resolved.connector,
330
- toolName: resolved.toolName,
331
- definition,
332
- },
333
- catalogMs: Date.now() - started,
334
- };
335
+ resolveTool(address, callOptions = {}) {
336
+ return runEdge(this.resolve(address, callOptions));
337
+ }
338
+ // resolveTool without the Promise, for InvocationService's pipeline, which
339
+ // yields it inside the call's deadline rather than crossing an edge. Never
340
+ // fails: an unloadable catalog is a resolution like any other refusal.
341
+ resolve(address, callOptions = {}) {
342
+ return Effect.suspend(() => {
343
+ const resolved = this.registry.resolveAddress(address);
344
+ if (!resolved) {
345
+ return Effect.succeed(this.unknownAddressFailure(address, address));
346
+ }
347
+ const { connector, toolName } = resolved;
348
+ const started = Date.now();
349
+ return Effect.match(this.catalog(connector.id, callOptions), {
350
+ onFailure: (cause) => this.catalogLoadFailure(cause, started, connector, toolName),
351
+ onSuccess: (tools) => {
352
+ const definition = tools.find((tool) => tool.name === toolName);
353
+ if (!definition) {
354
+ return this.unknownToolFailure(toolName, connector, started);
355
+ }
356
+ return {
357
+ ok: true,
358
+ resolved: { connector, toolName, definition },
359
+ catalogMs: Date.now() - started,
360
+ };
361
+ },
362
+ });
363
+ });
335
364
  }
336
365
  async search(args) {
337
366
  if (args.query !== undefined && typeof args.query !== "string") {
@@ -361,21 +390,14 @@ export class CatalogService {
361
390
  ? [scopedConnector]
362
391
  : []
363
392
  : this.registry.listConnectors();
364
- const catalogs = await mapSettledWithConcurrency(connectors, this.concurrency, (connector) =>
365
- // Unlike the describe path, this label never reaches a caller: search
366
- // only counts rejected catalogs (`unavailableCatalogs` below) and
367
- // renders its own guidance, so the folded name here stays internal and
368
- // needs no surface awareness.
369
- this.loadForDiscovery(connector.id, `search_tools probe of "${connector.id}"`));
370
- const searchableCatalogs = catalogs.map((catalog) => catalog.status === "fulfilled"
371
- ? {
372
- status: "fulfilled",
373
- value: toolsForSafety(catalog.value, safety),
374
- }
375
- : catalog);
393
+ // Named for this service's route: a scoped search echoes a timed-out
394
+ // probe's message back as `queryAnalysis.catalogError`, and a program
395
+ // cannot call search_tools.
396
+ const catalogs = await runEdge(this.discoveryCatalogs(connectors.map((connector) => connector.id), this.searchRoute));
397
+ const searchableCatalogs = catalogs.map((catalog) => Result.map(catalog, (tools) => toolsForSafety(tools, safety)));
376
398
  const matches = [];
377
399
  let matchMode = "all";
378
- const statistics = lexicalCorpusStatistics(searchableCatalogs.flatMap((catalog) => catalog.status === "fulfilled" ? [catalog.value] : []), retrievalQuery);
400
+ const statistics = lexicalCorpusStatistics(searchableCatalogs.flatMap((catalog) => Result.isSuccess(catalog) ? [catalog.success] : []), retrievalQuery);
379
401
  const trimmedQuery = query.trim();
380
402
  const isBrowse = trimmedQuery.length === 0;
381
403
  const queryTerms = lexicalQueryTerms(retrievalQuery);
@@ -394,8 +416,8 @@ export class CatalogService {
394
416
  if (!connector) {
395
417
  throw new Error("Catalog result has no corresponding connector");
396
418
  }
397
- if (catalog.status === "fulfilled") {
398
- for (const ranked of rankTools(catalog.value, retrievalQuery, mode, statistics, query)) {
419
+ if (Result.isSuccess(catalog)) {
420
+ for (const ranked of rankTools(catalog.success, retrievalQuery, mode, statistics, query)) {
399
421
  collected.push({
400
422
  connector,
401
423
  tool: ranked.tool,
@@ -407,8 +429,7 @@ export class CatalogService {
407
429
  });
408
430
  }
409
431
  }
410
- orderBase +=
411
- catalog.status === "fulfilled" ? catalog.value.length : 1;
432
+ orderBase += Result.isSuccess(catalog) ? catalog.success.length : 1;
412
433
  });
413
434
  return collected;
414
435
  };
@@ -444,12 +465,23 @@ export class CatalogService {
444
465
  ? this.outputSchema(match.connector.id, match.tool)
445
466
  : {};
446
467
  const input = match.tool.inputSchema ?? { type: "object" };
447
- const renderedInput = withSchemas
448
- ? renderSearchSchema(input, withSchemas)
449
- : undefined;
450
- const renderedOutput = withSchemas && output.schema
451
- ? renderSearchSchema(output.schema, withSchemas)
468
+ const signature = withSchemas === "typescript"
469
+ ? typescriptSignature(input, output.schema, {
470
+ observed: output.source === "observed",
471
+ description: false,
472
+ })
452
473
  : undefined;
474
+ const schemaFormat = withSchemas === "typescript" ? undefined : withSchemas;
475
+ const renderedInput = signature
476
+ ? { schema: undefined, truncated: signature.inputTruncated }
477
+ : schemaFormat
478
+ ? renderSearchSchema(input, schemaFormat)
479
+ : undefined;
480
+ const renderedOutput = signature
481
+ ? { schema: undefined, truncated: signature.outputTruncated }
482
+ : schemaFormat && output.schema
483
+ ? renderSearchSchema(output.schema, schemaFormat)
484
+ : undefined;
453
485
  const schemaKeys = withSchemas && args.includeSchemaKeys
454
486
  ? schemaKeyMetadata(input, output.schema)
455
487
  : undefined;
@@ -468,15 +500,16 @@ export class CatalogService {
468
500
  name: match.tool.name,
469
501
  address: `${match.connector.id}.${match.tool.name}`,
470
502
  ...(description !== undefined ? { description } : {}),
471
- ...(withSchemas
503
+ ...(schemaFormat
472
504
  ? {
473
505
  inputSchema: renderedInput?.schema,
474
506
  }
475
507
  : {}),
508
+ ...(signature ? { signature: signature.text } : {}),
476
509
  ...(renderedInput?.truncated
477
510
  ? { inputSchemaTruncated: true }
478
511
  : {}),
479
- ...(withSchemas && output.schema
512
+ ...(schemaFormat && output.schema
480
513
  ? {
481
514
  outputSchema: renderedOutput?.schema,
482
515
  }
@@ -535,16 +568,19 @@ export class CatalogService {
535
568
  unmatchedTerms.push(displayTerm(term));
536
569
  }
537
570
  }
538
- const unavailableCatalogs = catalogs.filter((catalog) => catalog.status === "rejected").length;
571
+ const unavailableCatalogs = catalogs.filter(Result.isFailure).length;
539
572
  // Named field by field rather than spread: `CallErrorDetails` also carries
540
573
  // connector, operation, recovery, and nextAction, and a discovery read is
541
574
  // not a call — widening the classifier must not silently widen what a
542
575
  // catalog search hands back.
543
576
  const scopedCatalogError = (() => {
544
- if (!scopedConnector || catalogs[0]?.status !== "rejected") {
577
+ const scopedCatalog = catalogs[0];
578
+ if (!scopedConnector ||
579
+ !scopedCatalog ||
580
+ Result.isSuccess(scopedCatalog)) {
545
581
  return undefined;
546
582
  }
547
- const error = classifyCallError(catalogs[0].reason, "catalog_lookup_failed");
583
+ const error = classifyCallError(scopedCatalog.failure, "catalog_lookup_failed");
548
584
  return {
549
585
  code: error.code,
550
586
  message: boundedEchoText(error.message),
@@ -717,19 +753,8 @@ export class CatalogService {
717
753
  .map((entry) => entry.resolved?.connector.id)
718
754
  .filter((id) => Boolean(id))),
719
755
  ];
720
- const loaded = await mapSettledWithConcurrency(connectorIds, this.concurrency, (id) => this.loadForDiscovery(id, `connecta.describe probe of "${id}"`));
721
- const catalogs = new Map();
722
- loaded.forEach((result, index) => {
723
- const connectorId = connectorIds[index];
724
- if (connectorId === undefined) {
725
- throw new Error("Catalog result has no corresponding connector id");
726
- }
727
- catalogs.set(connectorId, result.status === "fulfilled"
728
- ? result.value
729
- : result.reason instanceof Error
730
- ? result.reason
731
- : new Error(String(result.reason)));
732
- });
756
+ const loaded = await runEdge(this.discoveryCatalogs(connectorIds, "connecta.describe"));
757
+ const catalogs = new Map(connectorIds.map((id, index) => [id, loaded[index]]));
733
758
  return resolved.map(({ address, resolved: addressResolution }) => {
734
759
  if (!addressResolution) {
735
760
  const message = `Unknown address "${boundedEchoText(address)}"`;
@@ -743,8 +768,8 @@ export class CatalogService {
743
768
  };
744
769
  }
745
770
  const catalog = catalogs.get(addressResolution.connector.id);
746
- if (catalog instanceof Error) {
747
- const classified = classifyCallError(catalog, "catalog_lookup_failed");
771
+ if (catalog && Result.isFailure(catalog)) {
772
+ const classified = classifyCallError(catalog.failure, "catalog_lookup_failed");
748
773
  const message = boundedEchoText(classified.message);
749
774
  return {
750
775
  address: boundedEchoText(address),
@@ -759,10 +784,11 @@ export class CatalogService {
759
784
  },
760
785
  };
761
786
  }
762
- const tool = catalog?.find((item) => item.name === addressResolution.toolName);
787
+ const tools = catalog?.success ?? [];
788
+ const tool = tools.find((item) => item.name === addressResolution.toolName);
763
789
  if (!tool) {
764
790
  const message = `Unknown tool "${boundedEchoText(addressResolution.toolName)}" on connector "${addressResolution.connector.id}"`;
765
- const suggestions = describeSuggestions(addressResolution.connector.id, addressResolution.toolName, catalog ?? []);
791
+ const suggestions = describeSuggestions(addressResolution.connector.id, addressResolution.toolName, tools);
766
792
  return {
767
793
  address: boundedEchoText(address),
768
794
  error: message,
@@ -779,11 +805,19 @@ export class CatalogService {
779
805
  const input = tool.inputSchema ?? { type: "object" };
780
806
  const output = this.outputSchema(addressResolution.connector.id, tool);
781
807
  const description = summarizeDescription(tool.description, args.fullDescriptions === true);
808
+ const signature = format === "typescript"
809
+ ? typescriptSignature(input, output.schema, {
810
+ observed: output.source === "observed",
811
+ description: true,
812
+ })
813
+ : undefined;
782
814
  const compactInput = format === "compact"
783
815
  ? compactDescriptionSchema(input) : undefined;
784
816
  const compactOutput = format === "compact" && output.schema
785
817
  ? compactDescriptionSchema(output.schema) : undefined;
786
- const requiredReasons = guideRequiredReasons(addressResolution.connector, tool, compactInput?.truncated === true || compactOutput?.truncated === true);
818
+ const inputTruncated = compactInput?.truncated === true || signature?.inputTruncated === true;
819
+ const outputTruncated = compactOutput?.truncated === true || signature?.outputTruncated === true;
820
+ const requiredReasons = guideRequiredReasons(addressResolution.connector, tool, inputTruncated || outputTruncated);
787
821
  const guideSummary = connectorGuideSummary(addressResolution.connector);
788
822
  return {
789
823
  address,
@@ -801,10 +835,12 @@ export class CatalogService {
801
835
  guideRequiredReasons: requiredReasons,
802
836
  }
803
837
  : {}),
804
- inputSchema: compactInput?.text ?? input,
805
- ...(compactInput?.truncated ? { inputSchemaTruncated: true } : {}),
806
- ...(compactOutput?.truncated ? { outputSchemaTruncated: true } : {}),
807
- ...(output.schema
838
+ ...(signature
839
+ ? { signature: signature.text }
840
+ : { inputSchema: compactInput?.text ?? input }),
841
+ ...(inputTruncated ? { inputSchemaTruncated: true } : {}),
842
+ ...(outputTruncated ? { outputSchemaTruncated: true } : {}),
843
+ ...(output.schema && !signature
808
844
  ? {
809
845
  outputSchema: compactOutput?.text ?? output.schema,
810
846
  }