Concurrency & The Actor Model

Turn's concurrency model is inspired by Erlang and Elixir: actors. Every agent is an isolated process with its own environment, context, memory, and mailbox. Actors communicate by sending messages. They cannot share state.

This model makes multi-agent systems composable, fault-tolerant, and scalable by construction.


Every Agent Is a Process

A Turn process has:

  • Its own environment (let bindings)
  • Its own context window (isolated, token-bounded)
  • Its own memory (isolated HNSW graph)
  • Its own mailbox (message queue)
  • Its own PID (process identifier)

No two processes share any of these. Coordination happens exclusively through message passing.


spawn — Creating Child Agents

syntax
let pid = spawn { <program> };

Spawns a new concurrent agent process. Returns its PID immediately. The spawned process runs concurrently on the Tokio scheduler.

basic_spawn.tn
// Spawn a child agent that waits for a task
let analyst = spawn {
  let task = receive();
  let result = infer Insight {
      "Analyze this business dataset: " + task;
  };
  return result;
};

call("echo", "Agent spawned with PID: " + analyst);

send / receive — Message Passing

Agents communicate by sending values to each other's mailboxes:

syntax
send(pid, value);        // Non-blocking — puts message in pid's mailbox
let msg = receive();     // Blocks until a message arrives
pipeline.tn
// Spawn a specialized analyst agent
let analyst = spawn {
  let query = receive();
  return infer Analysis { query; };
};

// Spawn a writer agent that receives analyst output
let writer = spawn {
  let analysis = receive();
  return infer Report { "Draft an executive summary from: " + analysis.summary; };
};

// Orchestrate: send query to analyst, route result to writer
send(analyst, "Q4 performance data: revenue up 18%, churn up 3%");
let analysis_result = receive();     // analyst sends back to parent

send(writer, analysis_result);
let report = receive();

call("echo", report);

NOTE

receive() blocks the current process (not a thread — it yields to the Tokio scheduler). Other agents continue running while the receiving agent waits.


link and monitor — Supervision

Turn supports Erlang-style process linking for fault isolation and supervisor trees.

link(pid) — Bidirectional Crash Propagation

linking
let worker = spawn { ... };
link(worker);

// If worker crashes, the parent receives an EXIT signal in its mailbox
// If the parent crashes, worker receives an EXIT signal

When a linked process exits with an error, its linked partners receive an EXIT message in their mailboxes. You can choose to handle it or let it propagate — building supervisor trees just like Erlang/OTP.

monitor(pid) — Unidirectional Observation

monitoring
let worker = spawn { ... };
monitor(worker);

// If worker exits (normally or abnormally),
// this process receives a DOWN message — without crashing itself

monitor is one-directional: only the monitoring process is notified. Use it when you want observability without fate-sharing.


A Full Multi-Agent Example

The following is a simplified version of Turn's canonical boardroom example — demonstrating a swarm of specialized agents collaborating on a shared task:

boardroom.tn
struct BoardMemo { recommendation: Str, vote: Str, confidence: Num };

// Spawn three specialist agents
let cfo = spawn {
  let brief = receive();
  return infer BoardMemo {
      "You are the CFO. Evaluate the financial risk of: " + brief;
  };
};

let cto = spawn {
  let brief = receive();
  return infer BoardMemo {
      "You are the CTO. Evaluate technical feasibility of: " + brief;
  };
};

let cmo = spawn {
  let brief = receive();
  return infer BoardMemo {
      "You are the CMO. Evaluate market opportunity of: " + brief;
  };
};

// Distribute the brief
let proposal = "Acquire a 22-person AI startup for $12M to accelerate our inference roadmap.";
send(cfo, proposal);
send(cto, proposal);
send(cmo, proposal);

// Collect votes
let cfo_memo = receive();
let cto_memo = receive();
let cmo_memo = receive();

// Aggregate
call("echo", "CFO: " + cfo_memo.vote + " (confidence: " + cfo_memo.confidence + ")");
call("echo", "CTO: " + cto_memo.vote + " (confidence: " + cto_memo.confidence + ")");
call("echo", "CMO: " + cmo_memo.vote + " (confidence: " + cmo_memo.confidence + ")");

Each agent runs concurrently, maintains its own context, performs its own inference, and returns a typed BoardMemo. The orchestrating process collects results without any shared state.


Remote Agents

Turn supports spawning agents on remote nodes over TCP:

syntax
let remote_pid = spawn_remote("192.168.1.42:9001", {
  let task = receive();
  return infer HeavyAnalysis { task; };
});

send(remote_pid, my_dataset);

The Turn distributed switchboard routes messages between nodes transparently. From your code's perspective, a remote PID is identical to a local one — the same send/receive API, the same message types.


Next Steps

  • Example Agent — Full boardroom simulation with actor coordination
  • Runtime Model — How the Tokio scheduler maps actors to threads
  • Error Handling — How EXIT signals propagate through linked actors