@zackbart/connecta 0.24.3 → 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 (145) hide show
  1. package/AGENTS.md +58 -30
  2. package/CHANGELOG.md +317 -1
  3. package/README.md +5 -6
  4. package/dist/advertised-schema.js +53 -0
  5. package/dist/auth/downstream-oauth.js +490 -106
  6. package/dist/branding.d.ts +31 -2
  7. package/dist/branding.js +116 -8
  8. package/dist/call-admission.js +139 -113
  9. package/dist/catalog-service.js +141 -105
  10. package/dist/catalog.js +258 -33
  11. package/dist/concurrency.js +24 -23
  12. package/dist/connector-scope.js +6 -37
  13. package/dist/connectors/api.d.ts +3 -3
  14. package/dist/connectors/api.js +10 -2
  15. package/dist/connectors/guarded-fetch.js +34 -4
  16. package/dist/connectors/remote-mcp.d.ts +6 -5
  17. package/dist/connectors/remote-mcp.js +325 -245
  18. package/dist/credential-contract.d.ts +9 -0
  19. package/dist/credential-rules.js +2 -2
  20. package/dist/credentials.d.ts +6 -1
  21. package/dist/credentials.js +45 -3
  22. package/dist/errors.d.ts +11 -3
  23. package/dist/errors.js +2 -1
  24. package/dist/execute.js +363 -257
  25. package/dist/executor-admission.js +67 -50
  26. package/dist/executors/quickjs-runtime.js +4 -2
  27. package/dist/executors/quickjs.js +251 -210
  28. package/dist/fix-prompt.js +37 -0
  29. package/dist/index.d.ts +11 -2
  30. package/dist/index.js +63 -21
  31. package/dist/invocation.js +324 -285
  32. package/dist/meta-tools.js +304 -114
  33. package/dist/oauth-callback-outcome.js +94 -0
  34. package/dist/oauth-sealing.js +29 -0
  35. package/dist/operator-ui/fix-prompts.js +87 -0
  36. package/dist/operator-ui/generated.js +2 -2
  37. package/dist/operator-ui/model.d.ts +46 -1
  38. package/dist/operator-ui/setup-commands.js +53 -0
  39. package/dist/operator-ui/view.js +109 -2
  40. package/dist/program-source.js +205 -0
  41. package/dist/providers/cloudflare.d.ts +14 -2
  42. package/dist/providers/cloudflare.js +107 -16
  43. package/dist/providers/linear.d.ts +26 -4
  44. package/dist/providers/linear.js +19 -4
  45. package/dist/providers/mixpanel.d.ts +16 -3
  46. package/dist/providers/mixpanel.js +13 -2
  47. package/dist/providers/notion.d.ts +8 -1
  48. package/dist/providers/notion.js +85 -12
  49. package/dist/providers/revenuecat.d.ts +30 -4
  50. package/dist/providers/revenuecat.js +70 -5
  51. package/dist/providers/stripe.d.ts +10 -3
  52. package/dist/providers/stripe.js +30 -4
  53. package/dist/providers/vercel.js +12 -2
  54. package/dist/registry.d.ts +13 -5
  55. package/dist/registry.js +317 -258
  56. package/dist/routes/activity.js +120 -137
  57. package/dist/routes/credentials.js +174 -163
  58. package/dist/routes/mcp.js +129 -70
  59. package/dist/routes/oauth-management.js +60 -71
  60. package/dist/routes/oauth.js +44 -16
  61. package/dist/routes/operator.js +82 -0
  62. package/dist/routes/shared.d.ts +2 -0
  63. package/dist/routes/ui-data.js +210 -0
  64. package/dist/routes/ui.js +64 -32
  65. package/dist/runtime/admission.js +43 -0
  66. package/dist/runtime/call-admission.js +67 -0
  67. package/dist/runtime/connector-scope.js +72 -0
  68. package/dist/runtime/run.js +174 -0
  69. package/dist/runtime/services.js +130 -0
  70. package/dist/runtime/storage.js +66 -0
  71. package/dist/server.js +111 -89
  72. package/dist/skills.js +1 -1
  73. package/dist/storage/file.js +39 -2
  74. package/dist/storage/memory.js +28 -16
  75. package/dist/timeout.js +17 -47
  76. package/dist/types.d.ts +64 -7
  77. package/dist/ui.d.ts +16 -2
  78. package/dist/ui.js +68 -176
  79. package/dist/url-safety.js +157 -0
  80. package/dist/version.d.ts +1 -1
  81. package/dist/version.js +1 -1
  82. package/documentation/architecture.md +368 -182
  83. package/documentation/auth.md +277 -176
  84. package/documentation/code-mode.md +479 -329
  85. package/documentation/meta-tools.md +494 -417
  86. package/ethos.md +44 -47
  87. package/examples/worker/AGENTS.md +2 -1
  88. package/examples/worker/README.md +62 -14
  89. package/examples/worker/src/d1-storage.ts +116 -0
  90. package/examples/worker/src/index.ts +18 -21
  91. package/examples/worker/wrangler.jsonc +16 -4
  92. package/package.json +11 -4
  93. package/templates/node/.env.example +3 -3
  94. package/templates/node/AGENTS.md +5 -4
  95. package/templates/node/README.md +2 -1
  96. package/templates/node/package.json +1 -1
  97. package/templates/node/src/index.ts +23 -22
  98. package/dist/auth/downstream-oauth.d.ts +0 -200
  99. package/dist/catalog-fingerprint.d.ts +0 -19
  100. package/dist/catalog-limits.d.ts +0 -9
  101. package/dist/catalog-service.d.ts +0 -263
  102. package/dist/catalog.d.ts +0 -87
  103. package/dist/concurrency.d.ts +0 -3
  104. package/dist/connectors/guarded-fetch.d.ts +0 -92
  105. package/dist/credential-rules.d.ts +0 -85
  106. package/dist/execute.d.ts +0 -154
  107. package/dist/executor-result.d.ts +0 -10
  108. package/dist/executors/quickjs-child.d.ts +0 -1
  109. package/dist/executors/quickjs-protocol.d.ts +0 -67
  110. package/dist/favicon.d.ts +0 -1
  111. package/dist/identity.d.ts +0 -4
  112. package/dist/invocation.d.ts +0 -74
  113. package/dist/mcp-result.d.ts +0 -8
  114. package/dist/meta-tools.d.ts +0 -129
  115. package/dist/operator-ui/generated.d.ts +0 -2
  116. package/dist/operator-ui/view.d.ts +0 -151
  117. package/dist/result-shapes.d.ts +0 -13
  118. package/dist/retired-toolkits.d.ts +0 -2
  119. package/dist/routes/activity.d.ts +0 -2
  120. package/dist/routes/credentials.d.ts +0 -2
  121. package/dist/routes/mcp.d.ts +0 -10
  122. package/dist/routes/oauth-management.d.ts +0 -2
  123. package/dist/routes/oauth.d.ts +0 -2
  124. package/dist/routes/ui.d.ts +0 -2
  125. package/dist/server.d.ts +0 -9
  126. package/dist/skills.d.ts +0 -44
  127. package/dist/timeout.d.ts +0 -21
  128. package/dist/tool-safety.d.ts +0 -9
  129. package/documentation/call-admission.md +0 -158
  130. package/documentation/cloudflare.md +0 -471
  131. package/documentation/connector-guides.md +0 -176
  132. package/documentation/connectors.md +0 -431
  133. package/documentation/linear.md +0 -193
  134. package/documentation/mixpanel.md +0 -160
  135. package/documentation/notion.md +0 -308
  136. package/documentation/operations.md +0 -359
  137. package/documentation/operator-ui.md +0 -135
  138. package/documentation/optional-modules-upgrade.md +0 -243
  139. package/documentation/provider-conventions.md +0 -729
  140. package/documentation/request-admission.md +0 -204
  141. package/documentation/revenuecat.md +0 -305
  142. package/documentation/storage-and-credentials.md +0 -254
  143. package/documentation/stripe.md +0 -262
  144. package/documentation/upgrading.md +0 -768
  145. package/documentation/vercel.md +0 -241
@@ -2,56 +2,79 @@
2
2
 
3
3
  Connecta keeps one small tool surface in model context and resolves downstream
4
4
  tools behind it. `search_tools` finds addresses, the call tools enforce safety
5
- annotations, and `get_result` pages bounded results.
6
-
7
- ## The deployment surface
8
-
9
- Every deployment requires an executor and `tools/list` is exactly seven:
10
- `execute_code`, `search_tools`, `call_tool`, `call_destructive_tool`,
11
- `authorize_connector`, `get_result`, and `skills`. Discovery uses `connecta.search` and `connecta.describe`; programs compose
12
- calls with JavaScript promises ([#273](https://github.com/zackbart/connecta/issues/273)).
13
-
14
- Code-first is what a model sees. Read-only work has two routes: `call_tool` for
15
- one known address, and `execute_code` when discovery or any wider work is
16
- needed. Real hosted catalogs reversed the earlier synthetic result that made a
17
- top-level cold search look cheaper. Keeping discovery inside the program avoids
18
- returning every candidate schema to the model and removes a model round trip.
19
- The [guest API contract](./code-mode.md) is what a program is promised.
20
-
21
- The route is chosen before discovery. An unknown address, a result that will be
22
- reduced, a call whose arguments depend on an earlier result, or work with
23
- multiple operations starts with `execute_code` and keeps discovery, calls, and reduction inside it
24
- when the schemas and result shapes suffice. An unfamiliar provider result may
25
- return a small sample for inspection before continuing in another call. This
26
- exception avoids repeated guesses at text formats or collection roots; it does
27
- not restore a mandatory discovery-only round trip. Distinct operations get distinct short
28
- `connecta.search` queries in that program. A known address needs only
29
- `call_tool`.
30
-
31
- That routing is about read-only work, because that is the only work a program
32
- can do. Anything unannotated, write-capable, or destructive is inadmissible
33
- inside the sandbox, so multi-step destructive work discovers at the top level
34
- and runs each step through `call_destructive_tool` — where the host can put the
35
- question to a human. Telling an agent never to search at the top level for
36
- multiple calls would close the only route that work has
37
- ([#295](https://github.com/zackbart/connecta/issues/295)).
38
-
39
- `execute_code` accepts optional `diagnostics: true` when a caller is measuring
40
- a workflow. It adds only compact request-local timing and serialized-size
41
- aggregates; normal calls carry no diagnostics block or response-context cost.
42
- The measurements never contain program source, arguments, values, addresses,
5
+ annotations, `execute_code` runs read-only work as a program, and `get_result`
6
+ pages bounded results.
7
+
8
+ This guide is the contract an MCP client sees. The in-program `connecta.*` API
9
+ those tools imply belongs to [code mode](./code-mode.md); inbound identity and
10
+ credential administration belong to [auth](./auth.md).
11
+
12
+ ## The seven tools
13
+
14
+ Every deployment requires an executor, so `tools/list` is exactly seven. No
15
+ configuration adds an eighth or removes one. Resumable writes plan an
16
+ unconditional eighth, the destructive-annotated `resume_execution`
17
+ ([ethos](../ethos.md#decisions)); `execute_code` keeps its read-only hint,
18
+ because writes will run only there or for config-exempt tools.
19
+
20
+ | Tool | Arguments | Returns |
21
+ | --- | --- | --- |
22
+ | `execute_code` | `code`, `diagnostics?` | the program's reduced return value, plus a `diagnostics` block when asked |
23
+ | `search_tools` | `query?`, `connector?`, `safety?`, `limit?`, `offset?`, `fullDescriptions?`, `includeSchemas?: "compact" \| "json" \| "typescript"` | `{ connectors: [{ id, tools }], total, offset, limit, hasMore }`, plus `queryAnalysis` on a partial or failed search |
24
+ | `call_tool` | `address`, `args?`, `resultMode?: "mcp" \| "value"`, `timeoutMs?`, `diagnostics?` | the downstream result, bounded as [result representation](#result-representation) describes |
25
+ | `call_destructive_tool` | the same, plus `reason?` | the same |
26
+ | `authorize_connector` | `connector`, `force?` | the class-specific handoff in [authorization recovery](#authorization-recovery) |
27
+ | `get_result` | `id`, `offset?`, `maxBytes?` | a one-line JSON header `{ resultId, offset, bytes, totalBytes, hasMore, nextAction? }`, a newline, then the page as raw text ([paging](#paging-with-get_result)) |
28
+ | `skills` | `name?` | the listing when `name` is absent, that skill's markdown when it is present |
29
+
30
+ `limit` defaults to 8 and is capped at 100, as is one `connecta.describe` batch.
31
+ `get_result.offset` is a whole number of bytes ≥ 0 defaulting to 0 and
32
+ `maxBytes` a whole number ≥ 1 that defaults to, and is clamped to, the inline
33
+ cap of the call that stashed the result — 24,000 bytes unless
34
+ `calls.maxResultBytes` or a per-connector override says otherwise
35
+ ([why 24,000](#truncated-direct-call-results)). A value outside those domains is
36
+ an input error; a valid `maxBytes` above the cap is an upper bound, not an
37
+ error ([why](#paging-with-get_result)). `reason` is at most 500 characters of context for the host's human
38
+ approval view; Connecta neither treats it as authority nor sends it downstream,
39
+ and an empty or whitespace-only one reads as no reason rather than as grounds to
40
+ refuse a consequential call.
41
+
42
+ `diagnostics: true` adds compact request-local timing and serialized-size
43
+ aggregates for a caller measuring a workflow: a `diagnostics` block from
44
+ `execute_code`, a `timing` block on a call response that is already structured
45
+ (value mode, or a failure carrying recovery). Normal calls pay nothing for it,
46
+ and the measurements never contain program source, arguments, values, addresses,
43
47
  credentials, logs, or raw error text.
44
48
 
45
- Nothing became unreachable. `connecta.describe` takes the same addresses and
46
- formats as the internal catalog service, ordinary promises compose read-only
47
- calls, and an unfiltered
48
- `connecta.search({})` browses every catalog a program can reach. Live connector
49
- probing is an operator concern: the operator pages and `/health` own it.
50
-
51
- Program search includes a bounded `connectorTitle` on each tool when configured, so choosing an account or environment does not require a provider read. It is context, not a ranking input or proof of live access.
52
-
53
- The three discovery routes use deliberately different envelopes. These are
54
- their smallest successful one-tool shapes:
49
+ Connecta's own tools carry the annotations it demands of downstream tools:
50
+ read-only hints on all but `authorize_connector`, which mutates stored auth
51
+ state, and `call_destructive_tool`. Otherwise a host that gates on annotations
52
+ would prompt for every search, and a connecta aggregated behind another connecta
53
+ would be refused by its own policy.
54
+
55
+ ## Routing between the call surfaces
56
+
57
+ The route is chosen before discovery, and read-only work has exactly two:
58
+ `call_tool` for one known address, `execute_code` for everything wider — an
59
+ unknown address, a result that will be reduced, a call whose arguments depend on
60
+ an earlier result, or several operations. A program keeps discovery, calls, and
61
+ reduction together when the schemas and result shapes suffice, and gives each
62
+ distinct operation its own short `connecta.search` query. That is cheaper than it
63
+ looks: discovery inside the program returns no candidate schema to the model and
64
+ costs no round trip. One exception — an unfamiliar provider result may come back
65
+ as a small sample for inspection before continuing in another call, which avoids
66
+ repeated guesses at text formats and collection roots without restoring a
67
+ mandatory discovery-only round trip.
68
+
69
+ That covers read-only work, because that is the only work a program can do.
70
+ Anything unannotated, write-capable, or destructive is inadmissible inside the
71
+ sandbox, so multi-step destructive work discovers at the top level and runs each
72
+ step through `call_destructive_tool`, where the host can put the question to a
73
+ human. Telling an agent never to search at the top level for multiple calls
74
+ would close the only route that work has.
75
+
76
+ The three discovery routes use deliberately different envelopes. These are their
77
+ smallest successful one-tool shapes:
55
78
 
56
79
  ```js
57
80
  // Top-level search_tools
@@ -62,295 +85,383 @@ their smallest successful one-tool shapes:
62
85
  { tools: [{ name: "get_run", address: "ci.get_run", inputSchema: "{ runId: integer }" }] } // connecta.describe
63
86
  ```
64
87
 
88
+ Live connector probing is not a fourth: the operator pages and `/health` own it.
89
+
65
90
  ## Discovery context
66
91
 
67
- The deployment-derived `execute_code` description includes a live connector
68
- inventory before any catalog search. It preserves registry order and uses each
69
- canonical id and a distinct configured title without generating a second name for programs. Titles normalize whitespace and are capped at 48 UTF-8 bytes, so account and environment hints cannot consume the entire inventory.
70
- The complete inventory line is capped at 256 UTF-8 bytes. Entries stay whole,
71
- and a truncated line ends with the exact `+N more` count. This reads only the
72
- configured registry: it loads no catalog, probes no credential, grants no
73
- capability, and does not replace canonical discovery or addressing.
74
-
75
- Start a lookup with two to four distinctive action/object terms, not the full
76
- request. Read-only lookup belongs in `connecta.search` inside the program.
77
- Top-level `search_tools` remains available for explicit catalog inspection and
78
- approval-required discovery. Omit `limit` initially so the default
79
- eight-result page stays small. When the integration is obvious, set
80
- `connector` to its id: a scoped search loads that catalog alone, while an
81
- unscoped search must fan out across every configured connector. Leave the
82
- search unscoped when the right integration is genuinely ambiguous. Set
83
- `safety: "readOnly"` for generated code; `safety: "approvalRequired"` finds the
84
- complementary set that must cross `call_destructive_tool`. Omitting `safety`,
85
- or setting it to `"all"`, preserves the complete configured catalog. This is
86
- only a discovery filter: it neither grants authority nor changes invocation admission.
92
+ The deployment-derived `execute_code` description carries a live connector
93
+ inventory before any catalog search — registry order, each canonical id, and a
94
+ distinct configured title, with no second name minted for programs. Titles
95
+ normalize whitespace and cap at 48 UTF-8 bytes, the complete line caps at 256,
96
+ entries stay whole, and a truncated line ends with the exact `+N more` count, so
97
+ account and environment hints cannot eat the inventory. It reads only the
98
+ configured registry: no catalog load, no credential probe, no capability, and no
99
+ replacement for canonical discovery or addressing. Program search results carry
100
+ the same bounded `connectorTitle` per tool, so choosing an account or
101
+ environment costs no provider read. Context, not a ranking input and not proof
102
+ of live access.
103
+
104
+ Read-only lookup belongs in `connecta.search` inside the program; top-level
105
+ `search_tools` stays for explicit catalog inspection and approval-required
106
+ discovery. Both take the same arguments.
107
+
108
+ | Argument | What it does |
109
+ | --- | --- |
110
+ | `query` | two to four action/object terms; empty or whitespace-only browses |
111
+ | `connector` | scopes to one id, loading that catalog alone instead of fanning out across every configured connector. Set it when the integration is obvious, omit it when the right one is genuinely ambiguous |
112
+ | `safety` | `"readOnly"` for what generated code may call, `"approvalRequired"` for the complementary set that must cross `call_destructive_tool`, omitted or `"all"` for the complete configured catalog |
113
+ | `limit` / `offset` | page the ranked results; omit `limit` initially so the default eight-result page stays small |
114
+ | `includeSchemas` | `"compact"` for the rendered routing view, `"json"` for the exact schema, `"typescript"` for a function signature ([below](#typescript-signatures)) |
115
+ | `fullDescriptions` | unabridged tool purposes, at the obvious cost |
116
+
117
+ Neither `connector` nor `safety` grants authority or changes invocation
118
+ admission; they select what discovery shows and nothing else.
119
+
87
120
  `includeSchemas: "compact"` adds each match's input and any provider-declared
88
- output shape. When the provider declared none but an earlier successful call
89
- learned one, the same field carries the open observed schema beside
90
- `outputSchemaSource: "observed"`. That marker matters: observed fields and broad
91
- JSON types are routing evidence, not a provider contract, and every object field
92
- remains optional and open to unseen names. A provider declaration always wins.
93
- Bounded plain-object schemas also expose `inputKeys`,
94
- `requiredInputKeys`, and `outputKeys`; a zero-input object keeps
95
- `requiredInputKeys: []`, while an output object with no declared properties
96
- omits `outputKeys`. A truncated shape omits its corresponding list rather than
97
- repeating a large partial inventory. Matches carry declared
98
- behavior annotations. Lexical rank is only one signal: select a candidate whose
99
- required inputs are available, whose schema is complete enough for the call,
100
- and whose safety and available outputs fit the work. A reducer uses `outputKeys`
101
- before inspecting the value; it does not assume a collection is named `items`
102
- or `results`. When that shape is sufficient, call the returned address directly. Reserve schema
103
- expansion through `connecta.describe` for a search without schemas, an
104
- ambiguous compact shape, or exact
105
- constraints that require `format: "json"`.
106
-
107
- Observed schemas originate no provider traffic. A successful explicitly
108
- read-only call the user already made contributes names and broad types after
109
- Connecta unwraps the result. Arguments, scalar values, raw results, code,
110
- credentials, and errors are not retained, though property names may themselves
111
- be user-authored. Shapes merge in a 256-entry runtime cache for 24 hours under
112
- the exact tool definition that produced them. A changed definition, process
113
- restart, or Worker isolate eviction starts cold. Observation cannot fail the
114
- call, and the declared catalog remains the fallback.
115
-
116
- Compact search is deliberately a routing view, not a second copy of connector
117
- documentation. Tool purposes are capped at 160 characters, connector
118
- descriptions and property prose are omitted, required input fields render
119
- before optional ones, and each input or output shape is capped at 1,024 UTF-8
120
- bytes. Within that unchanged total, each enum node and each constraint
121
- annotation may spend at most 256 UTF-8 bytes. Numeric bounds, string length
122
- bounds, patterns, and formats render beside their type. A constraint that does
123
- not fit is dropped whole. If constraints push the full shape over 1,024 bytes,
124
- search retries the shape without them. Compact describe keeps declared
125
- constraints and property prose within its own 8,192-byte shape cap, sharing
126
- search's 2,000-visit rendering budget; a capped shape sets
127
- `inputSchemaTruncated` or `outputSchemaTruncated`, and `format: "json"` or
128
- JSON search returns the exact schema. A large enum keeps the longest whole-value prefix that fits, then
129
- adds `unknown` and a comment with the exact omitted-value count. An empty enum
130
- renders as the valid `never` type. A capped object becomes a valid
131
- required-first shape with `unknown` types; other shapes become
132
- `unknown /* truncated */`. Any cap marks the match with
133
- `inputSchemaTruncated` or `outputSchemaTruncated`; repeat the search with
134
- `includeSchemas: "json"` or use the existing describe path when exact
135
- constraints matter. `prefixItems` renders as a tuple, with the `items` type
136
- as its rest, an `unknown[]` rest when open, and no rest for `items: false`.
137
- `dependentSchemas` and `if`/`then`/`else` preserve the base shape and append
138
- `/* conditional */`, setting the truncation flag so the caller reads the exact
139
- JSON schema. `$dynamicRef` resolves a same-named definition like `$ref`; an
140
- unresolved dynamic reference renders as `unknown` with the truncation flag.
141
- These shapes share the same byte and work budgets. Small enums and both exact
142
- paths remain complete.
143
-
144
- ## Connector guide selection
145
-
146
- A connector may attach a deployment-owned guide as markdown, preserving the
121
+ output shape, plus `inputKeys`, `requiredInputKeys`, and `outputKeys` for
122
+ bounded plain objects. Where the provider declared no output shape but an
123
+ earlier successful call learned one, the same field carries that open observed
124
+ schema beside `outputSchemaSource: "observed"`. The marker is load-bearing:
125
+ observed names and broad types are routing evidence, never a provider contract,
126
+ and a provider declaration always wins. Observation originates no provider
127
+ traffic and cannot fail a call; the mechanism and its bounds are
128
+ [code mode](./code-mode.md#connectasearch)'s `S9`, and this surface only labels
129
+ what it returns.
130
+
131
+ Lexical rank is one signal among several: pick a candidate whose required inputs
132
+ are available, whose schema is complete enough for the call, and whose safety
133
+ and outputs fit the work. When that shape suffices, call the returned address
134
+ directly; reserve `connecta.describe` for a search without schemas, an ambiguous
135
+ compact shape, or exact constraints that need `format: "json"`.
136
+
137
+ Compact search is a routing view, not a second copy of connector documentation,
138
+ so it spends bytes on shape and none on prose: tool purposes cap at 160
139
+ characters, connector descriptions and property prose are dropped, and required
140
+ input fields render before optional ones. Both surfaces share one renderer, so
141
+ its byte and work budgets, its truncation renderings, and its handling of enums,
142
+ tuples, and conditional keywords are documented once under
143
+ [`connecta.search`](./code-mode.md#connectasearch) and
144
+ [`connecta.describe`](./code-mode.md#connectadescribe). The part a client must
145
+ act on is the flag: any cap sets `inputSchemaTruncated` or
146
+ `outputSchemaTruncated`, and that means repeat with `includeSchemas: "json"`, or
147
+ describe, when the exact constraints matter.
148
+
149
+ ### TypeScript signatures
150
+
151
+ `includeSchemas: "typescript"` replaces both schema fields with one
152
+ `signature`: the function `connecta.call(address, args)` resolves to, written
153
+ as a TypeScript type because agents write code against types more reliably
154
+ than against JSON Schema ([executor](https://github.com/UsefulSoftwareCo/executor)'s
155
+ discovery made the same bet).
156
+
157
+ ```ts
158
+ (args: { team: string; limit?: number /* <= 250 */ }) => Promise<unknown>
159
+ (args?: {}) => Promise<{ accounts: { stripe_context: string; livemode: boolean; name?: string }[] }>
160
+ (args: { team: string }) => Promise</* observed, not declared */ { cursor?: string; issues?: { id?: string }[] }>
161
+ ```
162
+
163
+ It is something to read, not something that runs: programs stay JavaScript,
164
+ and the refusal of erasable TypeScript in `execute_code` stands
165
+ ([ethos](../ethos.md#decisions)). The output half is the provider's declared
166
+ schema, else an `S9` observation that opens with the
167
+ `/* observed, not declared */` marker and keeps the `outputSchemaSource`
168
+ field, else `unknown` — runtime evidence never reads as a contract, even when
169
+ the signature is lifted out of its entry. A tool without parameters takes
170
+ `args?: {}`.
171
+
172
+ The walk, byte budgets, and truncation flags are compact's, each half
173
+ separately, and search, describe, and every discovery route share them, so the
174
+ same tool renders the same signature from `search_tools` and `connecta.search`.
175
+ What changes is the dialect: `number` for `integer`, `;` between members,
176
+ quoted non-identifier keys, `T[]` over a parenthesized union, `| null` for
177
+ OpenAPI `nullable` and type lists, index signatures for schema-valued
178
+ `additionalProperties`, `Record<string, unknown>` for an open object with no
179
+ declared properties, and JSDoc for property prose where describe keeps it.
180
+ Where compact would print a bare definition name or raw JSON, the signature
181
+ prints `unknown` with a marker and sets the flag: `/* recursive */` for a
182
+ `$ref` cycle, `/* unresolved */` for a missing target, `/* truncated */` past
183
+ depth four or an exhausted budget. `inputKeys` and friends come from the
184
+ declared schema exactly as they do for compact.
185
+
186
+ ## Connector guides and skills
187
+
188
+ A connector may attach a deployment-owned guide as markdown, keeping the
147
189
  original `usageGuide: string` configuration, or as
148
- `{ content, summary?, required? }`. The structured form does not register a
149
- connector or create a shared runtime template. `content` remains the markdown
150
- returned verbatim by `skills`; `summary` is normalized and refuses construction
151
- when it exceeds 120 characters. When it is absent, Connecta derives the same
152
- bounded fallback used by the skills listing: the first meaningful body
153
- paragraph, joined across physical Markdown line wraps and shortened at a
154
- sentence, clause, or word boundary, with a heading used only when the guide has
155
- no body. `required: true` is reserved for generic API wrappers and
156
- cross-operation conventions a complete downstream schema cannot express.
157
-
158
- Search and describe results keep the existing `guide: "connector:<id>"`
159
- pointer and add `guideSummary`. A matching tool also carries
160
- `guideRequired: true` and `guideRequiredReasons` when Connecta can prove review
161
- is necessary:
162
- `connector_required` for the explicit configuration above,
163
- `approval_required` for an unannotated or write-capable tool, and
164
- `schema_truncated` when a requested compact input or output shape was capped.
165
- The boolean is an instruction, not a server-side gate — nothing refuses the
166
- call, so the agent is told to fetch the guide before making it, for any reason
167
- listed. `connector_required` and `approval_required` survive exact schema
168
- expansion; `schema_truncated` is cleared by the describe that returns the exact
169
- shape, and describe reports whatever reasons remain in the same two fields.
170
- Otherwise it reads the
171
- bounded summary: connector-specific sequencing, units, pagination, aliases,
172
- and generic API conventions still require the guide when they affect the task,
173
- while a complete and unambiguous one-read schema proceeds directly.
174
- Guide lookup always uses an exact name returned by `skills({})`, search, or
175
- describe; callers do not manufacture `connector:<id>` from an unmarked
176
- connector.
177
-
178
- A connector-scoped lexical miss retains that connector's guide metadata under
179
- `queryAnalysis`. This matters for generic wrappers whose broad tool name does
180
- not contain endpoint vocabulary: a required guide remains discoverable before
181
- the caller falls back to an empty-query browse, rather than disappearing with
182
- the zero-tool page.
183
-
184
- The built-in `usage` skill is byte-identical across deployments and says to
185
- read it at most once per task. Connector guides remain scoped to the deployment
186
- that listed them, even when two deployments happen to use identical content.
187
- The always-loaded instructions and seven tool definitions own route selection,
188
- the fail-closed boundary, and the minimum guest syntax. The usage skill owns
189
- program selection detail, examples, runtime differences, and repair guidance.
190
- This split avoids two normative copies while preserving a valid first program
191
- for clients that never fetch the skill. Deployments without connector guides
192
- receive none of the short conditional guide pointers in their definitions.
193
-
194
- ## Task guidance
195
-
196
- `skills({ name: "investigate" })` provides on-demand guidance for purchase
197
- verification, experiment checks, and customer or deployment investigations.
198
- The execute description points to it when planning is unclear; routine reads need no additional guide fetch. It explains how to
199
- resolve app/account/environment, follow evidence across services, establish
200
- capability limits, and stop with a clear answer or a specific gap. It is shared
201
- guidance, not a saved workflow or a source of deployment-specific ids. Existing
202
- connector titles, purposes, and guides still own those distinctions.
203
-
204
- The usage skill keeps the executable mechanics. Its dependent-call example
205
- searches each operation separately, uses the page's `tools` array and canonical
206
- addresses, and reports unresolved evidence instead of inventing an address or
207
- querying another account. Its source runs against local fixtures in the QuickJS
208
- suite, including missing and approval-required evidence.
190
+ `{ content, summary?, required? }` — which registers no connector and creates no
191
+ shared runtime template. `content` is the markdown `skills` returns verbatim.
192
+ `summary` is normalized and refuses construction over 120 characters; absent,
193
+ Connecta derives the same bounded fallback the skills listing uses from the
194
+ guide's first meaningful body paragraph. `required: true` is reserved for
195
+ generic API wrappers and cross-operation conventions a complete downstream
196
+ schema cannot express.
197
+
198
+ Search and describe results carry a `guide: "connector:<id>"` pointer and a
199
+ `guideSummary`. A matching tool also carries `guideRequired: true` and
200
+ `guideRequiredReasons` when Connecta can prove review is necessary:
201
+
202
+ | Reason | Raised by | Survives exact schema expansion |
203
+ | --- | --- | --- |
204
+ | `connector_required` | the explicit `required: true` above | yes |
205
+ | `approval_required` | an unannotated or write-capable tool | yes |
206
+ | `schema_truncated` | a requested compact shape was capped | no — the describe that returns the exact shape clears it |
207
+
208
+ Describe reports whatever reasons remain in the same two fields. The boolean is
209
+ an instruction, not a server-side gate: nothing refuses the call, so the agent
210
+ is merely told to fetch the guide first, for any reason listed. Otherwise the
211
+ bounded summary decides — connector-specific sequencing, units, pagination,
212
+ aliases, and generic API conventions still need the guide when they affect the
213
+ task, while a complete and unambiguous one-read schema proceeds directly. Guide
214
+ lookup always uses an exact name returned by `skills({})`, search, or describe;
215
+ callers never manufacture `connector:<id>` from an unmarked connector.
216
+
217
+ A connector-scoped lexical miss keeps that connector's guide metadata under
218
+ `queryAnalysis`. This matters for generic wrappers whose broad tool name
219
+ contains no endpoint vocabulary: a required guide stays discoverable instead of
220
+ disappearing with the zero-tool page.
221
+
222
+ Two built-in skills are byte-identical across deployments. `usage` says to read
223
+ it at most once per task and owns program selection detail, examples, runtime
224
+ differences, and repair guidance; `investigate` is on-demand guidance for
225
+ purchase verification, experiment checks, and customer or deployment
226
+ investigations — shared guidance, not a saved workflow and not a source of
227
+ deployment-specific ids. The always-loaded instructions and tool definitions keep
228
+ route selection, the fail-closed boundary, and the minimum guest syntax, so a
229
+ client that never fetches a skill can still write a valid first program. Connector
230
+ guides stay scoped to the deployment that listed them even when two deployments
231
+ use identical content, and a deployment with no connector guides receives none of
232
+ the short conditional guide pointers in its tool definitions.
209
233
 
210
234
  ## Result representation
211
235
 
212
- For object results, `structuredContent` is the canonical full-fidelity value.
236
+ For object results, `structuredContent` is the canonical full-fidelity value and
213
237
  `content` carries the same complete value as compact JSON for clients that only
214
238
  consume text. Keeping both follows MCP's backwards-compatibility guidance;
215
- removing or summarizing the text copy is deferred until host-forwarding
216
- measurements demonstrate that supported clients do not need it.
217
-
218
- Plain-text guidance and errors remain text-only. A downstream MCP tool's native
219
- content blocks pass through when `call_tool` uses MCP result mode. When no
220
- text block exists and `structuredContent` is present, Connecta appends a text
221
- block containing its compact JSON, then applies the same content size guard.
222
- This preserves structured-only results, including `null`, arrays, and scalars.
223
- An existing text mirror stays unchanged; Connecta does not add another copy. Newly stashed JSON and
224
- downstream content envelopes use compact serialization, so `get_result` byte
225
- offsets and totals refer to that exact compact text.
226
-
227
- The direct-call stash keeps results for 15 minutes. `results.maxStashBytes`
228
- defaults to 8 MiB and `results.maxStashEntries` to 64 per `createConnecta`
229
- runtime, shared across all subjects and pools. Both accept non-negative safe
230
- integers; zero disables stashing. The byte budget counts the stored ASCII
231
- paging envelope, including base64 overhead, rather than only the result text.
232
- Capacity is reserved before each storage write, so concurrent requests cannot
233
- oversubscribe it. A full stash refuses new entries. A later stash attempt
234
- deletes expired entries before reusing their capacity; a failed deletion keeps
235
- the charge. These bounds cover writes by this runtime, not other processes,
236
- Worker isolates, or entries left by a previous runtime.
237
-
238
- Results belong to the authenticated subject whenever auth supplies a subject
239
- or user id, independently of activity configuration. The provider's namespace
240
- is used when present; otherwise the namespace is `connecta:auth:<provider kind>`.
241
- Keep subject ids distinct within that namespace. An explicit principal is the
242
- fallback subject when neither id is supplied. Open deployments and auth
243
- providers that supply no identity share one partition.
244
-
245
- New entries store UTF-8 bytes in a base64 envelope. After the KV read,
246
- `get_result` decodes only the requested byte range and a few boundary bytes;
247
- it does not encode the full text on every page. Storage still reads one full
248
- value. Pre-upgrade raw-text entries remain readable during their TTL using
249
- one full encoding per page. Offsets and `totalBytes` always describe the
239
+ dropping the text copy waits on host-forwarding measurements showing supported
240
+ clients do not need it. Plain-text guidance and errors stay text-only, and a
241
+ downstream MCP tool's native content blocks pass through in MCP result mode.
242
+ When no text block exists and `structuredContent` is present, Connecta appends a
243
+ text block carrying its compact JSON and then applies the same content size
244
+ guard, which preserves structured-only results including `null`, arrays, and
245
+ scalars. An existing text mirror stays unchanged; Connecta adds no second copy.
246
+ Newly stashed JSON and downstream content envelopes use compact serialization,
247
+ and a lone text block stashes as its own text, so `get_result` offsets and
248
+ totals describe exactly that text.
249
+
250
+ | Bound | Value |
251
+ | --- | --- |
252
+ | Stashed result TTL | 15 minutes |
253
+ | `results.maxStashBytes` | 8 MiB per `createConnecta` runtime |
254
+ | `results.maxStashEntries` | 64 per runtime |
255
+ | Top-level discovery result ceiling | 256,000 UTF-8 bytes |
256
+ | Downstream MCP `isError` text | 512 UTF-8 bytes plus an `…` marker |
257
+
258
+ The discovery ceiling counts text, `structuredContent`, and JSON escaping
259
+ together, because measuring one copy would advertise half the bytes the adapter
260
+ actually returns; error framing may shorten a bounded `isError` reason further to
261
+ fit the call's result cap on the same arithmetic. Both stash options accept
262
+ non-negative safe integers, zero disabling stashing, and are shared across all
263
+ subjects and pools; they count the stored ASCII paging envelope, base64 overhead
264
+ included, not only the result text. Capacity is reserved before each storage
265
+ write so concurrent requests cannot oversubscribe it, and a full stash refuses
266
+ new entries. A later attempt deletes expired entries before reusing their
267
+ capacity; a failed deletion keeps the charge. These bounds cover writes by this
268
+ runtime — not other processes, Worker isolates, or entries a previous runtime
269
+ left behind.
270
+
271
+ Results belong to the authenticated subject whenever auth supplies a subject or
272
+ user id, independently of activity configuration, under the provider's namespace
273
+ when it has one and `connecta:auth:<provider kind>` otherwise. Keep subject ids
274
+ distinct within that namespace. An explicit principal is the fallback subject
275
+ when neither id is supplied, and open deployments and auth providers that supply
276
+ no identity share one partition.
277
+
278
+ New entries store UTF-8 bytes in a base64 envelope split across storage keys,
279
+ 48 KiB of result text per chunk — widening past roughly 1.5 MB so no result
280
+ occupies more than 33 keys, because every chunk is also a write. `get_result`
281
+ reads and decodes only the chunks a page covers plus a few boundary bytes, so
282
+ paging a 1.2 MB result costs the same per page as paging a 300 KB one; it
283
+ neither encodes nor reads the whole result per page. Pre-upgrade entries remain readable during their TTL — the earlier
284
+ single-key envelope reads one full value per page, and raw text before that
285
+ also pays one full encoding. Offsets and `totalBytes` always describe the
250
286
  original UTF-8 text, not the envelope. A supplied offset inside a character
251
287
  moves back to its start; page ends also align to character boundaries, and a
252
288
  page smaller than one character widens just enough to make progress.
253
289
 
254
- A successfully stashed `call_tool` truncation notice carries both the historical `resultId` and an
255
- exact `nextAction: { tool: "get_result", arguments: { id, offset: 0 } }`. The
256
- handle is therefore
257
- directly actionable without copying an identifier out of prose. Program results
258
- and oversized discovery responses carry no such route — paging a program's
259
- return value is a refused shape, because a program can shrink anything before
260
- it returns.
261
-
262
- A refused stash or failed stash write cannot undo a downstream success. Both direct call tools
263
- return a truncated preview where usable, with a paging-unavailable notice and
264
- no `resultId` or paging action. Activity records success; the operator logger
265
- receives a fixed warning with the connector and tool, without storage error
266
- prose. For read-only work, reduce the result inside `execute_code`; repeating
267
- an approved write is not a way to recover its output. Other result-processing
268
- failures use a fixed `result_processing_failed` message and are never retryable.
269
-
270
- Downstream MCP `isError` text is bounded at its source to 512 UTF-8 bytes plus
271
- an `…` marker. Error framing may shorten it further to fit the call's result
272
- cap, counting JSON escaping and both copies in value mode.
273
-
274
- The top-level discovery ceiling is 256,000 UTF-8 bytes for the serialized tool
275
- result, including text, `structuredContent`, and JSON escaping. Measuring only
276
- one copy would advertise half the bytes the adapter actually returns.
277
-
278
290
  A per-call `timeoutMs` covers catalog resolution, admission, and connector
279
- execution with one deadline. The admission queue's own timeout may expire
280
- sooner, but it cannot extend the call deadline. Result processing happens after
281
- that deadline ends, because a completed downstream call must not turn into a
291
+ execution under one deadline. The admission queue's own timeout may expire
292
+ sooner but cannot extend the call deadline. Result processing happens after that
293
+ deadline ends, because a completed downstream call must not turn into a
282
294
  retryable timeout while Connecta prepares its response.
283
295
 
296
+ ### Truncated direct-call results
297
+
298
+ A `call_tool` or `call_destructive_tool` result over its cap — the connector's
299
+ `maxResultBytes`, else `calls.maxResultBytes`, else 24,000 bytes — comes back
300
+ as one text block. Its first line is the truncation notice, one line of compact
301
+ JSON; everything after the first newline is the preview:
302
+
303
+ ```text
304
+ {"truncated":true,"resultId":"…","totalBytes":161420,"hint":"This write already ran: do not call it again to see its result. Bytes 0-24000 of 161420 follow; page the rest with get_result using nextAction.","nextAction":{"tool":"get_result","arguments":{"id":"…","offset":24000}}}
305
+ {"ts":"2026-09-16T21:31:11.759Z","actor":"sam.ortiz@example.com",…
306
+ ```
307
+
308
+ The notice leads because clients cut oversized results from the end, and a
309
+ notice at the tail is the part they drop. Claude Code (measured on 2.1.280, from
310
+ its bundled source and with `claude -p` against a probe server) passes an MCP
311
+ result through untouched while its text blocks total at most 50,000 characters.
312
+ One character more and it replaces the whole result with a pointer to a spill
313
+ file plus the first 2,000 characters of `JSON.stringify(content, null, 2)`,
314
+ whatever `MAX_MCP_OUTPUT_TOKENS` says. Once the characters exceed twice
315
+ `MAX_MCP_OUTPUT_TOKENS` (25,000 by default, so again 50,000) it also counts real
316
+ tokens against that limit, and a result over it arrives as an error carrying no
317
+ content at all — no preview, no notice. The old layout — a
318
+ 50,000-byte preview, then the notice — crossed the first line every time, so an
319
+ agent without file tools never saw the handle; in the eval's write task two of
320
+ three models re-ran a billable export three times trying to read its result.
321
+
322
+ The default cap sits under both lines with room to spare. Preview plus notice
323
+ stays under 25,000 bytes, which is under 50,000 characters for any text, because
324
+ a character is at least one UTF-8 byte, and under 25,000 tokens for any text,
325
+ because a token covers at least one byte. A `get_result` page obeys the same
326
+ cap, and 24,000 matches the program result boundary in
327
+ [code mode](./code-mode.md). A deployment whose
328
+ clients take more can raise the cap, and one whose clients take less can lower
329
+ it, per connector if need be; a result between 24,000 and 50,000 bytes that
330
+ used to arrive whole now pages. A client that spills rather than rejects keeps
331
+ the notice whatever its threshold, since the notice is the first few hundred
332
+ characters. One that rejects outright — Claude Code with
333
+ `MAX_MCP_OUTPUT_TOKENS` set below 12,500 — needs a cap under twice its limit.
334
+
335
+ The preview is the head of what `get_result` pages, cut on a character
336
+ boundary, so `nextAction.arguments.offset` continues exactly where it stops. A
337
+ lone text block — including the one synthesized from `structuredContent` — is
338
+ measured, stashed, previewed, and paged as its text: wrapping it in a content
339
+ envelope added nothing but a second layer of JSON escaping, so a JSON payload
340
+ used to preview as `[{"type":"text","text":"{\"ts\":…` and every page carried
341
+ the escaping twice. Several text blocks keep the serialized envelope as the one
342
+ string measured, stashed, and paged, because their boundaries live there; they
343
+ preview as their text joined by newlines, and their next action starts at
344
+ offset 0. A content array carrying non-text blocks returns the notice alone,
345
+ because the head of a half-written base64 image helps nobody. In value mode the
346
+ notice is `data` itself, with no preview and a next action at offset 0.
347
+
348
+ The hint says what to do next. For a call not explicitly annotated read-only it
349
+ opens with “This write already ran: do not call it again to see its result.”
350
+ The approved write happened; repeating it to look again is how one export
351
+ becomes three. For an explicitly read-only call, repeating is the better route,
352
+ so the hint offers it first: to find something specific, repeat the read inside
353
+ `execute_code` with `connecta.call` and filter or search the result there; to
354
+ read it in full, page with `get_result`. Inside a program the inline cap does
355
+ not apply to a host call's result, only to what the program returns (`L6` in
356
+ [code mode](./code-mode.md) bounds a host call far higher), so the reduction
357
+ sees the whole result at once. When the hint named paging alone, the eval's
358
+ weakest model read a 185 KB CI log in 24,000-byte pages, skipped the range
359
+ holding the real failure, and named the flaky test near the top; before the
360
+ notice was visible, it had reduced the log in a program and found the failure.
361
+ `nextAction` stays the page handle for either kind of call, because paging is
362
+ the one next step connecta can spell out exactly, while a reduction is a
363
+ program the agent has to write. `resultId` stays beside the exact `nextAction`,
364
+ so the handle is actionable without copying an identifier out of prose.
365
+ Program results and oversized discovery responses carry no such route: paging a
366
+ program's return value is a refused shape, because a program can shrink
367
+ anything before it returns.
368
+
369
+ ### Paging with get_result
370
+
371
+ A page is the truncated result's shape again: one text block whose first line
372
+ is a JSON header and whose remainder is the page, raw.
373
+
374
+ ```text
375
+ {"resultId":"…","offset":24000,"bytes":24000,"totalBytes":161420,"hasMore":true,"nextAction":{"tool":"get_result","arguments":{"id":"…","offset":48000}}}
376
+ {"ts":"2026-09-17T23:42:55.357Z","actor":"noor.haddad@example.com",…
377
+ ```
378
+
379
+ `bytes` is what this page returned; `hasMore` is false on the last page, which
380
+ carries no `nextAction`. The page is the stashed text as-is — a lone text
381
+ block's own text, a value's compact JSON, several blocks' serialized content
382
+ array — and never a JSON string holding it. Pages used to arrive as
383
+ `{ offset, nextOffset?, totalBytes, text }`, which escaped every quote and
384
+ newline in the page: a second layer over JSON payloads that made them hard to
385
+ read and swelled a 20,000-byte page of JSON lines to 25,000 characters or more.
386
+
387
+ `maxBytes` is an upper bound clamped to the inline cap of the call that stashed
388
+ the result — the connector's override when it had one, recorded in the stash
389
+ entry — and a clamped request is answered, not refused, with `bytes` saying how
390
+ much came back. Clients cut an oversized page exactly as they cut the original
391
+ result. In the eval, agents that could finally see the handle asked for pages of
392
+ 50,000 bytes and more, and Claude Code rejected every one of those answers
393
+ outright, leaving an agent that had just been told not to repeat a write with
394
+ nothing to read. So no response to a truncated call or to a page request
395
+ exceeds the cap plus a header of a few hundred bytes. Value mode's unpageable
396
+ preview is cut short enough that its JSON escaping still fits. Entries stashed
397
+ before the cap was recorded page at the deployment cap.
398
+
399
+ A refused or failed stash write cannot undo a downstream success. Both call tools
400
+ then return the same layout with a paging-unavailable notice and no `resultId`
401
+ or paging action — the notice first, then the preview where one is usable, and
402
+ for a write the same warning not to repeat it. Activity records success, and
403
+ the operator logger receives a fixed warning naming the connector and tool
404
+ without storage error prose. For read-only work, reduce the result inside
405
+ `execute_code` — repeating an approved write is not a way to recover its output. Other result-processing failures use a fixed
406
+ `result_processing_failed` message and are never retryable.
407
+
284
408
  ## Lexical discovery
285
409
 
286
410
  `search_tools` tokenizes tool names and descriptions at punctuation and
287
- camel-case boundaries. Exact whole-token matches carry the most weight; a small
288
- set of inflectional variants preserves singular/plural and verb-form recall
289
- without allowing arbitrary mid-word substring matches. Ranking weights each
290
- query term by its document frequency across the available catalogs in that
291
- search, so a rare domain term outranks a ubiquitous action while action terms
292
- still distinguish `get`, `list`, `search`, and write operations. The scorer
293
- always evaluates useful near-matches instead of letting one broad all-term
294
- description hide them. Complete matches rank before ordinary partial matches;
295
- a partial candidate whose complete normalized tool name occurs in the
296
- normalized raw query competes with complete matches by score, and other
297
- candidates covering at least two terms fill the remaining page after them.
298
- Conversational cleanup applies only to scoring terms, never to the exact-name
411
+ camel-case boundaries. Exact whole-token matches carry the most weight, and a
412
+ small set of inflectional variants preserves singular/plural and verb-form
413
+ recall without admitting arbitrary mid-word substrings. Each query term is
414
+ weighted by its document frequency across the catalogs available to that search,
415
+ so a rare domain term outranks a ubiquitous action while action terms still
416
+ distinguish `get`, `list`, `search`, and write operations. Complete matches rank
417
+ before ordinary partial matches; a partial candidate whose complete normalized
418
+ tool name occurs in the normalized raw query competes with complete matches by
419
+ score, and other candidates covering at least two terms fill the remaining page.
420
+ Conversational cleanup applies to scoring terms only, never to the exact-name
299
421
  phrase check. If no tool covers every non-conversational term, the same scorer
300
422
  preserves the wider any-term fallback and marks the result
301
423
  `matchMode: "partial"`.
302
424
 
303
- Returned tool rows expose neither lexical scores nor per-result query coverage.
304
- The mixed complete/partial scorer still ranks rare domain terms, action terms,
305
- and exact tool-name phrases. Select from the returned purpose, address, schema,
306
- safety, and output shape. Page-level `queryAnalysis` remains the recovery path
307
- when no single result covers every term or no match exists.
308
-
309
- Only an empty or whitespace-only query browses. A non-empty query that
310
- normalizes to no ASCII lexical terms returns no tools instead of unrelated
311
- browse results. Its bounded `queryAnalysis.unmatchedTerms` contains the clipped
312
- raw query and guidance asks for ASCII action/object terms. A mixed query still
313
- searches with its ASCII terms; unsupported characters do not become false
314
- matches or per-tool coverage terms.
315
-
316
- Every partial or no-match lexical search also returns bounded page-level
317
- `queryAnalysis`; an all-term result needs no recovery advice.
318
- `representedTerms` occur in the current page, `otherResultTerms` occur only in
319
- another result, and
320
- `unmatchedTerms` have no lexical match in the catalogs that answered. Partial
321
- results explain that no single tool covered every term and recommend splitting
322
- distinct intents. A true negative says that no matching capability is
323
- configured and recommends refining, connector-scoping, or browsing; when a
324
- connector catalog was unavailable, the response includes
325
- `unavailableConnectorCount` instead of making that stronger claim. A no-match
326
- query whose terms name a configured connector's `id` or `title` never makes it
327
- either: connector identity is not in the lexical index — indexing it would move
328
- ranking for every query that already matches tools — so instead the guidance on
329
- an unscoped miss names up to three such connectors by ID and sends the caller
330
- to a scoped browse. Identity affects that one sentence and nothing else: no
331
- ranking, no result, and no new field. A search
332
- explicitly scoped to that unavailable connector also receives `catalogError` —
333
- the bounded classified failure (`code`, `message`, `retryable`, and any
334
- `retryAfterMs`) so the caller can tell a transient outage from one a deployment
335
- operator must clear. It carries nothing else the call-path classifier knows: a
336
- discovery read is not a call. Unscoped searches keep the count only — one
337
- connector's failure is not another search's context. An empty query browses
338
- rather than searches, so it reports no term analysis — except when the scope
339
- itself failed, where the same fields apply. A browse scoped to an unavailable
340
- connector carries `unavailableConnectorCount`, `catalogError`, and guidance,
341
- and an unscoped browse again carries the count alone. A browse scoped to an ID
342
- that is not configured at all carries `connectorScope`, `unknownConnector`, and
343
- the same omit-the-connector guidance the term-bearing path gives — nothing was
344
- attempted, so there is no count and no `catalogError` — and it names no
345
- connector but the one the caller supplied. A connector that correctly exposes
346
- no tools still reports no analysis, so the two do not serialize alike. The
347
- advice to browse a connector with an empty query must not land in silence that
348
- reads like a connector with no tools. Analysis
349
- from a connector-filtered search includes `connectorScope` and speaks only
350
- about that connector; `unknownConnector` distinguishes an unconfigured ID from
351
- a known connector with no match. Analysis covers at most eight distinct terms
352
- of at most 64 displayed characters each, marks longer input `truncated`, and
353
- never changes lexical ranking.
425
+ Tool rows expose neither lexical scores nor per-result query coverage. Select
426
+ from the returned purpose, address, schema, safety, and output shape; page-level
427
+ `queryAnalysis` is the recovery path when no single result covers every term or
428
+ none exists. It reports `representedTerms` (in the current page),
429
+ `otherResultTerms` (only in another result), and `unmatchedTerms` (no lexical
430
+ match in the catalogs that answered), covers at most eight distinct terms of at
431
+ most 64 displayed characters each while marking longer input `truncated`, and
432
+ never changes lexical ranking. A non-empty query that normalizes to no ASCII
433
+ lexical terms returns no tools rather than unrelated browse results, with the
434
+ clipped raw query in `unmatchedTerms`; a mixed query searches with its ASCII
435
+ terms, and unsupported characters become neither false matches nor coverage
436
+ terms.
437
+
438
+ What the analysis says next depends on why the page is thin:
439
+
440
+ | Case | Fields and guidance |
441
+ | --- | --- |
442
+ | Partial match | no single tool covered every term; split distinct intents |
443
+ | True negative | no matching capability is configured; refine, connector-scope, or browse |
444
+ | True negative naming a configured connector | the same, plus up to three such connectors named by id and a pointer to a scoped browse |
445
+ | Unscoped search or browse with some connector unavailable | `unavailableConnectorCount` alone |
446
+ | Scoped to an unavailable connector | `unavailableConnectorCount`, `catalogError`, guidance |
447
+ | Scoped to an unconfigured id | `connectorScope`, `unknownConnector`, omit-the-connector guidance |
448
+ | Connector configured and genuinely exposing no tools | no analysis |
449
+
450
+ Three of those rows earn their asymmetry. An unavailable catalog downgrades the
451
+ claim, because a search may not report that nothing is configured when it does
452
+ not know. Connector identity is deliberately absent from the lexical index —
453
+ indexing it would move ranking for every query that already matches tools — so a
454
+ query naming one gets exactly one extra sentence and nothing else: no ranking
455
+ change, no result, no new field. And a browse scoped to an unavailable connector
456
+ must not serialize like a connector that genuinely has no tools, or the advice to
457
+ browse lands in silence that reads like an answer.
458
+
459
+ `catalogError` is the bounded classified failure — `code`, `message`,
460
+ `retryable`, and any `retryAfterMs` — so a caller can tell a transient outage
461
+ from one an operator must clear, and nothing else the call-path classifier knows,
462
+ because a discovery read is not a call. Only an explicitly scoped search gets it:
463
+ one connector's failure is not another search's context, and a scope that was
464
+ never configured gets neither it nor a count, since nothing was attempted.
354
465
 
355
466
  ## Authorization recovery
356
467
 
@@ -391,113 +502,79 @@ The tool accepts no secret. `force` applies only to OAuth and may discard its
391
502
  stored grant before restarting consent. Static credential values are written
392
503
  only through the same-origin interactive-user credential route, and only for a
393
504
  connector visible to that user with the relevant shared or personal management
394
- permission. OAuth start, including `force`, requires that permission too. Core
395
- callbacks work without the UI for authorized interactive callers. After OAuth consent or a human update, retry
396
- the original operation; a static update is read from the vault on the next call
397
- and needs no redeploy.
505
+ permission; OAuth start, `force` included, requires that permission too. Core
506
+ callbacks work without the UI for authorized interactive callers. After OAuth
507
+ consent or a human update, retry the original operation; a static update is read
508
+ from the vault on the next call and needs no redeploy.
398
509
 
399
510
  ## Routing recovery
400
511
 
401
- Predictable local refusals carry structured recovery on both result modes.
402
- An unknown connector suggests an unscoped discovery query derived from the
512
+ Predictable local refusals carry structured recovery on both result modes. An
513
+ unknown connector suggests an unscoped discovery query derived from the
403
514
  attempted tool name; an unknown tool scopes the same query to the connector that
404
- answered. The suggested route follows the route the caller took: `tool:
405
- "search_tools"` for a top-level call, `function: "connecta.search"` with the same
406
- arguments when the miss happened inside `execute_code`, which has no way to call
407
- a tool. A read path that reaches an unannotated, write-capable, or destructive
408
- tool returns `nextAction` for `call_destructive_tool` with the canonical
409
- address. Nothing is executed by these records.
410
-
411
- `connecta.describe` keeps failures inline so one miss cannot discard the other
412
- schemas. Each failed entry keeps its human `error` and adds `errorDetails` with
413
- the equivalent invocation `code` and `retryable`. Address and tool misses use
414
- the same route-aware discovery action above. A close tool-name miss on a known
415
- connector may also carry `suggestions`: at most three deterministically ranked
416
- canonical addresses, with no scores or descriptions. An unknown connector
417
- stays unscoped and has no suggestions. A catalog-load failure carries only
418
- `code`, bounded `message`, `retryable`, and any `retryAfterMs`; discovery does
419
- not inherit later additions to the call-failure envelope.
420
-
421
- That route echoes the caller's own arguments back only while they fit a
422
- 512-byte budget, and then whole — never clipped. An error envelope is not
423
- size-guarded the way a result is, so an unbounded echo would let a large
424
- argument object produce a refusal many times the deployment's result cap, on
425
- both `call_tool` and program calls through `connecta.call`. Over budget, `args` is absent and the `purpose` says to
426
- re-send what was just sent: the agent already holds its own arguments, and half
427
- of them would describe a call nobody made.
428
-
429
- The address gets the same budget and the opposite rule: 512 bytes, clamped
430
- with a trailing `…` rather than dropped. It is caller-authored too — an
431
- invented one can be any length — and it reaches the error message *and* the
432
- recovery query, each of which lands in both the text content and
433
- `structuredContent`; unbounded, a 50 KB typo produced a 200 KB refusal under a
434
- 1 KB result cap. Dropping it is not an option the way dropping arguments is:
435
- the address is the thing being corrected, a clipped one still identifies the
436
- mistake, and a short one — every real one — comes back exact and untagged.
437
-
438
- Unknown `get_result.id`, `authorize_connector.connector`, and `skills.name`
439
- echoes use the same 512-byte clamp. `search_tools.connector` instead rejects
440
- values over 512 UTF-8 bytes with `invalid_args` before catalog lookup, so a
441
- clipped scope can never select a different connector. A failed result-storage
442
- read returns typed `unavailable` without exposing backend error text.
443
-
444
- `call_destructive_tool` accepts an optional
445
- `reason` of at most 500 characters for the host's human approval view. It is
446
- outer-call context only: Connecta neither treats it as authority nor passes it
447
- to the downstream connector, and an empty or whitespace-only one is read as no
448
- reason rather than as a reason to refuse the call.
449
-
450
- Activity carries an optional coarse `friction` class: `tool_not_found`,
451
- `schema_retry`, `destructive_reroute`, `auth_required`, or `result_too_large`.
452
- It is derived from the typed error code, except on the one call that has no
453
- error code to derive from: a result too large to return inline is friction for
454
- the agent while remaining `outcome: "success"`. That applies to a `call_tool`
455
- result, the only source of `result_too_large` friction. (Activity stored by
456
- older releases may still carry the retired `batch_call` source; nothing writes
457
- it today.)
458
- An oversized *discovery* response and an oversized program return are shaped
459
- differently and produce none, and an `errorCode` is written only when the call
460
- actually failed. The category adds no arguments, results, search text,
461
- generated code, credentials, or raw errors.
462
-
463
- An address whose connector does not exist is recorded too, as written, provided
464
- it has the `<connectorId>.<toolName>` shape at all — a string that never split
465
- into the two fields activity keeps still records nothing. A hallucinated
466
- connector id is the most common address mistake, and an operator reading
467
- activity should see it; addresses are already a first-class activity field, so
468
- nothing new is retained.
469
-
470
- What *is* new is that those fields now hold caller-authored text, so the
471
- recording seam clamps them: `connectorId` and `toolName` at 128 UTF-8 bytes
472
- each, `address` at 257, with a `…` marker. Far past any real id or tool name,
473
- and far short of a 40 KB invented one. The clamp is structural rather than a
474
- policy the writer applies, because "payload-free by construction" has to mean
475
- the event type has nowhere to put a payload — a 40 KB connector id is a payload
476
- wearing an id's clothing. Clamped rather than skipped: the invented id is
477
- exactly what an operator needs to see, and its first 128 bytes say as much
478
- about the mistake as all 40,000 would.
515
+ answered. The suggested route follows the route the caller took:
516
+ `tool: "search_tools"` for a top-level call, `function: "connecta.search"` with
517
+ the same arguments when the miss happened inside `execute_code`, which has no
518
+ way to call a tool. A read path that reaches an unannotated, write-capable, or
519
+ destructive tool returns `nextAction` for `call_destructive_tool` with the
520
+ canonical address. Nothing is executed by these records.
521
+
522
+ `connecta.describe` keeps its failures inline instead, so one miss cannot
523
+ discard the other schemas; each failed entry carries a human `error`, typed
524
+ `errorDetails`, the same route-aware discovery action, and — on a close
525
+ tool-name miss against a known connector — at most three deterministically
526
+ ranked canonical `suggestions` with no scores and no descriptions. A
527
+ catalog-load failure carries only `code`, bounded `message`, `retryable`, and
528
+ any `retryAfterMs`: discovery does not inherit later additions to the
529
+ call-failure envelope. The per-entry rules are
530
+ [code mode](./code-mode.md#connectadescribe)'s `S4`.
531
+
532
+ ### Echo budgets
533
+
534
+ An error envelope is not size-guarded the way a result is, and every echoed byte
535
+ lands twice — in the text content and in `structuredContent` — so a 50 KB
536
+ invented address once produced a 200 KB refusal against a 1 KB result cap. Every
537
+ caller-authored string a refusal repeats therefore gets 512 UTF-8 bytes, and
538
+ what happens over budget differs by field.
539
+
540
+ | Field | Over 512 UTF-8 bytes |
541
+ | --- | --- |
542
+ | `args` on `call_tool` and `connecta.call` | dropped whole; `purpose` says to re-send what was just sent |
543
+ | the attempted address | clamped with a trailing `…` |
544
+ | unknown `get_result.id`, `authorize_connector.connector`, `skills.name` | clamped the same way |
545
+ | `search_tools.connector` | rejected with `invalid_args` before catalog lookup |
546
+
547
+ Arguments go all or nothing because the agent already holds what it sent, and
548
+ half of it would describe a call nobody made. The address gets the opposite rule
549
+ because it is the thing being corrected: a clipped one still identifies the
550
+ mistake, and a short one — every real one — comes back exact and untagged. A
551
+ scope is rejected outright because a clipped one could select a different
552
+ connector. A failed result-storage read returns typed `unavailable` without
553
+ exposing backend error text.
554
+
555
+ Activity records each of these refusals with the coarse `friction` class derived
556
+ from its typed error code — `tool_not_found`, `schema_retry`,
557
+ `destructive_reroute`, `auth_required` — and clamps the caller-authored
558
+ `connectorId`, `toolName`, and `address` it keeps, both under
559
+ [code mode](./code-mode.md#activity)'s `V2` and `V3`. A `call_tool` result too
560
+ large to return inline is the one exception: it is friction
561
+ (`result_too_large`) on a call whose `outcome` is still `"success"`, so it
562
+ carries no `errorCode`.
479
563
 
480
564
  ## Argument recovery
481
565
 
482
- A remote MCP tool's advertised `inputSchema` is checked in the shared
483
- invocation path before admission and provider dispatch. A mismatch is the
484
- non-retryable `invalid_args`, consistently across `call_tool`,
485
- `call_destructive_tool`, generated-code failures, and rejected promises. The error
486
- names the connector and operation and carries bounded `validation.issues`:
487
- JSON Pointer `path`, schema-keyword `code`, and expected shape. Submitted
488
- values are never copied into those findings.
489
-
490
- At most three findings are returned; `validation.truncated` says when more
491
- exist. `nextAction` points to discovery scoped to the same connector and tool
492
- name when the compact schema is needed — routed like any other miss, so a
493
- program is sent to `connecta.search` and a top-level call to `search_tools` —
494
- while `retry` says to correct the listed arguments and reissue the original
495
- operation. A declared property reports only its failed schema keyword, while a
496
- truly undeclared property reports `additionalProperties`; validator-internal
497
- duplicate `additionalProperties` branches never reach the caller. A schema the local
498
- validator cannot evaluate passes through to the provider. Provider error prose
499
- is not parsed or guessed, so an unknown format remains
500
- `connector_call_failed`.
501
-
502
- Describe's nearby-address list uses the same three-item recovery bound. It
503
- contains addresses only; it never serializes ranking scores or result prose.
566
+ A remote MCP tool's advertised `inputSchema` is checked in the shared invocation
567
+ path before admission and provider dispatch, so a mismatch is the non-retryable
568
+ `invalid_args` identically on `call_tool`, `call_destructive_tool`,
569
+ generated-code failures, and rejected promises. The error names the connector and
570
+ operation and carries `validation.issues`: JSON Pointer `path`, schema-keyword
571
+ `code`, and expected shape, with submitted values never copied into a finding. At
572
+ most three are returned, and `validation.truncated` says when more exist — the
573
+ same three-item bound describe's nearby-address list uses.
574
+
575
+ `nextAction` points to discovery scoped to the same connector and tool name when
576
+ the compact schema is needed, routed like any other miss: a program to
577
+ `connecta.search`, a top-level call to `search_tools`. `retry` says to correct
578
+ the listed arguments and reissue the original operation. Which keyword a finding
579
+ names, and what the local validator declines to evaluate, is
580
+ [code mode](./code-mode.md#errors)'s `E8`.