@zackbart/connecta 0.24.4 → 0.26.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 (155) hide show
  1. package/AGENTS.md +43 -12
  2. package/CHANGELOG.md +524 -0
  3. package/README.md +32 -15
  4. package/SECURITY.md +6 -5
  5. package/bin/connecta.mjs +5 -1
  6. package/dist/activity.d.ts +10 -3
  7. package/dist/activity.js +10 -1
  8. package/dist/advertised-schema.js +53 -0
  9. package/dist/artifacts/connector.d.ts +55 -0
  10. package/dist/artifacts/connector.js +724 -0
  11. package/dist/artifacts/document.js +76 -0
  12. package/dist/artifacts/guide.js +140 -0
  13. package/dist/artifacts/html-scan.js +329 -0
  14. package/dist/artifacts/json.js +68 -0
  15. package/dist/artifacts/kv-store.d.ts +20 -0
  16. package/dist/artifacts/kv-store.js +140 -0
  17. package/dist/artifacts/markdown.js +449 -0
  18. package/dist/artifacts/module.d.ts +37 -0
  19. package/dist/artifacts/module.js +60 -0
  20. package/dist/artifacts/operations.d.ts +195 -0
  21. package/dist/artifacts/operations.js +944 -0
  22. package/dist/artifacts/refresh.d.ts +53 -0
  23. package/dist/artifacts/refresh.js +235 -0
  24. package/dist/artifacts/routes.js +194 -0
  25. package/dist/artifacts/types.d.ts +214 -0
  26. package/dist/artifacts/types.js +22 -0
  27. package/dist/artifacts/validate.d.ts +62 -0
  28. package/dist/artifacts/validate.js +652 -0
  29. package/dist/artifacts.d.ts +6 -0
  30. package/dist/artifacts.js +9 -0
  31. package/dist/auth/downstream-oauth.js +490 -106
  32. package/dist/call-admission.js +139 -113
  33. package/dist/catalog-service.js +152 -106
  34. package/dist/catalog.js +297 -38
  35. package/dist/concurrency.js +24 -23
  36. package/dist/connector-access.d.ts +18 -4
  37. package/dist/connector-access.js +65 -20
  38. package/dist/connector-caller.d.ts +8 -0
  39. package/dist/connector-caller.js +19 -0
  40. package/dist/connector-scope.js +6 -37
  41. package/dist/connectors/api.d.ts +2 -2
  42. package/dist/connectors/remote-mcp.d.ts +6 -5
  43. package/dist/connectors/remote-mcp.js +330 -240
  44. package/dist/credential-contract.d.ts +9 -0
  45. package/dist/credential-rules.js +2 -2
  46. package/dist/credentials.d.ts +6 -1
  47. package/dist/credentials.js +45 -3
  48. package/dist/errors.d.ts +16 -1
  49. package/dist/errors.js +28 -0
  50. package/dist/execute.js +745 -282
  51. package/dist/executor-admission.d.ts +11 -1
  52. package/dist/executor-admission.js +145 -63
  53. package/dist/executors/quickjs-runtime.js +4 -2
  54. package/dist/executors/quickjs.js +255 -212
  55. package/dist/fix-prompt.js +37 -0
  56. package/dist/index.d.ts +72 -7
  57. package/dist/index.js +281 -26
  58. package/dist/invocation.js +371 -286
  59. package/dist/meta-tools.js +213 -94
  60. package/dist/module-contracts.d.ts +21 -1
  61. package/dist/oauth-callback-outcome.js +94 -0
  62. package/dist/oauth-sealing.js +29 -0
  63. package/dist/operator-ui/fix-prompts.js +87 -0
  64. package/dist/operator-ui/generated.js +2 -2
  65. package/dist/operator-ui/model.d.ts +103 -1
  66. package/dist/operator-ui/setup-commands.js +53 -0
  67. package/dist/operator-ui/view.js +128 -3
  68. package/dist/program-source.js +321 -0
  69. package/dist/providers/notion.js +2 -2
  70. package/dist/providers/revenuecat.js +32 -5
  71. package/dist/providers/stripe.d.ts +3 -2
  72. package/dist/providers/vercel.js +1 -1
  73. package/dist/registry.d.ts +43 -7
  74. package/dist/registry.js +434 -288
  75. package/dist/resumable.js +1291 -0
  76. package/dist/routes/activity.js +74 -154
  77. package/dist/routes/actor-labels.js +114 -0
  78. package/dist/routes/credentials.js +196 -163
  79. package/dist/routes/mcp.js +228 -112
  80. package/dist/routes/oauth-management.js +73 -71
  81. package/dist/routes/oauth.js +44 -16
  82. package/dist/routes/operator.js +83 -0
  83. package/dist/routes/shared.d.ts +25 -1
  84. package/dist/routes/shared.js +20 -2
  85. package/dist/routes/ui-data.js +211 -0
  86. package/dist/routes/ui.js +76 -35
  87. package/dist/run-journal.js +347 -0
  88. package/dist/runtime/admission.js +43 -0
  89. package/dist/runtime/call-admission.js +67 -0
  90. package/dist/runtime/connector-scope.js +72 -0
  91. package/dist/runtime/run.js +174 -0
  92. package/dist/runtime/services.js +130 -0
  93. package/dist/runtime/shared-read.js +106 -0
  94. package/dist/runtime/storage.js +66 -0
  95. package/dist/server.js +137 -89
  96. package/dist/skills.js +28 -9
  97. package/dist/storage/file.js +39 -2
  98. package/dist/storage/memory.js +28 -16
  99. package/dist/timeout.js +39 -43
  100. package/dist/tool-safety.d.ts +27 -1
  101. package/dist/tool-safety.js +31 -0
  102. package/dist/types.d.ts +45 -7
  103. package/dist/ui.d.ts +25 -4
  104. package/dist/ui.js +77 -167
  105. package/dist/url-safety.js +157 -0
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/documentation/architecture.md +367 -16
  109. package/documentation/auth.md +163 -2
  110. package/documentation/code-mode.md +274 -56
  111. package/documentation/meta-tools.md +200 -44
  112. package/ethos.md +46 -47
  113. package/examples/worker/AGENTS.md +6 -0
  114. package/examples/worker/README.md +224 -14
  115. package/examples/worker/src/d1-activity-row.ts +14 -2
  116. package/examples/worker/src/d1-activity.ts +4 -2
  117. package/examples/worker/src/d1-storage.ts +116 -0
  118. package/examples/worker/src/index.ts +42 -7
  119. package/examples/worker/src/r2-artifact-blobs.ts +29 -0
  120. package/examples/worker/wrangler.jsonc +21 -5
  121. package/package.json +16 -4
  122. package/templates/node/AGENTS.md +9 -2
  123. package/templates/node/README.md +31 -3
  124. package/templates/node/package.json +1 -1
  125. package/templates/node/src/index.ts +27 -0
  126. package/dist/auth/downstream-oauth.d.ts +0 -200
  127. package/dist/catalog-fingerprint.d.ts +0 -19
  128. package/dist/catalog-limits.d.ts +0 -9
  129. package/dist/catalog-service.d.ts +0 -263
  130. package/dist/catalog.d.ts +0 -87
  131. package/dist/concurrency.d.ts +0 -3
  132. package/dist/connectors/guarded-fetch.d.ts +0 -96
  133. package/dist/credential-rules.d.ts +0 -85
  134. package/dist/execute.d.ts +0 -154
  135. package/dist/executor-result.d.ts +0 -10
  136. package/dist/executors/quickjs-child.d.ts +0 -1
  137. package/dist/executors/quickjs-protocol.d.ts +0 -67
  138. package/dist/favicon.d.ts +0 -1
  139. package/dist/identity.d.ts +0 -4
  140. package/dist/invocation.d.ts +0 -74
  141. package/dist/mcp-result.d.ts +0 -8
  142. package/dist/meta-tools.d.ts +0 -129
  143. package/dist/operator-ui/generated.d.ts +0 -2
  144. package/dist/operator-ui/view.d.ts +0 -188
  145. package/dist/result-shapes.d.ts +0 -13
  146. package/dist/retired-toolkits.d.ts +0 -2
  147. package/dist/routes/activity.d.ts +0 -2
  148. package/dist/routes/credentials.d.ts +0 -2
  149. package/dist/routes/mcp.d.ts +0 -10
  150. package/dist/routes/oauth-management.d.ts +0 -2
  151. package/dist/routes/oauth.d.ts +0 -2
  152. package/dist/routes/ui.d.ts +0 -2
  153. package/dist/server.d.ts +0 -9
  154. package/dist/skills.d.ts +0 -44
  155. package/dist/timeout.d.ts +0 -21
@@ -1,9 +1,12 @@
1
- import { compactDiscoverySchema, compactDescriptionSchema, lexicalCorpusStatistics, lexicalQueryTerms, lexicalSearchQuery, matchesLexicalTerm, rankTools, schemaObjectKeys, summarizeDiscoveryDescription, summarizeDescription, } from "./catalog.js";
2
- import { mapSettledWithConcurrency, resolveDiscoveryConcurrency, } from "./concurrency.js";
1
+ import { Effect, 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 { isExplicitlyReadOnly } from "./tool-safety.js";
6
+ import { runEdge, withDeadlineEffect } from "./runtime/run.js";
7
+ import { SharedRead } from "./runtime/shared-read.js";
8
+ import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs, } from "./timeout.js";
9
+ import { isApprovalExempt, isExplicitlyReadOnly, NO_EXEMPTIONS, } from "./tool-safety.js";
7
10
  export const DEFAULT_SEARCH_LIMIT = 8;
8
11
  export const MAX_SEARCH_LIMIT = 100;
9
12
  export const MAX_DESCRIBE_ADDRESSES = 100;
@@ -199,16 +202,31 @@ export class CatalogService {
199
202
  concurrency;
200
203
  searchRoute;
201
204
  readOptions;
202
- loaded = new Map();
203
- loading = new Map();
205
+ requestSignal;
206
+ approval;
207
+ // The request-scoped catalog cache: one shared read per connector asked
208
+ // about, started by the first asker and joined by every later one. A
209
+ // success stays for the rest of the request, so a search and the call after
210
+ // it see one catalog; a failure, or a read cancelled because every asker
211
+ // left it, is dropped so the next ask reads again. The service lives and
212
+ // dies with one request, so no read here is ever awaited from another.
213
+ //
214
+ // Registry.getTools also joins concurrent reads in one request scope, which
215
+ // the operator UI and status reads rely on. This map overlaps it only for
216
+ // reads still in flight and is kept for what the registry's cannot do: hold
217
+ // the success for the rest of the request, recorded by the read itself
218
+ // rather than by an asker, whose probe deadline may have ended its wait.
219
+ catalogs = new Map();
204
220
  constructor(registry, baseUrl, options = {}) {
205
221
  this.registry = registry;
206
222
  this.baseUrl = baseUrl;
223
+ this.approval = options.approval ?? NO_EXEMPTIONS;
207
224
  this.requestScope = options.requestScope ?? {};
208
225
  this.probeTimeoutMs =
209
226
  normalizeTimeoutMs(options.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
210
227
  this.concurrency = resolveDiscoveryConcurrency(options.concurrency);
211
228
  this.searchRoute = options.searchRoute ?? "search_tools";
229
+ this.requestSignal = options.requestSignal;
212
230
  this.readOptions = options.defer
213
231
  ? {
214
232
  defer: options.defer,
@@ -237,34 +255,44 @@ export class CatalogService {
237
255
  ? { function: "connecta.search", arguments: searchArgs, purpose }
238
256
  : { tool: "search_tools", arguments: searchArgs, purpose };
239
257
  }
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);
258
+ loadConnector(id, callOptions = {}) {
259
+ return runEdge(this.catalog(id, callOptions));
260
+ }
261
+ // One connector's catalog from the request-scoped cache, reading it through
262
+ // the registry when this request has not yet. No asker's options govern the
263
+ // read: it carries its own signal and the catalog deadline, the probe
264
+ // timeout, whichever asker starts it. Each asker waits under its own signal
265
+ // and deadline, so a short-deadline call that starts the read and a search
266
+ // that joins it each end on their own terms, and the read is cancelled only
267
+ // once every asker has gone (#571).
268
+ catalog(id, callOptions) {
269
+ return Effect.suspend(() => {
270
+ let read = this.catalogs.get(id);
271
+ if (!read) {
272
+ const started = new SharedRead((signal) => this.registry.getTools(id, this.baseUrl, this.requestScope, { signal, timeoutMs: this.probeTimeoutMs }, this.readOptions), (succeeded) => {
273
+ // Evicted before the read resumes anyone, so an asker that
274
+ // retries on hearing of the failure starts a fresh read.
275
+ if (!succeeded && this.catalogs.get(id) === started) {
276
+ this.catalogs.delete(id);
277
+ }
278
+ });
279
+ this.catalogs.set(id, started);
280
+ read = started;
281
+ }
282
+ return read.join(callOptions.signal);
256
283
  });
257
- this.loading.set(id, loading);
258
- return loading;
259
284
  }
260
- loadForDiscovery(id, label) {
261
- return withDeadline((signal) => this.loadConnector(id, {
262
- signal,
263
- timeoutMs: this.probeTimeoutMs,
264
- }), {
285
+ // Every named catalog under its own probe deadline, `concurrency` at a time,
286
+ // each settling in its input slot: an unavailable catalog is a Failure
287
+ // there, not a failed discovery. `route` names the surface in a timeout.
288
+ // A probe also ends with its request, so no listing outlives the caller
289
+ // that asked for it.
290
+ discoveryCatalogs(ids, route) {
291
+ return Effect.forEach(ids, (id) => Effect.result(withDeadlineEffect((signal) => this.catalog(id, { signal }), {
292
+ ...(this.requestSignal ? { signal: this.requestSignal } : {}),
265
293
  timeoutMs: this.probeTimeoutMs,
266
- timeoutError: new Error(`${label} timed out after ${this.probeTimeoutMs}ms`),
267
- });
294
+ timeoutError: new Error(`${route} probe of "${id}" timed out after ${this.probeTimeoutMs}ms`),
295
+ })), { concurrency: this.concurrency });
268
296
  }
269
297
  outputSchema(connectorId, tool) {
270
298
  if (tool.outputSchema)
@@ -306,32 +334,35 @@ export class CatalogService {
306
334
  toolName,
307
335
  };
308
336
  }
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
- };
337
+ resolveTool(address, callOptions = {}) {
338
+ return runEdge(this.resolve(address, callOptions));
339
+ }
340
+ // resolveTool without the Promise, for InvocationService's pipeline, which
341
+ // yields it inside the call's deadline rather than crossing an edge. Never
342
+ // fails: an unloadable catalog is a resolution like any other refusal.
343
+ resolve(address, callOptions = {}) {
344
+ return Effect.suspend(() => {
345
+ const resolved = this.registry.resolveAddress(address);
346
+ if (!resolved) {
347
+ return Effect.succeed(this.unknownAddressFailure(address, address));
348
+ }
349
+ const { connector, toolName } = resolved;
350
+ const started = Date.now();
351
+ return Effect.match(this.catalog(connector.id, callOptions), {
352
+ onFailure: (cause) => this.catalogLoadFailure(cause, started, connector, toolName),
353
+ onSuccess: (tools) => {
354
+ const definition = tools.find((tool) => tool.name === toolName);
355
+ if (!definition) {
356
+ return this.unknownToolFailure(toolName, connector, started);
357
+ }
358
+ return {
359
+ ok: true,
360
+ resolved: { connector, toolName, definition },
361
+ catalogMs: Date.now() - started,
362
+ };
363
+ },
364
+ });
365
+ });
335
366
  }
336
367
  async search(args) {
337
368
  if (args.query !== undefined && typeof args.query !== "string") {
@@ -361,21 +392,14 @@ export class CatalogService {
361
392
  ? [scopedConnector]
362
393
  : []
363
394
  : 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);
395
+ // Named for this service's route: a scoped search echoes a timed-out
396
+ // probe's message back as `queryAnalysis.catalogError`, and a program
397
+ // cannot call search_tools.
398
+ const catalogs = await runEdge(this.discoveryCatalogs(connectors.map((connector) => connector.id), this.searchRoute));
399
+ const searchableCatalogs = catalogs.map((catalog) => Result.map(catalog, (tools) => toolsForSafety(tools, safety)));
376
400
  const matches = [];
377
401
  let matchMode = "all";
378
- const statistics = lexicalCorpusStatistics(searchableCatalogs.flatMap((catalog) => catalog.status === "fulfilled" ? [catalog.value] : []), retrievalQuery);
402
+ const statistics = lexicalCorpusStatistics(searchableCatalogs.flatMap((catalog) => Result.isSuccess(catalog) ? [catalog.success] : []), retrievalQuery);
379
403
  const trimmedQuery = query.trim();
380
404
  const isBrowse = trimmedQuery.length === 0;
381
405
  const queryTerms = lexicalQueryTerms(retrievalQuery);
@@ -394,8 +418,8 @@ export class CatalogService {
394
418
  if (!connector) {
395
419
  throw new Error("Catalog result has no corresponding connector");
396
420
  }
397
- if (catalog.status === "fulfilled") {
398
- for (const ranked of rankTools(catalog.value, retrievalQuery, mode, statistics, query)) {
421
+ if (Result.isSuccess(catalog)) {
422
+ for (const ranked of rankTools(catalog.success, retrievalQuery, mode, statistics, query)) {
399
423
  collected.push({
400
424
  connector,
401
425
  tool: ranked.tool,
@@ -407,8 +431,7 @@ export class CatalogService {
407
431
  });
408
432
  }
409
433
  }
410
- orderBase +=
411
- catalog.status === "fulfilled" ? catalog.value.length : 1;
434
+ orderBase += Result.isSuccess(catalog) ? catalog.success.length : 1;
412
435
  });
413
436
  return collected;
414
437
  };
@@ -444,12 +467,23 @@ export class CatalogService {
444
467
  ? this.outputSchema(match.connector.id, match.tool)
445
468
  : {};
446
469
  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)
470
+ const signature = withSchemas === "typescript"
471
+ ? typescriptSignature(input, output.schema, {
472
+ observed: output.source === "observed",
473
+ description: false,
474
+ })
452
475
  : undefined;
476
+ const schemaFormat = withSchemas === "typescript" ? undefined : withSchemas;
477
+ const renderedInput = signature
478
+ ? { schema: undefined, truncated: signature.inputTruncated }
479
+ : schemaFormat
480
+ ? renderSearchSchema(input, schemaFormat)
481
+ : undefined;
482
+ const renderedOutput = signature
483
+ ? { schema: undefined, truncated: signature.outputTruncated }
484
+ : schemaFormat && output.schema
485
+ ? renderSearchSchema(output.schema, schemaFormat)
486
+ : undefined;
453
487
  const schemaKeys = withSchemas && args.includeSchemaKeys
454
488
  ? schemaKeyMetadata(input, output.schema)
455
489
  : undefined;
@@ -468,15 +502,16 @@ export class CatalogService {
468
502
  name: match.tool.name,
469
503
  address: `${match.connector.id}.${match.tool.name}`,
470
504
  ...(description !== undefined ? { description } : {}),
471
- ...(withSchemas
505
+ ...(schemaFormat
472
506
  ? {
473
507
  inputSchema: renderedInput?.schema,
474
508
  }
475
509
  : {}),
510
+ ...(signature ? { signature: signature.text } : {}),
476
511
  ...(renderedInput?.truncated
477
512
  ? { inputSchemaTruncated: true }
478
513
  : {}),
479
- ...(withSchemas && output.schema
514
+ ...(schemaFormat && output.schema
480
515
  ? {
481
516
  outputSchema: renderedOutput?.schema,
482
517
  }
@@ -503,6 +538,11 @@ export class CatalogService {
503
538
  ...(match.tool.annotations
504
539
  ? { annotations: match.tool.annotations }
505
540
  : {}),
541
+ // Config lets a program call this write without pausing (#566).
542
+ // A marker, not a class: the tool stays approval-required.
543
+ ...(isApprovalExempt(this.approval, match.connector, match.tool.name, match.tool)
544
+ ? { approval: "exempt" }
545
+ : {}),
506
546
  ...(requiredReasons
507
547
  ? {
508
548
  guideRequired: true,
@@ -535,16 +575,19 @@ export class CatalogService {
535
575
  unmatchedTerms.push(displayTerm(term));
536
576
  }
537
577
  }
538
- const unavailableCatalogs = catalogs.filter((catalog) => catalog.status === "rejected").length;
578
+ const unavailableCatalogs = catalogs.filter(Result.isFailure).length;
539
579
  // Named field by field rather than spread: `CallErrorDetails` also carries
540
580
  // connector, operation, recovery, and nextAction, and a discovery read is
541
581
  // not a call — widening the classifier must not silently widen what a
542
582
  // catalog search hands back.
543
583
  const scopedCatalogError = (() => {
544
- if (!scopedConnector || catalogs[0]?.status !== "rejected") {
584
+ const scopedCatalog = catalogs[0];
585
+ if (!scopedConnector ||
586
+ !scopedCatalog ||
587
+ Result.isSuccess(scopedCatalog)) {
545
588
  return undefined;
546
589
  }
547
- const error = classifyCallError(catalogs[0].reason, "catalog_lookup_failed");
590
+ const error = classifyCallError(scopedCatalog.failure, "catalog_lookup_failed");
548
591
  return {
549
592
  code: error.code,
550
593
  message: boundedEchoText(error.message),
@@ -717,19 +760,8 @@ export class CatalogService {
717
760
  .map((entry) => entry.resolved?.connector.id)
718
761
  .filter((id) => Boolean(id))),
719
762
  ];
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
- });
763
+ const loaded = await runEdge(this.discoveryCatalogs(connectorIds, "connecta.describe"));
764
+ const catalogs = new Map(connectorIds.map((id, index) => [id, loaded[index]]));
733
765
  return resolved.map(({ address, resolved: addressResolution }) => {
734
766
  if (!addressResolution) {
735
767
  const message = `Unknown address "${boundedEchoText(address)}"`;
@@ -743,8 +775,8 @@ export class CatalogService {
743
775
  };
744
776
  }
745
777
  const catalog = catalogs.get(addressResolution.connector.id);
746
- if (catalog instanceof Error) {
747
- const classified = classifyCallError(catalog, "catalog_lookup_failed");
778
+ if (catalog && Result.isFailure(catalog)) {
779
+ const classified = classifyCallError(catalog.failure, "catalog_lookup_failed");
748
780
  const message = boundedEchoText(classified.message);
749
781
  return {
750
782
  address: boundedEchoText(address),
@@ -759,10 +791,11 @@ export class CatalogService {
759
791
  },
760
792
  };
761
793
  }
762
- const tool = catalog?.find((item) => item.name === addressResolution.toolName);
794
+ const tools = catalog?.success ?? [];
795
+ const tool = tools.find((item) => item.name === addressResolution.toolName);
763
796
  if (!tool) {
764
797
  const message = `Unknown tool "${boundedEchoText(addressResolution.toolName)}" on connector "${addressResolution.connector.id}"`;
765
- const suggestions = describeSuggestions(addressResolution.connector.id, addressResolution.toolName, catalog ?? []);
798
+ const suggestions = describeSuggestions(addressResolution.connector.id, addressResolution.toolName, tools);
766
799
  return {
767
800
  address: boundedEchoText(address),
768
801
  error: message,
@@ -779,11 +812,19 @@ export class CatalogService {
779
812
  const input = tool.inputSchema ?? { type: "object" };
780
813
  const output = this.outputSchema(addressResolution.connector.id, tool);
781
814
  const description = summarizeDescription(tool.description, args.fullDescriptions === true);
815
+ const signature = format === "typescript"
816
+ ? typescriptSignature(input, output.schema, {
817
+ observed: output.source === "observed",
818
+ description: true,
819
+ })
820
+ : undefined;
782
821
  const compactInput = format === "compact"
783
822
  ? compactDescriptionSchema(input) : undefined;
784
823
  const compactOutput = format === "compact" && output.schema
785
824
  ? compactDescriptionSchema(output.schema) : undefined;
786
- const requiredReasons = guideRequiredReasons(addressResolution.connector, tool, compactInput?.truncated === true || compactOutput?.truncated === true);
825
+ const inputTruncated = compactInput?.truncated === true || signature?.inputTruncated === true;
826
+ const outputTruncated = compactOutput?.truncated === true || signature?.outputTruncated === true;
827
+ const requiredReasons = guideRequiredReasons(addressResolution.connector, tool, inputTruncated || outputTruncated);
787
828
  const guideSummary = connectorGuideSummary(addressResolution.connector);
788
829
  return {
789
830
  address,
@@ -801,16 +842,21 @@ export class CatalogService {
801
842
  guideRequiredReasons: requiredReasons,
802
843
  }
803
844
  : {}),
804
- inputSchema: compactInput?.text ?? input,
805
- ...(compactInput?.truncated ? { inputSchemaTruncated: true } : {}),
806
- ...(compactOutput?.truncated ? { outputSchemaTruncated: true } : {}),
807
- ...(output.schema
845
+ ...(signature
846
+ ? { signature: signature.text }
847
+ : { inputSchema: compactInput?.text ?? input }),
848
+ ...(inputTruncated ? { inputSchemaTruncated: true } : {}),
849
+ ...(outputTruncated ? { outputSchemaTruncated: true } : {}),
850
+ ...(output.schema && !signature
808
851
  ? {
809
852
  outputSchema: compactOutput?.text ?? output.schema,
810
853
  }
811
854
  : {}),
812
855
  ...(output.source ? { outputSchemaSource: output.source } : {}),
813
856
  ...(tool.annotations ? { annotations: tool.annotations } : {}),
857
+ ...(isApprovalExempt(this.approval, addressResolution.connector, tool.name, tool)
858
+ ? { approval: "exempt" }
859
+ : {}),
814
860
  };
815
861
  });
816
862
  }