feat(skills): added skills to agent block (#3149)

* feat(skills): added skills to agent block

* improvement(skills): audit fixes, docs, icon, and UX polish

* fix(skills): consolidate redundant permission checks in POST and DELETE

* more friendly error for duplicate skills in the same workspace

* fix(executor):  loop sentinel-end wrongly queued (#3148)

* fix(executor):  loop sentinel-end wrongly queued

* fix nested subflow error highlighting

* fix(linear): align tool outputs, queries, and pagination with API (#3150)

* fix(linear): align tool outputs, queries, and pagination with API

* fix(linear): coerce first param to number, remove duplicate conditions, add null guard

* fix(resolver): response format and evaluator metrics in deactivated branch (#3152)

* fix(resolver): response format in deactivated branch

* add evaluator metrics too

* add child workflow id to the workflow block outputs

* cleanup typing

* feat(slack): add file attachment support to slack webhook trigger (#3151)

* feat(slack): add file attachment support to slack webhook trigger

* additional file handling

* lint

* ack comment

* fix(skills): hide skill selection when disabled, remove dead code

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
This commit is contained in:
Waleed
2026-02-06 11:38:38 -08:00
committed by GitHub
parent ed5ed97c07
commit 71bd535d04
29 changed files with 12177 additions and 5 deletions

View File

@@ -11,9 +11,15 @@ import {
validateCustomToolsAllowed,
validateMcpToolsAllowed,
validateModelProvider,
validateSkillsAllowed,
} from '@/ee/access-control/utils/permission-check'
import { AGENT, BlockType, DEFAULTS, REFERENCE, stripCustomToolPrefix } from '@/executor/constants'
import { memoryService } from '@/executor/handlers/agent/memory'
import {
buildLoadSkillTool,
buildSkillsSystemPromptSection,
resolveSkillMetadata,
} from '@/executor/handlers/agent/skills-resolver'
import type {
AgentInputs,
Message,
@@ -57,8 +63,21 @@ export class AgentBlockHandler implements BlockHandler {
const providerId = getProviderFromModel(model)
const formattedTools = await this.formatTools(ctx, filteredInputs.tools || [])
// Resolve skill metadata for progressive disclosure
const skillInputs = filteredInputs.skills ?? []
let skillMetadata: Array<{ name: string; description: string }> = []
if (skillInputs.length > 0 && ctx.workspaceId) {
await validateSkillsAllowed(ctx.userId, ctx)
skillMetadata = await resolveSkillMetadata(skillInputs, ctx.workspaceId)
if (skillMetadata.length > 0) {
const skillNames = skillMetadata.map((s) => s.name)
formattedTools.push(buildLoadSkillTool(skillNames))
}
}
const streamingConfig = this.getStreamingConfig(ctx, block)
const messages = await this.buildMessages(ctx, filteredInputs)
const messages = await this.buildMessages(ctx, filteredInputs, skillMetadata)
const providerRequest = this.buildProviderRequest({
ctx,
@@ -723,7 +742,8 @@ export class AgentBlockHandler implements BlockHandler {
private async buildMessages(
ctx: ExecutionContext,
inputs: AgentInputs
inputs: AgentInputs,
skillMetadata: Array<{ name: string; description: string }> = []
): Promise<Message[] | undefined> {
const messages: Message[] = []
const memoryEnabled = inputs.memoryType && inputs.memoryType !== 'none'
@@ -803,6 +823,20 @@ export class AgentBlockHandler implements BlockHandler {
messages.unshift(...systemMessages)
}
// 8. Inject skill metadata into the system message (progressive disclosure)
if (skillMetadata.length > 0) {
const skillSection = buildSkillsSystemPromptSection(skillMetadata)
const systemIdx = messages.findIndex((m) => m.role === 'system')
if (systemIdx >= 0) {
messages[systemIdx] = {
...messages[systemIdx],
content: messages[systemIdx].content + skillSection,
}
} else {
messages.unshift({ role: 'system', content: skillSection.trim() })
}
}
return messages.length > 0 ? messages : undefined
}

View File

@@ -0,0 +1,122 @@
import { db } from '@sim/db'
import { skill } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, inArray } from 'drizzle-orm'
import type { SkillInput } from '@/executor/handlers/agent/types'
const logger = createLogger('SkillsResolver')
function escapeXml(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
interface SkillMetadata {
name: string
description: string
}
/**
* Fetch skill metadata (name + description) for system prompt injection.
* Only returns lightweight data so the LLM knows what skills are available.
*/
export async function resolveSkillMetadata(
skillInputs: SkillInput[],
workspaceId: string
): Promise<SkillMetadata[]> {
if (!skillInputs.length || !workspaceId) return []
const skillIds = skillInputs.map((s) => s.skillId)
try {
const rows = await db
.select({ name: skill.name, description: skill.description })
.from(skill)
.where(and(eq(skill.workspaceId, workspaceId), inArray(skill.id, skillIds)))
return rows
} catch (error) {
logger.error('Failed to resolve skill metadata', { error, skillIds, workspaceId })
return []
}
}
/**
* Fetch full skill content for a load_skill tool response.
* Called when the LLM decides a skill is relevant and invokes load_skill.
*/
export async function resolveSkillContent(
skillName: string,
workspaceId: string
): Promise<string | null> {
if (!skillName || !workspaceId) return null
try {
const rows = await db
.select({ content: skill.content, name: skill.name })
.from(skill)
.where(and(eq(skill.workspaceId, workspaceId), eq(skill.name, skillName)))
.limit(1)
if (rows.length === 0) {
logger.warn('Skill not found', { skillName, workspaceId })
return null
}
return rows[0].content
} catch (error) {
logger.error('Failed to resolve skill content', { error, skillName, workspaceId })
return null
}
}
/**
* Build the system prompt section that lists available skills.
* Uses XML format per the agentskills.io integration guide.
*/
export function buildSkillsSystemPromptSection(skills: SkillMetadata[]): string {
if (!skills.length) return ''
const skillEntries = skills
.map(
(s) =>
` <skill name="${escapeXml(s.name)}">\n <description>${escapeXml(s.description)}</description>\n </skill>`
)
.join('\n')
return [
'',
'You have access to the following skills. Use the load_skill tool to activate a skill when relevant.',
'',
'<available_skills>',
skillEntries,
'</available_skills>',
].join('\n')
}
/**
* Build the load_skill tool definition for injection into the tools array.
* Returns a ProviderToolConfig-compatible object so all providers can process it.
*/
export function buildLoadSkillTool(skillNames: string[]) {
return {
id: 'load_skill',
name: 'load_skill',
description: `Load a skill to get specialized instructions. Available skills: ${skillNames.join(', ')}`,
params: {},
parameters: {
type: 'object',
properties: {
skill_name: {
type: 'string',
description: 'Name of the skill to load',
enum: skillNames,
},
},
required: ['skill_name'],
},
}
}

View File

@@ -1,7 +1,14 @@
export interface SkillInput {
skillId: string
name?: string
description?: string
}
export interface AgentInputs {
model?: string
responseFormat?: string | object
tools?: ToolInput[]
skills?: SkillInput[]
// Legacy inputs (backward compatible)
systemPrompt?: string
userPrompt?: string | object