Skip to content

Make disclosure's resource-store contract explicit and injectable (BodyLoader -> Mapping resource store) + SearchHit.to_dict() #36

Description

@thorwhalen

Summary

Formalize ir_09 §5 pointer-passing into a documented, injectable resource-store contract on disclose/discover, and guarantee leaf outputs survive a serialization/subagent boundary. ir already implements lazy store[pointer] (Disclosure.pointer + POINTER_KEYS + _default_body_loader, select.py:493+) — but the dereference is hard-wired to local disk paths, and SearchHit lacks a public to_dict() (its score may be a numpy float from dense ranking).

Why (rationale)

  • ir_09 §5: retrievers/subagents return ranked pointers + snippets, never full payloads; the full payload is dereferenced on demand via store[pointer] — a MutableMapping resource store. ir's _default_body_loader IS that, but only over disk.
  • Making the store injectable (Mapping[pointer, payload]) lets ir's lazy disclosure work over dol stores / URLs / blob storage — the exact substrate the agent layer (and app_ef) needs, and a clean DI win for ir's own non-agent callers.
  • ir_09's MultiAgentAgent (§7) runs subagents in isolated contexts that return distilled pointer-sets across a process/serialization boundary. ir must guarantee its leaf outputs are lossless + numpy-free + carry the pointer so a lead can ir.disclose them later.

Scope

  • Promote BodyLoader (select.py:67, already Callable[[Mapping], str | None]) to a documented Protocol.
  • Add an optional store: Mapping[str, Any] | None = None param to disclose (and pass-through on discover): when given, a pointer is dereferenced as store[pointer]; when absent, fall back to today's disk _default_body_loader.
  • Preserve purity + stale-tolerance: a missing key / unreadable target -> body=None and metadata['disclosure']='pointer_unreadable' (today's behavior).
  • Add SearchHit.to_dict() casting score to float; audit that no numpy scalar leaks into any *.to_dict() (Selection, Disclosure, DiscoveryResult already have to_dict).
  • The eager-vs-lazy question (ir_09 §9) stays the caller's: ir hands back additive Disclosure objects either way.

API sketch (proposed — store= param, ResourceStore, SearchHit.to_dict do not exist yet)

# ir/select.py
from collections.abc import Mapping

ResourceStore = Mapping[str, Any]  # PROPOSED: pointer -> payload (dol store / URL map / blob)

def disclose(selection, *, level='body',
             loader: 'BodyLoader | None' = None,
             store: 'ResourceStore | None' = None) -> list[Disclosure]:  # PROPOSED store=
    # if store is not None: body = store.get(pointer)  (stale-tolerant)
    # else: today's _default_body_loader (disk)
    ...
# ir/base.py
@dataclass(frozen=True)
class SearchHit:
    artifact_id: str
    surface_kind: str
    score: float
    text: str
    metadata: dict
    def to_dict(self) -> dict:   # PROPOSED
        return {'artifact_id': self.artifact_id, 'surface_kind': self.surface_kind,
                'score': float(self.score), 'text': self.text, 'metadata': dict(self.metadata)}

Design notes

  • No new hard dependency; store is any Mapping (a dol store satisfies it).
  • Keeps stale-tolerance: disclosure never raises; missing pointer -> body=None.
  • The numpy-free guarantee is what makes the substrate edge safe for cross-process subagent returns (§7) and for the qh/HTTP facade.

Acceptance criteria

  • disclose(sel) with no store= behaves exactly as today (disk loader).
  • disclose(sel, store={pointer: 'body text'}) dereferences via the mapping; a missing key -> body=None + disclosure='pointer_unreadable'.
  • SearchHit.to_dict() exists and casts score to a Python float.
  • A round-trip test: every *.to_dict() is json.dumps-able (no numpy scalars) for dense-ranked hits.
  • BodyLoader / ResourceStore are documented as the §5 resource-store contract.

Refs ir_09 §5 (pointer-passing context engineering), §7 (multi-agent subagent boundary), §9 (eager vs lazy dereference — caller's choice).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions