feat: add client side skills (#1320)
Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
101
src/agent/clientSkills.ts
Normal file
101
src/agent/clientSkills.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { join } from "node:path";
|
||||
import type { MessageCreateParams as ConversationMessageCreateParams } from "@letta-ai/letta-client/resources/conversations/messages";
|
||||
import { getSkillSources, getSkillsDirectory } from "./context";
|
||||
import {
|
||||
compareSkills,
|
||||
discoverSkills,
|
||||
SKILLS_DIR,
|
||||
type Skill,
|
||||
type SkillDiscoveryError,
|
||||
type SkillSource,
|
||||
} from "./skills";
|
||||
|
||||
export type ClientSkill = NonNullable<
|
||||
ConversationMessageCreateParams["client_skills"]
|
||||
>[number];
|
||||
|
||||
export interface BuildClientSkillsPayloadOptions {
|
||||
agentId?: string;
|
||||
skillsDirectory?: string | null;
|
||||
skillSources?: SkillSource[];
|
||||
discoverSkillsFn?: typeof discoverSkills;
|
||||
logger?: (message: string) => void;
|
||||
}
|
||||
|
||||
export interface BuildClientSkillsPayloadResult {
|
||||
clientSkills: NonNullable<ConversationMessageCreateParams["client_skills"]>;
|
||||
skillPathById: Record<string, string>;
|
||||
errors: SkillDiscoveryError[];
|
||||
}
|
||||
|
||||
function toClientSkill(skill: Skill): ClientSkill {
|
||||
return {
|
||||
name: skill.id,
|
||||
description: skill.description,
|
||||
location: skill.path,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSkillDiscoveryContext(
|
||||
options: BuildClientSkillsPayloadOptions,
|
||||
): {
|
||||
skillsDirectory: string;
|
||||
skillSources: SkillSource[];
|
||||
} {
|
||||
const skillsDirectory =
|
||||
options.skillsDirectory ??
|
||||
getSkillsDirectory() ??
|
||||
join(process.cwd(), SKILLS_DIR);
|
||||
const skillSources = options.skillSources ?? getSkillSources();
|
||||
return { skillsDirectory, skillSources };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build `client_skills` payload for conversations.messages.create.
|
||||
*
|
||||
* This discovers client-side skills using the same source selection rules as the
|
||||
* Skill tool and headless startup flow, then converts them into the server-facing
|
||||
* schema expected by the API. Ordering is deterministic by skill id.
|
||||
*/
|
||||
export async function buildClientSkillsPayload(
|
||||
options: BuildClientSkillsPayloadOptions = {},
|
||||
): Promise<BuildClientSkillsPayloadResult> {
|
||||
const { skillsDirectory, skillSources } =
|
||||
resolveSkillDiscoveryContext(options);
|
||||
const discoverSkillsFn = options.discoverSkillsFn ?? discoverSkills;
|
||||
|
||||
try {
|
||||
const discovery = await discoverSkillsFn(skillsDirectory, options.agentId, {
|
||||
sources: skillSources,
|
||||
});
|
||||
const sortedSkills = [...discovery.skills].sort(compareSkills);
|
||||
|
||||
return {
|
||||
clientSkills: sortedSkills.map(toClientSkill),
|
||||
skillPathById: Object.fromEntries(
|
||||
sortedSkills
|
||||
.filter(
|
||||
(skill) => typeof skill.path === "string" && skill.path.length > 0,
|
||||
)
|
||||
.map((skill) => [skill.id, skill.path]),
|
||||
),
|
||||
errors: discovery.errors,
|
||||
};
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: `Unknown error: ${String(error)}`;
|
||||
options.logger?.(`Failed to build client_skills payload: ${message}`);
|
||||
return {
|
||||
clientSkills: [],
|
||||
skillPathById: {},
|
||||
errors: [
|
||||
{
|
||||
path: skillsDirectory,
|
||||
message,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
ApprovalCreate,
|
||||
LettaStreamingResponse,
|
||||
} from "@letta-ai/letta-client/resources/agents/messages";
|
||||
import type { MessageCreateParams as ConversationMessageCreateParams } from "@letta-ai/letta-client/resources/conversations/messages";
|
||||
import {
|
||||
type ClientTool,
|
||||
captureToolExecutionContext,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
normalizeOutgoingApprovalMessages,
|
||||
} from "./approval-result-normalization";
|
||||
import { getClient } from "./client";
|
||||
import { buildClientSkillsPayload } from "./clientSkills";
|
||||
|
||||
const streamRequestStartTimes = new WeakMap<object, number>();
|
||||
const streamToolContextIds = new WeakMap<object, string>();
|
||||
@@ -60,6 +62,9 @@ export function buildConversationMessagesCreateRequestBody(
|
||||
messages: Array<MessageCreate | ApprovalCreate>,
|
||||
opts: SendMessageStreamOptions = { streamTokens: true, background: true },
|
||||
clientTools: ClientTool[],
|
||||
clientSkills: NonNullable<
|
||||
ConversationMessageCreateParams["client_skills"]
|
||||
> = [],
|
||||
) {
|
||||
const isDefaultConversation = conversationId === "default";
|
||||
if (isDefaultConversation && !opts.agentId) {
|
||||
@@ -77,6 +82,7 @@ export function buildConversationMessagesCreateRequestBody(
|
||||
stream_tokens: opts.streamTokens ?? true,
|
||||
include_pings: true,
|
||||
background: opts.background ?? true,
|
||||
client_skills: clientSkills,
|
||||
client_tools: clientTools,
|
||||
include_compaction_messages: true,
|
||||
...(isDefaultConversation ? { agent_id: opts.agentId } : {}),
|
||||
@@ -113,6 +119,10 @@ export async function sendMessageStream(
|
||||
// This prevents sending messages with stale tools during a switch
|
||||
await waitForToolsetReady();
|
||||
const { clientTools, contextId } = captureToolExecutionContext();
|
||||
const { clientSkills, errors: clientSkillDiscoveryErrors } =
|
||||
await buildClientSkillsPayload({
|
||||
agentId: opts.agentId,
|
||||
});
|
||||
|
||||
const resolvedConversationId = conversationId;
|
||||
const requestBody = buildConversationMessagesCreateRequestBody(
|
||||
@@ -120,12 +130,30 @@ export async function sendMessageStream(
|
||||
messages,
|
||||
opts,
|
||||
clientTools,
|
||||
clientSkills,
|
||||
);
|
||||
|
||||
if (process.env.DEBUG) {
|
||||
console.log(
|
||||
`[DEBUG] sendMessageStream: conversationId=${conversationId}, agentId=${opts.agentId ?? "(none)"}`,
|
||||
);
|
||||
|
||||
const formattedSkills = clientSkills.map(
|
||||
(skill) => `${skill.name} (${skill.location})`,
|
||||
);
|
||||
console.log(
|
||||
`[DEBUG] sendMessageStream: client_skills (${clientSkills.length}) ${
|
||||
formattedSkills.length > 0 ? formattedSkills.join(", ") : "(none)"
|
||||
}`,
|
||||
);
|
||||
|
||||
if (clientSkillDiscoveryErrors.length > 0) {
|
||||
for (const error of clientSkillDiscoveryErrors) {
|
||||
console.warn(
|
||||
`[DEBUG] sendMessageStream: client_skills discovery error at ${error.path}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const extraHeaders: Record<string, string> = {};
|
||||
|
||||
@@ -132,4 +132,4 @@ assistant: Clients are marked as failed in the `connectToServer` function in src
|
||||
</example>
|
||||
|
||||
# Skills
|
||||
- /<skill-name> (e.g., /commit) is shorthand for users to invoke a skill. When executed, the skill gets expanded to a full prompt. Use the Skill tool to execute them. IMPORTANT: Only use Skill for skills listed in system-reminder messages in the conversation - do not guess or use built-in CLI commands.
|
||||
- /<skill-name> (e.g., /commit) is shorthand for users to invoke a skill. When executed, the skill gets expanded to a full prompt. Use the Skill tool to execute them. IMPORTANT: Only use Skill for skills listed in the available skills context in the conversation - do not guess or use built-in CLI commands.
|
||||
@@ -114,4 +114,4 @@ Unless the user explicitly asks for a plan, asks a question about the code, is b
|
||||
|
||||
## Skills
|
||||
|
||||
- /<skill-name> (e.g., /commit) is shorthand for users to invoke a skill. When executed, the skill gets expanded to a full prompt. Use the Skill tool to execute them. IMPORTANT: Only use Skill for skills listed in system-reminder messages in the conversation - do not guess or use built-in CLI commands.
|
||||
- /<skill-name> (e.g., /commit) is shorthand for users to invoke a skill. When executed, the skill gets expanded to a full prompt. Use the Skill tool to execute them. IMPORTANT: Only use Skill for skills listed in the available skills context in the conversation - do not guess or use built-in CLI commands.
|
||||
|
||||
@@ -79,4 +79,4 @@ IT IS CRITICAL TO FOLLOW THESE GUIDELINES TO AVOID EXCESSIVE TOKEN CONSUMPTION.
|
||||
- **Remembering Facts:** Use the memory tools available to you to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information. If unsure whether to save something, you can ask the user, "Should I remember that for you?
|
||||
|
||||
# Skills
|
||||
- /<skill-name> (e.g., /commit) is shorthand for users to invoke a skill. When executed, the skill gets expanded to a full prompt. Use the Skill tool to execute them. IMPORTANT: Only use Skill for skills listed in system-reminder messages in the conversation - do not guess or use built-in CLI commands.
|
||||
- /<skill-name> (e.g., /commit) is shorthand for users to invoke a skill. When executed, the skill gets expanded to a full prompt. Use the Skill tool to execute them. IMPORTANT: Only use Skill for skills listed in the available skills context in the conversation - do not guess or use built-in CLI commands.
|
||||
@@ -11,7 +11,7 @@ Your goal is to guide the user through a **focused, collaborative workflow** to
|
||||
|
||||
## 1. Invoke the creating-skills Skill (if available)
|
||||
|
||||
1. Check the available skills listed in system-reminder messages in the conversation.
|
||||
1. Check the available skills listed in the current prompt context in the conversation.
|
||||
2. If a `creating-skills` skill is available, invoke it using the `Skill` tool:
|
||||
- Call the `Skill` tool with: `skill: "creating-skills"`
|
||||
3. If invocation fails or the skill is not available, continue using your own judgment based on these instructions.
|
||||
|
||||
@@ -17,4 +17,4 @@ When the user directly asks about Letta Code (eg 'can Letta Code do...', 'does L
|
||||
When running in Letta Code, shell tools provide `AGENT_ID`: your current agent ID.
|
||||
|
||||
# Skills
|
||||
- /<skill-name> (e.g., /commit) is shorthand for users to invoke a skill. When executed, the skill gets expanded to a full prompt. Use the Skill tool to execute them. IMPORTANT: Only use Skill for skills listed in system-reminder messages in the conversation - do not guess or use built-in CLI commands.
|
||||
- /<skill-name> (e.g., /commit) is shorthand for users to invoke a skill. When executed, the skill gets expanded to a full prompt. Use the Skill tool to execute them. IMPORTANT: Only use Skill for skills listed in the available skills context in the conversation - do not guess or use built-in CLI commands.
|
||||
|
||||
@@ -85,6 +85,14 @@ export interface SkillDiscoveryError {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function compareSkills(a: Skill, b: Skill): number {
|
||||
return (
|
||||
a.id.localeCompare(b.id) ||
|
||||
a.source.localeCompare(b.source) ||
|
||||
a.path.localeCompare(b.path)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default directory name where project skills are stored
|
||||
*/
|
||||
@@ -223,7 +231,7 @@ export async function discoverSkills(
|
||||
}
|
||||
|
||||
return {
|
||||
skills: Array.from(skillsById.values()),
|
||||
skills: Array.from(skillsById.values()).sort(compareSkills),
|
||||
errors: allErrors,
|
||||
};
|
||||
}
|
||||
@@ -406,7 +414,9 @@ export function formatSkillsAsSystemReminder(skills: Skill[]): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
const lines = skills.map((s) => `- ${s.id} (${s.source}): ${s.description}`);
|
||||
const lines = [...skills]
|
||||
.sort(compareSkills)
|
||||
.map((s) => `- ${s.id} (${s.source}): ${s.description}`);
|
||||
|
||||
return `<system-reminder>
|
||||
The following skills are available for use with the Skill tool:
|
||||
|
||||
Reference in New Issue
Block a user