///|
/// Opaque host-side control handle owned by one Agent.
///
/// Hosts obtain this value from `Agent::control()`. The message inbox — the
/// single FIFO behind both step and turn deliveries — and the follow-up drain
/// remain private to the root package, so a host can submit or observe control
/// state without taking ownership of Agent's loop.
///|
pub struct AgentControl {
priv mailbox : &@puppetry.Mailbox
priv inbox : @puppetry.MessageInbox
priv mut seq : Int32
priv mut abort_hook : ((@kernel.RunId) -> Unit)?
}
///|
/// Framework-only construction. Product code receives this handle from an
/// Agent and cannot bind one to an arbitrary internal mailbox.
fn AgentControl::AgentControl(mailbox : &@puppetry.Mailbox) -> AgentControl {
{ mailbox, inbox: mailbox.message_inbox(), seq: 0, abort_hook: None, }
}
///|
/// Install the Agent-owned foreground cancellation callback. The callback is
/// invoked only after the mailbox accepts an abort for a live run.
fn AgentControl::set_abort_hook(
self : AgentControl,
hook : (@kernel.RunId) -> Unit,
) -> Unit {
self.abort_hook = Some(hook)
}
///|
fn AgentControl::next_command_id(
self : AgentControl,
prefix : String,
) -> @puppetry.CommandId {
self.seq = self.seq + 1
@puppetry.CommandId::unchecked("\{prefix}_\{self.seq}")
}
///|
/// Opaque handle on the turn that currently accepts control messages. A host
/// captures it from `current_message_target()` and passes it back as
/// `target~`, so a late submit is rejected instead of silently attaching to a
/// newer turn.
#derive(Eq)
pub struct MessageTarget {
priv address : @puppetry.MessageAddress
}
///|
pub extend MessageTarget with Eq::{equal, not_equal}
///|
/// Attribution scope for a delivered or undelivered message, taken from the
/// address the message was actually enqueued under — never the submitting
/// host's view of the active turn.
fn message_scope(address : @puppetry.MessageAddress) -> @types.EventScope {
{
session_id: @kernel.SessionId::unchecked(address.session_id),
run_id: address.run_id,
turn_id: address.turn_id,
}
}
///|
/// Register the Agent event surface for inbox deliveries: the follow-up
/// (Turn) confirmation callback projects public `MessageApplied` through the
/// Agent's event runtime, with the scope of the turn that actually committed
/// the message. Invoked synchronously by the inbox at the initial commit.
fn AgentControl::install_message_events(
self : AgentControl,
events : @event_runtime.EventRuntime,
) -> Unit {
self.inbox.set_follow_up_callback(fn(command, address) {
events.emit_core(
Some(message_scope(address)),
MessageApplied(
command_id=command.command_id.to_string(),
delivery=command.delivery,
),
)
})
self.inbox.set_step_delivery_callback(fn(command, reason) {
let event : @types.TurnEvent = match reason {
None =>
MessageApplied(command_id=command.command_id.to_string(), delivery=Step)
Some(reason) =>
MessageUndelivered(
command_id=command.command_id.to_string(),
delivery=Step,
reason~,
)
}
events.emit_core(Some(message_scope(command.address)), event)
})
}
///|
/// The turn that currently accepts control messages, or `None` while no turn
/// is open (idle, between turns, or during a compact operation).
pub fn AgentControl::current_message_target(
self : AgentControl,
) -> MessageTarget? {
self.inbox.current_address().map(fn(address) { { address, } })
}
///|
/// Currently-active run id, or `None` while the Agent is idle.
pub fn AgentControl::active_run_id(self : AgentControl) -> @kernel.RunId? {
self.mailbox.active_run_id()
}
///|
/// Currently-active turn id, or `None` while the Agent is idle.
pub fn AgentControl::active_turn_id(self : AgentControl) -> @kernel.TurnId? {
self.mailbox.active_turn_id()
}
///|
/// Without a target, submit to the turn active at this call. With a target,
/// reject a delayed submission if the captured address is no longer active.
fn AgentControl::enqueue_message(
self : AgentControl,
prefix : String,
delivery : @types.MessageDeliveryKind,
message : @kernel.Message,
target~ : MessageTarget? = None,
) -> @runtime.EnqueueOutcome {
let command_id = self.next_command_id(prefix)
match
self.inbox.enqueue(
command_id,
delivery,
message,
expected=target.map(fn(target) { target.address }),
) {
@puppetry.Accepted(command_id~) =>
@runtime.Accepted(command_id=command_id.to_string())
@puppetry.RejectedStale(reason~, ..) => @runtime.RejectedStale(reason~)
@puppetry.RejectedQueueFull(depth~, ..) =>
@runtime.RejectedQueueFull(depth~)
@puppetry.AbortAlreadyRequested(..) =>
@runtime.RejectedQueueFailure(
reason="message cannot be marked abort-already-requested",
)
}
}
///|
/// Submit a follow-up consumed by the Agent at a turn boundary. Without a
/// target, the current turn at this call owns the submission. Pass `target~`
/// to reject a delayed task whose originally observed turn is no longer active.
pub fn AgentControl::enqueue_follow_up(
self : AgentControl,
message : @kernel.Message,
target~ : MessageTarget? = None,
) -> @runtime.EnqueueOutcome {
self.enqueue_message(
"follow_up",
@types.MessageDeliveryKind::Turn,
message,
target~,
)
}
///|
/// Submit a steering message consumed at the next step boundary of the active
/// turn. Without a target, submit to the current turn at this call. Pass
/// `target~` to reject a delayed submission for an older turn.
pub fn AgentControl::enqueue_steer(
self : AgentControl,
message : @kernel.Message,
target~ : MessageTarget? = None,
) -> @runtime.EnqueueOutcome {
self.enqueue_message(
"steer",
@types.MessageDeliveryKind::Step,
message,
target~,
)
}
///|
/// Number of steers waiting for the active turn's next step boundary.
pub fn AgentControl::pending_steers(self : AgentControl) -> Int32 {
self.inbox.steer_depth()
}
///|
/// Number of follow-ups waiting for the Agent's next turn boundary.
pub fn AgentControl::pending_follow_ups(self : AgentControl) -> Int32 {
self.inbox.follow_up_depth()
}
///|
/// Bind the inbox to the admitted operation: one inbox operation per guard
/// operation, cleared by `finish_operation` before the guard releases.
fn AgentControl::bind_operation(
self : AgentControl,
operation_id : String,
session_id : String,
) -> Unit {
self.inbox.begin_operation(operation_id, session_id)
}
///|
/// Bind the per-turn identity before the turn executes, opening the step
/// window under the admitted operation.
fn AgentControl::bind_turn(
self : AgentControl,
run_id : @kernel.RunId,
turn_id : @kernel.TurnId,
) -> Unit {
self.inbox.bind_turn(run_id, turn_id)
}
///|
/// Associate a drained follow-up with the turn that will first commit it: the
/// pump confirms the id at that turn's initial boundary, which projects the
/// public `MessageApplied` and retires the record.
fn AgentControl::arm_follow_up(
self : AgentControl,
command_id : @puppetry.CommandId,
run_id : @kernel.RunId,
turn_id : @kernel.TurnId,
) -> Unit {
self.inbox.bind_turn(run_id, turn_id)
self.inbox.set_initial_command(command_id.to_string())
}
///|
/// Take the oldest queued follow-up for this operation. The record stays in
/// the inbox (selected) until its turn's initial commit confirms it.
fn AgentControl::select_follow_up(
self : AgentControl,
) -> @puppetry.QueuedMessage? {
self.inbox.select_follow_up()
}
///|
/// Settle the step window at one per-turn exit: every step message still
/// unconfirmed for this turn is returned for `MessageUndelivered` reporting.
fn AgentControl::finish_turn(
self : AgentControl,
run_id : @kernel.RunId,
turn_id : @kernel.TurnId,
) -> Array[@puppetry.QueuedMessage] {
self.inbox.finish_turn(run_id, turn_id)
}
///|
/// Settle the whole operation before the guard releases: every remaining
/// record — step and turn, queued or selected — is returned for
/// `MessageUndelivered` reporting, and the inbox is closed so the next
/// admission starts from an empty inbox.
fn AgentControl::finish_operation(
self : AgentControl,
) -> Array[@puppetry.QueuedMessage] {
self.inbox.finish_operation()
}
///|
/// Request cancellation of the active run. Repeated requests return the
/// displayable `AbortAlreadyRequested` outcome.
pub fn AgentControl::abort_active(
self : AgentControl,
detail : String?,
) -> @runtime.EnqueueOutcome {
let command_id = self.next_command_id("abort")
let cmd : @puppetry.AbortCommand = {
command_id,
target_run_id: match self.mailbox.active_run_id() {
Some(run) => run
None => return @runtime.RejectedStale(reason="no active run to abort")
},
detail,
}
match self.mailbox.enqueue_abort(cmd) {
@puppetry.Accepted(command_id~) => {
match self.abort_hook {
Some(hook) => hook(cmd.target_run_id)
None => ()
}
@runtime.Accepted(command_id=command_id.to_string())
}
@puppetry.RejectedStale(reason~, ..) => @runtime.RejectedStale(reason~)
@puppetry.RejectedQueueFull(depth~, ..) =>
@runtime.RejectedQueueFull(depth~)
@puppetry.AbortAlreadyRequested(..) => @runtime.AbortAlreadyRequested
}
}