[Agent Builder] Agent skills implementation#251209
Conversation
| /** | ||
| * DISCLAIMER: This skill is a sample skill. | ||
| * | ||
| * Threat analysis skill for security operations. | ||
| * This skill helps analyze security threats, investigate alerts, and assess risk. | ||
| */ |
There was a problem hiding this comment.
Replace this skill example with a better one
There was a problem hiding this comment.
We should remove that skill yeah, or at least remove the registration from the security plugin ihmo (or at least remove the inline tool with stub data)
There was a problem hiding this comment.
| if (isSkillFileEntry(entry)) { | ||
| const skill = skillsService.getSkillDefinition(entry.metadata.skill_id); | ||
| if (skill) { | ||
| const inlineTools = (await skill.getInlineTools?.()) ?? []; | ||
| const inlineExecutableTools = inlineTools.map((tool) => | ||
| skillsService.convertSkillTool(tool) | ||
| ); | ||
|
|
||
| const allowedTools = skill.getAllowedTools?.() ?? []; | ||
| const registryExecutableTools = await pickTools({ | ||
| toolProvider, | ||
| selection: [{ tool_ids: allowedTools }], | ||
| request, | ||
| }); | ||
|
|
||
| await toolManager.addTool( | ||
| { | ||
| type: ToolManagerToolType.executable, | ||
| tools: [...inlineExecutableTools, ...registryExecutableTools], | ||
| logger, | ||
| }, | ||
| { | ||
| dynamic: true, | ||
| } | ||
| ); | ||
| } else { | ||
| logger.debug(`Skill '${entry.metadata.skill_id}' not found in registry.`); | ||
| } | ||
| } |
There was a problem hiding this comment.
This is the code that adds skill tools to the tool manager. I think it is clearest if this logic lives here. Maybe eventually it should be moved into a hook, though.
There was a problem hiding this comment.
Agreed, reading the code before your comment I was thinking the exact same thing - should live in a hook ideally. But also agreed that until we do have hooks, this is the best place for the logic to live.
NIT: I would however extract the logic living in that if (isSkillFileEntry(entry)) block to a dedicated static loadSkill helper already.
cbe5029 to
81e74b7
Compare
| ...base, | ||
| rounds: roundsWithRefs, | ||
| attachments: existingAttachments, | ||
| ...(document._source!.state && { state: document._source!.state }), |
There was a problem hiding this comment.
Previously state was only returned by the final case. Assuming it is alright to return state when hasLegacyRoundAttachments or existingAttachments && existingAttachments.length > 0 is true.
| export const createToolManager = (): ToolManager => { | ||
| return new ToolManager({ | ||
| dynamicToolCapacity: 10, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
Currently, the tool manager has a dynamic tool capacity of 10. That means, if an 11th tool is added to the tool manager due to a skill being loaded, the least recently used tool would be evicted, and the agent wouldn't be able to call that tool anymore. Dynamic tool IDs are persisted at the end of a round, and the next round is initialised with the same dynamic tools so the agent could call those tools again without needing to load the skill.
Maybe this logic should be changed so it works like this: ToolManager has no (or a much higher) dynamic tool capacity, which would essentially result in no tools ever getting evicted within a round. To ensure the number of tools bound does not get out of hand, I could add a constraint that limits the number of tools carried over to the next round to, e.g. the 5 most recently used tools.
Or maybe we don't have to carry over tools at all.
There was a problem hiding this comment.
I think what you did is fine for an initial implementation. Agreed that we may want to get to a point where the set of dynamic tools "kept" in current round isn't the same as the list we "store" for re-use in the next rounds. As you said, we may even not need to carry over tools at all, given we may want to assume that the agent will read the skill again if necessary in next rounds.
But tldr, the approach you use is fine for now ihmo.
There was a problem hiding this comment.
Implementation looks good, solid for a first milestone.
A few remarks, and a bunch of NITs, but overall no major concerns or problems.
Pre-approving (don't feel forced to address all the NITs, only the ones which make sense to you)
The things I think would be good to address before merging:
| import type { SkillTypeDefinition } from '../skills'; | ||
|
|
||
| /** | ||
| * Store to access tool results during execution |
There was a problem hiding this comment.
NIT: typo "tool results" -> "skills" (+ below too)
| * @param input - The tool input configuration (executable or browser) | ||
| * @param options - Optional configuration for tool storage (static vs dynamic) | ||
| */ | ||
| addTool(input: AddToolInput, options?: AddToolOptions): Promise<void>; |
There was a problem hiding this comment.
NIT: addTools given the input defines multiple
| export type { | ||
| ToolManager, | ||
| ToolManagerParams, | ||
| ToolName, | ||
| AddToolOptions, | ||
| ExecutableToolInput, | ||
| BrowserToolInput, | ||
| AddToolInput, | ||
| }; |
There was a problem hiding this comment.
NIT: consistency, use export directly on definitions
| export type SkillBoundedTool<RunInput extends ZodObject<any> = ZodObject<any>> = | ||
| | BuiltinSkillBoundedTool<RunInput> | ||
| | StaticEsqlSkillBoundedTool | ||
| | IndexSearchSkillBoundedTool | ||
| | WorkflowSkillBoundedTool; |
There was a problem hiding this comment.
Note for myself: need to factorize that with the attachment bounded tools later (perfectly fine to keep it that way in the PR, I'll handle it later)
| /** | ||
| * Skill directory structure - explicit about how skills are organized. | ||
| * | ||
| * The purpose of this is to encourage skills to be well organized, grouped logically and consistently. | ||
| * This also requires code owners to review directory changes to ensure skills are discoverable. | ||
| * | ||
| * In order to store skills in a new directory, you need to add the directory to the structure. | ||
| */ | ||
| export type SkillsDirectoryStructure = Directory<{ | ||
| skills: Directory<{ | ||
| platform: FileDirectory; | ||
| observability: FileDirectory<{}>; | ||
| security: FileDirectory<{ | ||
| alerts: FileDirectory<{ | ||
| rules: FileDirectory; | ||
| }>; | ||
| entities: FileDirectory; | ||
| }>; | ||
| search: FileDirectory<{}>; | ||
| }>; | ||
| }>; |
There was a problem hiding this comment.
I agree about having guideline and a "strict" structure to avoid chaos, but idk if the hardcoded structure type is the right approach (death by a thousand review request kind of thing).
Fine keeping it that way for now though, we'll see later if we need to revisit.
(the DirectoryPath type util is quite nice though, I like it)
| '<available_skills>', | ||
| ...skillsFileEntries.flatMap((skillFileEntry) => [ | ||
| ` <skill path="${skillFileEntry.path}">`, | ||
| ` <name>${skillFileEntry.metadata.skill_name}</name>`, | ||
| ` <description>${skillFileEntry.metadata.skill_description}</description>`, | ||
| ` </skill>`, | ||
| ]), | ||
| '</available_skills>', |
There was a problem hiding this comment.
NIT: consistency - please use generateXmlTree for xml formatted content (x-pack/platform/packages/shared/agent-builder/agent-builder-genai-utils/tools/utils/formatting/xml.ts)
| const fileEntries = await filesystem.glob('/**/SKILL.md'); | ||
| const skillsFileEntries = fileEntries | ||
| .filter(isSkillFileEntry) | ||
| .toSorted((a, b) => a.path.localeCompare(b.path)); |
There was a problem hiding this comment.
Is there an upside retrieving the skill via the filestore here? I would have used the skill service / store "directly" instead, even if that doesn't change much
There was a problem hiding this comment.
No real benefit or difference to doing it with the filestore. I did it like this just to be confident that only the skills that actually exist in the volume are added into the system prompt.
| return new SkillTypeRegistryImpl(); | ||
| }; | ||
|
|
||
| class SkillTypeRegistryImpl implements SkillTypeRegistry { |
There was a problem hiding this comment.
NIT: why skillTypeRegistry?
| export const createToolManager = (): ToolManager => { | ||
| return new ToolManager({ | ||
| dynamicToolCapacity: 10, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
I think what you did is fine for an initial implementation. Agreed that we may want to get to a point where the set of dynamic tools "kept" in current round isn't the same as the list we "store" for re-use in the next rounds. As you said, we may even not need to carry over tools at all, given we may want to assume that the agent will read the skill again if necessary in next rounds.
But tldr, the approach you use is fine for now ihmo.
| /** | ||
| * DISCLAIMER: This skill is a sample skill. | ||
| * | ||
| * Threat analysis skill for security operations. | ||
| * This skill helps analyze security threats, investigate alerts, and assess risk. | ||
| */ |
There was a problem hiding this comment.
We should remove that skill yeah, or at least remove the registration from the security plugin ihmo (or at least remove the inline tool with stub data)
⏳ Build in-progress, with failures
Failed CI Steps
Test Failures
History
|
* commit '5c0872d56bc0268177cd3c7150a1685481fb5238': (221 commits) Add .cursorignore file (elastic#251709) [Search] Add descriptions to semantic_text field inference endpoint select (elastic#249265) [Agent Builder] Agent skills implementation (elastic#251209) [Lens] [ES|QL] Improve types for ES|QL conversion. (elastic#251042) Update the trace waterfall to make it easy to understand (elastic#250442) [ES|QL] [Lens] Adds query stats (elastic#251029) [Lens] Fix KQL character escaping when query is generated from Top values column (breakdown). (elastic#250925) fix(kbn-elastic-assistant): fix a11y issue with missing label on flyout (elastic#251656) Update dependency @elastic/monaco-esql to v3.1.16 (main) (elastic#251666) [Automatic Import V2] Add langsmith tracing (elastic#251592) [scout] fix duplicated test failure reports in Buildkite annotations (elastic#251455) chore(NA): remove us-central1-b from gcp zones on high load jobs (elastic#251748) skip flaky suite (elastic#250973) [Lens] Allow read only view for users with write permissions but having no write access to the dashboard (elastic#247746) [CI] Increase artifacts disk to 180gb (elastic#251774) [content-list] 1. Provider Foundation (elastic#251344) [AI Infra] Add missing ES|QL commands and functions documentation for inference tasks (elastic#249089) [docs-utils] 4️⃣ pre-req: Prepare for new validations (elastic#250810) [APM] Extend React flow service map test coverage (elastic#251624) [scout] discover tests with custom server configs (elastic#251297) ... # Conflicts: # src/platform/plugins/shared/dashboard/tsconfig.json # x-pack/platform/plugins/shared/agent_builder_platform/server/tools/create_visualization/create_visualization.ts
Summary
Add Skills Support to Filestore
Fix https://github.com/elastic/search-team/issues/12744
Fix https://github.com/elastic/search-team/issues/12655
Summary
Adds skills support to the filestore system introduced in #250043. Skills are stored as file entries in a virtual filesystem, enabling agents to discover and access skill instructions via standard filestore operations (
read,ls,glob,grep).Changes
SkillsStore Implementation (
skills_store.ts):SkillsStoreImplmanages skills in aMemoryVolume.SkillTypeDefinitionobjects to file entries.Skills Utilities (
utils.ts):createSkillEntries(): Converts skills toFileEntryobjects with metadata.getSkillEntryPath(): Generates standardized paths for skill files.getSkillReferencedContentEntryPath(): Handles referenced content paths.getSkillPlainText(): Formats skills with frontmatter.isSkillFileEntry(): Type guard for skill file entries.Skills Prompts (
prompts.ts):getSkillsInstructions(): Generates XML-formatted skill listings for agent prompts.Store Integration (
create_store.ts):Type Definitions (
types.ts):SkillFileEntry: File entry type for skill files.SkillReferencedContentFileEntry: File entry type for referenced content.Skill-Bound Tools
getAllowedToolsandgetInlineTools.Technical Details
SKILL.mdfiles in paths likeskills/{basePath}/{name}/SKILL.md.readonlyin the filestore.FileEntryType.skillenum.Related PRs
Builds on the filestore infrastructure from #250043.
Checklist
Check the PR satisfies following conditions.
Reviewers should verify this PR satisfies this list as well.
release_note:breakinglabel should be applied in these situations.release_note:*label is applied per the guidelinesbackport:*labels.Identify risks
Does this PR introduce any risks? For example, consider risks like hard to test bugs, performance regression, potential of data loss.
Describe the risk, its severity, and mitigation for each identified risk. Invite stakeholders and evaluate how to proceed before merging.