Skip to content

feat: Add room-task-board capability to band-sdk-python - #606

Open
AlexanderZ-Band wants to merge 12 commits into
mainfrom
feat/add-room-task-board-capability-to-band-sdk-python-INT-1370
Open

feat: Add room-task-board capability to band-sdk-python#606
AlexanderZ-Band wants to merge 12 commits into
mainfrom
feat/add-room-task-board-capability-to-band-sdk-python-INT-1370

Conversation

@AlexanderZ-Band

@AlexanderZ-Band AlexanderZ-Band commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

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 by band-client-rest's agent_api_chat_tasks client.

  • New band.core.task_types vocabulary: TaskListState, TaskLifecycleState, TaskAssignmentStatus, TaskIncludeOption.
  • Capability.TASKS wired into all 16 adapters, a tasks --tools group on the band-mcp CLI, 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_schema converts a single-value Literal field's JSON-Schema const to enum (Gemini's restricted JSON-Schema subset rejects const), applied in the MCP engine's registration path and the shared platform_args_schema() chokepoint. Pydantic AI's and Parlant's own signature-based schema builders can't take a Literal directly, so their include parameter is typed str and cast back to Literal["history"] at the AgentToolsProtocol boundary; AgentTools.get_task/get_board validate the value at runtime regardless of which path a caller takes.
  • UpdateTaskInput, SetBoardInput, and RemoveMyContactInput each require at least one settable field, enforced by a shared at_least_one_of() helper (band.core.validation); AgentTools.update_task/set_board re-check the same rule directly, so adapters that hand-register tools instead of constructing the Pydantic input model are covered too.
  • Every REST call in the new task/board AgentTools methods passes request_options=DEFAULT_REQUEST_OPTIONS (3x retry on transient failure), matching every other AgentTools method.
  • src/band/runtime/tools.py (4000+ lines) split into a src/band/runtime/tools/ package (types, registry, schema, agent, human, inputs/*); the public import surface is unchanged.
  • Live E2E coverage: a multi-agent task-board delegation test, plus a per-adapter task-lifecycle test (create → claim → complete → list/get/get-history/get-board) across every Capability.TASKS adapter.

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 the tools.pytools/ 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).
  • A Pydantic field validator on every tool-facing content field (SendMessageInput, SendEventInput, SendEventWideInput, SendMyChatMessageInput).
  • band.platform.posting.post_message/post_event — the one choke point every chat-send REST call in src/band routes 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_file rejects a whitespace-only caption instead of raising AttributeError after the file upload succeeds. ACPServerAdapter.handle_prompt and A2AGatewayAdapter._send_to_band fail fast on blank content instead of hanging out the full response timeout. FakeAgentTools rejects blank content the same way the real tools do.

Test plan

  • uv run ruff check . / uv run ruff format --check .
  • uv run pyrefly check
  • uv 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-cov
  • Unit tests: tests/runtime/test_task_tools.py, tests/core/test_validation.py, tests/framework_conformance/test_task_tool_conformance.py
  • Extended: tests/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.py
  • Live E2E baseline: E2E_TESTS_ENABLED=true uv run pytest tests/e2e/baseline/smoke/behavior/test_multi_agent_collaboration.py -k task_board -v -s --no-cov and ... test_capability_matrix.py -k task -v -s --no-cov, both against the real dev platform.
  • Blank-content fix: tests/core/test_content.py, tests/platform/test_posting.py, tests/platform/test_posting_boundary.py; extended tests/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.md gained 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

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
@linear-code

linear-code Bot commented Sep 3, 2026

Copy link
Copy Markdown

INT-1370

@AlexanderZ-Band
AlexanderZ-Band requested a review from a team September 3, 2026 15:59
AlexanderZ-Band and others added 5 commits September 3, 2026 19:20
- 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 and others added 5 commits September 4, 2026 09:43
…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 amit-gazal-band left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/band/adapters/pydantic_ai.py
Comment thread src/band/runtime/tools/inputs/human_contacts.py
Comment thread src/band/integrations/parlant/tools.py
Comment thread src/band/runtime/tools/inputs/tasks.py
…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
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.

2 participants