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.
The three kinds of composition
Section titled “The three kinds of composition”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.
Reuse structure with includes
Section titled “Reuse structure with includes”includes composes one machine’s structure into another, at two granularities.
Machine-level: include a whole base
Section titled “Machine-level: include a whole base”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 testsmachine 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-level: mix in one aspect
Section titled “Section-level: mix in one aspect”<section> includes "@ref" mixes the referenced krate’s same section into yours:
ensures includes "@acme/policy/pii" // apply a governance policyverifies includes "@safety/redteam_suite" // apply a shared test suiteaccepts 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 hereThe 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.
Import names with uses and defines
Section titled “Import names with uses and defines”uses "@acme/email/send" as send_email // alias a machine as a callable capabilityuses "@acme/utils" { slugify, truncate } // import only these helper functionsdefines slugify(s) = string.lowercase(string.replace(s, " ", "-")) // your own helperWhat each unlocks
Section titled “What each unlocks”behaves includes: a registry of governed, callable flows: resilience (run retry_with_backoff), sanitization (run redact_pii), integration plumbing (write the signing dance once,runit everywhere), domain subroutines (run score_lead).- Machine-level
includes: org compliance baselines and bundle bases: every agent includes@acme/compliance/hipaaand 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.
Calling another machine
Section titled “Calling another machine”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 |
A composed pipeline
Section titled “A composed pipeline”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.
The encapsulation principle
Section titled “The encapsulation principle”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 machinebehaves 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 storybehaves 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.assignmentsEach submachine is independently testable and potentially reusable.
Governance across boundaries
Section titled “Governance across boundaries”When machine A calls machine B:
- Machine A needs
machine.callpermission (or the specific capability required by B) - Machine B runs under its own governance rules
- A’s trust level is the ceiling for B (B cannot exceed A’s privileges)
- Each machine’s behavioral ledger is independent
- 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.
Passing data
Section titled “Passing data”Input keys in ask ... from are passed as the called machine’s accepts fields:
// Callerask enrich, from: "customer_enricher" customer_id: input.id include_history: true
// Called machinemachine customer_enricher accepts customer_id as text, is required include_history as boolean, default: falseTesting composed machines
Section titled “Testing composed machines”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.
Try it
Section titled “Try it”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.
Next steps
Section titled “Next steps”- Flows - Multi-flow machines for complex execution paths
- Actions - Stdlib actions
- ask … from reference - Full specification