@letta-ai/letta-code 0.13.0 → 0.13.2
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 +1 -1
- package/dist/types/protocol.d.ts +13 -1
- package/dist/types/protocol.d.ts.map +1 -1
- package/letta.js +1360 -569
- package/package.json +1 -1
- package/scripts/latency-benchmark.ts +341 -0
- package/skills/working-in-parallel/SKILL.md +57 -0
package/package.json
CHANGED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Latency Benchmark Script for Letta Code CLI
|
|
4
|
+
*
|
|
5
|
+
* Runs headless mode with LETTA_DEBUG_TIMINGS=1 and parses the output
|
|
6
|
+
* to measure latency breakdown at different stages.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* bun scripts/latency-benchmark.ts
|
|
10
|
+
* bun scripts/latency-benchmark.ts --scenario fresh-agent
|
|
11
|
+
* bun scripts/latency-benchmark.ts --iterations 5
|
|
12
|
+
*
|
|
13
|
+
* Requires: LETTA_API_KEY environment variable
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
|
|
18
|
+
interface ApiCall {
|
|
19
|
+
method: string;
|
|
20
|
+
path: string;
|
|
21
|
+
durationMs: number;
|
|
22
|
+
status?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface Milestone {
|
|
26
|
+
name: string;
|
|
27
|
+
offsetMs: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface BenchmarkResult {
|
|
31
|
+
scenario: string;
|
|
32
|
+
totalMs: number;
|
|
33
|
+
milestones: Milestone[];
|
|
34
|
+
apiCalls: ApiCall[];
|
|
35
|
+
exitCode: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ScenarioConfig {
|
|
39
|
+
name: string;
|
|
40
|
+
description: string;
|
|
41
|
+
args: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Define benchmark scenarios
|
|
45
|
+
const SCENARIOS: ScenarioConfig[] = [
|
|
46
|
+
{
|
|
47
|
+
name: "fresh-agent",
|
|
48
|
+
description: "Create new agent and send simple prompt",
|
|
49
|
+
args: [
|
|
50
|
+
"-p",
|
|
51
|
+
"What is 2+2? Reply with just the number.",
|
|
52
|
+
"--new-agent",
|
|
53
|
+
"--yolo",
|
|
54
|
+
"--output-format",
|
|
55
|
+
"json",
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "resume-agent",
|
|
60
|
+
description: "Resume last agent and send simple prompt",
|
|
61
|
+
args: [
|
|
62
|
+
"-p",
|
|
63
|
+
"What is 3+3? Reply with just the number.",
|
|
64
|
+
"--continue",
|
|
65
|
+
"--yolo",
|
|
66
|
+
"--output-format",
|
|
67
|
+
"json",
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "minimal-math",
|
|
72
|
+
description: "Simple math question (no tool calls)",
|
|
73
|
+
args: [
|
|
74
|
+
"-p",
|
|
75
|
+
"What is 5+5? Reply with just the number.",
|
|
76
|
+
"--continue",
|
|
77
|
+
"--yolo",
|
|
78
|
+
"--output-format",
|
|
79
|
+
"json",
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parse timing logs from stderr output
|
|
86
|
+
*/
|
|
87
|
+
function parseTimingLogs(stderr: string): {
|
|
88
|
+
milestones: Milestone[];
|
|
89
|
+
apiCalls: ApiCall[];
|
|
90
|
+
} {
|
|
91
|
+
const milestones: Milestone[] = [];
|
|
92
|
+
const apiCalls: ApiCall[] = [];
|
|
93
|
+
|
|
94
|
+
const lines = stderr.split("\n");
|
|
95
|
+
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
// Parse milestones: [timing] MILESTONE CLI_START at +0ms (12:34:56.789)
|
|
98
|
+
const milestoneMatch = line.match(
|
|
99
|
+
/\[timing\] MILESTONE (\S+) at \+(\d+(?:\.\d+)?)(ms|s)/,
|
|
100
|
+
);
|
|
101
|
+
if (milestoneMatch) {
|
|
102
|
+
const name = milestoneMatch[1]!;
|
|
103
|
+
let offsetMs = parseFloat(milestoneMatch[2]!);
|
|
104
|
+
if (milestoneMatch[3] === "s") {
|
|
105
|
+
offsetMs *= 1000;
|
|
106
|
+
}
|
|
107
|
+
milestones.push({ name, offsetMs });
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Parse API calls: [timing] GET /v1/agents/... -> 245ms (status: 200)
|
|
112
|
+
const apiMatch = line.match(
|
|
113
|
+
/\[timing\] (GET|POST|PUT|DELETE|PATCH) (\S+) -> (\d+(?:\.\d+)?)(ms|s)(?: \(status: (\d+)\))?/,
|
|
114
|
+
);
|
|
115
|
+
if (apiMatch) {
|
|
116
|
+
const method = apiMatch[1]!;
|
|
117
|
+
const path = apiMatch[2]!;
|
|
118
|
+
let durationMs = parseFloat(apiMatch[3]!);
|
|
119
|
+
if (apiMatch[4] === "s") {
|
|
120
|
+
durationMs *= 1000;
|
|
121
|
+
}
|
|
122
|
+
const status = apiMatch[5] ? parseInt(apiMatch[5], 10) : undefined;
|
|
123
|
+
apiCalls.push({ method, path, durationMs, status });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { milestones, apiCalls };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Run a single benchmark scenario
|
|
132
|
+
*/
|
|
133
|
+
async function runBenchmark(scenario: ScenarioConfig): Promise<BenchmarkResult> {
|
|
134
|
+
const start = performance.now();
|
|
135
|
+
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
const proc = spawn("bun", ["run", "dev", ...scenario.args], {
|
|
138
|
+
env: { ...process.env, LETTA_DEBUG_TIMINGS: "1" },
|
|
139
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
let stdout = "";
|
|
143
|
+
let stderr = "";
|
|
144
|
+
|
|
145
|
+
proc.stdout.on("data", (data) => {
|
|
146
|
+
stdout += data.toString();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
proc.stderr.on("data", (data) => {
|
|
150
|
+
stderr += data.toString();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
proc.on("close", (code) => {
|
|
154
|
+
const totalMs = performance.now() - start;
|
|
155
|
+
const { milestones, apiCalls } = parseTimingLogs(stderr);
|
|
156
|
+
|
|
157
|
+
resolve({
|
|
158
|
+
scenario: scenario.name,
|
|
159
|
+
totalMs,
|
|
160
|
+
milestones,
|
|
161
|
+
apiCalls,
|
|
162
|
+
exitCode: code ?? 1,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Timeout after 2 minutes
|
|
167
|
+
setTimeout(() => {
|
|
168
|
+
proc.kill("SIGTERM");
|
|
169
|
+
}, 120000);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Format duration for display
|
|
175
|
+
*/
|
|
176
|
+
function formatMs(ms: number): string {
|
|
177
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
178
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Print benchmark results
|
|
183
|
+
*/
|
|
184
|
+
function printResults(results: BenchmarkResult[]): void {
|
|
185
|
+
console.log("\n" + "=".repeat(70));
|
|
186
|
+
console.log("LATENCY BENCHMARK RESULTS");
|
|
187
|
+
console.log("=".repeat(70) + "\n");
|
|
188
|
+
|
|
189
|
+
for (const result of results) {
|
|
190
|
+
const scenario = SCENARIOS.find((s) => s.name === result.scenario);
|
|
191
|
+
console.log(`Scenario: ${result.scenario}`);
|
|
192
|
+
console.log(` ${scenario?.description || ""}`);
|
|
193
|
+
console.log(` Exit code: ${result.exitCode}`);
|
|
194
|
+
console.log(` Total wall time: ${formatMs(result.totalMs)}`);
|
|
195
|
+
console.log("");
|
|
196
|
+
|
|
197
|
+
// Print milestones
|
|
198
|
+
if (result.milestones.length > 0) {
|
|
199
|
+
console.log(" Milestones:");
|
|
200
|
+
let prevMs = 0;
|
|
201
|
+
for (const milestone of result.milestones) {
|
|
202
|
+
const delta = milestone.offsetMs - prevMs;
|
|
203
|
+
const deltaStr = prevMs === 0 ? "" : ` (+${formatMs(delta)})`;
|
|
204
|
+
console.log(
|
|
205
|
+
` +${formatMs(milestone.offsetMs).padStart(8)} ${milestone.name}${deltaStr}`,
|
|
206
|
+
);
|
|
207
|
+
prevMs = milestone.offsetMs;
|
|
208
|
+
}
|
|
209
|
+
console.log("");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Print API calls summary
|
|
213
|
+
if (result.apiCalls.length > 0) {
|
|
214
|
+
console.log(" API Calls:");
|
|
215
|
+
const totalApiMs = result.apiCalls.reduce((sum, c) => sum + c.durationMs, 0);
|
|
216
|
+
|
|
217
|
+
// Group by path pattern
|
|
218
|
+
const grouped: Record<string, { count: number; totalMs: number }> = {};
|
|
219
|
+
for (const call of result.apiCalls) {
|
|
220
|
+
// Normalize paths (remove UUIDs)
|
|
221
|
+
const normalizedPath = call.path.replace(
|
|
222
|
+
/[a-f0-9-]{36}/g,
|
|
223
|
+
"{id}",
|
|
224
|
+
);
|
|
225
|
+
const key = `${call.method} ${normalizedPath}`;
|
|
226
|
+
if (!grouped[key]) {
|
|
227
|
+
grouped[key] = { count: 0, totalMs: 0 };
|
|
228
|
+
}
|
|
229
|
+
grouped[key].count++;
|
|
230
|
+
grouped[key].totalMs += call.durationMs;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Sort by total time
|
|
234
|
+
const sorted = Object.entries(grouped).sort(
|
|
235
|
+
(a, b) => b[1].totalMs - a[1].totalMs,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
for (const [endpoint, stats] of sorted) {
|
|
239
|
+
const countStr = stats.count > 1 ? ` (x${stats.count})` : "";
|
|
240
|
+
console.log(
|
|
241
|
+
` ${formatMs(stats.totalMs).padStart(8)} ${endpoint}${countStr}`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
console.log(` ${"─".repeat(50)}`);
|
|
246
|
+
console.log(` ${formatMs(totalApiMs).padStart(8)} Total API time`);
|
|
247
|
+
console.log(
|
|
248
|
+
` ${formatMs(result.totalMs - totalApiMs).padStart(8)} CLI overhead (non-API)`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
console.log("\n" + "-".repeat(70) + "\n");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Summary table
|
|
256
|
+
console.log("SUMMARY");
|
|
257
|
+
console.log("-".repeat(70));
|
|
258
|
+
console.log(
|
|
259
|
+
"Scenario".padEnd(20) +
|
|
260
|
+
"Total".padStart(12) +
|
|
261
|
+
"API Time".padStart(12) +
|
|
262
|
+
"CLI Overhead".padStart(14),
|
|
263
|
+
);
|
|
264
|
+
console.log("-".repeat(70));
|
|
265
|
+
|
|
266
|
+
for (const result of results) {
|
|
267
|
+
const totalApiMs = result.apiCalls.reduce((sum, c) => sum + c.durationMs, 0);
|
|
268
|
+
const cliOverhead = result.totalMs - totalApiMs;
|
|
269
|
+
console.log(
|
|
270
|
+
result.scenario.padEnd(20) +
|
|
271
|
+
formatMs(result.totalMs).padStart(12) +
|
|
272
|
+
formatMs(totalApiMs).padStart(12) +
|
|
273
|
+
formatMs(cliOverhead).padStart(14),
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
console.log("-".repeat(70));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function main(): Promise<void> {
|
|
280
|
+
// Parse args
|
|
281
|
+
const args = process.argv.slice(2);
|
|
282
|
+
let scenarioFilter: string | null = null;
|
|
283
|
+
let iterations = 1;
|
|
284
|
+
|
|
285
|
+
for (let i = 0; i < args.length; i++) {
|
|
286
|
+
if (args[i] === "--scenario" && args[i + 1]) {
|
|
287
|
+
scenarioFilter = args[++i]!;
|
|
288
|
+
} else if (args[i] === "--iterations" && args[i + 1]) {
|
|
289
|
+
iterations = parseInt(args[++i]!, 10);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Check prereqs
|
|
294
|
+
if (!process.env.LETTA_API_KEY) {
|
|
295
|
+
console.error("Error: LETTA_API_KEY environment variable is required");
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Filter scenarios
|
|
300
|
+
const scenariosToRun = scenarioFilter
|
|
301
|
+
? SCENARIOS.filter((s) => s.name === scenarioFilter)
|
|
302
|
+
: SCENARIOS;
|
|
303
|
+
|
|
304
|
+
if (scenariosToRun.length === 0) {
|
|
305
|
+
console.error(`Error: Unknown scenario "${scenarioFilter}"`);
|
|
306
|
+
console.error(`Available scenarios: ${SCENARIOS.map((s) => s.name).join(", ")}`);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
console.log("Running latency benchmarks...");
|
|
311
|
+
console.log(`Scenarios: ${scenariosToRun.map((s) => s.name).join(", ")}`);
|
|
312
|
+
console.log(`Iterations: ${iterations}`);
|
|
313
|
+
console.log("");
|
|
314
|
+
|
|
315
|
+
const allResults: BenchmarkResult[] = [];
|
|
316
|
+
|
|
317
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
318
|
+
if (iterations > 1) {
|
|
319
|
+
console.log(`\n--- Iteration ${iter + 1} of ${iterations} ---`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (const scenario of scenariosToRun) {
|
|
323
|
+
console.log(`Running: ${scenario.name}...`);
|
|
324
|
+
const result = await runBenchmark(scenario);
|
|
325
|
+
allResults.push(result);
|
|
326
|
+
|
|
327
|
+
if (result.exitCode !== 0) {
|
|
328
|
+
console.warn(` Warning: ${scenario.name} exited with code ${result.exitCode}`);
|
|
329
|
+
} else {
|
|
330
|
+
console.log(` Completed in ${formatMs(result.totalMs)}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
printResults(allResults);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
main().catch((err) => {
|
|
339
|
+
console.error(err);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: working-in-parallel
|
|
3
|
+
description: Guide for working in parallel with other agents. Use when another agent is already working in the same directory, or when you need to work on multiple features simultaneously. Covers git worktrees as the recommended approach.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Working in Parallel
|
|
7
|
+
|
|
8
|
+
Use **git worktrees** to work in parallel when another agent is in the same directory.
|
|
9
|
+
|
|
10
|
+
Git worktrees let you check out multiple branches into separate directories. Each worktree has its own isolated files while sharing the same Git history and remote connections. Changes in one worktree won't affect others, so parallel agents can't interfere with each other.
|
|
11
|
+
|
|
12
|
+
Learn more: [Git worktree documentation](https://git-scm.com/docs/git-worktree)
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Create worktree with new branch (from main repo)
|
|
18
|
+
git worktree add -b fix/my-feature ../repo-my-feature main
|
|
19
|
+
|
|
20
|
+
# Work in the worktree
|
|
21
|
+
cd ../repo-my-feature
|
|
22
|
+
bun install # or npm install, pip install, etc.
|
|
23
|
+
|
|
24
|
+
# Make changes, commit, push, PR
|
|
25
|
+
git add <files>
|
|
26
|
+
git commit -m "fix: description"
|
|
27
|
+
git push -u origin fix/my-feature
|
|
28
|
+
gh pr create --title "Fix: description" --body "## Summary..."
|
|
29
|
+
|
|
30
|
+
# Clean up when done (from main repo)
|
|
31
|
+
git worktree remove ../repo-my-feature
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Key Commands
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
git worktree add -b <branch> <path> main # Create with new branch
|
|
38
|
+
git worktree add <path> <existing-branch> # Use existing branch
|
|
39
|
+
git worktree list # Show all worktrees
|
|
40
|
+
git worktree remove <path> # Remove worktree
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## When to Use
|
|
44
|
+
|
|
45
|
+
- Another agent is working in the current directory
|
|
46
|
+
- Long-running task in one session, quick fix needed in another
|
|
47
|
+
- User wants to continue development while an agent works on a separate feature
|
|
48
|
+
|
|
49
|
+
## Tips
|
|
50
|
+
|
|
51
|
+
- Name directories clearly: `../repo-feature-auth`, `../repo-bugfix-123`
|
|
52
|
+
- Install dependencies in new worktrees (`npm install`, `bun install`, `pip install`, etc.)
|
|
53
|
+
- Push changes before removing worktrees
|
|
54
|
+
|
|
55
|
+
## Alternative: Repo Clones
|
|
56
|
+
|
|
57
|
+
Some users prefer cloning the repo multiple times (`gh repo clone owner/repo project-01`) for simpler mental model. This uses more disk space but provides complete isolation. If the user expresses confusion about worktrees or explicitly prefers clones, use that approach instead.
|