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 → ClientKey Files
| File | Purpose |
|---|---|
server/utils/agent-types.ts | Type definitions for all agent types |
server/utils/agent-tools.ts | Tool definitions with orchestration metadata |
server/utils/agent-permissions.ts | Role-based tool filtering |
server/utils/agent-state-machine.ts | Project phase enforcement |
server/utils/agent-context.ts | Intent classification |
server/utils/agent-system-prompt.ts | System prompt construction |
server/utils/conversation-engine.ts | Conversation loop (async generator) |
server/utils/brain-cache.ts | In-memory content cache |
server/utils/content-strings.ts | Server-side dictionary reader |
Tool Definitions
Each tool carries orchestration metadata beyond what the LLM sees:
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
| Tool | Description | Phases | Workflow |
|---|---|---|---|
list_models | List content models | all | none |
get_content | Read content entries | active | none |
save_content | Create/update content | active | workflow-dependent |
delete_content | Delete entries or dictionary keys | active | workflow-dependent |
save_model | Create/update models | active | workflow-dependent |
validate | Validate content | all | none |
list_branches | List cr/* branches | active, init_pending, error | none |
merge_branch | Merge a branch | active, init_pending | manual |
reject_branch | Reject a branch | active, init_pending | manual |
init_project | Initialize .contentrain/ | uninitialized | auto-merge |
copy_locale | Copy between locales | active | workflow-dependent |
brain_query | Read from brain cache | active | none |
brain_search | Full-text search | active | none |
search_media | Search media library | active | none |
upload_media | Upload from URL | active | none |
get_media | Get asset metadata | active | none |
update_status | Publish/unpublish/archive entries | active | workflow-dependent |
update_media | Update asset alt/tags/focal point | active | none |
delete_media | Delete an asset and its variants | active | none |
branch_health | Check pending-branch warn/block thresholds | active, init_pending, error | none |
relation_expand | Resolve forward/reverse relations | active | none |
vocabulary | Read/update the project glossary | active | workflow-dependent |
add_locale | Register a new supported locale | active | workflow-dependent |
delete_model | Delete a model and all its content | active | workflow-dependent |
brain_analyze | Content analysis (SEO, parity, stale, quality) | active | none |
validate_schema | Schema-level validation | active | none |
list_submissions | List form submissions | active | none |
approve_submission | Approve submission | active | workflow-dependent |
reject_submission | Reject submission | active | none |
State Machine
The project state machine enforces valid operation sequences:
type ProjectPhase = 'uninitialized' | 'init_pending' | 'active' | 'error'Phase Derivation
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
| Phase | Allowed Tools | Blocked Tools |
|---|---|---|
uninitialized | init_project, read-only tools | All write tools except init |
init_pending | merge_branch, reject_branch, read-only | All content writes |
active | All tools except init_project | init_project |
error | merge_branch, reject_branch, read-only | All write tools |
When a tool is blocked, the state machine returns a clear reason and suggestion:
{
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:
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:
- Role Definition -- strict, bounded task executor identity
- Contentrain Architecture -- data format, field types, content kinds, relations, storage
- UI Context -- what the user is currently viewing (model, locale, entry, panel state, pinned items)
- Inferred Intent -- classified intent with default parameters
- Project State -- phase, initialization status, pending branches, content stats
- Project Configuration -- stack, locales, domains, workflow mode
- Content Schema -- full model definitions with all fields and constraints
- Relation Graph -- cross-model reference map
- Vocabulary -- shared terminology from
.contentrain/vocabulary.json - Permissions -- user role, available tools, model restrictions
- 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.webpTool 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).
| Tool | viewer | reviewer | editor | admin | owner |
|---|---|---|---|---|---|
list_models | Yes | Yes | Yes | Yes | Yes |
get_content | Yes | Yes | Yes | Yes | Yes |
brain_query | Yes | Yes | Yes | Yes | Yes |
brain_search | Yes | Yes | Yes | Yes | Yes |
brain_analyze | Yes | Yes | Yes | Yes | Yes |
validate | Yes | Yes | Yes | Yes | Yes |
validate_schema | Yes | Yes | Yes | Yes | Yes |
list_branches | Yes | Yes | Yes | Yes | Yes |
branch_health | Yes | Yes | Yes | Yes | Yes |
relation_expand | Yes | Yes | Yes | Yes | Yes |
search_media | Yes | Yes | Yes | Yes | Yes |
get_media | Yes | Yes | Yes | Yes | Yes |
list_submissions | Yes | Yes | Yes | Yes | Yes |
save_content | -- | -- | Yes | Yes | Yes |
delete_content | -- | -- | Yes | Yes | Yes |
copy_locale | -- | -- | Yes | Yes | Yes |
update_status | -- | -- | Yes | Yes | Yes |
upload_media | -- | -- | Yes | Yes | Yes |
update_media | -- | -- | Yes | Yes | Yes |
delete_media | -- | -- | Yes | Yes | Yes |
vocabulary | -- | -- | Yes | Yes | Yes |
merge_branch | -- | Yes | -- | Yes | Yes |
reject_branch | -- | Yes | -- | Yes | Yes |
approve_submission | -- | Yes | -- | Yes | Yes |
reject_submission | -- | Yes | -- | Yes | Yes |
save_model | -- | -- | -- | Yes | Yes |
init_project | -- | -- | -- | Yes | Yes |
add_locale | -- | -- | -- | Yes | Yes |
delete_model | -- | -- | -- | Yes | Yes |
Conversation Engine
The conversation engine is an AsyncGenerator that yields events:
async function* runConversationLoop(
config: ConversationConfig,
toolCtx: ToolExecutionContext,
): AsyncGenerator<ConversationEvent>Loop Behavior
- First iteration: uses
AIProvider.streamCompletionfor real-time streaming - Subsequent iterations (tool use continuations): uses
AIProvider.createCompletion(non-streaming) - Max iterations: 5 (prevents runaway tool loops)
- Tool result truncation: 2000 characters per result (context window management)
- Abort signal: supports client disconnect detection
Auto-Merge Decision
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:
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:
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: ValidationResultThe 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:
agentPrompt('role.definition') // → agent-prompts dictionary
agentMessage('forms.approved') // → agent-messages dictionary
errorMessage('auth.unauthorized') // → error-messages dictionaryThese are read from pre-generated SDK output -- synchronous, zero I/O.
Related Pages
- Content Engine -- write path internals
- Roles & Permissions -- permission model
- Chat API -- SSE endpoint
- Field Types -- content schema types