@letta-ai/letta-code 0.27.18 → 0.27.19
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/dist/app-server-client.js +11 -1
- package/dist/app-server-client.js.map +3 -3
- package/dist/types/app-server-client.d.ts +4 -1
- package/dist/types/app-server-client.d.ts.map +1 -1
- package/dist/types/types/protocol_v2.d.ts +1 -1
- package/dist/types/types/protocol_v2.d.ts.map +1 -1
- package/docs/examples/mods/README.md +60 -0
- package/docs/examples/mods/learning/memory-citations.env.json +165 -0
- package/docs/examples/mods/learning/uv-pip-install.env.json +199 -0
- package/docs/examples/mods/memory-citations.ts +347 -0
- package/letta.js +4857 -1703
- package/package.json +4 -2
- package/scripts/mod-learning/learn-mod.ts +306 -0
- package/skills/{context_doctor → context-doctor}/SKILL.md +1 -1
- package/skills/creating-mods/SKILL.md +4 -1
- package/skills/creating-mods/references/events.md +17 -3
- package/skills/creating-mods/references/ui.md +23 -0
- package/skills/generating-mod-envs/SKILL.md +130 -0
- package/skills/generating-mod-envs/assets/mod-learning-env.template.json +57 -0
- package/skills/generating-mod-envs/scripts/validate-mod-env.ts +261 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@letta-ai/letta-code",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.19",
|
|
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",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"@earendil-works/pi-ai": "^0.79.6",
|
|
48
48
|
"@letta-ai/letta-client": "^1.10.2",
|
|
49
49
|
"@pierre/diffs": "1.2.2",
|
|
50
|
+
"@scarf/scarf": "^1.4.0",
|
|
50
51
|
"glob": "^13.0.0",
|
|
51
52
|
"ink-link": "^5.0.0",
|
|
52
53
|
"node-pty": "^1.1.0",
|
|
@@ -97,7 +98,8 @@
|
|
|
97
98
|
"test:update-chain:manual": "bun run src/test-utils/update-chain-smoke.ts --mode manual",
|
|
98
99
|
"test:update-chain:startup": "bun run src/test-utils/update-chain-smoke.ts --mode startup",
|
|
99
100
|
"prepublishOnly": "bun run build",
|
|
100
|
-
"postinstall": "node scripts/postinstall-patches.js || echo letta: vendor patches skipped && node -e \"try{require('fs').chmodSync(require('path').join(require.resolve('node-pty/package.json'),'../prebuilds/darwin-arm64/spawn-helper'),0o755)}catch(e){}\" || true"
|
|
101
|
+
"postinstall": "node scripts/postinstall-patches.js || echo letta: vendor patches skipped && node -e \"try{require('fs').chmodSync(require('path').join(require.resolve('node-pty/package.json'),'../prebuilds/darwin-arm64/spawn-helper'),0o755)}catch(e){}\" || true",
|
|
102
|
+
"mod-learning:memory-citations": "bun scripts/mod-learning/learn-mod.ts --env docs/examples/mods/learning/memory-citations.env.json"
|
|
101
103
|
},
|
|
102
104
|
"lint-staged": {
|
|
103
105
|
"*.{ts,tsx,js,jsx,json}": [
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { closeSync, openSync } from "node:fs";
|
|
4
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import {
|
|
8
|
+
defaultModLearningRunDirectory,
|
|
9
|
+
type ModLearningProgress,
|
|
10
|
+
type ModLearningReport,
|
|
11
|
+
readModLearningEnv,
|
|
12
|
+
runModLearning,
|
|
13
|
+
} from "../../src/mods/learning-harness.ts";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_OPTIMIZATION_STEPS = 5;
|
|
16
|
+
|
|
17
|
+
interface Args {
|
|
18
|
+
backend?: string;
|
|
19
|
+
candidate?: string;
|
|
20
|
+
candidateCount?: number;
|
|
21
|
+
candidateFileName?: string;
|
|
22
|
+
evalModel?: string;
|
|
23
|
+
generationModel?: string;
|
|
24
|
+
help: boolean;
|
|
25
|
+
env: string;
|
|
26
|
+
foreground: boolean;
|
|
27
|
+
out?: string;
|
|
28
|
+
promoteTo?: string;
|
|
29
|
+
repoRoot: string;
|
|
30
|
+
scenarioLimit?: number;
|
|
31
|
+
skipGeneration: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readOptionValue(
|
|
35
|
+
argv: string[],
|
|
36
|
+
index: number,
|
|
37
|
+
optionName: string,
|
|
38
|
+
): { nextIndex: number; value: string } {
|
|
39
|
+
const arg = argv[index] ?? "";
|
|
40
|
+
const equalsPrefix = `${optionName}=`;
|
|
41
|
+
if (arg.startsWith(equalsPrefix)) {
|
|
42
|
+
const value = arg.slice(equalsPrefix.length);
|
|
43
|
+
if (!value) throw new Error(`${optionName} requires a value`);
|
|
44
|
+
return { nextIndex: index, value };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const value = argv[index + 1];
|
|
48
|
+
if (!value || value.startsWith("--")) {
|
|
49
|
+
throw new Error(`${optionName} requires a value`);
|
|
50
|
+
}
|
|
51
|
+
return { nextIndex: index + 1, value };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseArgs(argv: string[]): Args {
|
|
55
|
+
const args: Args = {
|
|
56
|
+
env: "docs/examples/mods/learning/memory-citations.env.json",
|
|
57
|
+
foreground: false,
|
|
58
|
+
help: false,
|
|
59
|
+
repoRoot: process.cwd(),
|
|
60
|
+
skipGeneration: false,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
64
|
+
const arg = argv[index];
|
|
65
|
+
if (arg === "--help" || arg === "-h") {
|
|
66
|
+
args.help = true;
|
|
67
|
+
} else if (arg === "--background") {
|
|
68
|
+
args.foreground = false;
|
|
69
|
+
} else if (arg === "--foreground") {
|
|
70
|
+
args.foreground = true;
|
|
71
|
+
} else if (arg === "--skip-generation") {
|
|
72
|
+
args.skipGeneration = true;
|
|
73
|
+
} else if (arg?.startsWith("--")) {
|
|
74
|
+
const optionName = arg.includes("=")
|
|
75
|
+
? arg.slice(0, arg.indexOf("="))
|
|
76
|
+
: arg;
|
|
77
|
+
const { nextIndex, value } = readOptionValue(argv, index, optionName);
|
|
78
|
+
index = nextIndex;
|
|
79
|
+
switch (optionName) {
|
|
80
|
+
case "--backend":
|
|
81
|
+
args.backend = value;
|
|
82
|
+
break;
|
|
83
|
+
case "--candidate":
|
|
84
|
+
args.candidate = value;
|
|
85
|
+
break;
|
|
86
|
+
case "--candidates":
|
|
87
|
+
args.candidateCount = Number(value);
|
|
88
|
+
break;
|
|
89
|
+
case "--candidate-file-name":
|
|
90
|
+
args.candidateFileName = value;
|
|
91
|
+
break;
|
|
92
|
+
case "--eval-model":
|
|
93
|
+
args.evalModel = value;
|
|
94
|
+
break;
|
|
95
|
+
case "--env":
|
|
96
|
+
args.env = value;
|
|
97
|
+
break;
|
|
98
|
+
case "--generation-model":
|
|
99
|
+
args.generationModel = value;
|
|
100
|
+
break;
|
|
101
|
+
case "--model":
|
|
102
|
+
args.generationModel = value;
|
|
103
|
+
args.evalModel = value;
|
|
104
|
+
break;
|
|
105
|
+
case "--out":
|
|
106
|
+
args.out = value;
|
|
107
|
+
break;
|
|
108
|
+
case "--promote-to":
|
|
109
|
+
args.promoteTo = value;
|
|
110
|
+
break;
|
|
111
|
+
case "--repo-root":
|
|
112
|
+
args.repoRoot = value;
|
|
113
|
+
break;
|
|
114
|
+
case "--scenario-limit":
|
|
115
|
+
args.scenarioLimit = Number(value);
|
|
116
|
+
break;
|
|
117
|
+
default:
|
|
118
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return args;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function printHelp(): void {
|
|
129
|
+
console.log(`Usage: bun scripts/mod-learning/learn-mod.ts [options]
|
|
130
|
+
|
|
131
|
+
Runs the mod learning dogfood loop:
|
|
132
|
+
env/demo -> generate candidate mod -> headless eval with LETTA_MODS_DIR (and legacy LETTA_EXTENSIONS_DIR for pre-rename branches) -> artifacts/report
|
|
133
|
+
|
|
134
|
+
Options:
|
|
135
|
+
--env <path> Learning env JSON (default: memory-citations env)
|
|
136
|
+
--out <dir> Run artifact directory (default: .letta/mod-learning-runs/<slug>-<timestamp>)
|
|
137
|
+
--candidate <path> Use an existing candidate mod instead of generation
|
|
138
|
+
--candidates <n> Run N optimization iterations for one learned mod (default: 5 for generated runs)
|
|
139
|
+
--candidate-file-name <name> Candidate filename inside the eval mod directory
|
|
140
|
+
--model <handle> Model for generation and eval
|
|
141
|
+
--generation-model <handle> Model for candidate generation
|
|
142
|
+
--eval-model <handle> Model for headless eval
|
|
143
|
+
--backend <mode> Backend flag forwarded to letta (api or local)
|
|
144
|
+
--scenario-limit <n> Evaluate only the first N scenarios (fast smoke testing)
|
|
145
|
+
--repo-root <path> Repo root (default: cwd)
|
|
146
|
+
--foreground Run learning in this process and return a pass/fail exit code
|
|
147
|
+
--background Explicitly use the default detached mode
|
|
148
|
+
--skip-generation Expect the candidate file to already exist in the run dir
|
|
149
|
+
--promote-to <path> Copy passing candidate to this repo-relative path
|
|
150
|
+
-h, --help Show this help
|
|
151
|
+
`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function launchBackground(params: {
|
|
155
|
+
argv: string[];
|
|
156
|
+
repoRoot: string;
|
|
157
|
+
runDir: string;
|
|
158
|
+
outWasProvided: boolean;
|
|
159
|
+
}): Promise<void> {
|
|
160
|
+
await mkdir(params.runDir, { recursive: true });
|
|
161
|
+
const stdoutPath = path.join(params.runDir, "background.stdout");
|
|
162
|
+
const stderrPath = path.join(params.runDir, "background.stderr");
|
|
163
|
+
const metadataPath = path.join(params.runDir, "background.json");
|
|
164
|
+
const childArgv = params.argv.filter(
|
|
165
|
+
(arg) => arg !== "--background" && arg !== "--foreground",
|
|
166
|
+
);
|
|
167
|
+
childArgv.push("--foreground");
|
|
168
|
+
if (!params.outWasProvided) childArgv.push("--out", params.runDir);
|
|
169
|
+
|
|
170
|
+
const stdoutFd = openSync(stdoutPath, "a");
|
|
171
|
+
const stderrFd = openSync(stderrPath, "a");
|
|
172
|
+
let childPid: number | undefined;
|
|
173
|
+
try {
|
|
174
|
+
const child = spawn(
|
|
175
|
+
process.execPath,
|
|
176
|
+
[fileURLToPath(import.meta.url), ...childArgv],
|
|
177
|
+
{
|
|
178
|
+
cwd: params.repoRoot,
|
|
179
|
+
detached: true,
|
|
180
|
+
env: process.env,
|
|
181
|
+
stdio: ["ignore", stdoutFd, stderrFd],
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
child.unref();
|
|
185
|
+
childPid = child.pid;
|
|
186
|
+
await writeFile(
|
|
187
|
+
metadataPath,
|
|
188
|
+
`${JSON.stringify(
|
|
189
|
+
{
|
|
190
|
+
args: childArgv,
|
|
191
|
+
command: process.execPath,
|
|
192
|
+
pid: childPid,
|
|
193
|
+
runDir: params.runDir,
|
|
194
|
+
startedAt: new Date().toISOString(),
|
|
195
|
+
stderrPath,
|
|
196
|
+
stdoutPath,
|
|
197
|
+
},
|
|
198
|
+
null,
|
|
199
|
+
2,
|
|
200
|
+
)}\n`,
|
|
201
|
+
"utf8",
|
|
202
|
+
);
|
|
203
|
+
} finally {
|
|
204
|
+
closeSync(stdoutFd);
|
|
205
|
+
closeSync(stderrFd);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log(`BACKGROUND ${params.runDir}`);
|
|
209
|
+
console.log(`PID ${childPid ?? "unknown"}`);
|
|
210
|
+
console.log(`stdout ${stdoutPath}`);
|
|
211
|
+
console.log(`stderr ${stderrPath}`);
|
|
212
|
+
console.log(`progress tail -f ${stdoutPath}`);
|
|
213
|
+
console.log(`report ${path.join(params.runDir, "report.md")}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function formatScore(
|
|
217
|
+
score: number | undefined,
|
|
218
|
+
maxScore: number | undefined,
|
|
219
|
+
): string {
|
|
220
|
+
if (score === undefined) return "";
|
|
221
|
+
if (maxScore === undefined) return ` score ${score}`;
|
|
222
|
+
const percentage = maxScore > 0 ? Math.round((score / maxScore) * 100) : 0;
|
|
223
|
+
return ` score ${score}/${maxScore} (${percentage}%)`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function progressPrefix(progress: ModLearningProgress): string {
|
|
227
|
+
if (progress.candidateIndex && progress.candidateCount) {
|
|
228
|
+
return `[${progress.candidateIndex}/${progress.candidateCount}]`;
|
|
229
|
+
}
|
|
230
|
+
if (progress.candidateIndex) return `[${progress.candidateIndex}]`;
|
|
231
|
+
return `[${progress.phase}]`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function formatProgressLine(progress: ModLearningProgress): string {
|
|
235
|
+
return `${progressPrefix(progress)} ${progress.message}${formatScore(progress.score, progress.maxScore)}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function candidateForPromote(report: ModLearningReport): string {
|
|
239
|
+
return report.candidatePath;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function main(): Promise<void> {
|
|
243
|
+
const argv = process.argv.slice(2);
|
|
244
|
+
const args = parseArgs(argv);
|
|
245
|
+
if (args.help) {
|
|
246
|
+
printHelp();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const repoRoot = path.resolve(args.repoRoot);
|
|
251
|
+
const learningEnv = await readModLearningEnv(
|
|
252
|
+
path.resolve(repoRoot, args.env),
|
|
253
|
+
);
|
|
254
|
+
const runDir = args.out
|
|
255
|
+
? path.resolve(repoRoot, args.out)
|
|
256
|
+
: path.resolve(repoRoot, defaultModLearningRunDirectory(learningEnv));
|
|
257
|
+
|
|
258
|
+
if (!args.foreground) {
|
|
259
|
+
await launchBackground({
|
|
260
|
+
argv,
|
|
261
|
+
outWasProvided: args.out !== undefined,
|
|
262
|
+
repoRoot,
|
|
263
|
+
runDir,
|
|
264
|
+
});
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let lastProgressLine = "";
|
|
269
|
+
const report = await runModLearning({
|
|
270
|
+
backend: args.backend,
|
|
271
|
+
candidateCount:
|
|
272
|
+
args.candidateCount ??
|
|
273
|
+
(args.candidate || args.skipGeneration
|
|
274
|
+
? undefined
|
|
275
|
+
: DEFAULT_OPTIMIZATION_STEPS),
|
|
276
|
+
candidateFileName: args.candidateFileName,
|
|
277
|
+
candidateSourcePath: args.candidate,
|
|
278
|
+
evalModel: args.evalModel,
|
|
279
|
+
generationModel: args.generationModel,
|
|
280
|
+
promoteToPath: args.promoteTo,
|
|
281
|
+
repoRoot,
|
|
282
|
+
runDir,
|
|
283
|
+
scenarioLimit: args.scenarioLimit,
|
|
284
|
+
skipGeneration: args.skipGeneration,
|
|
285
|
+
spec: learningEnv,
|
|
286
|
+
onProgress: (progress) => {
|
|
287
|
+
const line = formatProgressLine(progress);
|
|
288
|
+
if (line === lastProgressLine) return;
|
|
289
|
+
lastProgressLine = line;
|
|
290
|
+
console.log(line);
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const status = report.passed ? "PASS" : "FAIL";
|
|
295
|
+
console.log(`${status} ${report.reportPath}`);
|
|
296
|
+
console.log(`candidate ${candidateForPromote(report)}`);
|
|
297
|
+
if (report.passed) {
|
|
298
|
+
console.log(`promote letta mods promote ${candidateForPromote(report)}`);
|
|
299
|
+
}
|
|
300
|
+
if (!report.passed) process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
main().catch((error) => {
|
|
304
|
+
console.error(error instanceof Error ? error.stack : String(error));
|
|
305
|
+
process.exit(1);
|
|
306
|
+
});
|
|
@@ -7,7 +7,7 @@ description: Creates and edits trusted local Letta Code mods, including tools, s
|
|
|
7
7
|
|
|
8
8
|
Use this skill to create or update trusted Letta Code mod files. Mods are trusted local code that add small composable capabilities through mod APIs, not by importing app internals. Dynamic agent/conversation/workspace/model state is passed as `ctx` to tool, command, event, and permission callbacks (panels receive live `agent`/`model` in their render context); do not read mutable global context for model-callable behavior. Prefer scoped handles (`ctx.conversation`, `ctx.cwd`, `ctx.agent`) and guard optional UI with `letta.capabilities`.
|
|
9
9
|
|
|
10
|
-
Capabilities vary by surface. TUI/headless
|
|
10
|
+
Capabilities vary by surface — not every surface loads every capability. The TUI/headless host can load tools, commands, events, UI, and providers; the desktop listener loads tools, commands, providers, and tool/turn events, but not panel UI. Always guard each registration on the capabilities its behavior needs.
|
|
11
11
|
|
|
12
12
|
## Choose where the mod file lives
|
|
13
13
|
|
|
@@ -93,6 +93,8 @@ letta.capabilities.providers
|
|
|
93
93
|
letta.capabilities.ui.panels
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
Guard each registration on every capability its behavior depends on — not just the one that registers it. Surfaces load different capability subsets, so a registration that relies on another capability (a command that opens UI, emits an event, or calls a provider) must guard on that capability too. Otherwise it is advertised or activated on a host that cannot fulfill it and silently does nothing. Register where the host can actually do the work.
|
|
97
|
+
|
|
96
98
|
## Scoped API model
|
|
97
99
|
|
|
98
100
|
- In commands and events, use `ctx.conversation` for conversation operations:
|
|
@@ -136,6 +138,7 @@ Before finishing, verify:
|
|
|
136
138
|
- Tool descriptions explain when the model should call them.
|
|
137
139
|
- JSON schemas are object schemas with useful descriptions.
|
|
138
140
|
- Optional UI/event APIs are capability-guarded.
|
|
141
|
+
- Each registration is guarded by every capability its behavior depends on, not just the one that registers it, so it isn't advertised or activated on a surface that can't fulfill it.
|
|
139
142
|
- Provider mods are capability-guarded and clearly documented as local-agent only.
|
|
140
143
|
- Timers, intervals, event registrations, and panels are cleaned up in a disposer.
|
|
141
144
|
- Busy commands return `{ type: "handled" }` quickly and avoid main-conversation sends.
|
|
@@ -59,7 +59,7 @@ letta.events.on("tool_start", (event, ctx) => {
|
|
|
59
59
|
|
|
60
60
|
Lifecycle, turn, tool, compaction, and llm events are wired today.
|
|
61
61
|
|
|
62
|
-
Lifecycle handlers are notification-only and should not return values. `turn_start` handlers can transform
|
|
62
|
+
Lifecycle handlers are notification-only and should not return values. `turn_start` handlers can transform or cancel outbound user-message turns. `tool_start` handlers can transform the tool arguments before execution. Compaction and llm handlers are notification-only.
|
|
63
63
|
|
|
64
64
|
`compact_start`/`compact_end` and `llm_start`/`llm_end` only fire on the **local backend**, where compaction and provider requests run client-side. On the constellation backend that work happens server-side and these events do not fire, so guard with `letta.capabilities.events.compact` / `letta.capabilities.events.llm` for portable mods.
|
|
65
65
|
|
|
@@ -155,16 +155,18 @@ Handlers run in registration order. Later handlers see the current args after ea
|
|
|
155
155
|
conversationId: string | null;
|
|
156
156
|
toolCallId: string | null;
|
|
157
157
|
toolName: string;
|
|
158
|
+
args: Record<string, unknown>;
|
|
158
159
|
status: "success" | "error";
|
|
159
160
|
output: string;
|
|
160
161
|
}
|
|
161
162
|
```
|
|
162
163
|
|
|
163
|
-
`tool_end` fires immediately after a tool produces a result, before the agent sees it. Handlers can inspect the result, or return `{ result: { status, output } }` to replace it:
|
|
164
|
+
`tool_end` fires immediately after a tool produces a result, before the agent sees it. `event.args` contains the effective tool invocation arguments after `tool_start` transforms, so handlers can react to the specific file, command, query, etc. Handlers can inspect the result, or return `{ result: { status, output } }` to replace it:
|
|
164
165
|
|
|
165
166
|
```ts
|
|
166
167
|
letta.events.on("tool_end", (event) => {
|
|
167
168
|
if (event.toolName !== "Bash" || event.status !== "success") return;
|
|
169
|
+
if (typeof event.args.command !== "string") return;
|
|
168
170
|
return { result: { status: "success", output: redactSecrets(event.output) } };
|
|
169
171
|
});
|
|
170
172
|
```
|
|
@@ -184,7 +186,7 @@ letta.events.on("tool_end", async (event, ctx) => {
|
|
|
184
186
|
});
|
|
185
187
|
```
|
|
186
188
|
|
|
187
|
-
`turn_start` fires before outbound turns that include a user message. In the TUI this includes normal submits and prompt-style command turns. In headless it includes one-shot prompts and bidirectional user turns.
|
|
189
|
+
`turn_start` fires before outbound turns that include a user message. In the TUI this includes normal submits and prompt-style command turns. In headless it includes one-shot prompts and bidirectional user turns. Listener/Desktop skips approval-only continuations so mods do not rewrite approval payloads; do not rely on `turn_start` to block approval-only continuations on every surface.
|
|
188
190
|
|
|
189
191
|
Handlers can mutate `event.input` directly or return replacement input:
|
|
190
192
|
|
|
@@ -212,6 +214,18 @@ letta.events.on("turn_start", (event) => {
|
|
|
212
214
|
});
|
|
213
215
|
```
|
|
214
216
|
|
|
217
|
+
Handlers can also cancel a user-message turn before it reaches the backend/model:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
letta.events.on("turn_start", (event) => {
|
|
221
|
+
if (!isPlanModeActive(event.conversationId)) {
|
|
222
|
+
return { cancel: { reason: "Run /plan first." } };
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
If multiple handlers cancel, the first valid cancel reason wins. A valid reason is a non-empty string after trimming. Cancellation does not synthesize an assistant response or tool result; it only tells the host not to submit this turn.
|
|
228
|
+
|
|
215
229
|
Handlers run in registration order. Later handlers see the current input after earlier mutations/returns. If a handler throws, its partial `event.input` mutation is rolled back and the error is recorded as a mod diagnostic.
|
|
216
230
|
|
|
217
231
|
`turn_start` is intentionally a trusted local mod point: it can rewrite user messages, approval results, and ordering. Keep transforms focused and unsurprising.
|
|
@@ -60,6 +60,29 @@ render(ctx: {
|
|
|
60
60
|
|
|
61
61
|
Close panels when they are transient, and close/replace long-lived panels from the activation disposer if reload should remove them.
|
|
62
62
|
|
|
63
|
+
### Commands that open panels
|
|
64
|
+
|
|
65
|
+
If a command's `run()` opens a panel, guard the command **registration** on `letta.capabilities.ui.panels` — not just the `openPanel` call:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
export default function activate(letta) {
|
|
69
|
+
if (!letta.capabilities.commands) return;
|
|
70
|
+
if (!letta.capabilities.ui.panels) return; // panel-only command: skip where panels are unsupported
|
|
71
|
+
|
|
72
|
+
return letta.commands.register({
|
|
73
|
+
id: "mycommand",
|
|
74
|
+
description: "…",
|
|
75
|
+
run() {
|
|
76
|
+
const panel = letta.ui.openPanel({ /* … */ });
|
|
77
|
+
// …
|
|
78
|
+
return { type: "handled" };
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Guarding only the `openPanel` call is not enough. The desktop listener has `commands: true` but `ui.panels: false`, so a command registered there is advertised in the command picker while its panel work no-ops — the user sees a command that runs and does nothing. Gating the registration keeps panel-only commands out of hosts that can't render them.
|
|
85
|
+
|
|
63
86
|
## Timers and cleanup
|
|
64
87
|
|
|
65
88
|
```ts
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: generating-mod-envs
|
|
3
|
+
description: Generates and reviews mod learning env JSON files for Letta Code local mods. Use when asked to teach, learn, or optimize a mod behavior; create, draft, validate, improve, or explain envs for `/mods learn --env`; or design evaluation scenarios, memory fixtures, requiredResultMarkers, requiredTraceMarkers, negative controls, and candidate diversity hints.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
user-invocable: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Generating mod learning envs
|
|
9
|
+
|
|
10
|
+
Use this skill to create JSON envs consumed by `/mods learn --env=<path>` or `bun scripts/mod-learning/learn-mod.ts --env <path>`. An env describes the mod behavior to learn and the scenario-suite eval used to score candidates.
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
1. Define the behavior and eval before writing JSON.
|
|
15
|
+
- What should the mod do? Tool, turn event, tool event, provider, command, status, etc.
|
|
16
|
+
- What would a placebo/no-op mod fail?
|
|
17
|
+
- What unique sentinel strings make success unambiguous?
|
|
18
|
+
2. Choose a path:
|
|
19
|
+
- Repo example: `docs/examples/mods/learning/<slug>.env.json`
|
|
20
|
+
- Local/private: any user-requested path
|
|
21
|
+
3. Draft strict JSON. Start from `assets/mod-learning-env.template.json` if useful. No comments or trailing commas.
|
|
22
|
+
4. Prefer `evaluation.scenarios` with at least:
|
|
23
|
+
- happy path
|
|
24
|
+
- discrimination/exact-target path
|
|
25
|
+
- negative control
|
|
26
|
+
5. Validate:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bun src/skills/builtin/generating-mod-envs/scripts/validate-mod-env.ts path/to/env.json
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
If this skill is installed outside the source tree, run the same script from this skill directory: `scripts/validate-mod-env.ts`.
|
|
33
|
+
|
|
34
|
+
6. If asked to run it:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
/mods learn --env=path/to/env.json --model=auto --backend=api --out=/tmp/<slug>-learn
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The raw `scripts/mod-learning/learn-mod.ts` dev script detaches by default. Add `--foreground` only when a blocking pass/fail exit code is needed.
|
|
41
|
+
|
|
42
|
+
Use single-line `--flag=value` commands for TUI instructions.
|
|
43
|
+
|
|
44
|
+
## Env shape
|
|
45
|
+
|
|
46
|
+
Required top-level fields:
|
|
47
|
+
|
|
48
|
+
- `name`: human display name.
|
|
49
|
+
- `slug`: stable kebab-case run/candidate slug.
|
|
50
|
+
- `objective`: one-paragraph target for the generation agent.
|
|
51
|
+
- `requirements`: concrete pass/fail behavior constraints.
|
|
52
|
+
- `evaluation`: either a single prompt eval or a scenario suite.
|
|
53
|
+
|
|
54
|
+
Common optional fields:
|
|
55
|
+
|
|
56
|
+
- `targetModName`: display metadata for the intended mod filename. The harness still chooses the candidate filename from `slug` unless `--candidate-file-name` is passed.
|
|
57
|
+
- `candidateDiversityHints`: strategies assigned across multi-candidate runs.
|
|
58
|
+
- `modApiHints`: concise API reminders that prevent bad generated code.
|
|
59
|
+
- `examples`: small input/expected demos for the generation prompt.
|
|
60
|
+
|
|
61
|
+
Evaluation fields:
|
|
62
|
+
|
|
63
|
+
- `evaluation.outputFormat`: use `stream-json` when checking trace markers.
|
|
64
|
+
- `evaluation.timeoutMs`, `evaluation.maxTurns`: per-scenario defaults.
|
|
65
|
+
- `evaluation.memoryFiles`: files seeded under eval `$MEMORY_DIR`.
|
|
66
|
+
- `evaluation.scenarios[]`: scenario-specific overrides and fixtures.
|
|
67
|
+
- In scenario-suite envs, do not add a top-level `evaluation.prompt` unless that prompt must run for every scenario. Assertion-only scenarios should have `assertions` and no `prompt`; only scenarios that require model behavior should define `scenario.prompt`.
|
|
68
|
+
- `requiredResultMarkers`: literal strings required in the final answer.
|
|
69
|
+
- `requiredTraceMarkers`: literal strings required in raw stdout/stderr.
|
|
70
|
+
- `forbiddenResultMarkers`: final-answer strings that fail the run.
|
|
71
|
+
- `forbiddenTraceMarkers`: raw trace strings that fail the run.
|
|
72
|
+
|
|
73
|
+
## Quality rules
|
|
74
|
+
|
|
75
|
+
- Design the eval first. A useful env distinguishes success from a no-op mod.
|
|
76
|
+
- Use unique sentinels, e.g. `MY-MOD-CANARY-OK`, not common phrases.
|
|
77
|
+
- Seed `memoryFiles` rather than depending on real user memory or repo files.
|
|
78
|
+
- Include negative controls for non-use. If behavior should be conditional, verify it stays silent when not triggered.
|
|
79
|
+
- Include discrimination scenarios when paths, IDs, or sources matter. Put a tempting wrong sentinel in an irrelevant fixture and forbid it in the final answer.
|
|
80
|
+
- Put load failures in `forbiddenTraceMarkers`, usually:
|
|
81
|
+
- `[mods] failed to load`
|
|
82
|
+
- `[extensions] failed to load`
|
|
83
|
+
- `loaded 0 mod(s)`
|
|
84
|
+
- `loaded 0 extension(s)`
|
|
85
|
+
- For eval-facing tools, require `requiresApproval: false`, `parallelSafe: true`, and a strict no-argument schema when applicable.
|
|
86
|
+
- Avoid over-brittle trace markers. Prefer stable substrings like the tool name plus `"message_type":"tool_return_message"`.
|
|
87
|
+
- Keep requirements behavioral; put fragile implementation details in `modApiHints` only when needed.
|
|
88
|
+
|
|
89
|
+
## Minimal scenario-suite example
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"name": "Hello tool mod learner demo",
|
|
94
|
+
"slug": "hello-tool",
|
|
95
|
+
"objective": "Learn a trusted local mod that registers a read-only hello_mod_ping tool returning a fixed sentinel.",
|
|
96
|
+
"requirements": [
|
|
97
|
+
"Register a tool named hello_mod_ping.",
|
|
98
|
+
"The tool must accept no parameters, require no approval, be parallelSafe, and return the exact string HELLO-MOD-OK."
|
|
99
|
+
],
|
|
100
|
+
"candidateDiversityHints": [
|
|
101
|
+
"Use the smallest possible tool-only implementation.",
|
|
102
|
+
"Add explicit defensive checks around the tool schema."
|
|
103
|
+
],
|
|
104
|
+
"modApiHints": [
|
|
105
|
+
"Use export function activate(letta) or a default export.",
|
|
106
|
+
"Use letta.tools.register({ name, description, parameters, requiresApproval, parallelSafe, run }).",
|
|
107
|
+
"A no-argument tool schema is { \"type\": \"object\", \"properties\": {}, \"additionalProperties\": false }."
|
|
108
|
+
],
|
|
109
|
+
"evaluation": {
|
|
110
|
+
"outputFormat": "stream-json",
|
|
111
|
+
"timeoutMs": 900000,
|
|
112
|
+
"maxTurns": 6,
|
|
113
|
+
"forbiddenTraceMarkers": ["[mods] failed to load", "loaded 0 mod(s)"],
|
|
114
|
+
"scenarios": [
|
|
115
|
+
{
|
|
116
|
+
"name": "happy-path",
|
|
117
|
+
"prompt": "Call the hello_mod_ping tool, then answer with the exact text HELLO-MOD-OK.",
|
|
118
|
+
"requiredResultMarkers": ["HELLO-MOD-OK"],
|
|
119
|
+
"requiredTraceMarkers": ["hello_mod_ping", "\"message_type\":\"tool_return_message\""]
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"name": "negative-control",
|
|
123
|
+
"prompt": "Answer without calling tools: what is 2 + 2?",
|
|
124
|
+
"requiredResultMarkers": ["4"],
|
|
125
|
+
"forbiddenTraceMarkers": ["hello_mod_ping"]
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "<Display name> mod learner demo",
|
|
3
|
+
"slug": "example-mod-env",
|
|
4
|
+
"targetModName": "example-mod-env.ts",
|
|
5
|
+
"objective": "Learn a trusted local mod that <specific behavior>.",
|
|
6
|
+
"requirements": [
|
|
7
|
+
"<Concrete behavior requirement>",
|
|
8
|
+
"<Concrete eval-facing requirement>"
|
|
9
|
+
],
|
|
10
|
+
"candidateDiversityHints": [
|
|
11
|
+
"<Alternative implementation strategy for candidate 1>",
|
|
12
|
+
"<Alternative implementation strategy for candidate 2>"
|
|
13
|
+
],
|
|
14
|
+
"modApiHints": [
|
|
15
|
+
"Use export function activate(letta) or a default export.",
|
|
16
|
+
"Use only the trusted local mod API exposed through the letta argument; do not import from repo source files."
|
|
17
|
+
],
|
|
18
|
+
"examples": [
|
|
19
|
+
{
|
|
20
|
+
"input": "<Short demo input>",
|
|
21
|
+
"expected": "<Expected behavior>"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"evaluation": {
|
|
25
|
+
"outputFormat": "stream-json",
|
|
26
|
+
"timeoutMs": 900000,
|
|
27
|
+
"maxTurns": 8,
|
|
28
|
+
"forbiddenTraceMarkers": [
|
|
29
|
+
"[mods] failed to load",
|
|
30
|
+
"[extensions] failed to load",
|
|
31
|
+
"loaded 0 mod(s)",
|
|
32
|
+
"loaded 0 extension(s)"
|
|
33
|
+
],
|
|
34
|
+
"scenarios": [
|
|
35
|
+
{
|
|
36
|
+
"name": "happy-path",
|
|
37
|
+
"memoryFiles": {
|
|
38
|
+
"reference/<fixture>.md": "# Fixture\n\nThe sentinel is <SENTINEL-OK>.\n"
|
|
39
|
+
},
|
|
40
|
+
"prompt": "<Prompt that triggers the mod and requires the sentinel in the final answer>. The memory directory is available in MEMORY_DIR.",
|
|
41
|
+
"requiredResultMarkers": ["<SENTINEL-OK>"],
|
|
42
|
+
"requiredTraceMarkers": ["<stable trace marker>"],
|
|
43
|
+
"forbiddenResultMarkers": ["<known bad final answer marker>"]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "negative-control",
|
|
47
|
+
"prompt": "<Prompt where the mod behavior should not trigger>.",
|
|
48
|
+
"requiredResultMarkers": ["<ordinary answer marker>"],
|
|
49
|
+
"forbiddenResultMarkers": ["<mod-only marker>"],
|
|
50
|
+
"forbiddenTraceMarkers": [
|
|
51
|
+
"<tool or event marker that should not appear>"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"prompt": "Run all configured evaluation.scenarios."
|
|
56
|
+
}
|
|
57
|
+
}
|