How to build type-safe pipelines between AI agent tools.

Composition Guide

The real power of typed interfaces shows up when tools compose. If Tool A outputs CodeDiff and Tool B expects CodeDiff, the pipeline is structurally sound. If Tool A outputs String but Tool B expects SecurityReport, you have a type mismatch — and effector catches it before runtime.

Why Composition Matters

In untyped agent systems, composition is held together by convention and prayer. Developer A writes a code review tool. Developer B writes a notification tool. They "work together" because someone manually verified the JSON shapes match. When either tool changes, the pipeline silently breaks.

Effector makes composition verifiable:

npx @effectorhq/core compose check ./review-tool ./notify-tool

The Composition Model

Effector composition checking uses the type catalog and subtype relationships to verify pipeline compatibility.

Direct Match

The simplest case — output type matches input type exactly:

# Tool A: code-review
[effector.interface]
input = "CodeSnippet"
output = "ReviewReport"

# Tool B: review-summarizer
[effector.interface]
input = "ReviewReport"
output = "Summary"
npx @effectorhq/core compose check ./code-review ./review-summarizer
# ✓ Pipeline valid: CodeSnippet → ReviewReport → Summary

Subtype Match

Effector supports structural subtyping. SecurityReport is a subtype of ReviewReport — it has all the same required fields plus additional ones. So a tool that outputs SecurityReport can feed into a tool that expects ReviewReport:

# Tool A: security-scan
[effector.interface]
output = "SecurityReport"

# Tool B: review-summarizer (expects ReviewReport)
[effector.interface]
input = "ReviewReport"

This works because SecurityReport <: ReviewReport — every SecurityReport is a valid ReviewReport.

Compatibility Scores

Not all type relationships are binary. Effector assigns compatibility scores:

RelationshipScoreExample
Exact match1.0ReviewReportReviewReport
Direct subtype0.95SecurityReportReviewReport
Same category0.9LintReportReviewReport
Compatible fields0.8Types with overlapping required fields
Incompatible0.0ImageRefReviewReport

Multi-Step Pipelines

For longer pipelines, compose validates every adjacent pair:

# Pipeline: fetch → review → notify
[pipeline]
steps = ["git-fetch", "code-review", "slack-notify"]
npx @effectorhq/core compose check ./git-fetch ./code-review ./slack-notify
# ✓ git-fetch(Repository → CodeSnippet) → code-review(CodeSnippet → ReviewReport) → slack-notify(ReviewReport → SlackMessage)

Using effector-compose Programmatically

import { checkComposition } from '@effectorhq/compose';

const result = checkComposition(
  { output: 'SecurityReport' },
  { input: 'ReviewReport' }
);

console.log(result.compatible);  // true
console.log(result.score);       // 0.95
console.log(result.reason);      // "SecurityReport is a subtype of ReviewReport"

Context Propagation

Context types flow through the entire pipeline. If any tool in the chain requires GitHubCredentials in its context, the pipeline's aggregate context must include it:

# Tool A
[effector.interface]
context = ["Repository"]

# Tool B
[effector.interface]
context = ["GitHubCredentials"]

The composed pipeline requires both Repository and GitHubCredentials in its context.

Common Patterns

Fan-out

One tool's output feeds multiple downstream tools:

code-review → slack-notify
           → github-comment
           → jira-update

Each downstream tool must accept the upstream output type (or a supertype).

Aggregation

Multiple tools feed into a single aggregator:

security-scan  → aggregate-reports
lint-check     →
test-runner    →

The aggregator's input type must be a common supertype of all upstream outputs.

Conditional Routing

Use the compatibility score to route dynamically:

const tools = [slackNotify, discordNotify, emailNotify];
const bestMatch = tools
  .map(t => ({ tool: t, score: checkComposition(upstream, t).score }))
  .sort((a, b) => b.score - a.score)[0];

Next Steps