From dd80ca36551524a4fa2ee7f588caa96af4cab0fb Mon Sep 17 00:00:00 2001 From: Patrick Buckley Date: Mon, 15 Jun 2026 20:54:22 -0700 Subject: [PATCH] =?UTF-8?q?chore(attachments):=20hygiene=20sweep=20?= =?UTF-8?q?=E2=80=94=20dead=20code,=20stale=20comments,=20SDK=20type,=20pd?= =?UTF-8?q?f=20nit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove the unused PerceptionUnavailableError (never raised/caught/imported). - Reword the now-shipped 'Phase 3' placeholder comments on the Anthropic + OpenAI-Responses audio paths to describe the live upstream STT/perception fallback (these placeholders are defensive, not pending work). - Clarify the no-vision image fall-through comment (fires when perception is unconfigured OR can't see, not only the former). - Type AttachmentInfo.kind as the image|text|pdf|audio union in the TS SDK. - extract_pdf_text: append the truncation marker only when there's actual text, so a scanned PDF over the page cap returns '' (-> placeholder) instead of a content-free document part. --- sdk/typescript/src/types.ts | 3 +-- turnstone/core/pdf.py | 5 ++++- turnstone/core/perception.py | 4 ---- turnstone/core/providers/_anthropic.py | 7 ++++--- turnstone/core/providers/_openai_responses.py | 7 ++++--- turnstone/core/session.py | 5 +++-- 6 files changed, 16 insertions(+), 15 deletions(-) diff --git a/sdk/typescript/src/types.ts b/sdk/typescript/src/types.ts index 7402dfb7..21e685ae 100644 --- a/sdk/typescript/src/types.ts +++ b/sdk/typescript/src/types.ts @@ -96,8 +96,7 @@ export interface AttachmentInfo { filename: string; mime_type: string; size_bytes: number; - /** "image", "text", "pdf", or "audio". */ - kind: string; + kind: "image" | "text" | "pdf" | "audio"; } export type UploadAttachmentResponse = AttachmentInfo; diff --git a/turnstone/core/pdf.py b/turnstone/core/pdf.py index 6664ae25..55ef018f 100644 --- a/turnstone/core/pdf.py +++ b/turnstone/core/pdf.py @@ -54,7 +54,10 @@ def extract_pdf_text(data: bytes) -> str: textpage.close() page.close() text = "\n\n".join(p.strip() for p in parts if p.strip()) - if truncated: + # Only annotate truncation when there's actual text — otherwise a scanned + # (no text layer) PDF over the page cap would return just the marker, i.e. + # a content-free document part. Empty stays empty → caller placeholders it. + if truncated and text: text += f"\n\n[PDF truncated at {_MAX_PAGES} pages]" return text except Exception as exc: diff --git a/turnstone/core/perception.py b/turnstone/core/perception.py index a70dc645..991800eb 100644 --- a/turnstone/core/perception.py +++ b/turnstone/core/perception.py @@ -50,10 +50,6 @@ _DESCRIBE_PROMPT = ( ) -class PerceptionUnavailableError(RuntimeError): - """No usable perception backend is configured/resolvable (maps to a placeholder).""" - - class PerceptionBackendError(RuntimeError): """A configured perception backend failed during the perceive call.""" diff --git a/turnstone/core/providers/_anthropic.py b/turnstone/core/providers/_anthropic.py index 81537094..50044070 100644 --- a/turnstone/core/providers/_anthropic.py +++ b/turnstone/core/providers/_anthropic.py @@ -692,9 +692,10 @@ class AnthropicProvider: converted.append(block) continue if part.get("type") == "input_audio": - # Anthropic has no audio-input API. Phase 3 transcribes via the - # STT role upstream of this translator; until then surface a - # placeholder rather than passing an unsupported block to the API. + # Anthropic has no audio-input API. The STT-role fallback runs + # upstream of this translator (audio → text transcript when a + # model lacks supports_audio_input), so by here any remaining + # input_audio is a defensive placeholder, not the live path. converted.append( {"type": "text", "text": "[audio attachment — not supported by this model]"} ) diff --git a/turnstone/core/providers/_openai_responses.py b/turnstone/core/providers/_openai_responses.py index 849418ce..2402eb74 100644 --- a/turnstone/core/providers/_openai_responses.py +++ b/turnstone/core/providers/_openai_responses.py @@ -84,9 +84,10 @@ def convert_content_parts(parts: list[Any]) -> list[dict[str, Any]]: } ) elif ptype == "input_audio": - # Audio-input is not wired on the Responses lane; until the Phase 3 - # capability-gated fallback (STT) lands upstream, surface a - # placeholder rather than leaking an unhandled part to the API. + # Audio-input is not wired on the Responses lane. The capability-gated + # fallback (STT / perception) runs upstream of this translator, so by + # here any remaining input_audio is a defensive placeholder rather than + # an unhandled part leaking to the API. converted.append( {"type": "input_text", "text": "[audio attachment — not supported by this model]"} ) diff --git a/turnstone/core/session.py b/turnstone/core/session.py index 9b87c7b5..a603dafe 100644 --- a/turnstone/core/session.py +++ b/turnstone/core/session.py @@ -3015,8 +3015,9 @@ class ChatSession: perceived = self._perception_fallback_part(att, "image") if perceived is not None: return perceived - # No perception backend configured: emit the native image_url - # unchanged (the model may ignore it) — pre-existing behavior. + # No usable perception backend (none configured, or it can't see): + # emit the native image_url unchanged — a no-vision model ignores it. + # Image is intentionally left ungated here (pre-existing behavior). if kind == "audio" and not caps.supports_audio_input: return self._audio_fallback_part(att) return attachment_to_content_part(att)