@zackbart/connecta 0.10.5 → 0.11.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 (93) hide show
  1. package/AGENTS.md +8 -6
  2. package/CHANGELOG.md +150 -0
  3. package/README.md +5 -4
  4. package/bin/connecta.mjs +0 -7
  5. package/dist/activity.d.ts +11 -1
  6. package/dist/activity.d.ts.map +1 -1
  7. package/dist/activity.js +44 -3
  8. package/dist/activity.js.map +1 -1
  9. package/dist/catalog-service.d.ts +24 -0
  10. package/dist/catalog-service.d.ts.map +1 -1
  11. package/dist/catalog-service.js +68 -9
  12. package/dist/catalog-service.js.map +1 -1
  13. package/dist/connectors/api.d.ts +2 -2
  14. package/dist/connectors/remote-mcp.d.ts +1 -1
  15. package/dist/errors.d.ts +49 -4
  16. package/dist/errors.d.ts.map +1 -1
  17. package/dist/errors.js +68 -1
  18. package/dist/errors.js.map +1 -1
  19. package/dist/execute.d.ts +73 -3
  20. package/dist/execute.d.ts.map +1 -1
  21. package/dist/execute.js +161 -29
  22. package/dist/execute.js.map +1 -1
  23. package/dist/index.d.ts +28 -30
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +29 -37
  26. package/dist/index.js.map +1 -1
  27. package/dist/invocation.d.ts +9 -2
  28. package/dist/invocation.d.ts.map +1 -1
  29. package/dist/invocation.js +61 -31
  30. package/dist/invocation.js.map +1 -1
  31. package/dist/meta-tools.d.ts +24 -59
  32. package/dist/meta-tools.d.ts.map +1 -1
  33. package/dist/meta-tools.js +107 -359
  34. package/dist/meta-tools.js.map +1 -1
  35. package/dist/operator-ui/generated.d.ts +1 -1
  36. package/dist/operator-ui/generated.d.ts.map +1 -1
  37. package/dist/operator-ui/generated.js +1 -1
  38. package/dist/operator-ui/generated.js.map +1 -1
  39. package/dist/registry.d.ts +12 -10
  40. package/dist/registry.d.ts.map +1 -1
  41. package/dist/registry.js +8 -17
  42. package/dist/registry.js.map +1 -1
  43. package/dist/routes/mcp.d.ts.map +1 -1
  44. package/dist/routes/mcp.js +19 -21
  45. package/dist/routes/mcp.js.map +1 -1
  46. package/dist/routes/shared.d.ts +9 -11
  47. package/dist/routes/shared.d.ts.map +1 -1
  48. package/dist/routes/shared.js.map +1 -1
  49. package/dist/server.js +5 -4
  50. package/dist/server.js.map +1 -1
  51. package/dist/skills.d.ts +8 -18
  52. package/dist/skills.d.ts.map +1 -1
  53. package/dist/skills.js +13 -60
  54. package/dist/skills.js.map +1 -1
  55. package/dist/types.d.ts +6 -20
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/version.d.ts +1 -1
  58. package/dist/version.js +1 -1
  59. package/documentation/code-first-exploration.md +16 -16
  60. package/documentation/code-mode.md +137 -63
  61. package/documentation/connectors.md +1 -1
  62. package/documentation/meta-tools.md +96 -33
  63. package/documentation/rich-output-design.md +212 -0
  64. package/ethos.md +17 -19
  65. package/examples/node/README.md +1 -2
  66. package/examples/node/src/index.ts +1 -3
  67. package/examples/worker/README.md +19 -16
  68. package/examples/worker/src/d1-activity-row.ts +40 -0
  69. package/examples/worker/src/d1-activity.ts +3 -2
  70. package/examples/worker/src/index.ts +6 -14
  71. package/examples/worker/wrangler.jsonc +3 -6
  72. package/package.json +1 -1
  73. package/src/activity.ts +69 -3
  74. package/src/catalog-service.ts +113 -20
  75. package/src/connectors/api.ts +2 -2
  76. package/src/connectors/remote-mcp.ts +1 -1
  77. package/src/errors.ts +104 -3
  78. package/src/execute.ts +237 -37
  79. package/src/index.ts +60 -67
  80. package/src/invocation.ts +61 -19
  81. package/src/meta-tools.ts +136 -482
  82. package/src/operator-ui/browser.ts +10 -2
  83. package/src/operator-ui/generated.ts +1 -1
  84. package/src/registry.ts +7 -35
  85. package/src/routes/mcp.ts +19 -21
  86. package/src/routes/shared.ts +8 -11
  87. package/src/server.ts +7 -7
  88. package/src/skills.ts +11 -74
  89. package/src/types.ts +6 -21
  90. package/src/version.ts +1 -1
  91. package/templates/node/README.md +2 -1
  92. package/templates/node/package.json +1 -1
  93. package/templates/node/src/index.ts +1 -1
package/src/index.ts CHANGED
@@ -18,7 +18,6 @@ import type { ActivityReadGate, ActivityStore } from "./activity.js";
18
18
  import type {
19
19
  Connector,
20
20
  ConnectaBranding,
21
- ConnectaSurface,
22
21
  Executor,
23
22
  InboundAuth,
24
23
  KVStorage,
@@ -77,10 +76,11 @@ export interface ConnectaDiscoveryConfig {
77
76
  staleCatalogSeconds?: number;
78
77
  /**
79
78
  * Deadline (ms) for each downstream probe/catalog call fanned out by
80
- * `list_connectors`, `search_tools`, and `describe_tools`. Defaults to
81
- * 30_000. A timed-out connector degrades independently; this does not apply
82
- * to tool calls. Catalog walks receive the same cancellation signal, which
83
- * aborts an in-flight page where supported and prevents another from starting.
79
+ * `search_tools` and by `connecta.search`/`connecta.describe` inside
80
+ * `execute_code`. Defaults to 30_000. A timed-out connector degrades
81
+ * independently; this does not apply to tool calls. Catalog walks receive the
82
+ * same cancellation signal, which aborts an in-flight page where supported and
83
+ * prevents another from starting.
84
84
  */
85
85
  probeTimeoutMs?: number;
86
86
  }
@@ -88,9 +88,9 @@ export interface ConnectaDiscoveryConfig {
88
88
  /** Deployment-wide call deadlines and inline-result paging thresholds. */
89
89
  export interface ConnectaCallsConfig {
90
90
  /**
91
- * Deadline (ms) for `call_tool`/`batch_call` calls that pass no `timeoutMs`.
92
- * An explicit per-call value wins. Opt-in: unset by default, so existing
93
- * long-running calls gain no surprise deadline.
91
+ * Deadline (ms) for `call_tool`/`call_destructive_tool` calls that pass no
92
+ * `timeoutMs`. An explicit per-call value wins. Opt-in: unset by default, so
93
+ * existing long-running calls gain no surprise deadline.
94
94
  *
95
95
  * This bounds one attempt, not all retries. `execute_code` host calls are
96
96
  * unaffected because they already carry their own bound.
@@ -102,13 +102,19 @@ export interface ConnectaCallsConfig {
102
102
  * 50_000. Connectors may override it individually.
103
103
  */
104
104
  maxResultBytes?: number;
105
+ }
106
+
107
+ /** Budgets for rich output emitted by execute_code programs (`connecta.emit`). */
108
+ export interface ConnectaExecuteConfig {
105
109
  /**
106
- * Max serialized `batch_call` envelope size (bytes) before the full batch is
107
- * stashed for `get_result` and only an ordered outcome summary is returned
108
- * inline. Must be a finite whole number >= 1; invalid values warn and fall
109
- * back to 100_000. This cap is independent of per-connector child caps.
110
+ * Aggregate serialized bytes `connecta.emit` accepts per run. Default
111
+ * 4_000_000 a transport bound, not a context bound: emitted image/audio
112
+ * blocks reach the model as media, not base64 text. Invalid values fall
113
+ * back to the default.
110
114
  */
111
- maxBatchResultBytes?: number;
115
+ maxEmittedBytes?: number;
116
+ /** Content blocks `connecta.emit` accepts per run. Default 32. */
117
+ maxEmittedBlocks?: number;
112
118
  }
113
119
 
114
120
  export interface AdmissionPoolConfig {
@@ -164,6 +170,8 @@ export interface ConnectaConfig {
164
170
  discovery?: ConnectaDiscoveryConfig;
165
171
  /** Deployment-wide call deadlines and result paging threshold. */
166
172
  calls?: ConnectaCallsConfig;
173
+ /** Budgets for the `connecta.emit` rich-output channel in execute_code. */
174
+ execute?: ConnectaExecuteConfig;
167
175
  /** Bounded MCP and fallback code-mode admission. */
168
176
  admission?: ConnectaAdmissionConfig;
169
177
  /** Optional browser UI and OAuth result-page labels. */
@@ -182,22 +190,12 @@ export interface ConnectaConfig {
182
190
  /** Deployment metadata exposed by /health (for example a Worker version). */
183
191
  deploymentInfo?: Record<string, unknown>;
184
192
  /**
185
- * Sandbox for `execute_code`, and the switch that decides the surface: with
186
- * an executor a model sees the seven code-first tools, without one the nine
187
- * classic ones. Workers: `new DynamicWorkerExecutor({ loader: env.LOADER })`
188
- * from `@cloudflare/codemode`. Node: `quickJsExecutor()` from
189
- * "@zackbart/connecta/quickjs".
190
- */
191
- executor?: Executor;
192
- /**
193
- * Override the surface the `executor` implies. The only reason to set it is
194
- * `"classic"` alongside an executor — ten tools, the shape the eval gate's
195
- * *incremental* arm measures ("does adding `execute_code` to classic help on
196
- * its own?"). The gate's control arm is executor-free classic, which needs no
197
- * override. `"code-first"` is the default wherever an executor exists and
198
- * throws without one.
193
+ * Required sandbox for `execute_code`. Workers use
194
+ * `new DynamicWorkerExecutor({ loader: env.LOADER })` from
195
+ * `@cloudflare/codemode`; Node uses `quickJsExecutor()` from
196
+ * `@zackbart/connecta/quickjs`.
199
197
  */
200
- surface?: ConnectaSurface;
198
+ executor: Executor;
201
199
  }
202
200
 
203
201
  export interface Connecta {
@@ -291,6 +289,20 @@ function assertNoLegacyConfig(config: ConnectaConfig): void {
291
289
  "issue #179. Credentials now fail at use; see ethos.md.",
292
290
  );
293
291
  }
292
+ const calls = candidate.calls;
293
+ if (
294
+ typeof calls === "object" &&
295
+ calls !== null &&
296
+ hasOwn(calls, "maxBatchResultBytes")
297
+ ) {
298
+ throw new Error(
299
+ "`calls.maxBatchResultBytes` was removed in issue #273 along with " +
300
+ "batch_call. Remove it; a program's batching is bounded by " +
301
+ "execute_code's own limits — connecta.batch's per-run call ceiling " +
302
+ "and the executor result cap — while each call inside it still honours " +
303
+ "calls.maxResultBytes and any per-connector override.",
304
+ );
305
+ }
294
306
  const found: Array<readonly [string, string]> = [];
295
307
  if (hasOwn(candidate, "activity")) {
296
308
  const activity = candidate.activity;
@@ -429,43 +441,22 @@ function warnInsecureConfig(
429
441
  }
430
442
  }
431
443
 
432
- /**
433
- * The advertised surface: the executor is the switch. Configure one and the
434
- * deployment serves the seven-tool code-first surface; omit it and there is no
435
- * program to fold discovery and batching into, so it serves classic.
436
- *
437
- * Two mistakes are structural rather than recoverable, so neither is warned
438
- * past: a surface name connecta does not implement, which would otherwise
439
- * resolve to something the operator did not ask for; and `code-first` without
440
- * an executor, which would advertise six tools and no program surface.
441
- */
442
- function resolveSurface(config: ConnectaConfig): ConnectaSurface {
443
- const surface = config.surface;
444
- if (surface === undefined) {
445
- return config.executor ? "code-first" : "classic";
446
- }
447
- if (surface !== "classic" && surface !== "code-first") {
444
+ export function createConnecta(config: ConnectaConfig): Connecta {
445
+ assertNoLegacyConfig(config);
446
+ if (Object.prototype.hasOwnProperty.call(config, "surface")) {
448
447
  throw new Error(
449
- `ConnectaConfig.surface must be "classic" or "code-first", not ` +
450
- `${JSON.stringify(surface)}.`,
448
+ "ConnectaConfig.surface was removed in issue #273. Remove it; connecta " +
449
+ "now serves one seven-tool surface.",
451
450
  );
452
451
  }
453
- if (surface === "code-first" && !config.executor) {
452
+ if (!config.executor) {
454
453
  throw new Error(
455
- 'ConnectaConfig.surface "code-first" requires an executor: it folds ' +
456
- "list_connectors, describe_tools, and batch_call into connecta.search, " +
457
- "connecta.describe, and connecta.batch inside execute_code, so without " +
458
- "an executor there is nothing left to reach them through. Configure " +
459
- "one (quickJsExecutor() from \"@zackbart/connecta/quickjs\" on Node, " +
460
- "new DynamicWorkerExecutor({ loader: env.LOADER }) on Workers).",
454
+ "ConnectaConfig.executor is required. Configure quickJsExecutor() from " +
455
+ '"@zackbart/connecta/quickjs" on Node, or ' +
456
+ "new DynamicWorkerExecutor({ loader: env.LOADER }) from " +
457
+ '"@cloudflare/codemode" on Workers.',
461
458
  );
462
459
  }
463
- return surface;
464
- }
465
-
466
- export function createConnecta(config: ConnectaConfig): Connecta {
467
- assertNoLegacyConfig(config);
468
- const surface = resolveSurface(config);
469
460
  const storage = config.storage ?? memoryStorage();
470
461
  const logger = config.logger ?? defaultLogger();
471
462
  const credentialConnectors = config.connectors.filter((c) => c.credential);
@@ -509,9 +500,6 @@ export function createConnecta(config: ConnectaConfig): Connecta {
509
500
  ...(config.calls?.maxResultBytes !== undefined
510
501
  ? { maxResultBytes: config.calls.maxResultBytes }
511
502
  : {}),
512
- ...(config.calls?.maxBatchResultBytes !== undefined
513
- ? { maxBatchResultBytes: config.calls.maxBatchResultBytes }
514
- : {}),
515
503
  });
516
504
  const inboundAuth = normalizeAuth(
517
505
  accessTokens ? [accessTokens.auth, ...configuredAuth] : configuredAuth,
@@ -527,10 +515,10 @@ export function createConnecta(config: ConnectaConfig): Connecta {
527
515
  );
528
516
  let codeAdmission: AdmissionController | undefined;
529
517
  let executor = config.executor;
530
- if (executor && !isAdmittingExecutor(executor)) {
518
+ if (!isAdmittingExecutor(executor)) {
531
519
  codeAdmission = configuredCodeAdmission;
532
520
  executor = withExecutorAdmission(executor, codeAdmission);
533
- } else if (executor && config.admission?.code) {
521
+ } else if (config.admission?.code) {
534
522
  logger.warn(
535
523
  "[connecta] admission.code is ignored because the configured executor " +
536
524
  "implements acquire() and owns its admission pool; configure that " +
@@ -556,8 +544,7 @@ export function createConnecta(config: ConnectaConfig): Connecta {
556
544
  ...(config.activity?.deploymentId !== undefined
557
545
  ? { activityDeploymentId: config.activity.deploymentId }
558
546
  : {}),
559
- ...(executor !== undefined ? { executor } : {}),
560
- surface,
547
+ executor,
561
548
  requestAdmission,
562
549
  ...(config.calls?.defaultTimeoutMs !== undefined
563
550
  ? { defaultToolTimeoutMs: config.calls.defaultTimeoutMs }
@@ -568,6 +555,12 @@ export function createConnecta(config: ConnectaConfig): Connecta {
568
555
  ...(config.discovery?.concurrency !== undefined
569
556
  ? { discoveryConcurrency: config.discovery.concurrency }
570
557
  : {}),
558
+ ...(config.execute?.maxEmittedBytes !== undefined
559
+ ? { maxEmittedBytes: config.execute.maxEmittedBytes }
560
+ : {}),
561
+ ...(config.execute?.maxEmittedBlocks !== undefined
562
+ ? { maxEmittedBlocks: config.execute.maxEmittedBlocks }
563
+ : {}),
571
564
  ...(credentialVault !== undefined ? { credentialVault } : {}),
572
565
  ...(accessTokens !== undefined ? { accessTokens } : {}),
573
566
  ...(config.deploymentInfo !== undefined
@@ -632,7 +625,6 @@ export type {
632
625
  ConnectorCallAdmissionRule,
633
626
  ConnectorRollingWindowBudget,
634
627
  ConnectaBranding,
635
- ConnectaSurface,
636
628
  ConnectorCredentialAccess,
637
629
  ConnectorCredentialConfig,
638
630
  ConnectorCredentialFieldConfig,
@@ -667,6 +659,7 @@ export type {
667
659
  ActivityReadPage,
668
660
  ActivitySink,
669
661
  ActivityStore,
662
+ AgentFriction,
670
663
  ToolCallActivityEvent,
671
664
  } from "./activity.js";
672
665
  export { InvalidActivityCursorError } from "./activity.js";
package/src/invocation.ts CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  recordToolActivity,
3
3
  type ActivityCallSource,
4
4
  type ActivityRequestContext,
5
+ type AgentFriction,
5
6
  } from "./activity.js";
6
7
  import { isCallAdmissionError } from "./call-admission.js";
7
8
  import {
@@ -12,12 +13,13 @@ import {
12
13
  import {
13
14
  classifyCallError,
14
15
  ConnectorCallError,
16
+ echoedCallArgs,
15
17
  framingError,
16
18
  type AuthRecoveryMode,
17
19
  type CallErrorDetails,
18
20
  } from "./errors.js";
19
21
  import { unwrapMcpResult } from "./mcp-result.js";
20
- import type { RegistryView } from "./registry.js";
22
+ import { splitAddress, type RegistryView } from "./registry.js";
21
23
  import { isExplicitlyReadOnly } from "./tool-safety.js";
22
24
  import type { ToolDef } from "./types.js";
23
25
  import { validateToolInput } from "./validate.js";
@@ -150,6 +152,13 @@ export interface InvocationContext<T> {
150
152
  value: unknown,
151
153
  resolved: ResolvedCatalogTool,
152
154
  ) => T | Promise<T>;
155
+ /**
156
+ * Optional payload-free friction class derived from a *successful* result —
157
+ * today only an oversized one that had to be paged. It is deliberately not an
158
+ * `errorCode`: the call succeeded, and a consumer that keys its dashboards on
159
+ * "has an error code" must not count a truncation as a failure.
160
+ */
161
+ activityFriction?: (value: T) => AgentFriction | undefined;
153
162
  /**
154
163
  * Called after address/catalog/safety admission and before the first provider
155
164
  * attempt. Code mode uses it for its host-call budget.
@@ -252,6 +261,10 @@ export class InvocationService {
252
261
  let activityTarget:
253
262
  | Pick<ResolvedCatalogTool, "connector" | "toolName">
254
263
  | undefined;
264
+ // The address as written, used for activity when resolution never reached
265
+ // a connector. Only its two halves are recorded — the same fields activity
266
+ // has always carried — so no new class of payload enters the log.
267
+ const attempted = splitAddress(address);
255
268
  const timing = (): InvocationTiming => ({
256
269
  catalogMs,
257
270
  admissionMs,
@@ -262,25 +275,56 @@ export class InvocationService {
262
275
  });
263
276
  const record = (
264
277
  outcome: "success" | "error" | "timeout" | "cancelled",
265
- errorCode?: string,
278
+ classification: { errorCode?: string; friction?: AgentFriction } = {},
266
279
  ) => {
267
- if (!activityTarget) return;
280
+ const identity = activityTarget
281
+ ? {
282
+ connectorId: activityTarget.connector.id,
283
+ toolName: activityTarget.toolName,
284
+ }
285
+ : attempted;
286
+ if (!identity) return;
268
287
  recordToolActivity(this.activity, {
269
- connectorId: activityTarget.connector.id,
270
- toolName: activityTarget.toolName,
271
- address: `${activityTarget.connector.id}.${activityTarget.toolName}`,
288
+ connectorId: identity.connectorId,
289
+ toolName: identity.toolName,
290
+ address: `${identity.connectorId}.${identity.toolName}`,
272
291
  source: context.source,
273
292
  outcome,
274
293
  durationMs: Date.now() - started,
275
294
  attempts,
276
- ...(errorCode ? { errorCode } : {}),
295
+ ...(classification.errorCode
296
+ ? { errorCode: classification.errorCode }
297
+ : {}),
298
+ ...(classification.friction
299
+ ? { friction: classification.friction }
300
+ : {}),
277
301
  });
278
302
  };
279
303
  const failed = (error: CallErrorDetails): InvocationOutcome<T> => {
280
304
  const diagnostics = timing();
281
305
  const target = resolved ?? activityTarget;
306
+ const echoed =
307
+ error.code === "destructive_tool_requires_approval"
308
+ ? echoedCallArgs(args)
309
+ : {};
282
310
  const details =
283
- error.code === "auth_required" && target
311
+ error.code === "destructive_tool_requires_approval" && target
312
+ ? {
313
+ ...error,
314
+ nextAction: {
315
+ tool: "call_destructive_tool" as const,
316
+ arguments: {
317
+ address: `${target.connector.id}.${target.toolName}`,
318
+ ...echoed,
319
+ },
320
+ purpose:
321
+ "Ask the MCP host to approve this consequential call. " +
322
+ ("args" in echoed
323
+ ? "Re-send these arguments and add a short reason for the human reviewer."
324
+ : "Re-send the arguments you just sent — they are too large to echo back — and add a short reason for the human reviewer."),
325
+ },
326
+ }
327
+ : error.code === "auth_required" && target
284
328
  ? {
285
329
  ...error,
286
330
  connector: target.connector.id,
@@ -305,16 +349,13 @@ export class InvocationService {
305
349
  ...error,
306
350
  connector: target.connector.id,
307
351
  operation: `${target.connector.id}.${target.toolName}`,
308
- nextAction: {
309
- tool: "search_tools" as const,
310
- arguments: {
352
+ nextAction: this.catalog.searchRecovery(
353
+ {
311
354
  query: target.toolName,
312
355
  connector: target.connector.id,
313
- includeSchemas: "compact" as const,
314
356
  },
315
- purpose:
316
- "Inspect the current input shape if the validation findings are not sufficient.",
317
- },
357
+ "Inspect the current input shape if the validation findings are not sufficient.",
358
+ ),
318
359
  retry:
319
360
  `Correct the listed arguments and retry ` +
320
361
  `${target.connector.id}.${target.toolName}.`,
@@ -326,7 +367,7 @@ export class InvocationService {
326
367
  : details.code === "cancelled"
327
368
  ? "cancelled"
328
369
  : "error",
329
- details.code,
370
+ { errorCode: details.code },
330
371
  );
331
372
  return {
332
373
  ok: false,
@@ -354,8 +395,8 @@ export class InvocationService {
354
395
  // A connector whose catalog cannot be fetched is as unusable as one
355
396
  // whose execution fails, so it feeds health accounting the same way the
356
397
  // attempt catch below does — otherwise a connector every call fails
357
- // against (a revoked downstream grant, say) still reads clean from the
358
- // cheap `list_connectors({ probe: false })` signal.
398
+ // against (a revoked downstream grant, say) still reads clean in the
399
+ // deployment's health log.
359
400
  //
360
401
  // Recorded HERE rather than inside the registry's catalog fetch because
361
402
  // a cache hit that avoids a live listTools call records nothing — it is
@@ -591,7 +632,8 @@ export class InvocationService {
591
632
  : (result as T);
592
633
  resultProcessingMs += Date.now() - processingStarted;
593
634
  const diagnostics = timing();
594
- record("success");
635
+ const friction = context.activityFriction?.(value);
636
+ record("success", friction ? { friction } : {});
595
637
  return {
596
638
  ok: true,
597
639
  value,