0:00 Kent Bull | KERIpy Async Architecture | KERI Conference 2026
0:03 This is a developer focus
0:07 description for the async, the runtime
0:10 foundation of KERIpy.
0:12 And what this is that's essentially
0:13 what's the process, what's the execution
0:15 model, how does this run?
0:17 And
0:19 if I'm going to read the source code,
0:20 what do I need to know
0:23 in order for me to effectively
0:24 contribute
0:26 to the KERIpy code base.
0:28 And to learn it or to extend it.
0:30 And hopefully by the end of this
0:32 presentation you will have enough of a
0:34 mental map and a mental model to know
0:36 how to effectively talk to Claude
0:38 or Codex or another AI in case you need
0:41 to go and relearn this later to
0:43 concretize your own learning, to anchor
0:44 it in
0:46 or to potentially extend it.
0:49 We're going
0:51 to go over a lot: What structured
0:53 concurrency is, why it matters, how it's
0:55 superior
0:57 to promises async/await
0:59 callbacks and goto.
1:01 And then some common gotchas for when
1:04 you're trying to use KERIpy and why
1:06 things are done the way they are.
1:08 All right, so
1:10 hello is it's Evan, right? No, Keanu!
1:14 You probably know a
1:15 fair bit of this already.
1:19 I know I've talked to you
1:20 about this before, Joe. I was just
1:22 probably talking to you when we say
1:23 stuff about structured concurrency.
1:26 How many would say they're familiar
1:27 enough with structured
1:28 concurrency to be able to explain it?
1:33 Keanu?
1:34 You work at the KERI Foundation.
1:40 – I think, HIO,
1:41 the only time I've really dove into the
1:45 code, was when we had to port
1:48 into the web
1:51 base there. Because we had to use
1:53 HIO because it was async. It was
1:56 asynchronous so we had to adapt it
1:58 because in the web it was sync, so
2:01 we had to convert it and that's what
2:03 we did in the tutorial,
2:05 but I don't want to
2:07 say something that is completely
2:08 wrong, so ..
2:10 Okay, I see.
2:12 This will be valuable for you
2:14 then because maybe you haven't had as
2:16 much experience with the HIO async
2:17 runtime.
2:19 Great cuz if I even have one
2:21 more programmer
2:23 that really deeply understands HIO
2:25 after this, that will be good because I
2:27 got to get this knowledge out of my
2:28 brain.
2:30 Who's going to ask a question? So I
2:31 think structured concurrency is just
2:33 multi-threading and then how you
2:35 actually come to a
2:37 finality?
2:38 It's a good question. You're going in
2:40 the right direction. It's not
2:41 multi-threading. Okay. Are you familiar
2:43 with Go co-routines?
2:45 – Not in Python. So, it's a lightweight
2:48 co-routine construct from Go lang. I
2:51 just said the Go co-routines.
2:53 One of the reasons why Go became
2:55 so popular is people like the simplicity
2:57 of the mental model with Go co-routines.
2:59 Python co-routines are actually better
3:02 than go co-routines in my opinion, the
3:04 way Sam uses them in HIO.
3:06 If you want to think about what the
3:09 unit the base fundamental unit of
3:11 concurrency in a structured concurrency
3:13 framework is, it's a co-routine
3:17 that is cooperative with the other
3:19 co-routines. Usually a thread ..,
3:21 it's like
3:23 a fundamental
3:25 construct where it doesn't matter if you
3:27 block within one thread and the reason
3:29 why you use threads is because you want
3:31 to have a main thread that's not blocked
3:34 by work that will block that you put to
3:36 another thread.
3:39 If you want to adopt the mindset of
3:42 structured concurrency, you need to
3:44 think from a non-blocking mindset. Often
3:46 it's called a reactive mindset. So zero
3:49 of the co-routines should block.
3:53 And you might wonder, how is that
3:54 possible?
3:55 The way this is implemented at the IO
3:58 level with network IO and file IO
4:01 is in a fundamentally non-blocking way.
4:04 And how do you do that? You
4:05 essentially chop up your IO reads and
4:08 writes into chunks and then you… So,
4:10 it'll be blocking ever so slightly to
4:14 process one chunk and then it'll move
4:15 on.
4:16 But it's fast enough, right?
4:19 Exactly. It doesn't like it's like it
4:20 doesn't really block.
4:22 And not only that, but if you look
4:23 at the low-level socket implementation
4:25 in HIO,
4:27 it's it waits for a chunk and if
4:29 there's an error, it just waits and then
4:31 schedules itself for the next cycle of
4:32 the root scheduler to schedule that
4:34 co-routine again.
4:36 Okay. So,
4:38 we're still at the first two
4:39 slides two slides, so you're good.
4:42 So, we're talking about what structured
4:43 concurrency is. The fundamental unit of
4:45 structured concurrency, which we will
4:46 beat to death
4:48 in the rest of this as a Doer, it's a
4:50 co-routine.
4:52 A non-blocking co-routine. And if you
4:54 look at the readme of HIO that we're
4:56 diving into, it says,
4:58 a waitless hierarchical structured
5:01 cooperative concurrency
5:03 runtime.
5:05 Now, you might say, let's dive deep into
5:06 structured concurrency. Well, let's go
5:08 all the way back to 1968.
5:11 You might have heard of Edgar Dijkstra.
5:14 He's very popular in the computer
5:16 science space. There's a Dijkstra
5:18 pathfinding algorithm and lots of
5:19 other things.
5:20 He wrote an article back in 1968 that
5:23 says, “goto statement considered
5:24 harmful”.
5:26 And it's actually not that long. This is
5:27 the whole article, this page and this
5:29 page.
5:30 And he says a key part statement, the
5:33 unbridled use
5:35 of the goto statement
5:37 has an immediate consequence that it
5:40 becomes terribly hard
5:42 to find a meaningful set of coordinates
5:45 in which to describe the process
5:47 progress. He's trying to say this is
5:49 very difficult to understand from a
5:50 procedural standpoint, where I'm at in my
5:52 procedure if I've got goto statements
5:54 that could take me to arbitrary places.
5:57 And we'll give you a bit of a…
6:00 I'm going to give you a visual here. So,
6:01 this is flowmatic. It's
6:03 like Basic. It's like some of the early
6:05 programming languages and
6:06 this is a jump, but a jump is a goto.
6:09 And it's saying, "Okay, when you get
6:10 down here, if this set of conditions
6:11 happens, go to operation two."
6:14 Well, that seems like it might be okay
6:17 till you get a lot of goto statements.
6:19 And now how do you
6:21 understand what the
6:23 boundaries are here?
6:24 Have you ever heard of solid programming
6:26 principles? Highly cohesive, loosely
6:28 coupled? Where's the loose coupling?
6:30 That looks like tight coupling spaghetti
6:31 code.
6:33 Right? Well, that's where the name
6:34 spaghetti code came from. Right. We
6:36 don't like this. This is bad. This is
6:38 hard on humans. It's hard on LLMs.
6:40 Nobody likes it.
6:42 Now, the reason if we go back to couple
6:44 slides,
6:45 so goto, the go statement in Go for go
6:49 promises, callbacks,
6:51 they're all variations, if you look at
6:53 them closely,
6:55 of a goto statement. And the
6:57 problem is they break abstractions.
7:00 We as programmers endeavor
7:02 persistently
7:04 to have clean abstractions that let us
7:06 reason about the complexity of a
7:08 system that we're modeling. Like Sam
7:09 said earlier yesterday, all models are
7:11 wrong, but some are
7:13 useful.
7:14 All of our models of abstractions of the
7:16 real world are wrong, but they're useful
7:18 to accomplish a certain end. They're
7:20 abstractions.
7:22 We try really hard with software
7:23 architecture to have invariants about
7:26 abstractions, conditions that are true
7:28 no matter where our system is at. And we
7:29 create state machines to ever so
7:34 specifically describe the behavior in
7:36 the states of our system.
7:38 Well, that only works if our
7:40 abstractions are stable.
7:41 Goto breaks abstrations.
7:47 There's a very good article by a man named
7:49 Nathaniel J. Smith, not related to Sam
7:50 Smith,
7:52 where he talks about structured
7:54 concurrency
7:55 in the context of a goto statements in
7:57 in the context of structured
7:58 concurrency. And he talks about…
8:00 That's where this picture comes from.
8:03 He shows you how effectively a promise,
8:05 whether it's a JavaScript promise
8:08 or a
8:10 which is async/await in JavaScript,
8:12 or in Python, where you have async/
8:14 await, they're goto statements.
8:16 They really are.
8:18 And I'm not going to go into the deep…
8:19 So, we have so much to cover today.
8:21 You just go read the
8:22 article. You're going to
8:24 be so happy you read the article. Let me
8:26 go to…
8:27 There's actually a couple of articles
8:28 cuz he talks about what about timeouts
8:30 and cancellations. How do we
8:32 thread that through the code without
8:33 making it explode and blow up and look
8:35 ugly. This right here,
8:36 https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/
8:39 You can
8:40 take a picture now or get it with the
8:41 digest that Kim's going to give
8:42 everybody later [site keri.foundation]. This link is in the
8:44 notes.
8:45 What's fascinating is that Samuel Smith and
8:49 another excellent engineer, Charles
8:52 Lowell, who wrote Effection, which is a
8:53 structured concurrency library in
8:56 JavaScript, also TypeScript, they both
8:59 cite this article [Nathaniel J. Smith] as their inspiration.
9:01 I highly recommend, and in the
9:04 wallet that I put out
9:06 in preparation for yesterday's talk on
9:08 KERIA,
9:09 it's a job TypeScript application. It
9:11 uses Effection structured concurrency.
9:13 And we'll see a little bit of that code
9:14 later in this presentation.
9:16 So, you If you want to understand in
9:18 precise intricate detail why promises,
9:22 callbacks,
9:24 the Go channel, Golang, Go channels are
9:26 different kinds of
9:28 goto statements expresses concurrency
9:30 constructs, read this article. And
9:33 at the end of it, you'll be
9:34 disgusted with the different kind of
9:36 goto statements. And you'll say, "How can
9:37 I repair my abstractions? How can I have
9:40 clean abstractions?" Highly recommend
9:43 this article. You're just going to love
9:44 it. It's kind of long, so you
9:46 get a nice refreshment or give
9:48 yourself an hour or two.
9:51 So, what is structured concurrency?
9:52 Let me jump forward to a couple…
10:03 This is a borrowed diagram from Sam
10:07 Smith.
10:08 So, what's
10:10 interesting, you might say, “Well, let's
10:11 talk about root Python, the async/await.”
10:13 What it is the async HIO has an
10:15 event loop, and the event loop runs any
10:18 function that has async in the function,
10:19 right? So, the way in Python
10:22 you use their native async support
10:24 is it'll just run a
10:26 generator for you. And the way you
10:28 declare a function as a generator is you
10:30 mark it an async function.
10:33 The async function is the only way you
10:34 can run
10:36 This is not HIO. This is just regular
10:37 Python. The only way you can run an
10:39 await statement inside of a function is
10:41 if you declare it
10:43 with async as async def, right? That's
10:46 the signal to the Python runtime to plop
10:49 it into the async HIO event loop,
10:52 so it'll get scheduled. If you don't put
10:54 async in front of it,
10:55 it won't get scheduled, and then it'll
10:57 never get run. Well, now let's take that
10:59 kind of that mental model, and let's do
11:01 the HIO version of it.
11:03 The root scheduler is called a Doist.
11:06 The Doist is what will run all of your
11:09 async tasks. There are two kinds of
11:11 async tasks in HIO.
11:14 There's the DoDoer and the Doer. Now,
11:17 the DoDoer is an aggregation
11:19 construct. You can run many child tasks,
11:23 and it can schedule them. So, the
11:25 DoDoer is like a little bit of a
11:27 combination of a scheduler
11:29 and a task. It has a
11:31 function where it'll perform work and it
11:34 has the ability to add more tasks to
11:36 itself. You'll see a little bit of
11:37 similarity between HIO's structured
11:40 concurrency design and other async
11:42 frameworks like Trio and Curio
11:45 as and Effection as we go on.
11:47 So at a high-level architecture
11:48 diagram, this is what we're dealing
11:50 with. Primarily three concepts.
11:53 The scheduler,
11:54 a task aggregator that's also a task
11:56 itself,
11:57 and a task.
11:59 This is the fundamental unit of
12:00 execution in HIO.
12:03 The Doer. This is your task.
12:05 You can see this like a task group.
12:07 Schedulable task group.
12:13 So the guy who wrote this article about
12:15 fixing this problem.
12:19 Why would we use structured concurrency?
12:21 This slide says a couple of benefits.
12:24 The biggest benefit is my first bullet
12:25 point here. Program flow or task flow
12:29 follows lexical scope. Now, what does
12:31 that mean?
12:34 It means that the execution of the
12:36 program will follow your intuition as a
12:38 programmer.
12:39 You're going to write a function. You
12:40 expect control flow to go from the
12:42 beginning of the function to the end.
12:45 And when that function is over, you
12:47 expect operations you've created in that
12:49 function to have completed. Unless
12:51 you're doing threads or processes, in
12:52 which case you might return a handle
12:56 from the function to sort of go later,
12:58 but structured concurrency, usually the
13:01 mental model is
13:02 if I have a function that
13:04 fires off a bunch of tasks,
13:06 all of those should complete by the time
13:08 the function returns. Okay. And so what
13:10 you're doing is you're turning the
13:11 function
13:13 into a bit of a almost like a scheduler
13:15 of a bunch of subtasks. Are you familiar
13:17 with Erlang and a process supervisor?
13:19 No. Okay. That's a good mental model.
13:21 Erlang or Elixir?
13:23 No. so it's there's a process super
13:25 supervision model.
13:26 But I'll leave that terminology
13:28 aside.
13:29 It'll be clearer as
13:30 I get further into the code examples,
13:32 but essentially
13:33 the idea is that
13:35 if I enter into a function
13:37 and that function has tasks in it,
13:39 there's a guarantee that structured
13:41 concurrency provides that all those
13:43 tasks will finish or be canceled
13:46 by the time that I leave that function.
13:49 What that does is now you don't have any…
13:52 So, the promises break this
13:54 because what happens if
13:59 my outer scope cancels and returns and
14:02 then my promise returns later. Well, now
14:03 it's a different scope. It's a different
14:05 function scope. You might use closures
14:07 or other things to try to transfer state
14:08 to callbacks to promises as a kind
14:10 of a callback.
14:11 But, promises break this. So, what
14:13 essentially what this is saying
14:15 is that
14:16 your function is a kind of abstraction.
14:20 That function
14:22 you're not going to break the boundary
14:23 of that function cuz by the time that
14:25 function returns, everything in it will
14:27 be completed and you'll see that in code
14:28 in just a minute. Another thing,
14:30 so child tasks cannot outlive their
14:33 parent scope.
14:34 A function is like a parent scope.
14:37 All tasks you spawn within that will
14:38 finish before their parent returns. And
14:41 HIO enforces this with the dot done
14:43 attribute. Go ahead.
14:44 – When you say lexical scope, that
14:45 just makes me…
14:47 my brain, I go to the program task flow
14:52 can't outlive. Like, when the variables
14:54 that you declared in that scope go out
14:57 of scope,
14:58 the task outflow is also complete.
15:00 Yes, exactly. Precisely.
15:02 That's right.
15:04 Yes. Now, now that's a common structured…
15:06 – Closures and stuff. Like, you basically
15:08 say you don't have to deal with any of
15:09 that because
15:11 everything happens in context with stuff
15:14 that's in context with it.
15:15 Yeah. You don't have to pass any of that
15:16 state around cuz it's
15:19 you will you just avoid that problem.
15:20 – Yes, that's correct.
15:22 Okay. – This
15:23 make sense, this seems much more simple.
15:25 Exactly.
15:27 That's why people use it.
15:29 That's the whole selling point for
15:31 structured concurrency.
15:32 – But it feels more complicated for some
15:33 reason.
15:35 Well, Venkat Subramaniam, he's one of the most
15:37 popular speakers in the JVM space. He
15:39 says: it's unfamiliar. It's not that it's
15:43 difficult, it's unfamiliar.
15:45 Cuz once you learn structured
15:46 concurrency, you're like, "Oh, that maps
15:48 more onto how my brain thinks."
15:50 So, there's a little bit of upfront
15:51 learning curve, but then you're like,
15:53 "Oh, this is so straightforward. I like
15:55 this."
15:56 – What do you call that concept in the
15:57 Java world?
15:58 Is that a like a task?
16:00 They're trying to do I think
16:02 there's a fiber, if I remember right.
16:03 There's
16:04 JVM 22, I believe, is trying to do some
16:07 integration with some structured
16:08 concurrency, but it's ugly. It's not
16:11 very nice.
16:12 And Pythons and JavaScripts are very,
16:15 very nice.
16:16 HIO is the best Python
16:19 structured concurrency library out
16:21 there. It's not the most usable, but
16:23 as far as the most intuitive, by far the
16:25 most intuitive. David Beazley, a big
16:28 name in the space has one
16:29 called Curio that you'll see referenced
16:31 later. And then Nathaniel Smith has one
16:33 named Trio.
16:35 And then there's Sam's.
16:38 I've looked at all the other ones. Sam's
16:39 is the best.
16:40 It's not the most usable as far as
16:44 like there's nice wrapper
16:45 functions because the other ones have so
16:46 much more adoption, they hit a bunch of
16:48 problems, and there's a little bit more
16:49 wrapper functions to adapt them into the
16:51 async/await stuff. The async/await is a
16:53 dumpster fire. – Does Java have async
16:55 await?
16:57 Or
16:57 – They better never get it. It's a
16:59 dumpster fire. It's too bad that
17:02 it infected Python.
17:04 – It's future. I think
17:05 that's where it is.
17:06 Aasync/await, the
17:07 JavaScript form of async/await, was
17:10 forced upon the world because of the
17:11 limitations of the browser execution
17:13 engine. It was a mistake to put that
17:15 into the Python runtime itself. They
17:17 should have done structured concurrency.
17:19 If there's any mistake that
17:21 Guido made, the benevolent dictator
17:23 for life until he resigned
17:25 in Python, it was putting async/await
17:26 into Python.
17:28 They shouldn't have done that. It's
17:29 really ugly and garish. It works, but
17:32 it's overly simplified.
17:36 Because people were complaining about
17:38 generators. They were saying they're too
17:39 hard.
17:40 But you can
17:42 make frameworks that abstract the hard
17:44 parts and still preserve the structured
17:46 concurrency guarantees. So, Python had
17:48 an opportunity
17:50 the whole language ecosystem had an
17:51 opportunity to do adopt structured
17:53 concurrency and they missed it. And now
17:55 there's libraries.
17:57 Okay, so other important guarantees of
18:00 structured concurrency, if you spawn in
18:02 a lexical scope, you will join before
18:05 that lexical scope returns. And
18:07 in HIO that means
18:09 if you have a DoDoer and you spin up
18:11 some child processes, those will get
18:13 to the done state before the DoDoer
18:16 finishes. So, the Doist
18:18 will own all of its children will
18:20 complete before the Doist returns.
18:25 And some nice consequences of this is
18:29 then we have straightforward execution
18:30 handling. Failure cancellation
18:33 propagation throughout the task tree is
18:34 very intuitive from child tasks up to
18:37 parent tasks.
18:39 And now you can intelligently respond to
18:41 both failures and cancellation signals.
18:44 And there's actually a
18:46 sister article to the one that I
18:47 mentioned earlier, the note the notes on
18:49 structured concurrency goto statement
18:50 considered harmful. There's one on
18:52 timeouts and cancellations. And then
18:53 there's one on control C and interrupts.
18:55 You want to read all three. They're so
18:57 well written.
18:59 Okay, so and exceptions are carried all
19:01 the way up through the scheduling tree.
19:02 This is one of the major benefits of
19:04 structured concurrency, is that you can
19:07 have error propagation all the way up to
19:09 the root scheduler and handle it there
19:11 or in any of the parents. It's an
19:14 intuitive, straightforward way to handle
19:16 exceptions.
19:18 So, consistent, predictable exception
19:19 behavior. Very awesome and
19:20 amazing.
19:22 There's Trio and Nurseries from the
19:24 guy who wrote the article that you're
19:25 going to go read. And this is
19:27 an example from his article…
19:29 So, he uses “async with”, his
19:32 async with is essentially a Doist or a
19:35 DoDoer, just think
19:36 of it as a Doist.
19:39 So, you start one task, which might
19:41 have a child task, which might have a
19:42 child task.
19:44 And all of those complete before the
19:46 outer bounding lexical scope of the
19:48 <i>async with</i> returns.
19:51 And because it's a context manager, that
19:53 means you can have startup and teardown
19:55 logic that will complete predictably for
19:57 all these tasks, for any resources that
19:59 you need to open or close. It's kind of
20:01 Java started to go in this direction
20:03 with the try with resources, with the
20:04 auto-closeable interfaces, but they
20:06 didn't go to fully-structured concurrency
20:08 like this.
20:10 Okay, so I talked about HIO and Doers
20:12 a bit. We went here. We'll sort of
20:13 breeze past this. You already know the
20:14 Doist, Doer, DoDoer. Let me
20:16 double-check our time here. Oh, we've
20:17 only got 19 minutes left.
20:19 So, now we're getting into the HIO
20:21 internals. You get some of the high
20:23 level and an object level, what the
20:25 benefits are. Let's anchor this in
20:26 some code.
20:28 So, we mentioned the Doist is the root
20:29 scheduler, the code routine tasks are
20:31 the Doers and DoDoers.
20:32 We won't get into the details of time.
20:35 It's really fascinating. It's really
20:36 interesting. I highly recommend you read
20:38 the source. I had to read it a couple of
20:39 times.
20:40 And this is before AI came out, by the
20:42 way.
20:43 Now you can ask AI and it'll walk you
20:45 through it much faster than I learned
20:46 it. But you need to
20:48 understand the outer scheduler has
20:51 its own concept of time.
20:54 And it accumulates time.
20:56 You'll see the one sleep in the entire
20:58 system. There's only one "sleep", and it's
21:00 in the Doist, the root scheduler. And it
21:02 keeps track of its own cycle time, so
21:05 that .., and you don't need to worry about
21:06 completely understanding this, but just
21:08 anchored in your brain somewhere, the
21:09 Doist keeps track of time so it can have
21:11 deterministic scheduling of its array of
21:13 tasks.
21:15 In order to make it deterministic, it
21:18 has to keep track of its own sense of
21:19 elapsed time from the start. You always
21:22 start at zero and increase.
21:24 If you keep track of your own time and
21:25 your own sort of talk, essentially your
21:27 your next… how long you're going to
21:29 sleep before you advance to your next
21:32 execution of your array of
21:34 coroutines, you have to have an internal
21:36 concept of time. This came and bit me
21:38 and cost me 40 to 80 dollars in
21:41 in AI tokens with Codex.
21:44 Because it wasn't able to model
21:46 reality and realized that "Oh, HIO has
21:48 its own concept of time" and I actually
21:49 hit that as a bug.
21:50 [laughter]
21:51 So, you realize LLMs are going to
21:53 struggle with structured concurrency.
21:55 And if you don't know how to tell it to
21:56 do the right thing, you might waste tens
21:58 of dollars of tokens like I did. I'm
22:00 glad it wasn't hundreds.
22:01 So, remember, Doist has its own concept
22:03 of time, but we won't get into that
22:04 here. It's another presentation.
22:06 We're going to anchor this
22:08 in with some actual
22:10 commands. And we're going to go through
22:12 the KERIpy code base. If you've used
22:14 KERIpy,
22:15 you've done KLI incept.
22:17 What this does is it calls into what's
22:19 called kli.py this main function. This
22:22 is the entry point for the command line.
22:24 The important part is it says
22:26 runController.
22:27 That's important because it calls the
22:29 root scheduler.
22:30 Here, runController has these three
22:32 lines, defines the tock, which is the
22:34 run rate of the cycle time, how
22:36 quickly the root scheduler iterates over
22:38 all of its tasks.
22:41 Which means this is 1/32 of a second, so
22:43 it'll 32 times a second it'll run its
22:45 entire task list. Unless there's like
22:47 something that takes some time to
22:49 execute. Here's the instantiation of the
22:52 root scheduler. And then we say root
22:54 scheduler.do. This is a life cycle
22:56 function. This is the life cycle
22:58 function that runs the system.
23:01 So, this is the entry point of the HIO
23:03 runtime.
23:05 Okay, we don't have to worry about those
23:06 other parameters just yet. Let's get
23:08 through this.
23:09 And so, here's the whole class outline
23:11 of the Doist. Here's the Do. The Do
23:13 calls all of these other functions.
23:14 These enter, recur, exit.
23:17 So, think if for enter think
23:19 resource allocation and exit resource
23:22 closing. That's what the enter and exit
23:23 context is. There's an enter and an exit
23:27 for every task
23:29 in the HIO ecosystem. So, every Doer has
23:31 an enter and an exit.
23:33 Now, that means it
23:35 can be used as a context manager.
23:37 What does a context manager do?
23:38 What you're saying is
23:42 "Give me a before and an before to
23:44 allocate resources and an after to clean
23:46 up." That's why people use context
23:47 managers or try with resources in Java
23:50 or languages like that, so that you can
23:52 have automated cleanup [of processes, red.]
23:54 And so, what the enter and the exit do
23:57 is it allows you to make arbitrarily
23:58 smart your coroutines.
24:00 Python coroutines by themselves are dumb
24:02 and stupid. You can't add this nice cool
24:04 logic to it. This is a big selling point
24:06 of HIO
24:07 is that your tasks have an enter and an
24:09 exit.
24:10 Just about everything else in the
24:11 space only has a recur.
24:14 Go ahead. – Can you
24:16 implement handlers to those… They're
24:18 essentially events, when you
24:20 enter or when you exit then you can
24:22 attach handlers and then react to it?
24:25 – Think of in terms of
24:27 subclass. You subclass the Doer. You
24:30 override those
24:31 functions. That would be equivalent of
24:33 handlers. Okay, so now let's go deeper
24:35 into the Do function. You'll see.
24:37 So, the red arrows
24:39 are the life cycle functions. Remember I
24:41 said enter, exit, recur?
24:43 Enter.
24:45 Where are we? Recur, I guess I made this
24:47 one special cuz I'm going to go there
24:48 next. There's enter and exit, startup,
24:50 teardown.
24:52 We've got the recur, which we're going
24:53 to jump into later. Here's the only
24:55 sleep in the system. There is one sleep
24:58 in the entire Doist run time. It's this
25:00 right here. This is what accumulates
25:02 time, so essentially starts to
25:04 track the time elapsed to know when to
25:05 schedule the next co-routine.
25:11 One thing to be aware of, that self.that
25:12 enter, what it does is it takes all of
25:15 your tasks and runs enter on every task.
25:18 So, by the time that finishes, all your
25:20 resources have been provisioned for all
25:22 the tasks that are going to run. – After
25:24 that one line? – After that one line, yes.
25:26 And what it does,
25:28 it advances every single
25:30 co-routine to be primed to be ready,
25:32 essentially.
25:34 So, that readies all the tasks for
25:35 execution, and then recur begins the
25:38 infinite while loop to run all of your
25:39 tasks until you get a an abort an
25:42 interrupt signal, which will then
25:44 trigger exit. Like control-C will
25:46 trigger exit, and it'll exit all of the
25:48 child tasks, and then their nested
25:50 tasks, and then it'll finally exit the
25:51 Doist.
25:53 That's one of the cool
25:54 things about structured concurrency.
25:57 Okay, so we're going to jump in. Let's
25:58 jump in. So, now this is recur,
26:00 remember?
26:02 We're going into the blue arrow, or
26:03 the teal green arrow.
26:06 This is the recur function. The main
26:08 So, what actually runs…
26:10 We're still in the scheduler. When are
26:12 we going to get to the task? Well, right
26:14 here, this send. And that send right
26:15 there, that's the Python generator API.
26:19 Cuz remember, the
26:21 co-routines are just generators.
26:24 And the syntax to run a
26:26 generator
26:28 is this "send" right here.
26:30 You can ask
26:32 Claude or Codex about it.
26:34 There's also a really excellent… So,
26:36 Luciano Ramalho, the guy who wrote the
26:38 Python reference manual,
26:40 his 2016 version of the book has a
26:42 really great chapter on co-routines. And
26:44 then when the whole Python language
26:46 community went the wrong direction to
26:47 the oversimplified async/await,
26:49 for the 2022 version of the Python
26:51 reference manual, he did not include
26:53 that awesome content on co-routines, but
26:56 it's on his website. So, you can go to I
26:58 think it's [https://www.fluentpython.com/]
27:00 and you'll find the chapter 16, I think,
27:02 on the co-routines. It'll go
27:04 through that in exhaustively in-depth.
27:05 It'll teach you everything you want to
27:07 know about Python co-routines.
27:08 High-density, high-value content.
27:10 Anyway, so this is what runs it. We
27:12 won't worry about the rest of the stuff.
27:13 So, this interest interestingly enough,
27:15 last note on the root scheduler, this
27:17 tick advances you forward that next 1/32
27:20 of a second. It starts the next time it
27:22 schedules the time sleep, after which
27:26 the next iteration of all the tasks will
27:27 occur.
27:29 Okay, so now let's go into the
27:30 individual task. What it does, that ".send"
27:33 calls .., you might be familiar with
27:35 other Dunder methods, the underscore
27:37 underscore methods in Python.
27:39 It calls the call method of a Doer.
27:42 And what the Doer does is it says
27:44 self.do. So, what that send does,
27:48 this
27:49 the and it's dog, I won't get into the
27:50 syntax here. Dog stands for Doer
27:53 generator. There's some
27:55 Sam-isms here that it takes a little bit
27:56 of time to get accustomed to,
27:59 but this is essentially the generator
28:01 that is the subtask.
28:03 The "dog". So, we're doing the send
28:05 and that eventually results in self.do.
28:07 Now, why is that important?
28:09 The Doer.do is the entry point into the
28:12 life cycle of one task. And let's
28:15 look at that. Here's the Doer.do.
28:17 So, look at that. We've got an enter,
28:19 exit, and a couple of other life cycle
28:21 functions for different reasons that we
28:23 don't have time to get into, but
28:25 there's a <i>clean</i>, there's an
28:26 <i>abort</i>, there's a <i>cease</i>.
28:28 And the important thing is
28:30 that
28:31 Okay, you see the <i>enter</i> and the <i>exit</i>,
28:33 startup teardown, and you see the <i>recur</i>.
28:35 So, self.recur. This will actually
28:37 execute the self, but there's another
28:39 important thing
28:41 that you need to know a little bit about
28:42 Python generators, to know how this
28:44 works.
28:45 See how we say isgeneratorfunction,
28:47 Self.recur.
28:48 What that means is if you have a yield
28:50 statement, this is how Python works. If
28:53 you put a yield or a yield from in a
28:54 function body,
28:56 it turns that into a generator.
28:58 If you're going to do anything with
29:00 IO, the network, files,
29:02 that's usually where
29:05 you're going to have to wait. And
29:08 usually you use some sort of yield or
29:09 yield from or async/await. You need to
29:11 drive that
29:13 in a way to where it can be paused and
29:15 resumed later.
29:16 That's what generators are used for in
29:17 Python. So, you need to use yield from
29:20 syntax to drive the generator function.
29:22 The generator function will stay inert
29:24 and it'll never execute unless you have
29:26 a "yield from."
29:28 So, if you have a recur function in a
29:30 task that needs to call some business
29:32 logic that's going to execute like a
29:33 network call or a file call, you need
29:36 this yield from. This is what runs ..,
29:39 you can put synchronous logic in here,
29:41 in your recur, and it'll
29:43 execute this line, but when you
29:44 have async logic, it'll
29:47 call the yield from.
29:48 Now, let's we're going really fast and
29:50 we I know we only have 10 minutes left,
29:52 so I'm going to do my best to just keep
29:53 on going.
29:54 If it's uncomfortable, if you don't
29:56 aren't quite tracking, it's okay. Let's
29:59 get to the end and then we'll get
30:00 to some questions.
30:02 So, we went to the from the Doist.do
30:06 to the recur
30:08 of the main event
30:09 loop or scheduler. And now we're going
30:12 to the individual task do. And the
30:14 individual task do is taking us to the
30:16 recur of the task.
30:19 So, and you'll see it's empty except for
30:21 return false. You have to subclass it if
30:23 you want to do work, right? So, the
30:25 the main Doer construct is almost
30:27 like an abstract class in the sense that
30:29 you need to subclass it, override the
30:31 recur, and that's where you call your
30:33 business logic function.
30:35 And the false there is interesting. So,
30:37 if you return false, it'll keep
30:39 running until you return true. Because
30:42 the way that you end something is you
30:43 return true cuz it's the done or not
30:45 status. So, return true sets done to
30:48 true in the root scheduler for that
30:50 task.
30:51 That's why the default is to return
30:52 false.
30:54 Now, this is an example
30:56 from the KERIpy code base. If you've
30:57 used KERI it all, you probably have to
30:59 deal with database migrations. We have a
31:01 Doer that does database migrations. We
31:04 have a recur.
31:06 We open up the database.
31:09 And then after that, so really this
31:10 database stuff should be in an enter
31:12 function. Whoever wrote this wasn't
31:14 trying to fully embrace the life cycle
31:17 functions. This is set up that really
31:19 should be
31:20 in a
31:22 What do you call it? And you notice,
31:24 where's the cleanup?
31:27 We need an enter and an exit function.
31:28 So, this is not as good as it could
31:30 be, but it's sort of sufficient for now.
31:32 So, we run all this business logic, we
31:33 run database migrate, return true.
31:36 Runs the database migrations, return
31:38 true, and it's done.
31:39 So, this is a basic task. If I do
31:42 "kli migrate run"
31:44 this will trigger this.
31:46 It'll migrate your database version.
31:48 Now, if you're going to look through the
31:49 rest of the KERIpy code base, you
31:50 might say, "Why do we use so many
31:53 DoDoers, but not many Doers?"
31:56 And it's because
31:58 of one function, doing.doify.
32:03 That's kind of funny.
32:05 Sam has a really
32:07 fascinating naming policy.
32:09 But what doify does,
32:11 it essentially allows you to use yield
32:13 from in the body of a function. Remember
32:15 how I said structured concurrency says,
32:19 "Program flow follows lexical scope."
32:23 The reason why doify is used is because
32:26 it allows that sort of intuitive program
32:28 flow to follow lexical scope
32:30 contract to be met. And it's easy to
32:33 essentially schedule a lot of subtasks
32:34 in one lexical context that makes
32:36 intuitive sense.
32:38 And so, I'll give you an example here.
32:41 Oh, so and actually there's a
32:42 difference. So, this is the DoDoer
32:43 function. You'll notice there's a
32:44 critical difference. So, the reason why
32:46 you use doify is because there's there's
32:49 something that
32:50 I think is my opinion it's missing
32:52 from DoDoer.
32:54 I think Sam argues differently. He
32:55 argues for a different or
32:57 way to arrange things.
32:59 Personally, I think that the way that we
33:01 use doify
33:03 is evidence
33:05 that we should add the missing part
33:07 here. See here we have self.recur, but
33:09 we don't have the generator check.
33:10 Remember the generator check? If I go
33:12 one slide over, here's the Doer.
33:15 We say if is generator yield from. So,
33:17 what this means is a DoDoer will not
33:19 allow you to use yield from in its
33:21 recur. That's why we use doify all the
33:24 time. And Sam has this argument about
33:26 hierarchical state machines is the right
33:28 place to use state to essentially
33:30 manage multiple sets of Doers, and then
33:31 you have a scheduler Doer that manages
33:33 other Doers as a set of Doers within a
33:35 DoDoer. There's an argument there, but
33:37 I don't think it's very intuitive.
33:39 I understand the argument. Maybe I just
33:41 need to maybe I'm just unfamiliar and I
33:43 need to learn that. But to me, the fact
33:45 that we use doify so much is
33:46 evidence that we should add
33:50 this is generator
33:52 in the Doer and see now we're in DoDoer
33:54 right above here. We should put that is
33:57 generator check and run it that way in
33:58 my opinion. And you'll see why. So, now
34:00 let's get to the code. But notice the
34:02 DoDoer has the same life cycle functions.
34:05 Enter, exit, clean abort sees, and we
34:08 call recur.
34:10 So, the DoDoer
34:13 And you'll notice this return self.done
34:15 for when a task is done, true or false.
34:17 The difference between the DoDoer and
34:19 the Doer that is the DoDoer you can add
34:22 it's extend or remove. You can add tasks
34:24 to it or remove tasks to it. So, it's an
34:26 aggregation construct. The Doer is not.
34:30 So, you can see how the DoDoer is a bit
34:33 of a combination between a scheduler
34:36 and a task, but the Doer is just a task.
34:44 There's an example. So,
34:45 back to the [inaudible] incept.
34:47 Inside of the init function for the
34:49 Incept Doer, what you'll notice is a
34:50 DoDoer. We call doing.doify on the
34:53 main function that runs everything.
34:56 What that does
34:58 Here's the incept do. You see we have a
34:59 yield from yield from. It's
35:03 pretty intuitive to co-locate a lot of
35:05 that logic. Rather than create a
35:06 complicated state machine and worry
35:08 about the state of this Doer and that
35:10 Doer and sort of schedule them at the
35:11 right time, we just locate them in the
35:12 same lexical scope.
35:15 That's pretty easy for humans to
35:17 understand that program flow.
35:20 That's why it's so handy. That's why in
35:21 my opinion
35:23 this really should be a recur
35:25 function, and the recur should allow you
35:27 to use these yields and yields from
35:29 in the body
35:30 of the DoDoer's recur.
35:32 That's not how it works. I actually did
35:35 go through the trouble
35:36 in the DID:webs repository in our tests
35:39 to do it the way that Sam recommended,
35:40 and it's kind of interesting. It does
35:41 make you clean up your code a little bit…
35:45 There's an argument there. And what it
35:46 does is essentially the mental model is
35:48 if you're going to do things the right
35:49 idiomatic way according to Sam's
35:52 idea, every place where you have a yield
35:54 from should essentially be a
35:56 separate Doer.
35:58 You have at least one, two,
36:00 possibly three more here. And what you
36:02 do is you would add all of those…
36:05 See how we have a list of
36:07 Doers here, and then we send it when we
36:08 say
36:09 call the constructor we init Doers
36:11 equals Doers.
36:12 What you would do instead of having all
36:13 those yield froms in one function, you
36:15 would make separate Doers for that sub
36:17 part of the task, and you'd have that be
36:20 part of the task array that you send to
36:21 yourself when you're scheduling
36:23 yourself.
36:24 You can see it sort of force
36:26 you to lift things out, and make
36:27 dependencies more explicit. It's kind of
36:28 like smaller functions versus larger
36:30 functions. So it is more hygienic in
36:31 some senses, a little bit more
36:32 convoluted. So it's a bit of a
36:34 trade-off.
36:37 Now I'm just mention Effection. So
36:39 Effection is a JavaScript structured
36:41 concurrency library. Now that you
36:43 understand
36:45 the yield from syntax and the yield and
36:47 whatever, now you can see what it looks
36:48 like in JavaScript cuz JavaScript has a
36:50 construct that's like yield from.
36:53 If you see yield star in JavaScript,
36:55 it's the same thing. It runs a
36:57 co-routine
36:59 from that spot. So this is using the
37:01 Effection
37:03 structured concurrency library
37:05 just like how we did
37:06 doing.doify.
37:08 This is part of why I lean in favor of
37:10 making DoDoer.recur do allow yield from
37:13 because in Effection in the structured
37:15 concurrency library in TypeScript /
37:17 JavaScript
37:18 this whole function is like a doified
37:20 function.
37:21 You can have as many yield stars,
37:24 which are yield froms, as you want and
37:26 just sort of naturally proceed forth in
37:27 an execution chain in one function.
37:30 You'll see I'm saying yield from
37:31 list identifier service and we'll look
37:33 at that one layer deeper.
37:35 This actually it wraps the Signify
37:37 client. You'll see it has a Signify
37:38 client right here.
37:39 This is the wrapper. This is another
37:42 thing that I think is sort of missing
37:43 from HIO that we could add to it.
37:45 Wouldn't be that hard to add it. Sam has
37:46 actually been experimenting with the
37:48 wrappers.
37:49 More recently with trying to
37:51 integrate async/await into HIO. It's
37:54 already done in the Effection.
37:56 We say call promise and turn it into a
37:58 generator.
37:59 This allows you to preserve the
38:00 structured concurrency guarantees
38:03 even when you use promises in
38:04 JavaScript. You'll have promises at sort
38:06 of the edge of your architecture and all
38:08 the nice stuff in between you'll have
38:09 your structured concurrency guarantees.
38:11 – Just call a library you're importing
38:12 that you can't control.
38:13 – Exactly. Precisely. And
38:17 if you look at the wallet that I demoed
38:18 yesterday for my… I need a really
38:20 need to wrap up. Oh.
38:22 Okay, let me just run through the end.
38:23 Okay, the last thing is…
38:25 Look just look at these in the slides
38:26 later. You'll see this weird
38:28 convention used everywhere in KERIpy
38:30 the self.wind, self.talk and yield. And what
38:33 it is there's something with all
38:34 generators in JavaScript and in Python
38:35 where you need to prime it. Prime it
38:37 means advance to the first yield. You
38:39 don't want the priming of your generator
38:41 to run business logic.
38:44 You don't want it to come down here, so
38:45 you put this yield at the front. I think
38:47 that should be handled by a framework in
38:48 my opinion.
38:50 It's kind of weird to force people to
38:51 manage that, but that's why this exists.
38:53 And
38:55 Okay, and I would say there's
38:59 Oh, we'll cut it there.
39:02 That's HIO.
39:04 [applause]