@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.
Files changed (41) hide show
  1. package/README.md +2 -2
  2. package/dist/app-server-client.js +387 -0
  3. package/dist/app-server-client.js.map +10 -0
  4. package/dist/types/app-server-client.d.ts +99 -0
  5. package/dist/types/app-server-client.d.ts.map +1 -0
  6. package/dist/types/types/app-server-protocol.d.ts +3 -0
  7. package/dist/types/types/app-server-protocol.d.ts.map +1 -0
  8. package/dist/types/types/protocol.d.ts.map +1 -0
  9. package/dist/types/types/protocol_v2.d.ts +2277 -0
  10. package/dist/types/types/protocol_v2.d.ts.map +1 -0
  11. package/letta.js +22835 -19810
  12. package/package.json +12 -2
  13. package/scripts/check-bundled-skill-scripts.js +169 -0
  14. package/scripts/check-test-coverage.cjs +1 -1
  15. package/scripts/check.js +1 -0
  16. package/scripts/run-unit-tests.cjs +1 -1
  17. package/skills/converting-mcps-to-skills/SKILL.md +1 -12
  18. package/skills/converting-mcps-to-skills/scripts/mcp-stdio.ts +192 -57
  19. package/skills/{creating-extensions → creating-mods}/SKILL.md +29 -29
  20. package/skills/{creating-extensions → creating-mods}/references/architecture.md +9 -9
  21. package/skills/{creating-extensions → creating-mods}/references/commands.md +10 -10
  22. package/skills/{creating-extensions → creating-mods}/references/events.md +10 -10
  23. package/skills/{creating-extensions → creating-mods}/references/permissions.md +3 -3
  24. package/skills/{creating-extensions → creating-mods}/references/plan-mode.md +72 -31
  25. package/skills/{creating-extensions → creating-mods}/references/providers.md +7 -7
  26. package/skills/{creating-extensions → creating-mods}/references/tools.md +20 -2
  27. package/skills/{creating-extensions → creating-mods}/references/ui.md +4 -4
  28. package/skills/creating-skills/scripts/validate-skill.ts +129 -5
  29. package/skills/customizing-commands/SKILL.md +18 -18
  30. package/skills/customizing-statusline/SKILL.md +11 -11
  31. package/skills/customizing-statusline/references/api.md +8 -8
  32. package/skills/customizing-statusline/references/examples.md +1 -1
  33. package/skills/customizing-statusline/references/migration.md +1 -1
  34. package/skills/editing-letta-code-desktop-preferences/SKILL.md +67 -0
  35. package/skills/image-generation/SKILL.md +120 -0
  36. package/skills/modifying-the-harness/SKILL.md +21 -2
  37. package/skills/modifying-the-harness/scripts/add_permission.py +2 -1
  38. package/skills/modifying-the-harness/scripts/show_config.py +4 -3
  39. package/dist/types/protocol.d.ts.map +0 -1
  40. package/skills/converting-mcps-to-skills/scripts/package.json +0 -13
  41. /package/dist/types/{protocol.d.ts → types/protocol.d.ts} +0 -0
@@ -1,42 +1,42 @@
1
1
  ---
2
- name: creating-extensions
3
- description: Creates and edits trusted local Letta Code extensions, 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 an extension, add an agent-callable tool, add a slash command, add a local provider/model adapter, transform turns, react to app events, or add lightweight extension UI outside the dedicated /statusline flow.
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 Extensions
6
+ # Creating Mods
7
7
 
8
- Use this skill to create or update trusted global Letta Code extensions in:
8
+ Use this skill to create or update trusted global Letta Code mods in:
9
9
 
10
10
  ```text
11
- ~/.letta/extensions/
11
+ ~/.letta/mods/
12
12
  ```
13
13
 
14
- Extensions are trusted local apps for Letta Code. They add small composable capabilities through extension APIs, not by importing app internals. Prefer scoped handles (`ctx.conversation`, `ctx.cwd`, `ctx.agent`, `letta.getContext()`) and guard optional UI with `letta.capabilities`.
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 extensions for local provider discovery. Always guard optional capabilities.
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 | Extension tool |
23
- | User wants `/foo` to send a prompt or run local UI logic | Extension command |
24
- | Slash command represents a reusable agent workflow | Skill + thin extension command |
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 extension (local agents only) |
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/extensions/` for related files.
38
- 2. Preserve unrelated extension code. Prefer a focused new file if merging would be messy.
39
- 3. Choose the extension shape and load only the needed recipe:
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 extensions, also read `references/architecture.md`.
48
- 5. Write a single-file extension unless the user asks for something larger.
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 an extension breaks startup or command handling, recover with `letta --no-extensions` or `LETTA_DISABLE_EXTENSIONS=1 letta`.
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 extension shape
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 extension files.
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 extension 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.
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 extension diagnostics at:
102
+ Agents can inspect local mod diagnostics at:
103
103
 
104
104
  ```text
105
- ~/.letta/extensions/diagnostics/latest.json
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 extensions.
111
- - Custom provider extensions are local-backend/local-agent only. They do not add providers for Constellation/cloud agents.
112
- - Provider extensions may run in a provider-only listener context; keep provider registration independent from commands/tools/UI and guard everything else.
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; extensions activate on app start and `/reload`.
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 extensions
122
+ ## Pre-flight checklist for complex mods
123
123
 
124
124
  Before finishing, verify:
125
125
 
126
- - The extension has one clear owner/file and does not mix unrelated features.
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 extensions are capability-guarded and clearly documented as local-agent only.
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
- # Extension architecture patterns
1
+ # Mod architecture patterns
2
2
 
3
- Use this reference for non-trivial extensions: multiple capabilities, local state, timers, background model work, or UI.
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
- An extension is trusted local code that registers capabilities during activation and cleans them up on reload/shutdown. Keep the public surface small:
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 extension API does not expose a capability yet, avoid reaching around it.
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 extension 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.
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/extensions/`, for example:
99
+ For small persistent state, use a clearly named file under `~/.letta/mods/`, for example:
100
100
 
101
101
  ```text
102
- ~/.letta/extensions/my-extension.state.json
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 extension engine is alive. Always clear them:
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
- - Extension output is concise and actionable.
150
+ - Mod output is concise and actionable.
@@ -1,8 +1,8 @@
1
- # Extension command recipes
1
+ # Mod command recipes
2
2
 
3
3
  Use commands when the human explicitly invokes `/foo`.
4
4
 
5
- For complex command-driven extensions with panels, timers, local state, or background model work, also read `architecture.md`.
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 | Extension command |
21
- | `/foo` starts a complex reusable workflow | Skill + thin extension command |
22
- | Model should call the capability by itself | Extension tool |
23
- | Command needs transient UI while doing local work | Extension command + panel |
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 extensions. Do this intentionally and keep recovery in mind: start with `--no-extensions` or `LETTA_DISABLE_EXTENSIONS=1` if an override breaks command handling.
33
- - Duplicate extension command IDs fail unless `override: true` is intentional.
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 extension command context",
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 extension that combines commands, tools, events, permissions, and local state, see `plan-mode.md`.
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
- # Extension event recipes
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 extensions with state, timers, panels, or background model work, also read `architecture.md`.
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 extension events to replace settings-based hooks. Existing hooks still own blocking decisions and model feedback injection until each event has a typed return contract.
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 extensions:
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, extension tools, and external tools executed through the local tool manager. It runs after permission/approval classification and before `PreToolUse` hooks, so trusted local extensions can change the actual executed arguments after the approval UI has already classified the original request. Extension permission overlays are rechecked after `tool_start` on the final args.
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 an extension diagnostic.
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 extension point: it can rewrite commands, file paths, and other tool inputs before execution. Keep transforms focused and unsurprising.
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 an extension diagnostic.
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 extension point: it can rewrite user messages, approval results, and ordering. Keep transforms focused and unsurprising.
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 extension diagnostics.
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
- # Extension permission recipes
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 extensions:
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 extension overlays. Extension overlays can override normal auto-allow/default approval behavior, including unrestricted/yolo mode.
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 extension example
1
+ # Plan mode mod example
2
2
 
3
- Use this as the canonical multi-capability extension 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 extension APIs.
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 extensions self-contained and avoid importing Letta Code internals.
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/extensions/`, keyed by conversation ID:
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", "extensions", "plan-mode.state.json");
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", "extensions"), { recursive: true });
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 AskUserQuestion if you need to clarify the approach
103
- 5. Design a concrete implementation strategy
104
- 6. When ready, write the plan to the plan file, use AskUserQuestion to present the full plan for approval, and call exit_plan_mode after the user approves
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. When the plan is complete, read the plan file and present the full plan to the user with AskUserQuestion. The question should offer at least "Approve" and "Revise" options.
163
- 5. If the user approves, call exit_plan_mode immediately. If the user asks to revise, stay in plan mode and update the plan file.
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
- "list_directory",
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
- "search_file_content",
202
+ "searchfilecontent",
203
+ "searchfiles",
195
204
  "skill",
196
205
  "taskoutput",
197
- "update_plan",
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(/[\s-]/g, "").toLowerCase();
221
+ return toolName.replace(/[^a-z0-9]/gi, "").toLowerCase();
203
222
  }
204
223
 
205
224
  function isReadOnlyToolName(toolName) {
206
- const raw = toolName.toLowerCase();
207
- return readOnlyToolNames.has(raw) || readOnlyToolNames.has(normalizedToolName(toolName));
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
- if (isReadOnlyToolName(event.toolName)) return { decision: "allow" };
225
- if (isPlanFileWrite(event.toolName, event.args, event.workingDirectory || event.cwd)) {
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. You can only use read-only tools (Read, Grep, Glob, etc.) and write to the plan file. ` +
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
- `Use AskUserQuestion when your plan is ready for user approval, then call exit_plan_mode after approval.`,
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 extension version, `exit_plan_mode` is not the approval UI. The agent should present the plan with `AskUserQuestion` first, then call `exit_plan_mode` only after the user approves.
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
- requiresApproval: false,
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
- # Extension provider recipes
1
+ # Mod provider recipes
2
2
 
3
- Use provider extensions when the user wants a **local agent** to use a model provider that is not built into `/connect` and `/model`.
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 extensions 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.
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 extensions that combine a provider with commands, tools, UI, or state, also read `architecture.md`.
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/extensions/kilo.ts
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 extensions.
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-extension-types.ts` and pi-ai model types before using uncommon values.
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
- # Extension tool recipes
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 extension with commands, UI, local state, or events, also read `architecture.md`.
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
+ ```