Skip to content

PATTERN Cited by 1 source

Colocated child actor RPC

Pattern

When an agent (or any parent actor) needs to delegate work to subordinates that each have their own state + tool surface but need tight coordination, create child actors co-located with the parent — same host, same address space as far as the runtime can arrange — and expose them via typed RPC whose call latency is effectively a local function call. Each child keeps its own isolated storage; sharing between children is explicit, never implicit.

The pattern gives three structural properties:

  1. Isolation of state. Each child has its own storage + execution context; one child's state cannot leak into another's.
  2. Function-call latency for intra-agent coordination, because co-location avoids network round-trips.
  3. Typed RPC — compile-time catching of parameter shape, return type, and method presence. In TypeScript, misuse is a type error, not a runtime one.

Canonical instance: Project Think sub-agents (2026-04-15)

From the Project Think launch (sources/2026-04-15-cloudflare-project-think-building-the-next-generation-of-ai-agents):

import { Agent } from "agents";

export class ResearchAgent extends Agent {
  async search(query: string) { /* ... */ }
}
export class ReviewAgent extends Agent {
  async analyze(query: string) { /* ... */ }
}

export class Orchestrator extends Agent {
  async handleTask(task: string) {
    const researcher = await this.subAgent(ResearchAgent, "research");
    const reviewer = await this.subAgent(ReviewAgent, "review");
    const [research, review] = await Promise.all([
      researcher.search(task),
      reviewer.analyze(task)
    ]);
    return this.synthesize(research, review);
  }
}

Substrate properties (per post):

"Sub-agents are child Durable Objects colocated with the parent via Facets, each with their own isolated SQLite and execution context… Sub-agents are isolated at the storage level. Each one gets its own SQLite database, and there's no implicit sharing of data between them. This is enforced by the runtime where sub-agent RPC latency is a function call. TypeScript catches misuse at compile time."

The substrate — Durable Object Facets — is Cloudflare's primitive for child DOs sharing a parent's host, introduced as a building block without the Project Think post re- deriving it.

Why colocation is load-bearing

For agent orchestration, the call volume from parent to children is high — spawning parallel sub-tasks, gathering results, re- checking, iterating. At per-call network latency (even 1 ms per hop), multi-child orchestration adds tens of ms to every turn. At function-call latency, the cost is essentially the RPC serialisation.

Colocation also makes Promise.all([researcher.search(...), reviewer.analyze(...)]) return-at-slowest-child cleanly. A classical cross-host fan-out has both calls on the wire with all the usual slow-tail problems; co-located children's parallelism is essentially parent's-event-loop-driven.

State isolation between children

The critical contrast with shared-memory parallelism: each child has its own storage (SQLite, in Project Think), and the runtime enforces no implicit sharing. Cross-child data flow goes through the parent (parent reads from child A, passes to child B via RPC) or through an external store (D1, R2, KV). Never through ambient globals.

This gives actor-model serialisation at each child — no races on child-internal state while keeping the parent free to orchestrate concurrently.

Contrast with cross-host agent swarms

  • Colocated children: function-call latency, storage-level isolation, tight coordination. Good for one user's one task that decomposes into concurrent subtasks.
  • Cross-host agent swarm: each agent is a full actor in its own location, coordinated via messages or a shared topic. Good for multi-user / multi-tenant collaboration (e.g. one agent per user negotiating in a room).

Project Think's sub-agents are the first shape; the traditional per-user Durable Object addressability is the second. Same substrate, different composition.

When the pattern fits

  • Agent workflows where a parent planner decomposes tasks into parallel independent subtasks (research + review; draft + fact- check; fetch from multiple sources + synthesise).
  • Systems that need typed RPC between in-process components but want runtime-enforced isolation.
  • Workloads where tight coordination (parent-synthesises-children's- outputs) would be prohibitively slow across the wire.

When it doesn't

  • Children don't need persistent state — no reason to promote them to actors; plain async functions suffice.
  • Children are fully independent with their own user identity / billing / trust boundary — they want addressable actors in their own location, not colocated under a parent.
  • Heavy compute in one child — colocation means resource contention with parent + sibling; separate actors on distinct hosts may scale better.

Open design questions

  • Failure semantics. If a child dies, is the parent killed? Project Think's post doesn't decompose; the canonical answer is probably parent-survives + RPC-returns-error. Aggregating partial results across a failed child is the caller's problem.
  • Checkpoint scope. A resumable fiber running in the parent may have spawned children; whether a recovery of the parent re-spawns the children or inherits their state is a governance question.
  • Naming + lifetime. subAgent(Class, "name") with the same name returns the same child instance within the parent's scope; lifetime management (when does the child terminate?) follows the parent's lifetime in Project Think's default.

Seen in

Last updated · 200 distilled / 1,178 read