Skip to content

Skills ​

Skills are reusable instruction packages that give the agent specialized knowledge and procedures. They are loaded at runtime from directories and exposed to the LLM as the use_skill built-in tool.

How skills work ​

Skills follow a two-phase lifecycle: discovery at startup, then activation on demand.

Phase 1: Discovery (at startup) ​

When the application starts, initializeSkillRegistry() queries configured skill sources (built-in filesystem paths or injected sources). For each skill found:

  1. Skill frontmatter is parsed to extract metadata (name, description, tags, model preference) — the body is loaded lazily on demand.
  2. A SkillRecord is created with metadata and source provenance.
  3. Skills with duplicate names are resolved with explicit source-ordered precedence.

After discovery, buildSkillCatalog(metadata) generates a compact text block listing every skill's name, description, and tags. This catalog is composed into the system prompt, so the LLM always knows what skills are available.

initializeSkillRegistry(options)

Frontmatter parsed for each skill

Yields SkillRecord objects without bodies

buildSkillCatalog(metadata)

Produces catalog lines such as "- docker-ops: Docker container management [docker, deployment]"

Catalog composed into system prompt

Phase 2: Activation (when invoked) ​

There are two distinct activation paths:

  • Slash command path (CLI only): The user types /skillname args. The input is parsed, the skill body is loaded from disk, arguments are substituted, @path references are resolved, and the constructed prompt is sent to runAgentLoop.
  • use_skill tool path (all adapters): The AI decides to use a skill based on the catalog in its system prompt. It calls the use_skill tool. The tool handler loads the body, substitutes arguments, and returns the content as a tool result string.

See Two invocation paths for a detailed walkthrough of each path.

Loading skills ​

Automatic discovery ​

Skills are loaded automatically when you specify skill names:

typescript
import { askSeepient } from "seepient";

const result = await askSeepient("Deploy the staging environment", {
  skills: ["docker-ops"],
  tools: ["core", "comm", "advanced"],
});

With createSeepient ​

typescript
import { createSeepient } from "seepient";

const agent = await createSeepient({
  skills: ["docker-ops", "code-review"],
  tools: ["core", "comm"],
});

const reply = await agent.chat("Review my latest commit");

Skill sources ​

In addition to ambient filesystem discovery, Seepient allows embedders to inject external skill sources via the sources option on askSeepient() and createSeepient(), or pass inline skill definitions via the skills option. This enables serverless execution, multi-tenant skill partitioning, and remote database catalogs.

The SkillSource and SkillStore contracts ​

typescript
import type { SkillRecord, SkillSource, SkillStore, SkillLiteral } from "seepient";

// Any object implementing list() satisfies SkillSource
const dbSource: SkillSource = {
  async list(): Promise<SkillRecord[]> {
    return [
      {
        name: "customer-lookup",
        content: "---\nname: customer-lookup\ndescription: Query customer CRM\n---\nProcedure...",
        source: "crm-db", // Attribution label
      },
    ];
  },
};

Inline skill literals ​

For serverless functions, tests, or zero-infrastructure workflows, you can pass skill literals directly using skills:

typescript
import { askSeepient } from "seepient";

const result = await askSeepient("Summarize the latest report", {
  skills: [
    {
      name: "summarize",
      content: "---\nname: summarize\ndescription: Summarize text succinctly\n---\nSummary instructions...",
    },
  ],
});

Inline literals are automatically synthesized into a source attributed as "inline" and appended after any explicit sources. They always shadow skills of the same name.

Composition semantics ​

When constructing the agent's skill catalog, sources are composed in order with last-wins shadowing:

Tenancy ModeComposition PipelineBehavior
single (default)[new FsSkillSources(cwd), ...(sources ?? []), inline?]Built-in filesystem layers load first. Injected sources override filesystem skills of the same name. Inline literals shadow everything. Note: Passing sources without explicitly setting tenancy: "single" automatically upgrades tenancy mode to multi; pass tenancy: "single" explicitly to preserve ambient filesystem discovery alongside injected sources. Passing inline skills literals alone does not upgrade tenancy mode to multi.
multi[...(sources ?? []), inline?]Ambient filesystem discovery is never invoked. Skills originate solely from injected sources and inline literals. Explicitly passing sources automatically upgrades tenancy mode to multi.

Example composing filesystem, organization-wide, and tenant-specific sources:

typescript
import { createSeepient, FsSkillSources } from "seepient";

const agent = await createSeepient({
  sources: [
    new FsSkillSources(process.cwd()), // Local filesystem layers
    globalDbSource,                    // Shared company-wide skills
    tenantDbSource,                    // Tenant-specific overrides (shadows global)
  ],
});

Path reference resolution in injected skills

@path references inside injected/inline skill bodies are NOT resolved on the use_skill tool path (slash-command path only, resolved against process.cwd()); injected skills should avoid @path until 018's invocation rework.

Serverless and bundling disclosures ​

Serverless filesystem discovery fails silently

In serverless execution environments (AWS Lambda, Vercel Functions, Cloudflare Workers), filesystem-based ambient discovery typically finds no skills directories and produces an empty catalog silently. In serverless deployments, always inject explicit sources or pass inline skill literals (skills: [...]).

Source failure partial fail-open

If an injected skill source's list() throws an error, Seepient logs a warning naming the source label and continues composing the remaining sources rather than halting agent startup (partial fail-open).

Bundling skills in serverless packages

If your serverless deployment relies on filesystem-bundled skills, ensure your framework bundler is configured to trace the skills directory into the output artifact. For example, in Next.js, declare outputFileTracingIncludes in next.config.js:

javascript
// next.config.js
module.exports = {
  outputFileTracingIncludes: {
    '/api/**': ['./skills/**/*'],
  },
};

Saving generated skills ​

When Seepient generates a new skill at runtime (e.g. via Spec 016 workflows), or when saving skills programmatically, it saves the skill to the last SkillStore present in the effective sources list:

typescript
import {
  createSeepient,
  saveGeneratedSkill,
  type SkillStore,
  type SkillRecord,
  SkillStoreUnavailableError,
  SkillCollisionError,
} from "seepient";

class TenantSkillStore implements SkillStore {
  async list(): Promise<SkillRecord[]> {
    return [];
  }
  async save(record: SkillRecord): Promise<void> {
    // Persist to tenant database
  }
}

const store = new TenantSkillStore();

// Programmatic skill generation save:
const result = await saveGeneratedSkill({
  name: "summarize-meeting",
  description: "Summarizes meeting notes into action items",
  body: "# Summarize Meeting\nFollow these steps to extract action items...",
  tags: ["productivity", "meetings"],
  sources: [store],
});

console.log(`Saved v${result.version} to store`);

const agent = await createSeepient({
  sources: [store],
});

Key semantics for saveGeneratedSkill:

  • Store destination: Saves to the last SkillStore in sources.
  • Collision refusal: If a skill with the same name exists in any configured source, saving refuses with a SkillCollisionError unless replace: true is set.
  • Version bump: When replace: true is set, the version number automatically increments (e.g. v1 -> v2) and the changelog records the update.
  • Fails closed: If no SkillStore is present in sources, skill generation fails closed with SkillStoreUnavailableError (SKILL_STORE_UNAVAILABLE) rather than writing uncontained files to the local disk.

initializeSkillRegistry() ​

Bootstraps the skill registry by scanning skill directories. This is the public API for initializing skills programmatically:

typescript
import { initializeSkillRegistry } from "seepient";

// Scan the current working directory and configured paths for skills
await initializeSkillRegistry(process.cwd());

Call this at application startup to ensure skills are discovered before the first agent invocation. Seepient Agent calls this automatically when you pass skills to askSeepient() or createSeepient(), but you may call it explicitly to pre-load skills or inspect the registry.

Skill search paths ​

Seepient Agent searches for skills in the following locations, in priority order:

PriorityPathSource
1SEEPIENT_SKILLS_PATH env varColon-separated custom paths
2.seepient/skills/Project-level skills
3/mnt/skills/Docker volume mount
4~/.seepient/skills/Global user skills
5~/.agents/skills/Cross-agent shared skills
6Bundled skills/ directoryShipped with Seepient Agent

Higher-priority paths override skills with the same name from lower-priority paths.

Custom skill paths ​

bash
# Multiple paths, colon-separated
export SEEPIENT_SKILLS_PATH=/opt/skills:/home/user/my-skills
bash
# Disable bundled skills
export SEEPIENT_NO_BUNDLED_SKILLS=1

Bundled skills ​

Seepient ships skills in the bundled skills/ directory. They are discovered automatically (lowest priority, so project/user skills of the same name override them).

SkillPurpose
debug-mantraFour-mantra debugging discipline (reproduce, trace, falsify, cross-reference)
designEntry point for all design work via upstream OpenDesign catalog
docker-opsDocker container and image operations
howProcedural implementation step-by-step guidance
k8s-deployKubernetes deployment operations
log-analyzerLog file analysis and triage
management-talkTranslate engineering content for leadership and management
post-mortemCanonical post-mortem root cause analysis writeup
repo-review-ultra-deepComprehensive, evidence-backed codebase and PR audit
repo-review-ultra-deep-liteToken-efficient fast repository and PR review
scrutinizeOutsider-perspective architectural and change scrutiny
speckit-*SpecKit spec-driven workflow (analyze, checklist, clarify, constitution, implement, plan, specify, tasks, taskstoissues)
unslopRemove AI tells, filler, and repetitive prose patterns
whyArchitectural rationale and purpose investigation

Activate any of them by name:

typescript
import { askSeepient } from "seepient";

// Route any design request through the OpenDesign catalogue
await askSeepient("Design a dark-mode login screen for a fintech app", {
  skills: ["design"],
  tools: ["core", "comm", "advanced"],
});

INFO

design does not ship design procedures of its own. It fetches the upstream skill over the network on each invocation (sending the request externally), so it needs read_website available and outbound network access.

Skill metadata ​

Each skill exposes metadata for discovery and filtering:

typescript
interface SkillMetadata {
  name: string;
  description: string;
  version: string;
  tags: string[];
  allowedTools?: string[];
}

Creating custom skills ​

Skills are defined as SKILL.md files inside a named directory. The file uses YAML frontmatter followed by Markdown instructions:

Directory structure ​

my-skills/
  docker-ops/
    SKILL.md
  code-review/
    SKILL.md

SKILL.md format ​

markdown
---
name: docker-ops
description: Docker container and image management operations
version: 1.2.0
author: engineering-team
tags:
  - docker
  - deployment
  - containers
allowedTools:
  - execute_shell_command
  - read_file
  - write_file
priority: 10
args:
  - environment
  - service
model:
  provider: anthropic
  model: claude-sonnet-4-6-20260320
---

# Docker Operations Skill

You are a Docker operations specialist. Follow these procedures:

## Building Images

When asked to build a Docker image:
1. Check for an existing Dockerfile using read_file
2. Run: `docker build -t $1-$2 .`
3. Verify the image was created successfully

## Deploying

When deploying to environment $1:
1. Pull the latest image
2. Stop the existing container: `docker stop $2`
3. Start the new container: `docker run -d --name $2 ...`

Frontmatter fields ​

FieldTypeRequiredDescription
namestringYesUnique skill identifier (used in skills: [...] option)
descriptionstringYesShort description shown to the LLM for skill selection
versionstringNoSemantic version (defaults to "1.0.0")
authorstringNoSkill author
tagsstring[]NoTags for categorization
allowedToolsstring[]NoRestrict which tools the agent can use when this skill is active
prioritynumberNoHigher priority wins when skills have the same name (default: 0)
argsstring[]NoNamed arguments this skill accepts
modelobjectNoPer-skill model selection (see Provider switching)

Argument substitution ​

Skill bodies support template variables that are replaced at invocation time.

Positional arguments ​

Use $1, $2, ..., $N for positional arguments:

markdown
Build and deploy the $2 service to the $1 environment.

Invoked as: use_skill({ skill_name: "docker-ops", args: ["staging", "api-gateway"] })

Special variables ​

VariableDescription
$ALLAll arguments joined as a single string
$COUNTNumber of arguments passed
$FIRSTFirst argument (same as $1)
$LASTLast argument

Example with argument substitution ​

markdown
---
name: deploy
description: Deploy a service to an environment
args:
  - environment
  - service
---

Deploy the $2 service to $1:

1. Build: `docker build -t $2:$1 .`
2. Push: `docker push registry/$2:$1`
3. Deploy: `kubectl apply -f k8s/$1/$2.yaml --namespace $1`

The use_skill tool ​

Skills are exposed to the LLM as the use_skill built-in tool:

typescript
// The LLM calls this automatically when a user request matches a skill
{
  name: "use_skill",
  arguments: {
    skill_name: "docker-ops",
    args: {
      environment: "staging",
      service: "api-gateway"
    }
  }
}

You do not call use_skill directly. The LLM decides when to activate a skill based on the user's request and the skill descriptions in its system prompt.

@path file references ​

Skills support @path references that inline file contents:

markdown
Review the code in @src/index.ts and check @package.json for dependencies.

Supported patterns:

PatternResolves to
@path/to/fileRelative to project root (process.cwd())
@seepient_documents/file~/seepient_documents/file
@~/path/to/fileExplicit home directory path

Files are inlined with syntax highlighting. Limits:

LimitValueConfigurable
References per input10No
Per-file size1 MBNo
Cumulative total across all inlined files2 MBNo

When the cumulative total exceeds 2 MB, remaining references are skipped with a marker:

[Skipped: cumulative resolved size would exceed 2048KB limit]: @large-file.log

WARNING

All resolved paths must fall within the project root, ~/seepient_documents/, or ~/.seepient/. References outside these boundaries are rejected with an access-denied error.

Lazy body loading ​

Discovery reads only frontmatter (the YAML header) -- the skill body is never parsed at startup. This keeps startup fast regardless of how many skills are installed.

When a skill is invoked, registry.getBody(name) reads the file from disk via skill.filePath, extracts the body text after the --- delimiter, and caches it in an LRU cache with a maximum of 5 entries. When the cache is full, the least-recently-used entry is evicted.

typescript
// Internal flow in DefaultSkillRegistry
async getBody(name: string): Promise<string | undefined> {
  // 1. Check LRU cache
  const cached = this.bodyCache.get(name);
  if (cached !== undefined) return cached;

  // 2. Read from disk, extract body after frontmatter
  const content = await readFile(skill.filePath, 'utf-8');
  const body = extractBody(content);

  // 3. Cache and evict oldest if over capacity
  this.bodyCache.set(name, body);
  if (this.bodyCache.size > 5) { /* evict oldest */ }
}

The first invocation of any skill incurs a ~1--5 ms disk read, which is negligible compared to LLM API latency. At most 5 skill bodies are held in memory at once, regardless of the total number of installed skills.

Skill catalog injection ​

After discovery, buildSkillCatalog(metadata) generates a compact text block from SkillMetadata[]. Each skill gets one line in the format - name: description [tags], typically 40--80 characters.

The catalog is composed into the system prompt when constructing the agent context. Across all surfaces (CLI, SDK, Server), it is included in the available skills block of the system context:

AVAILABLE SKILLS (activate with use_skill tool):
- docker-ops: Docker container and image management operations [docker, deployment, containers]
- code-review: Perform code review [review, quality]
When a user request matches a skill, call use_skill with the skill name.

Body size limits ​

Skill bodies are guarded by a three-layer defense against oversized context injection:

Layer 1: Load-time warning (8K chars / ~2K tokens) ​

When a skill body is read into memory, its length is checked against SEEPIENT_SKILL_BODY_WARN_CHARS (default 8000). If it exceeds the threshold, a warning is emitted:

[SKILLS] Warning: Skill "docker-ops" body is 12450 chars (~3113 tokens).
Consider trimming below 8000 chars for optimal context usage.

Layer 2: Injection-time truncation (32K chars / ~8K tokens) ​

limitSkillBody() enforces a hard cap when the body is about to be injected. If the body exceeds the limit, it is truncated with a clear marker:

[... Skill body truncated: 45000 chars total, 32000 shown.
Reduce skill body size or set SEEPIENT_SKILL_BODY_MAX_CHARS to increase the limit. ...]

The function returns a TruncationResult with metadata about original and final sizes:

typescript
interface TruncationResult {
  body: string;                  // The (possibly truncated) body
  truncated: boolean;            // Whether truncation was applied
  originalChars: number;         // Original size in characters
  originalTokenEstimate: number; // Estimated original tokens (chars / 4)
  finalChars: number;            // Final size in characters
  finalTokenEstimate: number;    // Estimated final tokens (chars / 4)
}

Layer 3: Cumulative @path cap (2 MB total) ​

In interactive CLI mode, the @path resolver inlines file contents passed as arguments in slash command queries. It stops inlining when cumulative resolved content reaches 2 MB; remaining references are replaced with a skip marker.

Configurable thresholds ​

Environment variableDefaultDescription
SEEPIENT_SKILL_BODY_MAX_CHARS32000Hard truncation limit
SEEPIENT_SKILL_BODY_WARN_CHARS8000Soft warning threshold

TIP

If a skill body exceeds the size limit, break it down into modular skills or delegate detailed instructions to specialized sub-skills. Note that @path references are resolved for CLI invocation arguments, not inside skill bodies.

Provider switching ​

Skills can specify a preferred provider and model in their frontmatter:

yaml
---
name: code-review
description: Perform thorough code review
model:
  provider: anthropic
  model: claude-sonnet-4-6-20260320
---

When a skill with a model.provider field is invoked on the interactive slash-command path (REPL, TUI), createRuntimeSkillProviderSwitcher() in src/domain/skills/skill-invoker.ts handles the temporary switch:

  1. Captures the current provider and model.
  2. Creates an override for the provider runtime if the skill specifies a different one.
  3. After the skill execution completes, restores the original provider in a finally block.
typescript
// Conceptual: internal domain runtime usage (interactive slash-command path):
// import { createRuntimeSkillProviderSwitcher } from "./domain/skills/skill-invoker.js";

const switcher = createRuntimeSkillProviderSwitcher(runtime);
const switched = await switcher.switchIfNeeded(skillResult);

try {
  await agent.chat(skillResult.prompt);
} finally {
  if (switched) switcher.restore();
}

:::note Scope and limitations

  • Interactive slash-command path only: Provider switching is scoped to the interactive slash-command path (REPL, TUI). The LLM-initiated use_skill tool path executes within the agent's active model without switching providers.
  • Provider account mapping: The frontmatter model.provider field maps directly to providerAccount in ModelAssignmentOverride (skill-invoker.ts:183). assignment-resolver.ts:104 looks up the configured account by ID; in single-account setups this matches the vendor name (e.g. anthropic), but in multi-account configurations it requires the exact configured account ID.
  • Internal API: createRuntimeSkillProviderSwitcher is an internal domain-layer orchestrator and is not exported from the public seepient SDK entry. :::

Two invocation paths ​

Skills can be activated in two ways: via CLI slash commands or via the use_skill tool. Both paths share argument substitution and body size limits, but differ in reference resolution and provider switching: @path resolution and per-skill provider switching apply to the slash-command path only.

Path A: CLI slash command ​

When the user types a slash-prefixed command in the CLI REPL:

User types: /docker-ops build myapp:latest --no-cache

Step-by-step flow:

  1. Parse: parseInvocation() extracts the skill name and arguments.
    { skillName: "docker-ops", args: { positional: ["build", "myapp:latest", "--no-cache"], raw: "build myapp:latest --no-cache" } }
  2. Lookup: registry.get("docker-ops") returns the skill metadata.
  3. Load body: registry.getBody("docker-ops") reads the file from disk, caches in LRU.
  4. Substitute args: substituteArgs(body, args) replaces $1, $ALL, etc.
  5. Resolve references: resolveReferences(body) inlines @path files.
  6. Enforce limits: limitSkillBody(body) truncates if over 32K chars.
  7. Switch provider: createRuntimeSkillProviderSwitcher() switches provider if the skill specifies model.provider.
  8. Construct prompt: The body becomes a user message.
    [Skill: docker-ops activated]
    
    {skill body with substituted args}
    
    User request: build myapp:latest --no-cache
  9. Execute: runAgentLoop() runs with the constructed prompt.
  10. Restore: switcher.restore() returns the original provider.

Path B: use_skill tool (LLM-initiated) ​

When the AI decides to use a skill based on the catalog in its system prompt:

AI calls: use_skill({ skill_name: "docker-ops", args: { action: "build", image: "myapp:latest" } })

Step-by-step flow:

  1. Tool call received: The use_skill tool handler receives skill_name and optional args.

  2. Lookup: registry.get(skill_name) returns the skill metadata.

  3. Load body: registry.getBody(skill_name) reads from disk, caches in LRU.

  4. Substitute args: If args was provided, values are substituted into the body.

  5. Enforce limits: limitSkillBody(body) truncates if over 32K chars.

  6. Return result: Content is returned as a tool result string.

    # docker-ops Skill Activated
    
    {skill body with substituted args}
    
    ## Skill Arguments
    { "action": "build", "image": "myapp:latest" }
  7. The AI receives the content in the next loop iteration and incorporates it into its response.

INFO

The use_skill tool path does not perform provider switching or @path resolution. It returns the skill content as-is for the AI to use. Provider switching only applies to the CLI slash command path where the full invocation pipeline is used.

Released under the Business Source License 1.1.