Skip to content

Widen the capability binding matrix: composite outputs and richer scalar inputs #23

Description

@jmgilman

Problem

The capability binding matrix is narrow enough that the core Code Mode value proposition — a model writing a Starlark program that loops, filters, and aggregates over intermediate results in-runtime instead of round-tripping bulk data through the context window — is not expressible.

Current matrix (internal/binding/plan.go):

Direction Allowed
Input required string, optional *int64 — nothing else
Output flat struct of string / int64 / bool / float64 — no slices, no nesting, no optional fields

Concrete consequences:

  • A capability cannot return a list. "Search returns N results" is unrepresentable, so the canonical loop — fetch a collection, filter in Starlark, call a capability per surviving item, return a digest — cannot be written. Starlark's comprehensions and builtins have nothing to operate on.
  • A capability cannot take a required int, a bool, a float, or an optional string. page: int or dry_run: bool hit the wall immediately.
  • With composition unavailable, the model's best move is one native call wrapped in a mandatory def main(): — strictly worse ergonomics than a plain MCP tool for the same job. The program-authoring tax has no composition payoff.

The irony: the machinery to do this generically already ships. binding.ConvertFinal (internal/binding/output.go) is a generic Starlark→JSON walker supporting dicts, lists, tuples, and scalars, with depth limits, byte budgets, cycle rejection, int64 range checks, and NaN/Inf rejection. Only capability I/O (Plan.ConvertOutput and the fieldKind whitelist) is scalar-locked.

Proposal

Keep the registration API exactly as-is. Hosts still declare codemode.Capability[Input, Output] with plain Go structs. The change is what those types may contain.

Output side (the big unlock)

Accept any JSON-marshalable Go shape: slices, nested structs, pointer/optional fields, maps with string keys. Convert handler output to Starlark values (dicts/lists/scalars) with a generic reflection walker modeled on ConvertFinal's existing design, including its finiteness and range checks. Delete the output half of the fieldKind whitelist.

This should be a net deletion of complexity: the fieldKind enum and the per-kind switches in plan.go, signature.go, and output.go collapse into one walker.

Input side (widen, do not loosen)

Inputs stay strictly typed and validated before authorization, because binding produces the canonical argument map the authz.Authorizer evaluates. Widen the accepted kinds: required int64 / bool / float64, optional *string / *bool / *float64. Composite inputs (lists, nested objects) can follow as a second phase once output-side behavior is observed — they raise canonicalization questions that scalars do not.

Discovery stays derived, never declared

describe_api shapes must continue to be generated at registration from the reflected Output type so the schema the model sees cannot drift from handler behavior. binding.FieldShape will need to represent nesting and arrays (recursive shape or a JSON-Schema-like notation). No hand-written schemas.

Invariants that must not change

  • bind → canonicalize → authorize → dispatch ordering (internal/execution/execute.go)
  • The authorizer receives canonical arguments derived from validated typed input
  • MaxValueDepth / MaxResultBytes enforcement on values crossing the boundary
  • Registration-time rejection of genuinely unsupportable shapes (channels, funcs, interfaces, non-string map keys, cyclic types), with the existing field-level host-side error messages
  • Deterministic signatures and name-sorted discovery

Non-goals

  • Handlers returning JSON text (json.RawMessage / any passthrough): forfeits the derived describe_api contract and either strands the model with unparseable strings (no json module is predeclared) or reintroduces the same conversion one layer down.
  • Exposing starlarkjson or other stdlib modules to programs.
  • Changing the three-tool MCP surface.

Acceptance sketch

  • A capability declared with Output containing []struct{...} registers, and a Starlark program can for r in resp["items"]: over the result.
  • describe_api for that capability shows the nested/array shape, derived from the Go type.
  • A handler returning NaN, an over-range integer, or an unsupported kind still fails safely (capability failed), and unsupported types still fail at registration, not at call time.
  • docs/docs/reference/mcp-tools.md and docs/docs/reference/public-api.md updated to the widened matrix.

Origin

Product/UX review of the current surface at 8b5302b. Related but independent: the error-diagnostics issue (filed separately) — both target the model-side authoring loop.

Activity

  1. jmgilman commented on Aug 25, 2026

    @jmgilman
    ContributorAuthor

    Implemented and verified across six squash-merged pull requests:

    The acceptance path now registers []struct output, derives list[{...}] discovery, iterates it in Starlark, authorizes exact canonical scalar arguments, and returns a digest after one native call. Unsupported type graphs fail registration; non-finite/over-range returned numbers fail as capability failed; depth, per-value, and aggregate limits fail as resource limit exceeded.

    Final validation on the merged tree passed formatting, build, strict docs build, lint, the full test suite, the real MCP smoke tests, and the full race suite.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions