-
Notifications
You must be signed in to change notification settings - Fork 0
RFC 0004 Dangling Step Kinds
Status: Draft — spec only, not implemented
WorkflowStepKind includes "subskill" and "delegate", and both compile
and pack successfully today — but both throw unconditionally at
execution time:
// packages/runtime/src/index.ts
case "delegate": {
if (ctx.dry) return { output: { dry_run: true, task: step.task }, resultAs: step.result_as };
throw new Error("delegate requires an A2A adapter (not configured)");
}
case "subskill": {
throw new Error(`subskill ${step.skill_id} not resolved in this runtime invocation`);
}subskill doesn't even get the dry-run exemption delegate does — a
package containing one can't even be dry-run inspected without a runtime
error. This is a worse failure mode than refusing earlier: a skill using
either kind passes skill validate, passes skill pack, can even be
minted (nothing in assessSkillContract or validatePackageBytes flags
it) — and only fails deep into a real skill run --mode execute, on
whatever step happens to reach it. The gap between "protocol says this is
valid" and "runtime says this never works" is exactly the class of thing
BUG-3's sweep spent this tier closing everywhere else.
A subskill step should be resolvable the same way a package manager
resolves a dependency — which the manifest already has a shape for
(SkillManifest.dependencies: SkillDependency[], currently unused by
anything):
interface SkillDependency {
skill_id: string; // content-addressed (PROTO-1) — skl_<hash prefix>
version: string; // semver range
package_digest?: string; // optional pin — exact digest required if present
}Resolution order, matching how this protocol already treats trust (local-first, explicit-over-implicit):
-
Local-first: check a local skill cache/workspace directory (mirrors
how
@skillerr/registry's local transparency log is the offline-first default) for a package matchingskill_id+ satisfyingversion, and — ifpackage_digestis pinned — matching digest exactly (refuse on mismatch, don't silently accept a same-id-different-content package). - Registry lookup (optional, if configured): same trust rules as any other package — an unsigned/untrusted subskill is still gated by the same execute-time trust checks as the top-level package (SEC-* tier unchanged — resolving a subskill doesn't bypass deny-by-default or the trust gate).
-
Refuse, don't guess: no local match and no registry configured (or
no match there either) →
subskill_unresolved, a distinct refusal code, not a generic thrown error.
Since a subskill can itself contain subskill steps, resolution must
track the in-progress resolution chain and refuse on a cycle
(subskill_cycle_detected) rather than recursing until a stack overflow.
The chain itself is exactly what PROTO-9's lineage (SkillSource.parents)
already wants to record — a resolved subskill's package_digest becomes a
parents entry in the resolving skill's provenance, so skill inspect
can render the whole dependency chain (see RFC 0006).
delegate depends on an external Agent-to-Agent protocol adapter — a
different integration surface (a live network call to another agent, not
a local package resolution). This RFC doesn't attempt to fully specify
A2A integration; it recommends marking delegate explicitly
reserved/experimental in docs/PROTOCOL.md (packages using it should
know they're opting into an unstable, adapter-dependent step kind), and
leaves full A2A semantics to a future RFC once a concrete adapter exists
to design against.
Independent of full resolution semantics landing, assessSkillContract
and/or validateWorkflowShape could flag subskill/delegate steps as
requiring an explicit contract-level acknowledgment (e.g. a
forbidden_actions-style "uses experimental step kind" declaration) so
at minimum skill validate — not a live execute run — is where a package
author first learns their skill won't run standalone. This is a smaller,
independently-shippable change that doesn't require this RFC's full
resolution design; noted here so a future contributor doesn't have to
rediscover it.
-
workflow.schema.json'ssubskillstep already requiresskill_id(PROTO-7, already shipped) — no change needed there. - New refusal codes:
subskill_unresolved,subskill_cycle_detected(runtime-level, not schema-level). - No
SkillManifest/Workflowfield changes —dependenciesalready exists and is unused; this RFC is what makes it meaningful.
Additive — dependencies is optional today and stays optional. A
subskill step with no matching dependencies entry keeps failing (now
with a clear code instead of a generic thrown error, which is itself a
strict improvement, not a behavior change worth gating behind a version
bump).
Once implemented: a subskill step resolves against a matching local
dependency and dry-runs successfully; a package_digest-pinned dependency
refuses on a mismatched local package; two skills each declaring the other
as a subskill dependency refuse with subskill_cycle_detected instead of
recursing; an untrusted (unsigned/development-sealed) resolved subskill is
still gated by the top-level execute trust check.
- Should subskill resolution be recursive by default, or require an
explicit
--resolve-subskillsopt-in the first time this ships (safer default: refuse unless explicitly enabled, consistent with--allow-untrustedneeding to be opt-in elsewhere in this protocol)?