feat: Add room-task-board capability to band-sdk-python - #606
Open
AlexanderZ-Band wants to merge 12 commits into
Open
feat: Add room-task-board capability to band-sdk-python#606AlexanderZ-Band wants to merge 12 commits into
AlexanderZ-Band wants to merge 12 commits into
Conversation
Adds Capability.TASKS and 7 agent-surface tools (band_list_tasks, band_create_task, band_get_task, band_update_task, band_get_task_history, band_get_board, band_set_board) backed by band-client-rest's agent_api_chat_tasks client, wired through every adapter, the published band-mcp CLI (new `tasks` --tools group), and the shared MCP engine. - New band.core.task_types vocabulary (TaskListState, TaskLifecycleState, TaskAssignmentStatus) mirroring the Fern wire enums. - Root-caused and fixed a cross-provider JSON-Schema bug along the way: Pydantic renders a single-value Literal field as `const`, which Gemini's restricted JSON-Schema subset rejects. Fixed once in tool_filter.sanitize_tool_schema (const -> enum, lossless) and applied it to the MCP engine's tool registration path too, so band-mcp's advertised schema matches every other schema surface. - Hand-written wrappers for the three adapters that don't consume the central tool registry generically: CrewAI (+ CrewAIFlow), Pydantic AI, and Parlant -- the latter two use each field's real StrEnum type where possible so Parlant/pydantic-ai advertise real JSON-Schema enums instead of folding choices into prose. - Split the now-4000+-line src/band/runtime/tools.py into a src/band/runtime/tools/ package (types, registry, schema, agent, human, inputs/*) with a fully preserved public import surface -- it had grown unmanageable even before this feature's additions. - New/extended tests: unit coverage for all 7 AgentTools methods (with the real Fern client's autospec, catching two missing `request_options` retries the initial implementation dropped), framework-conformance registry/schema/dispatch checks, band-mcp wire-contract + config + standalone-spec coverage, and Parlant capability-gating tests. - Docs: platform-tools.md, capability-negotiation.md, and band-mcp's README get a Task Board Tools section. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
- sanitize_tool_schema() now runs inside platform_args_schema() (schema.py), the sole chokepoint CrewAI's tool schemas flow through -- band_get_task/band_get_board's single-value Literal include field was emitting raw JSON-Schema `const`, which Gemini's restricted schema subset rejects. The pydantic_ai wrapper hits the same const bug via its own signature-based schema builder (unrelated code path, doesn't go through platform_args_schema), fixed there by typing `include` as str (matching the parlant wrapper's existing workaround for the same constraint) and casting back to the real Literal type at the AgentToolsProtocol call boundary. - UpdateTaskInput's docstring promised "at least one field required" beyond id but had no validator enforcing it, so band_update_task(id=X) alone would silently issue a no-op REST write. Added the @model_validator(mode="after") this package already uses for the same kind of cross-field constraint (see StoreMemoryInput). - Removed two comments narrating refactor history in the new runtime/tools/ package, per the repo's comment-style rule. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
…s new return type platform_args_schema() now always returns a sanitized subclass, not the master model itself (see the schema.py change in 5d19baa) -- the doc's runnable snippet still asserted identity and was failing CI's markdown-docs gate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
- AGENTS.md, docs/platform-tools.md, docs/acp.md, packages/band-mcp/README.md, and two test-file docstrings still pointed at src/band/runtime/tools.py, which this PR's earlier package split (tools.py -> tools/) removed. - Four adapter docs (anthropic, claude_sdk, langgraph, codex) list every supported Capability/Emit in a table; Capability.TASKS was added to all four adapters' SUPPORTED_CAPABILITIES by this PR but never added as a row. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
INT-1370 shipped the 7 task-board tools but deferred the live E2E smoke. Adds declarative fixtures (TaskTool/TaskToolCalls/task_calls(), matching MemoryTool/MemoryToolCalls) plus two live scenarios: a per-adapter read/write coverage test, and a three-framework coordinator/specialist scenario where the coordinator hands off work through the task board (band_set_board/band_create_task) instead of chat, and each specialist claims, works, and completes its task via band_update_task alone. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
- SetBoardInput had no validator requiring at least one of goal_title/goal_summary, so band_set_board() with both omitted silently no-op'd and reported success. Add the same at-least-one-field validator UpdateTaskInput already has. - UpdateTaskInput's own validator used truthiness (`if not any(...)`) instead of `is not None`, so an explicit empty string (e.g. comment="") was incorrectly treated as unset and rejected. Fixed both validators to check `is not None`. - docs/capability-negotiation.md: clarify that only Capability.FILES has a CAPABILITY_FEATURE_FLAGS entry today -- MEMORY/CONTACTS/TASKS have none because the platform's AgentMe.feature_flags currently only documents ff_file_transfer, so they're always advertised. Verified against the installed band-client-rest's AgentMe docstring rather than assumed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
AlexanderZ-Band
enabled auto-merge
September 3, 2026 18:44
…ller
band.platform.posting.post_message/post_event is the new single REST
choke point for every chat send (AgentTools, the A2A/ACP/Slack bridges,
the contact hub-room notifier) — content with no visible characters
(whitespace-only, zero-width, bidi marks) is refused before the network
call instead of reaching the platform and 422ing, mirroring the
platform's own rule (Chat.validate_visible_content/1,
thenvoi-platform lib/thenvoi_com/thenvoi/chat.ex:3936) rather than a
naive non-empty check. Unlike the platform's rejection, this is
non-throwing (returns None) so a caller that can't tolerate an
exception mid-turn isn't put at risk by it.
SendMessageInput/SendEventInput (and the MCP front door's widened
SendEventWideInput) also reject blank content as a normal tool-argument
validation error, so an LLM calling band_send_message/band_send_event
directly gets an actionable message instead of a silent drop.
Also drops send_event's blank-content placeholder ("(no content)"):
every current tool_result emitter JSON-wraps its output, so the
placeholder's justification (a tool result with no text form) was
unreachable — what actually hit it was caller error or an ACP
THOUGHT/PLAN chunk with no text, both better served by the refusal
above. Event content truncation (the platform's 16384-char cap, which
only applies to events, not messages) moves into the shared choke
point too, so it covers the bridges that used to bypass it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
send_room_file's caption fallback only caught an empty string, so a whitespace-only or zero-width caption survived it, the send refused it (returning None), and `message.id` then raised AttributeError -- after the upload, leaving an orphaned attachment and handing the model 'NoneType has no attribute id' instead of anything actionable. It now falls back on has_visible_content, the same rule the send enforces. The human-scope twin, band_send_my_chat_message, posts through human_api_messages -- a namespace post_message/post_event don't cover -- so SendMyChatMessageInput.content carries the shared require_visible_content validator instead, and the AST boundary test's docstring records that (plus the scan's real limits: src/band only, direct x.method(...) calls only). FakeAgentTools now refuses blank content the way the real tools do (return None, record nothing), so a blank send can't pass a unit test and fail only in production -- the same fidelity principle its mention requirement already documents. Drops post_message/post_event's `if not response.data: raise` guard and the four tests pinning it: the Fern responses type `data` as required, so a null would fail inside the client's own validation and the branch can only fire against a mock. Both functions are keyword-only now, since (rest, room_id, request) with a bare id in the middle is exactly the shape that swaps silently at a call site holding more than one id. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
post_message now swallows blank/whitespace-only content into a silent None return instead of raising. handle_prompt in the ACP server adapter didn't check for that, so a blank prompt (blank text and/or no other room participants to @mention) fell through to a 300s wait for a reply that could never arrive, since nothing was actually posted. Check the return value, clean up the pending prompt, and raise ValueError with the platform's existing blank-content message immediately. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
…2A gateway hang Three real gaps surfaced by review of the task-board split: - UpdateTaskInput/SetBoardInput's "at least one field" model_validator only fires for callers that construct/validate that model. Parlant and pydantic-ai hand-register update_task/set_board as plain functions and never do, so an all-None call silently reached the REST API as a no-op PATCH/PUT. The guard now also lives in AgentTools.update_task/set_board themselves, so every caller is covered regardless of how it got there. - post_message/post_event dropped the old "raise if the platform returns no response data" check when carved out of the old AgentTools methods into the shared posting choke point, so a 2xx-with-null-body backend anomaly became indistinguishable from a blank-content refusal (silent None, no log). Restored the RuntimeError guard, matching every sibling REST call in agent.py. - A2AGatewayAdapter._send_to_band ignored post_message's None return (the same shape of bug band ACP's handle_prompt was already fixed for in this branch): a blank-content A2A request would post nothing and then hang in _await_response for the full response_timeout_s instead of failing fast. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
…sdk-python-INT-1370 Reconciles the room task-board capability (Capability.TASKS) with the already-merged Capability.FILES rollout across every adapter (#600): adds BandTool as the single agent-tool-name vocabulary and the image-passthrough/redaction helpers to the new src/band/runtime/tools/ package (split from the old tools.py on this branch), wires the 7 task tools into crewai's declarative ToolSpec catalog and parlant's capability-scoped tool closures, and restores MCP schema sanitization (needed by the task tools' Literal fields) alongside structured_output passthrough (needed by band_read_room_file's image results) in the MCP engine's single tool-registration path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
amit-gazal-band
requested changes
Sep 5, 2026
amit-gazal-band
left a comment
Collaborator
There was a problem hiding this comment.
A few correctness gaps and consistency nits from reviewing the room task-board changes. Note: the pydantic_ai/parlant at-least-one-field bypass on update_task/set_board, and the response.data guard in agent.py's task methods, were both already resolved by b5cfd9b1 (the guard turned out to be load-bearing, not dead code), so neither is repeated below. Two other blank-content-swallowed-as-success bugs (CrewAI's _send_message in catalog.py, pydantic_ai's band_send_message/band_send_event) are real but predate this PR -- that code hasn't changed since well before this branch, so they're not part of this diff; flagging separately rather than as inline comments here.
…eview - Add validate_include() in AgentTools.get_task/get_board so an invalid include value can't ride Parlant/pydantic-ai's unchecked str-to-Literal cast straight through to the REST API. - Extract a shared at_least_one_of() helper and use it for UpdateTaskInput/SetBoardInput's validators, AgentTools.update_task/ set_board's backstops, and a new RemoveMyContactInput validator (was 4 hand-written copies, now 5 call sites sharing one). - Unify the "history" include value on one TaskIncludeOption = Literal["history"] alias instead of a disconnected one-member enum. - Add an accurate comment on Parlant's include cast: verified against the installed parlant==3.3.2 source that its own schema builder raises at tool-registration time for a bare Literal -- a different, harder failure than pydantic-ai's Gemini-only const rejection. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LBTVbqCTPwkbEM9WKgYZAj
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.
Summary
Adds
Capability.TASKS: 7 agent-surface tools (band_list_tasks,band_create_task,band_get_task,band_update_task,band_get_task_history,band_get_board,band_set_board) backed byband-client-rest'sagent_api_chat_tasksclient.band.core.task_typesvocabulary:TaskListState,TaskLifecycleState,TaskAssignmentStatus,TaskIncludeOption.Capability.TASKSwired into all 16 adapters, atasks--toolsgroup on theband-mcpCLI, and hand-written wrappers for the three adapters (CrewAI/CrewAIFlow, Pydantic AI, Parlant) that don't consume the central tool registry generically.tool_filter.sanitize_tool_schemaconverts a single-valueLiteralfield's JSON-Schemaconsttoenum(Gemini's restricted JSON-Schema subset rejectsconst), applied in the MCP engine's registration path and the sharedplatform_args_schema()chokepoint. Pydantic AI's and Parlant's own signature-based schema builders can't take aLiteraldirectly, so theirincludeparameter is typedstrand cast back toLiteral["history"]at theAgentToolsProtocolboundary;AgentTools.get_task/get_boardvalidate the value at runtime regardless of which path a caller takes.UpdateTaskInput,SetBoardInput, andRemoveMyContactInputeach require at least one settable field, enforced by a sharedat_least_one_of()helper (band.core.validation);AgentTools.update_task/set_boardre-check the same rule directly, so adapters that hand-register tools instead of constructing the Pydantic input model are covered too.AgentToolsmethods passesrequest_options=DEFAULT_REQUEST_OPTIONS(3x retry on transient failure), matching every otherAgentToolsmethod.src/band/runtime/tools.py(4000+ lines) split into asrc/band/runtime/tools/package (types,registry,schema,agent,human,inputs/*); the public import surface is unchanged.Capability.TASKSadapter.Also included: reject blank/whitespace-only content at the source
Stacked on this branch because its core file,
src/band/runtime/tools/agent.py, only exists after thetools.py→tools/split above. Companion fix: band-sdk-typescript#174.band.core.content.has_visible_content— shared predicate mirroring the platform's own visible-content rule (Unicode categories L/N/P/S; whitespace/zero-width/bidi-mark-only strings don't count).contentfield (SendMessageInput,SendEventInput,SendEventWideInput,SendMyChatMessageInput).band.platform.posting.post_message/post_event— the one choke point every chat-send REST call insrc/bandroutes through; refuses blank content with a log warning instead of hitting the platform. Enforced by an AST guardrail,tests/platform/test_posting_boundary.py.send_room_filerejects a whitespace-only caption instead of raisingAttributeErrorafter the file upload succeeds.ACPServerAdapter.handle_promptandA2AGatewayAdapter._send_to_bandfail fast on blank content instead of hanging out the full response timeout.FakeAgentToolsrejects blank content the same way the real tools do.Test plan
uv run ruff check ./uv run ruff format --check .uv run pyrefly checkuv run pytest tests/ --ignore=tests/integration/ --ignore=tests/e2e/ -v(dev venv, full suite green)UV_PROJECT_ENVIRONMENT=.venv-crewai uv run pytest ...(crewai-specific suites)UV_PROJECT_ENVIRONMENT=.venv-parlant uv run pytest ...(parlant-specific suites)uv run pytest --markdown-docs $(git ls-files '*.md' ':!:examples/*') --no-covtests/runtime/test_task_tools.py,tests/core/test_validation.py,tests/framework_conformance/test_task_tool_conformance.pytests/runtime/test_tool_definitions.py,tests/mcp/test_wire_contract.py,test_config.py,test_standalone_spec.py,tests/integrations/parlant/test_tools.py,tests/framework_conformance/test_tool_name_drift.pyE2E_TESTS_ENABLED=true uv run pytest tests/e2e/baseline/smoke/behavior/test_multi_agent_collaboration.py -k task_board -v -s --no-covand... test_capability_matrix.py -k task -v -s --no-cov, both against the real dev platform.tests/core/test_content.py,tests/platform/test_posting.py,tests/platform/test_posting_boundary.py; extendedtests/runtime/test_tools.py,tests/testing/test_fake_tools.py,tests/integrations/acp/test_room_emitter.py,tests/integrations/acp/test_server_adapter.py,tests/mcp/test_engine.py;docs/rest-client.mdgained a runnable markdown-docs snippet.Followup filed: INT-1375 — genericize
pydantic_ai.py's per-tool hand-written registration into a schema-driven factory (assigned to Alexander Zaikman).🤖 Generated with Claude Code
https://claude.ai/code/session_01LBTVbqCTPwkbEM9WKgYZAj