@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/README.md +8 -63
- package/dist/app-server-client.js +28 -4
- package/dist/app-server-client.js.map +3 -3
- package/dist/types/app-server-client.d.ts +7 -2
- package/dist/types/app-server-client.d.ts.map +1 -1
- package/dist/types/types/protocol_v2.d.ts +1 -0
- package/dist/types/types/protocol_v2.d.ts.map +1 -1
- package/letta.js +5326 -1686
- package/package.json +1 -1
- package/skills/creating-mods/SKILL.md +1 -0
- package/skills/creating-mods/references/analysis-mode.md +407 -0
package/package.json
CHANGED
|
@@ -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.
|