Skip to content

ModelProvider: declare the kernel port for autolabeling — local and cloud adapters both ordinary #418

Description

@JArmandoAnaya

The decision this encodes

Recorded by Armando in the 2026-08-07 design session on the AI-assist substrate issue (#81),
which covered ModelProvider pre-annotation, the review loop in the UI, and importers:

ModelProvider is designed and declared now as a kernel port with no implementations,
shaped so that a local runner and a cloud service are both ordinary adapters later: MCP stays
a native first-class path; autolabeling will run locally and in the cloud eventually; nothing
ships yet, and nothing may be designed to preclude the cloud half. Local-first,
cloud-compatible after.

Nothing in this issue ships an implementation. The deliverable is a port whose shape
survives both futures.

Correction to the premise: the port already exists, as a placeholder

src/visionset/kernel/ports/model_provider.py is at HEAD (904b37a) and is exported from
kernel/ports/__init__.py:24. It is not a designed port and its own docstring says so —
"Phase 3 — declared now to fix the port surface". Its entire surface is:

def predict(self, asset: Asset, schema: AnnotationSchema) -> Sequence[Annotation]: ...

That shape fails the dual test in three ways, which is precisely the work:

  • Per-asset, so a provider that batches — which every hosted inference service does — pays
    one round trip per image.
  • Synchronous, returning a materialised Sequence, so a remote provider that takes minutes
    has nowhere to be except blocking its caller.
  • Discovered by nothing. Importer and Exporter name the visionset.formats entry-point
    group; this port names no group at all, so there is no way to install a provider even in
    principle.

So this issue re-shapes a declared placeholder. It does not add a new file to empty space,
and a PR that only creates one has not done the work.

The pattern to follow

kernel/ports/importer.py — a @runtime_checkable Protocol, discovered through an entry-point
group, zero implementations, documented as awaiting its producers. formats/registry.py
already filters one group by port (isinstance on an instance, because issubclass against a
protocol with data members raises), so reusing that group or declaring a new one both have
precedent — but the choice has to be made and argued rather than inherited.

Design questions the implementer answers in the PR, not in advance here

Every answer must hold for a hypothetical local ONNX runner and a hypothetical HTTP service
equally. That dual test is the acceptance criterion.

  • The invocation unit — asset, or batch? A per-asset call is simplest locally and worst
    remotely, which is exactly the tension the port has to resolve rather than pick a side of by
    accident.
  • The result shape — annotations carrying provenance='model', confidence, and model
    identity/version. The domain already requires model_ref whenever provenance is model
    (domain/annotation.py:69-71); the open question is whether that one string is the whole of
    model identity or whether the port owes more. These fields align with the work that surfaces
    provenance and confidence in the review loop (Model-produced annotations: provenance and confidence surfaced in the review loop #417), which is the rendering half of the same
    split.
  • How a slow or asynchronous provider reports progress without the port assuming a
    transport. The JobQueue port in kernel/ports/job_queue.py, which came out of the embedded
    job system with its SQLite adapter and in-process dispatcher (Embedded job system: a JobQueue port, a SQLite adapter, and an in-process spawn dispatcher #328), already exists and is
    explicitly not an executor — its separation of "where rows live" from "how the product asks
    for work" is the shape to lean on, not to duplicate.

Out of scope

Scope updated 2026-08-08: the port and the local adapter shipped as slice 2 (#434),
including cross-box NMS, the fp16 shims, and the weight-download job. This issue remains open
for the hosted adapter and the HTTP endpoint contract. The original scope text is preserved in
this issue's edit history.

  • Any adapter, local or hosted. When one is eventually built it inherits a constraint
    recorded on exercise the AI-assist substrate — ModelProvider pre-annotation, review loop in the UI, importers #81 and preserved here so it does not die with that thread: predictions are
    written into review_pending, never silently annotated, and schema validation is
    not relaxed for them.
  • Any UI.
  • Any job-queue infrastructure. If async turns out to need more than a port can express, flag
    it rather than build it
    — that is a separate decision.

Acceptance criteria

  • ModelProvider is a designed port rather than a placeholder: every element of its shape
    justified against both a local runner and a hosted service
  • Discovery is settled — an entry-point group is named, or its absence is argued in the
    docstring
  • Zero implementations ship
  • The docstring states the contract and its non-goals, in importer.py's and
    job_queue.py's register

Metadata

Metadata

Assignees

No one assigned

    Labels

    kernelvisionset.kernel — domain, ports, adapters, services

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions