@zackbart/connecta 0.10.5 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/AGENTS.md +8 -6
  2. package/CHANGELOG.md +150 -0
  3. package/README.md +5 -4
  4. package/bin/connecta.mjs +0 -7
  5. package/dist/activity.d.ts +11 -1
  6. package/dist/activity.d.ts.map +1 -1
  7. package/dist/activity.js +44 -3
  8. package/dist/activity.js.map +1 -1
  9. package/dist/catalog-service.d.ts +24 -0
  10. package/dist/catalog-service.d.ts.map +1 -1
  11. package/dist/catalog-service.js +68 -9
  12. package/dist/catalog-service.js.map +1 -1
  13. package/dist/connectors/api.d.ts +2 -2
  14. package/dist/connectors/remote-mcp.d.ts +1 -1
  15. package/dist/errors.d.ts +49 -4
  16. package/dist/errors.d.ts.map +1 -1
  17. package/dist/errors.js +68 -1
  18. package/dist/errors.js.map +1 -1
  19. package/dist/execute.d.ts +73 -3
  20. package/dist/execute.d.ts.map +1 -1
  21. package/dist/execute.js +161 -29
  22. package/dist/execute.js.map +1 -1
  23. package/dist/index.d.ts +28 -30
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +29 -37
  26. package/dist/index.js.map +1 -1
  27. package/dist/invocation.d.ts +9 -2
  28. package/dist/invocation.d.ts.map +1 -1
  29. package/dist/invocation.js +61 -31
  30. package/dist/invocation.js.map +1 -1
  31. package/dist/meta-tools.d.ts +24 -59
  32. package/dist/meta-tools.d.ts.map +1 -1
  33. package/dist/meta-tools.js +107 -359
  34. package/dist/meta-tools.js.map +1 -1
  35. package/dist/operator-ui/generated.d.ts +1 -1
  36. package/dist/operator-ui/generated.d.ts.map +1 -1
  37. package/dist/operator-ui/generated.js +1 -1
  38. package/dist/operator-ui/generated.js.map +1 -1
  39. package/dist/registry.d.ts +12 -10
  40. package/dist/registry.d.ts.map +1 -1
  41. package/dist/registry.js +8 -17
  42. package/dist/registry.js.map +1 -1
  43. package/dist/routes/mcp.d.ts.map +1 -1
  44. package/dist/routes/mcp.js +19 -21
  45. package/dist/routes/mcp.js.map +1 -1
  46. package/dist/routes/shared.d.ts +9 -11
  47. package/dist/routes/shared.d.ts.map +1 -1
  48. package/dist/routes/shared.js.map +1 -1
  49. package/dist/server.js +5 -4
  50. package/dist/server.js.map +1 -1
  51. package/dist/skills.d.ts +8 -18
  52. package/dist/skills.d.ts.map +1 -1
  53. package/dist/skills.js +13 -60
  54. package/dist/skills.js.map +1 -1
  55. package/dist/types.d.ts +6 -20
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/version.d.ts +1 -1
  58. package/dist/version.js +1 -1
  59. package/documentation/code-first-exploration.md +16 -16
  60. package/documentation/code-mode.md +137 -63
  61. package/documentation/connectors.md +1 -1
  62. package/documentation/meta-tools.md +96 -33
  63. package/documentation/rich-output-design.md +212 -0
  64. package/ethos.md +17 -19
  65. package/examples/node/README.md +1 -2
  66. package/examples/node/src/index.ts +1 -3
  67. package/examples/worker/README.md +19 -16
  68. package/examples/worker/src/d1-activity-row.ts +40 -0
  69. package/examples/worker/src/d1-activity.ts +3 -2
  70. package/examples/worker/src/index.ts +6 -14
  71. package/examples/worker/wrangler.jsonc +3 -6
  72. package/package.json +1 -1
  73. package/src/activity.ts +69 -3
  74. package/src/catalog-service.ts +113 -20
  75. package/src/connectors/api.ts +2 -2
  76. package/src/connectors/remote-mcp.ts +1 -1
  77. package/src/errors.ts +104 -3
  78. package/src/execute.ts +237 -37
  79. package/src/index.ts +60 -67
  80. package/src/invocation.ts +61 -19
  81. package/src/meta-tools.ts +136 -482
  82. package/src/operator-ui/browser.ts +10 -2
  83. package/src/operator-ui/generated.ts +1 -1
  84. package/src/registry.ts +7 -35
  85. package/src/routes/mcp.ts +19 -21
  86. package/src/routes/shared.ts +8 -11
  87. package/src/server.ts +7 -7
  88. package/src/skills.ts +11 -74
  89. package/src/types.ts +6 -21
  90. package/src/version.ts +1 -1
  91. package/templates/node/README.md +2 -1
  92. package/templates/node/package.json +1 -1
  93. package/templates/node/src/index.ts +1 -1
@@ -38,7 +38,8 @@ The recommendation is to evolve Connecta toward:
38
38
  and result retrieval;
39
39
  - policy, credentials, egress, and auditing enforced below generated code; and
40
40
  - the current tool surface retained as a compatibility path and evaluation
41
- control while the new path matures.
41
+ control while the new path matures — a retention that ended with #273, which
42
+ made the executor mandatory and deleted the classic surface outright.
42
43
 
43
44
  This was an exploratory spike, not a production implementation. The next agent
44
45
  should use these findings as a design brief rather than porting the prototype
@@ -195,7 +196,10 @@ It did not establish:
195
196
  complexity.
196
197
 
197
198
  The existing surface should therefore remain available during the transition
198
- as a compatibility mode, rollback path, and experimental control.
199
+ as a compatibility mode, rollback path, and experimental control. It was, and
200
+ then it was not: #224 made code-first the default and #273 removed the second
201
+ shape entirely. The open questions above outlived the control arm rather than
202
+ being settled by it.
199
203
 
200
204
  ## Recommended product shape
201
205
 
@@ -222,11 +226,10 @@ Turn the exploratory scenarios into a repeatable evaluation suite before
222
226
  changing the default surface. Run at least 20 independent samples per task and
223
227
  model, with prompt variation.
224
228
 
225
- That suite now exists as
226
- [`eval/code-first-gate`](../eval/code-first-gate/README.md): these ten scenarios
227
- as versioned tasks with three phrasings each, the classic nine-tool surface as
228
- the control arm, and a per-model report that ends in a flip/hold verdict without
229
- flipping anything.
229
+ That suite was built under
230
+ [`eval/code-first-gate`](../eval/code-first-gate/README.md). Its recorded results
231
+ remain as measurement history; the runnable comparison was retired when #273
232
+ removed the alternate deployment shapes.
230
233
 
231
234
  Capture:
232
235
 
@@ -254,16 +257,13 @@ catalogs, centralized invocation, admission control, and lazy connector
254
257
  capabilities. Build the new surface on those rather than recreating parallel
255
258
  logic in the executor.
256
259
 
257
- ### Phase 3: ship behind an explicit surface choice
260
+ ### Phase 3: consolidate the surface
258
261
 
259
- Offer code-first and classic modes initially. Executor-backed deployments can
260
- opt into code-first while deployments without a safe executor remain classic.
261
- Keep consequential writes outside the sandbox in both modes.
262
-
263
- Once the repeated model eval and production telemetry are healthy, make
264
- code-first the executor-backed default. Avoid adding new top-level read tools
265
- unless a measured case cannot be expressed safely or clearly through the
266
- programmable surface.
262
+ This phase is complete: code-first became the default in #224, then #273 made
263
+ the executor mandatory and removed the deployment choice. Consequential writes
264
+ remain outside the sandbox. New top-level read tools still require a measured
265
+ case that cannot be expressed safely or clearly through the programmable
266
+ surface.
267
267
 
268
268
  ### Phase 4: stabilize successful workflows
269
269
 
@@ -20,19 +20,11 @@ are stable and cited by the tests in [Verification](#verification).
20
20
 
21
21
  ## Deploy-time capability
22
22
 
23
- The `executor` passed to `createConnecta()` is the complete switch, and it selects
24
- the whole surface rather than one tool
25
- ([#224](https://github.com/zackbart/connecta/issues/224)): with a live `Executor`,
26
- `tools/list` is exactly seven `execute_code`, `search_tools`, `call_tool`,
27
- `call_destructive_tool`, `authorize_connector`, `get_result`, `skills`; without one
28
- it is the nine base meta-tools, whose `list_connectors`, `describe_tools`, and
29
- `batch_call` are what seven folds into `connecta.search`, `connecta.describe`, and
30
- `connecta.batch`.
31
-
32
- No feature flag, and no code tool advertised before it can be honored.
33
- `surface: "classic"` beside an executor is the one override (ten tools, the eval
34
- gate's incremental arm); `surface: "code-first"` without one throws at
35
- construction rather than advertise an absent program surface.
23
+ The `executor` passed to `createConnecta()` is required. `tools/list` is exactly
24
+ seven `execute_code`, `search_tools`, `call_tool`, `call_destructive_tool`,
25
+ `authorize_connector`, `get_result`, and `skills`. Construction fails when the
26
+ executor is missing, and the removed `surface` option is rejected rather than
27
+ ignored ([#273](https://github.com/zackbart/connecta/issues/273)).
36
28
 
37
29
  On Node, install the optional `quickjs-emscripten` peer and use the package's
38
30
  QuickJS subpath:
@@ -51,25 +43,20 @@ const connecta = createConnecta({
51
43
  CPU, wall-time, memory, stack, queue, result, log, and IPC bounds are configured
52
44
  on the executor. Server bundlers must keep the `@zackbart/connecta/quickjs`
53
45
  package files external so the child entry stays on disk. The
54
- [Node example](../examples/node/README.md) is enabled; remove its `executor` field
55
- for the nine-tool compatibility deployment.
46
+ [Node example](../examples/node/README.md) carries the complete setup.
56
47
 
57
- On Cloudflare Workers, the Worker Loader binding is both the paid capability and
58
- the configuration switch:
48
+ On Cloudflare Workers, the Worker Loader binding provides the required sandbox:
59
49
 
60
50
  ```ts
61
51
  createConnecta({
62
- ...(env.LOADER
63
- ? { executor: new DynamicWorkerExecutor({ loader: env.LOADER }) }
64
- : {}),
52
+ executor: new DynamicWorkerExecutor({ loader: env.LOADER }),
65
53
  // connectors, auth, storage…
66
54
  });
67
55
  ```
68
56
 
69
- Leave the binding absent on the Workers Free plan. Its absence must also be
70
- represented as optional in the deployment's `Env` type. The
57
+ Dynamic Workers require the Workers Paid plan. The
71
58
  [Worker example](../examples/worker/README.md#code-mode) carries the complete
72
- binding and package setup.
59
+ required binding and package setup.
73
60
 
74
61
  ## What an executor must implement
75
62
 
@@ -98,7 +85,8 @@ Connecta passes exactly one provider, named `connecta`. An executor must:
98
85
 
99
86
  1. **Expose each provider as a guest global** whose properties are its `fns`,
100
87
  called with the program's arguments and awaited. Connecta's provider carries
101
- `search`, `describe`, `call`, `batch`, and `__callNamespace` — see point 3.
88
+ `search`, `describe`, `call`, `batch`, `emit`, and `__callNamespace` — see
89
+ point 3.
102
90
  2. **Evaluate `prelude` after the provider globals exist and before the
103
91
  program**, in a scope where those globals are reachable. It is host-authored
104
92
  trusted code, never model input, and skipping it is not an option: connecta's
@@ -127,6 +115,10 @@ Optionally implement `AdmittingExecutor` (`acquire()` returning a lease whose
127
115
  `execute` runs once) for bounded admission (`L7`) and `close()` for shutdown;
128
116
  connecta wraps a plain `Executor` with `withExecutorAdmission` otherwise.
129
117
 
118
+ Note what is *not* on this list: [emitted output](#emitted-output) asks
119
+ nothing of an executor — `connecta.emit` is just another provider function
120
+ (`M8`).
121
+
130
122
  ## The program
131
123
 
132
124
  **P1.** A program is one JavaScript `async` arrow-function expression. It is
@@ -300,13 +292,22 @@ const outcomes = await connecta.batch([
300
292
  order. A success is `{ address, ok: true, data }`. A failure is
301
293
  `{ address, ok: false, error, errorDetails }`, where `error` is the message and
302
294
  `errorDetails` is the typed object described in [Errors](#errors) — the same two
303
- field names `batch_call` uses. One failing call never rejects the batch, and more
304
- than ten calls throws.
295
+ field names the host's internal batch path uses. One failing call never rejects
296
+ the batch, and more than ten calls throws.
305
297
 
306
298
  **S8.** `connecta.batch` is the classification channel: because a thrown host
307
299
  error crosses the bridge as a bare message (`E1`), a batch of one is the supported
308
300
  way for a program to *decide* something about a failure rather than report it.
309
301
 
302
+ ### connecta.emit
303
+
304
+ ```js
305
+ await connecta.emit({ type: "image", data: shot.data, mimeType: "image/png" });
306
+ ```
307
+
308
+ The rich-output channel, delivered after the JSON envelope on success. Its
309
+ clauses are [Emitted output](#emitted-output) (`M1`–`M10`).
310
+
310
311
  ## Errors
311
312
 
312
313
  **E1.** There are four error channels, and only two of them are typed.
@@ -354,11 +355,12 @@ program may do about it.
354
355
  sentence. A program cannot recover credentials — only an operator can — so the
355
356
  right move is to stop and let the failure reach the model.
356
357
 
357
- **E4.** A read-only refusal is not a downstream failure. An unannotated,
358
- write-capable, or destructive tool is refused in the sandbox with
359
- `destructive_tool_requires_approval` and stays refused; the program returns and
360
- the model crosses `call_destructive_tool`, where the host can ask a human.
361
- Generated code cannot mint that capability.
358
+ **E4.** An unannotated, write-capable, or destructive tool stays refused with
359
+ `destructive_tool_requires_approval`; `nextAction` carries its canonical address
360
+ to `call_destructive_tool`, plus the original arguments when they fit the
361
+ 512-byte echo budget whole or not at all, since a clipped copy is a different
362
+ call. The model's short `reason` for the human reviewer grants no authority,
363
+ never goes downstream, and generated code cannot mint the capability.
362
364
 
363
365
  **E5.** Failures of the *execution*, not of a call, never appear inside the
364
366
  guest: admission rejection (`executor_overloaded`, retryable, with
@@ -377,11 +379,9 @@ exactly first, by containment second — so a program that *wraps* a failure's
377
379
  message in its own text still reports the underlying typed failure. Keeping the
378
380
  type beats keeping the prose.
379
381
 
380
- **E7.** `retryable` for `unknown_address`, `unknown_tool`,
381
- `ambiguous_tool_alias`, and `destructive_tool_requires_approval` is pinned false,
382
- never inferred from an address containing `503`, `429`, or `temporar`.
382
+ **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.
383
383
 
384
- **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`.
384
+ **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`.
385
385
 
386
386
  ## Results and projection
387
387
 
@@ -423,12 +423,70 @@ on data nobody asked for.
423
423
 
424
424
  **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`).
425
425
 
426
- **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.
426
+ **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.
427
427
 
428
428
  **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.
429
429
 
430
430
  **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.
431
431
 
432
+ ## Emitted output
433
+
434
+ MCP-native output a return value cannot carry: base64 is not projectable, so a
435
+ block that survives intake uncapped (`S5`) must not die at the `R2` exit
436
+ guard. The argument and the refused alternatives live in the
437
+ [design record](./rich-output-design.md) and `ethos.md`
438
+ ([#267](https://github.com/zackbart/connecta/issues/267),
439
+ [#270](https://github.com/zackbart/connecta/issues/270)).
440
+
441
+ **M1.** `connecta.emit(block)` accepts exactly one block: `{ type: "text",
442
+ text }` or `{ type: "image" | "audio", data /* base64 */, mimeType }`, every
443
+ field a string, no extra fields, no `annotations`, no `_meta`, no sugar forms.
444
+ An invalid block throws catchably and nothing is accepted — rejected, not
445
+ stripped. The refused types are pointers: a guest-minted `resource_link` URI
446
+ is a lure a client may dereference.
447
+
448
+ **M2.** Blocks collect on the host in emission order and are delivered only
449
+ with a successful result, appended to `content` after the JSON envelope, which
450
+ gains `emitted: N`. A program that never emits produces the byte-for-byte
451
+ ordinary response (`R6`). `structuredContent` stays the envelope alone —
452
+ emission is presentation, not a second data channel.
453
+
454
+ **M3.** Return value and emission are independent: `R2` never measures emitted
455
+ bytes, a truncated return does not suppress delivered blocks, and blocks do
456
+ not shrink the return budget.
457
+
458
+ **M4.** A failed program delivers no blocks. The error result reports
459
+ `emittedDiscarded: N` when N > 0 — a field on the structured envelope, a
460
+ trailing line on the plain-text paths — never silently.
461
+
462
+ **M5.** Two budgets (`ConnectaConfig.execute.maxEmittedBytes` /
463
+ `.maxEmittedBlocks`, defaults 4,000,000 serialized bytes and 32 blocks) fail
464
+ loudly at the `emit` call, naming the budget and the room remaining; nothing
465
+ is partially accepted and prior blocks stand. No `get_result` stash: the
466
+ program learns while it can still choose differently. The byte default is a
467
+ transport bound, not a context bound — emitted media reaches the model as
468
+ media, not base64 text.
469
+
470
+ **M6.** No provenance is claimed: every emitted block is program output,
471
+ trusted exactly as much as the return value. Preservation is re-emission of
472
+ the raw downstream block, so `S5`'s uncapped fallthrough is contract.
473
+
474
+ **M7.** `emit` spends no host-call budget (`L4`); `M5`'s bounds are its only
475
+ bounds.
476
+
477
+ **M8.** Emission asks nothing of an executor: `emit` is a provider function,
478
+ blocks cross the guest boundary once as an argument, and `ExecuteResult` is
479
+ unchanged — `Executor` stays assignable from `@cloudflare/codemode`'s
480
+ `DynamicWorkerExecutor`, and any executor that bridges provider calls gets
481
+ emission for free.
482
+
483
+ **M9.** Request-local and unstreamed: blocks exist only in the finished
484
+ response, and `emit` resolving means "accepted," never "delivered."
485
+
486
+ **M10.** Activity stays payload-free. `diagnostics: true` adds one `emitted`
487
+ aggregate — count and serialized bytes, numbers only (`R8`), present only
488
+ when something was emitted.
489
+
432
490
  ## Retry semantics
433
491
 
434
492
  **Y1.** Connecta retries nothing beneath a program. `call_tool` accepts an
@@ -519,27 +577,34 @@ itself (`R2`, `R5`).
519
577
 
520
578
  ## Activity
521
579
 
522
- **V1.** One payload-free activity event per call that named a real connector,
523
- with `source: "execute_code"` — every dispatched call, plus every refusal
524
- connecta could attribute to a connector: a read-only refusal, an unknown tool on
525
- a known connector, an ambiguous shortcut, a connector whose catalog could not be
526
- loaded, a credential connecta could not supply, an exhausted host-call budget. A
527
- program that calls ten tools is ten events as legible as ten `call_tool` calls,
528
- which is what makes moving work into the sandbox an optimization, not a
529
- blindfold.
580
+ **V1.** One payload-free activity event per attempted call, with
581
+ `source: "execute_code"` — every dispatched call plus every local refusal: a
582
+ read-only refusal, an unknown tool, an ambiguous shortcut, an unloadable
583
+ catalog, a missing credential, an exhausted host-call budget, an address no
584
+ connector owns. Ten tools called is ten events, as legible as ten `call_tool`
585
+ calls which makes moving work into the sandbox an optimization, not a blindfold.
530
586
 
531
587
  **V2.** Each event carries `connectorId`, `toolName`, `address`, `source`,
532
588
  `outcome` (`success`, `error`, `timeout`, `cancelled`), `durationMs`,
533
- `attempts`, and `errorCode` when there was one — plus the request's id, actor,
534
- and server identity. It has nowhere to put arguments, results, program source,
535
- or raw error text, by construction. A failure the program *caught* is still
536
- recorded: the call happened. `address` is canonical (`A1`) for every call that
537
- resolved to a tool; for the refusals that never resolved to one it is the name the
538
- program used, which for a shortcut is the sanitized alias the honest record of
539
- what was attempted.
540
-
541
- **V3.** A call whose connector does not exist an unknown address emits
542
- nothing. There is no connector to attribute it to.
589
+ `attempts`, and `errorCode` when the call *failed* — plus request, actor, and
590
+ server identity. Typed codes derive an optional `friction`: `tool_not_found`,
591
+ `schema_retry`, `destructive_reroute`, or `auth_required`. The fifth class,
592
+ `result_too_large`, cannot reach an `execute_code` event: it belongs to a
593
+ `call_tool` result too large to return inline, and a program's own
594
+ return is refused paging by design rather than truncated into friction. There is
595
+ nowhere to put arguments, results, program source, or
596
+ raw error text; a caught failure is still recorded. `address` is
597
+ canonical (`A1`) where a tool resolved, otherwise the name the program used
598
+ for a shortcut its sanitized alias, the honest record of what was attempted.
599
+
600
+ **V3.** A call whose connector does not exist is recorded at the address as
601
+ written, *provided* it split into the two fields activity keeps — one with no
602
+ interior dot records nothing. An invented id is the address mistake an operator
603
+ most needs to see. But recording it as written puts caller-authored text in
604
+ fields that are otherwise operator- and connector-authored, so `connectorId`
605
+ and `toolName` clamp at 128 UTF-8 bytes (`address` at 257) with a `…` marker:
606
+ payload-free *by construction* means the event has nowhere to put a payload,
607
+ not merely that connecta declines to.
543
608
 
544
609
  **V4.** The execution itself emits no event. It has no address, and its one
545
610
  distinctive artifact is the program source, which is exactly what a payload-free
@@ -614,8 +679,8 @@ entry. Programs that ran before still run.
614
679
 
615
680
  - **`connecta.batch` failures gained `errorDetails`** (`S7`). They carried only a
616
681
  message, which left a program unable to tell a policy refusal from a transient
617
- failure. Additive, and it reuses `batch_call`'s field names so one shape covers
618
- both surfaces.
682
+ failure. Additive, and it reuses the host's internal batch field names, so a
683
+ program and the host describe a failed call the same way.
619
684
  - **A policy refusal can no longer look retryable** (`E7`). Pinned in code rather
620
685
  than read out of message text, so a connector named `svc-503` stops flipping a
621
686
  permanent refusal to `retryable: true`. This reaches the call tools too.
@@ -632,6 +697,10 @@ The middle three were places where the contract described behavior the code did
632
697
  not quite have. The code moved, because the described behavior is the one worth
633
698
  having.
634
699
 
700
+ One surface was added since: [emitted output](#emitted-output) (`M1`–`M10`,
701
+ [#270](https://github.com/zackbart/connecta/issues/270)) — additive by
702
+ construction, with the byte-for-byte no-emit promise pinned by test.
703
+
635
704
  ## Verification
636
705
 
637
706
  Every clause has a test. `test/guest-contract-cases.ts` holds the case table,
@@ -681,19 +750,24 @@ the upstream `Executor` shape assignable.
681
750
  | `L5`, `X2` | `test/quickjs-executor.test.ts` (CPU, heap) |
682
751
  | `L6`, `X10` | `test/quickjs-executor.test.ts` (bridge and IPC bounds for arguments and result; the address in the over-bound message) |
683
752
  | `L7` | `test/execute.test.ts`, `test/executor-admission.test.ts` |
684
- | `V1`, `V2` | `test/guest-api-contract.test.ts` (dispatched calls, and the four refusal classes that name a connector), `test/activity.test.ts` |
685
- | `V3`, `V4` | `test/guest-api-contract.test.ts` (no event without a connector) |
753
+ | `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) |
754
+ | `M1` | `test/guest-api-contract.test.ts` (invalid emits throw catchably, accept nothing), `test/execute-emit.test.ts` (every rejected shape) |
755
+ | `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) |
756
+ | `M4` | `test/guest-api-contract.test.ts` (discard is visible), `test/execute-emit.test.ts` (structured and plain paths) |
757
+ | `M5`, `M7` | `test/execute-emit.test.ts` (both budgets fail the crossing block; host-call budget untouched) |
758
+ | `M6`, `M9` | verdicts; `M1`'s strict typing and `M2`'s collect-then-deliver are their enforcement |
759
+ | `M8` | two arms passing one case table, `test/codemode-compat.test.ts` |
760
+ | `M10` | `test/execute-emit.test.ts` (aggregate present, numbers only, absent when nothing emitted) |
686
761
  | `X3` | `test/quickjs-executor.test.ts` (cancels a running child) |
687
762
  | `X4` | `test/guest-api-contract.test.ts` (string logs only) |
688
763
  | `X6` | `test/quickjs-executor.test.ts` (never-settling await) |
689
764
  | `X7` | `P3`'s tests; the Workers superset is deliberately unused |
690
765
 
691
- The surface itself is checked by `test/server.test.ts` (the exact seven, nine, and
692
- ten tool lists) and `test/code-first-surface.test.ts` (the fold's construction
693
- rules, refusals, copy, and measured size). The release audit compares the same
694
- two shapes:
766
+ The surface itself is checked by `test/server.test.ts` (the exact seven-tool
767
+ list) and `test/code-first-surface.test.ts` (the fold's construction rules, the
768
+ required executor, the refusals a removed top-level tool now gets, copy, and
769
+ measured size). There is one shape left to audit, so there is one audit:
695
770
 
696
771
  ```sh
697
772
  npm --prefix eval/current-version run audit
698
- npm --prefix eval/current-version run audit -- --executor disabled
699
773
  ```
@@ -33,7 +33,7 @@ Connecta deliberately sits between protocol generations
33
33
  [#206](https://github.com/zackbart/connecta/issues/206).
34
34
  - **Multi-round-trip results:** a downstream `input_required` result becomes a
35
35
  non-retryable `input_required_unsupported` failure. `call_tool`, the
36
- `execute_code` host bridge, and classic's `batch_call` all preserve the
36
+ `execute_code` host bridge and internal batch path both preserve the
37
37
  structured code. Relaying the
38
38
  opaque `requestState` is architecturally possible but gated until real hosts
39
39
  and downstreams adopt it.
@@ -4,26 +4,20 @@ 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
5
  annotations, and `get_result` pages bounded results.
6
6
 
7
- ## Which surface a deployment serves
7
+ ## The deployment surface
8
8
 
9
- The `executor` decides it, and there is nothing else to configure
10
- ([#224](https://github.com/zackbart/connecta/issues/224)):
11
-
12
- | | `tools/list` | Discovery breadth and batching |
13
- | --- | --- | --- |
14
- | **executor configured** | seven: `execute_code`, `search_tools`, `call_tool`, `call_destructive_tool`, `authorize_connector`, `get_result`, `skills` | `connecta.search`, `connecta.describe`, `connecta.batch` inside a program |
15
- | **no executor** | nine: the above minus `execute_code`, plus `list_connectors`, `describe_tools`, `batch_call` | those three top-level tools |
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 breadth and
12
+ batching live in `connecta.search`, `connecta.describe`, and `connecta.batch`
13
+ inside a program ([#273](https://github.com/zackbart/connecta/issues/273)).
16
14
 
17
15
  Code-first is what a model sees. Four overlapping ways to reach one connector
18
16
  became two: `search_tools` then `call_tool` for a single cold read — measurably
19
17
  cheaper direct than through a program — and `execute_code` for everything wider.
20
- The fold is worth 19.6% of the serialized tool definitions measured against the
21
- ten-tool shape an executor-backed deployment used to serve 10,675B to 8,587B —
22
- and, more durably, one fewer routing decision a model makes before doing any
23
- work. Note which baseline that is: the executor-free nine serialize to 7,207B,
24
- so the seven-tool surface is *larger* than the row below it in that table. It
25
- buys the program with those bytes. The [guest API contract](./code-mode.md) is
26
- what a program is promised.
18
+ The consolidation removed overlapping routing choices while preserving the
19
+ cheaper direct path for one cold call. The [guest API contract](./code-mode.md)
20
+ is what a program is promised.
27
21
 
28
22
  `execute_code` accepts optional `diagnostics: true` when a caller is measuring
29
23
  a workflow. It adds only compact request-local timing and serialized-size
@@ -31,20 +25,11 @@ aggregates; normal calls carry no diagnostics block or response-context cost.
31
25
  The measurements never contain program source, arguments, values, addresses,
32
26
  credentials, logs, or raw error text.
33
27
 
34
- Classic is the compatibility surface: what an executor-free deployment
35
- necessarily serves, since the program surface the fold depends on is not there.
36
- It is supported and tested, not an equal citizen in the docs. `surface:
37
- "classic"` beside an executor is the only override; it produces the ten-tool
38
- shape the [eval gate](../eval/code-first-gate/README.md)'s *incremental* arm
39
- measures. That gate's control arm is executor-free classic and needs no
40
- override.
41
-
42
28
  Nothing became unreachable. `connecta.describe` takes the same addresses and
43
- formats as `describe_tools`, `connecta.batch` runs the same 1–10 parallel
44
- read-only calls as `batch_call` and returns the same typed outcomes, and an
45
- unfiltered `connecta.search({})` browses every catalog a program can reach
46
- the part of `list_connectors` a model used. Live connector probing was the rest
47
- of it, and that is an operator concern: the operator pages and `/health` own it.
29
+ formats as the internal catalog service, `connecta.batch` runs 1–10 parallel
30
+ read-only calls and returns typed outcomes, and an unfiltered
31
+ `connecta.search({})` browses every catalog a program can reach. Live connector
32
+ probing is an operator concern: the operator pages and `/health` own it.
48
33
 
49
34
  ## Discovery context
50
35
 
@@ -61,8 +46,8 @@ shape. Bounded plain-object schemas also expose `inputKeys`,
61
46
  list rather than repeating a large partial inventory. Matches carry declared
62
47
  behavior annotations. When
63
48
  that shape is sufficient, call the returned address directly. Reserve schema
64
- expansion `connecta.describe` in a program, `describe_tools` on the classic
65
- surface — for a search without schemas, an ambiguous compact shape, or exact
49
+ expansion through `connecta.describe` for a search without schemas, an
50
+ ambiguous compact shape, or exact
66
51
  constraints that require `format: "json"`.
67
52
 
68
53
  Compact search is deliberately a routing view, not a second copy of connector
@@ -89,6 +74,14 @@ mode; they are not a duplicated Connecta object result. Newly stashed JSON and
89
74
  downstream content envelopes use compact serialization, so `get_result` byte
90
75
  offsets and totals refer to that exact compact text.
91
76
 
77
+ A `call_tool` truncation notice carries both the historical `resultId` and an
78
+ exact `nextAction: { tool: "get_result", arguments: { id, offset: 0 } }`. The
79
+ handle is therefore
80
+ directly actionable without copying an identifier out of prose; re-calling with
81
+ `fields` remains the smaller alternative when projection is possible. Program results and oversized discovery responses carry no such
82
+ route — paging a program's return value is a refused shape, because a program
83
+ can shrink anything before it returns.
84
+
92
85
  `fields` keeps its historical flat `{ "<path>": value }` result when every
93
86
  requested dot-path resolves. Dot notation traverses objects; append `[]` to an
94
87
  array field before continuing, as in `results[].id`. An exact downstream
@@ -172,6 +165,74 @@ only through the same-origin, Clerk-operator credential route. After OAuth
172
165
  consent or an operator update, retry the original operation; a static update is
173
166
  read from the vault on the next call and needs no redeploy.
174
167
 
168
+ ## Routing recovery
169
+
170
+ Predictable local refusals carry structured recovery on both result modes.
171
+ An unknown connector suggests an unscoped discovery query derived from the
172
+ attempted tool name; an unknown tool scopes the same query to the connector that
173
+ answered. The suggested route follows the route the caller took: `tool:
174
+ "search_tools"` for a top-level call, `function: "connecta.search"` with the same
175
+ arguments when the miss happened inside `execute_code`, which has no way to call
176
+ a tool. A read path that reaches an unannotated, write-capable, or destructive
177
+ tool returns `nextAction` for `call_destructive_tool` with the canonical
178
+ address. Nothing is executed by these records.
179
+
180
+ That route echoes the caller's own arguments back only while they fit a
181
+ 512-byte budget, and then whole — never clipped. An error envelope is not
182
+ size-guarded the way a result is, so an unbounded echo would let a large
183
+ argument object produce a refusal many times the deployment's result cap, on
184
+ both `call_tool` and calls a program routes through `connecta.call` or
185
+ `connecta.batch`. Over budget, `args` is absent and the `purpose` says to
186
+ re-send what was just sent: the agent already holds its own arguments, and half
187
+ of them would describe a call nobody made.
188
+
189
+ The address gets the same budget and the opposite rule: 512 bytes, clamped
190
+ with a trailing `…` rather than dropped. It is caller-authored too — an
191
+ invented one can be any length — and it reaches the error message *and* the
192
+ recovery query, each of which lands in both the text content and
193
+ `structuredContent`; unbounded, a 50 KB typo produced a 200 KB refusal under a
194
+ 1 KB result cap. Dropping it is not an option the way dropping arguments is:
195
+ the address is the thing being corrected, a clipped one still identifies the
196
+ mistake, and a short one — every real one — comes back exact and untagged.
197
+
198
+ Shortcut ambiguity inside `execute_code` returns every colliding canonical
199
+ address and points at `connecta.call`; the program or model must still choose
200
+ which one matches the user's intent. `call_destructive_tool` accepts an optional
201
+ `reason` of at most 500 characters for the host's human approval view. It is
202
+ outer-call context only: Connecta neither treats it as authority nor passes it
203
+ to the downstream connector, and an empty or whitespace-only one is read as no
204
+ reason rather than as a reason to refuse the call.
205
+
206
+ Activity carries an optional coarse `friction` class: `tool_not_found`,
207
+ `schema_retry`, `destructive_reroute`, `auth_required`, or `result_too_large`.
208
+ It is derived from the typed error code, except on the one call that has no
209
+ error code to derive from: a result too large to return inline is friction for
210
+ the agent while remaining `outcome: "success"`. That applies to a `call_tool`
211
+ result, the only source of `result_too_large` friction. (Activity stored by
212
+ older releases may still carry the retired `batch_call` source; nothing writes
213
+ it today.)
214
+ An oversized *discovery* response and an oversized program return are shaped
215
+ differently and produce none, and an `errorCode` is written only when the call
216
+ actually failed. The category adds no arguments, results, search text,
217
+ generated code, credentials, or raw errors.
218
+
219
+ An address whose connector does not exist is recorded too, as written, provided
220
+ it has the `<connectorId>.<toolName>` shape at all — a string that never split
221
+ into the two fields activity keeps still records nothing. A hallucinated
222
+ connector id is the most common address mistake, and an operator reading
223
+ activity should see it; addresses are already a first-class activity field, so
224
+ nothing new is retained.
225
+
226
+ What *is* new is that those fields now hold caller-authored text, so the
227
+ recording seam clamps them: `connectorId` and `toolName` at 128 UTF-8 bytes
228
+ each, `address` at 257, with a `…` marker. Far past any real id or tool name,
229
+ and far short of a 40 KB invented one. The clamp is structural rather than a
230
+ policy the writer applies, because "payload-free by construction" has to mean
231
+ the event type has nowhere to put a payload — a 40 KB connector id is a payload
232
+ wearing an id's clothing. Clamped rather than skipped: the invented id is
233
+ exactly what an operator needs to see, and its first 128 bytes say as much
234
+ about the mistake as all 40,000 would.
235
+
175
236
  ## Argument recovery
176
237
 
177
238
  A remote MCP tool's advertised `inputSchema` is checked in the shared
@@ -183,9 +244,11 @@ JSON Pointer `path`, schema-keyword `code`, and expected shape. Submitted
183
244
  values are never copied into those findings.
184
245
 
185
246
  At most three findings are returned; `validation.truncated` says when more
186
- exist. `nextAction` points to `search_tools` scoped to the same connector and
187
- tool name when the compact schema is needed, while `retry` says to correct the
188
- listed arguments and reissue the original operation. A schema the local
247
+ exist. `nextAction` points to discovery scoped to the same connector and tool
248
+ name when the compact schema is needed routed like any other miss, so a
249
+ program is sent to `connecta.search` and a top-level call to `search_tools` —
250
+ while `retry` says to correct the listed arguments and reissue the original
251
+ operation. A schema the local
189
252
  validator cannot evaluate passes through to the provider. Provider error prose
190
253
  is not parsed or guessed, so an unknown format remains
191
254
  `connector_call_failed`.