@letta-ai/letta-code 0.27.5 → 0.27.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/letta.js +372 -95
- package/package.json +1 -1
- package/skills/creating-extensions/SKILL.md +6 -2
- package/skills/creating-extensions/references/commands.md +1 -1
- package/skills/creating-extensions/references/events.md +3 -17
- package/skills/creating-extensions/references/permissions.md +90 -0
- package/skills/creating-extensions/references/plan-mode.md +285 -0
- package/skills/creating-extensions/references/providers.md +50 -148
- package/skills/customizing-commands/SKILL.md +2 -2
- package/skills/creating-extensions/references/btw-command.md +0 -106
|
@@ -1,57 +1,12 @@
|
|
|
1
1
|
# Extension provider recipes
|
|
2
2
|
|
|
3
|
-
Use provider extensions when the user wants
|
|
3
|
+
Use provider extensions when the user wants a **local agent** to use a model provider that is not built into `/connect` and `/model`.
|
|
4
4
|
|
|
5
|
-
|
|
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.
|
|
6
6
|
|
|
7
7
|
For multi-capability extensions that combine a provider with commands, tools, UI, or state, also read `architecture.md`.
|
|
8
8
|
|
|
9
|
-
##
|
|
10
|
-
|
|
11
|
-
- When to use
|
|
12
|
-
- Surface behavior and limitations
|
|
13
|
-
- Capability guard
|
|
14
|
-
- Basic API-key provider
|
|
15
|
-
- Config fields
|
|
16
|
-
- Dynamic model discovery
|
|
17
|
-
- Connect behavior
|
|
18
|
-
- Review checklist
|
|
19
|
-
|
|
20
|
-
## When to use
|
|
21
|
-
|
|
22
|
-
Use a provider extension when:
|
|
23
|
-
|
|
24
|
-
- the provider API is supported by the local pi-ai adapter stack; OpenAI-compatible providers usually use `api: "openai-completions"` or `api: "openai-responses"`
|
|
25
|
-
- the user wants the provider to appear in local `/connect` / desktop Connect model providers
|
|
26
|
-
- local `/model` should show static or dynamically listed models from that provider
|
|
27
|
-
- local turns should resolve model handles like `kilo/kilo-code`
|
|
28
|
-
|
|
29
|
-
Do **not** use a provider extension when the user is asking to configure a Constellation/cloud provider. For cloud agents, use the product's normal provider configuration path instead.
|
|
30
|
-
|
|
31
|
-
## Surface behavior and limitations
|
|
32
|
-
|
|
33
|
-
- TUI/headless local agents can load provider extensions along with other extension capabilities.
|
|
34
|
-
- The desktop listener loads provider extensions so desktop's local provider UI can list and connect them.
|
|
35
|
-
- Listener provider loading is provider-only. Do not rely on commands, tools, UI, events, or `letta.client` while registering a provider.
|
|
36
|
-
- Provider extensions should use local data, environment variables, local provider connections, and direct `fetch`/Node APIs. They should not use Constellation-specific APIs to make a local provider work.
|
|
37
|
-
|
|
38
|
-
## Capability guard
|
|
39
|
-
|
|
40
|
-
Always guard provider registration:
|
|
41
|
-
|
|
42
|
-
```ts
|
|
43
|
-
export default function activate(letta) {
|
|
44
|
-
if (!letta.capabilities.providers) return;
|
|
45
|
-
|
|
46
|
-
return letta.providers.register("kilo", {
|
|
47
|
-
// provider config
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
`letta.registerProvider(id, config)` is also supported, but prefer `letta.providers.register(...)` so the capability being used is explicit.
|
|
53
|
-
|
|
54
|
-
## Basic API-key provider
|
|
9
|
+
## Quick pattern
|
|
55
10
|
|
|
56
11
|
```ts
|
|
57
12
|
// ~/.letta/extensions/kilo.ts
|
|
@@ -63,7 +18,8 @@ export default function activate(letta) {
|
|
|
63
18
|
description: "Connect to Kilo's OpenAI-compatible API",
|
|
64
19
|
api: "openai-completions",
|
|
65
20
|
baseUrl: "https://api.kilo.example/v1",
|
|
66
|
-
apiKey: "KILO_API_KEY",
|
|
21
|
+
apiKey: "KILO_API_KEY", // env var name, not the raw secret
|
|
22
|
+
authHeader: true,
|
|
67
23
|
models: [
|
|
68
24
|
{
|
|
69
25
|
id: "kilo-code",
|
|
@@ -73,136 +29,82 @@ export default function activate(letta) {
|
|
|
73
29
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
74
30
|
contextWindow: 128000,
|
|
75
31
|
maxTokens: 8192,
|
|
32
|
+
compat: {
|
|
33
|
+
supportsDeveloperRole: false,
|
|
34
|
+
supportsReasoningEffort: false,
|
|
35
|
+
},
|
|
76
36
|
},
|
|
77
37
|
],
|
|
78
38
|
connect: {
|
|
79
|
-
fields: [
|
|
80
|
-
{ key: "apiKey", label: "Kilo API Key", secret: true },
|
|
81
|
-
],
|
|
39
|
+
fields: [{ key: "apiKey", label: "Kilo API Key", secret: true }],
|
|
82
40
|
},
|
|
83
41
|
});
|
|
84
42
|
}
|
|
85
43
|
```
|
|
86
44
|
|
|
87
|
-
After `/reload`,
|
|
45
|
+
After `/reload`, the provider appears in local `/connect` and desktop Connect model providers. Model handles are `<provider-id>/<model-id>`, for example `kilo/kilo-code`.
|
|
88
46
|
|
|
89
|
-
##
|
|
47
|
+
## Key rules
|
|
90
48
|
|
|
91
|
-
|
|
49
|
+
- Always guard with `letta.capabilities.providers`.
|
|
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.
|
|
52
|
+
- Do not hardcode real secrets. `apiKey: "ENV_VAR"` resolves `process.env.ENV_VAR` when present, or lets `/connect` save a local key.
|
|
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.
|
|
92
55
|
|
|
93
|
-
|
|
94
|
-
{
|
|
95
|
-
name?: string;
|
|
96
|
-
description?: string;
|
|
97
|
-
api?: string; // common: "openai-completions", "openai-responses", "anthropic-messages", "bedrock-converse-stream"
|
|
98
|
-
baseUrl?: string;
|
|
99
|
-
apiKey?: string; // env var name to resolve, e.g. "KILO_API_KEY"
|
|
100
|
-
headers?: Record<string, string>; // values resolve through process.env when present
|
|
101
|
-
authHeader?: boolean; // add Authorization: Bearer <apiKey>
|
|
102
|
-
models?: Array<model>;
|
|
103
|
-
listModels?: (connection) => Promise<Array<model>> | Array<model>;
|
|
104
|
-
connect?: boolean | { fields?: Array<field> };
|
|
105
|
-
}
|
|
106
|
-
```
|
|
56
|
+
## Model metadata
|
|
107
57
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
Model entries must include useful local runtime metadata:
|
|
58
|
+
Each model needs enough local runtime metadata for selection and context display:
|
|
111
59
|
|
|
112
60
|
```ts
|
|
113
61
|
{
|
|
114
62
|
id: "model-id-without-provider-prefix",
|
|
115
63
|
name: "Display Name",
|
|
116
|
-
api?: "openai-completions",
|
|
117
64
|
reasoning: false,
|
|
118
|
-
input: ["text"],
|
|
65
|
+
input: ["text"], // or ["text", "image"]
|
|
119
66
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
120
67
|
contextWindow: 128000,
|
|
121
68
|
maxTokens: 8192,
|
|
122
|
-
|
|
69
|
+
// Optional: compat: { supportsDeveloperRole: false, supportsReasoningEffort: false }
|
|
123
70
|
}
|
|
124
71
|
```
|
|
125
72
|
|
|
126
|
-
Do not set `model.baseUrl` when all models use the provider-level URL. Model-level `baseUrl` overrides the connected provider base URL, so `/connect` base URL overrides
|
|
73
|
+
Do not set `model.baseUrl` when all models use the provider-level URL. Model-level `baseUrl` overrides the connected provider base URL, so `/connect` base URL overrides are ignored. Use it only when a specific model intentionally needs a different endpoint.
|
|
74
|
+
|
|
75
|
+
## Connect fields
|
|
76
|
+
|
|
77
|
+
- `connect: undefined` / `true` uses default API key + base URL fields.
|
|
78
|
+
- `connect: { fields: [...] }` customizes local `/connect` / desktop fields.
|
|
79
|
+
- `connect: false` hides the provider from `/connect`; use only when credentials come entirely from env/local code.
|
|
80
|
+
- Custom fields are currently required. TUI pre-fills non-secret placeholders for convenience, but placeholders are not backend/protocol defaults.
|
|
81
|
+
- If the provider has a normal fixed `baseUrl`, set provider-level `baseUrl` and omit `baseUrl` from `connect.fields`; the local runtime uses the provider-level URL after the API key is saved.
|
|
82
|
+
- Include `{ key: "baseUrl", ... }` only when the user must enter or review/override the endpoint during connect.
|
|
127
83
|
|
|
128
84
|
## Dynamic model discovery
|
|
129
85
|
|
|
130
|
-
Use `listModels(connection)` when the provider exposes a models endpoint or the model list depends on
|
|
86
|
+
Use `listModels(connection)` only when the provider exposes a models endpoint or the model list depends on credentials:
|
|
131
87
|
|
|
132
88
|
```ts
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
api: "openai-completions",
|
|
139
|
-
baseUrl: "https://api.kilo.example/v1",
|
|
140
|
-
apiKey: "KILO_API_KEY",
|
|
141
|
-
async listModels(connection) {
|
|
142
|
-
const response = await fetch(`${connection.baseUrl}/models`, {
|
|
143
|
-
headers: {
|
|
144
|
-
Authorization: `Bearer ${connection.apiKey}`,
|
|
145
|
-
...connection.headers,
|
|
146
|
-
},
|
|
147
|
-
});
|
|
148
|
-
if (!response.ok) {
|
|
149
|
-
throw new Error(`Kilo model list failed: ${response.status}`);
|
|
150
|
-
}
|
|
151
|
-
const body = await response.json();
|
|
152
|
-
return body.data.map((model) => ({
|
|
153
|
-
id: model.id,
|
|
154
|
-
name: model.id,
|
|
155
|
-
reasoning: false,
|
|
156
|
-
input: ["text"],
|
|
157
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
158
|
-
contextWindow: 128000,
|
|
159
|
-
maxTokens: 8192,
|
|
160
|
-
}));
|
|
89
|
+
async listModels(connection) {
|
|
90
|
+
const response = await fetch(`${connection.baseUrl}/models`, {
|
|
91
|
+
headers: {
|
|
92
|
+
Authorization: `Bearer ${connection.apiKey}`,
|
|
93
|
+
...connection.headers,
|
|
161
94
|
},
|
|
162
95
|
});
|
|
96
|
+
if (!response.ok) throw new Error(`Model list failed: ${response.status}`);
|
|
97
|
+
const body = await response.json();
|
|
98
|
+
return body.data.map((model) => ({
|
|
99
|
+
id: model.id,
|
|
100
|
+
name: model.id,
|
|
101
|
+
reasoning: false,
|
|
102
|
+
input: ["text"],
|
|
103
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
104
|
+
contextWindow: 128000,
|
|
105
|
+
maxTokens: 8192,
|
|
106
|
+
}));
|
|
163
107
|
}
|
|
164
108
|
```
|
|
165
109
|
|
|
166
|
-
`connection`
|
|
167
|
-
|
|
168
|
-
```ts
|
|
169
|
-
{
|
|
170
|
-
id: string;
|
|
171
|
-
providerName: string;
|
|
172
|
-
baseUrl?: string;
|
|
173
|
-
apiKey?: string;
|
|
174
|
-
headers?: Record<string, string>;
|
|
175
|
-
}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
Keep dynamic listing lightweight and fail with short actionable errors. If listing is flaky, provide a static `models` fallback instead.
|
|
179
|
-
|
|
180
|
-
## Connect behavior
|
|
181
|
-
|
|
182
|
-
- `connect: undefined` or `connect: true` uses default API key + base URL fields.
|
|
183
|
-
- `connect: { fields: [...] }` customizes local `/connect` / desktop fields.
|
|
184
|
-
- `connect: false` hides the provider from `/connect`; use only when credentials come entirely from environment variables or local code.
|
|
185
|
-
- `apiKey: "ENV_VAR_NAME"` resolves from `process.env.ENV_VAR_NAME` when available. Do not hardcode real secrets.
|
|
186
|
-
- Custom fields are currently required. `placeholder` is display-only, not a default value.
|
|
187
|
-
- If the provider has a normal fixed `baseUrl`, set provider-level `baseUrl` and omit `baseUrl` from `connect.fields`; the local runtime will use the provider-level URL after the API key is saved.
|
|
188
|
-
- Include `{ key: "baseUrl", ... }` only when the user must enter or override the endpoint during connect.
|
|
189
|
-
|
|
190
|
-
Default custom fields use these keys when possible because the local provider store understands them:
|
|
191
|
-
|
|
192
|
-
```ts
|
|
193
|
-
{ key: "apiKey", label: "API Key", secret: true }
|
|
194
|
-
{ key: "baseUrl", label: "Base URL" }
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
## Review checklist
|
|
198
|
-
|
|
199
|
-
- The extension says or implies the provider is local-agent only when reporting back to the user.
|
|
200
|
-
- `letta.capabilities.providers` is checked before registration.
|
|
201
|
-
- Provider id is stable, lowercase, and suitable as the model-handle prefix.
|
|
202
|
-
- Model ids are unprefixed; Letta Code exposes them as `<provider-id>/<model-id>`.
|
|
203
|
-
- `contextWindow`, `maxTokens`, `input`, and `reasoning` metadata are accurate enough for local selection and context display.
|
|
204
|
-
- Model-level `baseUrl` is omitted unless the model intentionally needs a different endpoint from the provider/connection.
|
|
205
|
-
- `connect.fields` contains only fields the user must type; placeholders are not treated as defaults.
|
|
206
|
-
- Secrets are read from environment/local provider connections, not hardcoded.
|
|
207
|
-
- Provider registration does not depend on commands/tools/UI/events or `letta.client`.
|
|
208
|
-
- The user is told to run `/reload`, then configure the provider through local `/connect` or desktop Connect model providers.
|
|
110
|
+
`connection` has `{ id, providerName, baseUrl?, apiKey?, headers? }`. Keep dynamic listing lightweight; if it is flaky, prefer static `models`.
|
|
@@ -5,7 +5,7 @@ description: Creates, edits, and enables Letta Code extension-provided slash com
|
|
|
5
5
|
|
|
6
6
|
# Customizing Commands
|
|
7
7
|
|
|
8
|
-
Use this as the command-specific entrypoint for local extension slash commands. For broader extension work, recipes live in `../creating-extensions/references/commands.md`, `../creating-extensions/references/architecture.md`, `../creating-extensions/references/ui.md`, and `../creating-extensions/references/
|
|
8
|
+
Use this as the command-specific entrypoint for local extension slash commands. For broader extension work, recipes live in `../creating-extensions/references/commands.md`, `../creating-extensions/references/architecture.md`, `../creating-extensions/references/ui.md`, and `../creating-extensions/references/plan-mode.md`.
|
|
9
9
|
|
|
10
10
|
Extension files live in:
|
|
11
11
|
|
|
@@ -87,4 +87,4 @@ type ExtensionCommandResult =
|
|
|
87
87
|
- Simple output command, panel command, busy-safe conversation command: `../creating-extensions/references/commands.md`
|
|
88
88
|
- Complex command architecture, state, cleanup: `../creating-extensions/references/architecture.md`
|
|
89
89
|
- Panel/status UI patterns: `../creating-extensions/references/ui.md`
|
|
90
|
-
-
|
|
90
|
+
- Worked plan-mode command/tool composition: `../creating-extensions/references/plan-mode.md`
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
# `/btw` side-question extension example
|
|
2
|
-
|
|
3
|
-
This example runs while the main agent is busy because it forks the scoped conversation, streams a response in the fork, renders progress in a panel when panels are available, and returns `{ type: "handled" }` immediately.
|
|
4
|
-
|
|
5
|
-
```ts
|
|
6
|
-
export default function activate(letta) {
|
|
7
|
-
if (!letta.capabilities.commands) return;
|
|
8
|
-
|
|
9
|
-
function createOtid() {
|
|
10
|
-
return (
|
|
11
|
-
globalThis.crypto?.randomUUID?.() ??
|
|
12
|
-
`btw-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
13
|
-
);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function appendAssistantText(chunk, parts) {
|
|
17
|
-
if (chunk.message_type !== "assistant_message") return;
|
|
18
|
-
const content = chunk.content;
|
|
19
|
-
if (typeof content === "string") {
|
|
20
|
-
parts.push(content);
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
if (Array.isArray(content)) {
|
|
24
|
-
for (const part of content) {
|
|
25
|
-
if (part && typeof part === "object" && "text" in part) {
|
|
26
|
-
parts.push(String(part.text));
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
if (content && typeof content === "object" && "text" in content) {
|
|
32
|
-
parts.push(String(content.text));
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function openPanelOrNull(content) {
|
|
37
|
-
if (!letta.capabilities.ui.panels) return null;
|
|
38
|
-
return letta.ui.openPanel({ id: "btw", content });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return letta.commands.register({
|
|
42
|
-
id: "btw",
|
|
43
|
-
description: "Ask a side question in a forked conversation",
|
|
44
|
-
args: "<question>",
|
|
45
|
-
runWhenBusy: true,
|
|
46
|
-
showInTranscript: false,
|
|
47
|
-
run(ctx) {
|
|
48
|
-
const question = ctx.args.trim();
|
|
49
|
-
if (!question) {
|
|
50
|
-
const panel = openPanelOrNull(["/btw", "Usage: /btw <question>"]);
|
|
51
|
-
if (panel) setTimeout(() => panel.close(), 5_000);
|
|
52
|
-
return { type: "handled" };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const panel = openPanelOrNull([`/btw ${question}`, "..."]);
|
|
56
|
-
|
|
57
|
-
void (async () => {
|
|
58
|
-
try {
|
|
59
|
-
const forked = await ctx.conversation.fork({ hidden: true });
|
|
60
|
-
const stream = await forked.sendMessageStream(
|
|
61
|
-
[
|
|
62
|
-
{
|
|
63
|
-
role: "user",
|
|
64
|
-
content: `${question}\n\nAnswer briefly in 1-3 short sentences.`,
|
|
65
|
-
otid: createOtid(),
|
|
66
|
-
},
|
|
67
|
-
],
|
|
68
|
-
{
|
|
69
|
-
overrideModel: ctx.model.id ?? undefined,
|
|
70
|
-
workingDirectory: ctx.cwd,
|
|
71
|
-
},
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
const parts = [];
|
|
75
|
-
for await (const chunk of stream) {
|
|
76
|
-
appendAssistantText(chunk, parts);
|
|
77
|
-
panel?.update({
|
|
78
|
-
content: [`/btw ${question}`, parts.join("") || "..."],
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
panel?.update({
|
|
83
|
-
content: [
|
|
84
|
-
`done /btw ${question}`,
|
|
85
|
-
parts.join("").trim() || "No response.",
|
|
86
|
-
],
|
|
87
|
-
});
|
|
88
|
-
if (panel) setTimeout(() => panel.close(), 10_000);
|
|
89
|
-
} catch (error) {
|
|
90
|
-
panel?.update({
|
|
91
|
-
content: [
|
|
92
|
-
`error /btw ${question}`,
|
|
93
|
-
error instanceof Error ? error.message : String(error),
|
|
94
|
-
],
|
|
95
|
-
});
|
|
96
|
-
if (panel) setTimeout(() => panel.close(), 15_000);
|
|
97
|
-
}
|
|
98
|
-
})();
|
|
99
|
-
|
|
100
|
-
return { type: "handled" };
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
Add custom borders, right alignment, wrapping, or history only if the user asks for that polish.
|