@zackbart/connecta 0.10.4 → 0.10.6

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 (71) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/dist/activity.d.ts +11 -1
  3. package/dist/activity.d.ts.map +1 -1
  4. package/dist/activity.js +44 -3
  5. package/dist/activity.js.map +1 -1
  6. package/dist/catalog-service.d.ts +40 -0
  7. package/dist/catalog-service.d.ts.map +1 -1
  8. package/dist/catalog-service.js +97 -15
  9. package/dist/catalog-service.js.map +1 -1
  10. package/dist/errors.d.ts +48 -1
  11. package/dist/errors.d.ts.map +1 -1
  12. package/dist/errors.js +67 -0
  13. package/dist/errors.js.map +1 -1
  14. package/dist/execute.d.ts +72 -0
  15. package/dist/execute.d.ts.map +1 -1
  16. package/dist/execute.js +163 -10
  17. package/dist/execute.js.map +1 -1
  18. package/dist/index.d.ts +15 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +6 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/invocation.d.ts +9 -2
  23. package/dist/invocation.d.ts.map +1 -1
  24. package/dist/invocation.js +59 -29
  25. package/dist/invocation.js.map +1 -1
  26. package/dist/meta-tools.d.ts +12 -3
  27. package/dist/meta-tools.d.ts.map +1 -1
  28. package/dist/meta-tools.js +185 -30
  29. package/dist/meta-tools.js.map +1 -1
  30. package/dist/operator-ui/generated.d.ts +1 -1
  31. package/dist/operator-ui/generated.d.ts.map +1 -1
  32. package/dist/operator-ui/generated.js +1 -1
  33. package/dist/operator-ui/generated.js.map +1 -1
  34. package/dist/registry.d.ts +11 -0
  35. package/dist/registry.d.ts.map +1 -1
  36. package/dist/registry.js +5 -2
  37. package/dist/registry.js.map +1 -1
  38. package/dist/routes/mcp.d.ts.map +1 -1
  39. package/dist/routes/mcp.js +9 -0
  40. package/dist/routes/mcp.js.map +1 -1
  41. package/dist/routes/shared.d.ts +4 -0
  42. package/dist/routes/shared.d.ts.map +1 -1
  43. package/dist/routes/shared.js.map +1 -1
  44. package/dist/skills.d.ts +1 -1
  45. package/dist/skills.d.ts.map +1 -1
  46. package/dist/skills.js +1 -1
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/documentation/code-mode.md +125 -34
  50. package/documentation/meta-tools.md +91 -9
  51. package/documentation/rich-output-design.md +212 -0
  52. package/ethos.md +17 -19
  53. package/examples/worker/README.md +11 -3
  54. package/examples/worker/src/d1-activity-row.ts +40 -0
  55. package/examples/worker/src/d1-activity.ts +3 -2
  56. package/package.json +1 -1
  57. package/src/activity.ts +64 -3
  58. package/src/catalog-service.ts +166 -26
  59. package/src/errors.ts +102 -1
  60. package/src/execute.ts +240 -10
  61. package/src/index.ts +22 -0
  62. package/src/invocation.ts +59 -17
  63. package/src/meta-tools.ts +239 -37
  64. package/src/operator-ui/browser.ts +10 -2
  65. package/src/operator-ui/generated.ts +1 -1
  66. package/src/registry.ts +5 -2
  67. package/src/routes/mcp.ts +9 -0
  68. package/src/routes/shared.ts +4 -0
  69. package/src/skills.ts +1 -1
  70. package/src/version.ts +1 -1
  71. package/templates/node/package.json +1 -1
package/dist/skills.d.ts CHANGED
@@ -10,7 +10,7 @@ export declare const CONNECTA_INSTRUCTIONS = "Connecta exposes integrations behi
10
10
  */
11
11
  export declare const CODE_FIRST_INSTRUCTIONS = "Connecta exposes integrations behind seven meta-tools, and execute_code is the primary one: write an async arrow function and use connecta.search (empty query browses every catalog), connecta.describe, connecta.call, and connecta.batch inside it for discovery, two or more calls, dependent steps, loops, joins, and reducing large results before they reach you. For a single read at an unknown address, search_tools with 2\u20134 distinctive action/object terms and includeSchemas=\"compact\", then one call_tool \u2014 a lone cold call is cheaper direct than through a program. Use call_destructive_tool individually for unannotated, write-capable, or destructive tools; authorize_connector follows auth_required; get_result follows truncation. If this routing is unfamiliar, fetch skills({ name: \"usage\" }).";
12
12
  export declare const USAGE_SKILL = "# Connecta usage\n\n## Choose the smallest execution tool\n\nUse exact addresses returned by discovery; never invent one. Search with 2\u20134 distinctive action/object terms rather than the full request, and omit `limit` initially so the default page stays small.\n\n- Unknown address: `search_tools({ query, includeSchemas: \"compact\" })`; every match then includes its input shape plus any declared output shape and annotations.\n- Compact shape still ambiguous: `describe_tools({ addresses: [...] })`; use `format: \"json\"` only for exact constraints.\n- One explicitly read-only call: `call_tool`.\n- Two to ten independent explicitly read-only calls: `batch_call`.\n- Dependent read-only calls, loops, joins, branching, or large-result reduction: `execute_code` when available.\n- Any unannotated, write-capable, or destructive call: `call_destructive_tool`, individually and only after reviewing its schema and consequences.\n- Truncated result: retry with `fields` when possible; otherwise page it with `get_result`.\n- `auth_required`: use `authorize_connector`, give its recovery handoff to the operator, then retry the original call.\n\nUse `list_connectors({ probe: false })` for a fast observed-health inventory; use `probe: true` only to diagnose live health or authorization.\n\n## Code mode\n\nUnknown addresses plus dependent calls: search inside the run, not in an outer `search_tools`. Parallelize independent calls with `Promise.all` or `connecta.batch`.\n\nConnector namespace calls and `connecta.call` use the same read-only gate and throw on downstream errors. Catch only failures the workflow can handle; let authorization failures return to the agent for recovery.\n\nSkip code mode for one call, calls suited to `batch_call`, or tools lacking `readOnlyHint: true`. Return only the needed reduction.\n";
13
- export declare const CODE_FIRST_USAGE_SKILL = "# Connecta usage\n\n## The surface\n\nSeven tools: `execute_code`, `search_tools`, `call_tool`, `call_destructive_tool`, `authorize_connector`, `get_result`, `skills`. Broad discovery and multi-call work live inside a program rather than in top-level tools.\n\n## Choose the smallest execution tool\n\nUse exact addresses returned by discovery; never invent one. Search with 2\u20134 distinctive action/object terms rather than the full request.\n\n- One read at an unknown address: `search_tools({ query, includeSchemas: \"compact\" })`, then `call_tool` once. A lone cold call is cheaper direct than through a program.\n- Anything wider \u2014 two or more calls, dependent steps, loops, joins, branching, browsing a whole catalog, or a result that must be reduced: one `execute_code` run.\n- Any unannotated, write-capable, or destructive call: `call_destructive_tool`, individually and only after reviewing its schema and consequences. Generated code cannot make one.\n- Truncated result: retry with `fields` when possible; otherwise page it with `get_result`.\n- `auth_required`: use `authorize_connector`, give its recovery handoff to the operator, then retry the original call.\n\n## Inside a program\n\nOne async arrow function. The only capabilities are one global per connector (`<connectorId>.<toolName>(args)`), the four `connecta` functions, and `console.log`.\n\n- What exists: `connecta.search({})` browses every catalog; add `safety: \"readOnly\"` for only calls the program can execute, and `connector: \"<id>\"` to browse one. This filters discovery results, not authority, and each match carries its `address` and annotations.\n- Exact schemas for known addresses: `connecta.describe({ addresses: [...] })`; `format: \"json\"` only for exact constraints.\n- Two to ten independent calls: `connecta.batch([...])`. Each outcome is `{ address, ok: true, data }` or `{ address, ok: false, error, errorDetails: { code, retryable } }`, which is also how a program tells a policy refusal from a transient failure.\n- Search inside the run rather than searching first, and return only the reduction the answer needs \u2014 never raw payloads.\n- Only tools annotated `readOnlyHint: true` are reachable; the read-only gate, credentials, and admission are enforced below the sandbox, so nothing a program does widens what it can reach.\n";
13
+ export declare const CODE_FIRST_USAGE_SKILL = "# Connecta usage\n\n## The surface\n\nSeven tools: `execute_code`, `search_tools`, `call_tool`, `call_destructive_tool`, `authorize_connector`, `get_result`, `skills`. Broad discovery and multi-call work live inside a program rather than in top-level tools.\n\n## Choose the smallest execution tool\n\nUse exact addresses returned by discovery; never invent one. Search with 2\u20134 distinctive action/object terms rather than the full request.\n\n- One read at an unknown address: `search_tools({ query, includeSchemas: \"compact\" })`, then `call_tool` once. A lone cold call is cheaper direct than through a program.\n- Anything wider \u2014 two or more calls, dependent steps, loops, joins, branching, browsing a whole catalog, or a result that must be reduced: one `execute_code` run.\n- Any unannotated, write-capable, or destructive call: `call_destructive_tool`, individually and only after reviewing its schema and consequences. Generated code cannot make one.\n- Truncated result: retry with `fields` when possible; otherwise page it with `get_result`.\n- `auth_required`: use `authorize_connector`, give its recovery handoff to the operator, then retry the original call.\n\n## Inside a program\n\nOne async arrow function. The only capabilities are one global per connector (`<connectorId>.<toolName>(args)`), the four `connecta` functions, and `console.log`.\n\n- What exists: `connecta.search({})` browses every catalog; add `safety: \"readOnly\"` for only calls the program can execute, and `connector: \"<id>\"` to browse one. This filters discovery results, not authority, and each match carries its `address` and annotations.\n- Exact schemas for known addresses: `connecta.describe({ address: \"connector.tool\" })` for one or `connecta.describe({ addresses: [...] })` for many; `format: \"json\"` only for exact constraints.\n- Two to ten independent calls: `connecta.batch([...])`. Each outcome is `{ address, ok: true, data }` or `{ address, ok: false, error, errorDetails: { code, retryable } }`, which is also how a program tells a policy refusal from a transient failure.\n- Search inside the run rather than searching first, and return only the reduction the answer needs \u2014 never raw payloads.\n- Only tools annotated `readOnlyHint: true` are reachable; the read-only gate, credentials, and admission are enforced below the sandbox, so nothing a program does widens what it can reach.\n";
14
14
  /**
15
15
  * Appended to USAGE_SKILL only when the deployment actually has at least one
16
16
  * connector guide. A deployment with none — every deployment that has not
@@ -1 +1 @@
1
- {"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../src/skills.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7D,eAAO,MAAM,qBAAqB,+vBACysB,CAAC;AAE5uB;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,+yBAC4vB,CAAC;AAEjyB,eAAO,MAAM,WAAW,yyDAwBvB,CAAC;AAEF,eAAO,MAAM,sBAAsB,8yEAyBlC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,odAIpC,CAAC;AASF,iEAAiE;AACjE,wBAAgB,eAAe,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAIhE;AAED,oEAAoE;AACpE,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,GAAG,OAAO,CAI5E;AAsCD,uDAAuD;AACvD,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,CAGvE;AAyDD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,UAAU,EAAE,SAAS,SAAS,EAAE,EAChC,OAAO,GAAE,eAA2B,GACnC,YAAY,EAAE,CAehB;AAED,MAAM,MAAM,WAAW,GACrB;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvE;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,SAAS,SAAS,EAAE,EAChC,OAAO,GAAE,eAA2B,GACnC,WAAW,CAwCb"}
1
+ {"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../src/skills.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7D,eAAO,MAAM,qBAAqB,+vBACysB,CAAC;AAE5uB;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,+yBAC4vB,CAAC;AAEjyB,eAAO,MAAM,WAAW,yyDAwBvB,CAAC;AAEF,eAAO,MAAM,sBAAsB,u3EAyBlC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,odAIpC,CAAC;AASF,iEAAiE;AACjE,wBAAgB,eAAe,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAIhE;AAED,oEAAoE;AACpE,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,GAAG,OAAO,CAI5E;AAsCD,uDAAuD;AACvD,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,CAGvE;AAyDD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,UAAU,EAAE,SAAS,SAAS,EAAE,EAChC,OAAO,GAAE,eAA2B,GACnC,YAAY,EAAE,CAehB;AAED,MAAM,MAAM,WAAW,GACrB;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvE;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,SAAS,SAAS,EAAE,EAChC,OAAO,GAAE,eAA2B,GACnC,WAAW,CAwCb"}
package/dist/skills.js CHANGED
@@ -54,7 +54,7 @@ Use exact addresses returned by discovery; never invent one. Search with 2–4 d
54
54
  One async arrow function. The only capabilities are one global per connector (\`<connectorId>.<toolName>(args)\`), the four \`connecta\` functions, and \`console.log\`.
55
55
 
56
56
  - What exists: \`connecta.search({})\` browses every catalog; add \`safety: "readOnly"\` for only calls the program can execute, and \`connector: "<id>"\` to browse one. This filters discovery results, not authority, and each match carries its \`address\` and annotations.
57
- - Exact schemas for known addresses: \`connecta.describe({ addresses: [...] })\`; \`format: "json"\` only for exact constraints.
57
+ - Exact schemas for known addresses: \`connecta.describe({ address: "connector.tool" })\` for one or \`connecta.describe({ addresses: [...] })\` for many; \`format: "json"\` only for exact constraints.
58
58
  - Two to ten independent calls: \`connecta.batch([...])\`. Each outcome is \`{ address, ok: true, data }\` or \`{ address, ok: false, error, errorDetails: { code, retryable } }\`, which is also how a program tells a policy refusal from a transient failure.
59
59
  - Search inside the run rather than searching first, and return only the reduction the answer needs — never raw payloads.
60
60
  - Only tools annotated \`readOnlyHint: true\` are reachable; the read-only gate, credentials, and admission are enforced below the sandbox, so nothing a program does widens what it can reach.
package/dist/version.d.ts CHANGED
@@ -4,5 +4,5 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export declare const CONNECTA_VERSION = "0.10.4";
7
+ export declare const CONNECTA_VERSION = "0.10.6";
8
8
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -4,5 +4,5 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export const CONNECTA_VERSION = "0.10.4";
7
+ export const CONNECTA_VERSION = "0.10.6";
8
8
  //# sourceMappingURL=version.js.map
@@ -98,7 +98,8 @@ Connecta passes exactly one provider, named `connecta`. An executor must:
98
98
 
99
99
  1. **Expose each provider as a guest global** whose properties are its `fns`,
100
100
  called with the program's arguments and awaited. Connecta's provider carries
101
- `search`, `describe`, `call`, `batch`, and `__callNamespace` — see point 3.
101
+ `search`, `describe`, `call`, `batch`, `emit`, and `__callNamespace` — see
102
+ point 3.
102
103
  2. **Evaluate `prelude` after the provider globals exist and before the
103
104
  program**, in a scope where those globals are reachable. It is host-authored
104
105
  trusted code, never model input, and skipping it is not an option: connecta's
@@ -127,6 +128,10 @@ Optionally implement `AdmittingExecutor` (`acquire()` returning a lease whose
127
128
  `execute` runs once) for bounded admission (`L7`) and `close()` for shutdown;
128
129
  connecta wraps a plain `Executor` with `withExecutorAdmission` otherwise.
129
130
 
131
+ Note what is *not* on this list: [emitted output](#emitted-output) asks
132
+ nothing of an executor — `connecta.emit` is just another provider function
133
+ (`M8`).
134
+
130
135
  ## The program
131
136
 
132
137
  **P1.** A program is one JavaScript `async` arrow-function expression. It is
@@ -240,9 +245,10 @@ means "read the schema" where `[]` would claim the tool takes no fields. The
240
245
  lists come from the same walk that renders the compact schema, so a top-level
241
246
  `$ref` resolves and an `allOf` composes rather than reporting an empty list
242
247
  beside a schema that plainly shows fields; an object with no properties is the
243
- one case where `[]` is the truth. This metadata is code-mode-only:
244
- `search_tools` never carries it, and `includeSchemaKeys: false` buys the bytes
245
- back.
248
+ one case where `[]` is the truth. A truncated schema omits the corresponding
249
+ key list rather than repeating a large partial inventory. `search_tools`
250
+ carries the same metadata whenever schemas are requested. Code-mode callers
251
+ can set `includeSchemaKeys: false` to buy the bytes back.
246
252
 
247
253
  **S3.** Discovery is bounded and the bounds throw rather than silently shrink: a
248
254
  `limit` outside 1–100 is `invalid_args`, and a page whose serialized form
@@ -253,6 +259,8 @@ ask for less. As with every failure, the *thrown* error carries only the message
253
259
  ### connecta.describe
254
260
 
255
261
  ```js
262
+ const one = await connecta.describe({ address: "ci.get_run" });
263
+
256
264
  const { tools } = await connecta.describe({
257
265
  addresses: ["ci.get_run", "ci.get_job_logs"], // ≤ 100
258
266
  format: "compact", // or "json"
@@ -304,6 +312,15 @@ than ten calls throws.
304
312
  error crosses the bridge as a bare message (`E1`), a batch of one is the supported
305
313
  way for a program to *decide* something about a failure rather than report it.
306
314
 
315
+ ### connecta.emit
316
+
317
+ ```js
318
+ await connecta.emit({ type: "image", data: shot.data, mimeType: "image/png" });
319
+ ```
320
+
321
+ The rich-output channel, delivered after the JSON envelope on success. Its
322
+ clauses are [Emitted output](#emitted-output) (`M1`–`M10`).
323
+
307
324
  ## Errors
308
325
 
309
326
  **E1.** There are four error channels, and only two of them are typed.
@@ -351,11 +368,12 @@ program may do about it.
351
368
  sentence. A program cannot recover credentials — only an operator can — so the
352
369
  right move is to stop and let the failure reach the model.
353
370
 
354
- **E4.** A read-only refusal is not a downstream failure. An unannotated,
355
- write-capable, or destructive tool is refused in the sandbox with
356
- `destructive_tool_requires_approval` and stays refused; the program returns and
357
- the model crosses `call_destructive_tool`, where the host can ask a human.
358
- Generated code cannot mint that capability.
371
+ **E4.** An unannotated, write-capable, or destructive tool stays refused with
372
+ `destructive_tool_requires_approval`; `nextAction` carries its canonical address
373
+ to `call_destructive_tool`, plus the original arguments when they fit the
374
+ 512-byte echo budget whole or not at all, since a clipped copy is a different
375
+ call. The model's short `reason` for the human reviewer grants no authority,
376
+ never goes downstream, and generated code cannot mint the capability.
359
377
 
360
378
  **E5.** Failures of the *execution*, not of a call, never appear inside the
361
379
  guest: admission rejection (`executor_overloaded`, retryable, with
@@ -374,11 +392,9 @@ exactly first, by containment second — so a program that *wraps* a failure's
374
392
  message in its own text still reports the underlying typed failure. Keeping the
375
393
  type beats keeping the prose.
376
394
 
377
- **E7.** `retryable` for `unknown_address`, `unknown_tool`,
378
- `ambiguous_tool_alias`, and `destructive_tool_requires_approval` is pinned false,
379
- never inferred from an address containing `503`, `429`, or `temporar`.
395
+ **E7.** `retryable` for `unknown_address`, `unknown_tool`, `ambiguous_tool_alias`, and `destructive_tool_requires_approval` is pinned false, never inferred from an address containing `503`, `429`, or `temporar`. The first two carry `nextAction: { function: "connecta.search", arguments: { query, connector?, includeSchemas: "compact" } }` — the same scoped discovery the top-level record names, keyed to the surface the caller actually has. A program cannot call `search_tools`, so it is never told to. Both the message and the derived `query` clamp the address to 512 UTF-8 bytes with a `…` marker: the address is caller-authored and lands in the message, the query, the text content, and `structuredContent`, so an invented 50 KB one would otherwise produce a refusal orders of magnitude past the deployment's result cap. A clipped address still identifies the mistake; a short one — the common case — is exact and untagged.
380
396
 
381
- **E8.** A remote MCP tool whose advertised schema rejects the call fails before provider dispatch with `invalid_args`, carrying bounded, value-free `{ path, code, expected }` findings and scoped search recovery. Unsupported schemas pass through; unrecognized provider prose remains `connector_call_failed`.
397
+ **E8.** A remote MCP tool whose advertised schema rejects the call fails before provider dispatch with `invalid_args`, carrying bounded, value-free `{ path, code, expected }` findings and scoped search recovery keyed `function: "connecta.search"` like every other in-program miss. Unsupported schemas pass through; unrecognized provider prose remains `connector_call_failed`.
382
398
 
383
399
  ## Results and projection
384
400
 
@@ -420,12 +436,70 @@ on data nobody asked for.
420
436
 
421
437
  **R5.** `console.log`, `console.warn`, and `console.error` are captured in call order and returned as a single `logs` string, capped at 4,000 characters with a truncation marker. Logs survive failure — they ride along with the error result, which is what makes them worth writing. How a non-string argument renders is not contract (`X4`).
422
438
 
423
- **R6.** Nothing else is added to a normal program result. Passing `diagnostics: true` adds one request-local, payload-free `diagnostics` block; omitted and `false` are byte-for-byte the ordinary response path.
439
+ **R6.** Nothing else is added to a normal program result. Passing `diagnostics: true` adds one request-local, payload-free `diagnostics` block; a program that emitted adds `emitted: N` and its blocks (`M2`). Omitted, `false`, and emit-free are byte-for-byte the ordinary response path.
424
440
 
425
441
  **R7.** Timing separates admission, provider setup, total executor wall time, catalog work, and connector work. Catalog and connector values are cumulative, so parallel work can exceed executor wall time. Each used operation kind (`search`, `describe`, `call`, `batch`) gets one aggregate with count, failures, duration, returned serialized bytes, and catalog/connector time; batch adds only its total child count.
426
442
 
427
443
  **R8.** Diagnostics contain measurements and fixed operation names only: no addresses, arguments, results, code, credentials, logs, or raw errors. Result sizes are numbers, never previews. The collector exists only for the opted-in request; it is not activity, a session, or a stream.
428
444
 
445
+ ## Emitted output
446
+
447
+ MCP-native output a return value cannot carry: base64 is not projectable, so a
448
+ block that survives intake uncapped (`S5`) must not die at the `R2` exit
449
+ guard. The argument and the refused alternatives live in the
450
+ [design record](./rich-output-design.md) and `ethos.md`
451
+ ([#267](https://github.com/zackbart/connecta/issues/267),
452
+ [#270](https://github.com/zackbart/connecta/issues/270)).
453
+
454
+ **M1.** `connecta.emit(block)` accepts exactly one block: `{ type: "text",
455
+ text }` or `{ type: "image" | "audio", data /* base64 */, mimeType }`, every
456
+ field a string, no extra fields, no `annotations`, no `_meta`, no sugar forms.
457
+ An invalid block throws catchably and nothing is accepted — rejected, not
458
+ stripped. The refused types are pointers: a guest-minted `resource_link` URI
459
+ is a lure a client may dereference.
460
+
461
+ **M2.** Blocks collect on the host in emission order and are delivered only
462
+ with a successful result, appended to `content` after the JSON envelope, which
463
+ gains `emitted: N`. A program that never emits produces the byte-for-byte
464
+ ordinary response (`R6`). `structuredContent` stays the envelope alone —
465
+ emission is presentation, not a second data channel.
466
+
467
+ **M3.** Return value and emission are independent: `R2` never measures emitted
468
+ bytes, a truncated return does not suppress delivered blocks, and blocks do
469
+ not shrink the return budget.
470
+
471
+ **M4.** A failed program delivers no blocks. The error result reports
472
+ `emittedDiscarded: N` when N > 0 — a field on the structured envelope, a
473
+ trailing line on the plain-text paths — never silently.
474
+
475
+ **M5.** Two budgets (`ConnectaConfig.execute.maxEmittedBytes` /
476
+ `.maxEmittedBlocks`, defaults 4,000,000 serialized bytes and 32 blocks) fail
477
+ loudly at the `emit` call, naming the budget and the room remaining; nothing
478
+ is partially accepted and prior blocks stand. No `get_result` stash: the
479
+ program learns while it can still choose differently. The byte default is a
480
+ transport bound, not a context bound — emitted media reaches the model as
481
+ media, not base64 text.
482
+
483
+ **M6.** No provenance is claimed: every emitted block is program output,
484
+ trusted exactly as much as the return value. Preservation is re-emission of
485
+ the raw downstream block, so `S5`'s uncapped fallthrough is contract.
486
+
487
+ **M7.** `emit` spends no host-call budget (`L4`); `M5`'s bounds are its only
488
+ bounds.
489
+
490
+ **M8.** Emission asks nothing of an executor: `emit` is a provider function,
491
+ blocks cross the guest boundary once as an argument, and `ExecuteResult` is
492
+ unchanged — `Executor` stays assignable from `@cloudflare/codemode`'s
493
+ `DynamicWorkerExecutor`, and any executor that bridges provider calls gets
494
+ emission for free.
495
+
496
+ **M9.** Request-local and unstreamed: blocks exist only in the finished
497
+ response, and `emit` resolving means "accepted," never "delivered."
498
+
499
+ **M10.** Activity stays payload-free. `diagnostics: true` adds one `emitted`
500
+ aggregate — count and serialized bytes, numbers only (`R8`), present only
501
+ when something was emitted.
502
+
429
503
  ## Retry semantics
430
504
 
431
505
  **Y1.** Connecta retries nothing beneath a program. `call_tool` accepts an
@@ -516,27 +590,34 @@ itself (`R2`, `R5`).
516
590
 
517
591
  ## Activity
518
592
 
519
- **V1.** One payload-free activity event per call that named a real connector,
520
- with `source: "execute_code"` — every dispatched call, plus every refusal
521
- connecta could attribute to a connector: a read-only refusal, an unknown tool on
522
- a known connector, an ambiguous shortcut, a connector whose catalog could not be
523
- loaded, a credential connecta could not supply, an exhausted host-call budget. A
524
- program that calls ten tools is ten events as legible as ten `call_tool` calls,
525
- which is what makes moving work into the sandbox an optimization, not a
526
- blindfold.
593
+ **V1.** One payload-free activity event per attempted call, with
594
+ `source: "execute_code"` — every dispatched call plus every local refusal: a
595
+ read-only refusal, an unknown tool, an ambiguous shortcut, an unloadable
596
+ catalog, a missing credential, an exhausted host-call budget, an address no
597
+ connector owns. Ten tools called is ten events, as legible as ten `call_tool`
598
+ calls which makes moving work into the sandbox an optimization, not a blindfold.
527
599
 
528
600
  **V2.** Each event carries `connectorId`, `toolName`, `address`, `source`,
529
601
  `outcome` (`success`, `error`, `timeout`, `cancelled`), `durationMs`,
530
- `attempts`, and `errorCode` when there was one — plus the request's id, actor,
531
- and server identity. It has nowhere to put arguments, results, program source,
532
- or raw error text, by construction. A failure the program *caught* is still
533
- recorded: the call happened. `address` is canonical (`A1`) for every call that
534
- resolved to a tool; for the refusals that never resolved to one it is the name the
535
- program used, which for a shortcut is the sanitized alias the honest record of
536
- what was attempted.
537
-
538
- **V3.** A call whose connector does not exist an unknown address emits
539
- nothing. There is no connector to attribute it to.
602
+ `attempts`, and `errorCode` when the call *failed* — plus request, actor, and
603
+ server identity. Typed codes derive an optional `friction`: `tool_not_found`,
604
+ `schema_retry`, `destructive_reroute`, or `auth_required`. The fifth class,
605
+ `result_too_large`, cannot reach an `execute_code` event: it belongs to a
606
+ `call_tool` result or a `batch_call` child's too large to return inline,
607
+ and a program's own return is refused paging by design rather than truncated
608
+ into friction. There is nowhere to put arguments, results, program source, or
609
+ raw error text; a caught failure is still recorded. `address` is
610
+ canonical (`A1`) where a tool resolved, otherwise the name the program used
611
+ for a shortcut its sanitized alias, the honest record of what was attempted.
612
+
613
+ **V3.** A call whose connector does not exist is recorded at the address as
614
+ written, *provided* it split into the two fields activity keeps — one with no
615
+ interior dot records nothing. An invented id is the address mistake an operator
616
+ most needs to see. But recording it as written puts caller-authored text in
617
+ fields that are otherwise operator- and connector-authored, so `connectorId`
618
+ and `toolName` clamp at 128 UTF-8 bytes (`address` at 257) with a `…` marker:
619
+ payload-free *by construction* means the event has nowhere to put a payload,
620
+ not merely that connecta declines to.
540
621
 
541
622
  **V4.** The execution itself emits no event. It has no address, and its one
542
623
  distinctive artifact is the program source, which is exactly what a payload-free
@@ -629,6 +710,10 @@ The middle three were places where the contract described behavior the code did
629
710
  not quite have. The code moved, because the described behavior is the one worth
630
711
  having.
631
712
 
713
+ One surface was added since: [emitted output](#emitted-output) (`M1`–`M10`,
714
+ [#270](https://github.com/zackbart/connecta/issues/270)) — additive by
715
+ construction, with the byte-for-byte no-emit promise pinned by test.
716
+
632
717
  ## Verification
633
718
 
634
719
  Every clause has a test. `test/guest-contract-cases.ts` holds the case table,
@@ -678,8 +763,14 @@ the upstream `Executor` shape assignable.
678
763
  | `L5`, `X2` | `test/quickjs-executor.test.ts` (CPU, heap) |
679
764
  | `L6`, `X10` | `test/quickjs-executor.test.ts` (bridge and IPC bounds for arguments and result; the address in the over-bound message) |
680
765
  | `L7` | `test/execute.test.ts`, `test/executor-admission.test.ts` |
681
- | `V1`, `V2` | `test/guest-api-contract.test.ts` (dispatched calls, and the four refusal classes that name a connector), `test/activity.test.ts` |
682
- | `V3`, `V4` | `test/guest-api-contract.test.ts` (no event without a connector) |
766
+ | `V1`–`V4` | `test/guest-api-contract.test.ts` (dispatched calls, every refusal class including an address no connector owns, the friction each derives, no event for the execution itself), `test/activity.test.ts` (the shared code → friction table, and the identity clamp) |
767
+ | `M1` | `test/guest-api-contract.test.ts` (invalid emits throw catchably, accept nothing), `test/execute-emit.test.ts` (every rejected shape) |
768
+ | `M2`, `M3` | `test/guest-api-contract.test.ts` (delivery order, truncated return plus delivered blocks), `test/execute-emit.test.ts` (envelope, `structuredContent`, byte-for-byte no-emit path) |
769
+ | `M4` | `test/guest-api-contract.test.ts` (discard is visible), `test/execute-emit.test.ts` (structured and plain paths) |
770
+ | `M5`, `M7` | `test/execute-emit.test.ts` (both budgets fail the crossing block; host-call budget untouched) |
771
+ | `M6`, `M9` | verdicts; `M1`'s strict typing and `M2`'s collect-then-deliver are their enforcement |
772
+ | `M8` | two arms passing one case table, `test/codemode-compat.test.ts` |
773
+ | `M10` | `test/execute-emit.test.ts` (aggregate present, numbers only, absent when nothing emitted) |
683
774
  | `X3` | `test/quickjs-executor.test.ts` (cancels a running child) |
684
775
  | `X4` | `test/guest-api-contract.test.ts` (string logs only) |
685
776
  | `X6` | `test/quickjs-executor.test.ts` (never-settling await) |
@@ -55,8 +55,11 @@ or generated code; `safety: "approvalRequired"` finds the complementary set
55
55
  that must cross `call_destructive_tool`. Omitting `safety`, or setting it to
56
56
  `"all"`, preserves the complete configured catalog. This is only a discovery
57
57
  filter: it neither grants authority nor changes invocation admission.
58
- `includeSchemas: "compact"` adds each match's input and any
59
- declared output shape; matches also carry declared behavior annotations. When
58
+ `includeSchemas: "compact"` adds each match's input and any declared output
59
+ shape. Bounded plain-object schemas also expose `inputKeys`,
60
+ `requiredInputKeys`, and `outputKeys`; a truncated shape omits its corresponding
61
+ list rather than repeating a large partial inventory. Matches carry declared
62
+ behavior annotations. When
60
63
  that shape is sufficient, call the returned address directly. Reserve schema
61
64
  expansion — `connecta.describe` in a program, `describe_tools` on the classic
62
65
  surface — for a search without schemas, an ambiguous compact shape, or exact
@@ -86,12 +89,24 @@ mode; they are not a duplicated Connecta object result. Newly stashed JSON and
86
89
  downstream content envelopes use compact serialization, so `get_result` byte
87
90
  offsets and totals refer to that exact compact text.
88
91
 
92
+ A `call_tool` or `batch_call` truncation notice carries both the historical
93
+ `resultId` and an exact
94
+ `nextAction: { tool: "get_result", arguments: { id, offset: 0 } }`. The handle
95
+ is therefore directly actionable without copying an identifier out of prose;
96
+ re-calling with `fields` remains the smaller alternative when projection is
97
+ possible. Program results and oversized discovery responses carry no such
98
+ route — paging a program's return value is a refused shape, because a program
99
+ can shrink anything before it returns.
100
+
89
101
  `fields` keeps its historical flat `{ "<path>": value }` result when every
90
- requested dot-path resolves, except that an exact downstream `$connecta` field
91
- is always escaped under `data`. If any path misses—or that reserved name is
92
- selected—the result carries matches under `data` and reserves `$connecta` for a
102
+ requested dot-path resolves. Dot notation traverses objects; append `[]` to an
103
+ array field before continuing, as in `results[].id`. An exact downstream
104
+ `$connecta` field is always escaped under `data`. If any path misses—or that
105
+ reserved name is selected—the result carries matches under `data` and reserves `$connecta` for a
93
106
  `type: "field_projection"` recovery record naming each `unmatchedFields`
94
- entry. The discriminator means downstream fields named `data`, `projection`,
107
+ entry. When a miss matches a declared array path except for `[]`, the record
108
+ also carries the traversal hint. The discriminator means downstream fields
109
+ named `data`, `projection`,
95
110
  or `$connecta` remain ordinary values nested under `data`, never apparent
96
111
  metadata. A declared output schema contributes a bounded `availableFields`
97
112
  list and a `schemaCoverage` verdict. Only a completely analyzed, closed schema
@@ -166,6 +181,71 @@ only through the same-origin, Clerk-operator credential route. After OAuth
166
181
  consent or an operator update, retry the original operation; a static update is
167
182
  read from the vault on the next call and needs no redeploy.
168
183
 
184
+ ## Routing recovery
185
+
186
+ Predictable local refusals carry structured recovery on both result modes.
187
+ An unknown connector suggests an unscoped discovery query derived from the
188
+ attempted tool name; an unknown tool scopes the same query to the connector that
189
+ answered. The suggested route follows the caller's own surface: `tool:
190
+ "search_tools"` for a top-level call, `function: "connecta.search"` with the same
191
+ arguments when the miss happened inside `execute_code`, which has no way to call
192
+ a tool. A read path that reaches an unannotated, write-capable, or destructive
193
+ tool returns `nextAction` for `call_destructive_tool` with the canonical
194
+ address. Nothing is executed by these records.
195
+
196
+ That route echoes the caller's own arguments back only while they fit a
197
+ 512-byte budget, and then whole — never clipped. An error envelope is not
198
+ size-guarded the way a result is, so an unbounded echo would let a large
199
+ argument object produce a refusal many times the deployment's result cap, on
200
+ both `call_tool` and the `batch_call` envelope. Over budget, `args` is absent
201
+ and the `purpose` says to re-send what was just sent: the agent already holds
202
+ its own arguments, and half of them would describe a call nobody made.
203
+
204
+ The address gets the same budget and the opposite rule: 512 bytes, clamped
205
+ with a trailing `…` rather than dropped. It is caller-authored too — an
206
+ invented one can be any length — and it reaches the error message *and* the
207
+ recovery query, each of which lands in both the text content and
208
+ `structuredContent`; unbounded, a 50 KB typo produced a 200 KB refusal under a
209
+ 1 KB result cap. Dropping it is not an option the way dropping arguments is:
210
+ the address is the thing being corrected, a clipped one still identifies the
211
+ mistake, and a short one — every real one — comes back exact and untagged.
212
+
213
+ Shortcut ambiguity inside `execute_code` returns every colliding canonical
214
+ address and points at `connecta.call`; the program or model must still choose
215
+ which one matches the user's intent. `call_destructive_tool` accepts an optional
216
+ `reason` of at most 500 characters for the host's human approval view. It is
217
+ outer-call context only: Connecta neither treats it as authority nor passes it
218
+ to the downstream connector, and an empty or whitespace-only one is read as no
219
+ reason rather than as a reason to refuse the call.
220
+
221
+ Activity carries an optional coarse `friction` class: `tool_not_found`,
222
+ `schema_retry`, `destructive_reroute`, `auth_required`, or `result_too_large`.
223
+ It is derived from the typed error code, except on the one call that has no
224
+ error code to derive from: a `call_tool` result — or a `batch_call` child's —
225
+ too large to return inline is friction for the agent while remaining
226
+ `outcome: "success"`. That is the only source of `result_too_large` friction.
227
+ An oversized *discovery* response and an oversized program return are shaped
228
+ differently and produce none, and an `errorCode` is written only when the call
229
+ actually failed. The category adds no arguments, results, search text,
230
+ generated code, credentials, or raw errors.
231
+
232
+ An address whose connector does not exist is recorded too, as written, provided
233
+ it has the `<connectorId>.<toolName>` shape at all — a string that never split
234
+ into the two fields activity keeps still records nothing. A hallucinated
235
+ connector id is the most common address mistake, and an operator reading
236
+ activity should see it; addresses are already a first-class activity field, so
237
+ nothing new is retained.
238
+
239
+ What *is* new is that those fields now hold caller-authored text, so the
240
+ recording seam clamps them: `connectorId` and `toolName` at 128 UTF-8 bytes
241
+ each, `address` at 257, with a `…` marker. Far past any real id or tool name,
242
+ and far short of a 40 KB invented one. The clamp is structural rather than a
243
+ policy the writer applies, because "payload-free by construction" has to mean
244
+ the event type has nowhere to put a payload — a 40 KB connector id is a payload
245
+ wearing an id's clothing. Clamped rather than skipped: the invented id is
246
+ exactly what an operator needs to see, and its first 128 bytes say as much
247
+ about the mistake as all 40,000 would.
248
+
169
249
  ## Argument recovery
170
250
 
171
251
  A remote MCP tool's advertised `inputSchema` is checked in the shared
@@ -177,9 +257,11 @@ JSON Pointer `path`, schema-keyword `code`, and expected shape. Submitted
177
257
  values are never copied into those findings.
178
258
 
179
259
  At most three findings are returned; `validation.truncated` says when more
180
- exist. `nextAction` points to `search_tools` scoped to the same connector and
181
- tool name when the compact schema is needed, while `retry` says to correct the
182
- listed arguments and reissue the original operation. A schema the local
260
+ exist. `nextAction` points to discovery scoped to the same connector and tool
261
+ name when the compact schema is needed routed like any other miss, so a
262
+ program is sent to `connecta.search` and a top-level call to `search_tools` —
263
+ while `retry` says to correct the listed arguments and reissue the original
264
+ operation. A schema the local
183
265
  validator cannot evaluate passes through to the provider. Provider error prose
184
266
  is not parsed or guessed, so an unknown format remains
185
267
  `connector_call_failed`.