Skip to content

Agent System

Studio's AI agent is a Bounded Task Executor -- an LLM with access to a curated set of content management tools, constrained by a state machine, permissions, and context-aware system prompt.

Architecture Overview

User Message + UI Context
         |
         v
  Intent Classification
         |
         v
  System Prompt Construction
  (role + architecture + context + intent +
   state + config + schema + relations +
   vocabulary + permissions + rules)
         |
         v
  Conversation Engine (async generator)
         |
         +--→ AI Provider (streaming / non-streaming)
         |         |
         |         v
         |    Tool Call Decision
         |         |
         |         v
         |    State Machine Guard
         |         |
         |         v
         |    Tool Execution (Content Engine, Brain Cache, etc.)
         |         |
         |         v
         |    Affected Resources Accumulation
         |         |
         |         v
         |    Auto-Merge Decision (workflow + role)
         |
         v
  SSE Events → Client

Key Files

FilePurpose
server/utils/agent-types.tsType definitions for all agent types
server/utils/agent-tools.tsTool definitions with orchestration metadata
server/utils/agent-permissions.tsRole-based tool filtering
server/utils/agent-state-machine.tsProject phase enforcement
server/utils/agent-context.tsIntent classification
server/utils/agent-system-prompt.tsSystem prompt construction
server/utils/conversation-engine.tsConversation loop (async generator)
server/utils/brain-cache.tsIn-memory content cache
server/utils/content-strings.tsServer-side dictionary reader

Tool Definitions

Each tool carries orchestration metadata beyond what the LLM sees:

ts
interface StudioTool extends AITool {
  name: string
  description: string            // LLM-facing description
  inputSchema: Record<string, unknown>  // JSON Schema
  requiredPhase: ProjectPhase[]  // Which phases allow this tool
  defaultAffects: Partial<AffectedResources>  // Cache invalidation hints
  workflowBehavior: 'auto-merge' | 'workflow-dependent' | 'manual' | 'none'
}

Tool Inventory

ToolDescriptionPhasesWorkflow
list_modelsList content modelsallnone
get_contentRead content entriesactivenone
save_contentCreate/update contentactiveworkflow-dependent
delete_contentDelete entries or dictionary keysactiveworkflow-dependent
save_modelCreate/update modelsactiveworkflow-dependent
validateValidate contentallnone
list_branchesList cr/* branchesactive, init_pending, errornone
merge_branchMerge a branchactive, init_pendingmanual
reject_branchReject a branchactive, init_pendingmanual
init_projectInitialize .contentrain/uninitializedauto-merge
copy_localeCopy between localesactiveworkflow-dependent
brain_queryRead from brain cacheactivenone
brain_searchFull-text searchactivenone
search_mediaSearch media libraryactivenone
upload_mediaUpload from URLactivenone
get_mediaGet asset metadataactivenone
update_statusPublish/unpublish/archive entriesactiveworkflow-dependent
update_mediaUpdate asset alt/tags/focal pointactivenone
delete_mediaDelete an asset and its variantsactivenone
branch_healthCheck pending-branch warn/block thresholdsactive, init_pending, errornone
relation_expandResolve forward/reverse relationsactivenone
vocabularyRead/update the project glossaryactiveworkflow-dependent
add_localeRegister a new supported localeactiveworkflow-dependent
delete_modelDelete a model and all its contentactiveworkflow-dependent
brain_analyzeContent analysis (SEO, parity, stale, quality)activenone
validate_schemaSchema-level validationactivenone
list_submissionsList form submissionsactivenone
approve_submissionApprove submissionactiveworkflow-dependent
reject_submissionReject submissionactivenone

State Machine

The project state machine enforces valid operation sequences:

ts
type ProjectPhase = 'uninitialized' | 'init_pending' | 'active' | 'error'

Phase Derivation

ts
function deriveProjectPhase(config, pendingBranches, projectStatus): ProjectPhase {
  if (projectStatus === 'error') return 'error'
  if (!config) {
    const hasInitBranch = pendingBranches.some(b => b.name.startsWith('cr/new/init/'))
    return hasInitBranch ? 'init_pending' : 'uninitialized'
  }
  return 'active'
}

Transition Rules

PhaseAllowed ToolsBlocked Tools
uninitializedinit_project, read-only toolsAll write tools except init
init_pendingmerge_branch, reject_branch, read-onlyAll content writes
activeAll tools except init_projectinit_project
errormerge_branch, reject_branch, read-onlyAll write tools

When a tool is blocked, the state machine returns a clear reason and suggestion:

ts
{
  allowed: false,
  reason: 'Project is not initialized. No .contentrain/ directory exists.',
  suggestion: 'Call init_project first to set up the content structure.'
}

Intent Classification

Before building the system prompt, the agent classifies the user's intent:

ts
type IntentCategory =
  | 'content_operation'
  | 'model_operation'
  | 'branch_operation'
  | 'project_operation'
  | 'query'
  | 'out_of_scope'

Classification uses keyword matching against message text in both English and Turkish. When no keywords match but a model is selected in the UI, the system defaults to content_operation with medium confidence.

System Prompt Structure

The system prompt is assembled from 11 sections:

  1. Role Definition -- strict, bounded task executor identity
  2. Contentrain Architecture -- data format, field types, content kinds, relations, storage
  3. UI Context -- what the user is currently viewing (model, locale, entry, panel state, pinned items)
  4. Inferred Intent -- classified intent with default parameters
  5. Project State -- phase, initialization status, pending branches, content stats
  6. Project Configuration -- stack, locales, domains, workflow mode
  7. Content Schema -- full model definitions with all fields and constraints
  8. Relation Graph -- cross-model reference map
  9. Vocabulary -- shared terminology from .contentrain/vocabulary.json
  10. Permissions -- user role, available tools, model restrictions
  11. Rules -- workflow rules, plan-aware guidance, upgrade hints

Context-Aware Defaults

When the UI has a model selected, the agent uses it as a default without asking:

## Inferred Intent: content_operation
Default model: blog-post
Default locale: en
When confidence is high, use these defaults without asking.

Pinned Context Items

Users can pin content items from the context panel. These appear in the system prompt:

### Pinned Context
- Entry "a1b2c3d4e5f6" from Blog Post: {"title":"Hello World","excerpt":"..."}
- Asset: hero.webp (webp, 1920x1080) -> path: media/original/abc123.webp

Tool Role Matrix

This matrix governs the internal chat agent (agent-permissions.ts), which combines workspace roles (owner / admin / member) with project roles (editor / reviewer / viewer). Workspace owners and admins get every tool; workspace members fall back to their project role (defaulting to viewer). This is distinct from the Conversation API, whose API keys use a separate 3-role model (viewer / editor / admin).

Toolviewerreviewereditoradminowner
list_modelsYesYesYesYesYes
get_contentYesYesYesYesYes
brain_queryYesYesYesYesYes
brain_searchYesYesYesYesYes
brain_analyzeYesYesYesYesYes
validateYesYesYesYesYes
validate_schemaYesYesYesYesYes
list_branchesYesYesYesYesYes
branch_healthYesYesYesYesYes
relation_expandYesYesYesYesYes
search_mediaYesYesYesYesYes
get_mediaYesYesYesYesYes
list_submissionsYesYesYesYesYes
save_content----YesYesYes
delete_content----YesYesYes
copy_locale----YesYesYes
update_status----YesYesYes
upload_media----YesYesYes
update_media----YesYesYes
delete_media----YesYesYes
vocabulary----YesYesYes
merge_branch--Yes--YesYes
reject_branch--Yes--YesYes
approve_submission--Yes--YesYes
reject_submission--Yes--YesYes
save_model------YesYes
init_project------YesYes
add_locale------YesYes
delete_model------YesYes

Conversation Engine

The conversation engine is an AsyncGenerator that yields events:

ts
async function* runConversationLoop(
  config: ConversationConfig,
  toolCtx: ToolExecutionContext,
): AsyncGenerator<ConversationEvent>

Loop Behavior

  1. First iteration: uses AIProvider.streamCompletion for real-time streaming
  2. Subsequent iterations (tool use continuations): uses AIProvider.createCompletion (non-streaming)
  3. Max iterations: 5 (prevents runaway tool loops)
  4. Tool result truncation: 2000 characters per result (context window management)
  5. Abort signal: supports client disconnect detection

Auto-Merge Decision

ts
function shouldAutoMerge(workflow: string, permissions: AgentPermissions): boolean {
  if (workflow === 'auto-merge') return true
  // In review workflow, only Owner/Admin can auto-merge
  return permissions.workspaceRole === 'owner' || permissions.workspaceRole === 'admin'
}

Affected Resources

Every tool execution returns affected resources for targeted cache invalidation:

ts
interface AffectedResources {
  models: string[]         // Model IDs with content changes
  locales: string[]        // Locales that were modified
  snapshotChanged: boolean // Model definitions changed
  branchesChanged: boolean // Branch list changed
  branch?: string          // Specific branch affected
}

Resources are accumulated across all tool calls in a conversation turn and sent in the final done event. The client uses this to selectively refresh only the changed parts of the UI.

Brain Cache

The brain cache provides fast in-memory access to project content:

ts
const brain = await getOrBuildBrainCache(git, contentRoot, projectId)
// brain.models: Map<string, ModelDefinition>
// brain.content: Map<string, unknown>     // key: "modelId:locale"
// brain.meta: Map<string, unknown>        // key: "modelId:locale"
// brain.schemaValidation: ValidationResult

The cache is built from Git on first access and invalidated after any write operation via invalidateBrainCache(projectId).

Content Strings

Server-side text for agent prompts and error messages comes from Contentrain dictionaries:

ts
agentPrompt('role.definition')           // → agent-prompts dictionary
agentMessage('forms.approved')           // → agent-messages dictionary
errorMessage('auth.unauthorized')        // → error-messages dictionary

These are read from pre-generated SDK output -- synchronous, zero I/O.

Released under the AGPL-3.0 License.