feat(agent-designer): resolve skill bindings at invocation (replace + force skill-mode)#602
Merged
Merged
Conversation
… force skill-mode) Completes the tool/skill runtime-resolution gap (tools landed in #601). An Agent's `skill` bindings were stored by the Designer but inert at run time. This resolves them, mirroring the tool/model overrides: - Run-time (inference-api): `resolve_agent_invocation` now returns `plan.skills` (`ResolvedSkills`). When an Agent binds skills they *replace* the request's skills for the turn AND the route forces `agent_type="skill"` so the SkillAgent discloses exactly the bound set. Each bound skill is re-checked against the INVOKING user via `AppRoleService.can_access_skill`; a missing skill — or the Skills feature being disabled in this environment — blocks the turn with a message (D5). No skill binding ⇒ `plan.skills is None` ⇒ the request's agent_type/enabled_skills drive the turn as today. Wired by reassigning `effective_agent_type`/`effective_skill_ids` before the main-turn get_agent, so the values flow into the construction snapshot and a bound-skill agent resumes on the same skills_hash (resume-safe, same mechanism the tool slice relies on). - Design-time (app-api): `skill` dropped from inert (no inert kinds remain). A bound skill is flag-gated (`skills_enabled()`) and must be in the author's palette (`resolve_accessible_skill_ids`, the same source the picker fetches — cf. the tool check); the palette is resolved once per write and only when skills are enabled. Tests: +6 resolver (override, dedupe, flag-off block, block-on-missing, per-invoker, none →passthrough) + 6 validation (accessible/inaccessible/empty-ref/flag-off/fetch-once/lazy). Full backend suite green (4631 passed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Completes the tool/skill runtime-resolution gap (tools landed in #601). An Agent's
skillbindings were stored by the Designer but inert at run time — chatting an Agent ignored its bound skills. This resolves them, mirroring the shippedmodel/tooloverrides. With this, every binding kind resolves at invocation and no inert kinds remain.What changed
Run-time (inference-api) —
resolve_agent_invocationnow returnsplan.skills(ResolvedSkills). When an Agent binds skills they replace the request's skills for the turn, and the route forcesagent_type="skill"so theSkillAgentdiscloses exactly the bound set. Each bound skill is re-checked against the invoking user viaAppRoleService.can_access_skill; a missing skill — or the Skills feature being disabled in the environment — blocks the turn with a message (D5). No skill binding ⇒plan.skills is None⇒ the request'sagent_type/enabled_skillsdrive the turn exactly as today.Resume-safe by construction — the effective
agent_type/enabled_skillsare reassigned before the main-turnget_agent, so they flow into the construction snapshot (service.py:256-258) and a paused bound-skill agent resumes on the sameskills_hash. (Same mechanism the tool slice relies on; scope-verified there's no closure-local trap — the streaming generator starts afterget_agent.)Design-time (app-api) —
skilldropped from inert (no inert kinds remain). A bound skill is flag-gated (skills_enabled()) and must be in the author's palette (resolve_accessible_skill_ids, the same source the Designer picker fetches — cf. the tool check). The palette is resolved once per write, only when skills are enabled.Governance note
Binding skills to an Agent forces skill-mode for that turn, bypassing the admin chat-mode policy. This is intentional — an Agent is a governed authored capability (like model-override winning over the request) — but worth knowing when debugging why an agent runs in skill mode.
Tests
🤖 Generated with Claude Code