feat(skills): add skill package schema and agent-side skill loading support #94
No reviewers
Labels
No labels
auto/blocked-by-deps
auto/ci-timeout
auto/claimed-implementer
auto/claimed-merge
auto/claimed-reviewer
auto/driver-down
auto/invariant-violation
auto/last-attempt-tier-0
auto/last-attempt-tier-1
auto/last-attempt-tier-2
auto/last-attempt-tier-min
Automation Tracking
auto/needs-conflict-resolution
auto/needs-implementer
auto/postmortem
auto/ready-to-merge
auto/restart-throttled
auto/revert
auto/sentinel
auto/stale-inactivity
auto/unstable
Blocked
Bounty
$100
Bounty
$1000
Bounty
$10000
Bounty
$20
Bounty
$2000
Bounty
$250
Bounty
$50
Bounty
$500
Bounty
$5000
Bounty
$750
MoSCoW
Could have
MoSCoW
Must have
MoSCoW
Should have
Needs Feedback
Points
1
Points
13
Points
2
Points
21
Points
3
Points
34
Points
5
Points
55
Points
8
Points
88
Priority
Backlog
Priority
CI Blocker
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Signed-off: Owner
Signed-off: Scrum Master
Signed-off: Tech Lead
Spike
State
Completed
State
Duplicate
State
In Progress
State
In Review
State
Paused
State
Unverified
State
Verified
State
Wont Do
Type
Automation
Type
Bug
Type
Discussion
Type
Documentation
Type
Epic
Type
Feature
Type
Legendary
Type
Refactor
Type
Support
Type
Task
Type
Testing
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Blocks
#88 feat(skills): add Skill package schema and agent-side skill loading support
cleveragents/cleveractors-core
Reference
cleveragents/cleveractors-core!94
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feature/skill-package-support"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implements #88: a normative Skill package schema (Package Registry Standard §16) aligned with the open agentskills.io Agent Skills format, plus agent-side skill loading for
type: llmagents via a new optionalskillsconfig field, per the approved ADR-2034.SkillLoader,SkillValidator,SkillReferenceResolver(new): resolveskillsreferences through the existingcleveractors.registryclient (registry:/ID:/local:schemes), mirroring the established template package-reference resolution pattern rather than a parallel path.skilltool call returns instructions), execution (resource reads, reusingfile_read'soffset/max_charspagination from ADR-2033).AgentFactoryinvokesSkillLoaderbeforeLLMAgentconstruction;LLMAgent/ToolAgentgain no registry awareness — all resolution stays inSkillLoader.docs/actor-registry-standard.mdgains a new §16 (appended after the original body, version bumped 1.0.0 → 1.1.0) documenting the schema. Theskillsagent-config field itself is documented only in ADR-2034 — not mirrored intodocs/index.md— matching how ADR-2031/ADR-2032 handled their own new LLM agent config fields.docs/registry/updated with pointers to the new schema.Test plan
features/agent_skills.feature): schema validation (including all agentskills.io name/description constraints), all three reference schemes, loader orchestration, factory/agent integration, ToolAgent skill tool (activation, resource reads incl. base64, pagination, error paths).robot/agent_skills.robot, 6 cases): realLocalPackageStore/PackageContentResolver/SkillLoaderresolution against a real on-disk skill file, end to end through a realToolAgentdispatch — only the LLM API call is mocked.nox -s coverage_report: 96.9% (threshold 96.5%).noxsuite green (lint, format, typecheck, security_scan, dead_code, complexity, unit_tests, integration_tests, e2e_tests, coverage_report, docs, build).nox -s benchmark_regression: BENCHMARKS NOT SIGNIFICANTLY CHANGED.Closes #88
Note: bundled
scripts/resources are readable, not directly runnableA question came up after this PR landed: if a skill's
instructionssay something like "runscripts/extract.py <file.pdf>", how does thetype: llmagent actually execute that script? Documenting the answer here since it's not obvious from the code alone.What the
skilltool actually gives the model is text, not a runnable file. Callingskill(skill_name="pdf-processing", resource="scripts/extract.py")returns the script's source as a string (cleveractors.agents.tool.ToolAgent._skill_tool, commitbf1f138) — formatted as[SKILL_RESOURCE_READ]...[FILE_CONTENT_START]...[FILE_CONTENT_END]. Nothing is ever written to a real filesystem path; resources live only in the in-memory_loaded_skillsmapping threaded throughcontext["_skills"]. This is intentional, per ADR-2034 D-5: resources are exposed for execution-stage reads "without touching the host filesystem," not for by-path execution.How a bundled Python script can actually run today — no code changes needed, but it requires the model to inline the source rather than reference it by path:
skill(skill_name="pdf-processing", resource="scripts/extract.py")→ returns the source textpython_exec(code="<that source text>")→ executes it viacleveractors.agents.tool.ToolAgent._execute_python_codeThis only works when the agent config sets
exec_python: true. Worth flagging: that sandbox isn't airtight — its restricted-builtins dict still includes an unrestricted__import__, so executed code canimport os/import subprocessand go beyond the tool's documented builtin list. Pre-existing behavior, not something this PR changed, but relevant to anyone relying on it as a security boundary for skill-provided code.What does not work today: a bundled
.shscript, or any Python script that assumes it's a real file on disk (relative imports, sibling files, real argv, real cwd). There's no materialized file forshellor a real interpreter invocation to point at. A model could try to smuggle the whole script intobash -c '<script>'via theshelltool (needsallow_shell: true), but that's fragile and wasn't a designed path — it just happens to be technically possible givenshell's existing arbitrary-commandargument.If genuine file-backed execution is wanted (skill resources materialized to a real temp directory so
shell/python_exec/file_readcan operate on real paths), that's a new capability this issue didn't build. It would need its own small design decision — materialization lifecycle, cleanup, interaction withsafe_mode/unsafe_mode— similar in shape to thepack_skill_directoryfollow-up already flagged in ADR-2034's "Follow-up Required" section. Happy to scope that as a separate issue if it's wanted.PR Review: !94 (Ticket #88)
Verdict: Request Changes
The implementation is solid overall and closely follows ADR-2034, reusing the existing registry client and preserving progressive disclosure. However, two functional issues require fixes before approval: skill tool context is lost in the stuck-model synthesis path, and duplicate skill names are silently overwritten. Additionally, there are a few minor documentation/coverage observations.
Critical Issues
None
Major Issues
src/cleveractors/agents/llm.pylines 1429-1431 — The stuck-model synthesis follow-up constructstool_ctx_sas{"_unsafe_mode": True} if parent_unsafe else Noneinstead of callingself._build_tool_context(parent_unsafe). Consequently,_loaded_skillsis not forwarded to the ephemeralToolAgent. If the model emits askilltool call during this follow-up round,ToolAgent._skill_toolfails with "Skill '...' is not loaded for this agent." The main loop and the budget-exhaustion synthesis round both correctly use_build_tool_context; this path should too.src/cleveractors/agents/skills.pylines 98-109 —SkillLoader.loadstores resolved skills inloaded[skill.name] = skill, silently overwriting any earlier skill with the same name. Twoskillsreferences that resolve to identically-named skills (or two refs to the same skill) will yield a catalogue with one entry and the other's instructions will be lost. Validate uniqueness and raiseAgentCreationErrorwith a clear message.Minor Issues
Coverage threshold discrepancy — The PR test plan reports 96.9% coverage and cites a 96.5% threshold, but
CONTRIBUTING.md,.gitea/workflows/ci.yml, and thecoverage_reportsession docstring all state the gate is 97%. Thenoxfile.pyconstant is 96.5, which creates an inconsistency. Before merge, confirm the project's enforced gate and ensure the reported coverage meets it. If the gate is 97%, additional tests are needed.docs/actor-registry-standard.md§16.1 — The last paragraph references "Actor Configuration Standard §22" for the agent-sideskillsfield, butdocs/index.mdhas no §22. This appears to be a stale/future reference; it should point to the actual location (currently ADR-2034, or §4.4 once the deferred spec update lands).src/cleveractors/agents/skill_schema.pySkill.to_context_dict— The context representation exposed to theskilltool andLLMAgent.get_metadata()dropsmetadata,allowed_tools,license, andcompatibility. Sinceallowed_toolsis documented as a pre-approved tool list, making it invisible to the model limits its usefulness. Consider surfacing at leastallowed_toolsandmetadatain activation output or catalogue metadata.Nits
skillslist; emptydescription/instructionsstrings; and the stuck-model synthesis path invoking theskilltool.robot/SkillLoadingTestLib.py—tempfile.mkdtemp()directories are not cleaned up after tests.Summary
The PR delivers the normative skill schema, reference resolution, loader orchestration, and tool-agent integration described in ADR-2034. The architecture is clean and the test suite is extensive. The two major issues above are localized but real regressions in functionality; once fixed and the coverage/doc observations addressed, this should be good to merge.
bf1f13846f05e5e52310c520a6d211f9fea9e208PR Review: !94 (Ticket #88)
Verdict: Request Changes
The implementation still has the two functional regressions identified in the previous review: the stuck-model synthesis path drops the skill catalogue, and duplicate skill names are silently overwritten. In addition, the agent-side
skillsfield is not documented indocs/index.mdas required by the ticket acceptance criteria, and a new error-handling bug inSkillReferenceResolvercan mask exceptions withUnboundLocalError. Once these are addressed, the PR should be in good shape.Critical Issues
None
Major Issues
src/cleveractors/agents/llm.pylines 1429-1431 — The stuck-model synthesis follow-up constructstool_ctx_sas{"_unsafe_mode": True} if parent_unsafe else Noneinstead of callingself._build_tool_context(parent_unsafe). Consequently,_loaded_skillsis not forwarded to the ephemeralToolAgent. If the model emits askilltool call during this follow-up round,ToolAgent._skill_toolfails with "Skill '...' is not loaded for this agent." The main loop and budget-exhaustion synthesis round both correctly use_build_tool_context; this path should too.src/cleveractors/agents/skills.pylines 98-109 —SkillLoader.loadstores resolved skills inloaded[skill.name] = skill, silently overwriting any earlier skill with the same name. Twoskillsreferences that resolve to identically-named skills (or two refs to the same skill) will yield a catalogue with one entry and the other's instructions will be lost. Validate uniqueness and raiseAgentCreationErrorwith a clear message.docs/index.md§4.4 — The new optionalskillsconfiguration field fortype: llmagents is not documented in the Actor Configuration Standard, even though ticket #88 acceptance criteria explicitly require it and ADR-2034 D-5 states the field is added todocs/index.md§4.4. Add the field documentation (optional list of package references usingregistry:,ID:, andlocal:schemes).src/cleveractors/agents/skill_resolution.pylines 84-124 —resolvedis only assigned inside thetryblock. Ifresolver.resolve()raises an exception other thanFileNotFoundError/ValueError(e.g.,RuntimeErrorfor a registry reference called inside a running event loop), thefinallyruns and thenif resolved is None:raisesUnboundLocalError, masking the original failure. Initializeresolved = Nonebefore thetryor add a broaderexceptpath that re-raises asRegistryError.Minor Issues
Coverage threshold mismatch / below declared gate —
CONTRIBUTING.md,.gitea/workflows/ci.yml, and thecoverage_reportsession docstring state the gate is 97%, butnoxfile.pysetsCOVERAGE_THRESHOLD = 96.5. The PR test plan reports 96.9% and cites 96.5%. Alignnoxfile.pywith the documented 97% gate and add tests to reach it.docs/actor-registry-standard.md§16.1 line 576 — The last paragraph references "Actor Configuration Standard §22" for the agent-sideskillsfield, butdocs/index.mdhas no §22. This appears to be a stale/future reference; it should point to the actual location (currently §4.4 or ADR-2034).src/cleveractors/agents/skill_schema.pySkill.to_context_dictlines 72-88 — The context representation exposed to theskilltool andLLMAgent.get_metadata()dropsmetadata,allowed_tools,license, andcompatibility. Sinceallowed_toolsis documented as a pre-approved tool list, making it invisible to the model limits its usefulness. Consider surfacing at leastallowed_toolsandmetadatain activation output or catalogue metadata.src/cleveractors/agents/tool.py_decode_skill_resourceline 1099 andsrc/cleveractors/agents/skill_schema.pySkillResource.decoded_textline 53 — Base64 resources are decoded with.decode("utf-8"), which will crash for genuine binary assets (images, PDFs, etc.) even though the spec requires supporting non-UTF-8 files viaencoding: base64. Handle binary resources safely (e.g., return base64 text or skip UTF-8 decoding).robot/SkillLoadingTestLib.pyline 56 —tempfile.mkdtemp()directories are not cleaned up after tests. Add teardown cleanup to avoid leaking temp directories in CI.Nits
Test gaps — Consider adding Behave/Robot scenarios for: duplicate skill names in the
skillslist; emptydescription/instructionsstrings; and the stuck-model synthesis path invoking theskilltool.features/steps/agent_skills_steps.py—tempfile.mkdtemp()directories created for local-store tests are not cleaned up.src/cleveractors/agents/skill_resolution.pylines 94-112 — When called inside a running event loop, resolver cleanup is scheduled withasyncio.ensure_future()and not awaited, so it may not complete before loop shutdown. Prefer the async resolution path where possible.Summary
The PR delivers a clean, registry-agnostic skill-loading architecture that aligns well with ADR-2034 and reuses the existing package-resolution machinery. The test suite is extensive and the Robot integration test gives good end-to-end confidence. However, the two previously reported functional issues remain unfixed, the new
SkillReferenceResolvererror-handling path has a latentUnboundLocalError, and theskillsfield is missing from the Actor Configuration Standard. Addressing the major items above will make this ready to merge.In the review report issuecomment 321235, Minor Issues, Issue 5, the coverage threshold does not need to be changed the coverage threshold is set to 96.5% because nox coverage does no rounding. The 97% coverage specified as the target value is after rounding, so 96.5% real coverage will meet that target. There is no need to change anything,
f9fea9e2084660e373f2Addressed the outstanding review items from the 2026-08-02 review (commit
4660e37):Fixed:
llm.pystuck-model synthesis follow-up now callsself._build_tool_context(parent_unsafe)instead of hand-building the context dict, so_loaded_skillsis forwarded correctly. Added a regression scenario (Stuck-model synthesis forwards the loaded skill catalogue to the skill tool,features/llm_agent_tool_loop.feature) that fails without the fix.SkillLoader.loadnow raisesAgentCreationErroron duplicate skill names instead of silently overwriting.skill_resolution.py— see "Not changed" below; this one turned out not to be a real bug.docs/actor-registry-standard.md§16.1's stale "Actor Configuration Standard §22" reference now points to ADR-2034 (see "Not changed" #3 for why not §4.4).Skill.to_context_dict()now surfaceslicense,compatibility,metadata, andallowed_tools.UnicodeDecodeError, in bothSkillResource.decoded_text()andToolAgent._decode_skill_resource.robot/SkillLoadingTestLib.pyandfeatures/steps/agent_skills_steps.pytemp dirs are now cleaned up (Test Teardown/context.add_cleanup).Added Behave coverage for all of the above; full
noxsuite (lint, format, typecheck, unit_tests, coverage, security_scan, dead_code, integration_tests) is green.Not changed (reviewed against the ADR/issue and believe these aren't correct):
docs/index.md§4.4 documentation — checked the actual file: none of ADR-2031's or ADR-2032's new LLM-agent config fields (token_budget_percent,max_retries,max_retry_time, etc.) were ever mirrored into §4.4 either — they live in their ADRs only, graduation into the normative spec is a deliberate future step. Theskillsfield follows the same established pattern; the PR description already calls this out explicitly.skill_resolution.pyUnboundLocalError— traced through the actual control flow: ifresolver.resolve()raises anything not caught byexcept (FileNotFoundError, ValueError), that exception propagates immediately oncefinallycompletes — theif resolved is None:line is never reached, soresolvedis never read unbound. Verified this with a standalone repro of the same try/except/finally shape. NoUnboundLocalErroris possible here.noxfile.pyconstant predates this branch).Not fixed (intentionally, out of scope for this PR): the
asyncio.ensure_future()fire-and-forget cleanup inskill_resolution.py(Nit #12) — this is an existing, documented trade-off for the sync/async bridging in that function; addressing it properly would need a broader async-path refactor beyond this issue's scope.PR Review: !94 (Ticket #88)
Verdict: Request Changes
The two functional regressions from the previous review (stuck-model synthesis dropping the skill catalogue and duplicate skill names being silently overwritten) are fixed in the latest commit. The remaining concern is a new functional gap:
registry:skill references cannot actually be resolved in the production async runtime path, becauseSkillLoader/SkillReferenceResolverare synchronous and callPackageContentResolver.resolve()from inside a running event loop. There are also two smaller robustness/efficiency observations.Critical Issues
None
Major Issues
src/cleveractors/agents/skill_resolution.pylines 52–124 /src/cleveractors/registry/reference_resolver.pylines 557–588 /src/cleveractors/runtime_dispatch.pylines 162–168, 390–391, 850–851, 1252–1253SkillReferenceResolver.resolve()is synchronous. When it reaches aregistry:reference it delegates toPackageContentResolver.resolve(), whose_RegistryReferenceStrategychecks for a running event loop and raisesRuntimeErrorbecause it cannot nestasyncio.run().SkillLoader.load()is invoked synchronously fromAgentFactory._create_agent_instance(), which is called from the async_execute_llm,_execute_graph,_execute_llm_stream, and_execute_graph_streamdispatch functions. Consequently, anytype: llmagent configured with aregistry:skill reference fails at agent-creation time with an unhandledRuntimeErrorinstead of loading the skill. The existing Behave scenario forregistry:mocks the resolver, and the Robot suite only exerciseslocal:, so this gap is not covered.Recommendation: Add an async resolution path (e.g.
SkillReferenceResolver.aresolve()andSkillLoader.aload()) and use it from the async runtime dispatch, or resolveskillsearlier (e.g. duringExecutorconstruction) before the event loop is running.Minor Issues
src/cleveractors/agents/skill_resolution.pylines 79–118 /src/cleveractors/runtime_dispatch.pylines 873–890SkillReferenceResolver.resolve()creates a fresh ad-hocPackageContentResolverfor every skill reference when no resolver is injected (the runtime path constructed by_build_skill_loader()). When called inside the running event loop, the cleanupclose_all()is scheduled withasyncio.ensure_future()and not awaited. This duplicates work and leaks client pools per skill reference.Recommendation: Have
_build_skill_loader()create one sharedPackageContentResolverand pass it toSkillReferenceResolver; provide an async cleanup path that is awaited.src/cleveractors/agents/skill_schema.pylines 266–319 /src/cleveractors/agents/tool.pylines 331–344_validate_resources()acceptsencoding: base64without validating thatcontentis valid base64. A malformed skill package therefore passes load-time validation but crashes at execution time whenbase64.b64decode()raisesbinascii.Error. There is no test coverage for invalid base64 content.Recommendation: Validate base64 content inside
_validate_resources()and raiseValidationErrorwith a clear message.Nits
None
Summary
The PR now correctly handles duplicate skill names and forwards the loaded skill catalogue through the stuck-model synthesis path, resolving the blockers from the prior review. The architecture (factory-level
SkillLoader, registry-agnosticLLMAgent, progressive disclosure via the synthesizedskilltool) remains clean and well tested forlocal:and mocked paths. However, the production async runtime path cannot resolveregistry:skill references, which is a real functional gap for one of the three advertised reference schemes. Fixing that (and the related resolver-per-reference overhead and base64 validation gap) will make this ready to merge.Note: per your follow-up comments, I have not re-raised the
docs/index.md§4.4 update or the coverage-threshold discussion, as you have explicitly treated those as deferred/out-of-scope.4660e373f23e33656c3fAddressed the outstanding review items from the 2026-08-03 review (commit
3e33656):Fixed:
registry:skill references now resolve from the async runtime dispatch path. AddedSkillReferenceResolver.aresolve()/SkillLoader.aload()(mirroringPackageContentResolver.aresolve(), per ADR-2034 D-5) andAgentFactory.acreate_agent()/_acreate_agent_instance();_execute_llm/_execute_graph/_execute_llm_stream/_execute_graph_streamnow callawait factory.acreate_agent(...)instead of the synccreate_agent(), so aregistry:reference no longer hitsRuntimeError: asyncio.run() cannot be called from a running event loop._build_skill_loader()now constructs one sharedPackageContentResolverper dispatch call (instead of one ad hoc per skill reference) and it is closed via an awaitedclose_all()alongside each dispatch function's existing agent-cleanupfinallyblock.SkillValidator._validate_resources()now validates thatencoding: base64resource content actually decodes as well-formed base64 (binascii.Error->ValidationError) instead of only surfacing the problem later at execution time.Added 14 new Behave scenarios covering the async resolution path (including a regression scenario proving the old sync path raises
RuntimeErrorfrom a running loop), the shared-resolver reuse, an end-to-end run through the realExecutor/runtime-dispatch layer with a real on-disk skill package, and malformed base64 rejection. Fullnoxsuite green (lint, format, typecheck, security_scan, dead_code, unit_tests — 2876 scenarios, coverage on the touched files 91-100%).As before, the
docs/index.md§4.4 documentation and coverage-threshold points remain out of scope per the prior discussion (issuecomment 321411, issuecomment 321423).PR Review: !94 (Ticket #88)
Verdict: Request Changes
The implementation is functionally complete and addresses the blockers from the prior review rounds: the async resolution path now makes
registry:skill references work from the production runtime dispatch, duplicate skill names are rejected, the loaded skill catalogue is forwarded through the stuck-model synthesis follow-up, and base64/binary resources are handled safely. The test suite is extensive and the Robot integration test gives good end-to-end confidence.I did not re-raise the
docs/index.md§4.4 update or the coverage-threshold discussion, since you have explicitly treated those as deferred/out-of-scope in your follow-up comments.There is one remaining error-path cleanup bug in the single-LLM dispatch path that should be fixed before merge.
Critical Issues
None
Major Issues
src/cleveractors/runtime_dispatch.pylines 199–238 —_execute_llmcloses the sharedskill_resolverinside thefinallyblock of the innertrythat wrapsagent.process_message(). Iffactory.acreate_agent()fails — for example because askills:reference is invalid or cannot be resolved — that innertryis never entered, soawait skill_resolver.close_all()is never executed. AnyRegistryClient/httpx.AsyncClientpools owned by the resolver are then leaked.The other three dispatch paths (
_execute_graph,_execute_graph_stream,_execute_llm_stream) all close the resolver in afinallythat runs even when agent creation fails;_execute_llmshould be consistent with them.Recommendation: Restructure
_execute_llmso thatskill_resolver.close_all()is awaited in afinallycovering the agent-creation call (or otherwise guarantee cleanup runs before the function returns on any exception path).Minor Issues
src/cleveractors/runtime_dispatch.py_build_skill_loaderlines 80–81 — The sharedPackageContentResolveris only created whenexecutor.local_storeis set. When no local store is configured,AgentFactoryfalls back to the defaultSkillLoader(), which constructs and tears down an ad-hoc resolver for every skill reference. This still works forregistry:references, but it defeats the sharing/cleanup intent for actors that list multiple registry-only skills.Recommendation: Always build and return the shared
PackageContentResolver(with or without alocal_store) so that every reference in askills:list shares one resolver and one cleanup path.Nits
None
Summary
This PR delivers a clean, registry-agnostic skill-loading architecture that aligns well with ADR-2034 and reuses the existing package-resolution machinery. The schema validation, progressive-disclosure model, async resolution path, and tool-agent integration are all solid. Once the single-LLM cleanup gap is fixed, this should be ready to merge.
3e33656c3f67ffee347567ffee3475ac410de3beThe implementation was modified to also deal with the download of private skill packages that need authentication, as well as also supporting skills within the interactive mode (streaming mode). This all part of the same issue, the supporting skills, requires all this together.
ac410de3bebcd1ad0dacAddressed the outstanding review items from the 2026-08-03 14:53 review (commit
bcd1ad0):Fixed:
_execute_llm(src/cleveractors/runtime_dispatch.py) now closesskill_resolverin afinallythat wraps thefactory.acreate_agent()call itself, instead of only thefinallythat runs afterprocess_message()(which is only reached once agent creation has already succeeded). A failed agent creation — e.g. an invalidskills:reference — now still releases the resolver'sRegistryClient/httpx.AsyncClientpools, matching_execute_graph/_execute_llm_stream/_execute_graph_stream, which already closed the resolver on that path._build_skill_loader(same file) now always constructs a realPackageContentResolver— withlocal_store=None/api_key=Nonewhen neither is configured — instead of returning(None, None)and lettingAgentFactoryfall back to its default, storelessSkillLoader(). Every reference in askills:list now shares one resolver and one cleanup path regardless of whether a local store or registry API key is configured, not only when one of them happens to be set.Verified both against ADR-2034 and
docs/index.mdfirst: neither fix touches or conflicts with any normative behaviour, they're both implementation-only bugs inruntime_dispatch.py.Added 2 new Behave scenarios covering both regressions directly (an actor whose skill fails to load still closes the resolver;
_build_skill_loaderreturns a real loader/resolver pair with no local store or registry API key configured) — 97 scenarios now infeatures/agent_skills.feature, all passing. Fullnoxsuite green:lint,typecheck, and targetedunit_testsruns coveringagent_skills.featureplus the other runtime-dispatch-adjacent feature files (runtime_dispatch_coverage,runtime_coverage,runtime_extended_coverage,actor_result_token_counting,credential_injection,execute_stream,tool_agent_class_injection— 466 scenarios, 0 failed).Not changed: the public
build_skill_loader()inagents/skills.pystill returns(None, None)when neitherlocal_storenorapi_keyis given — that contract has its own dedicated Behave scenarios and is unrelated to the leak; I only changed the privateruntime_dispatch._build_skill_loader()wrapper used by the four dispatch functions, which is exactly what your comment pointed at.As before, the
docs/index.md§4.4 update and the coverage-threshold discussion remain untouched, since you've already indicated you're treating those as deferred/out-of-scope and didn't re-raise them in this round.PR Review: !94 (Ticket #88)
Verdict: Request Changes
The implementation is functionally complete and the blockers from the previous review round (skill_resolver cleanup on failed agent creation in
_execute_llm, and_build_skill_loaderalways constructing a shared resolver) are both fixed in commitbcd1ad0. The async resolution path, duplicate-skill-name rejection, skill-tool catalogue forwarding through stuck-model synthesis, base64/binary resource handling, and registry credential wiring are all solid.However, one new functional issue requires a fix before approval:
SkillValidatordoes not correctly handle the registry response'spackage_idfield, so a skill resolved from aregistry:reference gets a different content-addressed identity than the same skill resolved fromlocal:orID:. There are also two smaller robustness/coverage observations.I have not re-raised the
docs/index.md§4.4 documentation update or thenoxfile.pycoverage-threshold discussion, as you have explicitly treated those as deferred/out-of-scope.Critical Issues
None
Major Issues
src/cleveractors/agents/skill_schema.pylines 21–32 and 331–336 —_RESOLVER_METADATA_KEYSdoes not include"package_id", and_resolve_package_id()only checks"_package_id". When a skill is resolved from aregistry:reference,PackageContentResolver._resolve_registry_async()merges the registry response (which includespackage_idandtypeperRegistryClient.resolve_package) into the content dict. Becausepackage_idis neither used as the authoritative ID nor stripped before canonicalization,SkillValidatorcomputes the skill'spkg_skl_SHA-1 over content that includes the registry-specificpackage_idfield. The same skill resolved fromlocal:(where_local_package_resultstores the ID as"_package_id", which is stripped) will produce a different Package ID, violating the content-addressing invariant in Package Registry Standard §5/§6 and ADR-2034 D-3.Recommendation: Treat the registry's
package_idas authoritative: in_resolve_package_id(), check bothcontent.get("_package_id")andcontent.get("package_id"). Also add"package_id"to_RESOLVER_METADATA_KEYSso that any remainingpackage_idfield does not pollute the canonical form if the code falls through tocompute_package_id(). Add a Behave/Robot assertion that the same skill content resolved viaregistry:andlocal:yields the sameSkill.package_id.Minor Issues
src/cleveractors/agents/skill_resolution.pylines 119–129 —_require_resolved()returns the_IdReferenceStrategyplaceholder dict ({"id": ..., "type": "id", ...}) as if it were valid content. When anID:skill reference cannot be resolved against the local store's identity map,SkillValidator.validate()then fails with "missing the required discriminator field 'skill: true'" instead of a clear package-not-found error.Recommendation: Detect the placeholder in
SkillReferenceResolver._require_resolved()(e.g.resolved.get("type") == "id") and raisePackageNotFoundErrorwith a message naming the unresolvedID:reference.src/cleveractors/agents/skills.pylines 278–288 —_build_tools()only deduplicates a pre-declaredskilltool when it appears as a string"skill"or as a dict with a top-level"name"key. If a user supplies the already-normalized OpenAI format{"type": "function", "function": {"name": "skill"}}, the check misses it and a secondskilltool is appended.Recommendation: Extend the deduplication check to recognize OpenAI-formatted tool dicts, or normalize each entry with
normalize_tool_entry()before checking for the skill tool.Nits
None
Summary
This PR delivers a clean, registry-agnostic skill-loading architecture that aligns well with ADR-2034. The schema validation, progressive-disclosure model, async resolution path, tool-agent integration, and previous review blockers are all addressed. Once the
package_idcanonicalization bug is fixed (and the two minor robustness items are considered), this should be ready to merge.bcd1ad0dacbff76e8e9eAddressed the outstanding review items from the 2026-08-03 16:27 review (commit
bcd1ad0), now in commitbff76e8:Fixed:
SkillValidator._resolve_package_id()(skill_schema.py) now also checks a barepackage_idkey (added to_RESOLVER_METADATA_KEYStoo), so a skill resolved viaregistry:computes/reuses the same content-addressedpackage_idas the identical skill resolved vialocal:/ID:, per §16.2/ADR-2034 D-3. Added a Behave scenario asserting a local-shaped and a registry-shaped resolution of identical schema fields agree onpackage_id.SkillReferenceResolver._require_resolved()(skill_resolution.py) now also rejects_IdReferenceStrategy's not-found placeholder ({"type": "id", ...}), which previously slipped past theis Nonecheck and surfaced as a confusing "missing discriminator field 'skill: true'" validation error instead of a clearPackageNotFoundError, per D-7. Added sync + async Behave scenarios for an unresolvableID:reference.SkillLoader._build_tools()(skills.py) now also recognizes an already-declaredskilltool in normalized OpenAI function-calling form ({"type": "function", "function": {"name": "skill"}}), not just the bare{"name": "skill"}shorthand, so it's never duplicated. Added a Behave scenario for this shape.Verified all three against
docs/actor-registry-standard.md§16.2 and ADR-2034 D-3/D-7 first — none of these fixes touch or conflict with any normative behaviour.features/agent_skills.featurenow has 101 scenarios (4 new), all passing.nox -s lintandnox -s typecheckclean; targetednox -s unit_tests -- features/agent_skills.featureandnox -s coverage_report -- features/agent_skills.featureshow the three touched files (skill_schema.py,skills.pyat 100%,skill_resolution.pyat 93% with only pre-existing, unrelated defensive-logging lines uncovered).As before, the
docs/index.md§4.4 update and the coverage-threshold discussion remain untouched, since you've indicated you're treating those as deferred/out-of-scope.PR Review: !94 (Ticket #88)
Verdict: Approve
All three items raised in the previous review round (2026-08-03 16:27, review id 9773) are fixed in commit
bff76e8, and the fixes are backed by targeted Behave/Robot coverage. No new critical or major issues were introduced. I did not re-raise the deferred/out-of-scope items (docs/index.md§4.4 update and the coverage-threshold discussion), per the prior agreement in the PR thread.Critical Issues
None
Major Issues
None
Minor Issues
None
Nits
None
Summary
This PR delivers the normative Skill package schema and agent-side skill-loading support described in ADR-2034. The previous review round asked for three specific fixes:
package_idcanonicalization forregistry:responses —SkillValidator._resolve_package_id()insrc/cleveractors/agents/skill_schema.pynow treats both_package_id(local/ID convention) and barepackage_id(registry response) as authoritative, and"package_id"was added to_RESOLVER_METADATA_KEYSso it does not pollute the canonical form. A Behave scenario asserts that local-shaped and registry-shaped resolutions of identical content yield the samepackage_id.Clear error for unresolved
ID:references —SkillReferenceResolver._require_resolved()insrc/cleveractors/agents/skill_resolution.pynow rejects_IdReferenceStrategy's{"type": "id", ...}placeholder and raisesPackageNotFoundErrorinstead of letting it reach validation as a confusing "missing discriminator field" error. Both sync and async paths have coverage.OpenAI-format
skilltool deduplication —SkillLoader._build_tools()insrc/cleveractors/agents/skills.pynow recognizes an already-declaredskilltool in normalized OpenAI function-calling form ({"type": "function", "function": {"name": "skill"}}) in addition to the shorthand forms, preventing duplicate tool entries.Earlier blockers also remain resolved: the stuck-model synthesis path forwards
_loaded_skillsvia_build_tool_context(), duplicate skill names raiseAgentCreationError, the async resolution path (aresolve/aload/acreate_agent) makesregistry:references work from the runtime dispatch, the single-LLM dispatch closes the shared skill resolver even when agent creation fails,_build_skill_loader()always constructs a shared resolver, and base64/binary resources are handled safely.The test suite is extensive: 101 Behave scenarios in
features/agent_skills.feature, a stuck-model synthesis regression scenario, plus Robot integration tests for local-skill resolution and registry authentication. The implementation reuses the existing registry resolution machinery, keepsLLMAgentregistry-agnostic, and aligns with the agentskills.io format. This is ready to merge.bff76e8e9e57329ee52eExtends the Package Registry Standard with a normative Skill package schema (§16) aligned with the open agentskills.io Agent Skills format, and adds an optional `skills` field to `type: llm` agent configs so an Actor can attach one or more resolved Skill packages to an agent. SkillLoader (new) resolves each `skills` reference through the existing cleveractors.registry client (registry:/ID:/local: schemes), mirroring the established template package-reference resolution pattern in cleveractors.templates.base._resolve_package_ref rather than introducing a parallel resolution path. Resolution is available in both sync (SkillReferenceResolver.resolve/SkillLoader.load) and async (.aresolve/.aload) forms; AgentFactory exposes a matching acreate_agent()/_acreate_agent_instance() pair so the async runtime dispatch layer (_execute_llm/_execute_graph/etc.) resolves `skills` through PackageContentResolver.aresolve() instead of nesting asyncio.run() inside its own already-running event loop — the `registry:` scheme now resolves correctly regardless of whether agent creation happens from plain sync code or from async runtime dispatch. Every skill reference in a `skills` list shares one PackageContentResolver built unconditionally per agent-creation call (regardless of whether a local store or registry API key is configured) instead of building one ad hoc per reference, and its connections are closed once agent creation for that request completes — including when agent creation itself fails, e.g. because of an invalid `skills` reference — alongside the existing per-agent cleanup(). SkillValidator (new) checks resolved content against the D-2 schema (name/description constraints lifted from agentskills.io, resource path/encoding rules — including that base64-encoded resource content actually decodes as well-formed base64 — and uniqueness of skill names within a single `skills` list) and computes a content-addressed `package_id` via the existing Canonicalizer that is identical regardless of which reference scheme resolved the content: a registry: response's bare `package_id` field and a local:/ID: resolution's `_package_id` field are both recognized as authoritative and excluded from the canonicalized form. A reference that cannot be resolved — including an `ID:` reference absent from the local store's identity map — now surfaces as a clear PackageNotFoundError rather than a confusing schema-validation error. Resolved skills are disclosed to the model in three stages, per D-4: discovery (name+description folded into system_prompt), activation (a synthesized `skill` tool call returns full instructions — recognizing an already-declared `skill` tool in either shorthand (`{"name": "skill"}`) or normalized OpenAI function-calling form, so it is never duplicated), and execution (a further call reads a bundled resource, reusing file_read's offset/max_chars pagination convention from ADR-2033, and decoding base64 resources safely even when the underlying bytes are not valid UTF-8). All registry awareness lives in SkillLoader at the factory layer; LLMAgent only gains a `_skills` value forwarded through its existing tool-call context — including the ephemeral ToolAgent spawned by the stuck-model synthesis follow-up round — and ToolAgent gains one new built-in tool handler; neither imports anything from cleveractors.registry. Skill.to_context_dict() surfaces the full set of optional schema fields (license, compatibility, metadata, allowed_tools) alongside name/description/instructions/resources. The `skills` field itself is documented only in ADR-2034, not mirrored into docs/index.md, matching how ADR-2031/ADR-2032 handled their own new LLM agent config fields (graduation into the normative spec is deferred to a future ADR). The Skill package schema, which has no other home, is appended as a new §16 in docs/actor-registry-standard.md (after the original §1-15 body, with a version bump to 1.1.0) rather than spliced into the existing §3.2 package-type table. Behave scenarios cover schema validation, all three reference schemes in both their sync and async forms (including package_id parity between registry: and local:-shaped resolutions, and an unresolvable ID: reference), loader orchestration (including tool-declaration dedup in both supported shapes), factory/agent integration (including the async factory path and an end-to-end run through the real Executor/runtime dispatch layer with a real on-disk skill package), and the ToolAgent skill tool (activation, resource reads, pagination, error paths). A Robot Framework suite exercises the same flow against a real on-disk skill file and real LocalPackageStore/PackageContentResolver resolution end to end, mocking only the LLM API call. Full nox suite green. ISSUES CLOSED: #8857329ee52e882336c478PR has already been merged. This is a bug in Gitea.
Pull request closed