Skip to content

docs: add design for invoking a skill from a message - #552

Closed
andrii-novikov wants to merge 14 commits into
developmentfrom
docs/549-skill-invocation-design
Closed

andrii-novikov wants to merge 14 commits into
developmentfrom
docs/549-skill-invocation-design

Conversation

@andrii-novikov

Copy link
Copy Markdown
Collaborator

Applicable issues

Stacked PR. Based on feat/418-skills-as-dial-resource (#524), not on development — the design builds directly on DialSkillResolver / DialSkillReader and the <skill_files> inventory introduced there. Review #524 first; this PR's diff is a single new file.

Description of changes

Why. A skill reaches a QuickApp agent in exactly one way today: the app author lists it in ApplicationConfig.skills, QuickApps advertises it in <available_skills>, and the model decides whether to call read_skill. The user has no say. That leaves two gaps:

  • No deterministic invocation. Even for an attached skill, "please use the code-review skill" is a hint the model may ignore. Other agent products make /skill-name a guarantee.
  • No way in for the user's own skills. A user who wrote code-review in their own DIAL catalog cannot bring it into a conversation with an agent they don't own — under a per-request key the app can't read skills/<user-bucket>/… at all.

DIAL Core and DIAL Chat have agreed a wire contract that closes the access half: a user message may carry custom_content.skills[*], and Core auto-shares each referenced skill to the app's per-request key, exactly as it already does for custom_content.attachments[*] (done: epam/ai-dial-core#1956). This design specifies how QuickApps consumes that field.

What the design proposes.

  • Wire contract — read custom_content.skills[*] off user messages; skills are addressed by URL, never by name. Requests without the field behave exactly as today, system prompt included.
  • Resolution — a new _SkillInvocationInitializer resolves each picked URL through the existing DialSkillResolver. A chip that fails to load is reported to both the user and the model, and the request is still served.
  • Registration — a picked skill is listed as user:<name>:<hash>, so every name in <available_skills> stays unique. A user skill can never shadow an agent skill, and there is no precedence rule to reason about. The user's bucket id is never shown to the model.
  • Injection — a synthetic read_skill call and result are inserted after the user message, so the manifest is already in context when the model's turn starts. Re-picking a skill already present in history is skipped, and bundled files stay readable through read_skill for the rest of the conversation.
  • Phasing — phase 1 (this design) covers the user's own skills only; the agent's own skills stay model-invoked. Letting the user pick the agent's skills is deferred to Invoke agent skills from a chat message (phase 2) #550.

The doc also records the use cases, failure modes, a known gap (a share revoked mid-conversation), alternatives considered, and the per-package summary of changes. It is marked Approved after four review rounds.

No code changes — implementation follows in #549.

Checklist

  • Title of the pull request follows Conventional Commits specification
  • Design documented is updated/created and approved by the team (if applicable)
  • [ ] Documentation is updated/created (if applicable) (user-facing docs land with the implementation in Invoke user skills from a chat message (phase 1) #549)
  • [ ] Changes are tested on review environment (docs-only, nothing deployable)
  • [ ] App schema changes are backward compatible, or breaking changes are documented with a migration guide (no config model touched; the design's own Migration section notes the contract is additive)
  • [ ] Integration tests pass (docs-only)

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

https://claude.ai/code/session_0184dFYXFuKAjivbVvNxL2fa

andrii-novikov and others added 14 commits August 31, 2026 13:10
Designs a minimal first cut at consuming DIAL Core's folder-shaped skill
resources (epam/ai-dial-core#1633), scoped to ship without refactoring
anything that already works.

Phase 1: a new `dial-skill` config type over `skills/<bucket>/<path>`,
resolved per request into a manifest plus an inventory of the skill's
bundled text files, and `read_skill(skill_name, file_path?)` for
progressive disclosure — one round-trip per file the model actually asks
for. `dial_skills/` mirrors the existing `dial_prompt_skills/` package
shape; `SkillsRegistry` gains one merge pass and one async method.
Predefined skills and `dial-prompt` are untouched.

Readability is enforced by inventory membership: a `file_path` is served
only if it was advertised, which subsumes traversal, encoded-separator
and dotfile handling without a bespoke validator.

Deferred to phase 2 with reasons: binary/asset files, cross-request
caching, `read_skill` offload exclusion, editor skill browsing. No
QuickApps-side validator for `dial-skill` — Core validates `SKILL.md` on
write — though `/skills/validate` must be narrowed to the prompt config
so the published schema stops advertising a type it rejects.

Records the two blockers verified against the source: the `client.skills`
resource is written but unreleased in ai-dial-client-python, and
ai-dial-core has no skills auto-share (no `getSkills`,
`shareApplicationSkills`, or `ApiKeyData.attachedSkills`), which limits
phase 1 to skills the caller's own key can already reach.

Refs #418

Claude-Session: https://claude.ai/code/session_011mUSHykzn4b9y97fpjkskn
Phase 1 of the design in docs/designs/skills_as_dial_resource.md: an app
config can reference a DIAL skill resource, and the agent can open the
files bundled with it on demand.

A new `dial-skill` config type over `skills/<bucket>/<path>` joins
`SkillConfig`. At request initialization each configured skill costs one
`SKILL.md` read plus one recursive file listing; the listing is appended
to the manifest as a `<skill_files>` block, so the model learns what the
skill contains the moment it calls `read_skill`. It then opens one with
`read_skill(skill_name, file_path)` — one round-trip per file it actually
asks for, memoized for the rest of the request.

`dial_skills/` deliberately mirrors the existing `dial_prompt_skills/`
package shape rather than factoring out a shared lifecycle: the two are
identical in outline, and unifying them would mean rewriting the prompt
path. `SkillsRegistry` gains one merge pass and one async method.
Predefined skills and `dial-prompt` are untouched.

Readability is inventory membership: a `file_path` is served only if it
was advertised, and the inventory is a text-extension allowlist minus
folders, dotfiles and `SKILL.md`. That single check subsumes traversal,
encoded separators and hidden-file leakage without a bespoke validator —
the model can only ask for what it was told exists. Binary files are not
advertised and not readable in this phase.

`/skills/validate` is narrowed to `DialPromptSkillConfig` rather than
gaining a `dial-skill` branch: Core validates `SKILL.md` on write, so a
stored skill is already valid, and keeping the union would have published
an OpenAPI advertising a request the handler answers 400 to. The dead
`isinstance` check goes with it.

Also corrects a stale claim in docs/skills.md: the `skills` config field
is a plain `Field`, not a `PreviewField`, so it never required
ENABLE_PREVIEW_FEATURES.

Known incomplete, both recorded in the design doc:

- `aidial-client` is still pinned to 0.16.x, which has no `skills`
  resource — it lives on an unreleased branch. Access funnels through one
  `TODO(#418)` shim, so this cannot run against a real DIAL Core until
  that client ships.
- DIAL Core does not auto-share config-declared skills to the app's
  per-request key, so a `dial-skill` resolves only where the caller's own
  key already has access.

Verified: `make lint` clean, 1959 unit tests pass (54 new).

Refs #418

Claude-Session: https://claude.ai/code/session_011mUSHykzn4b9y97fpjkskn
Offloading a large read_skill result into a DIAL-file pointer would
strip the manifest's <skill_files> inventory from the tool result the
model sees, breaking progressive disclosure. Add
internal_skills_read_skill to _MANDATORY_EXCLUDED_TOOLS alongside the
existing read-back guards.
DialSkillConfig and DialSkillsModule were unconditionally active
regardless of ENABLE_PREVIEW_FEATURES, exposing a config type whose
resolution depends on an aidial-client attribute that does not exist
in any released client version yet. Mark DialSkillConfig with
@preview_model (strips it from the discriminated union / schema when
preview is off) and DialSkillsModule with @preview_module (drops it
from the DI module list), matching the pattern used by
FolderContextConfig / other preview-gated modules.
Replace the local editable path dependency on aidial-client (which
only resolved on a machine with both repos checked out side by side)
with a git dependency pinned to the feat/skills-read branch of
epam/ai-dial-client-python. This carries the skills resource
(/v2/skills reads) the feature needs but that has not been released
yet, and is resolvable by CI and other machines.

Still TEMPORARY - MUST NOT MERGE: must be swapped for a released
version once aidial-client ships the skills resource.
Status: Draft -> Approved. The D-1 client-release gate remains open
(aidial-client feat/skills-read is still unmerged/unreleased) and is
tracked as an implementation/merge blocker, not a design defect.
- docs: clarify dynamic skill registration is Partial (DIAL-prompt and
  DIAL-skill sources resolve per request)
- fix stage title formatting (remove spaces around /)
- remove stale TODO comment about pinned aidial-client version
SkillsRegistry previously reached into dial_skills' private submodules
for ResolvedDialSkill, _DialSkillsContext and MANIFEST_NAME, and did
the bundled-file read itself. dial_skills likewise reached into
skills' private _frontmatter/_skill_metadata modules.

- skills/__init__.py now exports SkillMetadata, parse_frontmatter and
  SkillFileNotFoundError as its public surface
- dial_skills/__init__.py exports DialSkillReader, DialSkillResolver,
  ResolvedDialSkill and _DialSkillsContext
- New DialSkillReader owns the manifest/inventory check, path
  normalization and cache-then-fetch for one bundled file; the cache
  is its own private state (request-scoped, used by no one else)
- _DialSkillsContext is pure state again (resolved_skills + exceptions),
  mirroring _DialPromptSkillsContext
- SkillsRegistry.read_skill_file collapses to a merge lookup plus one
  delegated call
- DialSkillResolver._fetch_one results now pair with their own URL via
  _fetch_labeled instead of correlating asyncio.gather output by index
- rename build_skill_files_block to _build_skill_files_block (internal)

Addresses PR #524 review comments.
Follows aidial-client's move to chained references: get_file(url, path)
and list_files(url, ...) become skills(url=...).files(path=...).read()
and .files.list(...). The files reference is built once per listing, so
the url is parsed once rather than on every page.

Replaces the MagicMock-based test doubles with hand-written fakes:
skills.files.list() and skills.files(path=...).read() land on different
auto-created mocks, so stubbing one silently failed to intercept the
other. The fakes mirror the real signatures, so drift surfaces as a
TypeError instead of a passing test.

Refs #418
Replace the temporary git-branch override (feat/skills-read) with the
released aidial-client 0.17.0, which ships the skills resource.

Claude-Session: https://claude.ai/code/session_01TqzW8dqXH92sC28kX6WoxQ
A skill reaches a QuickApp agent only if the app author attached it and the
model decides to call read_skill — the user has no deterministic way to say
"use this skill for this message", and no way in at all for skills from their
own catalog.

This design specifies how QuickApps consumes the custom_content.skills wire
contract agreed with DIAL Core (epam/ai-dial-core#1956): resolve each picked
skill through DialSkillResolver, register it under a collision-free
user:<name>:<hash> id, and inject a synthetic read_skill call and result after
the user message so the manifest is in context before the model's turn.

Phase 1 covers the user's own skills; picking the agent's own skills is
deferred to #550.

Claude-Session: https://claude.ai/code/session_0184dFYXFuKAjivbVvNxL2fa
@andrii-novikov andrii-novikov self-assigned this Sep 12, 2026
Base automatically changed from feat/418-skills-as-dial-resource to development September 14, 2026 09:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Invoke user skills from a chat message (phase 1)

1 participant