Diff Engine
How lettactl compares your local config against live server state and computes the minimum set of changes.
How Diffing Works
When you run `lettactl apply`, the engine follows a strict pipeline: parse YAML → resolve shared resources → fetch live state from the Letta server → diff each agent. For every agent in your config, it checks the agent registry (a name→ID lookup). If the agent doesn't exist yet, it's marked CREATE. If it exists, the engine fetches its full state and runs a field-by-field comparison. If every field matches, the agent is marked UNCHANGED. Otherwise, it generates a precise update plan with the minimum operations needed.
Field-Level Comparison
The engine compares 8 scalar fields individually: system prompt (trimmed, then string equality), description, LLM model (default: google_ai/gemini-2.5-pro), context window (default: 28000), embedding model (default: openai/text-embedding-3-small), embedding config (deep-normalized with sorted keys before JSON comparison), reasoning mode (default: true), and tags (arrays sorted before comparison). Each changed field increments the operation count by one.
Tool Diffing
Tools produce four buckets: toAdd, toRemove, toUpdate, and unchanged. Builtin tools (archival_memory_search, memory_replace, web_search, etc.) are implicitly available — if they appear in your config but not in the server response, they're marked unchanged rather than added. Protected tools (memory_insert, memory_replace, memory_rethink, conversation_search, and file search tools) are never removed, even with --force. Tool updates happen when source code changes are detected via content hashing — the old tool ID is detached and the new one attached.
Block Diffing
Memory blocks have five buckets: toAdd, toRemove, toUpdate (swap block IDs), toUpdateValue (in-place content update), and unchanged. The critical distinction is agent_owned. When agent_owned is true, the agent writes to the block during conversations and lettactl won't overwrite the content — only metadata like description or limit gets synced. When agent_owned is false, the YAML value is source of truth and the engine compares content strings, generating a toUpdateValue operation if they differ. Shared blocks are always treated as agent_owned regardless of config.
Folder Diffing
Folders track file-level changes using SHA-256 content hashes (truncated to 16 chars) stored in agent metadata under lettactl.folderFileHashes. On each apply, the engine compares current hashes against previous hashes. Updates only fire when both hashes exist and differ — this prevents false positives on first-time tracking. The engine also handles Letta's automatic file rename behavior: when duplicate filenames collide, Letta appends _(N) suffixes. The diff engine normalizes these back to canonical names, removes the suffixed variants, and re-uploads the clean file.
Drift Detection
Drift happens when the server state diverges from your YAML — someone manually edited an agent's prompt in the UI, or the agent modified its own memory. The --dry-run flag shows you exactly what drifted with a color-coded diff. Red lines show what would be removed, green lines show what would be added. Resources that would require --force to remove are labeled explicitly.
# See what drifted
lettactl apply -f fleet.yaml --dry-run
# Output shows:
# [~] support-agent (UPDATE - 3 changes)
# system_prompt:
# - You are a MODIFIED assistant.
# + You are the ORIGINAL assistant.
# Tool [-]: some_old_tool (requires --force)
# Block [~]: pricing_rules (value changed)Operation Counting & Conversation Preservation
Every diff tracks an operationCount — the sum of all individual changes across all categories. Each changed field counts as 1. Each tool add/remove/update counts as 1. Each block operation counts as 1. Folder operations count at the file level — a folder with 3 files to add and 1 to remove counts as 4 operations. An agent with zero operations is marked UNCHANGED. Every diff also carries a preservesConversation flag. In the current implementation, this is always true — all update operations (field changes, tool swaps, block updates, folder changes) maintain the agent's conversation history. The agent is never recreated from scratch during apply.
The --force Flag
By default, apply is additive — it adds and updates resources but never removes anything not in your config. This is safe for incremental adoption. The --force flag enables strict reconciliation: resources on the server that aren't in your YAML get detached. Tools, blocks, folders, and archives are all subject to force removal. The one exception: protected memory tools (memory_insert, memory_replace, memory_rethink, conversation_search, file search tools) are never removed, even with --force. They're critical for agent operation.
# Additive only (default) - won't remove anything
lettactl apply -f fleet.yaml
# Strict reconciliation - removes resources not in YAML
lettactl apply -f fleet.yaml --force
# Preview what --force would remove
lettactl apply -f fleet.yaml --force --dry-runSelective Apply
You don't have to deploy everything. Use --agent to filter by name pattern or --match to filter by tags. Combined with --dry-run, this lets you preview and deploy changes to specific parts of your fleet.
# Only agents matching a pattern
lettactl apply -f fleet.yaml --agent "support-*"
# Only agents with specific tags
lettactl apply -f fleet.yaml --match tags:tier=canary
# Combine both
lettactl apply -f fleet.yaml --agent "support-*" --match tags:team=eng