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
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