feat(skill): move catalogs into session prefixes

This commit is contained in:
Yichen Jiang
2026-07-10 14:19:06 +08:00
parent b9bf67d0a7
commit 6292d52236
54 changed files with 1019 additions and 691 deletions
+1
View File
@@ -15,6 +15,7 @@ packages/ Harness packages at packages/<group>/<pkg>/, all named @deepseek-ai
llm/ LLM seam + the DeepSeek adapters (hand-rolled + pi-ai design twin)
bash/ bash executor seam + local impl + model-facing bash tools
fs/ filesystem seam + local impl + policy gate + read/write/edit tools
skill/ skill provider registry + local impl + catalog/loader tool
web/ web seam + search/fetch providers + model-facing web tools
compact/ compaction seam + basic backend
subagent/ subagent seam + spawn/fork/ACP backends + delegation tool
+1 -1
View File
@@ -17,7 +17,6 @@ The default distribution is a composition, not a hierarchy. `packages/core/` is
| `ctx.sessions` | `dsh-session` | in-memory event-sourced sessions |
| `ctx.systemPrompt` | `dsh-system-prompt` | ordered prompt sections, tool schemas, and prompt variables |
| `ctx.tools` | `dsh-tools` | tool registry and [execution pipeline](tool-execution-pipeline.md) |
| `ctx.skills` | `dsh-skill` | provider registry for skills and request-time guidance |
| `ctx.agents` | `dsh-agent` | live agent registry, public `Agent` handle, `agent/*` vocabulary |
| `ctx.agentLoop` | `dsh-agent-loop` | shipped `ReactLoopAgent` driver |
@@ -29,6 +28,7 @@ The default distribution is a composition, not a hierarchy. `packages/core/` is
| `ctx.bash` | [`bash/`](../packages/bash/README.md) | foreground/background command execution |
| `ctx.codeRuntime` | [`code-runtime/`](../packages/code-runtime/README.md) | model-written program execution |
| `ctx.fs` | [`fs/`](../packages/fs/README.md) | filesystem provider primitives and policy events |
| `ctx.skills` | [`skill/`](../packages/skill/README.md) | skill provider registry and progressive disclosure |
| `ctx.web` | [`web/`](../packages/web/README.md) | search/fetch provider registries |
| `ctx.compact` | [`compact/`](../packages/compact/README.md) | session-surface compaction |
| `ctx.subagents` | [`subagent/`](../packages/subagent/README.md) | named delegation providers |
+4 -5
View File
@@ -41,10 +41,10 @@ flowchart LR
pkg_stdio_agent["stdio-agent"]
pkg_skill["skill"]
svc_skills["ctx.skills<br/>Skill provider registry"]
pkg_agent_core["agent-core"]
pkg_skill_local["skill-local"]
svc_agents["ctx.agents<br/>Agent registry"]
svc_agentLoop["ctx.agentLoop<br/>Concrete loop driver"]
pkg_agent_core["agent-core"]
pkg_bash["bash"]
svc_bash["ctx.bash<br/>Bash executor seam"]
pkg_bash_local["bash-local"]
@@ -91,6 +91,7 @@ flowchart LR
pkg_session_persistence_jsonl --> svc_sessionPersistence
pkg_session_persistence_sqlite --> svc_sessionPersistence
pkg_skill --> svc_skills
pkg_skill_local --> svc_skills
pkg_stdio_agent --> svc_userInteraction
pkg_subagent --> svc_subagents
pkg_subagent_acp --> svc_subagents
@@ -125,8 +126,6 @@ flowchart LR
svc_sessions --> pkg_invariants
svc_sessions --> pkg_session_persistence
svc_sessions --> pkg_subagent_inprocess
svc_skills --> pkg_agent_core
svc_skills --> pkg_skill_local
svc_skills --> pkg_tool_skill
svc_subagents --> pkg_tool_subagent
svc_systemPrompt --> pkg_agent_loop
@@ -156,9 +155,9 @@ flowchart LR
| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session-persistence/session-persistence), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`invariants`](../packages/support/invariants) | - | Owns append-only Session instances and emits the durable session event feed. |
| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session-persistence/session-persistence) | [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`session-persistence-sqlite`](../packages/session-persistence/session-persistence-sqlite) | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/ui/acp) | - | Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time. |
| `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop), [`tools`](../packages/core/tools), [`tool-fs`](../packages/fs/tool-fs), [`tool-web`](../packages/web/tool-web) | - | Collects prompt sections and model-facing tool schemas for each step. |
| `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tool-bash`](../packages/bash/tool-bash), [`tool-cordis`](../packages/cordis/tool-cordis), [`tool-fs`](../packages/fs/tool-fs), [`tool-skill`](../packages/core/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-todo`](../packages/todo/tool-todo), [`tool-web`](../packages/web/tool-web), [`acp`](../packages/ui/acp) | - | Registers tool definitions, exposes schemas to the prompt, and routes calls through tools/pre-execute and tools/post-execute. |
| `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tool-bash`](../packages/bash/tool-bash), [`tool-cordis`](../packages/cordis/tool-cordis), [`tool-fs`](../packages/fs/tool-fs), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-todo`](../packages/todo/tool-todo), [`tool-web`](../packages/web/tool-web), [`acp`](../packages/ui/acp) | - | Registers tool definitions, exposes schemas to the prompt, and routes calls through tools/pre-execute and tools/post-execute. |
| `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/ui/user-interaction) | [`stdio-agent`](../packages/ui/stdio-agent), [`acp`](../packages/ui/acp) | [`tool-ask-user`](../packages/ui/tool-ask-user), [`stdio-agent`](../packages/ui/stdio-agent), [`acp`](../packages/ui/acp) | - | UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise. |
| `ctx.skills` | `core` | [`skill`](../packages/core/skill) | - | [`agent-core`](../packages/core/agent-core), [`skill-local`](../packages/core/skill-local), [`tool-skill`](../packages/core/tool-skill) | - | Merges provider skill catalogs, injects request-time listings, and serves full skill bodies to the skill tool. |
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-local`](../packages/skill/skill-local) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. |
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/ui/acp), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`stdio-agent`](../packages/ui/stdio-agent), [`invariants`](../packages/support/invariants) | - | Owns live Agent handles and the create/resume factory seam. |
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-core`](../packages/core/agent-core) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
| `ctx.bash` | `seam` | [`bash`](../packages/bash/bash) | [`bash-local`](../packages/bash/bash-local) | [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | - | The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors can replace bash-local. |
+27 -14
View File
@@ -53,7 +53,7 @@ export interface Config {
toolOrder?: string[]
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
persistenceRoot?: string
/** Skill registry/local-provider config forwarded to the shared agent-core spine. */
/** Skill registry, local-provider, and model-facing consumer config forwarded to agent-core. */
skills?: agentCore.SkillConfig
}
```
@@ -70,7 +70,7 @@ Source: [`packages/ui/acp-agent/src/index.ts:50`](../packages/ui/acp-agent/src/i
* `agents` to the agent loop (an app that pre-creates no agents, like the ACP
* bridge, simply omits it), `persona` and `toolOrder` to the system-prompt
* plugin (the deployment's persona section and the explicit model-facing tool
* order), and `skills` to the skill registry/local provider. Every field is
* order), and `skills` to the skill registry/local provider/tool consumer. Every field is
* optional INPUT here because each owner's schema supplies the default (`[]` /
* `''` / absent — lexicographic / the DSH skill roots); the schema is the
* INTERSECTION of the owners' own schemas, so validation and defaulting can
@@ -83,22 +83,24 @@ export interface Config {
persona?: SystemPromptConfig['persona']
/** The explicit model-facing tool order (see dsh-system-prompt's `Config`). */
toolOrder?: SystemPromptConfig['toolOrder']
/** Skill registry and local provider config. */
/** Skill registry, local provider, and model-facing consumer config. */
skills?: SkillConfig
}
/** Skill bundle config forwarded to the registry and the local provider. */
/** Skill bundle config forwarded to the registry, local provider, and model-facing consumer. */
export interface SkillConfig {
/** Registry-level prompt/cache settings. */
/** Registry-level discovery cache settings. */
registry?: SkillRegistryConfig
/** Local filesystem skill provider settings. */
local?: SkillLocal.Config
/** Model-facing skill catalog and tool settings. */
tool?: toolSkill.Config
}
```
Depends on: [`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`SkillLocal`](../packages/core/skill-local/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt)
Depends on: [`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`SkillLocal`](../packages/skill/skill-local/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt) · [`toolSkill`](../packages/skill/tool-skill/src/index.ts)
Source: [`packages/core/agent-core/src/index.ts:84`](../packages/core/agent-core/src/index.ts)
Source: [`packages/core/agent-core/src/index.ts:86`](../packages/core/agent-core/src/index.ts)
## `@deepseek-ai/dsh-agent-loop`
@@ -496,14 +498,12 @@ Source: [`packages/session-persistence/session-persistence-sqlite/src/index.ts:5
```ts config-catalog
/** Skill registry configuration. */
export interface Config {
/** Maximum rendered description/whenToUse length in the prompt listing; minimum 3. */
promptFieldMaxLength?: number
/** Maximum number of cwd/provider discovery promises kept in the in-memory cache. */
/** Maximum number of completed cwd/provider catalog snapshots kept in memory. */
collectCacheMaxEntries?: number
}
```
Source: [`packages/core/skill/src/index.ts:111`](../packages/core/skill/src/index.ts)
Source: [`packages/skill/skill/src/index.ts:112`](../packages/skill/skill/src/index.ts)
## `@deepseek-ai/dsh-skill-local`
@@ -521,7 +521,7 @@ export interface Config {
}
```
Source: [`packages/core/skill-local/src/index.ts:39`](../packages/core/skill-local/src/index.ts)
Source: [`packages/skill/skill-local/src/index.ts:39`](../packages/skill/skill-local/src/index.ts)
## `@deepseek-ai/dsh-stdio-agent`
@@ -547,7 +547,7 @@ export interface Config {
persistenceRoot?: string
/** stdin-chat banner printed once on start. Defaults to `'ready.'`. */
welcome?: string
/** Skill registry/local-provider config forwarded to the shared agent-core spine. */
/** Skill registry, local-provider, and model-facing consumer config forwarded to agent-core. */
skills?: agentCore.SkillConfig
/**
* If set, the `main` agent RESUMES this persisted session id instead of
@@ -763,6 +763,20 @@ export interface Config {
Source: [`packages/fs/tool-fs/src/index.ts:48`](../packages/fs/tool-fs/src/index.ts)
## `@deepseek-ai/dsh-tool-skill`
Requires: `tools` · `skills`
```ts config-catalog
/** Model-facing skill catalog configuration. */
export interface Config {
/** Maximum normalized description length rendered in the session catalog; minimum 3. */
catalogDescriptionMaxLength?: number
}
```
Source: [`packages/skill/tool-skill/src/index.ts:19`](../packages/skill/tool-skill/src/index.ts)
## `@deepseek-ai/dsh-tool-subagent`
Requires: `tools` · `subagents`
@@ -941,7 +955,6 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
- `@deepseek-ai/dsh-timeout-policy` — requires `tools` ([`packages/timeout/timeout-policy/src/index.ts`](../packages/timeout/timeout-policy/src/index.ts))
- `@deepseek-ai/dsh-tool-ask-user` — requires `tools` · `userInteraction` ([`packages/ui/tool-ask-user/src/index.ts`](../packages/ui/tool-ask-user/src/index.ts))
- `@deepseek-ai/dsh-tool-bash` — requires `tools` · `bash` · `systemPrompt` ([`packages/bash/tool-bash/src/index.ts`](../packages/bash/tool-bash/src/index.ts))
- `@deepseek-ai/dsh-tool-skill` — requires `tools` · `skills` ([`packages/core/tool-skill/src/index.ts`](../packages/core/tool-skill/src/index.ts))
- `@deepseek-ai/dsh-tool-todo` — requires `tools` ([`packages/todo/tool-todo/src/index.ts`](../packages/todo/tool-todo/src/index.ts))
- `@deepseek-ai/dsh-tools` — requires `systemPrompt` ([`packages/core/tools/src/index.ts`](../packages/core/tools/src/index.ts))
- `@deepseek-ai/dsh-user-interaction` ([`packages/ui/user-interaction/src/index.ts`](../packages/ui/user-interaction/src/index.ts))
+2 -2
View File
@@ -259,7 +259,7 @@ A skill provider became resolvable in the `ctx.skills` registry. Consumers can o
'skill/provider-added'(provider: SkillProvider): void
```
Source: [`packages/core/skill/src/index.ts:131`](../../packages/core/skill/src/index.ts)
Source: [`packages/skill/skill/src/index.ts:130`](../../packages/skill/skill/src/index.ts)
### `skill/provider-removed` — emit
@@ -269,7 +269,7 @@ A skill provider left the registry because its plugin fiber was disposed.
'skill/provider-removed'(name: string): void
```
Source: [`packages/core/skill/src/index.ts:137`](../../packages/core/skill/src/index.ts)
Source: [`packages/skill/skill/src/index.ts:136`](../../packages/skill/skill/src/index.ts)
## `subagent/*`
+2 -3
View File
@@ -189,17 +189,16 @@ Source: [`packages/core/session/src/index.ts:405`](../../packages/core/session/s
## `ctx.skills` — `SkillService`
Registry of skill providers. It merges provider catalogs with stable first-wins duplicate handling, exposes sorted model-visible summaries, loads full skill bodies on demand, and renders the request-time catalog fragment.
Registry of skill providers. It merges provider catalogs with stable first-wins duplicate handling, exposes sorted model-visible summaries, and loads full skill bodies on demand.
```ts cordis-catalog
registerProvider(provider: SkillProvider): () => void
register(skill: SkillRegistration): () => void
async list(options: SkillLookupOptions = {}): Promise<SkillSummary[]>
async get(name: string, options: SkillLookupOptions = {}): Promise<SkillDefinition | undefined>
async renderModelListing(options: SkillLookupOptions = {}): Promise<string>
```
Source: [`packages/core/skill/src/index.ts:158`](../../packages/core/skill/src/index.ts)
Source: [`packages/skill/skill/src/index.ts:157`](../../packages/skill/skill/src/index.ts)
## `ctx.subagents` — `SubagentService`
+1 -1
View File
@@ -23,7 +23,7 @@ Everything else is documented on a **sub-page**, not here. The rule that draws t
| [bash.md](bash.md) | the bash executor seam: `BashExecRequest`/`Spec`, `BashRunResult`, background `BashTask`s |
| [code-runtime.md](code-runtime.md) | the code-execution seam: `CodeRunRequest`/`Result`, binding namespaces, captured logs, the `CodeRunFailure` taxonomy |
| [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 |
| [skills.md](skills.md) | the skill service: discovery priority, `SkillSummary`/`SkillDefinition`, session-prefix catalog, 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` |
+10 -10
View File
@@ -1,12 +1,12 @@
# Skills
The skill stack is split across three core packages: the registry ([dsh-skill](../../packages/core/skill), `ctx.skills`) merges provider catalogs and renders request-time guidance; the local provider ([dsh-skill-local](../../packages/core/skill-local)) scans project/custom/user directories; 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).
The [skill capability family](../../packages/skill) is split across three packages: the registry ([dsh-skill](../../packages/skill/skill), `ctx.skills`) merges provider catalogs; the local provider ([dsh-skill-local](../../packages/skill/skill-local)) scans project/custom/user directories; the consumer ([dsh-tool-skill](../../packages/skill/tool-skill)) owns the session-prefix catalog and model-facing `skill` tool. 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), [`packages/core/skill-local/src/index.ts`](../../packages/core/skill-local/src/index.ts), and [`packages/core/tool-skill/src/index.ts`](../../packages/core/tool-skill/src/index.ts).
Source: [`packages/skill/skill/src/index.ts`](../../packages/skill/skill/src/index.ts), [`packages/skill/skill-local/src/index.ts`](../../packages/skill/skill-local/src/index.ts), and [`packages/skill/tool-skill/src/index.ts`](../../packages/skill/tool-skill/src/index.ts).
## Provider registry
`ctx.skills` is a multi-provider registry. Providers can represent local directories, embedded plugin data, HTTP catalogs, or another source. The registry validates candidates, resolves duplicate skill names first-wins by rank/provider order/local order, and sorts the final model-visible catalog by `name` for deterministic prompt text. A provider `list()` rejection is logged and skipped without caching the degraded catalog; malformed candidates still fail fast because they violate the provider contract.
`ctx.skills` is a multi-provider registry. Providers can represent local directories, embedded plugin data, HTTP catalogs, or another source. Provider plugins register synchronously during `apply()`; remote initialization, authentication, and discovery are awaited by `list()`. The registry validates candidates, resolves duplicate skill names first-wins by rank/provider order/local order, and sorts the final summaries by `name` for deterministic consumers. A provider `list()` rejection is logged and skipped without caching the degraded catalog; malformed candidates still fail fast because they violate the provider contract.
```ts type-equiv
interface SkillProvider {
@@ -40,7 +40,7 @@ type SkillSource = 'project-dsh' | 'project-agents' | 'runtime' | 'user-dsh' | '
## Summaries, candidates, and complete definitions
`SkillSummary` is the model-visible shape: the request prompt gets name, source, provider, 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.
`SkillSummary` is the registry's model-invocable summary shape. Consumers choose which fields to render; the session catalog uses only `name` and `description`, never the body or absolute file path. `disableModelInvocation` hides a skill from model listings while allowing trusted code to load it by name.
```ts type-equiv
interface SkillSummary {
@@ -92,25 +92,25 @@ type SkillRegistration = Omit<SkillDefinition, 'provider'> & {
## Lookup and configuration
Skill lookup is cwd-sensitive because providers may expose workspace-local skills. If no git root is found, the local provider treats the supplied cwd itself as the project root.
Skill lookup is cwd-sensitive because providers may expose workspace-local skills, and its optional signal cancels provider work for the caller. If no git root is found, the local provider treats the supplied cwd itself as the project root.
```ts type-equiv
interface SkillLookupOptions {
cwd?: string | undefined
signal?: AbortSignal | undefined
}
```
The registry owns prompt/cache bounds. The local provider owns filesystem roots (`dshHome`, `agentsHome`, and `customSkillDirs`).
The registry owns only its discovery-cache bound. The local provider owns filesystem roots (`dshHome`, `agentsHome`, and `customSkillDirs`). The consumer owns its catalog description bound.
```ts type-equiv
interface Config {
promptFieldMaxLength?: number
collectCacheMaxEntries?: number
}
```
## Prompt and tool contract
## Session catalog and tool contract
`ctx.skills.renderModelListing()` returns a `## Skills` fragment wrapped in `<available_skills>`. Descriptions and `whenToUse` are whitespace-normalized, length-capped, XML-escaped, and have `{{` / `}}` split before rendering so skill metadata cannot be parsed as prompt-template variables. The listing is appended as a late `system-prompt/assemble` section for the calling agent, so it remains cwd-sensitive while still flowing through the reconstructable system-prompt path.
`dsh-tool-skill` contributes a user-role `<system-reminder>` through `agent/session-prefix`. The catalog contains sorted skill `name` and normalized, XML-escaped `description` only; it omits bodies, paths, sources, providers, and routing hints. Prefix discovery forwards the caller's abort signal through `SkillLookupOptions`. `catalogDescriptionMaxLength` is the consumer config for the description bound, with default `500` and integer minimum `3`. Its request-only, header-logged lifecycle is defined by the [session-prefix RFC](../rfc/implemented/feature/2026-07-07-session-prefix.md).
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 `<skill_content name="...">` block with the body plus provider resource guidance. The tool result is the only v1 path that exposes full skill instructions to the model.
The model-facing `skill({ name })` tool validates the kebab-case name, loads the complete definition for the calling agent cwd, reports an unresolved skill as unknown or no longer available, rejects `disableModelInvocation` skills, and returns a tool result containing `<skill_content name="...">`, `<skill_resources>`, and `<skill_instructions>`. `resourceBase` resolves explicitly referenced scripts, references, and assets only as needed; the loaded result does not enumerate a skill directory. The tool result is the model-visible path for complete instructions.
+4 -4
View File
@@ -14,7 +14,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:370`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) |
| `agent/queued` | `emit` | [`packages/core/agent/src/types.ts:290`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - |
| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:394`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
| `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:441`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
| `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:441`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`tool-skill`](../packages/skill/tool-skill) |
| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:305`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:281`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/ui/acp), [`invariants`](../packages/support/invariants), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`stdio-agent`](../packages/ui/stdio-agent) |
| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:451`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
@@ -26,13 +26,13 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `session/created` | `emit` | [`packages/core/session/src/index.ts:39`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`emit`) | [`invariants`](../packages/support/invariants), [`session-persistence`](../packages/session-persistence/session-persistence) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:47`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`emit`) | [`acp`](../packages/ui/acp), [`invariants`](../packages/support/invariants), [`session-persistence`](../packages/session-persistence/session-persistence), [`stdio-agent`](../packages/ui/stdio-agent) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:57`](../packages/core/session/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`parallel`) | [`session-persistence`](../packages/session-persistence/session-persistence) |
| `skill/provider-added` | `emit` | [`packages/core/skill/src/index.ts:131`](../packages/core/skill/src/index.ts) | [`skill`](../packages/core/skill) (`emit`) | - |
| `skill/provider-removed` | `emit` | [`packages/core/skill/src/index.ts:137`](../packages/core/skill/src/index.ts) | [`skill`](../packages/core/skill) (`emit`) | - |
| `skill/provider-added` | `emit` | [`packages/skill/skill/src/index.ts:130`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`emit`) | - |
| `skill/provider-removed` | `emit` | [`packages/skill/skill/src/index.ts:136`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`emit`) | - |
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:98`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude) |
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:72`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:83`](../packages/subagent/subagent/src/index.ts) | - | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:91`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude) |
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:38`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`skill`](../packages/core/skill) |
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:38`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | - |
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:44`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:118`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - |
| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:97`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`timeout-policy`](../packages/timeout/timeout-policy) |
+15 -15
View File
@@ -21,10 +21,7 @@ flowchart TD
pkg_agent_core["agent-core"]
pkg_agent_loop["agent-loop"]
pkg_session["session"]
pkg_skill["skill"]
pkg_skill_local["skill-local"]
pkg_system_prompt["system-prompt"]
pkg_tool_skill["tool-skill"]
pkg_tools["tools"]
end
subgraph group_bash["packages/bash"]
@@ -38,6 +35,11 @@ flowchart TD
pkg_fs_policy["fs-policy"]
pkg_tool_fs["tool-fs"]
end
subgraph group_skill["packages/skill"]
pkg_skill["skill"]
pkg_skill_local["skill-local"]
pkg_tool_skill["tool-skill"]
end
subgraph group_compact["packages/compact"]
pkg_compact["compact"]
pkg_compact_basic["compact-basic"]
@@ -117,6 +119,8 @@ flowchart TD
pkg_agent --> pkg_system_prompt
pkg_fs_local --> pkg_fs
pkg_fs_policy --> pkg_fs
pkg_skill_local --> pkg_fs
pkg_skill_local --> pkg_skill
pkg_compact --> pkg_llm
pkg_compact --> pkg_session
pkg_web_fetch_local --> pkg_timeout
@@ -129,8 +133,6 @@ flowchart TD
pkg_session_persistence --> pkg_session
pkg_llm_replay --> pkg_llm
pkg_llm_replay --> pkg_session
pkg_skill --> pkg_agent
pkg_skill --> pkg_system_prompt
pkg_tools --> pkg_agent
pkg_tools --> pkg_llm
pkg_tools --> pkg_system_prompt
@@ -153,12 +155,6 @@ flowchart TD
pkg_agent_loop --> pkg_session_persistence
pkg_agent_loop --> pkg_system_prompt
pkg_agent_loop --> pkg_tools
pkg_skill_local --> pkg_fs
pkg_skill_local --> pkg_skill
pkg_tool_skill --> pkg_agent
pkg_tool_skill --> pkg_llm
pkg_tool_skill --> pkg_skill
pkg_tool_skill --> pkg_tools
pkg_tool_bash --> pkg_agent
pkg_tool_bash --> pkg_bash
pkg_tool_bash --> pkg_llm
@@ -169,6 +165,10 @@ flowchart TD
pkg_tool_fs --> pkg_session
pkg_tool_fs --> pkg_system_prompt
pkg_tool_fs --> pkg_tools
pkg_tool_skill --> pkg_agent
pkg_tool_skill --> pkg_llm
pkg_tool_skill --> pkg_skill
pkg_tool_skill --> pkg_tools
pkg_subagent --> pkg_agent
pkg_subagent --> pkg_llm
pkg_subagent --> pkg_tools
@@ -257,6 +257,7 @@ flowchart TD
| --- | --- | --- |
| [`brand`](../packages/util/brand) | `util` | — |
| [`timeout`](../packages/util/timeout) | `util` | — |
| [`skill`](../packages/skill/skill) | `skill` | — |
| [`acp-snapshot`](../packages/support/acp-snapshot) | `support` | — |
| [`app-boot`](../packages/ui/app-boot) | `ui` | — |
| [`code-runtime`](../packages/code-runtime/code-runtime) | `code-runtime` | — |
@@ -273,6 +274,7 @@ flowchart TD
| [`agent`](../packages/core/agent) | `core` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
| [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs) |
| [`fs-policy`](../packages/fs/fs-policy) | `fs` | [`fs`](../packages/fs/fs) |
| [`skill-local`](../packages/skill/skill-local) | `skill` | [`fs`](../packages/fs/fs), [`skill`](../packages/skill/skill) |
| [`compact`](../packages/compact/compact) | `compact` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`web-fetch-local`](../packages/web/web-fetch-local) | `web` | [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`web`](../packages/web/web) |
@@ -281,7 +283,6 @@ flowchart TD
| [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`bash`](../packages/bash/bash), [`session`](../packages/core/session) |
| [`session-persistence`](../packages/session-persistence/session-persistence) | `session-persistence` | [`session`](../packages/core/session) |
| [`llm-replay`](../packages/support/llm-replay) | `support` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`skill`](../packages/core/skill) | `core` | [`agent`](../packages/core/agent), [`system-prompt`](../packages/core/system-prompt) |
| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt) |
| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl) | `session-persistence` | [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence) |
@@ -289,10 +290,9 @@ flowchart TD
| [`invariants`](../packages/support/invariants) | `support` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`user-interaction`](../packages/ui/user-interaction) | `ui` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm) |
| [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`skill-local`](../packages/core/skill-local) | `core` | [`fs`](../packages/fs/fs), [`skill`](../packages/core/skill) |
| [`tool-skill`](../packages/core/tool-skill) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/core/skill), [`tools`](../packages/core/tools) |
| [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`tools`](../packages/core/tools) |
| [`tool-web`](../packages/web/tool-web) | `web` | [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) |
| [`timeout-policy`](../packages/timeout/timeout-policy) | `timeout` | [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
@@ -302,7 +302,7 @@ flowchart TD
| [`acp`](../packages/ui/acp) | `ui` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) |
| [`tool-ask-user`](../packages/ui/tool-ask-user) | `ui` | [`agent`](../packages/core/agent), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) |
| [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | `guard` | [`agent`](../packages/core/agent), [`tools`](../packages/core/tools) |
| [`agent-core`](../packages/core/agent-core) | `core` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`skill`](../packages/core/skill), [`skill-local`](../packages/core/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/bash/tool-bash), [`tool-skill`](../packages/core/tool-skill), [`tools`](../packages/core/tools) |
| [`agent-core`](../packages/core/agent-core) | `core` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/bash/tool-bash), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) |
| [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent) |
| [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
@@ -6,44 +6,44 @@ Status: implemented
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.
DeepSeek Harness uses the same primitive so project-specific review, plugin-authoring, and tool-usage guidance lives next to the workspace or the user's agent configuration instead of being hard-coded into the loop.
## Decision
Add `@deepseek-ai/dsh-skill` as the provider registry (`ctx.skills`), `@deepseek-ai/dsh-skill-local` as the shipped local filesystem provider, and `@deepseek-ai/dsh-tool-skill` as the model-facing loader tool. `dsh-agent-core` loads the registry, local provider, and tool by default so stdio and ACP apps get the same behavior while future providers can contribute embedded or remote skills without changing the registry or tool.
`@deepseek-ai/dsh-skill` is the pure provider registry (`ctx.skills`), `@deepseek-ai/dsh-skill-local` is the shipped local filesystem provider, and `@deepseek-ai/dsh-tool-skill` owns the session-prefix catalog and model-facing loader tool. `dsh-agent-core` loads the registry, local provider, and consumer by default so stdio and ACP apps get the same behavior while embedded or remote providers contribute skills without changing the registry or consumer. Its `skills` config forwards `registry`, `local`, and `tool` branches to those owners.
Provider catalogs return ranked candidates. The registry validates each candidate, resolves same-name skills first-wins by rank, provider registration order, and provider-local order, then sorts model-visible summaries by skill name for deterministic prompt text. Runtime `ctx.skills.register(...)` remains a convenience for embedded in-process skills and uses project-over-user priority; `runtime` is reserved as the registry-owned provider name.
Provider plugins register synchronously during `apply()`. Provider catalogs return ranked candidates from awaited `list()` calls, where remote providers perform initialization, authentication, and discovery while honoring the lookup abort signal. The registry validates each candidate, resolves same-name skills first-wins by rank, provider registration order, and provider-local order, then sorts summaries by skill name for deterministic consumers. It caches only completed catalog snapshots and retries when a provider/runtime revision changes during discovery, so an unload cannot freeze a stale, unresolvable skill into a session prefix. Runtime `ctx.skills.register(...)` remains a convenience for embedded in-process skills and uses project-over-user priority; `runtime` is reserved as the registry-owned provider name.
The local provider scans cwd-sensitive project roots, custom roots, and user roots in first-wins rank order: project `.dsh`, project `.agents`, `customSkillDirs`, user `.dsh`, then user `.agents`. The user `.dsh/skills` scan skips `.system` so a system-owned directory is not treated as normal user content. DeepSeek Harness does not ship built-in system skills in v1; plugin-authoring skills can be supplied later by another provider.
The local provider scans cwd-sensitive project roots, custom roots, and user roots in first-wins rank order: project `.dsh`, project `.agents`, `customSkillDirs`, user `.dsh`, then user `.agents`. The user `.dsh/skills` scan skips `.system` so a system-owned directory is not treated as normal user content. DeepSeek Harness does not ship built-in system skills; embedded or remote providers supply additional skills when configured.
Each skill is either `<name>/SKILL.md` or `<name>.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 `js-yaml` or a hand-written parser: `yaml` is the already-declared modern parser for this package's limited frontmatter needs, and a narrow parser would either reject valid YAML users expect to work or grow into an unreviewed YAML subset.
Local skill filesystem I/O goes through `ctx.fs` when a filesystem service is loaded: project-root lookup probes `.git` with `resolve` and `stat`, root discovery uses `listDir`, and skill reads use `readText`. The Node filesystem remains a fallback for minimal contexts that mount `dsh-skill-local` without the fs seam. Missing roots, unreadable or malformed skill files, and transient provider `list()` failures degrade to warn-and-skip so one bad source does not make every agent request fail; malformed candidates still fail fast because they are provider contract violations.
The service injects a request-time `## Skills` fragment through the existing `system-prompt/assemble` waterfall. It appends a late section for the calling agent instead of mutating `GenerateOptions.system` in `agent/request`, because request configuration is now reconstructable model/sampling state while model-visible content flows through system prompt assembly. The fragment contains only stable routing metadata, splits `{{` / `}}` before template rendering, 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.
`dsh-tool-skill` contributes one user-role `<system-reminder>` catalog through [`agent/session-prefix`](2026-07-07-session-prefix.md). The catalog contains sorted skill name and description only; it excludes bodies, paths, sources, providers, and routing hints. Descriptions are whitespace-normalized, XML-escaped, and capped by `catalogDescriptionMaxLength`, whose default is `500` and minimum is `3`. The session-prefix seam freezes the request-only catalog per loop instance and records it in the request header, preserving reconstructability without adding it to durable history. Full skill bodies are never included in the catalog.
The `skill({ name })` tool loads one full skill for the current agent cwd and returns a `<skill_content name="...">` block with the body plus provider resource guidance. Local filesystem skills include base-directory guidance; embedded or remote providers can return URL or opaque provider-managed 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.
The `skill({ name })` tool loads one full skill for the current agent cwd and returns a tool result containing `<skill_content name="...">`, `<skill_resources>`, and `<skill_instructions>`. `resourceBase` supplies a directory, URL, or opaque provider-managed base for explicitly referenced scripts, references, and assets; resources load only as needed, without directory enumeration. An unresolved name reports that the skill is unknown or no longer available; invalid names and skills marked `disableModelInvocation` retain distinct tool errors. The tool result is the model-visible disclosure path.
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).
The data structures and catalog/tool contract are documented in [skills.md](../../../core-data-structures/skills.md), with service signatures in the generated [services catalog](../../../cordis-catalog/services.md).
## Alternatives considered
**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.
**Expose skills only as slash commands.** Rejected because model-initiated loading is the core capability; slash/ACP command advertisement does not change discovery.
**Put local filesystem scanning directly inside `ctx.skills`.** Rejected because coding agents, web agents, and future plugin ecosystems need different skill sources. A provider registry mirrors the subagent seam: the registry owns conflict resolution and consumers, while implementations own loading.
**Use a separate system-reminder message.** Rejected for the current loop because the provider-neutral system prompt surface is assembled through `system-prompt/assemble`. A later provider-specific surface can still split this fragment if needed.
**Use a system-prompt section.** Rejected because the rendered system prompt is a single string, while the catalog is a user-role `<system-reminder>` message with request-only lifecycle requirements. [`agent/session-prefix`](2026-07-07-session-prefix.md) is the selected mechanism: it places the catalog ahead of derived history and records the composed message in the request header.
**Materialize built-in DSH authoring skills under `~/.dsh/skills/.system`.** Rejected for v1 because bundled skills should not write user home on startup, and the product can receive those skills from a later embedded or remote provider.
**Materialize built-in DSH authoring skills under `~/.dsh/skills/.system`.** Rejected because bundled skills do not write user home on startup, and embedded or remote providers supply configured skills.
**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.
**Recursively discover nested `**/SKILL.md`.** Rejected. Flat files and one-level directory bundles cover the configured roots while keeping duplicate handling and catalog 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, one local provider, and one 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 agent-core spine includes one session-prefix contributor, one local provider, and one model-facing tool. Skill discovery is cwd-sensitive, so 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.
The catalog 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.
+2 -2
View File
@@ -348,7 +348,7 @@ The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-policy` (an `
### `skill`
Load the full instructions for one available skill by name. Use this when the current task matches a skill listed in the system prompt.
Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.
```json
{
@@ -365,7 +365,7 @@ Load the full instructions for one available skill by name. Use this when the cu
}
```
Source: [`packages/core/tool-skill/src/index.ts`](../packages/core/tool-skill/src/index.ts)
Source: [`packages/skill/tool-skill/src/index.ts`](../packages/skill/tool-skill/src/index.ts)
## `@deepseek-ai/dsh-tool-subagent`
@@ -1,29 +1,29 @@
{"type":"session","version":0,"id":"7c71aa6d-03f6-4b23-a997-5aa6304ce44e","createdAt":1783609396672,"cwd":"/var/folders/_g/59jgff8x2gqd39f5vy1wnbfc0000gn/T/acp-snap-cwd-ptkWg8"}
{"type":"turn/start","seq":0,"time":1783609396673,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
{"type":"user/message","seq":1,"time":1783609396674,"data":{"content":[{"type":"text","text":"Load the dsh-skill-creator skill with the skill tool, then reply DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"}
{"type":"step/start","seq":2,"time":1783609396682,"data":{"turn":1,"step":1}}
{"type":"request/header","seq":3,"time":1783609396683,"data":{"header":{"config":{"model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"assistant/chunk","seq":4,"time":1783609396683,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
{"type":"assistant/chunk","seq":5,"time":1783609396683,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"Load the requested skill."}}}
{"type":"assistant/chunk","seq":6,"time":1783609396683,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":7,"time":1783609396683,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_skill_load","name":"skill","argumentsDelta":"{\"name\":\"dsh-skill-creator\"}"}}}
{"type":"assistant/chunk","seq":8,"time":1783609396683,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Load the requested skill."}}}}
{"type":"assistant/chunk","seq":9,"time":1783609396683,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_skill_load","name":"skill","arguments":"{\"name\":\"dsh-skill-creator\"}"}}}}
{"type":"assistant/chunk","seq":10,"time":1783609396683,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":100,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":5}}}}
{"type":"assistant/chunk","seq":11,"time":1783609396683,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":12,"time":1783609396684,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"Load the requested skill."},{"type":"tool-call","id":"call_skill_load","name":"skill","arguments":"{\"name\":\"dsh-skill-creator\"}"}],"usage":{"inputTokens":100,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":5}},"sourceEventSeqs":[4,5,6,7,8,9,10,11],"surfaceOp":"append"}
{"type":"tool/call","seq":13,"time":1783609396684,"data":{"turn":1,"step":1,"callId":"call_skill_load","name":"skill","arguments":"{\"name\":\"dsh-skill-creator\"}"}}
{"type":"tool/result","seq":14,"time":1783609396685,"data":{"turn":1,"step":1,"callId":"call_skill_load","content":[{"type":"text","text":"<skill_content name=\"dsh-skill-creator\">\n# Skill: dsh-skill-creator\n\nUse this skill to write focused DeepSeek Harness skills.\n\nA skill is a directory `<name>/SKILL.md` or a flat `<name>.md` file with YAML frontmatter.\nFrontmatter must include kebab-case `name` and a concise `description` that tells the model when to load it.\nUse optional `whenToUse` for extra routing signal and `disableModelInvocation: true` for user-only skills.\nKeep the body procedural, evidence-oriented, and scoped to the workflow the skill owns.\n\nBase directory for this skill: /var/folders/_g/59jgff8x2gqd39f5vy1wnbfc0000gn/T/acp-snap-cwd-ptkWg8/.dsh/skills/dsh-skill-creator\nResolve relative files mentioned by this skill against the base directory before using them.\n</skill_content>"}],"isError":false},"sourceEventSeqs":[13],"surfaceOp":"append"}
{"type":"step/end","seq":15,"time":1783609396685,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":16,"time":1783609396685,"data":{"turn":1,"step":2}}
{"type":"assistant/chunk","seq":17,"time":1783609396686,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
{"type":"assistant/chunk","seq":18,"time":1783609396686,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The skill is loaded."}}}
{"type":"assistant/chunk","seq":19,"time":1783609396686,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
{"type":"assistant/chunk","seq":20,"time":1783609396686,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"DONE"}}}
{"type":"assistant/chunk","seq":21,"time":1783609396686,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The skill is loaded."}}}}
{"type":"assistant/chunk","seq":22,"time":1783609396686,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}}
{"type":"assistant/chunk","seq":23,"time":1783609396686,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":180,"outputTokens":10,"cacheReadTokens":0,"reasoningTokens":4}}}}
{"type":"assistant/chunk","seq":24,"time":1783609396686,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":25,"time":1783609396686,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The skill is loaded."},{"type":"text","text":"DONE"}],"usage":{"inputTokens":180,"outputTokens":10,"cacheReadTokens":0,"reasoningTokens":4}},"sourceEventSeqs":[17,18,19,20,21,22,23,24],"surfaceOp":"append"}
{"type":"step/end","seq":26,"time":1783609396686,"data":{"turn":1,"step":2}}
{"type":"turn/end","seq":27,"time":1783609396686,"data":{"turn":1,"reason":{"kind":"completed"}}}
{"type":"session","version":0,"id":"9eb4181f-2d05-49d3-98fc-3711fe2f5664","createdAt":1783654655599,"cwd":"/var/folders/_g/59jgff8x2gqd39f5vy1wnbfc0000gn/T/acp-snap-cwd-DhYwNW"}
{"type":"turn/start","seq":0,"time":1783654655602,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
{"type":"user/message","seq":1,"time":1783654655603,"data":{"content":[{"type":"text","text":"Load the dsh-skill-creator skill with the skill tool, then reply DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"}
{"type":"step/start","seq":2,"time":1783654655608,"data":{"turn":1,"step":1}}
{"type":"request/header","seq":3,"time":1783654655608,"data":{"header":{"config":{"model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
{"type":"assistant/chunk","seq":4,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
{"type":"assistant/chunk","seq":5,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"Load the requested skill."}}}
{"type":"assistant/chunk","seq":6,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":7,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_skill_load","name":"skill","argumentsDelta":"{\"name\":\"dsh-skill-creator\"}"}}}
{"type":"assistant/chunk","seq":8,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Load the requested skill."}}}}
{"type":"assistant/chunk","seq":9,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_skill_load","name":"skill","arguments":"{\"name\":\"dsh-skill-creator\"}"}}}}
{"type":"assistant/chunk","seq":10,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":100,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":5}}}}
{"type":"assistant/chunk","seq":11,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":12,"time":1783654655609,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"Load the requested skill."},{"type":"tool-call","id":"call_skill_load","name":"skill","arguments":"{\"name\":\"dsh-skill-creator\"}"}],"usage":{"inputTokens":100,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":5}},"sourceEventSeqs":[4,5,6,7,8,9,10,11],"surfaceOp":"append"}
{"type":"tool/call","seq":13,"time":1783654655609,"data":{"turn":1,"step":1,"callId":"call_skill_load","name":"skill","arguments":"{\"name\":\"dsh-skill-creator\"}"}}
{"type":"tool/result","seq":14,"time":1783654655610,"data":{"turn":1,"step":1,"callId":"call_skill_load","content":[{"type":"text","text":"<skill_content name=\"dsh-skill-creator\">\n<skill_resources>\nBase directory for this skill: /var/folders/_g/59jgff8x2gqd39f5vy1wnbfc0000gn/T/acp-snap-cwd-DhYwNW/.dsh/skills/dsh-skill-creator\nResolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed.\n</skill_resources>\n\n<skill_instructions>\nUse this skill to write focused DeepSeek Harness skills.\n\nA skill is a directory `<name>/SKILL.md` or a flat `<name>.md` file with YAML frontmatter.\nFrontmatter must include kebab-case `name` and a concise `description` that tells the model when to load it.\nUse optional `whenToUse` for extra routing signal and `disableModelInvocation: true` for user-only skills.\nKeep the body procedural, evidence-oriented, and scoped to the workflow the skill owns.\n</skill_instructions>\n</skill_content>"}],"isError":false},"sourceEventSeqs":[13],"surfaceOp":"append"}
{"type":"step/end","seq":15,"time":1783654655610,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":16,"time":1783654655610,"data":{"turn":1,"step":2}}
{"type":"assistant/chunk","seq":17,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
{"type":"assistant/chunk","seq":18,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The skill is loaded."}}}
{"type":"assistant/chunk","seq":19,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
{"type":"assistant/chunk","seq":20,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"DONE"}}}
{"type":"assistant/chunk","seq":21,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The skill is loaded."}}}}
{"type":"assistant/chunk","seq":22,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}}
{"type":"assistant/chunk","seq":23,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":180,"outputTokens":10,"cacheReadTokens":0,"reasoningTokens":4}}}}
{"type":"assistant/chunk","seq":24,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":25,"time":1783654655611,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The skill is loaded."},{"type":"text","text":"DONE"}],"usage":{"inputTokens":180,"outputTokens":10,"cacheReadTokens":0,"reasoningTokens":4}},"sourceEventSeqs":[17,18,19,20,21,22,23,24],"surfaceOp":"append"}
{"type":"step/end","seq":26,"time":1783654655611,"data":{"turn":1,"step":2}}
{"type":"turn/end","seq":27,"time":1783654655611,"data":{"turn":1,"reason":{"kind":"completed"}}}
@@ -2,7 +2,7 @@
{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"Load the requested skill."}}}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_skill_load","title":"Load skill dsh-skill-creator","kind":"read","status":"in_progress","rawInput":"dsh-skill-creator"}}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_skill_load","status":"completed","content":[{"type":"content","content":{"type":"text","text":"<skill_content name=\"dsh-skill-creator\">\n# Skill: dsh-skill-creator\n\nUse this skill to write focused DeepSeek Harness skills.\n\nA skill is a directory `<name>/SKILL.md` or a flat `<name>.md` file with YAML frontmatter.\nFrontmatter must include kebab-case `name` and a concise `description` that tells the model when to load it.\nUse optional `whenToUse` for extra routing signal and `disableModelInvocation: true` for user-only skills.\nKeep the body procedural, evidence-oriented, and scoped to the workflow the skill owns.\n\nBase directory for this skill: {{cwd}}/.dsh/skills/dsh-skill-creator\nResolve relative files mentioned by this skill against the base directory before using them.\n</skill_content>"}}]}}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_skill_load","status":"completed","content":[{"type":"content","content":{"type":"text","text":"<skill_content name=\"dsh-skill-creator\">\n<skill_resources>\nBase directory for this skill: {{cwd}}/.dsh/skills/dsh-skill-creator\nResolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed.\n</skill_resources>\n\n<skill_instructions>\nUse this skill to write focused DeepSeek Harness skills.\n\nA skill is a directory `<name>/SKILL.md` or a flat `<name>.md` file with YAML frontmatter.\nFrontmatter must include kebab-case `name` and a concise `description` that tells the model when to load it.\nUse optional `whenToUse` for extra routing signal and `disableModelInvocation: true` for user-only skills.\nKeep the body procedural, evidence-oriented, and scoped to the workflow the skill owns.\n</skill_instructions>\n</skill_content>"}}]}}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The skill is loaded."}}}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"DONE"}}}}
{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
File diff suppressed because one or more lines are too long
+1
View File
@@ -13,6 +13,7 @@ Packages are grouped by modular role at `packages/<group>/<pkg>/`. The group dir
| [`bash/`](bash/README.md) | Bash capability family: the executor seam, a local impl, and the model-facing tool | Product — stable surface |
| [`code-runtime/`](code-runtime/README.md) | Code-execution capability family: the abstract runtime seam for model-written programs + a worker-thread backend | Product — stable surface |
| [`fs/`](fs/README.md) | Filesystem capability family: the abstract seam, a local impl, and the model-facing file tools | Product — stable surface |
| [`skill/`](skill/README.md) | Skill capability family: the provider registry, local provider, and model-facing catalog/loader | Product — stable surface |
| [`compact/`](compact/README.md) | Compaction capability family: the abstract seam + a basic backend (tool deferred) | Product — stable surface |
| [`subagent/`](subagent/README.md) | Subagent capability family: the provider-registry seam and the model-facing delegation tool | Product — stable surface |
| [`web/`](web/README.md) | Web capability family: the abstract seam, search/fetch provider impls, and the model-facing web tools | Product — stable surface |
@@ -156,7 +156,6 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
'register(skill: SkillRegistration): () => void',
'async list(options: SkillLookupOptions = {}): Promise<SkillSummary[]>',
'async get(name: string, options: SkillLookupOptions = {}): Promise<SkillDefinition | undefined>',
'async renderModelListing(options: SkillLookupOptions = {}): Promise<string>',
],
},
{
@@ -691,7 +690,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'SkillLookupOptions',
declaration: 'export interface SkillLookupOptions {\n cwd?: string | undefined;\n}',
declaration: 'export interface SkillLookupOptions {\n cwd?: string | undefined;\n signal?: AbortSignal | undefined;\n}',
},
{
name: 'SkillProvider',
+2 -5
View File
@@ -1,19 +1,16 @@
# core/ — product API spine
The packages every harness build is assembled from: the session log, the system-prompt assembly, the tool registry, the agent vocabulary, and the one concrete loop that drives them. These are **product** packages — the stable surface plugins and consumers build against.
The session log, system-prompt assembly, tool registry, agent vocabulary, and concrete loop that form the harness's default control spine. These are **product** packages — the stable surface plugins and consumers build against.
| Package | Role | ctx key |
|---|---|---|
| `session/` | Event-sourced session log + in-memory store | `ctx.sessions` |
| `system-prompt/` | Prompt-section + tool-schema assembly registry | `ctx.systemPrompt` |
| `tools/` | Tool registry + `tools/pre-execute`/`tools/post-execute` pipeline | `ctx.tools` |
| `skill/` | Agent skill provider registry + request-time skill listing | `ctx.skills` |
| `skill-local/` | Local filesystem skill provider | (registers on `ctx.skills`) |
| `tool-skill/` | Model-facing `skill` loader tool | (registers on `ctx.tools`) |
| `agent/` | Agent interface, registry, `agent/*` event vocabulary | `ctx.agents` |
| `agent-loop/` | The concrete loop plugin: `ReactLoopAgent` + the loop driver | `ctx.agentLoop` |
| `agent-core/` | Bundle plugin: the default executor-less/UI-less spine as code | (loads the spine) |
`agent-loop` is the one concrete implementation of the `agent` seam and lives here because it is the harness's default product loop; everything else in `core/` is interface/vocabulary. Plugins depend on the `agent` vocabulary, never on `agent-loop` directly, so the loop stays swappable.
`agent-core` is the composition counterpart: one bundle plugin that loads the default spine (`timer` + `llm` + sessions + system-prompt + tools + skill registry + local skill provider + agents + invariants + `tool-bash` + `tool-skill` + `agent-loop`) and forwards `agent-loop`'s `agents` list as its own config. App packages (`ui/stdio-agent`, `ui/acp-agent`) consume it and add only a front door; a leaf adds the swappable backends plus any optional product tools it wants to expose. It lives in `core/` because it composes the shared core while leaving executors, LLM adapters, non-local skill providers, and UI front doors outside the bundle.
`agent-core` is the composition counterpart: one bundle plugin that loads the control spine plus selected default capabilities (`timer` + `llm` + sessions + system-prompt + tools + agents + invariants + the local [skill family](../skill/README.md) + `tool-bash` + `agent-loop`) and forwards `agent-loop`'s `agents` list as its own config. App packages (`ui/stdio-agent`, `ui/acp-agent`) consume it and add only a front door; a leaf adds the swappable backends plus any optional product tools it wants to expose. It lives in `core/` because it composes the shared control spine while leaving executors, LLM adapters, alternate skill providers, and UI front doors outside the bundle.
+3 -3
View File
@@ -14,12 +14,12 @@ This is the package to read to see **the whole plugin tree at once** — the tea
@deepseek-ai/dsh-session event-sourced session log + store
@deepseek-ai/dsh-system-prompt prompt-section + tool-schema assembly
@deepseek-ai/dsh-tools tool registry + tools/pre-execute/post-execute
@deepseek-ai/dsh-skill skill provider registry + prompt listing
@deepseek-ai/dsh-skill skill provider registry
@deepseek-ai/dsh-skill-local local filesystem skill provider
@deepseek-ai/dsh-agent agent registry + agent/* event vocabulary
@deepseek-ai/dsh-invariants dev-mode event-contract assertions
@deepseek-ai/dsh-tool-bash the model-facing bash/bash_output/bash_kill schemas
@deepseek-ai/dsh-tool-skill the model-facing skill loader schema
@deepseek-ai/dsh-tool-skill session-prefix skill catalog + model-facing loader schema
@deepseek-ai/dsh-agent-loop THE concrete loop (gets the forwarded `agents`)
(dsh-system-prompt gets the forwarded `persona`)
```
@@ -43,7 +43,7 @@ import type { Config } from '@deepseek-ai/dsh-agent-core'
// so validation and defaulting can never drift from the owners.
```
The bundle FORWARDS each field to the child that owns it: `agents` to `agent-loop` (default `[]`), so each app supplies its own pre-created agents — a stdio app pre-creates a `main`; the ACP app pre-creates none (it creates agents on demand at `session/new`) — `persona` to `dsh-system-prompt` (default `''`), the deployment's persona section; `toolOrder` to `dsh-system-prompt` (absent — lexicographic), the explicit model-facing tool order; and `skills.registry` / `skills.local` to the skill registry and local provider. Forwarding is exactly why the owners can live in the shared spine even though the apps disagree on what to configure.
The bundle FORWARDS each field to the child that owns it: `agents` to `agent-loop` (default `[]`), so each app supplies its own pre-created agents — a stdio app pre-creates a `main`; the ACP app pre-creates none (it creates agents on demand at `session/new`) — `persona` to `dsh-system-prompt` (default `''`), the deployment's persona section; `toolOrder` to `dsh-system-prompt` (absent — lexicographic), the explicit model-facing tool order; and `skills.registry`, `skills.local`, and `skills.tool` to the skill registry, local provider, and model-facing consumer. Forwarding is exactly why the owners can live in the shared spine even though the apps disagree on what to configure.
## Why a code bundle, not a shared YAML include
+8 -5
View File
@@ -62,12 +62,14 @@ import AgentLoop, { type Config as AgentLoopConfig } from '@deepseek-ai/dsh-agen
export const name = 'agent-core'
/** Skill bundle config forwarded to the registry and the local provider. */
/** Skill bundle config forwarded to the registry, local provider, and model-facing consumer. */
export interface SkillConfig {
/** Registry-level prompt/cache settings. */
/** Registry-level discovery cache settings. */
registry?: SkillRegistryConfig
/** Local filesystem skill provider settings. */
local?: SkillLocal.Config
/** Model-facing skill catalog and tool settings. */
tool?: toolSkill.Config
}
/**
@@ -75,7 +77,7 @@ export interface SkillConfig {
* `agents` to the agent loop (an app that pre-creates no agents, like the ACP
* bridge, simply omits it), `persona` and `toolOrder` to the system-prompt
* plugin (the deployment's persona section and the explicit model-facing tool
* order), and `skills` to the skill registry/local provider. Every field is
* order), and `skills` to the skill registry/local provider/tool consumer. Every field is
* optional INPUT here because each owner's schema supplies the default (`[]` /
* `''` / absent — lexicographic / the DSH skill roots); the schema is the
* INTERSECTION of the owners' own schemas, so validation and defaulting can
@@ -88,7 +90,7 @@ export interface Config {
persona?: SystemPromptConfig['persona']
/** The explicit model-facing tool order (see dsh-system-prompt's `Config`). */
toolOrder?: SystemPromptConfig['toolOrder']
/** Skill registry and local provider config. */
/** Skill registry, local provider, and model-facing consumer config. */
skills?: SkillConfig
}
@@ -96,6 +98,7 @@ export interface Config {
export const SkillConfigSchema: z<SkillConfig> = z.object({
registry: SkillService.Config,
local: SkillLocal.Config,
tool: toolSkill.Config,
})
/** Intersect the owners' schemas so validation + defaulting stay identical. */
@@ -134,6 +137,6 @@ export function apply(ctx: Context, config: Config): void {
ctx.plugin(AgentRegistry)
ctx.plugin(invariants)
ctx.plugin(toolBash)
ctx.plugin(toolSkill)
ctx.plugin(toolSkill, config.skills?.tool ?? {})
ctx.plugin(AgentLoop, { agents: config.agents ?? [] })
}
@@ -7,6 +7,15 @@ import Loader from '@cordisjs/plugin-loader'
import { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
import * as agentCore from '../src/index.ts'
import { AgentId } from '@deepseek-ai/dsh-agent'
import type { Message } from '@deepseek-ai/dsh-llm'
async function composePrefix(ctx: Context, cwd: string): Promise<Message[]> {
const empty: Message[] = []
return await ctx.waterfall(
'agent/session-prefix', { session: { header: { cwd } } } as never,
empty, new AbortController().signal, () => Promise.resolve(empty),
)
}
/**
* Unit coverage for the @deepseek-ai/dsh-agent-core bundle: mounting it brings
@@ -120,7 +129,7 @@ describe('dsh-agent-core bundle', () => {
await ctx.fiber.dispose()
})
it('forwards skill config to the registry and local provider', async () => {
it('forwards skill config to the registry, local provider, and model-facing consumer', async () => {
const home = await mkdtemp(join(tmpdir(), 'dsh-agent-core-skill-home-'))
const agentsHome = await mkdtemp(join(tmpdir(), 'dsh-agent-core-skill-agents-'))
const custom = await mkdtemp(join(tmpdir(), 'dsh-agent-core-skill-custom-'))
@@ -129,16 +138,17 @@ describe('dsh-agent-core bundle', () => {
const ctx = await mount({
agents: [],
skills: {
registry: { promptFieldMaxLength: 6 },
registry: { collectCacheMaxEntries: 4 },
local: {
dshHome: join(home, '.dsh'),
agentsHome: join(agentsHome, '.agents'),
customSkillDirs: [custom],
},
tool: { catalogDescriptionMaxLength: 6 },
},
})
expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['custom-skill'])
expect(await ctx.skills.renderModelListing()).toContain('description: Cus...')
expect(JSON.stringify(await composePrefix(ctx, '/tmp'))).toContain('- `custom-skill`: Cus...')
await ctx.fiber.dispose()
})
+3 -3
View File
@@ -33,13 +33,13 @@
"path": "../../core/tools"
},
{
"path": "../../core/skill"
"path": "../../skill/skill"
},
{
"path": "../../core/skill-local"
"path": "../../skill/skill-local"
},
{
"path": "../../core/tool-skill"
"path": "../../skill/tool-skill"
},
{
"path": "../../core/agent"
-38
View File
@@ -1,38 +0,0 @@
# @deepseek-ai/dsh-skill
Agent skill provider registry and model-facing skill guidance.
This package owns the `ctx.skills` interface. It does not know whether skills come from local files, embedded plugin data, HTTP, or another backend; providers register those sources with `ctx.skills.registerProvider(...)`. The shipped local implementation is [`@deepseek-ai/dsh-skill-local`](../skill-local).
## Service: `SkillService` (ctx key: `skills`)
### Public API
- `ctx.skills.registerProvider(provider): () => void` Registers a provider by unique `provider.name`. Duplicate provider names throw, and `runtime` is reserved for `ctx.skills.register(...)`. The registration is effect-scoped and HMR-safe.
- `ctx.skills.list({ cwd? })` Returns model-invocable skill summaries for the current workspace, merged across providers.
- `ctx.skills.get(name, { cwd? })` Returns the full winning skill, including disabled-for-model skills.
- `ctx.skills.register(skill): () => void` Registers a runtime embedded skill. Same-name runtime registrations are first-wins: a duplicate logs a warning and gets a no-op disposer.
- `ctx.skills.renderModelListing({ cwd? })` Renders the request-time `## Skills` catalog.
### Config
| Field | Default | Meaning |
|---|---|---|
| `promptFieldMaxLength` | `500` | Maximum rendered `description` / `whenToUse` length in the prompt listing; must be at least `3` because truncated fields reserve `...`. |
| `collectCacheMaxEntries` | `128` | Maximum cwd/provider discovery promises kept in memory. |
## Provider Contract
A provider returns `SkillCandidate[]` from `list(options)` and later receives the winning candidate back in `get(candidate, options)`. The candidate's `locator` is opaque to the registry, so a local provider can store a file path while a future HTTP provider can store a URL, id, or version token.
The registry validates candidate names, descriptions, ranks, and provider ownership. Candidate contract violations fail fast because the provider plugin is malformed; a provider `list()` rejection is treated as a transient source failure, logged, skipped for that request, and not cached. Duplicate skill names are resolved first-wins by `rank`, provider registration order, then the provider's own local order. The final model-visible summary list is sorted by skill `name` for deterministic prompt text and provider prefix-cache friendliness.
## Runtime Skills
`ctx.skills.register(...)` is a convenience for embedded runtime skills. Runtime skills use rank `250`: project providers can override them, while they override the shipped local provider's custom and user roots. Runtime registration is also first-wins within runtime contributions, so a duplicate contribution cannot remove the active one through its disposer.
## Prompt Integration
The service listens on `system-prompt/assemble` and appends a short `## Skills` section to the calling agent's assembled system prompt. The listing contains only stable routing metadata (`name`, `source`, `description`, and optional `whenToUse`), not skill bodies or absolute local paths. `description` and `whenToUse` are whitespace-normalized, capped, XML-escaped, and have `{{` / `}}` delimiters split so provider text cannot trip prompt-variable interpolation. Models load full instructions through the `skill` tool.
The prompt-injection surface is intentionally separate from provider loading: changing where skills come from means adding or swapping providers, not changing prompt assembly or the `skill` tool.
-15
View File
@@ -1,15 +0,0 @@
# @deepseek-ai/dsh-tool-skill
The model-facing `skill` tool for loading full skill instructions.
Requires `ctx.tools` and `ctx.skills` (`inject: ['tools', 'skills']`).
## Tool: `skill`
| Arg | Type | Notes |
|---|---|---|
| `name` | string (required) | Exact kebab-case skill name from the available skills listing. |
Execution uses the calling agent's `session.header.cwd` so workspace-sensitive providers can resolve the right winning skill. A successful call returns a text block containing `<skill_content name="...">`, the skill body, and provider resource guidance. Local filesystem skills include a base directory for resolving relative files; remote or embedded providers can return URL or opaque provider-managed guidance instead. Unknown names, invalid names, and skills marked `disableModelInvocation: true` return `isError` tool results through the normal tool registry error path.
The tool does not call `agent.inject()` in v1. Its result is already recorded as the tool result and becomes available to the next model step without duplicating the content as synthetic context.
-73
View File
@@ -1,73 +0,0 @@
/**
* Model-facing `skill` tool.
*
* @module @deepseek-ai/dsh-tool-skill
*/
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { assertNever } from '@deepseek-ai/dsh-llm'
import { isSkillName, type SkillDefinition } from '@deepseek-ai/dsh-skill'
export const name = 'tool-skill'
export const inject = ['tools', 'skills']
export function apply(ctx: Context): void {
const skillTool = defineTool({
name: 'skill',
description: 'Load the full instructions for one available skill by name. Use this when the current task matches a skill listed in the system prompt.',
parameters: {
name: { type: 'string', required: true, description: 'The exact skill name from the available skills list.' },
},
async execute(args, exec) {
if (!isSkillName(args.name)) {
throw new Error(`invalid skill name "${args.name}"`)
}
const skill = await ctx.skills.get(args.name, { cwd: exec.agent?.session.header.cwd })
if (!skill) {
throw new Error(`unknown skill "${args.name}"`)
}
if (skill.disableModelInvocation === true) {
throw new Error(`skill "${args.name}" is not available for model invocation`)
}
return [{ type: 'text', text: renderSkillContent(skill) }]
},
presentCall(args) {
return { card: 'generic', title: `Load skill ${args.name}`, kind: 'read', rawInput: args.name }
},
})
ctx.tools.register(skillTool)
}
function renderSkillContent(skill: SkillDefinition): string {
const resourceHint = renderResourceHint(skill)
return [
`<skill_content name="${skill.name}">`,
`# Skill: ${skill.name}`,
'',
skill.content,
'',
...resourceHint,
'</skill_content>',
].join('\n')
}
function renderResourceHint(skill: SkillDefinition): string[] {
const base = skill.resourceBase
if (base === undefined) {
return [`Resources for this skill are managed by provider "${skill.provider}".`]
}
switch (base.kind) {
case 'directory':
return [
`Base directory for this skill: ${base.path}`,
'Resolve relative files mentioned by this skill against the base directory before using them.',
]
case 'url':
return [`Base URL for this skill: ${base.url}`]
case 'opaque':
return [`Resources for this skill: ${base.description}`]
default:
return assertNever(base, 'SkillResourceBase.kind')
}
}
@@ -1,149 +0,0 @@
import { describe, expect, it } from 'vitest'
import { mkdir, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { Context } from 'cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import SkillService from '@deepseek-ai/dsh-skill'
import * as SkillLocal from '@deepseek-ai/dsh-skill-local'
import * as toolSkill from '@deepseek-ai/dsh-tool-skill'
async function tempDir(name: string): Promise<string> {
return await import('node:fs/promises').then(fs => fs.mkdtemp(join(tmpdir(), `dsh-${name}-`)))
}
async function writeSkill(root: string, name: string, description: string, body: string): Promise<void> {
const dir = join(root, name)
await mkdir(dir, { recursive: true })
await writeFile(join(dir, 'SKILL.md'), `---\nname: ${name}\ndescription: ${description}\n---\n\n${body}\n`)
}
async function setup(home: string): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SkillService)
await ctx.plugin(SkillLocal, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents') })
await ctx.plugin(toolSkill)
return ctx
}
describe('dsh-tool-skill', () => {
it('registers the skill tool schema and removes it on dispose', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
const home = await tempDir('tool-schema')
await ctx.plugin(SkillService)
await ctx.plugin(SkillLocal, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents') })
const fiber = await ctx.plugin(toolSkill)
expect(ctx.tools.schemas().map(tool => tool.name)).toEqual(['skill'])
expect(ctx.tools.get('skill')?.presentCall?.({ name: 'project-skill' })).toEqual({
card: 'generic',
title: 'Load skill project-skill',
kind: 'read',
rawInput: 'project-skill',
})
await fiber.dispose()
expect(ctx.tools.schemas()).toEqual([])
})
it('loads a skill for the calling agent cwd', async () => {
const home = await tempDir('tool-load')
const project = await tempDir('tool-project')
await mkdir(join(project, '.git'), { recursive: true })
await writeSkill(join(project, '.dsh/skills'), 'project-skill', 'Project skill', 'Project instructions.')
const ctx = await setup(home)
const result = await ctx.tools.execute({
callId: CallId('c1'),
name: 'skill',
arguments: { name: 'project-skill' },
agent: { session: { header: { cwd: project } } } as never,
})
expect(result.isError).toBe(false)
const block = result.content[0]
expect(block?.type).toBe('text')
if (block?.type !== 'text') throw new Error('expected text skill result')
expect(block.text).toContain('<skill_content name="project-skill">')
expect(block.text).toContain('Project instructions.')
})
it('renders provider-managed resource hints for non-local skills', async () => {
const home = await tempDir('tool-resource-hints')
const ctx = await setup(home)
ctx.skills.register({
name: 'opaque-skill',
description: 'Opaque skill',
source: 'runtime',
provider: 'runtime',
resourceBase: { kind: 'opaque', description: 'runtime memory' },
content: 'Opaque instructions.',
})
ctx.skills.register({
name: 'url-skill',
description: 'URL skill',
source: 'runtime',
provider: 'runtime',
resourceBase: { kind: 'url', url: 'https://skills.example.test/url-skill' },
content: 'URL instructions.',
})
ctx.skills.register({
name: 'provider-skill',
description: 'Provider skill',
source: 'runtime',
provider: 'runtime',
content: 'Provider instructions.',
})
const opaque = await ctx.tools.execute({ callId: CallId('c2'), name: 'skill', arguments: { name: 'opaque-skill' } })
const url = await ctx.tools.execute({ callId: CallId('c3'), name: 'skill', arguments: { name: 'url-skill' } })
const provider = await ctx.tools.execute({ callId: CallId('c4'), name: 'skill', arguments: { name: 'provider-skill' } })
if (opaque.content[0]?.type !== 'text' || url.content[0]?.type !== 'text' || provider.content[0]?.type !== 'text') {
throw new Error('expected text tool results')
}
expect(opaque.content[0].text).toContain('Resources for this skill: runtime memory')
expect(url.content[0].text).toContain('Base URL for this skill: https://skills.example.test/url-skill')
expect(provider.content[0].text).toContain('Resources for this skill are managed by provider "runtime"')
})
it('fails loud on an unknown resource base kind', async () => {
const home = await tempDir('tool-resource-assert-never')
const ctx = await setup(home)
ctx.skills.register({
name: 'rogue-resource-skill',
description: 'Rogue resource skill',
source: 'runtime',
provider: 'runtime',
resourceBase: { kind: 'future' } as never,
content: 'Rogue instructions.',
})
const result = await ctx.tools.execute({ callId: CallId('c5'), name: 'skill', arguments: { name: 'rogue-resource-skill' } })
expect(result.isError).toBe(true)
const block = result.content[0]
if (block?.type !== 'text') throw new Error('expected text tool result')
expect(block.text).toContain('unreachable variant')
})
it('returns isError for unknown, invalid, and model-disabled skills', async () => {
const home = await tempDir('tool-errors')
await writeSkill(join(home, '.dsh/skills'), 'hidden-skill', 'Hidden skill', 'Hidden instructions.')
await writeFile(join(home, '.dsh/skills/hidden-skill/SKILL.md'), '---\nname: hidden-skill\ndescription: Hidden skill\ndisableModelInvocation: true\n---\n\nHidden instructions.\n')
const ctx = await setup(home)
const unknown = await ctx.tools.execute({ callId: CallId('c1'), name: 'skill', arguments: { name: 'missing' } })
const invalid = await ctx.tools.execute({ callId: CallId('c2'), name: 'skill', arguments: { name: 'Bad_Name' } })
const disabled = await ctx.tools.execute({ callId: CallId('c3'), name: 'skill', arguments: { name: 'hidden-skill' } })
expect(unknown.isError).toBe(true)
expect(invalid.isError).toBe(true)
expect(disabled.isError).toBe(true)
})
})
+11
View File
@@ -0,0 +1,11 @@
# skill/ - skill capability family
The canonical three-package capability seam for reusable agent instructions: a provider registry, a local implementation, and the model-facing catalog/loader consumer. All are **product** packages.
| Package | Role | ctx key |
|---|---|---|
| `skill/` | Provider registry, precedence resolution, stable catalog snapshots, and full-definition lookup | `ctx.skills` |
| `skill-local/` | Project/custom/user filesystem provider | (registers on `ctx.skills`) |
| `tool-skill/` | Session-prefix catalog and model-facing `skill` loader | (registers on `ctx.tools`) |
The interface lives at `skill/skill/`. Providers register synchronously and perform asynchronous discovery through `ctx.skills`; `tool-skill` consumes only that interface, so an embedded or remote provider can replace or complement `skill-local` without changing the model-facing contract. `agent-core` loads this family by default, but it remains a capability outside the core control spine, parallel to [`bash/`](../bash/README.md), [`fs/`](../fs/README.md), [`web/`](../web/README.md), and [`subagent/`](../subagent/README.md).
@@ -2,7 +2,7 @@
Local filesystem provider for the `ctx.skills` registry.
This package implements one skill source. It scans local project, custom, and user skill roots, parses `SKILL.md` or flat Markdown skill files, and registers the provider on `ctx.skills`. The registry, prompt listing, and model-facing loader tool remain in `@deepseek-ai/dsh-skill` and `@deepseek-ai/dsh-tool-skill`.
This package implements one skill source. It scans local project, custom, and user skill roots, parses `SKILL.md` or flat Markdown skill files, and registers the provider on `ctx.skills`. The registry remains in `@deepseek-ai/dsh-skill`; the session-prefix catalog and model-facing loader tool remain in `@deepseek-ai/dsh-tool-skill`.
## Plugin
+34
View File
@@ -0,0 +1,34 @@
# @deepseek-ai/dsh-skill
Pure agent skill provider registry.
This package owns the `ctx.skills` interface. It does not know whether skills come from local files, embedded plugin data, HTTP, or another backend; providers register those sources with `ctx.skills.registerProvider(...)`. The shipped local implementation is [`@deepseek-ai/dsh-skill-local`](../skill-local).
## Service: `SkillService` (ctx key: `skills`)
### Public API
- `ctx.skills.registerProvider(provider): () => void` Registers a provider by unique `provider.name`. Duplicate provider names throw, and `runtime` is reserved for `ctx.skills.register(...)`. The registration is effect-scoped and HMR-safe.
- `ctx.skills.list({ cwd?, signal? })` Returns model-invocable skill summaries for the current workspace, merged across providers and sorted by name.
- `ctx.skills.get(name, { cwd?, signal? })` Returns the full winning skill, including disabled-for-model skills.
- `ctx.skills.register(skill): () => void` Registers a runtime embedded skill. Same-name runtime registrations are first-wins: a duplicate logs a warning and gets a no-op disposer.
### Config
| Field | Default | Meaning |
|---|---|---|
| `collectCacheMaxEntries` | `128` | Maximum completed cwd/provider catalog snapshots kept in memory. |
## Provider Contract
A provider registers synchronously from its `apply()` and returns `SkillCandidate[]` from `list(options)` when discovery is requested. Remote setup, authentication, and discovery belong in the awaited `list()` call rather than plugin registration. Providers should stop promptly when `options.signal` aborts; the registry also stops awaiting an uncooperative provider so agent cancellation cannot hang prefix composition. The provider later receives the winning candidate back in `get(candidate, options)`. The candidate's `locator` is opaque to the registry, so a local provider can store a file path while a remote provider can store a URL, id, or version token.
The registry validates candidate names, descriptions, ranks, and provider ownership. Candidate contract violations fail fast because the provider plugin is malformed; a provider `list()` rejection is treated as a transient source failure, logged, skipped for that request, and not cached. Only completed catalogs are cached, and a provider/runtime revision change during discovery discards the stale result and retries. Duplicate skill names are resolved first-wins by `rank`, provider registration order, then the provider's own local order. The final summary list is sorted by skill `name` for deterministic consumers.
## Runtime Skills
`ctx.skills.register(...)` is a convenience for embedded runtime skills. Runtime skills use rank `250`: project providers can override them, while they override the shipped local provider's custom and user roots. Runtime registration is also first-wins within runtime contributions, so a duplicate contribution cannot remove the active one through its disposer.
## Consumer boundary
The registry does not render model guidance or register model-facing tools. [`@deepseek-ai/dsh-tool-skill`](../tool-skill) consumes `ctx.skills` to provide the session-prefix catalog and `skill` tool, so providers remain independent of the model surface.
@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-skill",
"description": "Agent skill provider registry and prompt listing for the DeepSeek Harness",
"description": "Agent skill provider registry for the DeepSeek Harness",
"version": "0.0.1",
"private": true,
"type": "module",
@@ -22,16 +22,12 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
"cordis": "^4.0.0-rc.6"
},
"dependencies": {
"schemastery": "^3.18.0"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"cordis": "^4.0.0-rc.6"
}
}
@@ -1,10 +1,10 @@
/**
* Agent skill registry and request-time catalog rendering.
* Agent skill provider registry.
*
* This package is the interface third of the skill capability seam. Concrete
* providers such as `@deepseek-ai/dsh-skill-local` decide where skills come
* from; this service only merges provider catalogs, resolves the winning skill
* for a name, and exposes the model-facing catalog/tool consumers use.
* for a name, and exposes the winning summaries and definitions to consumers.
*
* @module @deepseek-ai/dsh-skill
*/
@@ -12,15 +12,11 @@
import { Context, Service } from 'cordis'
import z from 'schemastery'
import type Schema from 'schemastery'
import type {} from '@deepseek-ai/dsh-system-prompt'
import type {} from '@deepseek-ai/dsh-agent'
const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
const DEFAULT_PROMPT_FIELD_LENGTH = 500
const DEFAULT_COLLECT_CACHE_ENTRIES = 128
const RUNTIME_PROVIDER = 'runtime'
const RUNTIME_RANK = 250
const SKILL_PROMPT_SECTION_ORDER = 1000
/**
* Return whether a string is a valid kebab-case skill name.
@@ -83,9 +79,11 @@ export interface SkillDefinition extends SkillSummary {
/** Runtime skill contribution accepted by `ctx.skills.register()`. */
export type SkillRegistration = Omit<SkillDefinition, 'provider'> & { provider?: string }
/** Workspace selector used for cwd-sensitive provider discovery. */
/** Caller context used for cwd-sensitive and abortable provider work. */
export interface SkillLookupOptions {
cwd?: string | undefined
/** Abort discovery or loading work for the current caller. */
signal?: AbortSignal | undefined
}
/** Provider interface for one source of skills, such as local directories or a remote registry. */
@@ -93,15 +91,18 @@ export interface SkillProvider {
/** Unique provider name in the `ctx.skills` registry. */
name: string
/**
* List available skill candidates for the current lookup context.
* @param options - lookup options; `cwd` selects workspace-sensitive skills.
* List available skill candidates for the current lookup context. Provider
* plugins register synchronously during `apply()`; remote initialization,
* authentication, and discovery are awaited inside this method. Implementations
* should settle promptly when `options.signal` aborts.
* @param options - lookup options; `cwd` selects workspace-sensitive skills and `signal` cancels work.
* @returns provider candidates with precedence ranks and opaque locators.
*/
list(options: SkillLookupOptions): Promise<SkillCandidate[]>
/**
* Load a complete skill body for a previously listed candidate.
* @param candidate - the winning candidate originally returned by this provider.
* @param options - lookup options; `cwd` selects workspace-sensitive skills.
* @param options - lookup options; `cwd` selects workspace-sensitive skills and `signal` cancels work.
* @returns the full skill body, or `undefined` if it is no longer loadable.
*/
get(candidate: SkillCandidate, options: SkillLookupOptions): Promise<SkillDefinition | undefined>
@@ -109,9 +110,7 @@ export interface SkillProvider {
/** Skill registry configuration. */
export interface Config {
/** Maximum rendered description/whenToUse length in the prompt listing; minimum 3. */
promptFieldMaxLength?: number
/** Maximum number of cwd/provider discovery promises kept in the in-memory cache. */
/** Maximum number of completed cwd/provider catalog snapshots kept in memory. */
collectCacheMaxEntries?: number
}
@@ -152,52 +151,35 @@ interface CollectResult {
/**
* Registry of skill providers. It merges provider catalogs with stable
* first-wins duplicate handling, exposes sorted model-visible summaries, loads
* full skill bodies on demand, and renders the request-time catalog fragment.
* first-wins duplicate handling, exposes sorted model-visible summaries, and
* loads full skill bodies on demand.
*/
export class SkillService extends Service {
static Config: Schema<Config> = z.object({
promptFieldMaxLength: z.number().default(DEFAULT_PROMPT_FIELD_LENGTH),
collectCacheMaxEntries: z.number().default(DEFAULT_COLLECT_CACHE_ENTRIES),
})
private readonly promptFieldMaxLength: number
private readonly collectCacheMaxEntries: number
private readonly providers = new Map<string, { provider: SkillProvider; order: number }>()
private readonly runtime = new Map<string, SkillDefinition>()
private readonly collectCache = new Map<string, Promise<IndexedCandidate[]>>()
private readonly collectCache = new Map<string, IndexedCandidate[]>()
private providerRevision = 0
private nextProviderOrder = 0
private runtimeRevision = 0
constructor(ctx: Context, config: Config = {}) {
super(ctx, 'skills')
this.promptFieldMaxLength = config.promptFieldMaxLength ?? DEFAULT_PROMPT_FIELD_LENGTH
this.collectCacheMaxEntries = config.collectCacheMaxEntries ?? DEFAULT_COLLECT_CACHE_ENTRIES
assertPositiveInteger('promptFieldMaxLength', this.promptFieldMaxLength, 3)
assertPositiveInteger('collectCacheMaxEntries', this.collectCacheMaxEntries)
ctx.on('system-prompt/assemble', async (_assembly, context, next) => {
const result = await next()
const agent = context.agent
if (agent === undefined) return result
const listing = await this.renderModelListing({ cwd: agent.session.header.cwd })
if (listing.length > 0) {
result.sections.push({
name: 'skills:available',
order: SKILL_PROMPT_SECTION_ORDER,
text: listing,
})
}
return result
})
}
/**
* Register a skill provider. Throws if another provider already owns the same
* provider name, including the reserved runtime provider name. Effect-scoped
* and HMR-safe: disposing the caller's fiber unregisters the provider and
* invalidates cached catalogs.
* Register a skill provider synchronously during the provider plugin's
* `apply()`. Throws if another provider already owns the same provider name,
* including the reserved runtime provider name. Providers that need remote
* initialization do that work inside `list()` after registration. Effect-
* scoped and HMR-safe: disposing the caller's fiber unregisters the provider
* and invalidates cached catalogs.
* @param provider - the provider to register by `provider.name`.
* @returns a disposer that unregisters this provider.
*/
@@ -252,7 +234,7 @@ export class SkillService extends Service {
/**
* List model-invocable skill summaries for a workspace.
* @param options - lookup options; `cwd` selects the project roots to scan.
* @param options - lookup options; `cwd` selects project roots and `signal` cancels discovery.
* @returns sorted summaries, excluding skills disabled for model invocation.
*/
async list(options: SkillLookupOptions = {}): Promise<SkillSummary[]> {
@@ -260,13 +242,13 @@ export class SkillService extends Service {
.map(entry => entry.candidate)
.filter(skill => skill.disableModelInvocation !== true)
.map(toSummary)
.sort(compareSummary)
.sort(compareSkillSummary)
}
/**
* Load one full skill definition by name.
* @param name - kebab-case skill name.
* @param options - lookup options; `cwd` selects workspace-sensitive skills.
* @param options - lookup options; `cwd` selects workspace-sensitive skills and `signal` cancels work.
* @returns the full skill, including body content, or `undefined`.
*/
async get(name: string, options: SkillLookupOptions = {}): Promise<SkillDefinition | undefined> {
@@ -276,51 +258,27 @@ export class SkillService extends Service {
return await match.provider.get(match.candidate, options)
}
/**
* Render the request-time `## Skills` prompt fragment.
* @param options - lookup options; `cwd` selects workspace-sensitive skills.
* @returns an empty string when no model-invocable skills are available.
*/
async renderModelListing(options: SkillLookupOptions = {}): Promise<string> {
const skills = await this.list(options)
if (skills.length === 0) return ''
const entries = skills.map((skill) => {
const lines = [
`<skill name="${escapeAttr(skill.name)}" source="${escapeAttr(skill.source)}">`,
`description: ${promptLine(skill.description, this.promptFieldMaxLength)}`,
...skill.whenToUse ? [`whenToUse: ${promptLine(skill.whenToUse, this.promptFieldMaxLength)}`] : [],
'</skill>',
]
return lines.join('\n')
}).join('\n')
return [
'## Skills',
'Available skills are listed below. Load a skill with the `skill` tool before following its instructions; do not infer or follow instructions from a skill body that has not been loaded.',
'<available_skills>',
entries,
'</available_skills>',
].join('\n')
}
private async collect(options: SkillLookupOptions): Promise<IndexedCandidate[]> {
const key = collectCacheKey(options, this.providerRevision, this.runtimeRevision)
const cached = this.collectCache.get(key)
if (cached !== undefined) return cached
options.signal?.throwIfAborted()
while (true) {
const providerRevision = this.providerRevision
const runtimeRevision = this.runtimeRevision
const key = collectCacheKey(options, providerRevision, runtimeRevision)
const cached = this.collectCache.get(key)
if (cached !== undefined) return cached
const collected = this.collectFresh(options)
const cachedPromise = collected.then((result) => {
if (!result.cacheable) this.collectCache.delete(key)
const result = await this.collectFresh(options)
options.signal?.throwIfAborted()
if (providerRevision !== this.providerRevision || runtimeRevision !== this.runtimeRevision) continue
if (result.cacheable) {
this.collectCache.set(key, result.entries)
if (this.collectCache.size > this.collectCacheMaxEntries) {
const oldest = this.collectCache.keys().next() as IteratorYieldResult<string>
this.collectCache.delete(oldest.value)
}
}
return result.entries
}).catch((error: unknown) => {
this.collectCache.delete(key)
throw error
})
this.collectCache.set(key, cachedPromise)
if (this.collectCache.size > this.collectCacheMaxEntries) {
const oldest = this.collectCache.keys().next() as IteratorYieldResult<string>
this.collectCache.delete(oldest.value)
}
return cachedPromise
}
private async collectFresh(options: SkillLookupOptions): Promise<CollectResult> {
@@ -341,10 +299,11 @@ export class SkillService extends Service {
}
private async listAllCandidates(options: SkillLookupOptions): Promise<CollectResult> {
options.signal?.throwIfAborted()
const candidates: IndexedCandidate[] = []
let cacheable = true
let runtimeOrder = 0
for (const skill of [...this.runtime.values()].sort((a, b) => a.name.localeCompare(b.name))) {
for (const skill of [...this.runtime.values()].sort((a, b) => compareCodePoints(a.name, b.name))) {
candidates.push({
candidate: runtimeCandidate(skill),
provider: RUNTIME_SKILL_PROVIDER,
@@ -353,13 +312,16 @@ export class SkillService extends Service {
})
runtimeOrder += 1
}
for (const { provider, order } of this.providers.values()) {
for (const { provider, order } of [...this.providers.values()]) {
let localOrder = 0
const listed = await provider.list(options).catch((error: unknown) => {
let listed: SkillCandidate[] | undefined
try {
listed = await waitWithAbort(provider.list(options), options.signal)
} catch (error) {
if (options.signal?.aborted === true) throw toError(options.signal.reason)
cacheable = false
this.ctx.logger.warn(`skill provider "${provider.name}" skipped: ${errorMessage(error)}`)
return undefined
})
}
if (listed === undefined) continue
for (const candidate of listed) {
validateCandidate(candidate, provider.name)
@@ -436,8 +398,14 @@ function toSummary(skill: SkillDefinition | SkillCandidate): SkillSummary {
}
}
function compareSummary(left: SkillSummary, right: SkillSummary): number {
return left.name.localeCompare(right.name)
function compareSkillSummary(left: SkillSummary, right: SkillSummary): number {
return compareCodePoints(left.name, right.name)
}
function compareCodePoints(left: string, right: string): number {
if (left < right) return -1
if (left > right) return 1
return 0
}
function compareIndexedCandidates(left: IndexedCandidate, right: IndexedCandidate): number {
@@ -446,36 +414,46 @@ function compareIndexedCandidates(left: IndexedCandidate, right: IndexedCandidat
|| left.localOrder - right.localOrder
}
function promptLine(value: string, maxLength: number): string {
const normalized = value.replaceAll(/\s+/g, ' ').trim()
const truncated = normalized.length <= maxLength
? normalized
: `${normalized.slice(0, maxLength - 3)}...`
return escapeText(breakPromptTemplateDelimiters(truncated))
}
function breakPromptTemplateDelimiters(value: string): string {
return value.replaceAll('{{', '{ {').replaceAll('}}', '} }')
}
function assertPositiveInteger(name: string, value: number, minimum = 1): void {
if (!Number.isInteger(value) || value < minimum) {
throw new Error(`skill: ${name} must be an integer greater than or equal to ${minimum}`)
}
}
function escapeAttr(value: string): string {
return value.replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll('<', '&lt;')
}
function escapeText(value: string): string {
return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')
}
function collectCacheKey(options: SkillLookupOptions, providerRevision: number, runtimeRevision: number): string {
return JSON.stringify({ cwd: options.cwd, providerRevision, runtimeRevision })
}
function waitWithAbort<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T> {
if (signal === undefined) return promise
signal.throwIfAborted()
return new Promise<T>((resolve, reject) => {
const cleanup = (): void => {
signal.removeEventListener('abort', onAbort)
}
const onAbort = (): void => {
cleanup()
reject(toError(signal.reason))
}
signal.addEventListener('abort', onAbort, { once: true })
void promise.then(
(value) => {
cleanup()
resolve(value)
},
(error: unknown) => {
cleanup()
reject(toError(error))
},
)
if (signal.aborted) onAbort()
})
}
function toError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error))
}
function errorMessage(error: unknown): string {
return String(error)
}
@@ -1,11 +1,6 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import SkillService, { type SkillCandidate, type SkillDefinition, type SkillLookupOptions, type SkillProvider } from '@deepseek-ai/dsh-skill'
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
function agentForCwd(cwd: string): never {
return { session: { header: { cwd } } } as never
}
function memorySkill(name: string, description: string, rank: number, body = `${name} body.`): SkillCandidate {
return {
@@ -113,6 +108,9 @@ describe('SkillService registry', () => {
})
it('validates provider candidates and invalid registry caps', async () => {
const defaultedService = new SkillService(new Context())
expect(await defaultedService.list()).toEqual([])
const ctx = new Context()
await ctx.plugin(SkillService)
ctx.skills.registerProvider({
@@ -146,10 +144,33 @@ describe('SkillService registry', () => {
await expect(invalid.skills.list()).rejects.toThrow('skill provider')
}
await expect(new Context().plugin(SkillService, { promptFieldMaxLength: 2 })).rejects.toThrow('greater than or equal to 3')
await expect(new Context().plugin(SkillService, { collectCacheMaxEntries: 1.5 })).rejects.toThrow('collectCacheMaxEntries')
})
it('sorts model-visible summaries without locale-sensitive collation', async () => {
const ctx = new Context()
await ctx.plugin(SkillService)
ctx.skills.registerProvider(new MemoryProvider([
memorySkill('z-skill', 'Z skill', 10),
memorySkill('a-skill', 'A skill', 10),
]))
const localeCompare = vi.spyOn(String.prototype, 'localeCompare')
const sort = vi.spyOn(Array.prototype, 'sort')
try {
const skills = await ctx.skills.list()
expect(skills.map(skill => skill.name)).toEqual(['a-skill', 'z-skill'])
expect(localeCompare).not.toHaveBeenCalled()
const summaryComparator = sort.mock.calls.at(-1)?.[0]
expect(summaryComparator).toBeTypeOf('function')
expect(summaryComparator?.(skills[0], skills[0])).toBe(0)
} finally {
sort.mockRestore()
localeCompare.mockRestore()
}
})
it('caches provider discovery, skips failing providers, and invalidates on runtime skills', async () => {
const ctx = new Context()
await ctx.plugin(SkillService, { collectCacheMaxEntries: 1 })
@@ -203,43 +224,106 @@ describe('SkillService registry', () => {
expect(flakyCalls).toBe(3)
})
it('renders stable prompt guidance and omits it when no skills exist', async () => {
it('abandons an in-flight catalog when provider registrations change', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, { persona: 'base' })
await ctx.plugin(SkillService, { promptFieldMaxLength: 6 })
ctx.skills.registerProvider(new MemoryProvider([
{
...memorySkill('escaped-skill', 'Use </available_skills><oops> safely', 10),
whenToUse: 'Handle <tag> & marker',
await ctx.plugin(SkillService)
let markStarted: (() => void) | undefined
let release: (() => void) | undefined
const started = new Promise<void>((resolve) => { markStarted = resolve })
const gate = new Promise<void>((resolve) => { release = resolve })
const dispose = ctx.skills.registerProvider({
name: 'delayed',
async list() {
markStarted?.()
await gate
return [{ ...memorySkill('stale-skill', 'Stale', 10), provider: 'delayed' }]
},
]))
async get(candidate) {
return { ...candidate, content: 'Stale body.' }
},
})
const listing = await ctx.skills.renderModelListing()
expect(listing).toContain('description: Use...')
expect(listing).toContain('whenToUse: Han...')
expect(listing).not.toContain('</available_skills><oops>')
expect(renderPrompt(await ctx.systemPrompt.assemble({ agent: agentForCwd('/tmp') }))).toContain('## Skills')
expect(renderPrompt(await ctx.systemPrompt.assemble())).not.toContain('## Skills')
const pending = ctx.skills.list()
await started
dispose()
release?.()
const empty = new Context()
await empty.plugin(SystemPrompt, { persona: 'base' })
await empty.plugin(SkillService)
expect(await empty.skills.renderModelListing()).toBe('')
expect(renderPrompt(await empty.systemPrompt.assemble({ agent: agentForCwd('/tmp') }))).not.toContain('## Skills')
expect(await pending).toEqual([])
})
const direct = new SkillService(new Context(), {})
expect(await direct.renderModelListing()).toBe('')
const short = new Context()
await short.plugin(SkillService)
short.skills.registerProvider(new MemoryProvider([memorySkill('short-skill', 'Short', 10)]))
expect(await short.skills.renderModelListing()).toContain('description: Short')
it('stops waiting for discovery when its lookup signal aborts', async () => {
const ctx = new Context()
await ctx.plugin(SkillService)
let markStarted: (() => void) | undefined
let release: (() => void) | undefined
let seenSignal: AbortSignal | undefined
const started = new Promise<void>((resolve) => { markStarted = resolve })
const held = new Promise<SkillCandidate[]>((resolve) => {
release = () => { resolve([]) }
})
ctx.skills.registerProvider({
name: 'uncooperative',
list(options) {
seenSignal = options.signal
markStarted?.()
return held
},
async get() {
return undefined
},
})
const controller = new AbortController()
const reason = 'discovery cancelled'
const pending = ctx.skills.list({ signal: controller.signal })
const outcome = pending.then(
() => 'resolved',
(error: unknown) => error instanceof Error && error.message === reason ? 'aborted' : 'other-error',
)
await started
controller.abort(reason)
const templated = new Context()
await templated.plugin(SystemPrompt, { persona: 'base' })
await templated.plugin(SkillService)
templated.skills.registerProvider(new MemoryProvider([memorySkill('templated-skill', 'Use {{placeholder}} safely', 10)]))
const prompt = renderPrompt(await templated.systemPrompt.assemble({ agent: agentForCwd('/tmp') }))
expect(prompt).toContain('description: Use { {placeholder} } safely')
const settled = await Promise.race([
outcome,
new Promise<'timeout'>(resolve => setTimeout(() => { resolve('timeout') }, 25)),
])
release?.()
await pending.catch(() => undefined)
expect(seenSignal).toBe(controller.signal)
expect(settled).toBe('aborted')
})
it('does not miss an abort racing listener installation', async () => {
const ctx = new Context()
await ctx.plugin(SkillService)
const reason = new Error('racing abort')
let aborted = false
const signal = {
get aborted() {
return aborted
},
reason,
throwIfAborted() {
if (aborted) throw reason
},
addEventListener(_type: string, listener: () => void) {
aborted = true
listener()
},
removeEventListener() {},
} as unknown as AbortSignal
ctx.skills.registerProvider({
name: 'racing-abort',
list() {
return Promise.reject(new Error('late provider failure'))
},
async get() {
return undefined
},
})
await expect(ctx.skills.list({ signal })).rejects.toBe(reason)
await Promise.resolve()
})
it('rejects invalid runtime skill registrations and ignores duplicates', async () => {
@@ -8,8 +8,6 @@
"references": [
{ "path": "../../../vendor/cosmokit" },
{ "path": "../../../vendor/cordis" },
{ "path": "../../../vendor/schemastery" },
{ "path": "../agent" },
{ "path": "../system-prompt" }
{ "path": "../../../vendor/schemastery" }
]
}
+21
View File
@@ -0,0 +1,21 @@
# @deepseek-ai/dsh-tool-skill
The model-facing skill catalog and `skill` tool.
Requires `ctx.tools` and `ctx.skills` (`inject: ['tools', 'skills']`).
## Session-prefix catalog
The plugin contributes one user-role `<system-reminder>` catalog through `agent/session-prefix`. It resolves skills for the calling session's cwd, forwards the prefix abort signal to discovery, and lists only sorted `name` and `description` entries; skill bodies, paths, sources, providers, and `whenToUse` hints remain outside the catalog. The catalog is omitted when no model-invocable skills are available.
`catalogDescriptionMaxLength` controls normalized, XML-escaped catalog descriptions. Its default is `500` and values must be integers of at least `3`, which reserves room for a truncation ellipsis. The [session-prefix RFC](../../../docs/rfc/implemented/feature/2026-07-07-session-prefix.md) defines the request-only, header-logged lifecycle of this message.
## Tool: `skill`
| Arg | Type | Notes |
|---|---|---|
| `name` | string (required) | Exact kebab-case skill name from the available skills listing. |
Execution uses the calling agent's `session.header.cwd` so workspace-sensitive providers can resolve the right winning skill. A successful call returns one text tool result with `<skill_content name="...">`, containing `<skill_resources>` followed by `<skill_instructions>`. Resource guidance resolves paths or URLs explicitly referenced by the loaded instructions against `resourceBase`; referenced scripts, references, and assets load only when needed, and the tool does not enumerate a skill directory. Local filesystem skills provide a base directory, while remote or embedded providers can provide a URL or opaque provider-managed guidance. A name that cannot be resolved reports that the skill is unknown or no longer available; invalid names and skills marked `disableModelInvocation: true` retain distinct `isError` results.
The tool does not call `agent.inject()` in v1. Its result is already recorded as the tool result and becomes available to the next model step without duplicating the content as synthetic context.
@@ -28,6 +28,9 @@
"@deepseek-ai/dsh-tools": "^0.0.1",
"cordis": "^4.0.0-rc.6"
},
"dependencies": {
"schemastery": "^3.18.0"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
+152
View File
@@ -0,0 +1,152 @@
/**
* Session-prefix skill catalog and model-facing `skill` loader tool.
*
* @module @deepseek-ai/dsh-tool-skill
*/
import type { Context } from 'cordis'
import z from 'schemastery'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { assertNever, type Message } from '@deepseek-ai/dsh-llm'
import { isSkillName, type SkillDefinition, type SkillSummary } from '@deepseek-ai/dsh-skill'
export const name = 'tool-skill'
export const inject = ['tools', 'skills']
const DEFAULT_CATALOG_DESCRIPTION_MAX_LENGTH = 500
/** Model-facing skill catalog configuration. */
export interface Config {
/** Maximum normalized description length rendered in the session catalog; minimum 3. */
catalogDescriptionMaxLength?: number
}
/** Validate and default the model-facing skill catalog configuration. */
export const Config: z<Config> = z.object({
catalogDescriptionMaxLength: z.number().default(DEFAULT_CATALOG_DESCRIPTION_MAX_LENGTH),
})
/** Register the session-prefix skill catalog and the model-facing skill loader. */
export function apply(ctx: Context, config: Config = {}): void {
const catalogDescriptionMaxLength = config.catalogDescriptionMaxLength ?? DEFAULT_CATALOG_DESCRIPTION_MAX_LENGTH
assertPositiveInteger('catalogDescriptionMaxLength', catalogDescriptionMaxLength, 3)
ctx.on('agent/session-prefix', async (agent, _prefix, signal, next): Promise<Message[]> => {
const skills = await ctx.skills.list({ cwd: agent.session.header.cwd, signal })
const rest = await next()
if (skills.length === 0) return rest
return [renderCatalogMessage(skills, catalogDescriptionMaxLength), ...rest]
})
const skillTool = defineTool({
name: 'skill',
description: 'Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.',
parameters: {
name: { type: 'string', required: true, description: 'The exact skill name from the available skills list.' },
},
async execute(args, exec) {
if (!isSkillName(args.name)) {
throw new Error(`invalid skill name "${args.name}"`)
}
const skill = await ctx.skills.get(args.name, { cwd: exec.agent?.session.header.cwd, signal: exec.signal })
if (!skill) {
throw new Error(`skill "${args.name}" is unknown or no longer available`)
}
if (skill.disableModelInvocation === true) {
throw new Error(`skill "${args.name}" is not available for model invocation`)
}
return [{ type: 'text', text: renderSkillContent(skill) }]
},
presentCall(args) {
return { card: 'generic', title: `Load skill ${args.name}`, kind: 'read', rawInput: args.name }
},
})
ctx.tools.register(skillTool)
}
function renderSkillContent(skill: SkillDefinition): string {
const resourceHint = renderResourceHint(skill)
return [
`<skill_content name="${escapeAttr(skill.name)}">`,
'<skill_resources>',
...resourceHint,
'</skill_resources>',
'',
'<skill_instructions>',
skill.content,
'</skill_instructions>',
'</skill_content>',
].join('\n')
}
function renderResourceHint(skill: SkillDefinition): string[] {
const base = skill.resourceBase
if (base === undefined) {
return [
`Resources for this skill are managed by provider "${escapeText(skill.provider)}".`,
'Load referenced resources only as needed.',
]
}
switch (base.kind) {
case 'directory':
return [
`Base directory for this skill: ${escapeText(base.path)}`,
'Resolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed.',
]
case 'url':
return [
`Base URL for this skill: ${escapeText(base.url)}`,
'Resolve relative URLs mentioned by this skill against the base URL before using them. Load referenced resources only as needed.',
]
case 'opaque':
return [
`Resources for this skill: ${escapeText(base.description)}`,
'Load referenced resources only as needed.',
]
default:
return assertNever(base, 'SkillResourceBase.kind')
}
}
function renderCatalogMessage(skills: SkillSummary[], descriptionMaxLength: number): Message {
const entries = skills.map(skill => `- \`${skill.name}\`: ${catalogDescription(skill.description, descriptionMaxLength)}`)
return {
role: 'user',
content: [{
type: 'text',
text: [
'<system-reminder>',
'A skill is a reusable set of task-specific instructions. The following skills are available in this session:',
'',
'<available_skills>',
...entries,
'</available_skills>',
'',
"If the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.",
'</system-reminder>',
].join('\n'),
}],
}
}
function catalogDescription(value: string, maxLength: number): string {
const normalized = value.replaceAll(/\s+/g, ' ').trim()
const truncated = normalized.length <= maxLength
? normalized
: `${normalized.slice(0, maxLength - 3)}...`
return escapeText(truncated)
}
function assertPositiveInteger(name: string, value: number, minimum = 1): void {
if (!Number.isInteger(value) || value < minimum) {
throw new Error(`tool-skill: ${name} must be an integer greater than or equal to ${minimum}`)
}
}
function escapeAttr(value: string): string {
return value.replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll('<', '&lt;')
}
function escapeText(value: string): string {
return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')
}
@@ -0,0 +1,275 @@
import { describe, expect, it } from 'vitest'
import { mkdir, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { Context } from 'cordis'
import { CallId, type Message } from '@deepseek-ai/dsh-llm'
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import SkillService from '@deepseek-ai/dsh-skill'
import * as SkillLocal from '@deepseek-ai/dsh-skill-local'
import * as toolSkill from '@deepseek-ai/dsh-tool-skill'
async function tempDir(name: string): Promise<string> {
return await import('node:fs/promises').then(fs => fs.mkdtemp(join(tmpdir(), `dsh-${name}-`)))
}
async function writeSkill(root: string, name: string, description: string, body: string): Promise<void> {
const dir = join(root, name)
await mkdir(dir, { recursive: true })
await writeFile(join(dir, 'SKILL.md'), `---\nname: ${name}\ndescription: ${description}\n---\n\n${body}\n`)
}
async function setup(home: string, config: toolSkill.Config = {}): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SkillService)
await ctx.plugin(SkillLocal, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents') })
await ctx.plugin(toolSkill, config)
return ctx
}
function agentForCwd(cwd: string): never {
return { session: { header: { cwd } } } as never
}
async function composePrefix(ctx: Context, cwd: string, signal = new AbortController().signal): Promise<Message[]> {
const empty: Message[] = []
return await ctx.waterfall(
'agent/session-prefix', agentForCwd(cwd), empty, signal,
() => Promise.resolve(empty),
)
}
describe('dsh-tool-skill', () => {
it('registers the skill tool schema and removes it on dispose', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
const home = await tempDir('tool-schema')
await ctx.plugin(SkillService)
await ctx.plugin(SkillLocal, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents') })
ctx.skills.register({ name: 'lifecycle-skill', description: 'Lifecycle', source: 'runtime', content: 'body' })
const fiber = await ctx.plugin(toolSkill)
expect(ctx.tools.schemas().map(tool => tool.name)).toEqual(['skill'])
expect(await composePrefix(ctx, '/workspace')).toHaveLength(1)
expect(ctx.tools.get('skill')?.presentCall?.({ name: 'project-skill' })).toEqual({
card: 'generic',
title: 'Load skill project-skill',
kind: 'read',
rawInput: 'project-skill',
})
await fiber.dispose()
expect(ctx.tools.schemas()).toEqual([])
expect(await composePrefix(ctx, '/workspace')).toEqual([])
toolSkill.apply(ctx)
expect(ctx.tools.schemas().map(tool => tool.name)).toEqual(['skill'])
})
it('forwards the session-prefix abort signal to skill discovery', async () => {
const home = await tempDir('tool-prefix-signal')
const ctx = await setup(home)
let seenSignal: AbortSignal | undefined
ctx.skills.registerProvider({
name: 'signal-probe',
async list(options) {
seenSignal = options.signal
return []
},
async get() {
return undefined
},
})
const controller = new AbortController()
await composePrefix(ctx, '/workspace', controller.signal)
expect(seenSignal).toBe(controller.signal)
})
it('contributes a stable name-and-description catalog through the session prefix', async () => {
const home = await tempDir('tool-catalog')
const ctx = await setup(home, { catalogDescriptionMaxLength: 50 })
ctx.skills.register({
name: 'z-skill',
description: 'Long description '.repeat(5),
whenToUse: 'Never render this routing hint.',
source: 'secret-source',
provider: 'runtime',
resourceBase: { kind: 'directory', path: '/secret/path' },
content: 'Secret body.',
})
ctx.skills.register({
name: 'a-skill',
description: 'Use {{placeholder}} <safely> & carefully.',
source: 'runtime',
provider: 'runtime',
content: 'A body.',
})
ctx.on('agent/session-prefix', async (_agent, _prefix, _signal, next): Promise<Message[]> => [
{ role: 'user', content: [{ type: 'text', text: 'later contribution' }] },
...await next(),
])
const prefix = await composePrefix(ctx, '/workspace')
expect(prefix).toEqual([
{
role: 'user',
content: [{
type: 'text',
text: [
'<system-reminder>',
'A skill is a reusable set of task-specific instructions. The following skills are available in this session:',
'',
'<available_skills>',
'- `a-skill`: Use {{placeholder}} &lt;safely&gt; &amp; carefully.',
'- `z-skill`: Long description Long description Long descript...',
'</available_skills>',
'',
"If the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.",
'</system-reminder>',
].join('\n'),
}],
},
{ role: 'user', content: [{ type: 'text', text: 'later contribution' }] },
])
const rendered = JSON.stringify(prefix[0])
expect(rendered).not.toContain('whenToUse')
expect(rendered).not.toContain('secret-source')
expect(rendered).not.toContain('/secret/path')
expect(rendered).not.toContain('Secret body')
expect(renderPrompt(await ctx.systemPrompt.assemble({ agent: agentForCwd('/workspace') }))).not.toContain('<available_skills>')
})
it('does not contribute a session-prefix message when no skills are available', async () => {
const home = await tempDir('tool-empty-catalog')
const ctx = await setup(home)
expect(await composePrefix(ctx, '/workspace')).toEqual([])
})
it('validates the catalog description cap', async () => {
const home = await tempDir('tool-invalid-catalog-cap')
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SkillService)
await ctx.plugin(SkillLocal, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents') })
await expect(ctx.plugin(toolSkill, { catalogDescriptionMaxLength: 2 })).rejects.toThrow('greater than or equal to 3')
})
it('loads a skill for the calling agent cwd', async () => {
const home = await tempDir('tool-load')
const project = await tempDir('tool-project')
await mkdir(join(project, '.git'), { recursive: true })
await writeSkill(join(project, '.dsh/skills'), 'project-skill', 'Project skill', 'Project instructions.')
const ctx = await setup(home)
const result = await ctx.tools.execute({
callId: CallId('c1'),
name: 'skill',
arguments: { name: 'project-skill' },
agent: { session: { header: { cwd: project } } } as never,
})
expect(result.isError).toBe(false)
const block = result.content[0]
expect(block?.type).toBe('text')
if (block?.type !== 'text') throw new Error('expected text skill result')
expect(block.text).toBe([
'<skill_content name="project-skill">',
'<skill_resources>',
`Base directory for this skill: ${join(project, '.dsh/skills/project-skill')}`,
'Resolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed.',
'</skill_resources>',
'',
'<skill_instructions>',
'Project instructions.',
'</skill_instructions>',
'</skill_content>',
].join('\n'))
expect(block.text).not.toContain('# Skill:')
})
it('renders provider-managed resource hints for non-local skills', async () => {
const home = await tempDir('tool-resource-hints')
const ctx = await setup(home)
ctx.skills.register({
name: 'opaque-skill',
description: 'Opaque skill',
source: 'runtime',
provider: 'runtime',
resourceBase: { kind: 'opaque', description: 'runtime memory' },
content: 'Opaque instructions.',
})
ctx.skills.register({
name: 'url-skill',
description: 'URL skill',
source: 'runtime',
provider: 'runtime',
resourceBase: { kind: 'url', url: 'https://skills.example.test/url-skill' },
content: 'URL instructions.',
})
ctx.skills.register({
name: 'provider-skill',
description: 'Provider skill',
source: 'runtime',
provider: 'runtime',
content: 'Provider instructions.',
})
const opaque = await ctx.tools.execute({ callId: CallId('c2'), name: 'skill', arguments: { name: 'opaque-skill' } })
const url = await ctx.tools.execute({ callId: CallId('c3'), name: 'skill', arguments: { name: 'url-skill' } })
const provider = await ctx.tools.execute({ callId: CallId('c4'), name: 'skill', arguments: { name: 'provider-skill' } })
if (opaque.content[0]?.type !== 'text' || url.content[0]?.type !== 'text' || provider.content[0]?.type !== 'text') {
throw new Error('expected text tool results')
}
expect(opaque.content[0].text).toContain('<skill_resources>\nResources for this skill: runtime memory\nLoad referenced resources only as needed.\n</skill_resources>')
expect(url.content[0].text).toContain('<skill_resources>\nBase URL for this skill: https://skills.example.test/url-skill\nResolve relative URLs mentioned by this skill against the base URL before using them. Load referenced resources only as needed.\n</skill_resources>')
expect(provider.content[0].text).toContain('<skill_resources>\nResources for this skill are managed by provider "runtime".\nLoad referenced resources only as needed.\n</skill_resources>')
})
it('fails loud on an unknown resource base kind', async () => {
const home = await tempDir('tool-resource-assert-never')
const ctx = await setup(home)
ctx.skills.register({
name: 'rogue-resource-skill',
description: 'Rogue resource skill',
source: 'runtime',
provider: 'runtime',
resourceBase: { kind: 'future' } as never,
content: 'Rogue instructions.',
})
const result = await ctx.tools.execute({ callId: CallId('c5'), name: 'skill', arguments: { name: 'rogue-resource-skill' } })
expect(result.isError).toBe(true)
const block = result.content[0]
if (block?.type !== 'text') throw new Error('expected text tool result')
expect(block.text).toContain('unreachable variant')
})
it('returns isError for unknown, invalid, and model-disabled skills', async () => {
const home = await tempDir('tool-errors')
await writeSkill(join(home, '.dsh/skills'), 'hidden-skill', 'Hidden skill', 'Hidden instructions.')
await writeFile(join(home, '.dsh/skills/hidden-skill/SKILL.md'), '---\nname: hidden-skill\ndescription: Hidden skill\ndisableModelInvocation: true\n---\n\nHidden instructions.\n')
const ctx = await setup(home)
const unknown = await ctx.tools.execute({ callId: CallId('c1'), name: 'skill', arguments: { name: 'missing' } })
const invalid = await ctx.tools.execute({ callId: CallId('c2'), name: 'skill', arguments: { name: 'Bad_Name' } })
const disabled = await ctx.tools.execute({ callId: CallId('c3'), name: 'skill', arguments: { name: 'hidden-skill' } })
expect(unknown.isError).toBe(true)
expect(invalid.isError).toBe(true)
expect(disabled.isError).toBe(true)
const unknownBlock = unknown.content[0]
if (unknownBlock?.type !== 'text') throw new Error('expected text tool result')
expect(unknownBlock.text).toContain('skill "missing" is unknown or no longer available')
})
})
@@ -8,9 +8,10 @@
"references": [
{ "path": "../../../vendor/cosmokit" },
{ "path": "../../../vendor/cordis" },
{ "path": "../../../vendor/schemastery" },
{ "path": "../../llm/llm" },
{ "path": "../agent" },
{ "path": "../../core/agent" },
{ "path": "../skill" },
{ "path": "../tools" }
{ "path": "../../core/tools" }
]
}
+1 -1
View File
@@ -56,7 +56,7 @@ export interface Config {
toolOrder?: string[]
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
persistenceRoot?: string
/** Skill registry/local-provider config forwarded to the shared agent-core spine. */
/** Skill registry, local-provider, and model-facing consumer config forwarded to agent-core. */
skills?: agentCore.SkillConfig
}
+17 -4
View File
@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
import type { Message } from '@deepseek-ai/dsh-llm'
import * as acpAgent from '../src/index.ts'
/**
@@ -26,9 +27,20 @@ async function mount(config: acpAgent.Config): Promise<Context> {
return ctx
}
async function isolatedSkillsConfig(): Promise<NonNullable<acpAgent.Config['skills']>> {
async function isolatedSkillsConfig(catalogDescriptionMaxLength?: number): Promise<NonNullable<acpAgent.Config['skills']>> {
const home = await mkdtemp(join(tmpdir(), 'dsh-acp-agent-skills-'))
return { local: { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents') } }
return {
local: { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents') },
...catalogDescriptionMaxLength !== undefined ? { tool: { catalogDescriptionMaxLength } } : {},
}
}
async function composePrefix(ctx: Context): Promise<Message[]> {
const empty: Message[] = []
return await ctx.waterfall(
'agent/session-prefix', { session: { header: { cwd: '/tmp' } } } as never,
empty, new AbortController().signal, () => Promise.resolve(empty),
)
}
async function withIsolatedSkillHomes<T>(run: () => Promise<T>): Promise<T> {
@@ -92,8 +104,9 @@ describe('dsh-acp-agent composition', () => {
})
it('forwards skill config into agent-core', async () => {
const ctx = await mount({ model: 'mock', persona: 'hi', skills: await isolatedSkillsConfig() })
expect(await ctx.skills.list()).toEqual([])
const ctx = await mount({ model: 'mock', persona: 'hi', skills: await isolatedSkillsConfig(6) })
ctx.skills.register({ name: 'acp-skill', description: 'ACP skill', source: 'runtime', content: 'body' })
expect(JSON.stringify(await composePrefix(ctx))).toContain('- `acp-skill`: ACP...')
await ctx.fiber.dispose()
})
+1 -1
View File
@@ -72,7 +72,7 @@ export interface Config {
persistenceRoot?: string
/** stdin-chat banner printed once on start. Defaults to `'ready.'`. */
welcome?: string
/** Skill registry/local-provider config forwarded to the shared agent-core spine. */
/** Skill registry, local-provider, and model-facing consumer config forwarded to agent-core. */
skills?: agentCore.SkillConfig
/**
* If set, the `main` agent RESUMES this persisted session id instead of
@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import { AgentId } from '@deepseek-ai/dsh-agent'
import type { Message } from '@deepseek-ai/dsh-llm'
import { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
import * as stdioAgent from '../src/index.ts'
@@ -33,9 +34,20 @@ async function mount(config: stdioAgent.Config): Promise<Context> {
return ctx
}
async function isolatedSkillsConfig(): Promise<NonNullable<stdioAgent.Config['skills']>> {
async function isolatedSkillsConfig(catalogDescriptionMaxLength?: number): Promise<NonNullable<stdioAgent.Config['skills']>> {
const home = await mkdtemp(join(tmpdir(), 'dsh-stdio-agent-skills-'))
return { local: { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents') } }
return {
local: { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents') },
...catalogDescriptionMaxLength !== undefined ? { tool: { catalogDescriptionMaxLength } } : {},
}
}
async function composePrefix(ctx: Context): Promise<Message[]> {
const empty: Message[] = []
return await ctx.waterfall(
'agent/session-prefix', { session: { header: { cwd: '/tmp' } } } as never,
empty, new AbortController().signal, () => Promise.resolve(empty),
)
}
async function withIsolatedSkillHomes<T>(run: () => Promise<T>): Promise<T> {
@@ -117,8 +129,9 @@ describe('dsh-stdio-agent app', () => {
})
it('forwards skill config into agent-core', async () => {
const ctx = await mount({ model: 'mock', persona: 'hi', skills: await isolatedSkillsConfig() })
expect(await ctx.skills.list()).toEqual([])
const ctx = await mount({ model: 'mock', persona: 'hi', skills: await isolatedSkillsConfig(6) })
ctx.skills.register({ name: 'stdio-skill', description: 'Stdio skill', source: 'runtime', content: 'body' })
expect(JSON.stringify(await composePrefix(ctx))).toContain('- `stdio-skill`: Std...')
await ctx.fiber.dispose()
})
+69 -71
View File
@@ -269,10 +269,10 @@ importers:
version: link:../session
'@deepseek-ai/dsh-skill':
specifier: workspace:^
version: link:../skill
version: link:../../skill/skill
'@deepseek-ai/dsh-skill-local':
specifier: workspace:^
version: link:../skill-local
version: link:../../skill/skill-local
'@deepseek-ai/dsh-system-prompt':
specifier: workspace:^
version: link:../system-prompt
@@ -281,7 +281,7 @@ importers:
version: link:../../bash/tool-bash
'@deepseek-ai/dsh-tool-skill':
specifier: workspace:^
version: link:../tool-skill
version: link:../../skill/tool-skill
'@deepseek-ai/dsh-tools':
specifier: workspace:^
version: link:../tools
@@ -335,41 +335,6 @@ importers:
specifier: ^4.0.0-rc.6
version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
packages/core/skill:
dependencies:
schemastery:
specifier: ^3.18.0
version: 3.18.0
devDependencies:
'@deepseek-ai/dsh-agent':
specifier: workspace:^
version: link:../agent
'@deepseek-ai/dsh-system-prompt':
specifier: workspace:^
version: link:../system-prompt
cordis:
specifier: ^4.0.0-rc.6
version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
packages/core/skill-local:
dependencies:
schemastery:
specifier: ^3.18.0
version: 3.18.0
yaml:
specifier: ^2.4.2
version: 2.9.0
devDependencies:
'@deepseek-ai/dsh-fs':
specifier: workspace:^
version: link:../../fs/fs
'@deepseek-ai/dsh-skill':
specifier: workspace:^
version: link:../skill
cordis:
specifier: ^4.0.0-rc.6
version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
packages/core/system-prompt:
dependencies:
schemastery:
@@ -383,27 +348,6 @@ importers:
specifier: ^4.0.0-rc.6
version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
packages/core/tool-skill:
devDependencies:
'@deepseek-ai/dsh-agent':
specifier: workspace:^
version: link:../agent
'@deepseek-ai/dsh-llm':
specifier: workspace:^
version: link:../../llm/llm
'@deepseek-ai/dsh-skill':
specifier: workspace:^
version: link:../skill
'@deepseek-ai/dsh-skill-local':
specifier: workspace:^
version: link:../skill-local
'@deepseek-ai/dsh-tools':
specifier: workspace:^
version: link:../tools
cordis:
specifier: ^4.0.0-rc.6
version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
packages/core/tools:
devDependencies:
'@deepseek-ai/dsh-agent':
@@ -419,18 +363,6 @@ importers:
specifier: ^4.0.0-rc.6
version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
packages/ui/user-interaction:
devDependencies:
'@deepseek-ai/dsh-agent':
specifier: workspace:^
version: link:../../core/agent
'@deepseek-ai/dsh-llm':
specifier: workspace:^
version: link:../../llm/llm
cordis:
specifier: ^4.0.0-rc.6
version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
packages/fs/fs:
devDependencies:
'@deepseek-ai/dsh-brand':
@@ -713,6 +645,60 @@ importers:
specifier: ^4.0.0-rc.6
version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
packages/skill/skill:
dependencies:
schemastery:
specifier: ^3.18.0
version: 3.18.0
devDependencies:
cordis:
specifier: ^4.0.0-rc.6
version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
packages/skill/skill-local:
dependencies:
schemastery:
specifier: ^3.18.0
version: 3.18.0
yaml:
specifier: ^2.4.2
version: 2.9.0
devDependencies:
'@deepseek-ai/dsh-fs':
specifier: workspace:^
version: link:../../fs/fs
'@deepseek-ai/dsh-skill':
specifier: workspace:^
version: link:../skill
cordis:
specifier: ^4.0.0-rc.6
version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
packages/skill/tool-skill:
dependencies:
schemastery:
specifier: ^3.18.0
version: 3.18.0
devDependencies:
'@deepseek-ai/dsh-agent':
specifier: workspace:^
version: link:../../core/agent
'@deepseek-ai/dsh-llm':
specifier: workspace:^
version: link:../../llm/llm
'@deepseek-ai/dsh-skill':
specifier: workspace:^
version: link:../skill
'@deepseek-ai/dsh-skill-local':
specifier: workspace:^
version: link:../skill-local
'@deepseek-ai/dsh-tools':
specifier: workspace:^
version: link:../../core/tools
cordis:
specifier: ^4.0.0-rc.6
version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
packages/subagent/subagent:
devDependencies:
'@deepseek-ai/dsh-agent':
@@ -1188,6 +1174,18 @@ importers:
specifier: ^4.0.0-rc.6
version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
packages/ui/user-interaction:
devDependencies:
'@deepseek-ai/dsh-agent':
specifier: workspace:^
version: link:../../core/agent
'@deepseek-ai/dsh-llm':
specifier: workspace:^
version: link:../../llm/llm
cordis:
specifier: ^4.0.0-rc.6
version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
packages/util/brand:
devDependencies:
cordis:
+5 -3
View File
@@ -71,6 +71,7 @@ const GROUP_ORDER = [
'core',
'bash',
'fs',
'skill',
'compact',
'subagent',
'web',
@@ -138,9 +139,10 @@ const SERVICE_ROLES: ServiceRole[] = [
key: 'skills',
pkg: 'skill',
title: 'Skill provider registry',
mode: 'core',
consumers: ['agent-core', 'skill-local', 'tool-skill'],
note: 'Merges provider skill catalogs, injects request-time listings, and serves full skill bodies to the skill tool.',
mode: 'seam',
implementations: ['skill-local'],
consumers: ['tool-skill'],
note: 'Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies.',
},
{
key: 'agents',
+1
View File
@@ -42,6 +42,7 @@ const GROUP_ORDER = [
'core',
'bash',
'fs',
'skill',
'compact',
'subagent',
'web',
+1 -1
View File
@@ -163,7 +163,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
{
pkg: '@deepseek-ai/dsh-tool-skill',
dir: 'tool-skill',
source: 'packages/core/tool-skill/src/index.ts',
source: 'packages/skill/tool-skill/src/index.ts',
requires: ['ctx.tools', 'ctx.skills'],
writes: ['tool/call', 'tool/result'],
async mount(ctx) {
+9 -9
View File
@@ -83,15 +83,15 @@
{ "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": "SkillResourceBase", "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": "SkillCandidate", "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": "SkillProvider", "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/skills.md", "symbol": "SkillSource", "source": "packages/skill/skill/src/index.ts" },
{ "doc": "docs/core-data-structures/skills.md", "symbol": "SkillResourceBase", "source": "packages/skill/skill/src/index.ts" },
{ "doc": "docs/core-data-structures/skills.md", "symbol": "SkillSummary", "source": "packages/skill/skill/src/index.ts" },
{ "doc": "docs/core-data-structures/skills.md", "symbol": "SkillCandidate", "source": "packages/skill/skill/src/index.ts" },
{ "doc": "docs/core-data-structures/skills.md", "symbol": "SkillDefinition", "source": "packages/skill/skill/src/index.ts" },
{ "doc": "docs/core-data-structures/skills.md", "symbol": "SkillRegistration", "source": "packages/skill/skill/src/index.ts" },
{ "doc": "docs/core-data-structures/skills.md", "symbol": "SkillLookupOptions", "source": "packages/skill/skill/src/index.ts" },
{ "doc": "docs/core-data-structures/skills.md", "symbol": "SkillProvider", "source": "packages/skill/skill/src/index.ts" },
{ "doc": "docs/core-data-structures/skills.md", "symbol": "Config", "source": "packages/skill/skill/src/index.ts" },
{ "doc": "docs/core-data-structures/compaction.md", "symbol": "CompactionResult", "source": "packages/compact/compact/src/types.ts" },
+1
View File
@@ -45,6 +45,7 @@
"./packages/bash/*/src",
"./packages/code-runtime/*/src",
"./packages/fs/*/src",
"./packages/skill/*/src",
"./packages/compact/*/src",
"./packages/guard/*/src",
"./packages/subagent/*/src",
+3 -3
View File
@@ -21,9 +21,9 @@
{ "path": "./packages/core/agent" },
{ "path": "./packages/ui/user-interaction" },
{ "path": "./packages/core/tools" },
{ "path": "./packages/core/skill" },
{ "path": "./packages/core/skill-local" },
{ "path": "./packages/core/tool-skill" },
{ "path": "./packages/skill/skill" },
{ "path": "./packages/skill/skill-local" },
{ "path": "./packages/skill/tool-skill" },
{ "path": "./packages/ui/tool-ask-user" },
{ "path": "./packages/core/agent-loop" },
{ "path": "./packages/core/agent-core" },
+3 -3
View File
@@ -32,9 +32,9 @@
{ "path": "./packages/core/agent" },
{ "path": "./packages/ui/user-interaction" },
{ "path": "./packages/core/tools" },
{ "path": "./packages/core/skill" },
{ "path": "./packages/core/skill-local" },
{ "path": "./packages/core/tool-skill" },
{ "path": "./packages/skill/skill" },
{ "path": "./packages/skill/skill-local" },
{ "path": "./packages/skill/tool-skill" },
{ "path": "./packages/ui/tool-ask-user" },
{ "path": "./packages/core/agent-loop" },
{ "path": "./packages/core/agent-core" },