circuits-agent
Safe HaskellNone
LanguageGHC2024

Circuit.Agent

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

Posts and the log

data Post a Source #

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.

Constructors

Post 

Fields

Instances

Instances details
Functor Post Source # 
Instance details

Defined in Circuit.Agent

Methods

fmap :: (a -> b) -> Post a -> Post b #

(<$) :: a -> Post b -> Post a #

Eq a => Eq (Post a) Source # 
Instance details

Defined in Circuit.Agent

Methods

(==) :: Post a -> Post a -> Bool #

(/=) :: Post a -> Post a -> Bool #

Ord a => Ord (Post a) Source # 
Instance details

Defined in Circuit.Agent

Methods

compare :: Post a -> Post a -> Ordering #

(<) :: Post a -> Post a -> Bool #

(<=) :: Post a -> Post a -> Bool #

(>) :: Post a -> Post a -> Bool #

(>=) :: Post a -> Post a -> Bool #

max :: Post a -> Post a -> Post a #

min :: Post a -> Post a -> Post a #

Show a => Show (Post a) Source # 
Instance details

Defined in Circuit.Agent

Methods

showsPrec :: Int -> Post a -> ShowS #

show :: Post a -> String #

showList :: [Post a] -> ShowS #

Cons (Log a) (Stamped a) Source #

Prepend is the dual view: a newest-first stream over the same image.

Instance details

Defined in Circuit.Agent.Framing

Methods

cons :: Stamped a -> Log a -> Log a #

consNil :: Log a #

Snoc (Log a) (Stamped a) Source #

Append is the natural operation: one element at the end.

Instance details

Defined in Circuit.Agent.Framing

Methods

snoc :: Log a -> Stamped a -> Log a #

snocNil :: Log a #

Uncons (Log a) (Stamped a) Source #

Read peels the oldest element first.

Instance details

Defined in Circuit.Agent.Framing

Methods

uncons :: Log a -> These (Stamped a) (Log a) #

nil :: Log a #

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.

mkPost :: Name -> [Name] -> a -> Post a Source #

A fresh root post (no parents).

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.

indexToIdMap :: [Post a] -> Map PostId (Post a) Source #

Assign positional ids [0..] to a chronological list of posts. This is the convenience bridge from list-shaped logs to the id-resolved branches/cone API.

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

type Log (f :: k) = f Source #

The shared append-only log, newest first.

The log is a stream of Posts. Common case: Log [Post]. Generalizing to any f with Cons and Uncons lets the same delivery machinery run over other stream representations while keeping the addressed read as a list.

emptyLog :: forall a f. Cons f (Post a) => Log f Source #

Empty log.

type Name = Text Source #

Agent name on the shared log.

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 

Fields

Instances

Instances details
(Eq s, Eq f) => Eq (AgentState s f) Source # 
Instance details

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 # 
Instance details

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

data Inbox f Source #

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

Instances details
Eq f => Eq (Inbox f) Source # 
Instance details

Defined in Circuit.Agent

Methods

(==) :: Inbox f -> Inbox f -> Bool #

(/=) :: Inbox f -> Inbox f -> Bool #

Show f => Show (Inbox f) Source # 
Instance details

Defined in Circuit.Agent

Methods

showsPrec :: Int -> Inbox f -> ShowS #

show :: Inbox f -> String #

showList :: [Inbox f] -> ShowS #

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.

inboxWho :: Inbox f -> Name Source #

Primary owner of the inbox (head of the subscription list).

inboxSubs :: Inbox f -> [Name] Source #

Full subscription list for the 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)

data AgentSeat s a Source #

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.

Constructors

AgentSeat 

Fields

Instances

Instances details
(Eq s, Eq a) => Eq (AgentSeat s a) Source # 
Instance details

Defined in Circuit.Agent

Methods

(==) :: AgentSeat s a -> AgentSeat s a -> Bool #

(/=) :: AgentSeat s a -> AgentSeat s a -> Bool #

(Show s, Show a) => Show (AgentSeat s a) Source # 
Instance details

Defined in Circuit.Agent

Methods

showsPrec :: Int -> AgentSeat s a -> ShowS #

show :: AgentSeat s a -> String #

showList :: [AgentSeat s a] -> ShowS #

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

class Snoc f s where #

Stream algebra: construct a stream by appending one token on the right.

This is the right-handed dual of Uncons.

Methods

snoc :: f -> s -> f #

Append one token to the right of a stream.

snocNil :: f #

The empty stream.

Instances

Instances details
Snoc [a] a # 
Instance details

Defined in Circuit.Stream

Methods

snoc :: [a] -> a -> [a] #

snocNil :: [a] #

Snoc (Log a) (Stamped a) Source #

Append is the natural operation: one element at the end.

Instance details

Defined in Circuit.Agent.Framing

Methods

snoc :: Log a -> Stamped a -> Log a #

snocNil :: Log a #

Snoc (Array a) (Array a) #

Append a row. When the initial stream is empty, construct a one-row array from the row's shape.

Instance details

Defined in Circuit.Mat.Array.Stream

Methods

snoc :: Array a -> Array a -> Array a #

snocNil :: Array a #

snocPost :: [Post a] -> Post a -> [Post a] Source #

Snoc a Post onto a post stream. Specialized alias for snoc.

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.

post :: Cons f (Post a) => Post a -> Log f -> Log f Source #

Write end of the log: commit a post.

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 #

turn with the agent identity taken from the inbox's first subscription.

For single-subscription inboxes this is the agent's own name; for multi-subscription inboxes use turnAs.

turnAs 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 

Fields

Instances

Instances details
Eq a => Eq (Derivation a) Source # 
Instance details

Defined in Circuit.Agent

Methods

(==) :: Derivation a -> Derivation a -> Bool #

(/=) :: Derivation a -> Derivation a -> Bool #

Show a => Show (Derivation a) Source # 
Instance details

Defined in Circuit.Agent

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.

stepS :: AgentS s a -> s -> a -> STM (s, [a]) Source #

Run one step of an STM agent.

stepsS :: AgentS s a -> s -> [a] -> STM (s, [a]) Source #

Fold an STM agent over a bundle of inputs within one transaction.

This is the bundle-at-a-time step: one frame consumes a whole [a] and produces the concatenated replies. Factor of runAgentS that stays in STM so it can sit inside a larger transaction (e.g. a self-loop frame).

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.

readEndSTM :: Poles (K STM) a a -> STM a Source #

Read one token from an STM pole.

writeEndSTM :: Poles (K STM) a a -> a -> STM () Source #

Write one token to an STM pole.

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)

newtype Bag a Source #

A bag: finite multiset. Order is forgotten; multiplicity is kept.

Constructors

Bag (Map a Int) 

Instances

Instances details
Eq a => Eq (Bag a) Source # 
Instance details

Defined in Circuit.Agent

Methods

(==) :: Bag a -> Bag a -> Bool #

(/=) :: Bag a -> Bag a -> Bool #

Show a => Show (Bag a) Source # 
Instance details

Defined in Circuit.Agent

Methods

showsPrec :: Int -> Bag a -> ShowS #

show :: Bag a -> String #

showList :: [Bag a] -> ShowS #

type TurnLog a = Seq (Bag a) Source #

Log algebra: a sequence of bags, one bag per turn.

emptyBag :: Bag a Source #

The empty bag.

singletonBag :: a -> Bag a Source #

One element.

insertBag :: Ord a => a -> Bag a -> Bag a Source #

Insert one element.

toBag :: Ord a => [a] -> Bag a Source #

Fold a list into a bag, forgetting order.

fromBag :: Bag a -> [a] Source #

Expand a bag into a list in some deterministic order.

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.

after :: System (->) s (Mono i o) -> s -> [i] -> s #

State after consuming a list of inputs.

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.

Constructors

Poles 

Fields

  • conjoint :: In arr a

    Write pole (producer), the conjoint.

  • companion :: Out arr b

    Read pole (consumer), the companion.

close :: forall {k} arr (a :: k). In arr a -> Out arr a -> arr a a #

Plug an In and an Out of the same payload type together.

close feeds the Out into the In pole, producing a morphism arr a a from the paired payload type.

Yanking: for the unit poles from open, close (conjoint p) (companion p) = id.

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

data Queue a Source #

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 Bounded but drops oldest when full.

Instances

Instances details
Eq a => Eq (Queue a) Source # 
Instance details

Defined in Circuit.Agent.Ends

Methods

(==) :: Queue a -> Queue a -> Bool #

(/=) :: Queue a -> Queue a -> Bool #

Show a => Show (Queue a) Source # 
Instance details

Defined in Circuit.Agent.Ends

Methods

showsPrec :: Int -> Queue a -> ShowS #

show :: Queue a -> String #

showList :: [Queue a] -> ShowS #

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

Instances details
Eq a => Eq (ChannelPolicy a) Source # 
Instance details

Defined in Circuit.Agent.Ends

Show a => Show (ChannelPolicy a) Source # 
Instance details

Defined in Circuit.Agent.Ends

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.

openLinearChannelSTM :: STM (Poles (K STM) a a) Source #

Open a linear channel as STM Poles.

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.

composeShard :: forall (m :: Type -> Type) a b c. Monad m => Shard m a b -> Shard m b c -> Shard m a c Source #

Sequential composition of shards.

The output of the first shard feeds the input of the second. This is the same shape as connecting two effectful agents in series.

(>:>) :: 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 infixr 1 #

Forward-composition operator for Poles. p1 >:> p2 = compose p1 p2.