diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 5f4647db04..fd2eb68161 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -176,7 +176,7 @@ async get(name: string, options: SkillLookupOptions = {}): Promise ``` -Source: [`packages/core/skill/src/index.ts:113`](../../packages/core/skill/src/index.ts) +Source: [`packages/core/skill/src/index.ts:128`](../../packages/core/skill/src/index.ts) ## `ctx.subagents` — `SubagentService` diff --git a/docs/core-data-structures/core.md b/docs/core-data-structures/core.md index 7f38abe985..6c565766b9 100644 --- a/docs/core-data-structures/core.md +++ b/docs/core-data-structures/core.md @@ -21,6 +21,7 @@ Everything else is documented on a **sub-page**, not here. The rule that draws t | [tools.md](tools.md) | `ToolDefinition` full fields, the schema DSL, `ToolExecution`/`ToolResult`, tool-presentation UI types, the `tools/pre-execute`/`tools/post-execute` pipeline | | [bash.md](bash.md) | the bash executor seam: `BashExecRequest`/`Spec`, `BashRunResult`, background `BashTask`s | | [filesystem.md](filesystem.md) | the filesystem seam: `FsTarget`, read/write/edit outcomes, observed-file state, `FsErrorCode` | +| [skills.md](skills.md) | the skill service: discovery priority, `SkillSummary`/`SkillDefinition`, prompt listing, model-facing `skill` loading | | [compaction.md](compaction.md) | the compaction seam: the `compact/*` session events, `CompactionResult`, the `CompactService` interface | | [subagent.md](subagent.md) | the subagent seam: the named-provider registry, `SubagentStartRequest`/`Result`/`Run`, the start-time-vs-runtime capability split | | [web.md](web.md) | the web access seam: `WebSearchRequest`/`Result`, `WebFetchRequest`/`Result`, `WebFetchBody`, provider/capability status, `WebError` | diff --git a/docs/core-data-structures/skills.md b/docs/core-data-structures/skills.md new file mode 100644 index 0000000000..930bc9f636 --- /dev/null +++ b/docs/core-data-structures/skills.md @@ -0,0 +1,89 @@ +# Skills + +The skill stack is split across two core packages: the service ([dsh-skill](../../packages/core/skill), `ctx.skills`) discovers and parses local `SKILL.md` instructions, injects a stable request-time listing, and exposes full skill bodies on demand; the consumer ([dsh-tool-skill](../../packages/core/tool-skill), model-facing `skill`) loads one complete body for progressive disclosure. Skills are optional instructions, not session events, so their vocabulary lives here rather than in [core.md](core.md). + +Source: [`packages/core/skill/src/index.ts`](../../packages/core/skill/src/index.ts) and [`packages/core/tool-skill/src/index.ts`](../../packages/core/tool-skill/src/index.ts). + +## Discovery priority + +For a request with a cwd, `ctx.skills` finds the nearest git root and scans roots in first-wins order: + +| Priority | Source | Root | +|---|---|---| +| 1 | `project-dsh` | `/.dsh/skills` | +| 2 | `project-agents` | `/.agents/skills` | +| 3 | `runtime` | `ctx.skills.register(...)` | +| 4 | `user-dsh` | `~/.dsh/skills` | +| 5 | `user-agents` | `~/.agents/skills` | +| 6 | `extra` | `Config.extraRoots` | +| 7 | `system` | `~/.dsh/skills/.system` | + +The user DSH root skips its `.system` child during normal scanning so built-in skills are discovered exactly once. Same-name skills keep the highest-priority copy and log a warning for later duplicates. After this priority pass, model-visible summaries are sorted by `name` before prompt rendering so the `## Skills` fragment is deterministic and friendly to provider prefix caches. + +## Skill identity + +Skill names are kebab-case (`^[a-z0-9]+(?:-[a-z0-9]+)*$`). A skill can be a directory bundle (`/SKILL.md`) or a flat Markdown file (`.md`). Nested recursive `**/SKILL.md` discovery is intentionally outside v1. + +```ts type-equiv +type SkillSource = 'project-dsh' | 'project-agents' | 'runtime' | 'user-dsh' | 'user-agents' | 'extra' | 'system' +``` + +## Summaries and complete definitions + +`SkillSummary` is the model-visible shape: the request prompt gets the name, source, description, and optional routing hint, but never the body or absolute file path. `disableModelInvocation` hides a skill from listings while allowing trusted code to load it by name. + +```ts type-equiv +interface SkillSummary { + name: string + description: string + whenToUse?: string + disableModelInvocation?: boolean + directory: string + source: SkillSource +} +``` + +`SkillDefinition` is the complete parsed result returned by `ctx.skills.get()` and used by the `skill` tool. `directory` is the base directory for resolving relative references in the skill body; `path` is present for disk skills; `metadata` preserves optional frontmatter for future consumers without changing v1 routing behavior. + +```ts type-equiv +interface SkillDefinition extends SkillSummary { + content: string + path?: string + metadata?: Record +} +``` + +Runtime skills use the same complete shape and participate in the same first-wins collection order. The returned disposer removes the contribution and invalidates discovery caches. + +```ts type-equiv +type SkillRegistration = Omit & { + disableModelInvocation?: boolean +} +``` + +## Lookup and configuration + +Skill lookup is cwd-sensitive because project skill roots are relative to the current workspace. If no git root is found, the supplied cwd itself is the project root. + +```ts type-equiv +interface SkillLookupOptions { + cwd?: string | undefined +} +``` + +The service can be pointed at alternate user roots in tests or deployments. `installSystemSkills` controls whether bundled system skills are materialized under `/skills/.system` on startup. + +```ts type-equiv +interface Config { + dshHome?: string + agentsHome?: string + extraRoots?: string[] + installSystemSkills?: boolean +} +``` + +## Prompt and tool contract + +`ctx.skills.renderModelListing()` returns a `## Skills` fragment wrapped in ``. Descriptions and `whenToUse` are whitespace-normalized, length-capped, and XML-escaped before rendering. The listing is appended to the same `GenerateOptions.system` string by the `agent/request` waterfall, after the base system prompt is assembled. + +The model-facing `skill({ name })` tool validates the kebab-case name, loads the complete definition for the calling agent cwd, rejects unknown or `disableModelInvocation` skills, and returns a `` block with the body plus base-directory and relative-path guidance. The tool result is the only v1 path that exposes full skill instructions to the model. diff --git a/docs/rfc/README.md b/docs/rfc/README.md index 9633691cf7..8343b877c7 100644 --- a/docs/rfc/README.md +++ b/docs/rfc/README.md @@ -95,6 +95,7 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r | [dsh-hook-protocol — the shared Claude Code / Codex hook wire-protocol core](implemented/feature/2026-06-30-hook-protocol-lib.md) | 2026-06-30 | | [Interception seams — the typed-Decision surface a hook programs against](implemented/feature/2026-06-30-interception-seams.md) | 2026-06-30 | | [Subagent lifecycle enrichment — lastAssistantMessage (observe-only)](implemented/feature/2026-06-30-subagent-observe-enrich.md) | 2026-06-30 | +| [Skill system — progressive disclosure instructions for agents](implemented/feature/2026-07-05-skill-system.md) | 2026-07-05 | ### Simplification diff --git a/docs/rfc/implemented/feature/2026-07-05-skill-system.md b/docs/rfc/implemented/feature/2026-07-05-skill-system.md new file mode 100644 index 0000000000..eea4ac91ae --- /dev/null +++ b/docs/rfc/implemented/feature/2026-07-05-skill-system.md @@ -0,0 +1,45 @@ +# Skill system — progressive disclosure instructions for agents + +## Status + +Implemented. + +## Context + +Agent products have converged on a skill pattern: keep the request prompt small by listing only available instruction bundles, then load the full body when the model decides a task matches. Codex, Claude Code, OpenCode, and Kimi Code differ in details, but all separate discovery metadata from complete instructions so a workspace can carry reusable behavior without paying the full prompt cost on every turn. + +DeepSeek Harness needs the same primitive because project-specific review, plugin-authoring, and tool-usage guidance should live next to the workspace or the user's agent configuration instead of being hard-coded into the loop. The repo is still unreleased, so this change establishes the foundation directly as first-class packages rather than a compatibility layer around an older format. + +## Decision + +Add `@deepseek-ai/dsh-skill` as the discovery service (`ctx.skills`) and `@deepseek-ai/dsh-tool-skill` as the model-facing loader tool. `dsh-agent-core` loads both by default so stdio and ACP apps get the same behavior. + +Discovery scans cwd-sensitive project roots, runtime registrations, user roots, extra roots, and system roots in first-wins priority order: project `.dsh`, project `.agents`, runtime, user `.dsh`, user `.agents`, extra roots, then `~/.dsh/skills/.system`. The user `.dsh/skills` scan skips `.system` so built-ins are not discovered twice. Same-name lower-priority skills are ignored with a warning, which lets project and user skills override built-ins deliberately. + +Each skill is either `/SKILL.md` or `.md` with YAML frontmatter. `name` and `description` are required; `whenToUse`, `disableModelInvocation`, and `metadata` are optional. Names are kebab-case. YAML frontmatter is parsed with the `yaml` package instead of a hand-written parser because the format already exposes an open `metadata` object and should behave like ordinary skill files rather than a bespoke key/value subset. + +The service injects a request-time `## Skills` fragment through the existing `agent/request` waterfall. It appends to `GenerateOptions.system` instead of changing `systemPrompt.assemble()`, because the available project skills depend on the calling agent's cwd. The fragment contains only stable routing metadata and is sorted by skill name after first-wins collection, so equivalent workspaces produce deterministic prompt text and better prefix-cache reuse. Full skill bodies are never included in the listing. + +The `skill({ name })` tool loads one full skill for the current agent cwd and returns a `` block with the body plus base-directory guidance. Invalid names, unknown skills, and skills marked `disableModelInvocation` return tool errors. v1 does not additionally inject the loaded body into session context; the tool result is the model-visible disclosure path. + +System skills are ordinary skill files materialized under `~/.dsh/skills/.system` on startup. v1 ships `dsh-plugin-creator` and `dsh-skill-creator` there so the agent can help author DeepSeek Harness plugins and future skills using the same mechanism users can override. + +The data structures and prompt/tool contract are documented in [skills.md](../../../core-data-structures/skills.md), with service signatures in the generated [services catalog](../../../cordis-catalog/services.md). + +## Rejected alternatives + +**Inject full skill bodies into every system prompt.** Rejected because it destroys progressive disclosure and makes every request pay for instructions that may not apply. + +**Expose skills only as slash commands.** Rejected for v1 because model-initiated loading is the core capability; slash/ACP command advertisement can layer on later without changing discovery. + +**Use a separate system-reminder message.** Rejected for the current loop because `agent/request` already owns the last mutation point before the adapter call and `GenerateOptions.system` is the provider-neutral system prompt surface. A later provider-specific surface can still split this fragment if needed. + +**Recursively discover nested `**/SKILL.md`.** Rejected for v1. Flat files and one-level directory bundles cover the configured roots while keeping duplicate handling and prompt order easy to reason about. + +**Hand-parse frontmatter.** Rejected because the accepted schema includes an open `metadata` object. A narrow parser would either reject valid YAML users expect to work or grow into an unreviewed YAML subset. + +## Consequences + +The agent-core spine now includes one more request-time contributor and one more model-facing tool. Skill discovery is cwd-sensitive, so tests and callers that create agents with different session cwd values can observe different project skill overrides by design. + +The prompt fragment is deterministic for a fixed root set and runtime registration revision, but disk changes are not watched; discovery is memoized until runtime registration invalidates the cache or the process restarts. That keeps v1 simple and avoids adding file watching policy before there is a concrete user flow for hot-reloading skills. diff --git a/packages/core/skill/src/index.ts b/packages/core/skill/src/index.ts index 1d1707c722..ace0d9f6eb 100644 --- a/packages/core/skill/src/index.ts +++ b/packages/core/skill/src/index.ts @@ -25,31 +25,46 @@ export function isSkillName(name: string): boolean { return SKILL_NAME.test(name) } +/** Origin bucket for a discovered skill. The value is prompt-visible metadata, not part of precedence by itself. */ export type SkillSource = 'project-dsh' | 'project-agents' | 'runtime' | 'user-dsh' | 'user-agents' | 'extra' | 'system' +/** Model-visible skill metadata returned by `ctx.skills.list()` and rendered into the request prompt. */ export interface SkillSummary { + /** Kebab-case identifier used with the `skill` tool. */ name: string + /** Short routing description shown to the model. */ description: string + /** Optional extra routing guidance shown to the model. */ whenToUse?: string + /** Whether the skill is hidden from model listings while remaining loadable by trusted callers. */ disableModelInvocation?: boolean + /** Base directory for resolving skill-relative references. */ directory: string + /** Discovery source that produced this winning skill. */ source: SkillSource } +/** Complete parsed skill definition, including the body loaded by `ctx.skills.get()`. */ export interface SkillDefinition extends SkillSummary { + /** Markdown instruction body after frontmatter removal. */ content: string + /** Absolute file path when the skill came from disk; runtime skills may omit it. */ path?: string + /** Parsed optional metadata object from frontmatter. */ metadata?: Record } +/** Runtime skill contribution accepted by `ctx.skills.register()`. */ export type SkillRegistration = Omit & { disableModelInvocation?: boolean } +/** Workspace selector used for cwd-sensitive project-root discovery. */ export interface SkillLookupOptions { cwd?: string | undefined } +/** Skill plugin configuration. */ export interface Config { /** DeepSeek Harness config root. Defaults to `$DSH_HOME` or `~/.dsh`. */ dshHome?: string diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index b1c3163782..13712d872c 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -62,6 +62,13 @@ { "doc": "docs/core-data-structures/filesystem.md", "symbol": "FsPolicyExec", "source": "packages/fs/fs-policy/src/types.ts" }, { "doc": "docs/core-data-structures/filesystem.md", "symbol": "FileReadOutcome", "source": "packages/fs/tool-fs/src/read-render.ts" }, + { "doc": "docs/core-data-structures/skills.md", "symbol": "SkillSource", "source": "packages/core/skill/src/index.ts" }, + { "doc": "docs/core-data-structures/skills.md", "symbol": "SkillSummary", "source": "packages/core/skill/src/index.ts" }, + { "doc": "docs/core-data-structures/skills.md", "symbol": "SkillDefinition", "source": "packages/core/skill/src/index.ts" }, + { "doc": "docs/core-data-structures/skills.md", "symbol": "SkillRegistration", "source": "packages/core/skill/src/index.ts" }, + { "doc": "docs/core-data-structures/skills.md", "symbol": "SkillLookupOptions", "source": "packages/core/skill/src/index.ts" }, + { "doc": "docs/core-data-structures/skills.md", "symbol": "Config", "source": "packages/core/skill/src/index.ts" }, + { "doc": "docs/core-data-structures/compaction.md", "symbol": "CompactionResult", "source": "packages/compact/compact/src/types.ts" }, { "doc": "docs/core-data-structures/subagent.md", "symbol": "SubagentCapabilities", "source": "packages/subagent/subagent/src/types.ts" },