@letta-ai/letta-code 0.19.7 → 0.19.9
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/letta.js +3711 -1738
- package/package.json +2 -1
- package/scripts/latency-benchmark.ts +18 -9
- package/skills/context_doctor/SKILL.md +132 -0
- package/skills/context_doctor/scripts/estimate_system_tokens.ts +181 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@letta-ai/letta-code",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.9",
|
|
4
4
|
"description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"typescript": "^5.0.0"
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|
|
67
|
+
"prepare": "node .husky/install.mjs",
|
|
67
68
|
"lint": "bunx --bun @biomejs/biome@2.2.5 check src",
|
|
68
69
|
"fix": "bunx --bun @biomejs/biome@2.2.5 check --write src",
|
|
69
70
|
"typecheck": "tsc --noEmit",
|
|
@@ -128,7 +128,9 @@ function parseTimingLogs(stderr: string): {
|
|
|
128
128
|
/**
|
|
129
129
|
* Run a single benchmark scenario
|
|
130
130
|
*/
|
|
131
|
-
async function runBenchmark(
|
|
131
|
+
async function runBenchmark(
|
|
132
|
+
scenario: ScenarioConfig,
|
|
133
|
+
): Promise<BenchmarkResult> {
|
|
132
134
|
const start = performance.now();
|
|
133
135
|
|
|
134
136
|
return new Promise((resolve) => {
|
|
@@ -210,16 +212,16 @@ function printResults(results: BenchmarkResult[]): void {
|
|
|
210
212
|
// Print API calls summary
|
|
211
213
|
if (result.apiCalls.length > 0) {
|
|
212
214
|
console.log(" API Calls:");
|
|
213
|
-
const totalApiMs = result.apiCalls.reduce(
|
|
215
|
+
const totalApiMs = result.apiCalls.reduce(
|
|
216
|
+
(sum, c) => sum + c.durationMs,
|
|
217
|
+
0,
|
|
218
|
+
);
|
|
214
219
|
|
|
215
220
|
// Group by path pattern
|
|
216
221
|
const grouped: Record<string, { count: number; totalMs: number }> = {};
|
|
217
222
|
for (const call of result.apiCalls) {
|
|
218
223
|
// Normalize paths (remove UUIDs)
|
|
219
|
-
const normalizedPath = call.path.replace(
|
|
220
|
-
/[a-f0-9-]{36}/g,
|
|
221
|
-
"{id}",
|
|
222
|
-
);
|
|
224
|
+
const normalizedPath = call.path.replace(/[a-f0-9-]{36}/g, "{id}");
|
|
223
225
|
const key = `${call.method} ${normalizedPath}`;
|
|
224
226
|
if (!grouped[key]) {
|
|
225
227
|
grouped[key] = { count: 0, totalMs: 0 };
|
|
@@ -262,7 +264,10 @@ function printResults(results: BenchmarkResult[]): void {
|
|
|
262
264
|
console.log("-".repeat(70));
|
|
263
265
|
|
|
264
266
|
for (const result of results) {
|
|
265
|
-
const totalApiMs = result.apiCalls.reduce(
|
|
267
|
+
const totalApiMs = result.apiCalls.reduce(
|
|
268
|
+
(sum, c) => sum + c.durationMs,
|
|
269
|
+
0,
|
|
270
|
+
);
|
|
266
271
|
const cliOverhead = result.totalMs - totalApiMs;
|
|
267
272
|
console.log(
|
|
268
273
|
result.scenario.padEnd(20) +
|
|
@@ -301,7 +306,9 @@ async function main(): Promise<void> {
|
|
|
301
306
|
|
|
302
307
|
if (scenariosToRun.length === 0) {
|
|
303
308
|
console.error(`Error: Unknown scenario "${scenarioFilter}"`);
|
|
304
|
-
console.error(
|
|
309
|
+
console.error(
|
|
310
|
+
`Available scenarios: ${SCENARIOS.map((s) => s.name).join(", ")}`,
|
|
311
|
+
);
|
|
305
312
|
process.exit(1);
|
|
306
313
|
}
|
|
307
314
|
|
|
@@ -323,7 +330,9 @@ async function main(): Promise<void> {
|
|
|
323
330
|
allResults.push(result);
|
|
324
331
|
|
|
325
332
|
if (result.exitCode !== 0) {
|
|
326
|
-
console.warn(
|
|
333
|
+
console.warn(
|
|
334
|
+
` Warning: ${scenario.name} exited with code ${result.exitCode}`,
|
|
335
|
+
);
|
|
327
336
|
} else {
|
|
328
337
|
console.log(` Completed in ${formatMs(result.totalMs)}`);
|
|
329
338
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Context Doctor
|
|
3
|
+
id: context_doctor
|
|
4
|
+
description: Identify and repair degradation in system prompt, external memory, and skills preventing you from following instructions or remembering information as well as you should.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Context Doctor
|
|
8
|
+
Your context is managed by yourself, along with additional memory subagents. Your context includes:
|
|
9
|
+
- Your system prompt and instructions (contained in `system/`)
|
|
10
|
+
- Your external memory
|
|
11
|
+
- Your skills (procedural memory)
|
|
12
|
+
|
|
13
|
+
Over time, context can degrade — bloat and poor prompt quality erode your ability to remember the right things and follow instructions properly. This skill helps you identify issues with your context and repair them collaboratively with the user.
|
|
14
|
+
|
|
15
|
+
## Operating Procedure
|
|
16
|
+
|
|
17
|
+
### Step 1: Identifying and resolving context issues
|
|
18
|
+
Explore your memory files to identify issues. Consider what is confusing about your own prompts and context, and resolve the issues.
|
|
19
|
+
|
|
20
|
+
Below are additional common issues with context and how they can be resolved:
|
|
21
|
+
|
|
22
|
+
### Context quality
|
|
23
|
+
Your system prompt and memory filesystem should be well structured and clear.
|
|
24
|
+
|
|
25
|
+
**Questions to ask**:
|
|
26
|
+
- Is my system prompt clear and well formatted?
|
|
27
|
+
- Are there wasteful or unnecessary tokens in my prompts?
|
|
28
|
+
- Do I know when to load which files in my memory filesystem?
|
|
29
|
+
|
|
30
|
+
#### System prompt bloat
|
|
31
|
+
Prompts that are compiled as part of the system prompt (contained in `system/`) should only take up about 10% of the total context size, though this is a recommendation, not a hard requirement. Usually this means about 15-20k tokens.
|
|
32
|
+
|
|
33
|
+
Use the following script to evaluate the token usage of the system prompt:
|
|
34
|
+
```bash
|
|
35
|
+
bun scripts/estimate_system_tokens.ts --memory-dir "$MEMORY_DIR"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Questions to ask**:
|
|
39
|
+
- Do all these tokens need to be passed to the LLM on every turn, or can they be retrieved when needed through being part of external memory of my conversation history?
|
|
40
|
+
- Do any of these prompts confuse or distract me?
|
|
41
|
+
- Am I able to effectively follow critical instructions (e.g. persona information, user preferences) given the current prompt structure and contents?
|
|
42
|
+
|
|
43
|
+
**Solution**: Reduce the size of the system prompt if needed:
|
|
44
|
+
- Move files outside of `system/` so they are no longer part of the system prompt
|
|
45
|
+
- Compact information to be more information dense or eliminate redundancy
|
|
46
|
+
- Leverage progressive disclosure: move some context outside of `system/` and reference it to pull in dynamically
|
|
47
|
+
|
|
48
|
+
**Scope**: You may refine, tighten, and restructure prompts to improve clarity and adherence — but do not change the intended semantics. The goal is better signal, not different behavior.
|
|
49
|
+
- Do not alter persona-defining content (who you are, how you communicate)
|
|
50
|
+
- Do not remove or change user identity or preferences (e.g. the human's name, their stated goals)
|
|
51
|
+
- Do not rewrite instructions in ways that shift their meaning — only reduce noise and improve structure
|
|
52
|
+
|
|
53
|
+
#### Context redundancy and unclear organization
|
|
54
|
+
The context in the memory filesystem should have a clear structure, with a well-defined purpose for each file. Memory file descriptions should be precise and non-overlapping. Their contents should be consistent with the description, and have non-overlapping content to other files.
|
|
55
|
+
|
|
56
|
+
**Questions to ask**:
|
|
57
|
+
- Do the descriptions make clear what file is for what?
|
|
58
|
+
- Do the contents of the file match the descriptions? (you can ask subagents to check)
|
|
59
|
+
|
|
60
|
+
**Solution**: Read all memory files (use subagents for efficiency), then:
|
|
61
|
+
- Consolidate redundant files
|
|
62
|
+
- Reorganize files and rewrite descriptions to have clear separation of concerns
|
|
63
|
+
- Avoid duplication by referencing common files from multiple places (e.g. `[[reference/api]]`)
|
|
64
|
+
- Rewrite unclear or low-quality content
|
|
65
|
+
|
|
66
|
+
#### Invalid context format
|
|
67
|
+
Files in the memory filesystem must follow certain structural requirements:
|
|
68
|
+
- Must have a `system/persona.md`
|
|
69
|
+
- Must NOT have overlapping file and folder names (e.g. `system/human.md` and `system/human/identity.md`)
|
|
70
|
+
- Must follow specification for skills (e.g. `skills/{skill_name}/`) with the format:
|
|
71
|
+
```
|
|
72
|
+
skill-name/
|
|
73
|
+
├── SKILL.md # Required: metadata + instructions
|
|
74
|
+
├── scripts/ # Optional: executable code
|
|
75
|
+
├── references/ # Optional: documentation
|
|
76
|
+
├── assets/ # Optional: templates, resources
|
|
77
|
+
└── ... # Any additional files or directories
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Solution**: Reorganize files to follow the required structure
|
|
81
|
+
|
|
82
|
+
### Poor use of progressive disclosure
|
|
83
|
+
Only critical information should be in the system prompt, since it's passed on every turn. Use progressive disclosure so that context only *sometimes* needed can be dynamically retrieved.
|
|
84
|
+
|
|
85
|
+
Files that are outside of `system/` are not part of the system prompt, and must be dynamically loaded. You must index your files to ensure your future self can discover them: for example, make sure that files have informative names and descriptions, or are referenced from parts of your system prompt. Otherwise, you will never discover the external context or make use of it.
|
|
86
|
+
|
|
87
|
+
**Solution**:
|
|
88
|
+
- Reference external skills from the relevant parts of in-context memory:
|
|
89
|
+
```
|
|
90
|
+
When running a migration, always use the skill [[skills/db-migrations]]
|
|
91
|
+
```
|
|
92
|
+
or external memory files:
|
|
93
|
+
```
|
|
94
|
+
Sarah's active projects are: Letta Code [[projects/letta_code.md]] and Letta Cloud [[projects/letta_cloud]]
|
|
95
|
+
```
|
|
96
|
+
- Ensure that contents of files match the file name and descriptions
|
|
97
|
+
- Make sure your future self will be able to find and load external files when needed.
|
|
98
|
+
|
|
99
|
+
### Step 2: Implement context fixes
|
|
100
|
+
Create a plan for what fixes you want to make, then implement them.
|
|
101
|
+
|
|
102
|
+
Before moving on, verify:
|
|
103
|
+
- [ ] System prompt token budget reviewed (target ~10% of context, usually 15-20k tokens)
|
|
104
|
+
- [ ] No overlapping or redundant files remain
|
|
105
|
+
- [ ] All file descriptions are unique, accurate, and match their contents
|
|
106
|
+
- [ ] Moved-out knowledge has references from in-context memory so it can be discovered
|
|
107
|
+
- [ ] No semantic changes to persona, user identity, or behavioral instructions
|
|
108
|
+
|
|
109
|
+
### Step 3: Commit and push
|
|
110
|
+
Review changes, then commit with a descriptive message:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
cd $MEMORY_DIR
|
|
114
|
+
git status # Review what changed before staging
|
|
115
|
+
git add <specific files> # Stage targeted paths — avoid blind `git add -A`
|
|
116
|
+
git commit --author="<AGENT_NAME> <<ACTUAL_AGENT_ID>@letta.com>" -m "fix(doctor): <summary> 🏥
|
|
117
|
+
|
|
118
|
+
<identified issues and implemented solutions>"
|
|
119
|
+
|
|
120
|
+
git push
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Step 4: Final checklist and message
|
|
124
|
+
Tell the user what issues you identitied, the fixes you made, the commit you made, and also recommend that they run `/recompile` to apply these changes to the current system prompt.
|
|
125
|
+
|
|
126
|
+
Before finishing make sure you:
|
|
127
|
+
- [ ] Resolved all the identified context issues
|
|
128
|
+
- [ ] Pushed your changes successfully
|
|
129
|
+
- [ ] Told the user to run `/recompile` to refresh the system prompt and apply changes
|
|
130
|
+
|
|
131
|
+
## Critical information
|
|
132
|
+
- **Ask the user about their goals for you, not the implementation**: You understand your own context best, and should follow the guidelines in this document. Do NOT ask the user about their structural preferences - the context is for YOU, not them. Ask them how they want YOU to behave or know instead.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { getClient } from "../../../../agent/client";
|
|
6
|
+
import { settingsManager } from "../../../../settings-manager";
|
|
7
|
+
|
|
8
|
+
const BYTES_PER_TOKEN = 4;
|
|
9
|
+
|
|
10
|
+
type FileEstimate = {
|
|
11
|
+
path: string;
|
|
12
|
+
tokens: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type ParsedArgs = {
|
|
16
|
+
memoryDir?: string;
|
|
17
|
+
agentId?: string;
|
|
18
|
+
top: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function parseArgs(argv: string[]): ParsedArgs {
|
|
22
|
+
const parsed: ParsedArgs = { top: 20 };
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const arg = argv[i];
|
|
26
|
+
if (arg === "--memory-dir") {
|
|
27
|
+
parsed.memoryDir = argv[i + 1];
|
|
28
|
+
i++;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (arg === "--agent-id") {
|
|
32
|
+
parsed.agentId = argv[i + 1];
|
|
33
|
+
i++;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (arg === "--top") {
|
|
37
|
+
const raw = argv[i + 1];
|
|
38
|
+
const value = Number.parseInt(raw ?? "", 10);
|
|
39
|
+
if (!Number.isNaN(value) && value >= 0) {
|
|
40
|
+
parsed.top = value;
|
|
41
|
+
}
|
|
42
|
+
i++;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function estimateTokens(text: string): number {
|
|
50
|
+
return Math.ceil(Buffer.byteLength(text, "utf8") / BYTES_PER_TOKEN);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizePath(value: string): string {
|
|
54
|
+
return value.replaceAll("\\", "/");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function walkMarkdownFiles(dir: string): string[] {
|
|
58
|
+
if (!existsSync(dir)) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const out: string[] = [];
|
|
63
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
64
|
+
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (entry.name.startsWith(".")) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const full = join(dir, entry.name);
|
|
70
|
+
if (entry.isDirectory()) {
|
|
71
|
+
if (entry.name === ".git") {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
out.push(...walkMarkdownFiles(full));
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
78
|
+
out.push(full);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function inferAgentIdFromMemoryDir(memoryDir: string): string | null {
|
|
86
|
+
const parts = normalizePath(memoryDir).split("/");
|
|
87
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
88
|
+
if (parts[i] === "agents" && parts[i + 1]?.startsWith("agent-")) {
|
|
89
|
+
return parts[i + 1];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const maybe = parts.at(-2);
|
|
94
|
+
return maybe?.startsWith("agent-") ? maybe : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function resolveAgentId(
|
|
98
|
+
memoryDir: string,
|
|
99
|
+
cliAgentId?: string,
|
|
100
|
+
): Promise<string> {
|
|
101
|
+
if (cliAgentId) {
|
|
102
|
+
return cliAgentId;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (process.env.AGENT_ID) {
|
|
106
|
+
return process.env.AGENT_ID;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const inferred = inferAgentIdFromMemoryDir(memoryDir);
|
|
110
|
+
if (inferred) {
|
|
111
|
+
return inferred;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const fromSession = settingsManager.getEffectiveLastAgentId(process.cwd());
|
|
115
|
+
if (fromSession) {
|
|
116
|
+
return fromSession;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
throw new Error(
|
|
120
|
+
"Unable to resolve agent ID. Pass --agent-id or set AGENT_ID.",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function formatNumber(value: number): string {
|
|
125
|
+
return value.toLocaleString("en-US");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function main(): Promise<number> {
|
|
129
|
+
await settingsManager.initialize();
|
|
130
|
+
|
|
131
|
+
const args = parseArgs(process.argv.slice(2));
|
|
132
|
+
const memoryDir = args.memoryDir || process.env.MEMORY_DIR;
|
|
133
|
+
|
|
134
|
+
if (!memoryDir) {
|
|
135
|
+
throw new Error("Missing memory dir. Pass --memory-dir or set MEMORY_DIR.");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const systemDir = join(memoryDir, "system");
|
|
139
|
+
if (!existsSync(systemDir)) {
|
|
140
|
+
throw new Error(`Missing system directory: ${systemDir}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const agentId = await resolveAgentId(memoryDir, args.agentId);
|
|
144
|
+
|
|
145
|
+
// Use the SDK auth path used by letta-code (OAuth + API key handling via getClient).
|
|
146
|
+
const client = await getClient();
|
|
147
|
+
await client.agents.retrieve(agentId);
|
|
148
|
+
|
|
149
|
+
const files = walkMarkdownFiles(systemDir).sort();
|
|
150
|
+
const rows: FileEstimate[] = [];
|
|
151
|
+
|
|
152
|
+
for (const filePath of files) {
|
|
153
|
+
const text = readFileSync(filePath, "utf8");
|
|
154
|
+
const rel = normalizePath(filePath.slice(memoryDir.length + 1));
|
|
155
|
+
rows.push({ path: rel, tokens: estimateTokens(text) });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const estimatedTotalTokens = rows.reduce((sum, row) => sum + row.tokens, 0);
|
|
159
|
+
|
|
160
|
+
console.log("Estimated total tokens");
|
|
161
|
+
console.log(` ${formatNumber(estimatedTotalTokens)}`);
|
|
162
|
+
|
|
163
|
+
console.log("\nPer-file token estimates");
|
|
164
|
+
console.log(` ${"tokens".padStart(8)} path`);
|
|
165
|
+
|
|
166
|
+
const sortedRows = [...rows].sort((a, b) => b.tokens - a.tokens);
|
|
167
|
+
for (const row of sortedRows.slice(0, Math.max(0, args.top))) {
|
|
168
|
+
console.log(` ${formatNumber(row.tokens).padStart(8)} ${row.path}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
main()
|
|
175
|
+
.then((code) => {
|
|
176
|
+
process.exit(code);
|
|
177
|
+
})
|
|
178
|
+
.catch((error: unknown) => {
|
|
179
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
180
|
+
process.exit(1);
|
|
181
|
+
});
|