Compare commits

...

1 Commits

Author SHA1 Message Date
HAL9000 e571ed4dea spec: add TuiMaterializer A2A integration layer specification (v3.7.0) [AUTO-ARCH-4]
CI / lint (pull_request) Successful in 37s
CI / typecheck (pull_request) Successful in 53s
CI / security (pull_request) Successful in 53s
CI / quality (pull_request) Successful in 50s
CI / build (pull_request) Successful in 42s
CI / helm (pull_request) Successful in 29s
CI / push-validation (pull_request) Successful in 30s
CI / integration_tests (pull_request) Successful in 4m40s
CI / e2e_tests (pull_request) Successful in 4m36s
CI / unit_tests (pull_request) Successful in 6m17s
CI / docker (pull_request) Successful in 1m28s
CI / coverage (pull_request) Successful in 15m59s
CI / status-check (pull_request) Successful in 1s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Successful in 57m17s
2026-04-13 20:59:15 +00:00
+173
View File
@@ -30597,6 +30597,179 @@ Hotkeys vary by the current screen and focused widget. The help panel (`F1`) alw
| `up` / `down` | Scroll help content |
## TuiMaterializer A2A Integration Layer (v3.7.0)
!!! adr "Architecture Decision"
The TuiMaterializer A2A integration layer is specified here in response to [Issue #8442](https://github.com/cleveragents/cleveragents-core/issues/8442), which identified that this component was entirely missing from both the spec and the implementation. Related ADRs: [ADR-044](adr/ADR-044-tui-architecture-and-framework.md) (TUI Architecture), [ADR-045](adr/ADR-045-tui-persona-system.md) (Persona System), [ADR-046](adr/ADR-046-tui-reference-and-command-system.md) (Reference/Command System), [ADR-047](adr/ADR-047-acp-standard-adoption.md) (A2A Standard Adoption).
### Overview
The `TuiMaterializer` is the bridge between the A2A (Agent-to-Agent) protocol event stream and the Textual UI widget system. It accepts normal prompt submissions from the TUI input, dispatches them to the A2A backend, and routes streaming response events to the appropriate Textual widgets. Without this component, the TUI is a UI shell with no AI backend connectivity.
**Related ADRs**:
- [ADR-044](adr/ADR-044-tui-architecture-and-framework.md) — TUI Architecture and Framework
- [ADR-045](adr/ADR-045-tui-persona-system.md) — Persona System
- [ADR-046](adr/ADR-046-tui-reference-and-command-system.md) — Reference and Command System
- [ADR-047](adr/ADR-047-acp-standard-adoption.md) — A2A Standard Adoption
**Related Issue**: [#8442](https://github.com/cleveragents/cleveragents-core/issues/8442) — TuiMaterializer missing from spec and implementation
### Module Boundaries
| Property | Value |
|----------|-------|
| **Module** | `cleveragents.tui.materializer` |
| **File** | `src/cleveragents/tui/materializer.py` |
| **Layer** | Presentation (TUI layer) |
#### Responsibilities
- Accepting prompt submissions from the TUI input widget
- Dispatching prompts to the A2A backend via the A2A client
- Receiving and routing A2A streaming response events to Textual widgets
- Managing A2A session binding per TUI session tab
- Handling permission request events
- Handling thought block events
#### Public Interfaces
- `TuiMaterializer` — main class, instantiated per TUI session tab
- `TuiMaterializerConfig` — configuration value object
#### Dependencies (Allowed)
- `cleveragents.a2a.client.A2AClient` — for dispatching to A2A backend
- Textual widget references (passed via constructor, not imported directly)
#### Forbidden Dependencies
The `TuiMaterializer` **must not** import from `cleveragents.domain` or `cleveragents.infrastructure` directly. All domain and infrastructure access must flow through the A2A client boundary.
### Class Interface
```python
class TuiMaterializer:
"""Bridges A2A protocol events to Textual UI widget updates."""
def __init__(
self,
a2a_client: A2AClient,
conversation_widget: ConversationWidget,
permission_handler: PermissionHandler,
thought_block_handler: ThoughtBlockHandler,
session_id: str,
) -> None: ...
async def submit_prompt(
self,
prompt: str,
persona: Optional[PersonaConfig] = None,
) -> None:
"""
Dispatch a user prompt to the A2A backend and stream the response
to the conversation widget.
This is the primary entry point called by on_input_submitted in the
Textual app when the user submits a normal (non-command, non-shell) message.
"""
...
async def cancel_current_request(self) -> None:
"""Cancel the currently streaming A2A request, if any."""
...
def bind_session(self, session_id: str) -> None:
"""Bind this materializer to a specific A2A session."""
...
```
### A2A Event Routing
The materializer subscribes to the A2A event stream and routes events as follows:
| A2A Event Type | Textual Widget | Action |
|---|---|---|
| `TextChunk` | `ConversationWidget` | Append text chunk to current message bubble |
| `ThoughtBlock` | `ThoughtBlockWidget` | Create/update thought block display |
| `ToolCallStart` | `ConversationWidget` | Show tool call indicator |
| `ToolCallResult` | `ConversationWidget` | Show tool result inline |
| `PermissionRequest` (single file) | `PermissionQuestionWidget` | Show inline permission prompt |
| `PermissionRequest` (multi-file) | `PermissionsScreen` | Push permissions screen |
| `StreamEnd` | `ConversationWidget` | Finalize current message bubble |
| `ErrorEvent` | `ConversationWidget` | Show error message |
### Streaming Architecture
```
User Input
on_input_submitted() [app.py]
TuiMaterializer.submit_prompt()
A2AClient.stream_request(prompt, session_id)
A2A Event Stream (async generator)
├─► TextChunk ──────────► ConversationWidget.append_chunk()
├─► ThoughtBlock ────────► ThoughtBlockWidget.update()
├─► ToolCallStart ───────► ConversationWidget.show_tool_call()
├─► PermissionRequest ───► PermissionHandler.handle()
└─► StreamEnd ───────────► ConversationWidget.finalize_message()
```
### Session Tab Binding
Each TUI session tab has its own `TuiMaterializer` instance bound to a specific A2A session ID. When the user switches tabs, the active materializer changes. Multi-session tabs with independent A2A bindings are supported.
The session ID is stored in SQLite at `~/.local/state/cleveragents/tui.db` and restored on TUI restart, enabling session resumption across TUI launches.
### ConversationWidget Specification
The `ConversationWidget` must be a scrollable log widget (not a static `_Static` widget). It must support:
- `append_chunk(text: str)` — append text to the current streaming message
- `show_tool_call(tool_name: str, args: dict)` — show a tool call indicator
- `finalize_message()` — seal the current message bubble
- `show_error(message: str)` — display an error message
- Scrolls to bottom automatically as new content arrives
### Permission Handling
- **Single-file permission requests**: rendered inline in the conversation via `PermissionQuestionWidget`
- **Multi-file permission requests**: push `PermissionsScreen` as a modal
- User approval/denial is sent back to the A2A backend via `A2AClient.respond_to_permission()`
### Integration Points
| Integration | Detail |
|-------------|--------|
| **TUI App** | `_TextualCleverAgentsTuiApp.on_input_submitted` must instantiate/use `TuiMaterializer` for normal mode input |
| **A2A Client** | `TuiMaterializer` uses `A2AClient` from `cleveragents.a2a.client` |
| **Persona System** | Active persona config is passed to `submit_prompt()` to configure the A2A request |
| **Session Persistence** | Session ID is stored in SQLite at `~/.local/state/cleveragents/tui.db` |
### Error Handling
| Error | Handling Strategy |
|-------|-------------------|
| `A2AConnectionError` | A2A backend unreachable; show error in conversation widget |
| `A2ASessionExpiredError` | Session expired; prompt user to reconnect |
| `PermissionDeniedError` | User denied permission; show denial message in conversation |
### Cross-Cutting Concerns
- **Loading States**: Show spinner in conversation widget while waiting for first chunk
- **Cancellation**: User can press `Escape` to cancel a streaming request; `cancel_current_request()` is called by the TUI app's `on_key` handler
- **Content Pruning**: Long conversations are pruned per safety behavior settings to prevent unbounded memory growth
- **Logging**: All A2A dispatches logged at `DEBUG` level with session ID for traceability
## Configuration
!!! adr "Architecture Decision"