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,
|
ApprovalCreate,
|
||||||
LettaStreamingResponse,
|
LettaStreamingResponse,
|
||||||
} from "@letta-ai/letta-client/resources/agents/messages";
|
} from "@letta-ai/letta-client/resources/agents/messages";
|
||||||
|
import type { MessageCreateParams as ConversationMessageCreateParams } from "@letta-ai/letta-client/resources/conversations/messages";
|
||||||
import {
|
import {
|
||||||
type ClientTool,
|
type ClientTool,
|
||||||
captureToolExecutionContext,
|
captureToolExecutionContext,
|
||||||
@@ -19,6 +20,7 @@ import {
|
|||||||
normalizeOutgoingApprovalMessages,
|
normalizeOutgoingApprovalMessages,
|
||||||
} from "./approval-result-normalization";
|
} from "./approval-result-normalization";
|
||||||
import { getClient } from "./client";
|
import { getClient } from "./client";
|
||||||
|
import { buildClientSkillsPayload } from "./clientSkills";
|
||||||
|
|
||||||
const streamRequestStartTimes = new WeakMap<object, number>();
|
const streamRequestStartTimes = new WeakMap<object, number>();
|
||||||
const streamToolContextIds = new WeakMap<object, string>();
|
const streamToolContextIds = new WeakMap<object, string>();
|
||||||
@@ -60,6 +62,9 @@ export function buildConversationMessagesCreateRequestBody(
|
|||||||
messages: Array<MessageCreate | ApprovalCreate>,
|
messages: Array<MessageCreate | ApprovalCreate>,
|
||||||
opts: SendMessageStreamOptions = { streamTokens: true, background: true },
|
opts: SendMessageStreamOptions = { streamTokens: true, background: true },
|
||||||
clientTools: ClientTool[],
|
clientTools: ClientTool[],
|
||||||
|
clientSkills: NonNullable<
|
||||||
|
ConversationMessageCreateParams["client_skills"]
|
||||||
|
> = [],
|
||||||
) {
|
) {
|
||||||
const isDefaultConversation = conversationId === "default";
|
const isDefaultConversation = conversationId === "default";
|
||||||
if (isDefaultConversation && !opts.agentId) {
|
if (isDefaultConversation && !opts.agentId) {
|
||||||
@@ -77,6 +82,7 @@ export function buildConversationMessagesCreateRequestBody(
|
|||||||
stream_tokens: opts.streamTokens ?? true,
|
stream_tokens: opts.streamTokens ?? true,
|
||||||
include_pings: true,
|
include_pings: true,
|
||||||
background: opts.background ?? true,
|
background: opts.background ?? true,
|
||||||
|
client_skills: clientSkills,
|
||||||
client_tools: clientTools,
|
client_tools: clientTools,
|
||||||
include_compaction_messages: true,
|
include_compaction_messages: true,
|
||||||
...(isDefaultConversation ? { agent_id: opts.agentId } : {}),
|
...(isDefaultConversation ? { agent_id: opts.agentId } : {}),
|
||||||
@@ -113,6 +119,10 @@ export async function sendMessageStream(
|
|||||||
// This prevents sending messages with stale tools during a switch
|
// This prevents sending messages with stale tools during a switch
|
||||||
await waitForToolsetReady();
|
await waitForToolsetReady();
|
||||||
const { clientTools, contextId } = captureToolExecutionContext();
|
const { clientTools, contextId } = captureToolExecutionContext();
|
||||||
|
const { clientSkills, errors: clientSkillDiscoveryErrors } =
|
||||||
|
await buildClientSkillsPayload({
|
||||||
|
agentId: opts.agentId,
|
||||||
|
});
|
||||||
|
|
||||||
const resolvedConversationId = conversationId;
|
const resolvedConversationId = conversationId;
|
||||||
const requestBody = buildConversationMessagesCreateRequestBody(
|
const requestBody = buildConversationMessagesCreateRequestBody(
|
||||||
@@ -120,12 +130,30 @@ export async function sendMessageStream(
|
|||||||
messages,
|
messages,
|
||||||
opts,
|
opts,
|
||||||
clientTools,
|
clientTools,
|
||||||
|
clientSkills,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (process.env.DEBUG) {
|
if (process.env.DEBUG) {
|
||||||
console.log(
|
console.log(
|
||||||
`[DEBUG] sendMessageStream: conversationId=${conversationId}, agentId=${opts.agentId ?? "(none)"}`,
|
`[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> = {};
|
const extraHeaders: Record<string, string> = {};
|
||||||
|
|||||||
@@ -132,4 +132,4 @@ assistant: Clients are marked as failed in the `connectToServer` function in src
|
|||||||
</example>
|
</example>
|
||||||
|
|
||||||
# Skills
|
# 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
|
## 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?
|
- **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
|
# 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. 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:
|
2. If a `creating-skills` skill is available, invoke it using the `Skill` tool:
|
||||||
- Call the `Skill` tool with: `skill: "creating-skills"`
|
- 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.
|
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.
|
When running in Letta Code, shell tools provide `AGENT_ID`: your current agent ID.
|
||||||
|
|
||||||
# Skills
|
# 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;
|
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
|
* Default directory name where project skills are stored
|
||||||
*/
|
*/
|
||||||
@@ -223,7 +231,7 @@ export async function discoverSkills(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
skills: Array.from(skillsById.values()),
|
skills: Array.from(skillsById.values()).sort(compareSkills),
|
||||||
errors: allErrors,
|
errors: allErrors,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -406,7 +414,9 @@ export function formatSkillsAsSystemReminder(skills: Skill[]): string {
|
|||||||
return "";
|
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>
|
return `<system-reminder>
|
||||||
The following skills are available for use with the Skill tool:
|
The following skills are available for use with the Skill tool:
|
||||||
|
|||||||
@@ -8182,9 +8182,8 @@ export default function App({
|
|||||||
cmd.finish(outputLines.join("\n"), true);
|
cmd.finish(outputLines.join("\n"), true);
|
||||||
|
|
||||||
// Manual /compact bypasses stream compaction events, so trigger
|
// Manual /compact bypasses stream compaction events, so trigger
|
||||||
// post-compaction reminder/skills reinjection on the next user turn.
|
// post-compaction reflection reminder/auto-launch on the next user turn.
|
||||||
contextTrackerRef.current.pendingReflectionTrigger = true;
|
contextTrackerRef.current.pendingReflectionTrigger = true;
|
||||||
contextTrackerRef.current.pendingSkillsReinject = true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let errorOutput: string;
|
let errorOutput: string;
|
||||||
|
|
||||||
|
|||||||
@@ -73,13 +73,16 @@ export function createCommandRunner({
|
|||||||
onCommandFinished,
|
onCommandFinished,
|
||||||
}: RunnerDeps) {
|
}: RunnerDeps) {
|
||||||
function getHandle(id: string, input: string): CommandHandle {
|
function getHandle(id: string, input: string): CommandHandle {
|
||||||
// biome-ignore lint/style/noNonNullAssertion: forward-reference pattern — overwritten synchronously below. null! preferred over no-ops to crash loudly if invariant breaks.
|
const uninitialized = (): never => {
|
||||||
|
throw new Error("CommandHandle callback used before initialization");
|
||||||
|
};
|
||||||
|
|
||||||
const handle: CommandHandle = {
|
const handle: CommandHandle = {
|
||||||
id,
|
id,
|
||||||
input,
|
input,
|
||||||
update: null!,
|
update: uninitialized,
|
||||||
finish: null!,
|
finish: uninitialized,
|
||||||
fail: null!,
|
fail: uninitialized,
|
||||||
};
|
};
|
||||||
|
|
||||||
const update = (updateData: CommandUpdate) => {
|
const update = (updateData: CommandUpdate) => {
|
||||||
|
|||||||
@@ -454,7 +454,6 @@ function extractTextPart(v: unknown): string {
|
|||||||
function markCompactionCompleted(ctx?: ContextTracker): void {
|
function markCompactionCompleted(ctx?: ContextTracker): void {
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
ctx.pendingCompaction = true;
|
ctx.pendingCompaction = true;
|
||||||
ctx.pendingSkillsReinject = true;
|
|
||||||
ctx.pendingReflectionTrigger = true;
|
ctx.pendingReflectionTrigger = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ export type ContextTracker = {
|
|||||||
currentTurnId: number;
|
currentTurnId: number;
|
||||||
/** Set when a compaction event is seen; consumed by the next usage_statistics push */
|
/** Set when a compaction event is seen; consumed by the next usage_statistics push */
|
||||||
pendingCompaction: boolean;
|
pendingCompaction: boolean;
|
||||||
/** Set when compaction happens; consumed by the next user message to reinject skills reminder */
|
|
||||||
pendingSkillsReinject: boolean;
|
|
||||||
/** Set when compaction happens; consumed by the next user message to trigger memory reminder/spawn */
|
/** Set when compaction happens; consumed by the next user message to trigger memory reminder/spawn */
|
||||||
pendingReflectionTrigger: boolean;
|
pendingReflectionTrigger: boolean;
|
||||||
};
|
};
|
||||||
@@ -28,7 +26,6 @@ export function createContextTracker(): ContextTracker {
|
|||||||
contextTokensHistory: [],
|
contextTokensHistory: [],
|
||||||
currentTurnId: 0, // simple in-memory counter for now
|
currentTurnId: 0, // simple in-memory counter for now
|
||||||
pendingCompaction: false,
|
pendingCompaction: false,
|
||||||
pendingSkillsReinject: false,
|
|
||||||
pendingReflectionTrigger: false,
|
pendingReflectionTrigger: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -38,6 +35,5 @@ export function resetContextHistory(ct: ContextTracker): void {
|
|||||||
ct.lastContextTokens = 0;
|
ct.lastContextTokens = 0;
|
||||||
ct.contextTokensHistory = [];
|
ct.contextTokensHistory = [];
|
||||||
ct.pendingCompaction = false;
|
ct.pendingCompaction = false;
|
||||||
ct.pendingSkillsReinject = false;
|
|
||||||
ct.pendingReflectionTrigger = false;
|
ct.pendingReflectionTrigger = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
} from "./agent/approval-recovery";
|
} from "./agent/approval-recovery";
|
||||||
import { handleBootstrapSessionState } from "./agent/bootstrapHandler";
|
import { handleBootstrapSessionState } from "./agent/bootstrapHandler";
|
||||||
import { getClient } from "./agent/client";
|
import { getClient } from "./agent/client";
|
||||||
|
import { buildClientSkillsPayload } from "./agent/clientSkills";
|
||||||
import { setAgentContext, setConversationId } from "./agent/context";
|
import { setAgentContext, setConversationId } from "./agent/context";
|
||||||
import { createAgent } from "./agent/create";
|
import { createAgent } from "./agent/create";
|
||||||
import { handleListMessages } from "./agent/listMessagesHandler";
|
import { handleListMessages } from "./agent/listMessagesHandler";
|
||||||
@@ -1459,13 +1460,22 @@ ${SYSTEM_REMINDER_CLOSE}
|
|||||||
// Pre-load specific skills' full content (used by subagents with skills: field)
|
// Pre-load specific skills' full content (used by subagents with skills: field)
|
||||||
if (preLoadSkillsRaw) {
|
if (preLoadSkillsRaw) {
|
||||||
const { readFile: readFileAsync } = await import("node:fs/promises");
|
const { readFile: readFileAsync } = await import("node:fs/promises");
|
||||||
|
const { skillPathById } = await buildClientSkillsPayload({
|
||||||
|
agentId: agent.id,
|
||||||
|
skillSources: resolvedSkillSources,
|
||||||
|
logger: (message) => {
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
console.warn(`[DEBUG] ${message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
const skillIds = preLoadSkillsRaw
|
const skillIds = preLoadSkillsRaw
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
const loadedContents: string[] = [];
|
const loadedContents: string[] = [];
|
||||||
for (const skillId of skillIds) {
|
for (const skillId of skillIds) {
|
||||||
const skillPath = sharedReminderState.skillPathById[skillId];
|
const skillPath = skillPathById[skillId];
|
||||||
if (!skillPath) continue;
|
if (!skillPath) continue;
|
||||||
try {
|
try {
|
||||||
const content = await readFileAsync(skillPath, "utf-8");
|
const content = await readFileAsync(skillPath, "utf-8");
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export type SharedReminderMode =
|
|||||||
export type SharedReminderId =
|
export type SharedReminderId =
|
||||||
| "session-context"
|
| "session-context"
|
||||||
| "agent-info"
|
| "agent-info"
|
||||||
| "skills"
|
|
||||||
| "permission-mode"
|
| "permission-mode"
|
||||||
| "plan-mode"
|
| "plan-mode"
|
||||||
| "reflection-step-count"
|
| "reflection-step-count"
|
||||||
@@ -40,11 +39,6 @@ export const SHARED_REMINDER_CATALOG: ReadonlyArray<SharedReminderDefinition> =
|
|||||||
"subagent",
|
"subagent",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "skills",
|
|
||||||
description: "Available skills system reminder (with reinjection)",
|
|
||||||
modes: ["interactive", "headless-one-shot", "headless-bidirectional"],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "permission-mode",
|
id: "permission-mode",
|
||||||
description: "Permission mode reminder",
|
description: "Permission mode reminder",
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
import { join } from "node:path";
|
|
||||||
import type { MessageCreate } from "@letta-ai/letta-client/resources/agents/agents";
|
import type { MessageCreate } from "@letta-ai/letta-client/resources/agents/agents";
|
||||||
import { getSkillsDirectory } from "../agent/context";
|
import type { SkillSource } from "../agent/skills";
|
||||||
import {
|
|
||||||
discoverSkills,
|
|
||||||
formatSkillsAsSystemReminder,
|
|
||||||
SKILLS_DIR,
|
|
||||||
type SkillSource,
|
|
||||||
} from "../agent/skills";
|
|
||||||
import { buildAgentInfo } from "../cli/helpers/agentInfo";
|
import { buildAgentInfo } from "../cli/helpers/agentInfo";
|
||||||
import {
|
import {
|
||||||
buildCompactionMemoryReminder,
|
buildCompactionMemoryReminder,
|
||||||
@@ -101,52 +94,6 @@ async function buildSessionContextReminder(
|
|||||||
return reminder || null;
|
return reminder || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildSkillsReminder(
|
|
||||||
context: SharedReminderContext,
|
|
||||||
): Promise<string | null> {
|
|
||||||
const previousSkillsReminder = context.state.cachedSkillsReminder;
|
|
||||||
// Keep a stable empty baseline so a later successful discovery can diff
|
|
||||||
// against "" and trigger reinjection, even after an earlier discovery failure.
|
|
||||||
let latestSkillsReminder = previousSkillsReminder ?? "";
|
|
||||||
|
|
||||||
try {
|
|
||||||
const skillsDir = getSkillsDirectory() || join(process.cwd(), SKILLS_DIR);
|
|
||||||
const { skills } = await discoverSkills(skillsDir, context.agent.id, {
|
|
||||||
sources: context.skillSources,
|
|
||||||
});
|
|
||||||
latestSkillsReminder = formatSkillsAsSystemReminder(skills);
|
|
||||||
context.state.skillPathById = Object.fromEntries(
|
|
||||||
skills
|
|
||||||
.filter(
|
|
||||||
(skill) => typeof skill.path === "string" && skill.path.length > 0,
|
|
||||||
)
|
|
||||||
.map((skill) => [skill.id, skill.path as string]),
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
// Keep previous snapshot when discovery fails.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
previousSkillsReminder !== null &&
|
|
||||||
previousSkillsReminder !== latestSkillsReminder
|
|
||||||
) {
|
|
||||||
context.state.pendingSkillsReinject = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.state.cachedSkillsReminder = latestSkillsReminder;
|
|
||||||
|
|
||||||
const shouldInject =
|
|
||||||
!context.state.hasInjectedSkillsReminder ||
|
|
||||||
context.state.pendingSkillsReinject;
|
|
||||||
if (!shouldInject) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.state.hasInjectedSkillsReminder = true;
|
|
||||||
context.state.pendingSkillsReinject = false;
|
|
||||||
return latestSkillsReminder || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildPlanModeReminder(
|
async function buildPlanModeReminder(
|
||||||
context: SharedReminderContext,
|
context: SharedReminderContext,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
@@ -398,7 +345,6 @@ export const sharedReminderProviders: Record<
|
|||||||
> = {
|
> = {
|
||||||
"agent-info": buildAgentInfoReminder,
|
"agent-info": buildAgentInfoReminder,
|
||||||
"session-context": buildSessionContextReminder,
|
"session-context": buildSessionContextReminder,
|
||||||
skills: buildSkillsReminder,
|
|
||||||
"permission-mode": buildPermissionModeReminder,
|
"permission-mode": buildPermissionModeReminder,
|
||||||
"plan-mode": buildPlanModeReminder,
|
"plan-mode": buildPlanModeReminder,
|
||||||
"reflection-step-count": buildReflectionStepReminder,
|
"reflection-step-count": buildReflectionStepReminder,
|
||||||
|
|||||||
@@ -22,12 +22,8 @@ export interface ToolsetChangeReminder {
|
|||||||
export interface SharedReminderState {
|
export interface SharedReminderState {
|
||||||
hasSentAgentInfo: boolean;
|
hasSentAgentInfo: boolean;
|
||||||
hasSentSessionContext: boolean;
|
hasSentSessionContext: boolean;
|
||||||
hasInjectedSkillsReminder: boolean;
|
|
||||||
cachedSkillsReminder: string | null;
|
|
||||||
skillPathById: Record<string, string>;
|
|
||||||
lastNotifiedPermissionMode: PermissionMode | null;
|
lastNotifiedPermissionMode: PermissionMode | null;
|
||||||
turnCount: number;
|
turnCount: number;
|
||||||
pendingSkillsReinject: boolean;
|
|
||||||
pendingReflectionTrigger: boolean;
|
pendingReflectionTrigger: boolean;
|
||||||
pendingAutoInitReminder: boolean;
|
pendingAutoInitReminder: boolean;
|
||||||
pendingCommandIoReminders: CommandIoReminder[];
|
pendingCommandIoReminders: CommandIoReminder[];
|
||||||
@@ -40,12 +36,8 @@ export function createSharedReminderState(): SharedReminderState {
|
|||||||
return {
|
return {
|
||||||
hasSentAgentInfo: false,
|
hasSentAgentInfo: false,
|
||||||
hasSentSessionContext: false,
|
hasSentSessionContext: false,
|
||||||
hasInjectedSkillsReminder: false,
|
|
||||||
cachedSkillsReminder: null,
|
|
||||||
skillPathById: {},
|
|
||||||
lastNotifiedPermissionMode: null,
|
lastNotifiedPermissionMode: null,
|
||||||
turnCount: 0,
|
turnCount: 0,
|
||||||
pendingSkillsReinject: false,
|
|
||||||
pendingReflectionTrigger: false,
|
pendingReflectionTrigger: false,
|
||||||
pendingAutoInitReminder: false,
|
pendingAutoInitReminder: false,
|
||||||
pendingCommandIoReminders: [],
|
pendingCommandIoReminders: [],
|
||||||
@@ -63,10 +55,6 @@ export function syncReminderStateFromContextTracker(
|
|||||||
state: SharedReminderState,
|
state: SharedReminderState,
|
||||||
contextTracker: ContextTracker,
|
contextTracker: ContextTracker,
|
||||||
): void {
|
): void {
|
||||||
if (contextTracker.pendingSkillsReinject) {
|
|
||||||
state.pendingSkillsReinject = true;
|
|
||||||
contextTracker.pendingSkillsReinject = false;
|
|
||||||
}
|
|
||||||
if (contextTracker.pendingReflectionTrigger) {
|
if (contextTracker.pendingReflectionTrigger) {
|
||||||
state.pendingReflectionTrigger = true;
|
state.pendingReflectionTrigger = true;
|
||||||
contextTracker.pendingReflectionTrigger = false;
|
contextTracker.pendingReflectionTrigger = false;
|
||||||
|
|||||||
88
src/tests/agent/clientSkills.test.ts
Normal file
88
src/tests/agent/clientSkills.test.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import type { Skill, SkillDiscoveryResult } from "../../agent/skills";
|
||||||
|
|
||||||
|
const baseSkill: Skill = {
|
||||||
|
id: "base",
|
||||||
|
name: "Base",
|
||||||
|
description: "Base skill",
|
||||||
|
path: "/tmp/base/SKILL.md",
|
||||||
|
source: "project",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("buildClientSkillsPayload", () => {
|
||||||
|
test("returns deterministically sorted client skills and path map", async () => {
|
||||||
|
const { buildClientSkillsPayload } = await import(
|
||||||
|
"../../agent/clientSkills"
|
||||||
|
);
|
||||||
|
|
||||||
|
const discoverSkillsFn = async (): Promise<SkillDiscoveryResult> => ({
|
||||||
|
skills: [
|
||||||
|
{
|
||||||
|
...baseSkill,
|
||||||
|
id: "z-skill",
|
||||||
|
description: "z",
|
||||||
|
path: "/tmp/z/SKILL.md",
|
||||||
|
source: "project",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...baseSkill,
|
||||||
|
id: "a-skill",
|
||||||
|
description: "a",
|
||||||
|
path: "/tmp/a/SKILL.md",
|
||||||
|
source: "bundled",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
errors: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await buildClientSkillsPayload({
|
||||||
|
agentId: "agent-1",
|
||||||
|
skillsDirectory: "/tmp/.skills",
|
||||||
|
skillSources: ["project", "bundled"],
|
||||||
|
discoverSkillsFn,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.clientSkills).toEqual([
|
||||||
|
{
|
||||||
|
name: "a-skill",
|
||||||
|
description: "a",
|
||||||
|
location: "/tmp/a/SKILL.md",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "z-skill",
|
||||||
|
description: "z",
|
||||||
|
location: "/tmp/z/SKILL.md",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(result.skillPathById).toEqual({
|
||||||
|
"a-skill": "/tmp/a/SKILL.md",
|
||||||
|
"z-skill": "/tmp/z/SKILL.md",
|
||||||
|
});
|
||||||
|
expect(result.errors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fails open with empty client_skills when discovery throws", async () => {
|
||||||
|
const { buildClientSkillsPayload } = await import(
|
||||||
|
"../../agent/clientSkills"
|
||||||
|
);
|
||||||
|
|
||||||
|
const discoverSkillsFn = async (): Promise<SkillDiscoveryResult> => {
|
||||||
|
throw new Error("boom");
|
||||||
|
};
|
||||||
|
|
||||||
|
const logs: string[] = [];
|
||||||
|
const result = await buildClientSkillsPayload({
|
||||||
|
skillsDirectory: "/tmp/.skills",
|
||||||
|
discoverSkillsFn,
|
||||||
|
logger: (m) => logs.push(m),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.clientSkills).toEqual([]);
|
||||||
|
expect(result.skillPathById).toEqual({});
|
||||||
|
expect(result.errors).toHaveLength(1);
|
||||||
|
expect(result.errors[0]?.path).toBe("/tmp/.skills");
|
||||||
|
expect(
|
||||||
|
logs.some((m) => m.includes("Failed to build client_skills payload")),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
35
src/tests/agent/message-client-skills.test.ts
Normal file
35
src/tests/agent/message-client-skills.test.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { buildConversationMessagesCreateRequestBody } from "../../agent/message";
|
||||||
|
|
||||||
|
describe("buildConversationMessagesCreateRequestBody client_skills", () => {
|
||||||
|
test("includes client_skills alongside client_tools", () => {
|
||||||
|
const body = buildConversationMessagesCreateRequestBody(
|
||||||
|
"default",
|
||||||
|
[{ type: "message", role: "user", content: "hello" }],
|
||||||
|
{ agentId: "agent-1", streamTokens: true, background: true },
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "ShellCommand",
|
||||||
|
description: "Run shell command",
|
||||||
|
parameters: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "debugging",
|
||||||
|
description: "Debugging checklist",
|
||||||
|
location: "/tmp/.skills/debugging/SKILL.md",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(body.client_tools).toHaveLength(1);
|
||||||
|
expect(body.client_skills).toEqual([
|
||||||
|
{
|
||||||
|
name: "debugging",
|
||||||
|
description: "Debugging checklist",
|
||||||
|
location: "/tmp/.skills/debugging/SKILL.md",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -107,5 +107,24 @@ describe.skipIf(process.platform === "win32")(
|
|||||||
result.errors.some((error) => error.path.includes("broken-link")),
|
result.errors.some((error) => error.path.includes("broken-link")),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("returns discovered skills in deterministic sorted order", async () => {
|
||||||
|
mkdirSync(projectSkillsDir, { recursive: true });
|
||||||
|
writeSkill(join(projectSkillsDir, "z-skill"), "Z Skill");
|
||||||
|
writeSkill(join(projectSkillsDir, "a-skill"), "A Skill");
|
||||||
|
writeSkill(join(projectSkillsDir, "m-skill"), "M Skill");
|
||||||
|
|
||||||
|
const result = await discoverSkills(projectSkillsDir, undefined, {
|
||||||
|
skipBundled: true,
|
||||||
|
sources: ["project"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.errors).toHaveLength(0);
|
||||||
|
expect(result.skills.map((skill) => skill.id)).toEqual([
|
||||||
|
"a-skill",
|
||||||
|
"m-skill",
|
||||||
|
"z-skill",
|
||||||
|
]);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -66,4 +66,53 @@ describe("Skills formatting (system reminder)", () => {
|
|||||||
expect(result).toContain("project-skill (project)");
|
expect(result).toContain("project-skill (project)");
|
||||||
expect(result).toContain("global-skill (global)");
|
expect(result).toContain("global-skill (global)");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("sorts skills deterministically before formatting", () => {
|
||||||
|
const skills: Skill[] = [
|
||||||
|
{
|
||||||
|
id: "z-skill",
|
||||||
|
name: "Z Skill",
|
||||||
|
description: "Last by id",
|
||||||
|
path: "/test/.skills/z-skill/SKILL.md",
|
||||||
|
source: "project",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "a-skill",
|
||||||
|
name: "A Skill",
|
||||||
|
description: "First by id",
|
||||||
|
path: "/test/.skills/a-skill/SKILL.md",
|
||||||
|
source: "project",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "same-id",
|
||||||
|
name: "Same Id Global",
|
||||||
|
description: "Global variant",
|
||||||
|
path: "/global/.skills/same-id/SKILL.md",
|
||||||
|
source: "global",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "same-id",
|
||||||
|
name: "Same Id Project",
|
||||||
|
description: "Project variant",
|
||||||
|
path: "/project/.skills/same-id/SKILL.md",
|
||||||
|
source: "project",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = formatSkillsAsSystemReminder(skills);
|
||||||
|
|
||||||
|
const aSkillIndex = result.indexOf("- a-skill (project): First by id");
|
||||||
|
const sameIdGlobalIndex = result.indexOf(
|
||||||
|
"- same-id (global): Global variant",
|
||||||
|
);
|
||||||
|
const sameIdProjectIndex = result.indexOf(
|
||||||
|
"- same-id (project): Project variant",
|
||||||
|
);
|
||||||
|
const zSkillIndex = result.indexOf("- z-skill (project): Last by id");
|
||||||
|
|
||||||
|
expect(aSkillIndex).toBeGreaterThan(-1);
|
||||||
|
expect(sameIdGlobalIndex).toBeGreaterThan(aSkillIndex);
|
||||||
|
expect(sameIdProjectIndex).toBeGreaterThan(sameIdGlobalIndex);
|
||||||
|
expect(zSkillIndex).toBeGreaterThan(sameIdProjectIndex);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ describe("accumulator usage statistics", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(tracker.pendingCompaction).toBe(true);
|
expect(tracker.pendingCompaction).toBe(true);
|
||||||
expect(tracker.pendingSkillsReinject).toBe(true);
|
|
||||||
expect(tracker.pendingReflectionTrigger).toBe(true);
|
expect(tracker.pendingReflectionTrigger).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -126,7 +125,6 @@ describe("accumulator usage statistics", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(tracker.pendingCompaction).toBe(true);
|
expect(tracker.pendingCompaction).toBe(true);
|
||||||
expect(tracker.pendingSkillsReinject).toBe(true);
|
|
||||||
expect(tracker.pendingReflectionTrigger).toBe(true);
|
expect(tracker.pendingReflectionTrigger).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ describe("contextTracker", () => {
|
|||||||
{ timestamp: 1, tokens: 111, turnId: 1, compacted: true },
|
{ timestamp: 1, tokens: 111, turnId: 1, compacted: true },
|
||||||
];
|
];
|
||||||
tracker.pendingCompaction = true;
|
tracker.pendingCompaction = true;
|
||||||
tracker.pendingSkillsReinject = true;
|
|
||||||
tracker.pendingReflectionTrigger = true;
|
tracker.pendingReflectionTrigger = true;
|
||||||
tracker.currentTurnId = 9;
|
tracker.currentTurnId = 9;
|
||||||
|
|
||||||
@@ -21,7 +20,6 @@ describe("contextTracker", () => {
|
|||||||
expect(tracker.lastContextTokens).toBe(0);
|
expect(tracker.lastContextTokens).toBe(0);
|
||||||
expect(tracker.contextTokensHistory).toEqual([]);
|
expect(tracker.contextTokensHistory).toEqual([]);
|
||||||
expect(tracker.pendingCompaction).toBe(false);
|
expect(tracker.pendingCompaction).toBe(false);
|
||||||
expect(tracker.pendingSkillsReinject).toBe(false);
|
|
||||||
expect(tracker.pendingReflectionTrigger).toBe(false);
|
expect(tracker.pendingReflectionTrigger).toBe(false);
|
||||||
expect(tracker.currentTurnId).toBe(9);
|
expect(tracker.currentTurnId).toBe(9);
|
||||||
});
|
});
|
||||||
|
|||||||
19
src/tests/headless/client-skills-wiring.test.ts
Normal file
19
src/tests/headless/client-skills-wiring.test.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
describe("headless client skills wiring", () => {
|
||||||
|
test("pre-load-skills resolves skill paths from client-skills helper", () => {
|
||||||
|
const headlessPath = fileURLToPath(
|
||||||
|
new URL("../../headless.ts", import.meta.url),
|
||||||
|
);
|
||||||
|
const source = readFileSync(headlessPath, "utf-8");
|
||||||
|
|
||||||
|
expect(source).toContain("buildClientSkillsPayload({");
|
||||||
|
expect(source).toContain(
|
||||||
|
"const { skillPathById } = await buildClientSkillsPayload",
|
||||||
|
);
|
||||||
|
expect(source).toContain("const skillPath = skillPathById[skillId]");
|
||||||
|
expect(source).not.toContain("sharedReminderState.skillPathById");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
|
||||||
import type { SharedReminderContext } from "../../reminders/engine";
|
|
||||||
import { sharedReminderProviders } from "../../reminders/engine";
|
|
||||||
import { createSharedReminderState } from "../../reminders/state";
|
|
||||||
|
|
||||||
function buildContext(): SharedReminderContext {
|
|
||||||
return {
|
|
||||||
mode: "interactive",
|
|
||||||
agent: {
|
|
||||||
id: "agent-1",
|
|
||||||
name: "Agent 1",
|
|
||||||
description: null,
|
|
||||||
lastRunAt: null,
|
|
||||||
},
|
|
||||||
state: createSharedReminderState(),
|
|
||||||
sessionContextReminderEnabled: true,
|
|
||||||
reflectionSettings: {
|
|
||||||
trigger: "off",
|
|
||||||
behavior: "reminder",
|
|
||||||
stepCount: 25,
|
|
||||||
},
|
|
||||||
skillSources: ["bundled"],
|
|
||||||
resolvePlanModeReminder: () => "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("shared skills reminder", () => {
|
|
||||||
test("recovers from discovery failure and reinjects after next successful discovery", async () => {
|
|
||||||
const provider = sharedReminderProviders.skills;
|
|
||||||
const context = buildContext();
|
|
||||||
|
|
||||||
const mutableProcess = process as typeof process & { cwd: () => string };
|
|
||||||
const originalCwd = mutableProcess.cwd;
|
|
||||||
try {
|
|
||||||
mutableProcess.cwd = () => {
|
|
||||||
throw new Error("cwd unavailable for test");
|
|
||||||
};
|
|
||||||
|
|
||||||
const first = await provider(context);
|
|
||||||
expect(first).toBeNull();
|
|
||||||
expect(context.state.hasInjectedSkillsReminder).toBe(true);
|
|
||||||
expect(context.state.cachedSkillsReminder).toBe("");
|
|
||||||
} finally {
|
|
||||||
mutableProcess.cwd = originalCwd;
|
|
||||||
}
|
|
||||||
|
|
||||||
const second = await provider(context);
|
|
||||||
expect(second).not.toBeNull();
|
|
||||||
expect(context.state.pendingSkillsReinject).toBe(false);
|
|
||||||
if (second) {
|
|
||||||
expect(second).toContain("<system-reminder>");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -15,7 +15,7 @@ How to invoke:
|
|||||||
- `skill: "ms-office-suite:pdf"` - invoke using fully qualified name
|
- `skill: "ms-office-suite:pdf"` - invoke using fully qualified name
|
||||||
|
|
||||||
Important:
|
Important:
|
||||||
- Available skills are listed in system-reminder messages in the conversation
|
- Available skills are included in your current prompt context in the conversation
|
||||||
- When a skill matches the user's request, this is a BLOCKING REQUIREMENT: invoke the relevant Skill tool BEFORE generating any other response about the task
|
- When a skill matches the user's request, this is a BLOCKING REQUIREMENT: invoke the relevant Skill tool BEFORE generating any other response about the task
|
||||||
- NEVER mention a skill without actually calling this tool
|
- NEVER mention a skill without actually calling this tool
|
||||||
- Do not invoke a skill that is already running
|
- Do not invoke a skill that is already running
|
||||||
|
|||||||
Reference in New Issue
Block a user