Skip to content

RFC-0061: Information-Preserving Degradation for ModalityFilter (design discussion) - #377

Merged
Million-mo merged 9 commits into
wolf1069b:mainfrom
Million-mo:docs/rfc-0061-modality-filter-information-preserving-degradation
Aug 21, 2026
Merged

RFC-0061: Information-Preserving Degradation for ModalityFilter (design discussion)#377
Million-mo merged 9 commits into
wolf1069b:mainfrom
Million-mo:docs/rfc-0061-modality-filter-information-preserving-degradation

Conversation

@Million-mo

@Million-mo Million-mo commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

RFC design discussion PR for ModalityFilterCapability degradation, now with a working implementation on this branch. The motivating defect: when the active model does not support a modality, the capability degrades an image to a bare MIME placeholder ([image/png]), which:

  1. Destroys retrievability — no filename, no location, no identifier.
  2. Blocks delegation — a text-only model cannot hand the image to a vision-capable subagent or file tool.
  3. Is misleading / hallucination-prone — reads like descriptive content that is actually absent.
  4. Fails silently — the user's intent (analyze this image) is lost without signal.

Motivation (real deployment)

Encountered while debugging a text-only model (glm52 / kimi-k2) failing on pasted images through the opencode server — the same downstream scenario as opencode issue #42758.

What this RFC does

Surveys 4 options with an evaluation matrix:

Option Description Retrievability Total
1. Status quo bare [image/png] placeholder 1/5 17/30
2. Metadata placeholder honest, hallucination-resistant text (no persistence) 2/5 24/30
3. Persist + reference session-scoped bytes + retrievable reference 5/5 24/30
4. Hybrid (recommended) metadata now + opt-in reference strategy 4/5 26/30

Recommends Option 4: upgrade describe() to honest metadata immediately (cheap, in-core), add an opt-in reference strategy for manifests that declare a vision-capable subagent/tool consumer.

Grounding: opencode ecosystem (HEAD 040b856)

  • Bare placeholders were rejected in PR #29279 on hallucination risk ("invites the LLM to invent details").
  • Bare placeholder is reported as a defect in #42758 ("agent has no way to access the actual image content").
  • Community consensus (#29216): "don't destroy the image reference in the first place".
  • Auto model-switching is explicitly NOT_PLANNED (#32601); persistence attempts (PR #32680 etc.) remain open/unmerged.

Implementation on this branch (2026-08-19)

Landing prototype of Option 4 + converter-layer mime integrity so binary content reaches the filter intact:

  • d06514464 — information-preserving describe (honest metadata placeholder with anti-hallucination guardrail) + opt-in reference strategy (session-scoped scratch persistence under tempfile).
  • 115ca7784BlobResourceContents.mimeType passthrough in MCP conversions; removal of the _DOCUMENT_FORMATS whitelist in ACP resource_to_content so arbitrary-mime embedded blobs surface as BinaryContent (octet-stream fallback). ModalityFilterCapability remains the single decision point for unsupported modalities.
  • RFC doc updated to reflect the implementation status (Current State, Goals superseded note, Decision Record).

Discussion ask

  1. Is the bare placeholder a genuine defect, or intended lossy behavior?
  2. Should AgentPool ship a storage-backed reference strategy (differentiating from opencode's unresolved PRs)?
  3. If persistence, where do bytes live (session storage / SQLite / fs scratch)?

Files

  • docs/rfcs/draft/RFC-0061-modality-filter-information-preserving-degradation.md (new, under review)
  • src/wolfharness/capabilities/modality_filter.py (implementation)
  • src/wolfharness/capabilities/modality_utils.py (implementation)
  • src/wolfharness/mcp_server/conversions.py (mime passthrough)
  • src/wolfharness_server/acp_server/converters.py (drop _DOCUMENT_FORMATS whitelist)

…ty filter

* docs(rfc): design discussion for modality_filter degradation

Proposes replacing the bare MIME placeholder ([image/png]) that
ModalityFilterCapability emits when degrading unsupported multimodal
content with an information-preserving placeholder carrying retrievable
metadata (filename/mime/source), and optionally a session-scoped
persisted reference so a text-only model can delegate the image to a
vision-capable subagent or file tool.

The problem is grounded in opencode ecosystem findings (HEAD 040b856):
bare placeholders were rejected in PR #29279 on hallucination risk, are
reported as a defect in issue #42758 (no way to access actual content),
and the community consensus (#29216) is to preserve the reference rather
than destroy it. Auto model-switching is deliberately out of scope
(NOT_PLANNED upstream).

Surveys 4 options (status quo / metadata-only / persist+reference /
hybrid) with an evaluation matrix and recommends the hybrid: upgrade
describe() to honest metadata now, add an opt-in reference strategy
for manifests that declare vision-capable consumers.
Phase 1 (RFC-0061): describe_multimodal_content() no longer emits a bare
[image/png] placeholder. Binary content now states its media type, that
direct model processing is unsupported, and whether a file identifier is
available. Control characters in caller-supplied identifiers are escaped
to prevent prompt injection via malformed filenames. URL content keeps its
[image: url] / [audio: url] form since the URL is already retrievable.

Phase 2: new opt-in 'reference' strategy persists binary content to a
per-session scratch directory and replaces it with a [file: <path>]
reference that a vision-capable subagent or the agent's read tool can
open. Scratch dirs live under tempfile.gettempdir()/wolfharness-modality/
{session_id}/; after_node_run() removes them. URL and UploadedFile content
has no local bytes and falls back to describe. The strategy is wired
through the ModalityFilterCapabilityConfig schema.
@Million-mo
Million-mo force-pushed the docs/rfc-0061-modality-filter-information-preserving-degradation branch from efbf553 to d065144 Compare August 19, 2026 07:41
…ersions

- mcp_server/conversions.py: pass BlobResourceContents.mimeType through
  to BinaryContent instead of hardcoding application/octet-stream
- acp_server/converters.py: drop _DOCUMENT_FORMATS whitelist gate in
  resource_to_content so arbitrary-mime embedded blobs reach the model,
  with octet-stream fallback; modality filtering stays downstream in
  ModalityFilterCapability
- add unit tests for both conversions (mime passthrough, fallback,
  image binary preservation)
- Current State: note original bare placeholder vs info-preserving impl landed
- Goals: mark 'no protocol server rewrites' superseded; document converter-layer
  mime passthrough rationale
- Decision Record: add 2026-08-19 entry for describe+reference+mime fix commits
pydantic-ai falls back to a short content hash (sha1[:6]) when no explicit
identifier is set. A bare hash is not a retrievable reference, so presenting
it as 'File: <hash>' misled text-only models into thinking they could open
it. Hash-shaped identifiers now degrade to the 'no file reference available'
branch; real paths from fsspec-style tools still surface.
…oldown

list_resources/list_resource_templates now query providers concurrently
under a 10s timeout; McpServerCap caches listings and invalidates on
reconnect (servers rarely send resources/list_changed) and on exit;
failed MCP connections enter a 30s cooldown so retries aren't re-paid
per call; viking per-directory ls runs in parallel; http(s) URIs are
forwarded to providers (MCP resources may legally use any RFC3986
scheme) instead of being rejected up front.
…e cap

read_resource returns image bytes (base64 blob + MIME) for vision-capable
models, gated on _should_return_image_bytes() and viking:// scheme; a
5MB cap degrades oversize images to a text URI hint on both read_resource
and viking_read; ToolReturn truncation preserves binary content instead
of serializing to bytes repr or char-splitting string content.
…erving-degradation

Resolve conflict in mcp_server_cap.py: integrate PR wolf1069b#384's direct client
fallback and _connect_via_pool() extraction with our connect cooldown.
The cooldown check now lives inside _connect_via_pool() (pool-based
retry path) while the direct-client fallback (no pool) proceeds without
cooldown.
@Million-mo
Million-mo merged commit 179d8ad into wolf1069b:main Aug 21, 2026
12 checks passed
@Million-mo
Million-mo deleted the docs/rfc-0061-modality-filter-information-preserving-degradation branch August 21, 2026 01:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant