Skip to main content

AI Copilot Architecture

For humans and LLMs contributing to Songbird’s AI features.

Overview

Songbird’s AI copilot is a realtime music production assistant that lives inside the DAW. It routes user messages to specialized agents, assembles context from the current project state, and provides suggestions driven by DAW events and learned user preferences. The architecture is organized into six subsystems:
react_ui/src/lib/ai/
├── core/           — Copilot orchestrator + intent router
├── agents/         — Specialized agents (MixEngineer, Composer, SoundDesigner)
├── hooks/          — Event-driven copilot behaviors (DAW event → suggestion)
├── instincts/      — Per-user/per-project learned preferences
├── context/        — Tiered context assembly (hot/warm/cold)
├── pipeline/       — Generate-then-refine two-pass pipeline
├── timeline/       — Timeline-driven pre-computation
├── providers/      — LLM provider abstraction (Gemini)
├── tools/          — Tool declarations and executor
├── cache/          — Context caching for large prompts
├── bird/           — .bird notation utilities
├── routing/        — Intent classification
├── docs/           — Prompt templates and documentation
├── types.ts        — Shared types (Intent, ModelTier, etc.)
└── index.ts        — Barrel exports

Core Pipeline

Request Flow

User message
  → Intent classification (LLM + regex fallback)
  → Agent routing (intent → specialized agent)
  → Tiered context assembly (hot/warm/cold)
  → Instincts injection (learned preferences)
  → LLM call (streaming, with tools)
  → [Optional] Refinement pass (for creative bird_edit)
  → Result

Intent Types

IntentDescriptionAgentModel Tier
bird_editCompose, edit notes, arrangeComposerbalanced/pro
mixerVolume, pan, sends, routingMixEngineerfast
mix_feedbackMixing advice and analysisMixEngineerbalanced
set_paramPlugin parameter changesPlugin Editorfast
bpmTempo changesGeneralfast
lyriaAI audio generationGeneralbalanced
shortcutDirect tool invocation(shortcut)
chatGeneral conversationGeneralbalanced

Chat Modes

  • copilot — Full tool-use mode. Agent can call tools to modify the project.
  • advisor — Read-only conversational mode. No tools, just musical guidance.

Event-Driven Hooks

File: hooks/copilot-hooks.ts + hooks/store-integration.ts The hook system maps DAW events to copilot behaviors. Hooks are async, non-blocking, and never touch the audio thread.

Hook Events

EventFires WhenExample Behavior
onPlaybackStartTransport startsPrepare timeline suggestions
onPlaybackStopTransport stopsOffer mixing tips
onSectionBoundaryPlayback crosses sectionSuggest transitions
onBpmChangeBPM changedAdjust rhythm suggestions
onScaleChangeKey/scale changedUpdate harmonic context
onTrackAddedNew track createdSuggest instruments/FX
onTrackRemovedTrack deleted
onMixerIdleNo mixer changes for N secondsOffer mixing feedback
onPatternEditNotes edited in MIDI editorSuggest variations
onGenerationCompleteAI generation finishesSuggest refinements

Architecture

Zustand store change
  → store-integration.ts (subscriber, edge detection)
  → fireHook(event, context)
  → copilot-hooks.ts (throttle check, find handlers)
  → handler(context) → HookSuggestion[]
  → suggestion queue (priority, TTL, max size)
  → onSuggestions callback → UI
Key design decisions:
  • Hooks are throttled per event type (configurable MIN_INTERVAL_MS) to prevent storms
  • Suggestion queue has TTL — stale suggestions are culled automatically
  • The initialized flag inside the transport subscriber skips the initial C++ hydration diff
  • Store subscriptions use dependency injection (passed as callbacks) to avoid circular imports

Registering Custom Hooks

import { registerHook } from '@/lib/ai';

registerHook('onSectionBoundary', {
  id: 'suggest-transition',
  priority: 5,
  handler: async (ctx) => [{
    type: 'transition',
    title: 'Add transition fill',
    description: `Transition from ${ctx.payload.prevSection} to ${ctx.payload.newSection}`,
    confidence: 0.7,
  }],
});

Per-User/Per-Project Instincts

File: instincts/instinct-engine.ts Tracks learned user preferences with confidence scoring. Inspired by ECC’s continuous learning pattern.

Instinct Categories

CategoryWhat It Tracks
velocity_rangePreferred velocity ranges per instrument
voicing_styleOpen vs closed voicings, inversions
rhythm_densitySparse vs dense patterns
plugin_preferencePreferred plugins per role
genre_tendencyDetected genre patterns
scale_preferencePreferred scales/modes
mix_styleMixing tendencies
arrangement_styleArrangement patterns

Confidence Mechanics

  • Initial confidence: 0.5
  • On acceptance: +0.1 (capped at 0.95)
  • On rejection: -0.15 (floor at 0.1)
  • Prompt threshold: Only instincts >= 0.3 confidence are injected into prompts
  • Global promotion: Instincts appearing in 2+ projects with >= 0.6 confidence are promoted to global scope

Storage

JSON-serialized via juceBridge under key songbird-instincts. Debounced saves (3s).

Pattern Key Matching

When observe() is called with a patternKey, it’s stored as pattern._key in the instinct object. Future observe() calls match by category + _key, enabling stable reinforcement across sessions without needing exact pattern JSON equality.

Tiered Context Loading

File: context/tiered-context.ts Instead of dumping the entire project state on every LLM call, context is assembled in tiers with per-intent token budgets.

Tiers

TierContentWhen Included
HotTransport state, recent actions (last 30s), active instinctsAlways
WarmTrack list with instruments/FX, arrangement structure, mixer levelsMost intents
ColdFull .bird file, historical editsOnly bird_edit

Token Budgets by Intent

IntentBudgetTiers
shortcut200hot
bpm500hot
mixer1500hot + warm
set_param2000hot + warm
lyria1500hot + warm
chat3000hot + warm
mix_feedback4000hot + warm
bird_edit6000hot + warm + cold

Assembly Logic

  1. Always include hot context
  2. Add warm blocks if required by intent OR if < 50% of budget used
  3. Add cold blocks if required by intent OR if < 30% of budget used
  4. Each block is individually checked against remaining budget before inclusion
  5. tiersUsed accurately reports which tiers had blocks actually included

Generate-Then-Refine Pipeline

File: pipeline/generate-refine.ts A two-pass pipeline for creative content: let the model be creative first, then evaluate against musical constraints.

When It Triggers

Only for bird_edit intent with creative keywords: compose, write, create, fill, extend, generate, add, build, arrange, orchestrate, harmonize, improvise.

Pass 1: Generate (Creative)

Standard copilot call — unconstrained generation using the balanced/pro model.

Pass 2: Refine (Evaluation)

A fast-model call that evaluates the generated content against:
CheckWhat It Catches
key_mismatchNotes outside the project’s key signature
scale_violationNotes outside the active scale
rhythm_clashRhythmic patterns that conflict with existing tracks
density_mismatchToo dense or too sparse relative to context
range_issueNotes outside playable range for the instrument
style_mismatchDoesn’t match user’s instinct preferences

Output

interface PipelineResult {
  finalContent: string;    // refined content (or original if no issues)
  wasRefined: boolean;     // whether refinement changed anything
  issues: RefinementIssue[];  // what was found and fixed
  summary: string;         // human-readable summary
}

Timeline-Driven Pre-Computation

File: timeline/pre-compute.ts Uses playback position and arrangement structure to predict what the user needs next and pre-generate suggestions before they’re needed.

How It Works

Transport position update (during playback)
  → Calculate bars remaining in current section
  → If within lookAheadBars (default: 4) of section boundary
  → Pre-compute suggestions for the next section
  → Cache with TTL (default: 60s)
  → Available via getPreComputedSuggestions() when user reaches that section

Suggestion Types

TypeWhen Generated
transitionApproaching a section boundary
continuationCurrent section is about to loop
variationUser has been in a section for a while
mix_adjustmentSection change may need mix tweaks

Configuration

interface PreComputeConfig {
  lookAheadBars: number;      // default: 4
  maxConcurrentJobs: number;  // default: 2
  suggestionTtlMs: number;    // default: 60000
  enabled: boolean;           // default: true
}

Cache Invalidation

  • Suggestions are invalidated when a section is edited (invalidateSection())
  • TTL-based expiry for stale suggestions
  • Manual cancelAll() available for cleanup

Integration Points

copilot.ts (Main Orchestrator)

The copilot send() method integrates the subsystems:
  • Step 3b: After building the agent/standard system prompt, appends tiered context from assembleTieredContext() (transport + mixer + instincts)
  • Step 7: After generation, checks shouldUseRefine() and optionally runs the refinement pass

store.ts (Store Subscriptions)

Hook integration is initialized via dynamic import to avoid circular dependencies:
let _copilotHooksUnsub: (() => void) | null = null;
import('@/lib/ai/hooks/store-integration').then(({ initCopilotHooks }) => {
  _copilotHooksUnsub = initCopilotHooks(
    (listener) => useTransportStore.subscribe(listener),
    (listener) => useMixerStore.subscribe(listener),
    () => useTransportStore.getState(),
    () => useMixerStore.getState(),
  );
});
HMR cleanup uses a dedicated module-level variable (not _unsubs array) to avoid race conditions with async imports.

index.ts (Barrel Exports)

All subsystems are exported from @/lib/ai:
// Hooks
import { registerHook, fireHook, onSuggestions } from '@/lib/ai';

// Instincts
import { observeInstinct, acceptInstinct, rejectInstinct } from '@/lib/ai';

// Context
import { assembleContext } from '@/lib/ai';

// Pipeline
import { shouldUseRefine, buildRefinePrompt } from '@/lib/ai';

// Pre-compute
import { onPositionUpdate, getPreComputedSuggestions } from '@/lib/ai';

Wiring Status

The subsystems are at different stages of integration:
SubsystemModuleWired IntoStatus
Hookscopilot-hooks.tsstore.ts subscriptionsActive (fires on DAW events)
Instinctsinstinct-engine.tscopilot.ts prompt injectionScaffolding (observe/accept/reject not called yet)
Tiered Contexttiered-context.tscopilot.ts Step 3bActive (assembles on every request)
Generate-Refinegenerate-refine.tscopilot.ts Step 7Active (triggers for creative bird_edit)
Pre-Computepre-compute.tsstore.ts transport updatesScaffolding (compute handler not wired to LLM)
Store Integrationstore-integration.tsstore.ts dynamic importActive (fires hooks, feeds pre-compute)
Next steps to fully wire:
  1. Call observe() / accept() / reject() on instincts from copilot response handlers
  2. Wire setComputeHandler() to call the copilot for pre-generation
  3. Track recentActions and pass them to assembleContext()
  4. Replace 'current-project' placeholder with actual project ID from .bird filename
  5. Add UI for displaying hook suggestions to the user

Key Files

FileRole
core/copilot.tsMain orchestrator — routes, builds prompt, calls LLM, refines
core/router.tsIntent classification (LLM + regex fallback)
hooks/copilot-hooks.tsHook registry, throttling, suggestion queue
hooks/store-integration.tsZustand → hook event wiring
instincts/instinct-engine.tsLearned preferences with confidence scoring
context/tiered-context.tsHot/warm/cold context assembly
pipeline/generate-refine.tsTwo-pass creative generation + evaluation
timeline/pre-compute.tsPredictive suggestion pre-generation
agents/base.tsAgent registry
agents/mix-engineer.tsMixing agent
agents/composer.tsComposition agent
agents/sound-designer.tsSound design agent
providers/gemini.tsGemini LLM provider
tools/executor.tsTool execution system
types.tsShared types
index.tsBarrel exports