Skip to content

Assistant & Block System

The assistant system renders AI agent responses as structured blocks rather than flat text. This enables rich rendering of tool calls, code execution, tables, plans, and custom space-provided blocks.

Block Types

Request Blocks (User Input)

TypeDescription
textText message
imageAttached image (drag-drop or paste)
fileAttached file

Response Blocks (Agent Output)

TypeDescription
textMarkdown text content
toolTool call visualization (name, input, result, running/done/error state)
codeCode block with syntax highlighting
svgSVG visualization
imageGenerated or returned image
errorError message
statusStatus update (thinking, processing)
questionExtracted question (used by architect flow)
planPlan or spec output
tasklistTask list with checkable items
progressProgress indicator
tableTabular data (headers + rows)
jsonJSON data display
actionActionable suggestion
linkExternal link
diffCode diff
customSpace-provided custom renderer

Turn Model

A Turn represents one complete request/response cycle:

ts
interface Turn {
  id: string
  request: RequestBlock[]      // What the user sent
  response: ResponseBlock[]    // What the agent returned (built up during streaming)
  agentId: string
  status: 'pending' | 'streaming' | 'done' | 'error'
  timestamp: number
  turns?: number               // Multi-turn count from operator
}

useAgentSession Composable

File: operator/useAgentSession.ts (529 lines)

The primary composable for block-based agent interactions:

ts
const session = useAgentSession()

// State
session.turns              // Ref<Turn[]> — all conversation turns
session.isLoading          // Ref<boolean>
session.selectedAgent      // Ref<string> — 'general', 'brainstorm', etc.

// Actions
session.send(blocks)       // Send RequestBlock[] → streams response into new Turn
session.stop()             // Cancel active stream
session.clear()            // Reset conversation

// Persistence
session.saveSession()      // Save current session
session.loadSession(id)    // Restore a session
session.listSessions()     // List saved sessions

How Streaming Builds Blocks

  1. User calls send([{ type: 'text', content: 'Build a login page' }])
  2. A new Turn is created with status: 'streaming' and empty response
  3. useOperator().dispatchStream() starts the stream
  4. As events arrive, blocks are added/updated:
    • text event → append text to current TextBlock (or create new one)
    • tool.call event → push new ToolBlock with state: 'running'
    • tool.result event → find matching ToolBlock by call_id, update with result and state: 'done'
    • status event → update stream status display
    • done event → if assistant type specified, normalize the final envelope
  5. Turn status set to 'done'

Per-Type Normalization

If an assistantType is specified (e.g., 'architect'), the final content is passed through a custom normalizer that converts the raw output into the appropriate block types for that assistant.

Assistant Envelope

File: assistant/schema.ts

The operator can return structured output in the assistant.v1 envelope format (validated with Zod):

ts
{
  version: 'assistant.v1',
  state: 'ok' | 'refusal' | 'incomplete' | 'error',
  provider: 'anthropic',
  content: {
    title?: 'Project Plan',
    summary?: 'A brief summary...',
    blocks: [
      { type: 'markdown', text: '## Architecture\n...' },
      { type: 'steps', items: [{ title: 'Step 1', body: '...', status: 'done' }] },
      { type: 'kv', items: [{ label: 'Framework', value: 'Vue 3' }] },
      { type: 'list', items: ['Item 1', 'Item 2'], ordered: false }
    ],
    data?: { customKey: 'customValue' }
  }
}

Normalization Pipeline

File: assistant/normalize.ts

Converts raw operator output into renderable ResponseBlock[]:

  1. Try to parse as assistant.v1 envelope
  2. If valid envelope:
    • title → TextBlock with # Title
    • summary → TextBlock
    • markdown block → TextBlock
    • list block → TextBlock (formatted as markdown list)
    • steps block → TaskListBlock
    • kv block → TableBlock
    • data → JsonBlock
  3. If not an envelope: treat as raw text → single TextBlock
  4. Per-type normalizers can override the entire pipeline (e.g., architect custom normalizer)

Assistant Type Registry

File: assistant/registry.ts

Each assistant type defines its rendering behavior:

ts
interface AssistantTypeConfig {
  id: string                    // 'general', 'brainstorm', 'architect', 'coder'
  label: string                 // Display name
  entryAgent: string            // Agent ID to dispatch to
  renderMode: 'blocks' | 'timeline' | 'custom'
  finalSchema?: string          // 'assistant.v1', 'architect.v1'
  sessionScope: 'global' | 'assistant' | 'project'
  supportsAttachments: boolean
  requiresProjectPath: boolean
  source: 'builtin' | string   // 'builtin' or 'space:{spaceId}'
  normalizer?: (raw: unknown) => ResponseBlock[]  // Custom normalization
}

Builtin Types

TypeEntry AgentSession ScopeNotes
generalgeneralglobalDefault chat, always available
brainstormbrainstormassistantIdea exploration
architectarchitectprojectCustom normalizer for Q&A blocks
codercoderprojectHighest iteration limit (50)

Space-Defined Types

Spaces can register custom assistant types via their manifest:

json
{
  "assistant": {
    "id": "myspace",
    "entryAgent": "specialist",
    "renderMode": "blocks",
    "sessionScope": "project",
    "supportsAttachments": true
  }
}

Space types are loaded via loadSpaceAssistantTypes() and unloaded on space removal via unregisterAssistantTypesBySource('space:{id}').

Component Hierarchy

AssistantPanel.vue
  ├── Header (agent selector dropdown)
  ├── AgentView.vue
  │     ├── RequestBubble.vue (per turn — user message)
  │     └── ResponseBlocks.vue (per turn — agent response)
  │           ├── TextBlock renderer (markdown)
  │           ├── ToolCard.vue (collapsible tool call)
  │           │     ├── State indicator (spinner/check/error)
  │           │     ├── Tool name + input params
  │           │     └── Expandable result panel
  │           ├── CodeBlock renderer (syntax highlighted)
  │           ├── TableBlock renderer
  │           ├── TaskListBlock renderer
  │           └── ... other block type renderers
  └── AgentInput.vue
        ├── Text input (glassmorphism)
        ├── Attachment zone (drag-drop)
        ├── Mic button (speech recognition)
        └── Send button

Runtime Resolution

File: assistant/runtime.ts

Maps UI surfaces to assistant types:

ts
resolveAssistantType({ surface: 'assistant-panel' })  // → 'general'
resolveAssistantType({ surface: 'architect-page' })   // → 'architect'
resolveAssistantType({ surface: 'coder-page' })       // → 'coder'

Construct Team — Internal Developer Documentation