@letta-ai/letta-code 0.27.7 → 0.27.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/README.md +2 -2
- package/dist/app-server-client.js +387 -0
- package/dist/app-server-client.js.map +10 -0
- package/dist/types/app-server-client.d.ts +99 -0
- package/dist/types/app-server-client.d.ts.map +1 -0
- package/dist/types/types/app-server-protocol.d.ts +3 -0
- package/dist/types/types/app-server-protocol.d.ts.map +1 -0
- package/dist/types/types/protocol.d.ts.map +1 -0
- package/dist/types/types/protocol_v2.d.ts +2277 -0
- package/dist/types/types/protocol_v2.d.ts.map +1 -0
- package/letta.js +22835 -19810
- package/package.json +12 -2
- package/scripts/check-bundled-skill-scripts.js +169 -0
- package/scripts/check-test-coverage.cjs +1 -1
- package/scripts/check.js +1 -0
- package/scripts/run-unit-tests.cjs +1 -1
- package/skills/converting-mcps-to-skills/SKILL.md +1 -12
- package/skills/converting-mcps-to-skills/scripts/mcp-stdio.ts +192 -57
- package/skills/{creating-extensions → creating-mods}/SKILL.md +29 -29
- package/skills/{creating-extensions → creating-mods}/references/architecture.md +9 -9
- package/skills/{creating-extensions → creating-mods}/references/commands.md +10 -10
- package/skills/{creating-extensions → creating-mods}/references/events.md +10 -10
- package/skills/{creating-extensions → creating-mods}/references/permissions.md +3 -3
- package/skills/{creating-extensions → creating-mods}/references/plan-mode.md +72 -31
- package/skills/{creating-extensions → creating-mods}/references/providers.md +7 -7
- package/skills/{creating-extensions → creating-mods}/references/tools.md +20 -2
- package/skills/{creating-extensions → creating-mods}/references/ui.md +4 -4
- package/skills/creating-skills/scripts/validate-skill.ts +129 -5
- package/skills/customizing-commands/SKILL.md +18 -18
- package/skills/customizing-statusline/SKILL.md +11 -11
- package/skills/customizing-statusline/references/api.md +8 -8
- package/skills/customizing-statusline/references/examples.md +1 -1
- package/skills/customizing-statusline/references/migration.md +1 -1
- package/skills/editing-letta-code-desktop-preferences/SKILL.md +67 -0
- package/skills/image-generation/SKILL.md +120 -0
- package/skills/modifying-the-harness/SKILL.md +21 -2
- package/skills/modifying-the-harness/scripts/add_permission.py +2 -1
- package/skills/modifying-the-harness/scripts/show_config.py +4 -3
- package/dist/types/protocol.d.ts.map +0 -1
- package/skills/converting-mcps-to-skills/scripts/package.json +0 -13
- /package/dist/types/{protocol.d.ts → types/protocol.d.ts} +0 -0
|
@@ -1,42 +1,42 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: creating-
|
|
3
|
-
description: Creates and edits trusted local Letta Code
|
|
2
|
+
name: creating-mods
|
|
3
|
+
description: Creates and edits trusted local Letta Code mods, including tools, slash commands, local-only model providers, lifecycle/turn events, scoped conversation helpers, panels, status values, and capability-gated behavior. Use when asked to make a mod, add an agent-callable tool, add a slash command, add a local provider/model adapter, transform turns, react to app events, or add lightweight mod UI outside the dedicated /statusline flow.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Creating
|
|
6
|
+
# Creating Mods
|
|
7
7
|
|
|
8
|
-
Use this skill to create or update trusted global Letta Code
|
|
8
|
+
Use this skill to create or update trusted global Letta Code mods in:
|
|
9
9
|
|
|
10
10
|
```text
|
|
11
|
-
~/.letta/
|
|
11
|
+
~/.letta/mods/
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Mods are trusted local code for Letta Code. They add small composable capabilities through mod APIs, not by importing app internals. Prefer scoped handles (`ctx.conversation`, `ctx.cwd`, `ctx.agent`, `letta.getContext()`) and guard optional UI with `letta.capabilities`.
|
|
15
15
|
|
|
16
|
-
Capabilities vary by surface. TUI/headless may load tools, commands, events, UI, and providers; the desktop listener loads provider-only
|
|
16
|
+
Capabilities vary by surface. TUI/headless may load tools, commands, events, UI, and providers; the desktop listener loads provider-only mods for local provider discovery. Always guard optional capabilities.
|
|
17
17
|
|
|
18
18
|
## Choose the right capability
|
|
19
19
|
|
|
20
20
|
| User wants | Build |
|
|
21
21
|
| --- | --- |
|
|
22
|
-
| Agent/model should autonomously call a local capability |
|
|
23
|
-
| User wants `/foo` to send a prompt or run local UI logic |
|
|
24
|
-
| Slash command represents a reusable agent workflow | Skill + thin
|
|
22
|
+
| Agent/model should autonomously call a local capability | Mod tool |
|
|
23
|
+
| User wants `/foo` to send a prompt or run local UI logic | Mod command |
|
|
24
|
+
| Slash command represents a reusable agent workflow | Skill + thin mod command |
|
|
25
25
|
| Command should work while the main agent is busy | Command with `runWhenBusy: true`, `handled`, panel/status, and usually `ctx.conversation.fork()` |
|
|
26
26
|
| Show transient output above input | Panel, usually from a command |
|
|
27
27
|
| Show small persistent state | Status value |
|
|
28
28
|
| React to app/session lifecycle or transform outbound turns | Event |
|
|
29
29
|
| Enforce dynamic allow/ask/deny policy for tool calls | Permission overlay |
|
|
30
|
-
| Add a custom model/API provider for local agents | Provider
|
|
30
|
+
| Add a custom model/API provider for local agents | Provider mod (local agents only) |
|
|
31
31
|
| Change the bottom statusline appearance | Use `customizing-statusline`, not this skill |
|
|
32
32
|
|
|
33
33
|
Default to a **tool** when the model should decide when to use the capability. Default to a **command** when the human explicitly invokes it. Compose capabilities when the UX needs it, e.g. command + panel + scoped conversation fork.
|
|
34
34
|
|
|
35
35
|
## Workflow
|
|
36
36
|
|
|
37
|
-
1. Inspect `~/.letta/
|
|
38
|
-
2. Preserve unrelated
|
|
39
|
-
3. Choose the
|
|
37
|
+
1. Inspect `~/.letta/mods/` for related files.
|
|
38
|
+
2. Preserve unrelated mod code. Prefer a focused new file if merging would be messy.
|
|
39
|
+
3. Choose the mod shape and load only the needed recipe:
|
|
40
40
|
- tools: `references/tools.md`
|
|
41
41
|
- commands: `references/commands.md`
|
|
42
42
|
- local custom providers: `references/providers.md`
|
|
@@ -44,13 +44,13 @@ Default to a **tool** when the model should decide when to use the capability. D
|
|
|
44
44
|
- permissions: `references/permissions.md`
|
|
45
45
|
- panels/status/capabilities: `references/ui.md`
|
|
46
46
|
- complex plan-mode composition: `references/plan-mode.md`
|
|
47
|
-
4. For multi-capability or stateful
|
|
48
|
-
5. Write a single-file
|
|
47
|
+
4. For multi-capability or stateful mods, also read `references/architecture.md`.
|
|
48
|
+
5. Write a single-file mod unless the user asks for something larger.
|
|
49
49
|
6. Return disposers for registered providers/commands/tools/events, timers, subscriptions, and panels that should close on reload.
|
|
50
50
|
7. Do a basic review: valid names, descriptions present, schemas are object schemas, optional capabilities guarded, scoped APIs used, cleanup returned.
|
|
51
|
-
8. Tell the user the absolute file path changed and to run `/reload`. If
|
|
51
|
+
8. Tell the user the absolute file path changed and to run `/reload`. If a mod breaks startup or command handling, recover with `letta --no-mods` or `LETTA_DISABLE_MODS=1 letta`.
|
|
52
52
|
|
|
53
|
-
## Core
|
|
53
|
+
## Core mod shape
|
|
54
54
|
|
|
55
55
|
```ts
|
|
56
56
|
export default function activate(letta) {
|
|
@@ -93,25 +93,25 @@ letta.capabilities.ui.customStatuslineRenderer
|
|
|
93
93
|
- `forked.sendMessageStream([...])` to stream from a fork
|
|
94
94
|
- In tools, use `ctx.conversation.getHistory()` when the tool needs recent context.
|
|
95
95
|
- Use `letta.client` only for server-specific Letta API calls; do not use it as a substitute for scoped conversation helpers.
|
|
96
|
-
- Do not import `@/backend`, `@/cli`, or other Letta Code internals from
|
|
96
|
+
- Do not import `@/backend`, `@/cli`, or other Letta Code internals from mod files.
|
|
97
97
|
|
|
98
98
|
## Diagnostics
|
|
99
99
|
|
|
100
|
-
Use `letta.diagnostics.report({ message, severity })` sparingly as a debug utility for
|
|
100
|
+
Use `letta.diagnostics.report({ message, severity })` sparingly as a debug utility for mod setup/runtime problems an agent should inspect, such as missing required environment variables or failed local configuration. Default severity is `"error"`; use `severity: "warning"` only for optional/degraded behavior. Keep messages short and actionable, and do not dump routine logs or large state.
|
|
101
101
|
|
|
102
|
-
Agents can inspect local
|
|
102
|
+
Agents can inspect local mod diagnostics at:
|
|
103
103
|
|
|
104
104
|
```text
|
|
105
|
-
~/.letta/
|
|
105
|
+
~/.letta/mods/diagnostics/latest.json
|
|
106
106
|
```
|
|
107
107
|
|
|
108
108
|
## Rules
|
|
109
109
|
|
|
110
|
-
- Global trusted code only for now. Do not create project
|
|
111
|
-
- Custom provider
|
|
112
|
-
- Provider
|
|
110
|
+
- Global trusted code only for now. Do not create project mods.
|
|
111
|
+
- Custom provider mods are local-backend/local-agent only. They do not add providers for Constellation/cloud agents.
|
|
112
|
+
- Provider mods may run in a provider-only listener context; keep provider registration independent from commands/tools/UI and guard everything else.
|
|
113
113
|
- Do not assume extra npm packages are available.
|
|
114
|
-
- Do not do surprising side effects on startup;
|
|
114
|
+
- Do not do surprising side effects on startup; mods activate on app start and `/reload`.
|
|
115
115
|
- Keep user-facing output short and intentional.
|
|
116
116
|
- Prefer Node/Bun standard APIs (`node:child_process`, `node:fs`, etc.) for local work.
|
|
117
117
|
- For shell execution, prefer `execFile`/`spawn` over shell strings.
|
|
@@ -119,16 +119,16 @@ Agents can inspect local extension diagnostics at:
|
|
|
119
119
|
- For `runWhenBusy: true`, do not return `prompt`; return `handled` and own the UI/background work.
|
|
120
120
|
- Treat `turn_start` as powerful trusted code: keep transforms narrow and unsurprising.
|
|
121
121
|
|
|
122
|
-
## Pre-flight checklist for complex
|
|
122
|
+
## Pre-flight checklist for complex mods
|
|
123
123
|
|
|
124
124
|
Before finishing, verify:
|
|
125
125
|
|
|
126
|
-
- The
|
|
126
|
+
- The mod has one clear owner/file and does not mix unrelated features.
|
|
127
127
|
- Command/tool IDs are valid; command overrides of built-ins are intentional, and tool IDs do not collide with built-ins.
|
|
128
128
|
- Tool descriptions explain when the model should call them.
|
|
129
129
|
- JSON schemas are object schemas with useful descriptions.
|
|
130
130
|
- Optional UI/event/statusline APIs are capability-guarded.
|
|
131
|
-
- Provider
|
|
131
|
+
- Provider mods are capability-guarded and clearly documented as local-agent only.
|
|
132
132
|
- Timers, intervals, event registrations, and panels are cleaned up in a disposer.
|
|
133
133
|
- Busy commands return `{ type: "handled" }` quickly and avoid main-conversation sends.
|
|
134
134
|
- Conversation work uses `ctx.conversation` or forked handles, not app internals.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Mod architecture patterns
|
|
2
2
|
|
|
3
|
-
Use this reference for non-trivial
|
|
3
|
+
Use this reference for non-trivial mods: multiple capabilities, local state, timers, background model work, or UI.
|
|
4
4
|
|
|
5
5
|
## Contents
|
|
6
6
|
|
|
@@ -14,14 +14,14 @@ Use this reference for non-trivial extensions: multiple capabilities, local stat
|
|
|
14
14
|
|
|
15
15
|
## Mental model
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
A mod is trusted local code that registers capabilities during activation and cleans them up on reload/shutdown. Keep the public surface small:
|
|
18
18
|
|
|
19
19
|
- activation registers commands/tools/events/UI
|
|
20
20
|
- command/tool/event handlers receive scoped context
|
|
21
21
|
- state is local and explicit
|
|
22
22
|
- cleanup is returned from activation
|
|
23
23
|
|
|
24
|
-
Do not import Letta Code internals. If the
|
|
24
|
+
Do not import Letta Code internals. If the mod API does not expose a capability yet, avoid reaching around it.
|
|
25
25
|
|
|
26
26
|
Capabilities vary by host surface. Keep each registration behind the matching `letta.capabilities` guard so one file can run in TUI, headless, and provider-only listener contexts.
|
|
27
27
|
|
|
@@ -92,14 +92,14 @@ if (letta.capabilities.events.lifecycle && letta.capabilities.ui.statusValues) {
|
|
|
92
92
|
|
|
93
93
|
### `turn_start` transform
|
|
94
94
|
|
|
95
|
-
Use `turn_start` only when the
|
|
95
|
+
Use `turn_start` only when the mod needs to inspect or transform the outbound user-message turn. Keep transforms local and predictable. Prefer appending/prepending focused context or replacing explicit shortcuts over broad rewrites.
|
|
96
96
|
|
|
97
97
|
## Local state
|
|
98
98
|
|
|
99
|
-
For small persistent state, use a clearly named file under `~/.letta/
|
|
99
|
+
For small persistent state, use a clearly named file under `~/.letta/mods/`, for example:
|
|
100
100
|
|
|
101
101
|
```text
|
|
102
|
-
~/.letta/
|
|
102
|
+
~/.letta/mods/my-mod.state.json
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
Use atomic-ish writes when practical: write the full JSON file from an in-memory object after each change. Validate parsed state and fall back gracefully if the file is missing or malformed.
|
|
@@ -108,7 +108,7 @@ Keep state separate from source code. Do not store secrets in plain JSON; use ex
|
|
|
108
108
|
|
|
109
109
|
## Timers and subscriptions
|
|
110
110
|
|
|
111
|
-
Timers are okay for active-session behavior, but they only run while the
|
|
111
|
+
Timers are okay for active-session behavior, but they only run while the mod engine is alive. Always clear them:
|
|
112
112
|
|
|
113
113
|
```ts
|
|
114
114
|
const timer = setInterval(update, 30_000);
|
|
@@ -147,4 +147,4 @@ Tools currently receive `ctx.conversation.getHistory()` but not fork/send helper
|
|
|
147
147
|
- `runWhenBusy: true` commands return `handled`, not `prompt`.
|
|
148
148
|
- Background model work uses a forked conversation.
|
|
149
149
|
- Local filesystem and shell work uses scoped paths and `execFile`/`spawn`.
|
|
150
|
-
-
|
|
150
|
+
- Mod output is concise and actionable.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Mod command recipes
|
|
2
2
|
|
|
3
3
|
Use commands when the human explicitly invokes `/foo`.
|
|
4
4
|
|
|
5
|
-
For complex command-driven
|
|
5
|
+
For complex command-driven mods with panels, timers, local state, or background model work, also read `architecture.md`.
|
|
6
6
|
|
|
7
7
|
## Contents
|
|
8
8
|
|
|
@@ -17,10 +17,10 @@ For complex command-driven extensions with panels, timers, local state, or backg
|
|
|
17
17
|
|
|
18
18
|
| Need | Use |
|
|
19
19
|
| --- | --- |
|
|
20
|
-
| `/foo` expands to a prompt |
|
|
21
|
-
| `/foo` starts a complex reusable workflow | Skill + thin
|
|
22
|
-
| Model should call the capability by itself |
|
|
23
|
-
| Command needs transient UI while doing local work |
|
|
20
|
+
| `/foo` expands to a prompt | Mod command |
|
|
21
|
+
| `/foo` starts a complex reusable workflow | Skill + thin mod command |
|
|
22
|
+
| Model should call the capability by itself | Mod tool |
|
|
23
|
+
| Command needs transient UI while doing local work | Mod command + panel |
|
|
24
24
|
| Command needs model output while the main agent is busy | `runWhenBusy: true` command + forked `ctx.conversation` |
|
|
25
25
|
|
|
26
26
|
If the command represents a durable agent workflow (for example `/goal`), put the workflow instructions in a skill and keep the command as a small launcher/prompt.
|
|
@@ -29,8 +29,8 @@ If the command represents a durable agent workflow (for example `/goal`), put th
|
|
|
29
29
|
|
|
30
30
|
- Do not include the leading slash. Use `id: "review"`, not `id: "/review"`.
|
|
31
31
|
- Use a lowercase slug with letters, numbers, and hyphens only.
|
|
32
|
-
- Built-in commands like `/reload`, `/model`, `/statusline`, etc. can be overridden by trusted local
|
|
33
|
-
- Duplicate
|
|
32
|
+
- Built-in commands like `/reload`, `/model`, `/statusline`, etc. can be overridden by trusted local mods. Do this intentionally and keep recovery in mind: start with `--no-mods` or `LETTA_DISABLE_MODS=1` if an override breaks command handling.
|
|
33
|
+
- Duplicate mod command IDs fail unless `override: true` is intentional.
|
|
34
34
|
|
|
35
35
|
## Prompt command
|
|
36
36
|
|
|
@@ -68,7 +68,7 @@ export default function activate(letta) {
|
|
|
68
68
|
|
|
69
69
|
return letta.commands.register({
|
|
70
70
|
id: "whereami",
|
|
71
|
-
description: "Show the active
|
|
71
|
+
description: "Show the active mod command context",
|
|
72
72
|
run(ctx) {
|
|
73
73
|
return {
|
|
74
74
|
type: "output",
|
|
@@ -125,4 +125,4 @@ const stream = await forked.sendMessageStream([
|
|
|
125
125
|
|
|
126
126
|
Do not send directly to the active conversation from a busy command; fork first unless the user explicitly asked to affect the main conversation later.
|
|
127
127
|
|
|
128
|
-
For a worked multi-capability
|
|
128
|
+
For a worked multi-capability mod that combines commands, tools, events, permissions, and local state, see `plan-mode.md`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Mod event recipes
|
|
2
2
|
|
|
3
|
-
Use events when trusted local code should react to app/session changes or transform outbound turns without the human explicitly invoking a command. For event-driven
|
|
3
|
+
Use events when trusted local code should react to app/session changes or transform outbound turns without the human explicitly invoking a command. For event-driven mods with state, timers, panels, or background model work, also read `architecture.md`.
|
|
4
4
|
|
|
5
5
|
## Contents
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ Use events when trusted local code should react to app/session changes or transf
|
|
|
12
12
|
- Conversation status example
|
|
13
13
|
- Rules
|
|
14
14
|
|
|
15
|
-
This is the first slice of the hooks-v2 direction. The long-term goal is for typed
|
|
15
|
+
This is the first slice of the hooks-v2 direction. The long-term goal is for typed mod events to replace settings-based hooks. Existing hooks still own blocking decisions and model feedback injection until each event has a typed return contract.
|
|
16
16
|
|
|
17
17
|
## Capabilities
|
|
18
18
|
|
|
@@ -22,7 +22,7 @@ letta.capabilities.events.tools
|
|
|
22
22
|
letta.capabilities.events.turns
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
Guard events when writing portable
|
|
25
|
+
Guard events when writing portable mods:
|
|
26
26
|
|
|
27
27
|
```ts
|
|
28
28
|
export default function activate(letta) {
|
|
@@ -115,7 +115,7 @@ Lifecycle handlers are notification-only and should not return values. `turn_sta
|
|
|
115
115
|
}
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
-
`tool_start` fires immediately before a client-side tool executes. This includes built-in tools,
|
|
118
|
+
`tool_start` fires immediately before a client-side tool executes. This includes built-in tools, mod tools, and external tools executed through the local tool manager. It runs after permission/approval classification and before `PreToolUse` hooks, so trusted local mods can change the actual executed arguments after the approval UI has already classified the original request. Mod permission overlays are rechecked after `tool_start` on the final args.
|
|
119
119
|
|
|
120
120
|
Handlers can inspect `event.args`, mutate it directly, or return replacement args:
|
|
121
121
|
|
|
@@ -134,9 +134,9 @@ letta.events.on("tool_start", (event) => {
|
|
|
134
134
|
});
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
-
Handlers run in registration order. Later handlers see the current args after earlier mutations/returns. If a handler throws, its partial `event.args` mutation is rolled back and the error is recorded as
|
|
137
|
+
Handlers run in registration order. Later handlers see the current args after earlier mutations/returns. If a handler throws, its partial `event.args` mutation is rolled back and the error is recorded as a mod diagnostic.
|
|
138
138
|
|
|
139
|
-
`tool_start` is intentionally a trusted local
|
|
139
|
+
`tool_start` is intentionally a trusted local mod point: it can rewrite commands, file paths, and other tool inputs before execution. Keep transforms focused and unsurprising.
|
|
140
140
|
|
|
141
141
|
`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.
|
|
142
142
|
|
|
@@ -166,9 +166,9 @@ letta.events.on("turn_start", (event) => {
|
|
|
166
166
|
});
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
-
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
|
|
169
|
+
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.
|
|
170
170
|
|
|
171
|
-
`turn_start` is intentionally a trusted local
|
|
171
|
+
`turn_start` is intentionally a trusted local mod point: it can rewrite user messages, approval results, and ordering. Keep transforms focused and unsurprising.
|
|
172
172
|
|
|
173
173
|
Handlers also receive:
|
|
174
174
|
|
|
@@ -221,5 +221,5 @@ export default function activate(letta) {
|
|
|
221
221
|
|
|
222
222
|
- Do not block user flow unless the event's typed contract explicitly supports blocking.
|
|
223
223
|
- Do not use lifecycle events for safety decisions yet. Existing hooks still own blocking behavior.
|
|
224
|
-
- Catch expected local errors if the user-facing outcome matters. Uncaught errors are isolated and recorded as
|
|
224
|
+
- Catch expected local errors if the user-facing outcome matters. Uncaught errors are isolated and recorded as mod diagnostics.
|
|
225
225
|
- Return disposers from activation for event registrations, timers, subscriptions, and status values.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Mod permission recipes
|
|
2
2
|
|
|
3
3
|
Use permission overlays when trusted local code should participate in tool approval decisions. Prefer permissions over `tool_start` denial for policy: permissions run before approval UI and again before execution on final tool arguments.
|
|
4
4
|
|
|
@@ -8,7 +8,7 @@ Use permission overlays when trusted local code should participate in tool appro
|
|
|
8
8
|
letta.capabilities.permissions
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Guard registrations when writing portable
|
|
11
|
+
Guard registrations when writing portable mods:
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
14
|
export default function activate(letta) {
|
|
@@ -71,7 +71,7 @@ Composition rules across overlays:
|
|
|
71
71
|
- then `allow`
|
|
72
72
|
- `undefined` means no opinion
|
|
73
73
|
|
|
74
|
-
User/configured hard denials still win before
|
|
74
|
+
User/configured hard denials still win before mod overlays. Mod overlays can override normal auto-allow/default approval behavior, including unrestricted/yolo mode.
|
|
75
75
|
|
|
76
76
|
## Two phases
|
|
77
77
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Plan mode
|
|
1
|
+
# Plan mode mod example
|
|
2
2
|
|
|
3
|
-
Use this as the canonical multi-capability
|
|
3
|
+
Use this as the canonical multi-capability mod example. It composes a slash command, model-callable tools, turn reminders, permission overlays, and local state to recreate the old built-in plan-mode flow with mod APIs.
|
|
4
4
|
|
|
5
|
-
This is a pattern reference, not a full product implementation. Keep local
|
|
5
|
+
This is a pattern reference, not a full product implementation. Keep local mods self-contained and avoid importing Letta Code internals.
|
|
6
6
|
|
|
7
7
|
## Contents
|
|
8
8
|
|
|
@@ -24,13 +24,15 @@ This is a pattern reference, not a full product implementation. Keep local exten
|
|
|
24
24
|
-> remind the agent that only read-only tools and plan-file writes are allowed
|
|
25
25
|
-> permission overlay denies mutations outside ~/.letta/plans/*.md
|
|
26
26
|
-> agent writes the plan with normal Write/Edit/ApplyPatch tools
|
|
27
|
-
-> agent reads the plan and calls AskUserQuestion with Approve / Revise
|
|
27
|
+
-> agent reads the plan and calls AskUserQuestion with the full current plan text and Approve / Revise
|
|
28
28
|
-> if approved, agent calls exit_plan_mode
|
|
29
29
|
-> exit_plan_mode clears state and returns the approved-plan execution handoff
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
Plan files are normal markdown files. Do not add a special `update_plan_file` tool unless the user explicitly wants that abstraction. Let the agent use normal write tools and constrain those tools with permissions.
|
|
33
33
|
|
|
34
|
+
Plan approval must show the user the full current plan text. Do not ask "does this look right?" with only a summary. After every revision, read the plan file again and present the full revised plan in the `AskUserQuestion.question` body before exiting plan mode.
|
|
35
|
+
|
|
34
36
|
## Capabilities used
|
|
35
37
|
|
|
36
38
|
Guard each registration with the matching capability:
|
|
@@ -38,13 +40,13 @@ Guard each registration with the matching capability:
|
|
|
38
40
|
- `commands`: `/plan` for explicit human entry
|
|
39
41
|
- `tools`: `enter_plan_mode` and `exit_plan_mode` for model-driven entry/exit
|
|
40
42
|
- `events.turns`: append a focused plan-mode reminder while active
|
|
41
|
-
- `permissions`: block mutating tools except plan-file writes
|
|
43
|
+
- `permissions`: block mutating tools except planning coordination tools and plan-file writes
|
|
42
44
|
|
|
43
45
|
Do not use panels for persistent mode state. Panels are transient UI and can be noisy/fragile for mode indicators. Do not add a custom statusline renderer just to show plan mode; `setStatuslineRenderer` is a single global renderer, not an additive slot. This example intentionally keeps visible mode state out of scope.
|
|
44
46
|
|
|
45
47
|
## State
|
|
46
48
|
|
|
47
|
-
Use small local state under `~/.letta/
|
|
49
|
+
Use small local state under `~/.letta/mods/`, keyed by conversation ID:
|
|
48
50
|
|
|
49
51
|
```ts
|
|
50
52
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -52,7 +54,7 @@ import { homedir } from "node:os";
|
|
|
52
54
|
import { join, relative } from "node:path";
|
|
53
55
|
|
|
54
56
|
const PLANS_DIR = join(homedir(), ".letta", "plans");
|
|
55
|
-
const STATE_PATH = join(homedir(), ".letta", "
|
|
57
|
+
const STATE_PATH = join(homedir(), ".letta", "mods", "plan-mode.state.json");
|
|
56
58
|
const GLOBAL_CONVERSATION_ID = "__global__";
|
|
57
59
|
|
|
58
60
|
type PlanSession = {
|
|
@@ -79,7 +81,7 @@ function readState(): PlanState {
|
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
function writeState(state: PlanState): void {
|
|
82
|
-
mkdirSync(join(homedir(), ".letta", "
|
|
84
|
+
mkdirSync(join(homedir(), ".letta", "mods"), { recursive: true });
|
|
83
85
|
writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
|
|
84
86
|
}
|
|
85
87
|
```
|
|
@@ -99,9 +101,10 @@ In plan mode, you should:
|
|
|
99
101
|
1. Thoroughly explore the codebase to understand existing patterns
|
|
100
102
|
2. Identify similar features and architectural approaches
|
|
101
103
|
3. Consider multiple approaches and their trade-offs
|
|
102
|
-
4. Use
|
|
103
|
-
5.
|
|
104
|
-
6.
|
|
104
|
+
4. Use direct read-only tools for exploration. Do not launch coding, general-purpose, or fork subagents in plan mode; they may mutate files and should be denied. Only recall-style subagents are allowed if available.
|
|
105
|
+
5. Use AskUserQuestion if you need to clarify the approach
|
|
106
|
+
6. Design a concrete implementation strategy
|
|
107
|
+
7. When ready, write the plan to the plan file, read the plan file, use AskUserQuestion to present the full current plan text for approval, and call exit_plan_mode after the user approves
|
|
105
108
|
|
|
106
109
|
Remember: DO NOT write or edit any files except the plan file. This is a read-only exploration and planning phase.
|
|
107
110
|
|
|
@@ -159,8 +162,9 @@ Plan mode is active. The user indicated that they do not want you to execute yet
|
|
|
159
162
|
1. Answer the user's query comprehensively, using the AskUserQuestion tool if you need to ask the user clarifying questions.
|
|
160
163
|
2. Write your implementation plan to the plan file. Plan file path: ${session.planFilePath}
|
|
161
164
|
3. If using apply_patch, use this exact relative path in patch headers: ${relativePatchPath}
|
|
162
|
-
4.
|
|
163
|
-
5.
|
|
165
|
+
4. Use direct read-only tools for exploration. Do not launch coding, general-purpose, or fork subagents in plan mode; they may mutate files and should be denied. Only recall-style subagents are allowed if available.
|
|
166
|
+
5. When the plan is complete, read the plan file and present the full current plan text to the user with AskUserQuestion. The question body must include the entire plan, not a summary. The question should offer at least "Approve" and "Revise" options.
|
|
167
|
+
6. If the user approves, call exit_plan_mode immediately. If the user asks to revise, stay in plan mode, update the plan file, then read and present the full revised plan again.
|
|
164
168
|
Do NOT make any file changes outside the plan file or run any tools that modify the system state until the user has approved the plan and you have called exit_plan_mode.
|
|
165
169
|
</system-reminder>`;
|
|
166
170
|
}
|
|
@@ -176,35 +180,58 @@ if (letta.capabilities.events.turns) {
|
|
|
176
180
|
|
|
177
181
|
## Permission overlay
|
|
178
182
|
|
|
179
|
-
Use a permission overlay, not `tool_start`, for policy. Normalize tool names by family; UI display names and provider-specific tool names drift (`Read`, `read`, `read_file`, `ReadFile`, `SearchFileContent`, etc.).
|
|
183
|
+
Use a permission overlay, not `tool_start`, for policy. Normalize tool names by family; UI display names and provider-specific tool names drift (`Read`, `read`, `read_file`, `ReadFile`, `SearchFileContent`, etc.). Keep pure read-only tools separate from planning coordination tools like `AskUserQuestion` and todo/plan updates so the policy stays honest.
|
|
180
184
|
|
|
181
185
|
```ts
|
|
182
186
|
const readOnlyToolNames = new Set([
|
|
183
|
-
"askuserquestion",
|
|
184
|
-
"ask_user_question",
|
|
185
187
|
"glob",
|
|
188
|
+
"globgemini",
|
|
186
189
|
"grep",
|
|
190
|
+
"grepfiles",
|
|
191
|
+
"list",
|
|
187
192
|
"listdir",
|
|
188
|
-
"
|
|
193
|
+
"listdirectory",
|
|
189
194
|
"ls",
|
|
195
|
+
"notebookread",
|
|
190
196
|
"read",
|
|
191
|
-
"read_file",
|
|
192
197
|
"readfile",
|
|
198
|
+
"readfilegemini",
|
|
199
|
+
"readlsp",
|
|
200
|
+
"readmanyfiles",
|
|
193
201
|
"search",
|
|
194
|
-
"
|
|
202
|
+
"searchfilecontent",
|
|
203
|
+
"searchfiles",
|
|
195
204
|
"skill",
|
|
196
205
|
"taskoutput",
|
|
197
|
-
"
|
|
198
|
-
"view_image",
|
|
206
|
+
"viewimage",
|
|
199
207
|
]);
|
|
200
208
|
|
|
209
|
+
const planningToolNames = new Set([
|
|
210
|
+
"askuserquestion",
|
|
211
|
+
"enterplanmode",
|
|
212
|
+
"exitplanmode",
|
|
213
|
+
"todowrite",
|
|
214
|
+
"updateplan",
|
|
215
|
+
"writetodos",
|
|
216
|
+
]);
|
|
217
|
+
|
|
218
|
+
const readOnlySubagentTypes = new Set(["recall"]);
|
|
219
|
+
|
|
201
220
|
function normalizedToolName(toolName) {
|
|
202
|
-
return toolName.replace(/[
|
|
221
|
+
return toolName.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
203
222
|
}
|
|
204
223
|
|
|
205
224
|
function isReadOnlyToolName(toolName) {
|
|
206
|
-
|
|
207
|
-
|
|
225
|
+
return readOnlyToolNames.has(normalizedToolName(toolName));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isPlanningToolName(toolName) {
|
|
229
|
+
return planningToolNames.has(normalizedToolName(toolName));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function isAllowedReadOnlySubagent(args) {
|
|
233
|
+
const subagentType = args?.subagent_type;
|
|
234
|
+
return typeof subagentType === "string" && readOnlySubagentTypes.has(normalizedToolName(subagentType));
|
|
208
235
|
}
|
|
209
236
|
|
|
210
237
|
function isPlanFileWrite(toolName, args, cwd) {
|
|
@@ -220,18 +247,28 @@ if (letta.capabilities.permissions) {
|
|
|
220
247
|
check(event) {
|
|
221
248
|
const session = getSession(event.conversationId);
|
|
222
249
|
if (!session) return;
|
|
250
|
+
const toolName = String(event.toolName);
|
|
251
|
+
const args = event.args ?? {};
|
|
252
|
+
|
|
253
|
+
if (isReadOnlyToolName(toolName)) return { decision: "allow" };
|
|
254
|
+
if (isPlanningToolName(toolName)) return { decision: "allow", reason: "planning" };
|
|
223
255
|
|
|
224
|
-
|
|
225
|
-
if (
|
|
256
|
+
const normalized = normalizedToolName(toolName);
|
|
257
|
+
if ((normalized === "agent" || normalized === "task") && isAllowedReadOnlySubagent(args)) {
|
|
258
|
+
return { decision: "allow", reason: "read-only subagent" };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (isPlanFileWrite(toolName, args, event.workingDirectory || event.cwd)) {
|
|
226
262
|
return { decision: "allow", reason: "plan file" };
|
|
227
263
|
}
|
|
228
264
|
|
|
229
265
|
return {
|
|
230
266
|
decision: "deny",
|
|
231
267
|
reason:
|
|
232
|
-
`Plan mode is active.
|
|
268
|
+
`Plan mode is active. Use direct read-only tools (Read, Grep, Glob, List, Search, Skill, TaskOutput, safe read-only Bash), planning tools (AskUserQuestion, TodoWrite/UpdatePlan), or recall-style subagents only. ` +
|
|
269
|
+
`Do not use coding, general-purpose, or fork subagents in plan mode. ` +
|
|
233
270
|
`Write your plan to: ${session.planFilePath}. ` +
|
|
234
|
-
`
|
|
271
|
+
`When ready, read the plan file and include the full current plan text in AskUserQuestion for approval, then call exit_plan_mode after approval.`,
|
|
235
272
|
};
|
|
236
273
|
},
|
|
237
274
|
}));
|
|
@@ -242,16 +279,18 @@ Shell allowlists are easy to get wrong. Start conservative: allow clearly read-o
|
|
|
242
279
|
|
|
243
280
|
## Exit tool
|
|
244
281
|
|
|
245
|
-
In the
|
|
282
|
+
In the mod version, `exit_plan_mode` is not the approval UI. The agent should read the plan file, present the full current plan text with `AskUserQuestion`, then call `exit_plan_mode` only after the user approves.
|
|
283
|
+
|
|
284
|
+
Use `approvalPolicy: "alwaysAsk"` so the final state transition still pauses for human confirmation in unrestricted/yolo mode.
|
|
246
285
|
|
|
247
286
|
```ts
|
|
248
287
|
if (letta.capabilities.tools) {
|
|
249
288
|
disposers.push(letta.tools.register({
|
|
250
289
|
name: "exit_plan_mode",
|
|
251
290
|
description:
|
|
252
|
-
"Exit plan mode only after the plan file has been written, the full plan has been presented with AskUserQuestion, and the user has approved it.",
|
|
291
|
+
"Exit plan mode only after the plan file has been written, the full current plan text has been presented with AskUserQuestion, and the user has approved it.",
|
|
253
292
|
parameters: { type: "object", properties: {}, additionalProperties: false },
|
|
254
|
-
|
|
293
|
+
approvalPolicy: "alwaysAsk",
|
|
255
294
|
parallelSafe: false,
|
|
256
295
|
run(ctx) {
|
|
257
296
|
const session = getSession(ctx.conversation.id);
|
|
@@ -282,4 +321,6 @@ if (letta.capabilities.tools) {
|
|
|
282
321
|
## Notes
|
|
283
322
|
|
|
284
323
|
- Keep `exit_plan_mode` as the final state transition and execution handoff. The approved-plan text in its tool return is useful model context.
|
|
324
|
+
- Plan approval must include the full current plan text in `AskUserQuestion.question`, not just a summary or "does this look right?". After revisions, re-read the file and present the full revised plan again.
|
|
325
|
+
- Keep arbitrary coding subagents denied in plan mode unless the runtime has a true read-only child mode. With the current subagent set, allow only recall-style subagents.
|
|
285
326
|
- If the user renames the plan file, exit logic can use the newest non-empty `~/.letta/plans/*.md` modified after plan mode started, or accept an optional plan path. Keep the user-facing flow normal: write plan file, ask approval, then exit.
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Mod provider recipes
|
|
2
2
|
|
|
3
|
-
Use provider
|
|
3
|
+
Use provider mods when the user wants a **local agent** to use a model provider that is not built into `/connect` and `/model`.
|
|
4
4
|
|
|
5
|
-
Important: provider
|
|
5
|
+
Important: provider mods are local-backend/local-agent only. They register local provider metadata for the TUI, headless local runtime, and desktop listener. They do not add providers for Constellation/cloud agents.
|
|
6
6
|
|
|
7
|
-
For multi-capability
|
|
7
|
+
For multi-capability mods that combine a provider with commands, tools, UI, or state, also read `architecture.md`.
|
|
8
8
|
|
|
9
9
|
## Quick pattern
|
|
10
10
|
|
|
11
11
|
```ts
|
|
12
|
-
// ~/.letta/
|
|
12
|
+
// ~/.letta/mods/kilo.ts
|
|
13
13
|
export default function activate(letta) {
|
|
14
14
|
if (!letta.capabilities.providers) return;
|
|
15
15
|
|
|
@@ -48,10 +48,10 @@ After `/reload`, the provider appears in local `/connect` and desktop Connect mo
|
|
|
48
48
|
|
|
49
49
|
- Always guard with `letta.capabilities.providers`.
|
|
50
50
|
- Prefer `letta.providers.register(...)` over legacy `letta.registerProvider(...)`.
|
|
51
|
-
- Keep provider registration independent from commands/tools/UI/events and `letta.client`; the desktop listener loads provider-only
|
|
51
|
+
- Keep provider registration independent from commands/tools/UI/events and `letta.client`; the desktop listener loads provider-only mods.
|
|
52
52
|
- Do not hardcode real secrets. `apiKey: "ENV_VAR"` resolves `process.env.ENV_VAR` when present, or lets `/connect` save a local key.
|
|
53
53
|
- Use stable lowercase provider ids. Model ids must be unprefixed and must not contain `/`.
|
|
54
|
-
- Set `api` at provider or model level. Common values include `"openai-completions"`, `"openai-responses"`, `"anthropic-messages"`, and `"bedrock-converse-stream"`; check `src/backend/dev/pi-provider-
|
|
54
|
+
- Set `api` at provider or model level. Common values include `"openai-completions"`, `"openai-responses"`, `"anthropic-messages"`, and `"bedrock-converse-stream"`; check `src/backend/dev/pi-provider-mod-types.ts` and pi-ai model types before using uncommon values.
|
|
55
55
|
|
|
56
56
|
## Model metadata
|
|
57
57
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Mod tool recipes
|
|
2
2
|
|
|
3
3
|
Use tools when the agent/model should call a local capability autonomously.
|
|
4
4
|
|
|
5
|
-
For tools that are part of a larger
|
|
5
|
+
For tools that are part of a larger mod with commands, UI, local state, or events, also read `architecture.md`.
|
|
6
6
|
|
|
7
7
|
## Contents
|
|
8
8
|
|
|
@@ -17,6 +17,7 @@ For tools that are part of a larger extension with commands, UI, local state, or
|
|
|
17
17
|
- Description: explain when the model should use it.
|
|
18
18
|
- Parameters: JSON Schema object. Use `additionalProperties: false` when possible.
|
|
19
19
|
- `requiresApproval: false` only for read-only, low-risk local introspection.
|
|
20
|
+
- `approvalPolicy: "alwaysAsk"` only for tools that must pause for human approval even in unrestricted/yolo mode.
|
|
20
21
|
- `parallelSafe: true` only for read-only tools with no shared mutation or long-lived exclusive resource.
|
|
21
22
|
- Use `ctx.cwd` / `ctx.workingDirectory` as the workspace.
|
|
22
23
|
- Use `await ctx.conversation.getHistory()` when a tool needs recent conversation context. It returns the most recent messages in chronological order by default.
|
|
@@ -124,3 +125,20 @@ letta.tools.register({
|
|
|
124
125
|
},
|
|
125
126
|
});
|
|
126
127
|
```
|
|
128
|
+
|
|
129
|
+
## Always-ask tool
|
|
130
|
+
|
|
131
|
+
Use `approvalPolicy: "alwaysAsk"` when a tool represents a human gate rather than a risky operation. Deny rules and permission overlays still win, but unrestricted/yolo mode will not auto-approve it.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
letta.tools.register({
|
|
135
|
+
name: "exit_plan_mode",
|
|
136
|
+
description: "Exit plan mode after the user has reviewed and approved the plan.",
|
|
137
|
+
parameters: { type: "object", properties: {}, additionalProperties: false },
|
|
138
|
+
approvalPolicy: "alwaysAsk",
|
|
139
|
+
parallelSafe: false,
|
|
140
|
+
run(ctx) {
|
|
141
|
+
return "Plan approved. You can now start coding.";
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
```
|