Version: 1.2.0 (February 2026) Status: Production Ready
from tools.canvas_tool import present_chart, present_markdown, present_form, update_canvas
# Present a chart
await present_chart(
user_id="user-1",
chart_type="line_chart",
data=[{"x": "Jan", "y": 100}, {"x": "Feb", "y": 150}],
title="Sales Trend"
)
# Update chart data (NEW!)
await update_canvas(
user_id="user-1",
canvas_id="canvas-abc123",
updates={"data": [{"x": "Jan", "y": 100}, {"x": "Feb", "y": 200}]}
)# Session A
await present_chart(
user_id="user-1",
chart_type="line_chart",
data=[...],
session_id="session-a" # Isolated canvas
)
# Session B (different canvas, no collision)
await present_markdown(
user_id="user-1",
content="# Report",
session_id="session-b" # Separate canvas
)await present_chart(
user_id: str, # Required: User ID
chart_type: str, # Required: "line_chart", "bar_chart", "pie_chart"
data: List[Dict], # Required: Chart data
title: str = None, # Optional: Chart title
agent_id: str = None, # Optional: Agent ID (for governance)
session_id: str = None, # Optional: Session ID (NEW!)
**kwargs # Additional chart options
)Complexity: 1 (LOW) Maturity: STUDENT+
await present_markdown(
user_id: str,
content: str, # Markdown content
title: str = None,
agent_id: str = None,
session_id: str = None # NEW!
)Complexity: 1 (LOW) Maturity: STUDENT+
await present_form(
user_id: str,
form_schema: Dict, # Form schema with fields
title: str = None,
agent_id: str = None,
session_id: str = None # NEW!
)Complexity: 2 (MODERATE) Maturity: INTERN+
await update_canvas(
user_id: str,
canvas_id: str, # Existing canvas ID
updates: Dict, # Update data
agent_id: str = None,
session_id: str = None
)Complexity: 2 (MODERATE) Maturity: INTERN+
Examples:
# Update data
await update_canvas(user_id="user-1", canvas_id="abc", updates={"data": [...]})
# Update title
await update_canvas(user_id="user-1", canvas_id="abc", updates={"title": "New Title"})
# Update multiple fields
await update_canvas(
user_id="user-1",
canvas_id="abc",
updates={"title": "New", "data": [...], "color": "#FF0000"}
)from tools.registry import get_tool_registry
registry = get_tool_registry()
# List all tools
tools = registry.list_all()
# List by category
canvas_tools = registry.list_by_category("canvas")
# List by maturity level
intern_tools = registry.list_by_maturity("INTERN")
# Search
results = registry.search("chart")
# Get metadata
metadata = registry.get("present_chart")
print(metadata.description)
print(metadata.complexity)
print(metadata.maturity_required)# List all tools
GET /api/tools
# Filter by category
GET /api/tools?category=canvas
# Filter by maturity
GET /api/tools?maturity=INTERN
# Get tool details
GET /api/tools/present_chart
# Search tools
GET /api/tools/search?query=chart
# Get statistics
GET /api/tools/stats
# List categories
GET /api/tools/categories| Level | Actions | Maturity Required |
|---|---|---|
| 1 (LOW) | present_chart, present_markdown | STUDENT+ |
| 2 (MODERATE) | present_form, update_canvas, browser_navigate, device_camera_snap | INTERN+ |
| 3 (HIGH) | submit_form, device_screen_record_start, device_screen_record_stop | SUPERVISED+ |
| 4 (CRITICAL) | device_execute_command, delete, execute | AUTONOMOUS |
| Level | Confidence | Capabilities |
|---|---|---|
| STUDENT | <0.5 | Read-only (charts, markdown) |
| INTERN | 0.5-0.7 | Streaming, form presentation, canvas updates |
| SUPERVISED | 0.7-0.9 | Form submissions, screen recording |
| AUTONOMOUS | >0.9 | Full autonomy, command execution |
// Client-side WebSocket handling
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
switch(message.type) {
case "canvas:update":
const { action, component, canvas_id, session_id, data } = message.data;
if (action === "present") {
// Present new component
renderComponent(component, data, canvas_id);
} else if (action === "update") {
// Update existing component (NEW!)
updateComponent(canvas_id, data);
} else if (action === "close") {
// Close canvas
closeCanvas();
}
break;
}
};canvas:present- New component presentedcanvas:update- Component updated (NEW!)canvas:close- Canvas closedcanvas:delete- Component deleted
# Default channel (no session)
channel = f"user:{user_id}" # "user:user-1"
# Session-specific channel (NEW!)
channel = f"user:{user_id}:session:{session_id}" # "user:user-1:session:abc"CREATE TABLE canvas_audit (
id VARCHAR PRIMARY KEY,
user_id VARCHAR NOT NULL,
canvas_id VARCHAR,
session_id VARCHAR, -- NEW!
component_type VARCHAR NOT NULL,
action VARCHAR NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);All canvas actions are logged:
from core.models import CanvasAudit
from core.database import SessionLocal
db = SessionLocal()
# Query canvas audit for a user
audits = db.query(CanvasAudit).filter(
CanvasAudit.user_id == "user-1"
).order_by(CanvasAudit.created_at.desc()).limit(10).all()
for audit in audits:
print(f"{audit.action} {audit.component_type} - {audit.created_at}")
print(f" Session: {audit.session_id}") # NEW!
print(f" Agent: {audit.agent_id}")# Canvas update tests
pytest tests/test_canvas_updates.py -v
# Session isolation tests
pytest tests/test_canvas_sessions.py -v
# Tool registry tests
pytest tests/test_tool_registry.py -v
# All canvas tests
pytest tests/test_canvas*.py tests/test_tool_registry.py -v36 passed in 0.60s
cd backend
alembic upgrade headalembic current
# Output: 3552e6844c1d (add session_id to canvas_audit)# Initial presentation
result = await present_chart(
user_id="user-1",
chart_type="line_chart",
data=[{"x": 1, "y": 10}],
title="Live Dashboard"
)
canvas_id = result["canvas_id"]
# Real-time updates
while True:
new_data = fetch_latest_data()
await update_canvas(
user_id="user-1",
canvas_id=canvas_id,
updates={"data": new_data}
)
await asyncio.sleep(5)# Agent A: Sales data
await present_chart(
user_id="user-1",
chart_type="line_chart",
data=sales_data,
session_id="sales-session",
agent_id="agent-sales"
)
# Agent B: Marketing data (concurrent, no collision)
await present_chart(
user_id="user-1",
chart_type="bar_chart",
data=marketing_data,
session_id="marketing-session",
agent_id="agent-marketing"
)from tools.registry import get_tool_registry
registry = get_tool_registry()
# Find tools an agent can use
agent_tools = registry.list_by_maturity("INTERN")
print(f"Agent has access to {len(agent_tools)} tools:")
for tool_name in agent_tools:
metadata = registry.get(tool_name)
print(f" - {metadata.name}: {metadata.description}")result = await update_canvas(
user_id="user-1",
canvas_id="nonexistent",
updates={"title": "New"}
)
# Result: {"success": True, ...}
# Note: Update will be broadcast but client may ignore if canvas_id not found# STUDENT agent tries to update canvas
result = await update_canvas(
user_id="user-1",
canvas_id="canvas-123",
updates={"data": [...]},
agent_id="student-agent"
)
# Result: {"success": False, "error": "Agent not permitted to update canvas"}| Operation | Latency | Notes |
|---|---|---|
| present_chart | ~1ms | + governance |
| update_canvas | ~1-2ms | + governance + audit |
| Tool discovery | <1ms | In-memory lookup |
| Session isolation | <0.1ms | Channel multiplexing |
- Always use session_id for parallel workflows
- Update canvas instead of re-presenting for dynamic data
- Use tool registry for tool discovery
- Check audit trail for debugging
- Test governance with different agent maturity levels
# Check canvas_id exists
# Check user_id matches
# Check WebSocket connection
# Enable debug logging
import logging
logging.basicConfig(level=logging.DEBUG)# Use unique session IDs
session_id = f"{agent_id}-{workflow_id}-{timestamp}"
# Or use UUID
import uuid
session_id = str(uuid.uuid4())# Check tool registry
registry = get_tool_registry()
metadata = registry.get("tool_name")
if not metadata:
print("Tool not registered")
# Check module imports
import tools.canvas_tool
print(dir(tools.canvas_tool))- Full Documentation:
docs/CANVAS_ENHANCEMENTS_COMPLETE.md - Original Analysis: Gap analysis document
- Test Suite:
tests/test_canvas*.py - Implementation:
tools/canvas_tool.py,tools/registry.py
Last Updated: February 1, 2026 Version: 1.2.0
The Canvas State API enables AI agents to programmatically access canvas component state without OCR. All canvas components expose structured state via:
- Hidden accessibility trees - DOM elements with
role="log"anddata-canvas-stateattributes - JavaScript API - Global
window.atom.canvasobject - WebSocket events - Real-time state change broadcasts
Method 1: Hidden Accessibility Trees (Recommended for AI Agents)
Query the DOM for hidden state elements:
// Get all canvas state elements
const canvasStates = document.querySelectorAll('[data-canvas-state]');
// Get specific canvas state
const operationState = document.querySelector('[data-canvas-state="agent_operation_tracker"]');
// Parse state JSON
const state = JSON.parse(operationState.textContent);Attributes:
data-canvas-state- Component identifier (e.g., "agent_operation_tracker", "view_orchestrator")data-canvas-type- Canvas type (generic, docs, email, sheets, orchestration, terminal, coding)role- ARIA role ("log" for state, "alert" for errors)
// Get specific canvas state
const state = window.atom.canvas.getState('canvas-id');
// Get all canvas states
const allStates = window.atom.canvas.getAllStates();
// Subscribe to state changes
const unsubscribe = window.atom.canvas.subscribe('canvas-id', (state) => {
console.log('State changed:', state);
});
// Unsubscribe when done
unsubscribe();
// Subscribe to all canvas changes
const unsubAll = window.atom.canvas.subscribeAll((event) => {
console.log('Canvas changed:', event.canvas_id, event.state);
});Subscribe to canvas state changes via WebSocket:
socket.addEventListener('message', (event) => {
const message = JSON.parse(event.data);
if (message.type === 'canvas:state_change') {
console.log('Canvas state changed:', message.canvas_id);
console.log('New state:', message.state);
}
});{
canvas_type: 'generic',
component: 'agent_operation_tracker',
operation_id: string,
agent_id: string,
agent_name: string,
operation_type: string,
status: 'running' | 'waiting' | 'completed' | 'failed',
current_step: string,
current_step_index: number,
total_steps?: number,
progress: number, // 0-100
context: {
what?: string,
why?: string,
next?: string
},
logs_count: number,
started_at: string,
completed_at?: string
}{
canvas_type: 'generic',
component: 'view_orchestrator',
layout: 'split_horizontal' | 'split_vertical' | 'grid' | 'tabs',
current_view: string,
active_views: [{
view_id: string,
view_type: 'canvas' | 'browser' | 'terminal' | 'app',
title: string,
status: 'active' | 'background' | 'closed',
url?: string,
command?: string
}],
canvas_guidance?: {
agent_id: string,
message: string,
what_youre_seeing: string,
controls: Array<{ label: string; action: string }>
}
}{
canvas_type: 'generic',
component: 'line_chart' | 'bar_chart' | 'pie_chart',
chart_type: 'line' | 'bar' | 'pie',
data_points: Array<{
x: string | number,
y: number,
label?: string
}>,
axes_labels?: {
x?: string,
y?: string
},
title?: string,
legend?: boolean
}{
canvas_type: 'generic',
component: 'form',
form_schema: {
fields: Array<{
name: string,
type: string,
label: string,
required: boolean
}>
},
form_data: Record<string, any>,
validation_errors: Array<{
field: string,
message: string
}>,
submit_enabled: boolean,
submitted?: boolean
}// Agent wants to check operation progress
const operationDiv = document.querySelector('[data-canvas-state="agent_operation_tracker"]');
const operation = JSON.parse(operationDiv.textContent);
if (operation.status === 'completed') {
console.log('Operation completed:', operation.progress + '%');
} else {
console.log('Current step:', operation.current_step);
console.log('Progress:', operation.progress + '%');
}// Agent monitors form for submission
const formState = window.atom.canvas.getState('form-workflow-approval');
if (formState.submitted) {
console.log('Form submitted with data:', formState.form_data);
} else if (formState.validation_errors.length > 0) {
console.log('Form has errors:', formState.validation_errors);
}// Agent subscribes to all canvas state changes
const unsubscribe = window.atom.canvas.subscribeAll((event) => {
console.log('Canvas changed:', event.canvas_type);
console.log('New state:', event.state);
// Agent decision logic based on state
if (event.state.status === 'completed') {
console.log('Operation completed, proceeding to next step...');
}
});- State serialization overhead: <10ms per canvas render
- Use getState for one-time queries (faster than DOM parsing)
- Use subscribe for real-time monitoring (WebSocket events)
- Accessibility tree is always available (no JavaScript execution required)
| Canvas Type | Component | State Fields |
|---|---|---|
| generic | agent_operation_tracker | operation_id, status, progress, context |
| generic | view_orchestrator | layout, active_views, canvas_guidance |
| generic | line_chart | data_points, axes_labels, chart_type |
| generic | bar_chart | data_points, axes_labels, chart_type |
| generic | pie_chart | data_points, legend |
| generic | form | form_data, validation_errors, submit_enabled |
| orchestration | workflow_canvas | tasks, nodes, connections |
| terminal | terminal_canvas | lines, cursor_pos, working_dir |
- Agent Guidance System - Real-time agent monitoring
- Episodic Memory Integration - Agent learning system
- Canvas AI Accessibility - AI-readable canvas state