| Safe Haskell | None |
|---|---|
| Language | GHC2024 |
Circuit.Agent
Contents
- Posts and the log
- Pure agents
- Inbox
- Delivery
- Effectful ends (symmetric streams)
- Agent as Shard (change of base into K)
- Token seat (stream buffers around a Shard)
- Forces
- Derivations
- Session assembly
- Running one step
- Effectful agents
- STM agents (S) and IO-bound agents (X)
- Container dial (Bag / Seq log algebra)
- Behaviour (stream semantics)
- Choice (level-1 grammar fragment)
- Seat-level tensors (product await, coproduct race)
- Re-exports for end construction
- Shard combinators
Description
Moore agents on a shared, addressed log; opaque shards for effects.
Pure agent shape:
type Agent arr s a b = System arr s (Mono a b)
Free carrier s is required by the pretense (tape vs summary). The common
log case is Agent (->) [Post] Post [Post] — state is the received stream.
That carrier is a parse of the tape: each committed Post is one token.
Effectful boundary (preferred pin):
type Shard m a b = Ends (K m) a b
Symmetric in the common log case: commit a list of posts, emit a list of
posts. One-post reality (hit enter) is not another type — lift with (:[])
on the commit side (prefixIn). LogEnds is the same shape (dual seat on
the log). Opacity is commit/emit only; no interior.
Change of base (circuits-parser sense): agentShard reinterprets a pure
Agent at K m ends — same Moore citizen, effectful interface.
Direct shards (hermes session, muster-agent) skip the pure coalgebra and
inhabit Shard only.
Token seat around a list shard (parser dual): stream f = [Post], token
s = Post. batchEnds snocs tokens into a stream (build f); unbatchEnds
peels with the same coalgebra as Uncons on lists. Compose with the
shard via (:>:>)
portShard = batchEnds … >:> shard >:> unbatchEnds … :: Shard m Post [Post] >:> Shard m [Post] [Post] >:> Shard m [Post] Post :: Port m
Queue ends (openSTM /openIO) are the effectful token wire of the same
shape when you need a bare Post/Post@ channel without a shard.
Design card: coffee/loom/agent.md.
Synopsis
- data Post a = Post {}
- type PostId = Natural
- mkPost :: Name -> [Name] -> a -> Post a
- replyTo :: Name -> PostId -> Post a -> b -> Post b
- synthesis :: Name -> [Name] -> [PostId] -> b -> Post b
- sortNub :: Ord a => [a] -> [a]
- indexToIdMap :: [Post a] -> Map PostId (Post a)
- branches :: Map PostId (Post a) -> Post a -> [[Name]]
- branchesByIndex :: [Post a] -> Post a -> [[Name]]
- cone :: Map PostId (Post a) -> Post a -> [Name]
- coneByIndex :: [Post a] -> Post a -> [Name]
- type Log (f :: k) = f
- emptyLog :: forall a f. Cons f (Post a) => Log f
- type Name = Text
- type Agent (arr :: Type -> Type -> Type) s a b = System arr s (Mono a b)
- data AgentState s f = AgentState {}
- emptyAgentState :: forall a s f. (Snoc s (Post a), Uncons f (Post a)) => [Name] -> AgentState s f
- tape :: ([i] -> o) -> Agent (->) [i] i o
- selfrec :: ([i] -> i) -> Agent (->) [i] i i
- data Inbox f
- emptyInbox :: forall a f. Uncons f (Post a) => [Name] -> Inbox f
- appendInbox :: Snoc f (Post a) => Post a -> Inbox f -> Inbox f
- unconsInbox :: forall a f. Uncons f (Post a) => Inbox f -> These (Post a) (Inbox f)
- inboxWho :: Inbox f -> Name
- inboxSubs :: Inbox f -> [Name]
- deliversTo :: Post a -> [Name] -> Bool
- type Shard (m :: k1 -> Type) a (b :: k1) = Poles (K m) a b
- type LogEnds (m :: k1 -> Type) a (b :: k1) = Shard m a b
- shard :: Monad m => (a -> m ()) -> m a -> Shard m a a
- logEnds :: Monad m => (a -> m ()) -> m a -> LogEnds m a a
- data AgentSeat s a = AgentSeat {}
- feedAgent :: Agent (->) s (Post a) [Post a] -> [Post a] -> AgentSeat s a -> AgentSeat s a
- flushOutbox :: AgentSeat s a -> ([Post a], AgentSeat s a)
- agentShard :: Monad m => m (AgentSeat s a) -> (AgentSeat s a -> m ()) -> Agent (->) s (Post a) [Post a] -> Shard m [Post a] [Post a]
- runAgentShard :: Agent (->) s (Post a) [Post a] -> AgentSeat s a -> [Post a] -> ([Post a], AgentSeat s a)
- type Port (m :: Type -> Type) a = Poles (K m) (Post a) (Post a)
- class Snoc f s where
- snocPost :: [Post a] -> Post a -> [Post a]
- batchEnds :: forall f s m. (Monad m, Snoc f s) => m f -> (f -> m ()) -> Shard m s f
- unbatchEnds :: forall f s m. (Monad m, Semigroup f, Uncons f s) => m f -> (f -> m ()) -> Shard m f s
- portShard :: Monad m => m [Post a] -> ([Post a] -> m ()) -> m [Post a] -> ([Post a] -> m ()) -> Shard m [Post a] [Post a] -> Port m a
- watch :: forall a f. Uncons f (Post a) => [Name] -> Log f -> [Post a]
- post :: Cons f (Post a) => Post a -> Log f -> Log f
- turn :: forall a s f. (Cons f (Post a), Uncons f (Post a)) => Agent (->) s (Post a) [Post a] -> AgentState s f -> Log f -> (AgentState s f, Log f, Maybe (Derivation a))
- turnAs :: forall a s f. (Cons f (Post a), Uncons f (Post a)) => Name -> Agent (->) s (Post a) [Post a] -> AgentState s f -> Log f -> (AgentState s f, Log f, Maybe (Derivation a))
- hasPending :: forall a s f. Uncons f (Post a) => AgentState s f -> Bool
- loop :: forall a s f. (Snoc s (Post a), Snoc f (Post a), Cons f (Post a), Uncons f (Post a)) => [(Name, Agent (->) s (Post a) [Post a])] -> Log f -> ([(Name, AgentState s f)], Log f, [Derivation a])
- loopSubs :: forall a s f. (Snoc s (Post a), Snoc f (Post a), Cons f (Post a), Uncons f (Post a)) => [RosterEntry s a] -> Log f -> ([(Name, AgentState s f)], Log f, [Derivation a])
- loopWith :: forall a s f. (Snoc f (Post a), Cons f (Post a), Uncons f (Post a)) => [(Name, Agent (->) s (Post a) [Post a])] -> [(Name, AgentState s f)] -> Log f -> ([(Name, AgentState s f)], Log f, [Derivation a])
- loopWithSubs :: forall a s f. (Snoc f (Post a), Cons f (Post a), Uncons f (Post a)) => [RosterEntry s a] -> [(Name, AgentState s f)] -> Log f -> ([(Name, AgentState s f)], Log f, [Derivation a])
- loops :: forall a s f. (Snoc f (Post a), Cons f (Post a), Uncons f (Post a)) => [(Name, Agent (->) s (Post a) [Post a])] -> [(Name, AgentState s f)] -> Log f -> [([(Name, AgentState s f)], Log f, [Derivation a])]
- loopsSubs :: forall a s f. (Snoc f (Post a), Cons f (Post a), Uncons f (Post a)) => [RosterEntry s a] -> [(Name, AgentState s f)] -> Log f -> [([(Name, AgentState s f)], Log f, [Derivation a])]
- loopHetero :: forall a s f. (Snoc f (Post a), Cons f (Post a), Uncons f (Post a)) => [(Name, s, Agent (->) s (Post a) [Post a])] -> Log f -> ([(Name, AgentState s f)], Log f, [Derivation a])
- loopHeteroSubs :: forall a s f. (Snoc f (Post a), Cons f (Post a), Uncons f (Post a)) => [(Name, s, [Name], Agent (->) s (Post a) [Post a])] -> Log f -> ([(Name, AgentState s f)], Log f, [Derivation a])
- meetingLoop :: forall a s f. (Snoc f (Post a), Cons f (Post a), Uncons f (Post a)) => [(Name, Agent (->) s (Post a) [Post a])] -> Trace Either (->) ([(Name, AgentState s f)], Log f, [Derivation a]) ([(Name, AgentState s f)], Log f, [Derivation a])
- meetingLoopSubs :: forall a s f. (Snoc f (Post a), Cons f (Post a), Uncons f (Post a)) => [RosterEntry s a] -> Trace Either (->) ([(Name, AgentState s f)], Log f, [Derivation a]) ([(Name, AgentState s f)], Log f, [Derivation a])
- seedAgentState :: forall a s f. (Snoc s (Post a), Snoc f (Post a), Uncons f (Post a)) => [Name] -> Log f -> AgentState s f
- type RosterEntry s a = (Name, [Name], Agent (->) s (Post a) [Post a])
- data Derivation a = Derivation {}
- session :: Uncons f (Post a) => [Name] -> Log f -> [a]
- run1 :: Agent (->) s i o -> s -> i -> (o, s)
- agentM :: forall (m :: Type -> Type) s a b. Applicative m => Agent (->) s a b -> Agent (K m) s a b
- runAgentM :: Monad m => Agent (K m) s a b -> s -> a -> m (b, s)
- type AgentS s a = Agent (K STM) s a [a]
- type AgentX s a = Agent (K IO) s a [a]
- agentX :: AgentS s a -> AgentX s a
- awaitS :: AgentS s1 a -> AgentS s2 a -> AgentS (s1, s2) a
- raceS :: AgentS s1 a -> AgentS s2 a -> AgentS (s1, s2) a
- raceIO :: AgentX s1 a -> AgentX s2 a -> AgentX (s1, s2) a
- stepS :: AgentS s a -> s -> a -> STM (s, [a])
- stepsS :: AgentS s a -> s -> [a] -> STM (s, [a])
- runAgentS :: AgentS s a -> s -> [a] -> IO ([a], s)
- readEndSTM :: Poles (K STM) a a -> STM a
- writeEndSTM :: Poles (K STM) a a -> a -> STM ()
- agentLoopS :: AgentS s a -> s -> Poles (K STM) a a -> Poles (K STM) a a -> STM s
- selfLoopS :: AgentS s a -> s -> Poles (K STM) a a -> STM s
- agentLoopL :: AgentS s a -> Poles (K STM) [a] [a] -> Poles (K STM) [a] [a] -> Trace Either (K STM) s s
- selfLoopL :: AgentS s a -> s -> Poles (K STM) [a] [a] -> STM s
- newtype Bag a = Bag (Map a Int)
- type TurnLog a = Seq (Bag a)
- emptyBag :: Bag a
- singletonBag :: a -> Bag a
- insertBag :: Ord a => a -> Bag a -> Bag a
- toBag :: Ord a => [a] -> Bag a
- fromBag :: Bag a -> [a]
- type Beh a = [Post a] -> [Post a]
- beh :: Agent (->) s (Post a) [Post a] -> s -> Beh a
- after :: System (->) s (Mono i o) -> s -> [i] -> s
- branchAgent :: (s -> Bool) -> Agent (->) s a b -> Agent (->) s a b -> Agent (->) s a b
- awaitA :: Agent (->) s1 (Post a) [Post a] -> Agent (->) s2 (Post a) [Post a] -> Agent (->) (s1, s2) (Post a) [Post a]
- raceA :: Agent (->) s1 (Post a) [Post a] -> Agent (->) s2 (Post a) [Post a] -> Agent (->) (s1, s2) (Post a) [Post a]
- data Poles (arr :: k -> k1 -> Type) (a :: k) (b :: k1) = Poles {}
- close :: forall {k} arr (a :: k). In arr a -> Out arr a -> arr a a
- polesK :: Monad m => (a -> m ()) -> m b -> Poles (K m) a b
- prefixIn :: forall {k} arr (a :: k) (b :: k). Category arr => arr a b -> In arr b -> In arr a
- data Queue a
- data ChannelPolicy a
- = Linear
- | SingleSlot
- | SwapOne
- | LatestValue a
- | BoundedN Int
- | NewestN Int
- openChannel :: ChannelPolicy a -> IO (Poles (K IO) a a)
- openChannelSTM :: ChannelPolicy a -> STM (Poles (K STM) a a)
- openLinearChannel :: IO (Poles (K IO) a a)
- openLinearChannelSTM :: STM (Poles (K STM) a a)
- data HaltChannel (p :: ChannelPolicy a) where
- HaltChannel :: forall a (p :: ChannelPolicy a). IsLinear p => Poles (K STM) a a -> HaltChannel p
- type family IsLinear (p :: ChannelPolicy a) where ...
- openHaltChannel :: STM (HaltChannel ('Linear :: ChannelPolicy a))
- writeHaltChannel :: forall a (p :: ChannelPolicy a). HaltChannel p -> a -> STM ()
- readHaltChannel :: forall a (p :: ChannelPolicy a). HaltChannel p -> STM a
- openSTM :: Queue a -> STM (Poles (K STM) a a)
- openIO :: Queue a -> IO (Poles (K IO) a a)
- pipeEnds :: Poles (K IO) a b -> (TQueue b -> IO (Poles (K IO) b c)) -> IO (Poles (K IO) a c, IO ())
- prefixShard :: forall (m :: Type -> Type) a' a b. Monad m => (a' -> a) -> Shard m a b -> Shard m a' b
- suffixShard :: forall (m :: Type -> Type) b b' a. Monad m => (b -> b') -> Shard m a b -> Shard m a b'
- codecShard :: forall (m :: Type -> Type) a' a b b'. Monad m => (a' -> a) -> (b -> b') -> Shard m a b -> Shard m a' b'
- composeShard :: forall (m :: Type -> Type) a b c. Monad m => Shard m a b -> Shard m b c -> Shard m a c
- (>:>) :: forall {k} (arr :: k -> k -> Type) (a :: k) (b :: k) (c :: k) (bot :: k). HasDual bot arr => Poles arr a b -> Poles arr b c -> Poles arr a c
Posts and the log
A single entry on the shared log, polymorphic in payload.
Routing is by name list: a post delivers to every agent whose name appears
in to (the audience). thread is the ancestry edges: [] for a root
post, otherwise the PostIds of the posts being replied to or synthesised
from. Parents are a set (duplicates discarded) in normalised (sorted)
order — the smart constructors keep it so.
Instances
| Functor Post Source # | |
| Eq a => Eq (Post a) Source # | |
| Ord a => Ord (Post a) Source # | |
| Show a => Show (Post a) Source # | |
| Cons (Log a) (Stamped a) Source # | Prepend is the dual view: a newest-first stream over the same image. |
| Snoc (Log a) (Stamped a) Source # | Append is the natural operation: one element at the end. |
| Uncons (Log a) (Stamped a) Source # | Read peels the oldest element first. |
type PostId = Natural Source #
Absolute post identity. In the stamped log this is the line id assigned
by the single writer. In pure meeting logs it is the position in the
oldest-first log, but branches and cone resolve by the id itself, not by
position in the passed-in list. Use indexToIdMap to assign ids [0..] from
a chronological list.
replyTo :: Name -> PostId -> Post a -> b -> Post b Source #
A reply: the audience is the parent's sender plus the rest of the
parent's audience (minus self); the sole thread edge cites the parent's
PostId.
synthesis :: Name -> [Name] -> [PostId] -> b -> Post b Source #
A synthesis: one descendant of several parents — the object-level wire-merge dual to merging agents. The ancestry cites every parent id as a normalised set (sorted, duplicates discarded).
sortNub :: Ord a => [a] -> [a] Source #
Sorted, duplicate-free. The normalised-set primitive of the thread design: parent sets, audiences, and cones are all kept in this form.
branches :: Map PostId (Post a) -> Post a -> [[Name]] Source #
The label-branches from a post to its conversation roots, resolved by
exact PostId. The current post is not in the map; its id is inferred as
the id after the largest key in the prior map. Only thread edges strictly
less than the current id are resolved (ancestors must be prior posts). A
dangling or future id is silently ignored. A root post has one trivial
branch; every parent edge contributes its own path. Branches of replies
are pure cons:
branches (indexToIdMap prior) (replyTo who i p b)
map (who :) (branches (Map.filterWithKey (k _ -> k < i) (indexToIdMap prior)) p)
>>>let p1 = mkPost "tony" ["grok"] "hi" :: Post String; p2 = mkPost "grok" ["tony"] "hello"; r = replyTo "kimi" 1 p2 "a" in branchesByIndex [p1, p2] r == map ("kimi" :) (branchesByIndex [p1, p2] p2)True
>>>let p1 = mkPost "tony" ["grok"] "hi" :: Post String; p2 = mkPost "grok" ["tony"] "hello"; r1 = replyTo "kimi" 1 p2 "a"; r2 = replyTo "tony" 2 r1 "b" in branchesByIndex [p1, p2, r1] r2[["tony","kimi","grok"]]
branchesByIndex :: [Post a] -> Post a -> [[Name]] Source #
Convenience wrapper: resolve branches from a chronological list, assigning
ids [0..].
cone :: Map PostId (Post a) -> Post a -> [Name] Source #
The ancestry cone: every name appearing on any branch from a post to its roots, as a normalised set — the "who contributed to this" query, free with the log. Includes the post's own sender.
Cone-union law:
cone (indexToIdMap prior) (synthesis who aud is b)
sortNub (who : concatMap (cone (Map.filterWithKey (k _ -> k < i) (indexToIdMap prior)) . (priorMap Map.!)) is)
>>>let p1 = mkPost "tony" ["grok"] "hi" :: Post String; p2 = mkPost "grok" ["tony"] "hello"; r1 = replyTo "kimi" 1 p2 "a"; prior = [p2, p1, r1] in coneByIndex prior (synthesis "sum" [] [2, 0] "Σ") == sortNub ("sum" : concatMap (coneByIndex prior) [r1, p2])True
>>>let p1 = mkPost "tony" ["grok"] "hi" :: Post String; p2 = mkPost "grok" ["tony"] "hello"; r1 = replyTo "kimi" 1 p2 "a"; prior = [p2, p1, r1] in coneByIndex prior (synthesis "sum" [] [2, 0] "Σ")["grok","kimi","sum","tony"]
coneByIndex :: [Post a] -> Post a -> [Name] Source #
Convenience wrapper: resolve cone from a chronological list, assigning ids
[0..].
Pure agents
type Agent (arr :: Type -> Type -> Type) s a b = System arr s (Mono a b) Source #
Agent: a Moore machine with free carrier, polymorphic in the base arrow.
System arr s (Mono a b) ≅ arr (s, a) (s, b) after collapsing unit
positions. Common log case: Agent (->) s (Post a) [Post a] (input = one post,
output = list of posts). Agent (K m) s a b is the monadic Moore
machine.
data AgentState s f Source #
State for a pure agent in delivery: free carrier plus an addressed inbox.
Constructors
| AgentState | |
Instances
| (Eq s, Eq f) => Eq (AgentState s f) Source # | |
Defined in Circuit.Agent Methods (==) :: AgentState s f -> AgentState s f -> Bool # (/=) :: AgentState s f -> AgentState s f -> Bool # | |
| (Show s, Show f) => Show (AgentState s f) Source # | |
Defined in Circuit.Agent Methods showsPrec :: Int -> AgentState s f -> ShowS # show :: AgentState s f -> String # showList :: [AgentState s f] -> ShowS # | |
emptyAgentState :: forall a s f. (Snoc s (Post a), Uncons f (Post a)) => [Name] -> AgentState s f Source #
Empty carrier and empty inbox for the named agent.
tape :: ([i] -> o) -> Agent (->) [i] i o Source #
Born empty, conses each received input onto its history.
>>>iterateSystem (tape length) [] [1,2,3 :: Int][1,2,3]
selfrec :: ([i] -> i) -> Agent (->) [i] i i Source #
Like tape, but also conses the agent's own output onto its history.
This is the internal-monologue construction: an agent's outputs are on the same log as its percepts, visible to its own future turns.
Inbox
Addressed stream of unread posts for one agent.
The Inbox owns a list of subscribed names and the unread stream. Only
posts whose to list intersects the subscription list are peeled by
unconsInbox. The common case is a singleton subscription [agentName];
multi-cast wires list several names.
Instances
emptyInbox :: forall a f. Uncons f (Post a) => [Name] -> Inbox f Source #
Empty inbox for the subscribed agent(s).
appendInbox :: Snoc f (Post a) => Post a -> Inbox f -> Inbox f Source #
Append a post to the right of the inbox.
unconsInbox :: forall a f. Uncons f (Post a) => Inbox f -> These (Post a) (Inbox f) Source #
Peel the oldest addressed post from the inbox, returning the rest.
Non-matching posts are skipped and discarded. An empty or exhausted inbox
returns That with an empty inbox.
Delivery
deliversTo :: Post a -> [Name] -> Bool Source #
Lightweight delivery predicate.
A post delivers when any of the subscribed names appears in the post's to
list. Multi-cast is direct: a post addressed to several names reaches each
subscriber. A post with to = ["all"] broadcasts to every subscriber;
to = [] and to = [""] deliver to no one (discard). This predicate is a
small stepping stone toward a relational copy/discard delivery model
(FinRel); the full wiring is future work.
Effectful ends (symmetric streams)
type Shard (m :: k1 -> Type) a (b :: k1) = Poles (K m) a b Source #
Opaque effectful ends: commit an a, emit a b.
Common log case: Shard m [Post] [Post]. Pipeline speaks a stream on
commit and emit. Keyboard one-shot:
prefixIn (:[]) -- Post -> [Post] on the conjoint
Emit is an onslaught of posts (empty = quiet / done for that poll).
type LogEnds (m :: k1 -> Type) a (b :: k1) = Shard m a b Source #
Same ends shape as Shard — dual seat on the log (journal 013).
shard :: Monad m => (a -> m ()) -> m a -> Shard m a a Source #
Build a Shard from monadic commit and emit actions.
logEnds :: Monad m => (a -> m ()) -> m a -> LogEnds m a a Source #
Build log ends (same as shard; dual seat).
Agent as Shard (change of base into K)
State behind an agentShard: free carrier plus a pending emit queue.
Commit parses inputs into the carrier and enqueues one output list per input (the Moore step). Emit flushes the queue — empty means quiet.
feedAgent :: Agent (->) s (Post a) [Post a] -> [Post a] -> AgentSeat s a -> AgentSeat s a Source #
Pure parse step: fold committed posts through the coalgebra.
flushOutbox :: AgentSeat s a -> ([Post a], AgentSeat s a) Source #
Take the outbox; leave carrier unchanged.
agentShard :: Monad m => m (AgentSeat s a) -> (AgentSeat s a -> m ()) -> Agent (->) s (Post a) [Post a] -> Shard m [Post a] [Post a] Source #
Reinterpret a pure Agent as a list Shard.
agentShard get put sys :: Shard m [Post] [Post]
is the change of base from (->) (the Moore coalgebra) into
K m ends: commit = parse inputs, emit = flush replies. The
interior stays opaque at the Shard boundary — only [Post] in and out.
get / put hold the AgentSeat (e.g. IORef in IO, or
State in tests). Example — reply agent over State:
let sys = tape (\hist -> (peek hist) { from = "j", to = [from (peek hist)], body = "ack: " <> body (peek hist) })
sh = agentShard get put sys :: Shard (State (AgentSeat [Post])) [Post] [Post]
in evalState (runK (close (conjoint sh) (companion sh)) [humanPost]) (AgentSeat [] [])
runAgentShard :: Agent (->) s (Post a) [Post a] -> AgentSeat s a -> [Post a] -> ([Post a], AgentSeat s a) Source #
One closed turn of an agent-as-shard: commit ins, emit replies, new seat.
Pure form of close on agentShard without choosing a monad:
runAgentShard sys seat ins = runState (close (agentShard get put sys) ins) seat
Token seat (stream buffers around a Shard)
type Port (m :: Type -> Type) a = Poles (K m) (Post a) (Post a) Source #
Single-post ends: keyboard / one-out seat.
Obtained by buffering a list Shard on both sides, or by a bare queue
(openSTM / openIO).
A tool call from an agent is just a Post: the to list names the
tool, body carries the arguments. No extra type — emit that Post on a
Port (or post it on the log for the tool agent to watch).
Stream algebra: construct a stream by appending one token on the right.
This is the right-handed dual of Uncons.
Methods
Append one token to the right of a stream.
The empty stream.
batchEnds :: forall f s m. (Monad m, Snoc f s) => m f -> (f -> m ()) -> Shard m s f Source #
Ends s f: commit snocs a token; emit flushes the whole stream
(parser takeRest — drain policy is "the stream", not a count).
get / put hold the stream buffer.
unbatchEnds :: forall f s m. (Monad m, Semigroup f, Uncons f s) => m f -> (f -> m ()) -> Shard m f s Source #
Ends f s: commit appends a stream; emit peels one token
(parser next / uncons). Empty stream is quiet — the buffer is left
empty and the returned token is undefined because the polymorphic token
type has no empty value. In practice the list Shard layer ensures quiet
periods are represented by an empty stream, so this case should not be
reached.
get / put hold the stream buffer.
portShard :: Monad m => m [Post a] -> ([Post a] -> m ()) -> m [Post a] -> ([Post a] -> m ()) -> Shard m [Post a] [Post a] -> Port m a Source #
Token seat around a list Shard: buffer on both ends via stream coalgebra.
portShard getIn putIn getOut putOut sh = batchEnds getIn putIn >:> sh >:> unbatchEnds getOut putOut
Flush/drain is not a separate policy knob — it is build-stream (snoc)
and peel-stream (uncons), the same syntax as parsers over [s].
Forces
watch :: forall a f. Uncons f (Post a) => [Name] -> Log f -> [Post a] Source #
Read end of the log: all posts matching any subscription, oldest first.
Traversal is newest-to-oldest; matching posts are prepended, so the accumulator is already oldest-first.
turn :: forall a s f. (Cons f (Post a), Uncons f (Post a)) => Agent (->) s (Post a) [Post a] -> AgentState s f -> Log f -> (AgentState s f, Log f, Maybe (Derivation a)) Source #
Arguments
| :: forall a s f. (Cons f (Post a), Uncons f (Post a)) | |
| => Name | Agent identity recorded in the derivation. |
| -> Agent (->) s (Post a) [Post a] | |
| -> AgentState s f | |
| -> Log f | |
| -> (AgentState s f, Log f, Maybe (Derivation a)) |
One delivery round: peel one addressed post, step the machine, post each output.
The AgentState carries the free carrier s and an addressed inbox. Only
one post is consumed per call; repeated calls drain the inbox. Outputs are
committed newest-first via post. When a post is processed, the returned
Derivation records the agent name, the input post, and the emitted outputs.
turnAs lets the caller supply the agent identity used in the derivation;
this matters when an inbox has multiple subscriptions (card-addressing) and
the first subscription is not the agent's own name.
hasPending :: forall a s f. Uncons f (Post a) => AgentState s f -> Bool Source #
Whether the agent's inbox has an addressed post waiting.
loop :: forall a s f. (Snoc s (Post a), Snoc f (Post a), Cons f (Post a), Uncons f (Post a)) => [(Name, Agent (->) s (Post a) [Post a])] -> Log f -> ([(Name, AgentState s f)], Log f, [Derivation a]) Source #
Round-robin turn-loop until no agent has pending deliveries (quiescence).
Roster order is the schedule. Each pass runs turn for every agent that
still has pending work at its slot. Passes repeat until a pass starts with
nobody pending. Carriers start empty for every name.
loopSubs :: forall a s f. (Snoc s (Post a), Snoc f (Post a), Cons f (Post a), Uncons f (Post a)) => [RosterEntry s a] -> Log f -> ([(Name, AgentState s f)], Log f, [Derivation a]) Source #
Multi-seat-card variant of loop: each agent carries its own subscription
list, so several agents can share a card name.
loopWith :: forall a s f. (Snoc f (Post a), Cons f (Post a), Uncons f (Post a)) => [(Name, Agent (->) s (Post a) [Post a])] -> [(Name, AgentState s f)] -> Log f -> ([(Name, AgentState s f)], Log f, [Derivation a]) Source #
Resumable loop: supply the initial states and inboxes.
Implemented as an Either trace over the roster: each pass is one
iteration of the feedback channel, quiescence returns a Right result.
Backwards-compatible wrapper; for explicit subscriptions use loopWithSubs.
loopWithSubs :: forall a s f. (Snoc f (Post a), Cons f (Post a), Uncons f (Post a)) => [RosterEntry s a] -> [(Name, AgentState s f)] -> Log f -> ([(Name, AgentState s f)], Log f, [Derivation a]) Source #
Multi-seat-card variant of loopWith.
loops :: forall a s f. (Snoc f (Post a), Cons f (Post a), Uncons f (Post a)) => [(Name, Agent (->) s (Post a) [Post a])] -> [(Name, AgentState s f)] -> Log f -> [([(Name, AgentState s f)], Log f, [Derivation a])] Source #
Transitive unfolding of a meeting.
Each element is one state of the round-robin schedule. Divergence becomes
observable (the list is infinite) and loop is simply the last quiescent
element. The third component collects one Derivation for every post that
was processed by turn across the schedule.
Backwards-compatible wrapper; for explicit subscriptions use loopsSubs.
loopsSubs :: forall a s f. (Snoc f (Post a), Cons f (Post a), Uncons f (Post a)) => [RosterEntry s a] -> [(Name, AgentState s f)] -> Log f -> [([(Name, AgentState s f)], Log f, [Derivation a])] Source #
Multi-seat-card variant of loops.
loopHetero :: forall a s f. (Snoc f (Post a), Cons f (Post a), Uncons f (Post a)) => [(Name, s, Agent (->) s (Post a) [Post a])] -> Log f -> ([(Name, AgentState s f)], Log f, [Derivation a]) Source #
Resumable loop with a heterogeneous roster: each agent supplies its own
initial carrier, while inboxes are still seeded from the shared log.
Backwards-compatible wrapper; for explicit subscriptions use loopHeteroSubs.
loopHeteroSubs :: forall a s f. (Snoc f (Post a), Cons f (Post a), Uncons f (Post a)) => [(Name, s, [Name], Agent (->) s (Post a) [Post a])] -> Log f -> ([(Name, AgentState s f)], Log f, [Derivation a]) Source #
Multi-seat-card variant of loopHetero.
meetingLoop :: forall a s f. (Snoc f (Post a), Cons f (Post a), Uncons f (Post a)) => [(Name, Agent (->) s (Post a) [Post a])] -> Trace Either (->) ([(Name, AgentState s f)], Log f, [Derivation a]) ([(Name, AgentState s f)], Log f, [Derivation a]) Source #
The same meeting as a Trace value: yank body over the Either
tensor, quiescence returned as a Right payload.
Backwards-compatible wrapper; for explicit subscriptions use meetingLoopSubs.
meetingLoopSubs :: forall a s f. (Snoc f (Post a), Cons f (Post a), Uncons f (Post a)) => [RosterEntry s a] -> Trace Either (->) ([(Name, AgentState s f)], Log f, [Derivation a]) ([(Name, AgentState s f)], Log f, [Derivation a]) Source #
Multi-seat-card variant of meetingLoop.
seedAgentState :: forall a s f. (Snoc s (Post a), Snoc f (Post a), Uncons f (Post a)) => [Name] -> Log f -> AgentState s f Source #
Empty carrier and an inbox seeded from the log for the subscribed agent(s).
type RosterEntry s a = (Name, [Name], Agent (->) s (Post a) [Post a]) Source #
A roster entry with explicit subscriptions: agent name, subscription names, and the agent itself. The subscription list is the set of names whose posts the agent's inbox should receive; the agent's own name need not be in it.
Derivations
data Derivation a Source #
A node in the meeting's derivation tree.
dChildren is kept flat (empty) in this pass. Building the causal child
tree from routed outputs is future work.
Constructors
| Derivation | |
Instances
| Eq a => Eq (Derivation a) Source # | |
Defined in Circuit.Agent | |
| Show a => Show (Derivation a) Source # | |
Defined in Circuit.Agent Methods showsPrec :: Int -> Derivation a -> ShowS # show :: Derivation a -> String # showList :: [Derivation a] -> ShowS # | |
Session assembly
session :: Uncons f (Post a) => [Name] -> Log f -> [a] Source #
Per-agent session assembly: the bodies an agent actually sees.
Running one step
run1 :: Agent (->) s i o -> s -> i -> (o, s) Source #
Run a monomial system for one step.
Consume i, then extract the output from the successor state (Process /
iterateSystem timing).
Effectful agents
agentM :: forall (m :: Type -> Type) s a b. Applicative m => Agent (->) s a b -> Agent (K m) s a b Source #
Lift a pure agent into the K arrow of any functor.
This is the change of base from (->) to K m on the agent itself:
the same Moore coalgebra, but each step now lives in m.
runAgentM :: Monad m => Agent (K m) s a b -> s -> a -> m (b, s) Source #
Run one step of a monadic agent.
STM agents (S) and IO-bound agents (X)
type AgentS s a = Agent (K STM) s a [a] Source #
STM agent: state is handled transparently inside an STM transaction.
type AgentX s a = Agent (K IO) s a [a] Source #
IO agent: the STM boundary has been crossed; state is no longer transparently handled.
agentX :: AgentS s a -> AgentX s a Source #
Cross from the transparent STM world into the IO boundary.
awaitS :: AgentS s1 a -> AgentS s2 a -> AgentS (s1, s2) a Source #
Seat-level product / await in STM.
raceS :: AgentS s1 a -> AgentS s2 a -> AgentS (s1, s2) a Source #
Seat-level coproduct / race in STM.
raceIO :: AgentX s1 a -> AgentX s2 a -> AgentX (s1, s2) a Source #
Temporal race under IO: run both branches concurrently, return the first branch to emit a non-empty output, and cancel the loser. If the first branch to finish emits nothing, wait for the other branch. If both emit nothing, the right branch's (empty) result is returned.
This is the honest K-IO refinement of raceS: the winner is whichever
step produces a mark first, not the left-biased deterministic rule.
runAgentS :: AgentS s a -> s -> [a] -> IO ([a], s) Source #
Run an STM agent over a list of inputs, crossing into IO at the boundary.
agentLoopS :: AgentS s a -> s -> Poles (K STM) a a -> Poles (K STM) a a -> STM s Source #
Wire an STM agent between an inbox and an outbox, running until
quiescence. Quiescence is detected via orElse: if the inbox is empty
(retry), the loop returns the current state.
selfLoopS :: AgentS s a -> s -> Poles (K STM) a a -> STM s Source #
Self-loop: the agent reads from and writes to the same STM end.
agentLoopL :: AgentS s a -> Poles (K STM) [a] [a] -> Poles (K STM) [a] [a] -> Trace Either (K STM) s s Source #
Wire an STM agent between an inbox and an outbox, running until quiescence, expressed as a 'Trace Either' value.
selfLoopL :: AgentS s a -> s -> Poles (K STM) [a] [a] -> STM s Source #
Self-loop as a 'Trace Either' citizen.
Container dial (Bag / Seq log algebra)
A bag: finite multiset. Order is forgotten; multiplicity is kept.
singletonBag :: a -> Bag a Source #
One element.
Behaviour (stream semantics)
type Beh a = [Post a] -> [Post a] Source #
Agent behaviour: a pure function from an input stream to an output stream.
Each input post is stepped through the agent; the per-step output lists are concatenated into a single output stream. This is the stream semantics of the Moore coalgebra, independent of any effectful boundary.
beh :: Agent (->) s (Post a) [Post a] -> s -> Beh a Source #
Run an agent from an initial carrier to obtain its Behaviour.
Choice (level-1 grammar fragment)
branchAgent :: (s -> Bool) -> Agent (->) s a b -> Agent (->) s a b -> Agent (->) s a b Source #
Conditional agent: branch between two agents based on the current state.
This is a level-1 grammar fragment: the carrier can carry a mode, and the agent dispatches to one of two Moore machines depending on that mode. The predicate is evaluated on the carrier before the input is consumed, which is the honest Moore shape: output is a function of state, and the chosen branch's update function determines the next state.
Seat-level tensors (product await, coproduct race)
awaitA :: Agent (->) s1 (Post a) [Post a] -> Agent (->) s2 (Post a) [Post a] -> Agent (->) (s1, s2) (Post a) [Post a] Source #
Seat-level product / await: both agents run on the same input; states are paired; emits are concatenated left-to-right.
raceA :: Agent (->) s1 (Post a) [Post a] -> Agent (->) s2 (Post a) [Post a] -> Agent (->) (s1, s2) (Post a) [Post a] Source #
Seat-level coproduct / race: both agents run on the same input; states are paired; the left emit wins if non-empty, otherwise the right emit wins.
Re-exports for end construction
data Poles (arr :: k -> k1 -> Type) (a :: k) (b :: k1) #
A matched pair of channel poles: one In and one Out.
This is the bi-polar communication contract. The conjoint (In)
consumes payloads of type a; the companion (Out) produces payloads
of type b. For symmetric channels such as queues a = b.
Together with prefixIn and suffixOut, Poles carries an enriched
profunctor structure over the base category arr: prefixIn is the
left action of arr on In poles, and suffixOut is the right action
of arr on Out poles.
polesK :: Monad m => (a -> m ()) -> m b -> Poles (K m) a b #
Specialization of poles for K actions.
write :: a -> m () consumes the input payload; receive :: m b
produces the output payload. The dualising-object handling is hidden
inside the K wrappers.
prefixIn :: forall {k} arr (a :: k) (b :: k). Category arr => arr a b -> In arr b -> In arr a #
Precompose an arr-morphism with an In pole.
Given f :: arr a b and an In pole at type b, produce an In pole
at type a. Running the resulting pole first executes f and then
commits through the original pole.
This is the left (contravariant) action of the base category on In
poles. Specialised to unit poles it is the canonical way to build
effectful write poles.
>>>let polesU = open :: Poles (->) () ()>>>let inA = prefixIn (const ()) (conjoint polesU) :: In (->) Int>>>commit inA (companion polesU) 42()
How messages are queued between producer and consumer.
Constructors
| Unbounded | Unbounded FIFO queue. |
| Bounded Int | Bounded FIFO with backpressure (write blocks when full). |
| Single | Single-slot buffer (write blocks when full). |
| SwapQ | Single-slot buffer, overwrite-on-full. Write always succeeds; read empties. |
| Latest a | Always holds the latest value (overwrites, never blocks). |
| Newest Int | Like |
data ChannelPolicy a Source #
A channel policy names the residual mediator that governs an effectful
channel. This is the Track-B relocation of the old Queue annotation:
the policy is a value passed at allocation time, not a field of the
channel type. The constructors match the ?-modality vocabulary from the
B0 spike; Linear is the empty-residual default and the only policy on
which halt marks are safe.
Constructors
| Linear | Unbounded FIFO: empty residual, preserves every token in order. This is the effectful face of a linear process. |
| SingleSlot | Single-slot buffer with backpressure (write blocks when full). |
| SwapOne | Single-slot overwrite: write always succeeds, read empties. A weakening policy that can drop a halt mark. |
| LatestValue a | Always holds the latest value; requires a seed for the first read. A weakening policy suitable for diagnostics, not for halt marks. |
| BoundedN Int | Bounded FIFO with backpressure. |
| NewestN Int | Bounded FIFO dropping oldest when full. A weakening policy suitable for bounded diagnostics. |
Instances
| Eq a => Eq (ChannelPolicy a) Source # | |
Defined in Circuit.Agent.Ends Methods (==) :: ChannelPolicy a -> ChannelPolicy a -> Bool # (/=) :: ChannelPolicy a -> ChannelPolicy a -> Bool # | |
| Show a => Show (ChannelPolicy a) Source # | |
Defined in Circuit.Agent.Ends Methods showsPrec :: Int -> ChannelPolicy a -> ShowS # show :: ChannelPolicy a -> String # showList :: [ChannelPolicy a] -> ShowS # | |
openChannel :: ChannelPolicy a -> IO (Poles (K IO) a a) Source #
Open a channel policy as IO Poles.
openChannelSTM :: ChannelPolicy a -> STM (Poles (K STM) a a) Source #
Open a channel policy as STM Poles.
openLinearChannel :: IO (Poles (K IO) a a) Source #
Open a linear channel as IO Poles.
Linear is the default policy: unbounded FIFO, empty residual, preserves
every token in order. This is the effectful face of a linear process.
data HaltChannel (p :: ChannelPolicy a) where Source #
A channel statically known to be linear.
The index p :: ChannelPolicy a is checked by IsLinear at construction
time. Attempting to build a HaltChannel with a non-linear policy fails
to typecheck.
Constructors
| HaltChannel :: forall a (p :: ChannelPolicy a). IsLinear p => Poles (K STM) a a -> HaltChannel p |
type family IsLinear (p :: ChannelPolicy a) where ... Source #
Type-level witness that a channel policy is linear.
Only Linear is allowed to carry halt marks; any other policy produces a
compile-time type error.
Equations
| IsLinear ('Linear :: ChannelPolicy a) = () | |
| IsLinear (p :: ChannelPolicy a) = TypeError ('Text "only 'Linear' channels can carry halt marks") :: Constraint |
openHaltChannel :: STM (HaltChannel ('Linear :: ChannelPolicy a)) Source #
Open a halt-mark channel. This is openLinearChannelSTM with a
type-level certificate.
writeHaltChannel :: forall a (p :: ChannelPolicy a). HaltChannel p -> a -> STM () Source #
Write a token to a halt-mark channel.
readHaltChannel :: forall a (p :: ChannelPolicy a). HaltChannel p -> STM a Source #
Read a token from a halt-mark channel.
openSTM :: Queue a -> STM (Poles (K STM) a a) Source #
Open a queue strategy as STM Poles.
Allocates STM primitives and returns a matched pair of ends sharing
the same mutable channel. Both ends live in STM, so you can compose
operations across channels in a single atomically block.
openIO :: Queue a -> IO (Poles (K IO) a a) Source #
Open a queue strategy as IO Poles.
Like openSTM, but each primitive operation is wrapped in its own
atomically. You cannot batch multiple writes or a write-plus-read
into a single STM transaction; for that use openSTM and wrap in
atomically yourself.
pipeEnds :: Poles (K IO) a b -> (TQueue b -> IO (Poles (K IO) b c)) -> IO (Poles (K IO) a c, IO ()) Source #
Honest sequential composition of two allocated ends via an intermediate queue and a pump.
pipeEnds e1 makeE2 allocates a queue of b values, builds the right end
around that queue with makeE2, and starts a pump that moves values from
e1 into the right end. The returned Poles uses e1 for input and the
built right end for output; the close action cancels the pump.
This is the coend-style composition that composePoles cannot express: the
intermediate carrier is a real queue (the residual's home) rather than the
unit type, so a multi-read consumer can accumulate inputs before emitting.
Shard combinators
prefixShard :: forall (m :: Type -> Type) a' a b. Monad m => (a' -> a) -> Shard m a b -> Shard m a' b Source #
Adapt a shard on the commit side (contravariant).
Transform the input before it is committed. One common use is
session assembly: prefixShard session changes the payload that the
shard posts.
suffixShard :: forall (m :: Type -> Type) b b' a. Monad m => (b -> b') -> Shard m a b -> Shard m a b' Source #
Adapt a shard on the emit side (covariant).
Transform the output after it is emitted. One common use is a
transport envelope: suffixShard (map addHeader) decorates every
emitted post.
codecShard :: forall (m :: Type -> Type) a' a b b'. Monad m => (a' -> a) -> (b -> b') -> Shard m a b -> Shard m a' b' Source #
Adapt both sides of a shard at once.
codecShard f g = prefixShard f . suffixShard g.