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.