The actor model

JSConf Budapest 2024

David Khourshid ยท @davidkpiano
stately.ai

behind the scenes

Origin story

Setting the scene

Dealing with complex logic

Flashback

Origin story

Alan Kay

A Universal Modular ACTOR Formalism for Artificial Intelligence (1973)

Carl Hewitt, Peter Bishop, Richard Steiger

Gul Agha

  • Formalized the actor model

    Actors: A Model of Concurrent Computation in Distributed Systems

Making it look easy

State machines ๐Ÿ˜ตโ€๐Ÿ’ซ

Actor model ๐ŸŽญ

๐Ÿ‘ฉโ€๐Ÿ’ป

๐Ÿง”

I would like a coffee...

What would you like?

ย Actorย 

I would like a coffee, please.

ย Actorย 

๐Ÿ’ญ

๐Ÿ’ฌ

๐Ÿ’ฌ

Here you go. โ˜•๏ธ
Thanks!

๐Ÿ‘ฉโ€๐Ÿ’ป

๐Ÿง”

๐Ÿ‘ฉโ€๐Ÿณ

โ˜•๏ธโ”

๐Ÿ“

HUFโ“

HUF

๐Ÿ“„โœ…

โ˜•๏ธ

โ˜•๏ธ

Acting

The real world

Sequence diagram

What is an actor?

๐Ÿค– All computation is performed within an actor

๐Ÿ“ฉ Actors can communicate only through messages

๐Ÿ’ฅ In response to a message, an actor can:

๐Ÿ“ฌ Send messages to other actors

๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Create a finite number of child actors

๐‘“ (๐‘ฅ)

Behavior

State

๐ŸŽญ Change its state/behavior

A

B

โœ‰๏ธ

โœ‰๏ธ

C

โœ‰๏ธ

What is an actor?

๐Ÿค– All computation is performed within an actor

๐Ÿ“ฉ Actors can communicate only through messages

๐Ÿ’ฅ In response to a message, an actor can:

๐Ÿ“ฌ Send messages to other actors

๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Create a finite number of child actors

๐ŸŽญ Change its state/behavior

A

โœ‰๏ธ

๐Ÿค– All computation is performed within an actor

๐Ÿ“ฉ Actors can communicate only through messages

๐Ÿ’ฅ In response to a message, an actor can:

๐Ÿ“ฌ Send messages to other actors

๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Create a finite number of child actors

๐ŸŽญ Change its state/behavior

A โ†’ A'

What is an actor?

A

โœ‰๏ธ

๐Ÿค– All computation is performed within an actor

๐Ÿ“ฉ Actors can communicate only through messages

๐Ÿ’ฅ In response to a message, an actor can:

๐Ÿ“ฌ Send messages to other actors

๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Create a finite number of child actors

๐ŸŽญ Change its state/behavior

B

โœ‰๏ธ

What is an actor?

A

โœ‰๏ธ

๐Ÿค– All computation is performed within an actor

๐Ÿ“ฉ Actors can communicate only through messages

๐Ÿ’ฅ In response to a message, an actor can:

๐Ÿ“ฌ Send messages to other actors

๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Create a finite number of child actors

๐ŸŽญ Change its state/behavior

๐Ÿ†•

๐Ÿ†•

What is an actor?

An actor's script

  • ๐Ÿ“ฌ Send & receive messages

  • ๐ŸŽญ Change its internal state

  • ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Spawn child actors

{
  orders: [/* ... */],
  inventory: {
    // ...
  },
  sales: 134.65
}

An actor's script

Cast and crew

Actor systems

โœ‰๏ธ

state 1

โœ‰๏ธ

state 2

โœ‰๏ธ

โœ‰๏ธ

processing...

state ??

Right on queue

Actor mailboxes

โœ‰๏ธ

โœ‰๏ธ

โœ‰๏ธ

โœ‰๏ธ

Right on queue

Actor mailboxes

โœ‰๏ธ

state 1

โœ‰๏ธ

โœ‰๏ธ

โœ‰๏ธ

Right on queue

Actor mailboxes

โœ‰๏ธ

state 1

โœ‰๏ธ

state 2

โœ‰๏ธ

โœ‰๏ธ

Right on queue

Actor mailboxes

โœ‰๏ธ

state 1

โœ‰๏ธ

state 2

โœ‰๏ธ

โœ‰๏ธ

state 3

โœ‰๏ธ

Right on queue

Actor mailboxes

โœ‰๏ธ

state 1

โœ‰๏ธ

state 2

โœ‰๏ธ

โœ‰๏ธ

state 3

โœ‰๏ธ

state 4

Right on queue

Actor mailboxes

Getting the role

Actors backstage

Backend applications

Actors onstage

Frontend applications

Rehearsal timeย 

Making an actor

actor.send(message)
actor.subscribe(โ€ฆ)
actor.send({ type: '$subscribe', ref });
// ...
subscribedRefs.forEach(actorRef => {
  actorRef.send({ type: '$snapshot', snapshot });
});
function createActor(idk) {
  // ...

  return {
    send: (event) => {
      // ...
    }
  };
}

const actor = createActor();

actor.send({ type: 'inc' });
function createActor(idk) {
  let state = {
    count: 0
  }; 

  return {
    send: (event) => {
      // ...
      if (event.type === 'increment') {
        state.count++;
      }
    }
  };
}

const actor = createActor();

actor.send({ type: 'inc' });
function createActor(transition) {
  let state = {
    count: 0
  }; 

  return {
    send: (event) => {
      state = transition(state, event);
    }
  };
}

const actor = createActor(/* ... */);

actor.send({ type: 'inc' });
function createActor(logic) {
  let state = logic.initialState;

  return {
    send: (event) => {
      state = logic.transition(state, event);
    }
  };
}

const logic = {
  transition: (state, event) => {/* ... */},
  initialState: {/* ... */}
};

const actor = createActor(logic);

actor.send({ type: 'inc' });

Don't call us, we'll call you

Subscribing to actors

function createActor(logic) {
  let state = logic.initialState;
  const observers = new Set();

  return {
    send: (event) => {
      state = logic.transition(state, event);
      observers.forEach((observer) => {
        observer.next(state);
      });
    },
    subscribe: (observer) => {
      observers.add(observer);

      return () => {
        observers.delete(observer);
      };
    }
  }; 
}

const logic = {
  transition: (state, event) => {
    if (event.type === 'inc') {
      return { ...state, count: state.count + 1 };
    }
    return state;
  },
  initialState: { count: 0 }
};

const actor = createActor(logic);

actor.subscribe((s) => {
  console.log(s);
});

actor.send({ type: 'inc' });
// => { count: 1 }
function createActor(logic) {
  let state = {
    status: 'inactive',
    context: logic.initialContext
  };

  const observers = new Set();

  return {
    send: (event) => {
      state = logic.transition(state.context, event);
      observers.forEach((observer) => {
        observer.next(state);
      });
    },
    subscribe: (observer) => {
      observers.add(observer);

      return () => {
        observers.delete(observer);
      };
    },
    start: () => {
      state.status = 'active';
      observers.forEach((observer) => {
        observer.next(state);
      });
    }
  };
}

const logic = {
  transition: (ctx, event) => {
    if (event.type === 'inc') {
      return { ...ctx, count: ctx.count + 1 };
    }
    return ctx;
  },
  initialContext: { count: 0 }
};

const actor = createActor(logic);

actor.subscribe((s) => {
  console.log(s);
});

actor.start();
// { context: { count: 0 } }

actor.send({ type: 'inc' });
// { context: { count: 1 } }

Special FX

Managing side-effects

function createActor(logic) {
  let state = {
    status: 'inactive',
    context: logic.initialContext
  };

  const observers = new Set();

  const actor = {
    send: (event) => {
      state = logic.transition(
        state.context,
        event,
        actor
      );
      observers.forEach((observer) => {
        observer.next(state);
      });
    },
    subscribe: (observer) => {
      observers.add(observer);

      return () => {
        observers.delete(observer);
      };
    },
    start: () => {
      state.status = 'active';
      observers.forEach((observer) => {
        observer.next(state);
      });
    }
  };

  return actor;
}

const logic = {
  transition: (ctx, event, self) => {
    if (event.type === 'load') {
      const promise = new Promise((res) => {
        setTimeout(() => {
          res({ name: 'David' });
        }, 1000);
      });

      promise.then((output) => {
        self.send({ type: 'resolve', data: output });
      });

      return ctx;
    } else if (event.type === 'resolve') {
      return {
        ...ctx,
        user: event.output
      };
    }
    return ctx;
  },
  initialContext: { user: null }
};

const actor = createActor(logic);

actor.subscribe((s) => {
  console.log(s);
});

actor.start();
// { context: { count: 0 } }

actor.send({ type: 'inc' });
// { context: { count: 1 } }

Guest appearance

Actors in XState

npm i xstate

stately.ai/docs

import { createActor } from 'xstate';
import { someLogic } from './someLogic';

const actor = createActor(someLogic);

actor.subscribe((snapshot) => {
  console.log(snapshot);
});

actor.start();
npm i xstate
import { fromTransition, createActor } from 'xstate';

const counterLogic = fromTransition(
  // Behavior
  (state, event) => {
    if (event.type === 'inc') {
      return {
        ...state,
        count: state.count + 1
      };
    }
    return state;
  },
  // Initial state
  { count: 0 }
);

const counterActor = createActor(counterLogic);
counterActor.subscribe(/* ... */);
counterActor.start();
counterActor.send({ type: 'inc' });
npm i xstate
import { fromTransition, createActor } from 'xstate';

const counterLogic = fromTransition(
  // Behavior
  (state, event) => {
    if (event.type === 'inc') {
      return {
        ...state,
        count: state.count + 1
      };
    }
    return state;
  },
  // Initial state
  // with input
  ({ input }) => ({
    count: input.initialCount
  })
);

const counterActor = createActor(counterLogic, {
  input: {
    initialCount: 100
  }
});
counterActor.subscribe(/* ... */);
counterActor.start();
counterActor.send({ type: 'inc' });
npm i xstate
import { fromPromise, createActor } from 'xstate';
import { fetchUser } from './fetchUser';

const promiseLogic = fromPromise(async ({ input }) => {
  const user = await fetchUser(input.userId);

  return user;
});

const promiseActor = createActor(promiseLogic, {
  input: { userId: 'user42' }
});

promiseActor.subscribe((s) => {
  if (s.status === 'done') {
    console.log(s.output);
  }
});

promiseActor.start();
npm i xstate
import { setup, createActor } from 'xstate';

const counterMachine = setup({
  actors: {
    promiseLogic,
    counterLogic
  }
}).createMachine({
  initial: 'gettingUser',
  states: {
    gettingUser: {
      invoke: {
        src: 'promiseLogic',
        input: {/* ... */},
        onDone: {
          target: 'counting'
        }
      }
    },
    counting: {
      invoke: {
        id: 'counter',
        src: 'counterLogic',
        input: {
          initialCount: 0
        },
        onSnapshot: {
          target: 'reachedMaxCount',
          guard: ({ event }) => {
            return event.snapshot.context.count === 10;
          }
        }
      },
      on: {
        inc: {
          actions: sendTo('counter', { type: 'inc' })
        }
      }
    },
    reachedMaxCount: {
      type: 'final'
    }
  }
});

const counterActor = createActor(counterMachine);

counterActor.subscribe((s) => {
  console.log(s);
});

counterActor.start();

counterActor.send({ type: 'inc' });
npm i xstate
actor.send(anEvent);

actor.subscribe(s => {ย โ€ฆย });

actor.getSnapshot();

actor.start();

Visual FX

Visualizing actors

npm i @statelyai/inspect

import { setup, sendTo, assign, fromCallback } from "xstate";

export const machine = setup({
  types: {
    context: {} as {
      cardNumber: string;
      progress?: number;
    },
    events: {} as
      | { type: "card.read" }
      | { type: "dispenser.done" }
      | { type: "user.select" }
      | { type: "card.valid" }
      | { type: "card.notEnoughCredits" }
      | { type: "user.insertCard" }
      | { type: "dispenser.dispensing" },
  },
  actors: {
    card: fromCallback(({ sendBack, receive }) => {
      // Read card
      // Validate card
    }),
    dispenser: fromCallback(({ sendBack, receive }) => {
      // Dispense coffee
      // Report dispenser status
    }),
  },
}).createMachine({
  id: "coffee",
  context: {
    // ...
  },
  invoke: [
    {
      src: "card",
      id: "card",
    },
    {
      src: "dispenser",
      id: "dispenser",
    },
  ],
  initial: "idle",
  states: {
    idle: {
      on: {
        "user.insertCard": {
          actions: sendTo("card", ({ event }) => ({
            type: "cardEntered",
          })),
        },
        "card.read": {
          target: "cardInserted",
        },
      },
    },
    cardInserted: {
      on: {
        "card.valid": {
          target: "selecting",
        },
        "card.notEnoughCredits": {
          target: "error",
        },
      },
    },
    selecting: {
      on: {
        "user.select": {
          target: "readyToDispense",
          actions: sendTo("dispenser", { type: "dispense" }),
        },
      },
    },
    error: {
      after: {
        500: {
          target: "idle",
        },
      },
    },
    readyToDispense: {
      on: {
        "dispenser.dispensing": {
          target: "dispensing",
        },
      },
    },
    dispensing: {
      on: {
        "dispenser.done": {
          target: "finished",
        },
        "dispenser.progress": {
          actions: assign({
            progress: ({ event }) => event.progress,
          }),
        },
      },
      exit: assign({ progress: undefined }),
    },
    finished: {
      after: {
        1000: {
          target: "idle",
        },
      },
    },
  },
});
npm i xstate

Stunt doubles

Location transparency

Understudies

Fault tolerance

Actor critics

Pros and cons

  • Easy to scale
  • Fault tolerance
  • Location transparency
  • No shared state
  • Event-driven
  • "Microservice hell"
  • Learning curve
  • Indirection
  • Unfamiliarity

Talk to my agent

Actors and AI Agents

What is an agent?

Performs tasks

Observes

Receives feedback

โ†’ accomplish goal

โ†’ learn environment

โ†’ improve over time

Agents are actors

Message passing

Internal state

Spawning actors

Prompts & observations

Memory (short/long-term)

Multi-agent architecture

npm i @statelyai/agent@beta

(it's completely open-source)

import { createAgent } from '@statelyai/agent';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
import { todosMachine } from './todosMachine';

const agent = createAgent({
  model: openai('gpt-4o'),
  name: 'todos',
  events: {
    'todo.add': z.object({ โ€ฆ }).describe('Adds a new todo'),
    // โ€ฆ
  }
});

// ...

const plan = await agent.decide({
  goal,
  state,
  machine: todosMachine,
});

plan?.nextEvent;
// {
//   type: 'todo.add',
//   ...
// }











AGENT

npm i @statelyai/agent@beta

AGENT

npm i @statelyai/agent@beta
Demo

The actor model is an
intuitive way to model
complex systems ๐ŸŽญ

Let's go have a coffee.

END SCENE

Thank you JSConf Budapest!

JSConf Budapest 2024

David Khourshid ยท @davidkpiano
stately.ai

The actor model, behind the scenes

By David Khourshid

The actor model, behind the scenes

  • 305