Skip to content

feat(agent): multimodal prompt — native image/audio content blocks gated by promptCapabilities - #774

Merged
kaizhou-lab merged 7 commits into
mainfrom
feat/multimodal-prompt
Aug 4, 2026
Merged

feat(agent): multimodal prompt — native image/audio content blocks gated by promptCapabilities#774
kaizhou-lab merged 7 commits into
mainfrom
feat/multimodal-prompt

Conversation

@kaizhou-lab

Copy link
Copy Markdown
Contributor

Summary

Attachments that the target agent can natively consume (per its declared promptCapabilities) are now delivered as protocol image/audio content blocks instead of file-path text. Agents without the capability keep the exact pre-existing path-based wire form, byte for byte. Frontend half: iOfficeAI/AionUi#3849.

How it works

  • Dispatch-time partition (aionui-ai-agent/src/media.rs): the only layer that knows the agent's capabilities splits SendMessageData.files into native-block media vs path files, stripping/rebuilding the [[AION_FILES]] block with the same validate-then-rebuild approach as the aionrs precedent (manager/aionrs/content.rs). Persisted/broadcast message content is untouched — UI chips and history render exactly as before. Degradation to path form: capability absent, non-media mime, SVG, unreadable, or >5MB (warn-logged).
  • Capability surface: new IAgentTask::prompt_media_caps() (default none) — AcpAgentManager reads the initialize response's promptCapabilities; SessionAgentTask reads the backend's prompt_blocks.
  • ACP path (agent_session_flow): session/prompt becomes a text block plus one base64 ImageContent/AudioContent per capable attachment (uri: file://…). Any read failure degrades the whole prompt back to the plain text form. Transport is LinesCodec::new() (unbounded) on both sides.
  • Session path (session_agent): capable media rides as seam ContentBlock::Image/Audio; the claude/codex adapters' existing base64 encoders consume them unchanged.
  • Capability pipeline to the frontend: ACP agents already persist prompt_capabilities inside agent_metadata.agent_capabilities (003 seeds + live handshake refresh). Migration 037 json_patches the same key onto the direct-CLI rows (claude/codex: image only), mirroring 036's fork pattern. The conversation detail response gains a prompt_capability {image, audio} projection next to fork_capability (one metadata load feeds both; lists stay N+1-free).

Live verification

  • claude 2.1.221 (--input-format stream-json): a generated 24px red-square-on-blue PNG sent as the adapter's source-wrapped base64 image block → "图中是蓝色背景上的一个红色正方形。"
  • codebuddy --acp: session/prompt with {type:"image", data, mimeType, uri} → "A red square centered on a blue background." (stopReason: end_turn, no error with the uri field present)

Testing (targeted; CI runs the full workspace suite)

  • aionui-ai-agent: media partition (6), ACP prompt blocks (3), session send partition (1) — all pass
  • aionui-conversation: lib 376 + conversation_extended 38 (incl. new get_projects_prompt_capability_on_detail_path) — all pass
  • aionui-db: 330 pass with migration 037
  • cargo fmt --check clean; targeted clippy (-p ai-agent/conversation/api-types) clean

zk added 7 commits August 4, 2026 19:29
…dia partition

- SendMessageData stays unchanged; partition happens at the dispatch layer
  (the only layer that knows the target agent's capabilities), mirroring the
  aionrs strip-and-rebuild precedent in manager/aionrs/content.rs.
- IAgentTask::prompt_media_caps() (default none) implemented by
  AcpAgentManager (initialize promptCapabilities) and SessionAgentTask
  (backend prompt_blocks).
- media::partition_media splits attachments into native-block media vs
  path files, rewriting the [[AION_FILES]] block to the exact
  resolve_chat_message format; SVG, oversized (>5MB), unreadable, and
  capability-less attachments degrade to paths byte-identically.
build_prompt_blocks partitions attachments by the agent's declared
promptCapabilities: capable media rides as base64 ImageContent/AudioContent
blocks (uri: file://path) after the text block; everything else stays in
the [[AION_FILES]] path list byte-identical to the previous wire form.
Any read failure degrades the whole prompt back to the plain text form.
Transport is LinesCodec::new() (unbounded) on both sides, so multi-MB
frames pass unchanged.
…blocks

SessionAgentTask::send_message now partitions attachments through the same
media partition: backend-capable media rides as ContentBlock::Image/Audio
(raw bytes + mime), the rest keeps the path-text + ResourceLink form. A
read failure degrades that attachment back to a ResourceLink. The claude
and codex adapters' existing base64 encoders consume the blocks unchanged
(claude image pinned at protocols/samples/claude-cli/2.1.177).
ACP agents already carry prompt_capabilities inside the handshake-persisted
agent_metadata.agent_capabilities (003 seeds + live initialize refresh),
which reaches the frontend verbatim. Migration 037 json_patches the same
key onto the direct-CLI rows (claude/codex: image only), mirroring 036's
session_capabilities.fork pattern. Antigravity stays text-only (absent key
reads as unsupported).
PromptCapabilityView {image, audio} joins fork_capability in the same
detail-only post-fill: one agent_metadata.agent_capabilities load now
feeds both projections (agent_capabilities_for_agent + two pure view
fns). List responses stay N+1-free.
Both dispatch paths (ACP prompt / session Command::Send) now emit one
info line with image/audio counts when a prompt carries native media
blocks, so production logs can distinguish native-block delivery from
the path-text fallback (the degradation warns already existed).
@kaizhou-lab

Copy link
Copy Markdown
Contributor Author

Local e2e — real UI wire shapes, full upload chain

Ran a local instance (aioncore --local) and replayed the exact requests the renderer makes:

  1. POST /api/conversations with the guid-page body (assistant.id only, no type/agent_id) → claude conversation created; GET /api/conversations/{id} projects prompt_capability: {"image": true, "audio": false} (migration 037 + detail post-fill).
  2. POST /api/fs/upload (multipart file + conversation_id, same as FileService.uploadFileViaHttp) → <tmp>/aionui/<conv>/probe-red-square.png.
  3. POST /api/conversations/{id}/messages with files: [{"kind":"upload","path":...}] (same as collectChatFileRefs).

Result:

  • Server log (info level): session prompt carries native media content blocks conversation_id=609a4c0e images=1 audios=0 — the attachment took the native-block path, not the path-text fallback.
  • claude's reply: "图中是一个蓝色背景上居中的红色正方形(实心方块)。" — exact pixel content of the generated probe PNG, with the prompt forbidding tool use, so the pixels came through the base64 vision block.

The last commit adds that info line on both dispatch paths (ACP + session) so native-block delivery is diagnosable in production logs.

@kaizhou-lab
kaizhou-lab merged commit 5a78a0b into main Aug 4, 2026
6 checks passed
@kaizhou-lab
kaizhou-lab deleted the feat/multimodal-prompt branch August 4, 2026 14:16
kaizhou-lab pushed a commit that referenced this pull request Aug 5, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.1.59](v0.1.58...v0.1.59)
(2026-08-05)


### Features

* **agent:** multimodal prompt — native image/audio content blocks gated
by promptCapabilities
([#774](#774))
([5a78a0b](5a78a0b))
* **team:** add read-only mailbox/task activity API and real-time events
([#740](#740))
([f86c053](f86c053))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
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