JSConf Budapest 2024
David Khourshid ยท @davidkpiano
stately.ai
Carl Hewitt, Peter Bishop, Richard Steiger
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
๐โ
โ๏ธ
โ๏ธ
๐ค 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
โ๏ธ
๐ค 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'
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
โ๏ธ
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
๐
๐
{
orders: [/* ... */],
inventory: {
// ...
},
sales: 134.65
}
โ๏ธ
state 1
โ๏ธ
state 2
โ๏ธ
โ๏ธ
processing...
state ??
โ๏ธ
โ๏ธ
โ๏ธ
โ๏ธ
โ๏ธ
state 1
โ๏ธ
โ๏ธ
โ๏ธ
โ๏ธ
state 1
โ๏ธ
state 2
โ๏ธ
โ๏ธ
โ๏ธ
state 1
โ๏ธ
state 2
โ๏ธ
โ๏ธ
state 3
โ๏ธ
โ๏ธ
state 1
โ๏ธ
state 2
โ๏ธ
โ๏ธ
state 3
โ๏ธ
state 4
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' });
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 } }
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 } }
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();
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
Easy to scale
Fault tolerance
Location transparency
No shared state
Event-driven
"Microservice hell"
Learning curve
Indirection
Unfamiliarity
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
JSConf Budapest 2024
David Khourshid ยท @davidkpiano
stately.ai