Skip to content

[Feat]: SkillContext + skill chains #330

Description

@rosspeili

Feature Description

Skillware today excels at one skill, one tool, one execute(). Hosts with many skills or fixed middleware order reinvent discovery merge, Directive budgeting, and sequential execute() loops.

Add (opt-in only — nothing changes until callers use the new APIs):

SkillContext Skill chains
Entry ctx = SkillContext() run_chain("sanitize_input", host_input={...})
Purpose Registry brief + tools + progressive Directive load Named ordered skills + JSON handoff
Config Constructor filters chains: in merged YAML
CLI skillware context show skillware chain list | show | validate | run | dry-run

Cross-skill orchestration vocabulary: chain / chains / chaining only. Do not add framework run_pipeline / pipelines: (reserved for in-skill actions e.g. uk_companies_house_handler, EVM config, unrelated domains).

Skills still never call each other.


Backward compatibility (non-negotiable)

This release is strictly additive. Existing installs and scripts must behave identically unless they opt into new APIs.

Guarantee How
SkillLoader.load_skill() unchanged Same signature, bundles, shadow rules, execute_module flag
Discovery / skillware list unchanged Same IDs, tiers, ordering; SkillContext reads discovery, does not alter it
Existing CLI commands unchanged list, doctor, test, config, paths, mail, … — new context and chain subcommands only
YAML without chains: Config load identical; config.chains is {}
Legacy chains: placeholder e.g. chains: { default: [] } from early example — ignored safely (no steps key); config still loads; optional one-line warning
Unknown / malformed chain entries Skip entry + warn; do not fail entire config or block skillware list
config.extra for other keys theme, custom keys — still merged as today; only chains moves from extra → parsed field (like mail)
No import side effects import skillware / from skillware import SkillContext does not scan disk or run chains
No default chain execution Chains run only via run_chain(), skillware chain run, or explicit host code
Existing tests green Full pytest tests/ passes; only extend tests where asserting new config.chains shape

Regression gate before merge: pytest tests/ + black --check + flake8 on touched modules.


Locked decisions

# Decision
D1 from skillware import SkillContext → ctx = SkillContext()
D2 brief default; execute() auto-prepares; prepare() for tool-select disclosure
D3 Discovery filters (all / one / list / category / root / exclude / optional cap); no default cap
D4 run_chain(), ChainResult, validate_chain(), load_chain(), list_chains()
D5 chains: first-class in SkillwareConfig; global + project merge; project wins on name clash
D6 No separate chain file; no CLI chain authoring (chain add); YAML source of truth
D7 All 3 example chains + conditional when: on step (see schema + example 4)
D8 No Aura / external host exports in Skillware
D9 Single delivery: core + config + CLI + tests + #297 docs

Config — chains: storage

Layer File Precedence
Global ~/.config/skillware/config.yaml Base
Project .skillware.yaml (walk-up) Overrides global per chain name
  • Add SkillwareConfig.chains: Dict[str, ChainDefinition] (default {}).
  • Add "chains" to _KNOWN_TOP_LEVEL_KEYS (removed from opaque extra when parsed, same pattern as mail).
  • Invalid legacy blocks do not break merge.

Chain YAML schema

chains:
  <name>:
    description: string          # list/show
    when: string                 # human doc — when to pick this chain
    stop_on_error: true          # default true
    steps:
      - id: scan                   # optional; referenced by when.prior_step
        skill: category/skill_id   # required
        params: {}                 # static; merged into execute()
        input_from:                # param -> binding
          source_text: host.source_text
        map_out:                   # output field -> binding (optional)
          sanitized_text: next.raw_text
        when:                        # optional — skip step if condition false
          prior_step: scan           # step id | 0-based index | skill_id of earlier step
          field: is_safe             # dot path in that step's output
          equals: true               # v1: strict equality only

Bindings

Prefix Meaning
host.<key> host_input[key] (dot path if nested)
next.<param> Next step execute param (via map_out queue)
prev.<field> Previous executed step output (dot path); skipped steps do not update prev for this rule — use explicit prior_step + field in input_from when needed

Step when: (conditional skip)

Rule Behavior
Condition true Run step normally
Condition false Step skipped (not an error); recorded in ChainResult.steps[].status = "skipped"
prior_step missing / forward ref validate fails; run fails closed
field missing in prior output Treat as false → skip (document); warn in validate if manifest lacks field
Chain outcome ok all executed steps succeeded; partial if any skipped and none failed; failed if any executed step failed and stop_on_error

Dry-run: resolve when against mock or prior dry-run outputs; mark steps would_skip.

Validation (validate_chain / chain validate)

  • Skills resolve via discovery (--strict → error; default → warn for missing optional deps).
  • Required host.* keys aggregated for chain show.
  • map_out keys checked against manifest outputs when present (warn, not fail, if absent).
  • when.prior_step must reference an earlier step.
  • No circular next/prev maps.

Example chains (.skillware.yaml.example)

1. sanitize_input (with conditional rewriter)

  sanitize_input:
    description: Scan untrusted text; compress only if safe.
    when: Untrusted text is about to enter model context.
    steps:
      - id: scan
        skill: security/prompt_injection_firewall
        params:
          sensitivity: balanced
          input_mode: auto
        input_from:
          source_text: host.source_text
        map_out:
          sanitized_text: next.raw_text
      - skill: optimization/prompt_rewriter
        when:
          prior_step: scan
          field: is_safe
          equals: true
        params:
          compression_aggression: medium

If unsafe, step 2 skipped; ChainResult.final = firewall output; status = partial.

2. preflight_untrusted_html

  preflight_untrusted_html:
    description: HTML-mode injection scan before parsing or summarization.
    when: Raw HTML or rich markup from the web enters the agent.
    steps:
      - skill: security/prompt_injection_firewall
        params:
          input_mode: html
          sensitivity: balanced
        input_from:
          source_text: host.source_text

3. scan_then_gate

  scan_then_gate:
    description: Sanitize untrusted text, then evaluate token budget gate.
    when: Autonomous loop with untrusted ingest and a token ceiling.
    steps:
      - id: scan
        skill: security/prompt_injection_firewall
        params:
          sensitivity: balanced
          input_mode: auto
        input_from:
          source_text: host.source_text
      - skill: monitoring/token_limiter
        params:
          action: check
        input_from:
          task_id: host.task_id
          current_token_count: host.current_token_count
          max_allowed_tokens: host.max_allowed_tokens

Step 2 uses host metrics; firewall still runs as preflight.


CLI

Command Purpose
skillware chain list Names, description, when, step count
skillware chain show <name> Steps, bindings, required host_input, step when
skillware chain validate [name] CI-safe; exit 1 on errors
skillware chain run <name> Execute via run_chain()
skillware chain dry-run <name> Resolve params + evaluate when; no execute()
skillware chain run sanitize_input --var source_text="hello"
skillware chain run sanitize_input --var source_text=@file.html --json
skillware chain run scan_then_gate \
  --var source_text=@in.txt --var task_id=job-1 \
  --var current_token_count=12000 --var max_allowed_tokens=32000
Command Purpose
skillware context show Brief registry (filters mirror SkillContext)
skillware context show --skill ID --categories a,b --roots bundled --mode brief
skillware context export -o ctx.md Debug export

New subcommands only — do not change existing command help or defaults.


Python API

SkillContext (wraps loader; does not replace it)

from skillware import SkillContext

ctx = SkillContext()  # filters: skill=, skills=, categories=, roots=, exclude_roots=, max_skills=
system = ctx.merge_system(host_prompt)
tools = ctx.tools("gemini")  # claude | openai | deepseek
prep = ctx.prepare("compliance/tos_evaluator")
out = ctx.execute("compliance/tos_evaluator", {...})  # auto-prepare if needed

Internally uses SkillLoader.load_skill(..., execute_module=False) for brief assembly; full load on prepare/execute only.

Chains

from skillware.chains import list_chains, load_chain, validate_chain, run_chain

result = run_chain(
    "sanitize_input",
    host_input={"source_text": raw},
    stop_on_error=True,
    validate_params=True,
)
# result.status: ok | partial | failed
# result.steps[].status: ok | skipped | failed
# result.final, result.errors

ChainResult (stable public shape)

@dataclass(frozen=True)
class ChainStepResult:
    index: int
    skill_id: str
    status: str          # ok | skipped | failed
    output: dict | None
    skip_reason: str | None

@dataclass(frozen=True)
class ChainResult:
    chain_name: str
    status: str          # ok | partial | failed
    steps: tuple[ChainStepResult, ...]
    final: dict | None
    errors: tuple[str, ...]

Rationale

  • [Docs]: add dedicated skill chaining guide under docs/usage #297 needs real APIs, not pseudocode.
  • Operators need validate/run/dry-run without writing Python.
  • Conditional skip avoids running rewriter on blocked input — common middleware pattern, minimal schema (equals only).
  • Additive design protects existing Skillware adopters and CI.

Affected paths (optional)

skillware/context.py, skillware/chains.py, skillware/core/config.py, skillware/__init__.py, skillware/cli.py, .skillware.yaml.example, tests/test_skill_context.py, tests/test_chains.py, tests/test_config.py, tests/test_cli.py, docs/usage/skill_chaining.md, docs/usage/cli.md, CHANGELOG.md

Implementation Idea

Core (new modules — no breaking edits to loader public API)

  • skillware/context.py — SkillContext, filters, modes, prepare/execute, adapter delegation
  • skillware/chains.py — parse, bind, when evaluator, run/validate/dry-run
  • skillware/core/config.py — parse chains, merge layers, legacy-safe
  • skillware/__init__.py — export SkillContext only (chains from skillware.chains)
  • .skillware.yaml.example — uncomment chains: with all 3 examples

CLI

  • context and chain subparsers (additive)
  • config show — show parsed chain names count (keep reserved-section note for empty/legacy)

Tests

  • tests/test_skill_context.py — filters, modes, prepare/execute, adapter parity with loader
  • tests/test_chains.py — all 3 chains; when skip + when run; nested host_input; stop_on_error; dry-run
  • tests/test_config.py — merge, project override, legacy default: [] still loads
  • tests/test_cli.py — chain validate/run/dry-run smoke
  • Full pytest tests/ regression

Docs


Acceptance criteria

  • Zero behavior change for existing loader/CLI/skill paths without opt-in (regression suite green)
  • SkillContext() + all discovery filters work
  • chains: global + project merge; legacy placeholder safe
  • sanitize_input skips rewriter when is_safe is false; runs when true
  • preflight_untrusted_html, scan_then_gate pass API + CLI + tests
  • when: validated and visible in show / dry-run
  • chain validate CI-ready (exit 1 on structural errors)
  • No framework pipeline vocabulary for cross-skill chaining
  • [Docs]: add dedicated skill chaining guide under docs/usage #297 closed

Explicitly out of scope (future issues)

Item Notes
skillware chain add (write YAML) Hand-edit + validate
Multi-action skills in YAML chains Gmail / EVM — manual Tier 1
when operators beyond equals e.g. in, gt — later
Parallel / branching chains Linear + skip only
Async run_chain #18
Subprocess isolation #112
NL chain generation —
Full #296 config hub Only chains parse here
Aura / third-party adapters External repos conform later

Test plan

  1. pytest tests/test_skill_context.py tests/test_chains.py tests/test_config.py tests/test_cli.py
  2. pytest tests/ — full regression
  3. Cases: legacy config with chains: {default: []}; project overrides global chain name; when-skip partial; when-run ok; validate exit code
  4. black --check / flake8 on new/changed Python

PR checklist

  • No changes to skill bundle layouts or skill.py behavior
  • SkillLoader public methods unchanged (context/chains call in, not refactor)
  • Legacy YAML loads; malformed chain entries warn-only
  • CHANGELOG Added (not Changed) for new APIs
  • Trust model doc note if execute_module=False path documented for brief mode

Activity

  1. self-assigned this
    on Sep 3, 2026
  2. added theissue type on Sep 3, 2026
  3. added
    documentationImprovements or additions to documentation.
    enhancementNew feature or request.
    core frameworkChanges to loader, env, config merge (skillware/core/config.py), base classes, or model adapters.
    testingpytest, doc-drift guards, or CI test coverage.
    cliskillware CLI — doctor, paths, config show, mail submenu, interactive menu, or docs/usage/cli.md.
    on Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

cliskillware CLI — doctor, paths, config show, mail submenu, interactive menu, or docs/usage/cli.md.core frameworkChanges to loader, env, config merge (skillware/core/config.py), base classes, or model adapters.documentationImprovements or additions to documentation.enhancementNew feature or request.testingpytest, doc-drift guards, or CI test coverage.

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions