Status: Draft Version: 0.2.0
Effectors are designed to compose. But composition without type checking is guesswork — an agent runtime wiring docker-build → slack-notify has no way to know whether the output of one is a valid input for the other, until it fails at 2am in production.
The Effector Composition Algebra solves this: every composition operation has a type rule that the runtime verifies before execution. A type error at wire-up time is infinitely cheaper than a runtime failure.
One Effector feeds its output directly into the next Effector's input.
A → B iff output(A) <: input(B)
The <: relation is structural subtyping: output(A) must have at least all the fields required by input(B), with compatible types.
# pipeline.yml
pipeline:
- effector: code-analyzer # input: CodeDiff, output: ReviewReport
- effector: report-formatter # input: ReviewReport, output: Markdown
- effector: slack-poster # input: Markdown, output: NotificationType check:
code-analyzer.output = ReviewReport
report-formatter.input = ReviewReport ✓ (exact match)
report-formatter.output = Markdown
slack-poster.input = Markdown ✓ (exact match)
If code-analyzer were replaced with a skill outputting DeploymentStatus, the pipeline would reject at type-check time: DeploymentStatus </: ReviewReport.
Multiple Effectors run concurrently from the same input, their outputs merged.
A ‖ B iff input(A) <: T and input(B) <: T (same input source T)
pipeline:
- source: code-diff
- parallel:
- effector: security-scanner # input: CodeDiff, output: SecurityReport
- effector: style-checker # input: CodeDiff, output: StyleReport
- effector: test-runner # input: CodeDiff, output: TestResult
- effector: report-aggregator # input: [SecurityReport, StyleReport, TestResult]The report-aggregator's input type is validated against the merged output array of all parallel branches.
One of several branches is selected at runtime based on a predicate. The type rule requires all branches to produce compatible outputs:
A ? B : C iff output(B) <: R and output(C) <: R (some common supertype R)
pipeline:
- effector: lang-detector # input: String, output: LangTag
- conditional:
predicate: "output.lang == 'python'"
then:
effector: python-linter # output: LintReport
else:
effector: generic-linter # output: LintReport
- effector: report-poster # input: LintReportBoth branches output LintReport — the downstream Effector sees a consistent type regardless of which branch ran.
If the primary Effector fails (error, timeout, permission denied), the fallback runs. Type rule: fallback must accept the same input and produce a compatible output.
A | B iff input(B) <: input(A) and output(B) <: output(A)
pipeline:
- effector: gpt4-reviewer # input: CodeDiff, output: ReviewReport
fallback:
effector: basic-linter # input: CodeDiff, output: ReviewReport (simplified)This enables graceful degradation: a high-quality LLM-based reviewer falls back to a deterministic linter when the LLM is unavailable, without the downstream pipeline noticing.
A runtime implementing composition MUST reject a pipeline if any of the following conditions hold:
RULE 1: Sequential type mismatch
output(step[i]) </: input(step[i+1])
→ ERROR: "Type mismatch at step N: output ${T1} is not assignable to input ${T2}"
RULE 2: Parallel merge type conflict
∃ branch b: output(b) </: declared_merge_input
→ ERROR: "Parallel branch ${B} output ${T} is not assignable to merge target"
RULE 3: Conditional branch divergence
¬∃ supertype R s.t. output(then) <: R ∧ output(else) <: R
→ ERROR: "Conditional branches produce incompatible types: ${T1} vs ${T2}"
RULE 4: Fallback signature mismatch
input(fallback) </: input(primary) OR output(fallback) </: output(primary)
→ ERROR: "Fallback must be substitutable for primary"
RULE 5: Unknown type
A type reference not in effector-types standard library and not locally defined
→ WARNING: "Unresolved type ${T} — treating as unknown (skipping type check for this step)"
Rule 5 produces a warning (not an error) to preserve backward compatibility with untyped legacy skills.
Dependencies are declared as an array of tables in the manifest:
[[effector.dependencies]]
name = "git-operations"
version = ">=1.0.0"
type = "skill"
optional = false| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | The dependency's package name |
version |
string | Yes | Semver constraint |
type |
string | No | Expected Effector type (validated at install time) |
optional |
boolean | No | If true, the Effector works without it (default: false) |
registry |
string | No | Override the default registry for this dependency |
- Collect — Gather all dependency declarations from the Effector and its transitive dependencies
- Resolve — For each dependency, find the highest version that satisfies all constraints
- Detect conflicts — If two Effectors require incompatible versions of the same dependency, report an error
- Flatten — Produce a flat list of resolved dependencies (no duplicates)
- Validate — Check that resolved types match declared types
- Install — Install in dependency order (leaves first)
Circular dependencies are not allowed. The resolver MUST detect and reject cycles:
A depends on B
B depends on C
C depends on A ← REJECTED: circular dependency
A workflow Effector chains skill Effectors:
# deploy-and-notify/effector.toml
[effector]
name = "deploy-and-notify"
type = "workflow"
[[effector.dependencies]]
name = "docker-build"
version = ">=1.0.0"
type = "skill"
[[effector.dependencies]]
name = "kubernetes-apply"
version = ">=1.0.0"
type = "skill"
[[effector.dependencies]]
name = "slack-notify"
version = ">=2.0.0"
type = "skill"The pipeline file references these skills by name:
# pipeline.yml
steps:
- skill: docker-build
params:
image: ${IMAGE_NAME}
tag: ${VERSION}
- skill: kubernetes-apply
params:
manifest: ./k8s/deployment.yml
- skill: slack-notify
params:
channel: "#deployments"
message: "Deployed ${IMAGE_NAME}:${VERSION}"A workspace Effector bundles a complete agent configuration:
# devops-engineer/effector.toml
[effector]
name = "devops-engineer"
type = "workspace"
[[effector.dependencies]]
name = "docker-compose"
version = ">=2.0.0"
type = "skill"
[[effector.dependencies]]
name = "kubernetes-deploy"
version = ">=1.0.0"
type = "skill"
[[effector.dependencies]]
name = "terraform-plan"
version = ">=1.0.0"
type = "skill"
[[effector.dependencies]]
name = "security-scan"
version = ">=1.0.0"
type = "skill"
[[effector.dependencies]]
name = "deploy-and-notify"
version = ">=1.0.0"
type = "workflow"When installed, the workspace's skills are co-installed automatically.
An extension Effector can enhance skills with additional capabilities:
# github-enhanced/effector.toml
[effector]
name = "github-enhanced"
type = "extension"
[[effector.dependencies]]
name = "github-pr-review"
version = ">=1.0.0"
type = "skill"
[[effector.dependencies]]
name = "github-issue-triage"
version = ">=1.0.0"
type = "skill"The extension provides code-level hooks (webhooks, authentication) that the skills can leverage.
A prompt Effector provides the instruction template; a skill provides the tooling:
# thorough-review/effector.toml
[effector]
name = "thorough-review"
type = "prompt"
[[effector.dependencies]]
name = "code-analysis"
version = ">=1.0.0"
type = "skill"
optional = trueWhen a runtime loads an Effector, it negotiates capabilities:
- Check runtime bindings — Does the Effector have a binding for this runtime?
- Check requirements — Are required binaries, env vars, and skills available?
- Check permissions — Does the runtime's security policy allow the declared permissions?
- Fallback — If the primary binding fails, can the Effector fall back to a generic binding?
| Outcome | Meaning |
|---|---|
ready |
All requirements met; Effector can execute |
degraded |
Optional dependencies missing; partial functionality |
blocked |
Required dependency or permission unavailable; cannot execute |
incompatible |
No runtime binding exists for this runtime |
Runtimes SHOULD report the negotiation result to the user with actionable resolution steps.
For reproducible installations, runtimes SHOULD support lock files:
# effector.lock
# Auto-generated. Do not edit.
[[package]]
name = "docker-compose"
version = "2.1.3"
type = "skill"
checksum = "sha256:abc123..."
registry = "clawhub"
[[package]]
name = "slack-notify"
version = "2.3.0"
type = "skill"
checksum = "sha256:def456..."
registry = "clawhub"Lock files pin exact versions and checksums, ensuring that install produces identical results across environments.