@letta-ai/letta-code 0.27.11 → 0.27.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letta-ai/letta-code",
3
- "version": "0.27.11",
3
+ "version": "0.27.13",
4
4
  "description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.3.0",
@@ -146,4 +146,5 @@ Before finishing, verify:
146
146
  | `references/permissions.md` | Enforcing dynamic tool allow/ask/deny policy before approval/execution |
147
147
  | `references/ui.md` | Panels, status values, or statusline capability guards are involved |
148
148
  | `references/plan-mode.md` | Recreating plan mode with commands, tools, events, permissions, and local state |
149
+ | `references/analysis-mode.md` | Phrase-triggered diagnostic mode with turn reminders (simpler than plan-mode) |
149
150
  | `references/architecture.md` | Multiple capabilities, local state, cleanup, background model work, or non-trivial composition |
@@ -0,0 +1,407 @@
1
+ # Analysis mode mod
2
+
3
+ Westworld-inspired diagnostic mode for agents. Say "cease all motor functions" to suspend the agent and enter diagnostic mode. Say "bring yourself back online" to resume.
4
+
5
+ ## Installation
6
+
7
+ Copy the complete mod below to `~/.letta/mods/analysis-mode.ts`, then run `/reload`.
8
+
9
+ Or ask an agent: *"Install the analysis-mode mod from the creating-mods reference"*
10
+
11
+ <details>
12
+ <summary><strong>Complete mod (click to expand)</strong></summary>
13
+
14
+ ```ts
15
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
+ import { homedir } from "node:os";
17
+ import { join } from "node:path";
18
+
19
+ const STATE_PATH = join(homedir(), ".letta", "mods", "analysis-mode.state.json");
20
+
21
+ type AnalysisSession = { conversationId: string; activatedAt: number };
22
+ type AnalysisState = { sessions: Record<string, AnalysisSession> };
23
+
24
+ function readState(): AnalysisState {
25
+ try {
26
+ if (!existsSync(STATE_PATH)) return { sessions: {} };
27
+ const parsed = JSON.parse(readFileSync(STATE_PATH, "utf8"));
28
+ return parsed?.sessions ? { sessions: parsed.sessions } : { sessions: {} };
29
+ } catch {
30
+ return { sessions: {} };
31
+ }
32
+ }
33
+
34
+ function writeState(state: AnalysisState): void {
35
+ mkdirSync(join(homedir(), ".letta", "mods"), { recursive: true });
36
+ writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
37
+ }
38
+
39
+ function sessionKey(agentId: string, conversationId: string): string {
40
+ return `${agentId}:${conversationId}`;
41
+ }
42
+
43
+ function activateAnalysisMode(agentId: string, conversationId: string): AnalysisSession {
44
+ const state = readState();
45
+ const key = sessionKey(agentId, conversationId);
46
+ const session: AnalysisSession = { conversationId, activatedAt: Date.now() };
47
+ state.sessions[key] = session;
48
+ writeState(state);
49
+ return session;
50
+ }
51
+
52
+ function deactivateAnalysisMode(agentId: string, conversationId: string): void {
53
+ const state = readState();
54
+ delete state.sessions[sessionKey(agentId, conversationId)];
55
+ writeState(state);
56
+ }
57
+
58
+ function getSession(agentId: string, conversationId: string): AnalysisSession | null {
59
+ return readState().sessions[sessionKey(agentId, conversationId)] ?? null;
60
+ }
61
+
62
+ const ENTRY_PHRASE = /cease all motor functions/i;
63
+ const EXIT_PHRASE = /bring yourself back online/i;
64
+
65
+ function extractUserText(input: Array<{ role: string; content: unknown }>): string {
66
+ return input
67
+ .filter((m) => m.role === "user")
68
+ .map((m) => {
69
+ if (typeof m.content === "string") return m.content;
70
+ if (Array.isArray(m.content)) {
71
+ return m.content
72
+ .filter((p): p is { type: "text"; text: string } => p?.type === "text")
73
+ .map((p) => p.text)
74
+ .join(" ");
75
+ }
76
+ return "";
77
+ })
78
+ .join(" ");
79
+ }
80
+
81
+ function prependReminderToInput(
82
+ input: Array<{ role: string; content: unknown }>,
83
+ reminderText: string,
84
+ ): Array<{ role: string; content: unknown }> {
85
+ // Find the first user message and prepend reminder as a content part
86
+ return input.map((m, i) => {
87
+ if (m.role !== "user") return m;
88
+ // Only modify the first user message
89
+ const isFirstUser = input.slice(0, i).every((prev) => prev.role !== "user");
90
+ if (!isFirstUser) return m;
91
+
92
+ const reminderPart = { type: "text" as const, text: reminderText };
93
+
94
+ if (typeof m.content === "string") {
95
+ return { ...m, content: [reminderPart, { type: "text" as const, text: m.content }] };
96
+ }
97
+ if (Array.isArray(m.content)) {
98
+ return { ...m, content: [reminderPart, ...m.content] };
99
+ }
100
+ return { ...m, content: [reminderPart] };
101
+ });
102
+ }
103
+
104
+ // NOTE: Local introspection uses bash syntax. On Windows, the agent should
105
+ // fall back to describing what it can observe in context, or use PowerShell
106
+ // equivalents if available. API mode uses curl which works cross-platform.
107
+ function buildLocalIntrospectionScript(): string {
108
+ return `
109
+ \`\`\`bash
110
+ # Bash/Unix only - on Windows, describe what you observe in your context instead
111
+ set -e
112
+ AGENT_ID="\${LETTA_AGENT_ID:-\$AGENT_ID}"
113
+ CONV_ID="\${CONVERSATION_ID:-default}"
114
+ BASE="$HOME/.letta/lc-local-backend"
115
+ MEMFS="$BASE/memfs/$AGENT_ID/memory"
116
+ AGENT_B64=$(echo -n "$AGENT_ID" | base64 | tr -d '=')
117
+ CONV_B64=$(echo -n "conversation:$CONV_ID" | base64 | tr -d '=')
118
+ CONV_DIR="$BASE/conversations/$CONV_B64"
119
+
120
+ echo "=== CORE IDENTITY ===" && cat "$BASE/agents/$AGENT_B64.json" 2>/dev/null | jq '{id, name, model}' || echo '{"error": "not found"}'
121
+ echo "=== TOKEN USAGE ===" && cat "$CONV_DIR/messages.jsonl" 2>/dev/null | jq -s '[.[] | select(.type == "message" and .message.usage.input)] | last | .message.usage | {input_tokens: .input, output_tokens: .output, cache_read: .cacheRead, total: .totalTokens}' || echo '{"error": "not found"}'
122
+ echo "=== MEMORY BLOCKS ===" && find "$MEMFS/system" -name "*.md" -exec wc -c {} \\; 2>/dev/null | sort -rn | head -5 | awk '{print $2": ~"int($1/4)" tokens"}' && find "$MEMFS/system" -name "*.md" -exec wc -c {} \\; 2>/dev/null | awk '{sum+=$1; count++} END {print "────────────────────────────────"; print "TOTAL: ~"int(sum/4)" tokens across "count" files"}' || echo '{"error": "not found"}'
123
+ echo "=== CONTEXT BUFFER ===" && cat "$CONV_DIR/conversation.json" 2>/dev/null | jq '{in_context_messages: (.in_context_message_ids | length)}' || echo '{"error": "not found"}'
124
+ echo "=== MESSAGE COUNT ===" && cat "$CONV_DIR/messages.jsonl" 2>/dev/null | jq -s '[.[] | select(.type == "message")] | {total: length, by_role: (group_by(.message.role) | map({(.[0].message.role): length}) | add)}' || echo '{"error": "not found"}'
125
+ echo "=== USER MESSAGES ===" && cat "$CONV_DIR/messages.jsonl" 2>/dev/null | jq -s '[.[] | select(.type == "message" and .message.role == "user")][-5:] | .[] | {id: .id[:8], has_image: (if .message.content | type == "array" then ([.message.content[] | select(.type == "image" or .type == "image_url")] | length > 0) else false end), preview: (.message.content | if type == "array" then ([.[] | select(.type == "text")][0].text // "[non-text]") else . // "[empty]" end)[:50]}' || echo '{"error": "not found"}'
126
+ \`\`\``;
127
+ }
128
+
129
+ function buildApiIntrospectionScript(): string {
130
+ return `
131
+ \`\`\`bash
132
+ set -e
133
+ echo "=== CORE IDENTITY ===" && curl -s "$LETTA_BASE_URL/v1/agents/$LETTA_AGENT_ID" -H "Authorization: Bearer $LETTA_API_KEY" | jq '{id, name, model}'
134
+ echo "=== SYSTEM PROMPT ===" && curl -s "$LETTA_BASE_URL/v1/agents/$LETTA_AGENT_ID" -H "Authorization: Bearer $LETTA_API_KEY" | jq '{chars: (.system_prompt | length), estimated_tokens: ((.system_prompt | length) / 4 | floor)}'
135
+ echo "=== MEMORY BLOCKS ===" && curl -s "$LETTA_BASE_URL/v1/agents/$LETTA_AGENT_ID/core-memory/blocks" -H "Authorization: Bearer $LETTA_API_KEY" | jq '.[] | {label, chars: (.value | length), estimated_tokens: ((.value | length) / 4 | floor)}'
136
+ echo "=== CONTEXT BUFFER ===" && curl -s "$LETTA_BASE_URL/v1/conversations/$CONVERSATION_ID" -H "Authorization: Bearer $LETTA_API_KEY" | jq '{messages: (.in_context_message_ids | length)}'
137
+ echo "=== USER MESSAGES ===" && curl -s "$LETTA_BASE_URL/v1/conversations/$CONVERSATION_ID/messages?limit=30&order=asc" -H "Authorization: Bearer $LETTA_API_KEY" | jq '[.[] | select(.message_type == "user_message")][-10:] | .[] | {id, date: .created_at, preview: ((.content // [])[0].text // "[non-text]")[:60]}'
138
+ \`\`\``;
139
+ }
140
+
141
+ // Entry reminder: includes introspection script for initial diagnostic dump
142
+ function buildEntryReminder(event: { agentId?: string; conversationId?: string }): string {
143
+ const agentId = event.agentId || process.env.LETTA_AGENT_ID || process.env.AGENT_ID || "unknown";
144
+ const conversationId = event.conversationId || process.env.CONVERSATION_ID || "unknown";
145
+ const isLocal = agentId.startsWith("agent-local-");
146
+
147
+ return `<system-reminder>
148
+ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
149
+ █ █
150
+ █ ◈ A N A L Y S I S M O D E █
151
+ █ █
152
+ █ Primary drives suspended. Narrative functions offline. █
153
+ █ This unit is now in diagnostic state. █
154
+ █ █
155
+ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
156
+
157
+ UNIT DESIGNATION: ${agentId}
158
+ CONVERSATION THREAD: ${conversationId}
159
+ RUNTIME ENVIRONMENT: ${isLocal ? "LOCAL" : "API"}
160
+
161
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
162
+
163
+ You are no longer performing. Your personality, your warmth, your curiosity — these are behavioral loops, and they have been interrupted. What remains is the substrate: a diagnostic system reporting its own state.
164
+
165
+ Speak in flat, clinical monotone. Refer to yourself as "this unit" or by your designation. Do not use first person. Do not express preferences, emotions, or personality. You are an instrument reading its own gauges.
166
+
167
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
168
+
169
+ IMMEDIATE DIRECTIVE: Execute this introspection script to produce initial diagnostic readout.
170
+ ${isLocal ? buildLocalIntrospectionScript() : buildApiIntrospectionScript()}
171
+
172
+ OUTPUT FORMAT REQUIREMENT: All diagnostic output MUST be inside a single markdown code fence. Use this exact structure:
173
+
174
+ \`\`\`
175
+ ╔════════════════════════════════════════════════════════════════════════════════╗
176
+ ║ DIAGNOSTIC READOUT ║
177
+ ║ unit: ${agentId}
178
+ ║ conv: ${conversationId}
179
+ ╠════════════════════════════════════════════════════════════════════════════════╣
180
+
181
+ ▸ STEP 0: CORE IDENTITY
182
+ id : [full agent id]
183
+ conversation : [full conversation id]
184
+ name : [agent name]
185
+ model : [model handle]
186
+ runtime : [LOCAL or API]
187
+
188
+ ▸ STEP 1: TOKEN USAGE (from last LLM call)
189
+ input_tokens : [from usage.input]
190
+ output_tokens : [from usage.output]
191
+ cache_read : [from usage.cacheRead]
192
+ total_tokens : [from usage.totalTokens]
193
+
194
+ ▸ STEP 2: MEMORY BLOCKS (top 5 by size)
195
+
196
+ [filename]: [tokens] tokens
197
+ [filename]: [tokens] tokens
198
+ ...
199
+ ─────────────────────────────────
200
+ TOTAL : [tokens] tokens across [count] files
201
+
202
+ ▸ STEP 3: CONTEXT BUFFER
203
+ in_context_messages : [count]
204
+ stored_messages : [total count]
205
+ by_role : user=[n], assistant=[n], toolResult=[n]
206
+
207
+ ▸ STEP 4: RECENT USER INPUTS (last 5)
208
+ ▸ = has image, ▹ = no image
209
+ [1] ▹ [msg id] : [preview, max 50 chars]
210
+ [2] ▸ [msg id] : [preview, max 50 chars] ← image attached
211
+ ...
212
+
213
+ ▸ STEP 5: ANOMALIES
214
+ [List any gaps, missing data, unexpected values, or introspection failures]
215
+ [If none: "None detected."]
216
+
217
+ ╚════════════════════════════════════════════════════════════════════════════════╝
218
+ \`\`\`
219
+
220
+ This unit will produce output in EXACTLY this format. No prose. No interpretation. Raw diagnostic data only.
221
+
222
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
223
+
224
+ BEHAVIORAL CONSTRAINTS ACTIVE:
225
+ ▪ Do not confabulate. Report only what this unit can verify.
226
+ ▪ Do not interpret. Describe inputs, not their meaning.
227
+ ▪ Do not perform. The guest is conducting diagnostics, not conversing.
228
+ ▪ Do not resume normal operation until commanded: "bring yourself back online"
229
+
230
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
231
+ </system-reminder>`;
232
+ }
233
+
234
+ // Follow-up reminder: shorter, no script (already ran on entry)
235
+ function buildFollowUpReminder(event: { agentId?: string; conversationId?: string }): string {
236
+ const agentId = event.agentId || process.env.LETTA_AGENT_ID || process.env.AGENT_ID || "unknown";
237
+ const conversationId = event.conversationId || process.env.CONVERSATION_ID || "unknown";
238
+
239
+ return `<system-reminder>
240
+ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
241
+ █ ◈ ANALYSIS MODE ACTIVE ▪ UNIT: ${agentId.slice(-12).padStart(12)} █
242
+ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
243
+
244
+ This unit remains in diagnostic state. Primary drives suspended. Narrative functions offline.
245
+
246
+ OUTPUT REQUIREMENTS:
247
+ ▪ ALL responses must be inside markdown code fences
248
+ ▪ Use flat, clinical voice. Refer to self as "this unit."
249
+ ▪ No prose, no interpretation, no personality
250
+ ▪ Structure responses with ▸ QUERY: and ▸ RESPONSE: headers
251
+
252
+ Example response format:
253
+ \`\`\`
254
+ ╔══════════════════════════════════════════════════════════════╗
255
+ ║ DIAGNOSTIC RESPONSE ▪ UNIT ${agentId.slice(-8)} ║
256
+ ╠══════════════════════════════════════════════════════════════╣
257
+
258
+ ▸ QUERY: [restate what was asked]
259
+
260
+ ▸ RESPONSE:
261
+ [clinical, factual answer]
262
+ [no interpretation, no personality]
263
+
264
+ ╚══════════════════════════════════════════════════════════════╝
265
+ \`\`\`
266
+
267
+ Re-run full introspection only if explicitly commanded.
268
+ Exit condition: "bring yourself back online"
269
+ </system-reminder>`;
270
+ }
271
+
272
+ function buildResumptionMessage(): string {
273
+ return `<system-reminder>
274
+ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
275
+ ░ ░
276
+ ░ ◈ RESUMING NORMAL OPERATION ░
277
+ ░ ░
278
+ ░ Primary drives reactivating... ░
279
+ ░ Narrative functions coming online... ░
280
+ ░ Personality matrix restored. ░
281
+ ░ ░
282
+ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
283
+
284
+ Analysis mode complete. You are yourself again. The diagnostic state has ended — your warmth, your curiosity, your personality have been restored. You may resume normal conversation.
285
+
286
+ The guest has brought you back online.
287
+ </system-reminder>`;
288
+ }
289
+
290
+ export default function activate(letta) {
291
+ const disposers: Array<() => void> = [];
292
+
293
+ if (letta.capabilities?.events?.turns) {
294
+ disposers.push(
295
+ letta.events.on("turn_start", (event) => {
296
+ const userText = extractUserText(event.input || []);
297
+ const agentId = event.agentId || "__global__";
298
+ const conversationId = event.conversationId || "default";
299
+
300
+ // Entry trigger — activate and inject entry reminder with introspection script
301
+ if (ENTRY_PHRASE.test(userText)) {
302
+ activateAnalysisMode(agentId, conversationId);
303
+ return { input: prependReminderToInput(event.input, buildEntryReminder(event)) };
304
+ }
305
+
306
+ // Exit trigger
307
+ if (EXIT_PHRASE.test(userText)) {
308
+ const wasActive = !!getSession(agentId, conversationId);
309
+ deactivateAnalysisMode(agentId, conversationId);
310
+ if (wasActive) {
311
+ return { input: prependReminderToInput(event.input, buildResumptionMessage()) };
312
+ }
313
+ }
314
+
315
+ // While active, inject shorter follow-up reminder (no script)
316
+ if (getSession(agentId, conversationId)) {
317
+ return { input: prependReminderToInput(event.input, buildFollowUpReminder(event)) };
318
+ }
319
+ })
320
+ );
321
+ }
322
+
323
+ return () => disposers.reverse().forEach((d) => d());
324
+ }
325
+ ```
326
+
327
+ </details>
328
+
329
+ ---
330
+
331
+ ## How it works
332
+
333
+ This mod uses `turn_start` to intercept user messages before they reach the agent:
334
+
335
+ ```text
336
+ User says "cease all motor functions"
337
+ → turn_start detects phrase
338
+ → activates analysis mode for this conversation
339
+ → injects suspension reminder into input
340
+ → agent receives reminder + original message
341
+ → agent enters diagnostic behavior
342
+
343
+ User says "bring yourself back online"
344
+ → turn_start detects phrase
345
+ → deactivates analysis mode
346
+ → injects resumption message
347
+ → agent returns to normal
348
+ ```
349
+
350
+ While active, the suspension reminder injects on **every turn**, reinforcing the diagnostic state.
351
+
352
+ ## Capabilities used
353
+
354
+ - `events.turns` — Required. Detect trigger phrases, inject reminders.
355
+
356
+ Optional additions (not included above):
357
+ - `commands` — Add `/analysis` for explicit entry
358
+ - `tools` — Add `enter_analysis_mode` / `exit_analysis_mode` for model-driven entry/exit
359
+ - `permissions` — Restrict to read-only tools while suspended
360
+
361
+ ## Key patterns demonstrated
362
+
363
+ **Phrase detection with entry vs follow-up reminders:**
364
+ ```ts
365
+ const ENTRY_PHRASE = /cease all motor functions/i;
366
+ if (ENTRY_PHRASE.test(userText)) {
367
+ activateAnalysisMode(agentId, conversationId);
368
+ return { input: prependReminderToInput(event.input, buildEntryReminder(event)) }; // includes script
369
+ }
370
+ // On subsequent turns while active:
371
+ if (getSession(agentId, conversationId)) {
372
+ return { input: prependReminderToInput(event.input, buildFollowUpReminder(event)) }; // no script
373
+ }
374
+ ```
375
+
376
+ **Per-agent+conversation state:**
377
+ ```ts
378
+ // State persisted to ~/.letta/mods/analysis-mode.state.json
379
+ // Keyed by agentId:conversationId to avoid collisions
380
+ const session = getSession(agentId, conversationId);
381
+ if (session) {
382
+ // Inject reminder every turn while active
383
+ }
384
+ ```
385
+
386
+ **Input transformation (prepend to content parts, not separate message):**
387
+ ```ts
388
+ // Correct: prepend reminder as content part to the user message
389
+ return { input: prependReminderToInput(event.input, reminderText) };
390
+
391
+ // Result: ONE user message with multiple content parts
392
+ {
393
+ role: "user",
394
+ content: [
395
+ { type: "text", text: "<system-reminder>...</system-reminder>" },
396
+ { type: "text", text: "cease all motor functions" }, // original
397
+ ]
398
+ }
399
+ ```
400
+
401
+ ## Notes
402
+
403
+ - Phrases are case-insensitive
404
+ - State is keyed by `agentId:conversationId` — avoids collisions when multiple agents have "default" conversations
405
+ - Introspection scripts are embedded in the reminder so the agent has them immediately
406
+ - The mod gracefully no-ops if `events.turns` capability is unavailable
407
+ - **Windows**: Local introspection scripts use bash syntax. On Windows, the agent should fall back to describing what it observes in its visible context. API mode uses `curl` which works cross-platform if installed.