Zum Inhalt springen
Developer Preview — APIs and language features may change before 1.0

Composition

Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.

Real systems are not single machines. They are compositions: one machine calling another, each with its own contract, governance, and audit trail. In mashin, composition is a first-class concept.

Composition in mashinTalk is materialized, not referenced. Every reusable piece is expanded into your machine, so you read the machine, not a class tree. mashinTalk has no inheritance. And how things combine depends on what they are. Three verbs:

You want to… Verb Construct
Include shared structure (governance, goals, tests, flows, surfaces) include machine X includes @base, <section> includes @ref
Call shared behavior (steps/logic) call behaves includes @lib then run <flow>; ask s from @m at runtime
Check you match a contract check accepts includes @proto (conformance), verifies includes @suite
Depend on a name / helper functions import uses "@acme/utils" { slugify }, defines f(x) = ...

The rule to remember: include structure, call behavior, check contracts — and your I/O contract is always yours. Includes never write a machine’s accepts or responds with; they constrain it, and the compiler enumerates the demands. A base upgrade can never change what your machine accepts or returns.

includes composes one machine’s structure into another, at two granularities.

machine X includes "@base" is shorthand for including every compostable section of the base at once: its governance, goals, stores, surfaces, tests, and named flows. It does not touch your contract — you author accepts and responds with yourself, always:

// @acme/base/support_agent (the base)
machine support_agent
accepts
message as text, is required
responds with
reply as text
ensures
permissions
allowed to model
// your machine: authors its own contract; includes the base's governance and tests
machine billing_agent includes "@acme/base/support_agent"
accepts
message as text, is required
responds with
reply as text
behaves
compute reply
{reply: "Billing: " + input.message}

After compile, billing_agent’s Definition contains the model permission and the base’s tests, each tagged with where it came from (included from @acme/base/[email protected]). Nothing is hidden in the base, and nothing was written into your contract. If an included test or flow uses a field you did not declare, the compile fails with an error naming the field, the test or flow, and its origin — the include tells you what your contract must contain; you write it.

One authoring rule for base machines: tests that exercise the base’s own main line cannot transfer to an includer (the main line is never imported), so an includable base writes its tests against its flows and its contract — the pattern the standard kits already follow.

<section> includes "@ref" mixes the referenced krate’s same section into yours:

ensures
includes "@acme/policy/pii" // apply a governance policy
verifies
includes "@safety/redteam_suite" // apply a shared test suite
accepts
includes "@pipeline/message_v2" // "I implement this interface" (conformance check)

For accepts / responds with, the conformance check is the only way I/O participates in composition (interface intent): you hand-write your contract, and the include verifies it is a superset of the protocol. The other sections merge as a union. Collisions are always loud, never last-wins.

Share behavior with behaves includes (flow library)

Section titled “Share behavior with behaves includes (flow library)”

Behavior is never absorbed invisibly. You import a library’s named flows and call them explicitly:

// @acme/lib/resilience (the library)
machine resilience
behaves
flows
flow add_tax
compute total
{total: input.amount * 1.08}
machine checkout
accepts
amount as number, is required
responds with
total as number
behaves includes "@acme/lib/resilience"
compute base
{amount: input.amount}
run add_tax // explicit call site; you SEE the flow runs here

The imported flow becomes a named subroutine; the only thing in your main line is the run you wrote. Reading checkout top to bottom (compute base then run add_tax), you know exactly what executes. This is the difference between composition you can read and a class hierarchy you have to excavate.

uses "@acme/email/send" as send_email // alias a machine as a callable capability
uses "@acme/utils" { slugify, truncate } // import only these helper functions
defines slugify(s) = string.lowercase(string.replace(s, " ", "-")) // your own helper
  • behaves includes: a registry of governed, callable flows: resilience (run retry_with_backoff), sanitization (run redact_pii), integration plumbing (write the signing dance once, run it everywhere), domain subroutines (run score_lead).
  • Machine-level includes: org compliance baselines and bundle bases: every agent includes @acme/compliance/hipaa and picks up the standard permissions, redaction, event declarations, and test suite in one line — while authoring its own contract.
  • Section-level includes: apply one aspect: a governance policy, a shared safety test suite, or a protocol you conform to so machines interoperate in a pipeline or marketplace.

Because every include expands into the governed Definition (version-pinned, provenance-tagged), a change to a shared base flows to every consumer as a governed, promotable version bump in the evolution ledger, not a silent global mutation. Composition here is policy distribution you can prove. See ADR: Composition semantics for the full model.

At runtime, you call another machine with ask ... from the same way you call a stdlib action machine. Each call produces an intent that is mediated before execution. The called machine runs under its own governance rules, and capabilities can only narrow through composition (the callee cannot exceed the caller’s permissions).

ask validate, from: "@myorg/orders/validate"
order_id: input.order_id
returns
valid as boolean
errors as list
assuming
valid: true
errors: []

The from parameter takes a machine path. Three namespaces:

Namespace Example Meaning
@mashin/actions/* @mashin/actions/http/get Official standard library
@orgname/* @myorg/billing/charge Organization machines
bare name "data_enricher" Local machine in the same project

Here is an order processing pipeline built from three machines:

machine process_order
accepts
order_id as text, is required
customer_id as text, is required
responds with
status as text
shipping_estimate as number
behaves
ask validate, from: "@myorg/orders/validate"
order_id: input.order_id
returns
valid as boolean
errors as list
decide check_validation
when steps.validate.valid
ask ship, from: "@myorg/shipping/calculate"
order_id: input.order_id
customer_id: input.customer_id
returns
cost as number
days as number
otherwise
compute reject
{status: "rejected", shipping_estimate: 0}
compute result
{
status: "approved",
shipping_estimate: steps.ship ? steps.ship.days : 0
}
ensures
permissions
allowed to
call
verifies
test "valid order gets a shipping estimate"
assuming validate {valid: true, errors: []}
assuming ship {cost: 9.99, days: 3}
given {order_id: "ord_1", customer_id: "cus_1"}
expect {status: "approved", shipping_estimate: 3}

Each ask ... from call invokes an independent machine. @myorg/orders/validate has its own accepts, ensures, and behaves. When it runs, its governance rules apply. The caller’s trust level constrains what the callee can do (trust ceiling), but governance does not leak across boundaries.

When a machine grows past 5-6 steps, look for groups of related steps that can be extracted into submachines. The parent should read as a high-level narrative:

// Before: 8 steps in one machine
behaves
ask fetch_tickets, from: "@mashin/actions/http/get"
url: input.api_url
compute parse_tickets
{parsed: steps.fetch_tickets.body}
ask classify, using: "anthropic:claude-haiku-4-5"
with task "Classify these tickets"
compute filter_urgent
{urgent: steps.classify.items.filter(i => i.priority == "urgent")}
ask assign_agents, from: "agent_lookup"
tickets: filter_urgent.urgent
compute match_assignments
{matched: steps.assign_agents.assignments}
ask notify_slack, from: "@mashin/actions/http/post"
url: "https://slack.com/api/notify"
compute done
{status: "complete"}
// After: parent reads as a story
behaves
ask get_and_classify, from: "@myorg/support/classify_tickets"
api_url: input.api_url
ask assign, from: "@myorg/support/assign_agents"
tickets: steps.get_and_classify.urgent
ask notify, from: "@myorg/support/notify_team"
assignments: steps.assign.assignments

Each submachine is independently testable and potentially reusable.

When machine A calls machine B:

  1. Machine A needs machine.call permission (or the specific capability required by B)
  2. Machine B runs under its own governance rules
  3. A’s trust level is the ceiling for B (B cannot exceed A’s privileges)
  4. Each machine’s behavioral ledger is independent
  5. The call itself is recorded in A’s ledger (target, inputs, result)

This is transitive governance. You do not need to redeclare B’s permissions inside A. Each machine is self-governing.

Input keys in ask ... from are passed as the called machine’s accepts fields:

// Caller
ask enrich, from: "customer_enricher"
customer_id: input.id
include_history: true
// Called machine
machine customer_enricher
accepts
customer_id as text, is required
include_history as boolean, default: false

Use assuming to mock the called machine’s response in tests:

ask validate, from: "@myorg/orders/validate"
order_id: input.order_id
assuming
valid: true
errors: []

In test mode, the called machine is never invoked. The assuming values are returned immediately. This means you can test the caller’s logic without running (or even having access to) the callee.

Build a three-machine pipeline: one machine that fetches data, one that classifies it with an LLM, and a parent machine that orchestrates them. Use ask ... from in the parent to call the other two. Add assuming blocks so the parent can be tested independently.