#Posoco events

    @events owns the open Event type and the owner-bound Events capability. Posoco publishes core notifications as Core(TurnEvent). A community protocol package adds its own constructor and keeps a closed payload ADT, so consumers pattern match with static types: no JSON codec, no downcast, no runtime registry.

    ///|
    pub(all) enum SprintEvent {
    Started(Int32)
    Finished
    }

    ///|
    pub(all) extenum @events.Event += {
    SprintChanged(SprintEvent)
    }

    ///|
    test "a community protocol extends the open event type" {
    let event : @events.Event = SprintChanged(Started(3))
    let described = match event {
    SprintChanged(Started(sprint)) => "started #\{sprint}"
    SprintChanged(Finished) => "finished"
    @events.Core(_) => "core notification"
    _ => "another protocol"
    }
    assert_eq(described, "started #3")
    }

    A capability publishes only in the namespace its owner was granted while subscribe may listen to any namespace. Handlers are synchronous and non-raising; cancel is idempotent and a new listener receives no history.

    #One resource per Agent

    EventSystem() is the host-owned resource behind every capability and subscription. Create it before the ports and the Agent that will consume it, then hand the same value to exactly one Agent(exts~, config~, events~):

    1. Define an ADT and extend the open @posoco.Event.
    2. let system = @posoco.EventSystem(). While no Agent owns it, the system already grants publisher(owner~) capabilities and accepts subscribe.
    3. Give a plain object its Events with system.publisher(owner="status_bar"). The grant is bound to Extension("status_bar"), the object needs no Lifecycle, and the capability stays valid after the Agent takes over.
    4. Compose Agent(exts~, config~, events=system). Construction takes exclusive ownership before aggregation; a second Agent on the same value is rejected and leaves the first Agent working.
    5. Extensions can still declare requires: [@posoco.Capability::Events] and lifecycle: [self], then obtain view.events() in Lifecycle::on_compose. An object that needs to publish earlier can instead receive its capability from the host, including before that object's construction.
    6. Hosts subscribe with system.subscribe(handler) before composition or with agent.subscribe_events(handler) afterwards, optionally filtering with namespace_=Some(Extension(id)) or namespace_=Some(Posoco). None means all namespaces.
    7. Handle every Result, retain the subscription, and call cancel() when done. After Agent::shutdown — or system.close() on a system no Agent owns — publisher grants, publications and new subscriptions return Err(Closed); an extension publishing Core returns Err(ReservedEvent).

    A failed or cancelled turn does not close the resource. Any composition failure after takeover does close it and revokes the capabilities already handed out, so a failed construction never leaves a usable half-composed resource. A closed or already-owned system is reported with a typed error; it is never reopened, replaced, or silently handed to a second owner.

    ///|
    let system = @posoco.EventSystem()
    // An untaken system already accepts listeners and grants capabilities.

    ///|
    let subscription = system.subscribe(handler).unwrap()
    // `publisher.publish(...)` is tagged Extension("status_bar") from now on,
    // including after the Agent takes the resource over.

    ///|
    let publisher = system.publisher(owner="status_bar").unwrap()

    ///|
    let agent = @posoco.Agent(exts~, config~, events=system)
    // Retain subscription until its listener is no longer needed, then cancel it.
    // `Agent::shutdown` closes the resource; retained capabilities return Closed.

    publisher rejects a blank owner with Err(InvalidOwner), publishing Core returns Err(ReservedEvent), closing an owned resource returns Err(Owned), and community publication and subscription return Err(Closed) after shutdown. The shared EventError describes both capability and resource operations; it does not give a capability access to resource management. The runnable end-to-end version of this host flow is examples/quickstart.

    The manifest id binds publishing identity and must be unique for Events users. Posoco is reserved for core notifications, not an extra bus. Observer is a core-only adapter on the same dispatcher; Hook remains interception, and awaited journal/checkpoint commits remain separate from notifications.

    For reusable protocols, export the payload ADT and constructor from a protocol package. The root @posoco.Event facade can be extended, but an extenum constructor must be qualified by its defining package, e.g. @events.Core or @protocol.SprintChanged, not by a package that only re-exports the type. Keep a wildcard for the open Event; exhaustively match the closed payload.

    Delivery uses registration order and a per-batch listener snapshot; reentrant publications queue rather than recurse. Changes to subscriptions during a batch apply to the next batch. Use immutable community payloads: they are not deep copied. scope is optional delivery attribution, not a routing filter.

    The following isolated test uses fake callbacks only to illustrate the listener shape; from_callbacks and envelope construction are framework/test seams, not steps a product extension needs to perform.

    ///|
    test "subscribe to typed deliveries and cancel" {
    let delivered : Array[String] = []
    let dispatch : Ref[(@events.EventEnvelope) -> Unit] = Ref(fn(_) { () })
    let events = @events.Events::from_callbacks(
    publish=fn(_event, _scope) { Ok(()) },
    subscribe=fn(handler, _namespace_) {
    // A real runtime records the handler and filters by namespace.
    dispatch.val = handler
    Ok(@events.Subscription::from_cancel(fn() { () }))
    },
    )
    let subscription = events.subscribe(fn(envelope) {
    match envelope.event {
    SprintChanged(Started(sprint)) =>
    // The envelope namespace is system-produced delivery metadata.
    match envelope.namespace_of() {
    Extension("sprint") => delivered.push("sprint started #\{sprint}")
    _ => delivered.push("unexpected namespace")
    }
    _ => ()
    }
    })
    guard subscription is Ok(handle) else { fail("expected a subscription") }
    (dispatch.val)(
    @events.EventEnvelope(
    namespace_=@events.Extension("sprint"),
    event=SprintChanged(Started(4)),
    ),
    )
    handle.cancel()
    handle.cancel()
    assert_eq(delivered, ["sprint started #4"])
    }

    Event

    pub extenum Event {
    }

    The notification value listeners receive. Posoco owns the core wrapper; a community protocol package adds its own constructor:

    pub(all) extenum @events.Event += {
    SprintChanged(SprintEvent)
    }

    Consumers import the protocol package and pattern match the payload with its static type — no JSON codec, downcast or runtime registry. Matching on the open type needs a wildcard branch; the business ADT stays exhaustive.

    A community payload is a value, not a serialization boundary: publish it as immutable data. Posoco snapshots core TurnEvent payloads but does not deep-copy arbitrary ADTs.

    EventEnvelope

    pub struct EventEnvelope {
    namespace_ : EventNamespace
    scope :
    EventScope
    ?
    event : Event
    }

    One delivered event with the metadata the system produced for it. Subscribers read namespace_, scope and event; they never author delivery metadata. scope is None for operation-level notifications that belong to no run.

    namespace is a reserved word in MoonBit, so the field is namespace_ and namespace_of() is the plain-reader accessor.

    Example

    test {
    let envelope = @events.EventEnvelope(
    namespace_=@events.Posoco,
    event=@events.Core(TurnStarted),
    )
    assert_eq(envelope.namespace_of(), @events.Posoco)
    assert_eq(envelope.scope, None)
    guard envelope.event is @events.Core(TurnStarted) else {
    fail("expected a core turn start")
    }
    }

    EventEnvelope::EventEnvelope

    fn EventEnvelope::EventEnvelope(namespace_~ : EventNamespace, event~ : Event, scope? :
    EventScope
    ?) -> EventEnvelope

    Framework construction hook: the runtime stamps real delivery metadata. Subscribers receive envelopes and do not need this constructor.

    EventEnvelope::namespace_of

    fn EventEnvelope::namespace_of(self : EventEnvelope) -> EventNamespace

    EventError

    pub(all) enum EventError {
    Closed
    ReservedEvent
    InvalidOwner
    Owned
    } derive(Eq,
    Debug
    )

    Typed failure of event capability and resource operations.

    EventError::equal

    fn EventError::equal(EventError, EventError) -> Bool

    EventError::not_equal

    fn EventError::not_equal(x : EventError, y : EventError) -> Bool

    EventNamespace

    pub(all) enum EventNamespace {
    Posoco
    Extension(String)
    } derive(Eq,
    Debug
    )

    Delivery namespace of one event. Posoco is reserved for core notifications; an extension capability publishes only its own Extension(manifest_id). The namespace is system-produced envelope metadata, never a string a publisher can spoof.

    EventNamespace::equal

    EventNamespace::not_equal

    fn EventNamespace::not_equal(x : EventNamespace, y : EventNamespace) -> Bool

    Events

    pub struct Events {
    // private fields
    }

    Owner-bound event capability. publish is fixed to the namespace granted by EventSystem::publisher(owner~) or CompositionView::events(), while subscribe may listen to any namespace. The latter binds the manifest id; only Posoco core holds the core publish handle.

    Events::from_callbacks

    fn Events::from_callbacks(publish~ : (Event,
    EventScope
    ?) -> Result[Unit, EventError], subscribe~ : ((EventEnvelope) -> Unit, EventNamespace?) -> Result[Subscription, EventError]) -> Events

    Framework construction hook. Products obtain a connected capability through EventSystem::publisher or CompositionView::events, not this constructor.

    Events::publish

    fn Events::publish(self : Events, event : Event, scope? :
    EventScope
    ?) -> Result[Unit, EventError]

    Publish one event in the namespace this capability is bound to. scope carries session/run/turn attribution when the notification belongs to a run. An extension capability holding a core payload fails with ReservedEvent; a closed resource fails with Closed; no listener succeeds with no side effect. A community payload is delivered as published, so it must be immutable data rather than a value the publisher mutates later.

    Example

    test {
    // This fake models the real Agent's reserved-payload rejection.
    // Products receive a connected capability from EventSystem or CompositionView.
    let events = @events.Events::from_callbacks(
    publish=fn(event, _scope) {
    match event {
    @events.Core(_) => Err(@events.ReservedEvent)
    _ => Ok(())
    }
    },
    subscribe=fn(_handler, _namespace_) { Err(@events.Closed) },
    )
    guard events.publish(@events.Core(TurnStarted)) is Err(ReservedEvent) else {
    fail("extensions must not publish core notifications")
    }
    }

    Events::subscribe

    fn Events::subscribe(self : Events, handler : (EventEnvelope) -> Unit, namespace_? : EventNamespace?) -> Result[Subscription, EventError]

    Register one synchronous, non-raising listener. Delivery follows registration order; the namespace_ argument narrows delivery to one namespace, and None listens to every namespace including Posoco. A listener added or cancelled inside a delivery batch takes effect from the next batch, and a new listener receives no history.

    Example

    test {
    let seen_filter : Ref[@events.EventNamespace?] = Ref(None)
    let events = @events.Events::from_callbacks(
    publish=fn(_event, _scope) { Ok(()) },
    subscribe=fn(_handler, namespace_) {
    seen_filter.val = namespace_
    Ok(@events.Subscription::from_cancel(fn() { () }))
    },
    )
    let subscription = events.subscribe(
    fn(_envelope) { () },
    namespace_=Some(@events.Posoco),
    )
    guard subscription is Ok(_) else { fail("expected a subscription") }
    assert_eq(seen_filter.val, Some(@events.Posoco))
    }

    Subscription

    pub struct Subscription {
    // private fields
    }

    Opaque handle of one listener registration. cancel is idempotent and releases the handler reference; a handle whose runtime is gone cancels to a no-op.

    Example

    test {
    let cancels = Ref(0)
    let subscription = @events.Subscription::from_cancel(fn() {
    cancels.val = cancels.val + 1
    })
    subscription.cancel()
    subscription.cancel()
    assert_eq(cancels.val, 1)
    }

    Subscription::cancel

    fn Subscription::cancel(self : Subscription) -> Unit

    Subscription::from_cancel

    fn Subscription::from_cancel(cancel : () -> Unit) -> Subscription

    Framework construction hook. Product code receives subscriptions from Events::subscribe and does not need this constructor.