Merge remote-tracking branch 'origin/master' into codex/simp-drop-skill-provider-events

# Conflicts:
#	docs/event-producer-consumer.md
This commit is contained in:
Tianyi Cui
2026-07-14 18:00:41 +08:00
562 changed files with 4439 additions and 12572 deletions

View File

@@ -28,7 +28,7 @@ Default roots are resolved in this provider's rank order:
| 400 | `user-dsh` | `<dshHome>/skills` |
| 500 | `user-agents` | `<agentsHome>/skills` |
The project root is the nearest ancestor containing `.git`; without one, the current cwd is used. The user DSH root skips its `.system` child so system-owned directories are not accidentally treated as normal user skills. This provider ships no built-in system skills; another provider can supply embedded or remote built-ins.
The project root is the nearest ancestor containing `.git`; without one, the current cwd is used. The user DSH root skips its `.system` child so system-owned directories are not treated as normal user skills. This provider supplies project and user skills; another provider may supply built-in system skills.
When `ctx.fs` is available, discovery lists roots through `ctx.fs.listDir`, reads skill files through `ctx.fs.readText`, and probes `.git` through the filesystem service. Full skill loads forward the lookup abort signal to filesystem metadata and content reads. Without a filesystem service, the provider falls back to abortable Node filesystem I/O so minimal local contexts can still load skills. Missing, unreadable, or malformed skill files warn and skip instead of failing the whole request.

View File

@@ -21,11 +21,11 @@ This package owns the `ctx.skills` interface. It does not know whether skills co
## Provider Contract
A provider registers synchronously from its `apply()` and returns `readonly SkillCandidate[]` from `list(options)` when discovery is requested. The provider, lookup options, candidates, and loaded definitions are readonly same-process contracts: the registry borrows them rather than cloning, freezing, or rebinding callbacks. 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 uncooperative discovery and loading work so agent cancellation cannot hang prefix composition or skill loading.
A provider registers synchronously and performs remote setup, authentication, and discovery in its awaited `list(options)` call. Provider objects, lookup options, candidates, and definitions are borrowed readonly rather than cloned or rebound. Providers should honor `options.signal`; the registry also stops awaiting uncooperative discovery or loading after cancellation.
The registry validates parsed provider candidates before caching them and validates loaded definitions before returning them. The winning provider receives the exact candidate and opaque `locator` identity it returned from `list()`; a local provider can therefore use a file-path handle while a remote provider can use a URL, id, or version token. Callers and providers must honor the readonly contract after handing values to the registry.
The registry validates candidates before caching and definitions before returning them. The winning provider receives the same candidate and opaque `locator` it returned from `list()`, allowing backend-specific file, URL, id, or version handles. Callers and providers must preserve the readonly contract.
Parsed candidate and loaded-definition fields are validated at the provider boundary: names/descriptions/content use their declared string types, ranks are finite numbers, and `disableModelInvocation` is boolean when present. Candidate contract violations fail fast because the provider or its parser 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.
Contract violations fail fast. A rejected `list()` is treated as a transient source failure: it is logged, skipped, and not cached. Only completed catalogs are cached; a provider or runtime revision change discards an in-flight result and retries. Duplicate names resolve by rank, provider registration order, then provider-local order. Summaries are sorted by skill name.
## Runtime Skills

View File

@@ -158,14 +158,9 @@ export class SkillService extends Service {
}
/**
* 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. Providers
* are readonly same-process registrations: the registry borrows the provider
* object and invokes its methods directly. Effect-scoped and HMR-safe:
* disposing the caller's fiber unregisters the provider and invalidates
* cached catalogs.
* Register a borrowed same-process provider synchronously during plugin apply. Duplicate and
* reserved names throw; remote initialization belongs in `list()`. Fiber disposal unregisters
* the provider and invalidates catalog caches.
* @param provider - the provider to register by `provider.name`.
* @returns the exact Cordis effect disposer that unregisters this provider;
* composite effects may yield it directly to preserve teardown ordering.
@@ -195,16 +190,11 @@ export class SkillService extends Service {
}
/**
* Register a runtime skill contribution. Runtime registrations are treated as
* embedded provider entries with project-over-user priority. Same-name runtime
* registrations are first-wins: a duplicate logs a warning and gets a no-op
* disposer so it cannot remove the active contribution. Runtime definitions
* are readonly same-process registrations; the registry borrows their nested
* resource metadata.
* Register a borrowed readonly runtime skill. Project entries outrank runtime entries, which
* outrank user entries. Same-name runtime entries are first-wins; a duplicate logs a warning and
* receives a no-op disposer so it cannot remove the winner.
* @param skill - the complete skill definition to expose for discovery.
* @returns the exact Cordis effect disposer that removes this runtime
* contribution and invalidates caches; composite effects may yield it
* directly to preserve teardown ordering.
* @returns the exact Cordis effect disposer, preserving composite teardown order and invalidating caches.
*/
register(skill: SkillRegistration): () => void {
validateRuntimeSkill(skill)
@@ -246,12 +236,9 @@ export class SkillService extends Service {
}
/**
* Load one full skill definition by name. The provider receives the winning
* candidate it returned during discovery, including its opaque locator, and
* the registry returns the provider's definition after validating it.
* Cancellation is rechecked after catalog
* selection (including a cache hit), and provider loading is raced against the
* same signal so an uncooperative provider cannot hang the caller.
* Load and validate the winning candidate, passing its opaque discovery locator back to the
* provider. Cancellation is rechecked after selection, including cache hits, and raced against
* loading so an uncooperative provider cannot hang the caller.
* @param name - kebab-case skill name.
* @param options - lookup options; `cwd` selects workspace-sensitive skills and `signal` cancels work.
* @returns the full skill, including body content, or `undefined`.

View File

@@ -16,7 +16,11 @@ The plugin contributes one user-role `<system-reminder>` catalog through `agent/
|---|---|---|
| `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.
Execution uses the calling agent's `session.header.cwd` so workspace-sensitive providers resolve the winning skill. A successful call returns one text result containing `<skill_content name="...">`, `<skill_resources>`, and `<skill_instructions>`.
Resource guidance resolves only paths or URLs explicitly referenced by the instructions against `resourceBase`; scripts, references, and assets load on demand, and the result does not enumerate a skill directory. Local providers may supply a directory, while remote or embedded providers may supply a URL or opaque loading guidance.
An unresolved name reports that the skill is unknown or no longer available. Invalid names and `disableModelInvocation: true` skills produce distinct error 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.

View File

@@ -66,10 +66,8 @@ export function apply(ctx: Context, config: Config = {}): void {
throw new Error('dsh-tool-skill: registered skill tool is not visible in the global registry')
}
// Register after the tool so reverse-order fiber teardown removes this
// guidance listener before its referenced tool. Exact definition identity is
// the shared truth for restrictions and scoped shadows: another tool merely
// named `skill` must not inherit this plugin's catalog or instructions.
// Register after the tool so reverse teardown removes guidance first. Exact definition
// identity prevents a scoped shadow merely named `skill` from inheriting this catalog.
ctx.on('agent/session-prefix', async (agent, _prefix, signal, next): Promise<Message[]> => {
if (ctx.tools.get(skillTool.name, agent) !== registeredSkillTool) return await next()
const skills = await ctx.skills.list({ cwd: agent.session.header.cwd, signal })