feat: room file tools behind Capability.FILES - #525
Draft
eric-descourtis-thenvoi wants to merge 14 commits into
Draft
feat: room file tools behind Capability.FILES#525eric-descourtis-thenvoi wants to merge 14 commits into
eric-descourtis-thenvoi wants to merge 14 commits into
Conversation
Adds three built-in agent tools for the platform's file-transfer surface: band_list_room_files, band_read_room_file, band_send_room_file. They speak to the agent file endpoints through the REST client's own transport (the generated client does not expose them yet) and are gated behind the new Capability.FILES, default off, because the endpoints require a deployment with file storage configured. read_room_file returns images as MCP-shaped content so runtimes that forward MCP blocks give the model real vision input; the Claude SDK bridge now passes such results through instead of json-dumping them into a text block, and stops stripping a custom tool's room_id argument when the input model declares that field as part of its own contract. CrewAI and PydanticAI get concrete wrappers; the tool-family registries, prompts drift checks, protocol, and testing fake are extended accordingly. Tool-name sets treat files like memory: excluded from BASE/CHAT, included in ALL. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
|
Dogfooding note: while live-testing this branch, agent turns went silent-empty (~1s, $0.00, no tool calls) and the adapter logged nothing. A probe with
|
The agent messages index is oldest-first and offers no descending order, so taking one page per delivery status returns the OLDEST messages in the room. In any room with more than a page of history the newest files — the ones an agent is nearly always asking about — are exactly the ones missing, and reversing a page of the oldest messages cannot recover them. The tool's own header says 'newest first'. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The index is oldest-first by default, so the four listing queries returned the FIRST fifty messages ever addressed to this agent and reversing that page could not recover the newest — the tool's primary use, 'find the file someone just sent me', silently answered with the oldest files. Two tests in this repo have asserted sort_order=desc since the feature commit and have been red ever since; the platform now accepts the parameter on the cursor path. With the server returning newest-first the local reversal is removed: it would put the oldest of the page on top and truncation would keep the wrong end. The paging test added alongside this work is retired — the two sort_order tests already pin the contract, and walking cursors is no longer how the newest page is reached. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r adapters read_room_file returns MCP content so a bridge can hand the model real vision input. Only the Claude bridge forwards that shape: CrewAI json-dumps the tool result and pydantic-ai stringifies it, so up to the 3 MiB inline limit — about 4.2 million characters once base64-encoded — arrives in the model's context as prose. The shared tool description promises 'Images are shown to you directly' on all four adapters that now advertise Capability.FILES. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
describe_tool_result_as_text renders MCP content as plain text, keeping the text blocks (which already name the file, its type and its size) and replacing the image block with a line saying the picture cannot be shown here. CrewAI and pydantic-ai now call it around read_room_file. Both serialize whatever the tool returns, so the base64 payload — about 4.2 million characters at the 3 MiB inline limit — used to arrive in the model's context as prose: no picture, an unusable context, and the bill for both. The Claude bridge keeps the MCP shape and keeps real vision. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
replied gates the adapter's 'the agent said nothing this turn' error, and it is keyed to the single name band_send_message. band_send_room_file posts an attachment message to the room — a real, room-visible response — so an agent that answers by sharing a file is reported as having produced no reply, and the room gets a spurious error after a successful share. is_room_posting_tool already knows the answer and is the shared vocabulary for exactly this question. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reply gate now asks is_room_posting_tool instead of comparing against one name, so band_send_room_file — which posts an attachment message to the room — stops being reported as 'the agent said nothing this turn'. That error used to land in the room right after a successful share. _SEND_MESSAGE_TOOL stays: it still names the tool whose arguments carry mentions to resolve, which is a different question from 'did this reach the room'. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…missing Phoenix answers an unrouted path with 404, and read_room_file maps every 404 to 'no such file in this room (check the file_id with band_list_room_files)'. On a deployment without the agent file routes that is false for every id the agent tries: it concludes each file is gone, while the listing tool — which reads a different endpoint — keeps showing them. The agent then loops list, read, list. The second test pins the discrimination: a genuine missing file must still be sent to the listing tool. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… size Two answers that were wrong in the same direction, both about telling the agent what actually happened: - A 404 from an unrouted path is no longer reported as 'no such file'. Phoenix answers an unrouted path with 404 too, so on a build without the agent file routes every id read as missing while the listing tool kept showing them — an endless list, read, list. The discriminator is the body: a real answer from the file routes goes through the API error view and carries an 'error' object, the framework's own not-found does not. - send_room_file bounds text_content at 256 KiB. Unbounded, a model could materialise megabytes of string and hold it encoded and raw before the platform's own cap ever answered. The pre-existing missing-file test carried a bodyless 404, which no real file route returns; it now uses the platform's actual error shape, which is what makes the discrimination testable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The capability check is what puts the three room-file tools in front of a model. Nothing asserted it, so deleting the check left every suite green while the tools shipped to agents that never asked for them — and the gate is the whole opt-in story for a feature that reads a room's files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Capability.FILES is the operator saying "this agent may use files"; it cannot say "this deployment has them". One build meets SaaS nodes, on-prem nodes granted room files and on-prem nodes without them, and today it advertises the tools to all three — so on the third the model spends real turns discovering a 404. Only an explicit false counts as a refusal: a platform that predates the capability block still serves the endpoints, and reading its silence as a no would strip the tools from a deployment that works today. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The agent already fetches its own identity at connect to validate its key, and the platform now answers that call with the optional features this deployment serves. Reading it there costs no extra round trip. A refused capability is removed from the adapter's feature set before on_started, because that is where adapters build their tool lists — and the capability set is the one seam all of them read, so dropping an entry hides its tools in every framework at once. Only an explicit false refuses. Silence is not a no: a platform that predates the capability block still serves the file endpoints. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The invariant that is easy to break and expensive to rediscover: the prune has to land before on_started, because that is where every adapter builds its tool list, and silence from the platform is not a refusal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…through The read works only because the Fern models accept extra keys — they were generated before this field existed. Regenerated with extra="forbid" the block would be dropped in transit, every deployment would look like one that said nothing, and the file tools would stay advertised against nodes that 404 them with nothing erroring anywhere. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Three built-in room file tools behind a new opt-in
Capability.FILES:band_list_room_filesband_read_room_fileband_send_room_filePlus two Claude SDK bridge fixes the vision path needs:
_make_resultpasses through results that are already MCP-shaped ({"content": [...]}) instead of json-dumping an image block into base64 prose.room_idas a real input field keeps it; the strip now applies only to the schema-injected extra.How
band-client-rest==0.0.10) doesn't expose the agent file endpoints yet, so the tools use the client's own transport (same base URL/auth/pool) with a comment marking the migration point once Fern regen lands.BASE/CHAT, present inALL, ownFILE_TOOL_NAMESset and"files"category.band_send_room_filecounts as a room-posting tool for reply suppression; the two readers joinREAD_ONLY_TOOL_NAMES.claude_sdk(definition-driven, vision capable),pydantic_aiandcrewai/crewai_flow(concrete wrappers). Other adapters keep their current capability set — enabling them is a follow-up per adapter.processing/processed/pending/unfiltered) because the message that triggered the current turn is stillprocessing— skip that view and "the file you were just sent" doesn't exist yet.Capability negotiation (added since first review)
Capability.FILESis the operator saying "this agent may use files". It cannot say "thisdeployment has them" — one SDK build talks to the SaaS node, to an on-prem node whose licence
grants room files and to one whose licence does not, and only the last 404s these endpoints. So
the SDK asks.
GET /api/v1/agent/menow answers with afeature_flagsblock (Agent API 1.15.0) under the sameff_*keys the web app and JAM read from their own boot payload — one vocabulary, three clients.PlatformRuntimekeeps the answer from the metadata fetch it already performs, so there is noextra round trip, and
Agent.start()drops any refused capability from the adapter'sAdapterFeaturesbeforeon_started. Every adapter builds its tool list there and they allread
features.capabilities, so one prune hides the tools in every framework at once.Only an explicit
falserefuses. A platform that answers nothing is not saying no — it maypredate the flag while still serving the endpoints, so reading silence as a refusal would take
working tools away from a deployment that has them.
src/band/runtime/capabilities.pyis the oneplace the flag key is spelled.
The platform half is thenvoi-platform #2651, which also puts the whole feature behind
ff_file_transfer, off by default until a deployment asks for it.Platform dependency
The endpoints ship in thenvoi-platform PLT-1209/1210/1211 (in review). The capability defaults off everywhere, so this is safe to merge ahead of them; agents opt in with
AdapterFeatures(capabilities={Capability.FILES}).Proven end-to-end on a dev platform node: an agent read a text file word-for-word, described a webp book cover from pixels, and authored+attached its own file — the same flows now covered by 18 unit tests (
tests/runtime/test_file_tools.py,tests/integrations/claude_sdk/test_mcp_content_passthrough.py).Tests
uv run pytest tests/ --ignore=tests/integration/ --ignore=tests/e2e/: 4578 passed. The only residue is thedesktop_appsuite, which needs a workingnodeon PATH: in a full run this box's asdf shim answersunknown command: node, and those same tests pass in isolation here and on cleanmain.ruff check/ruff format/pyrefly check: clean.🤖 Generated with Claude Code