///|
/// Built-in slash command port for `/compact` and `/fork-here`.
/// `invoke` enqueues a signal on the ControlMailbox for the pump's safe points.
/// The BuiltinCommandPort holds a reference to the Puppet's ControlMailbox.
/// It is constructed once by `Agent::Agent` from the long-lived mailbox
/// reference, then merged with extension commands when the one PuppetConfig
/// is assembled. Extension commands with the same id
/// (`compact` / `fork-here`) take precedence — the product layer can
/// override the builtins by declaring a command with that id.
///|
/// Built-in command processor. Holds a borrowed `&Mailbox` so `/compact`
/// and `/fork-here` can enqueue fork/compact signals. The mailbox
/// reference is shared with the Puppet and the Agent: one `ControlMailbox`
/// instance, three views. The Agent owns the concrete composition.
priv struct BuiltinCommandPort {
mailbox : &@puppetry.Mailbox
/// Monotonic counter for command ids. Lives on the port so successive
/// invokes produce distinct ids without needing wall-clock time.
mut seq : Int
mut compact_enabled : Bool
mut fork_enabled : Bool
}
///|
fn BuiltinCommandPort::BuiltinCommandPort(
mailbox : &@puppetry.Mailbox,
) -> BuiltinCommandPort {
{ mailbox, seq: 0, compact_enabled: true, fork_enabled: true }
}
///|
/// Helper: mint a unique command id. Combines a stable prefix with the
/// port's monotonic counter so the same id is never returned twice.
fn BuiltinCommandPort::next_command_id(
self : BuiltinCommandPort,
prefix : String,
) -> @puppetry.CommandId {
self.seq = self.seq + 1
@puppetry.CommandId::unchecked("\{prefix}_\{self.seq}")
}
///|
impl @port.CommandPort for BuiltinCommandPort with fn commands(
self : BuiltinCommandPort,
) -> Array[@port.CommandDef] {
let definitions : Array[@port.CommandDef] = []
if self.compact_enabled {
definitions.push(@port.CommandDef::{
id: "compact",
label: "Compact",
description: "Compact the conversation (the modelport decides the strategy)",
category: "session",
ctype: Action,
params: [],
aliases: [],
shortcut: None,
icon: None,
visible: true,
metadata: None,
})
}
if self.fork_enabled {
definitions.push(@port.CommandDef::{
id: "fork-here",
label: "Fork Here",
description: "Fork the conversation at the given message index (0-based)",
category: "session",
ctype: Action,
params: [
@port.CommandParam::{
name: "index",
label: "Message Index",
description: "The 0-based transcript index at which to split the new thread",
ptype: Int,
required: false,
default: None,
choices: None,
positional: true,
},
],
aliases: [],
shortcut: None,
icon: None,
visible: true,
metadata: None,
})
}
definitions
}
///|
impl @port.CommandPort for BuiltinCommandPort with fn invoke(
self : BuiltinCommandPort,
id : String,
args : Json,
) -> @port.CommandOutcome raise @error.CommandError {
match (self.mailbox.active_run_id(), self.mailbox.active_turn_id()) {
(Some(run_id), Some(turn_id)) =>
match id {
"compact" if !self.compact_enabled =>
raise @error.CommandError::NotFound(id)
"compact" => {
// Manual compact: trigger=Manual. The modelport can pick a
// thorough strategy (LLM summary, OpenAI compat compact endpoint).
let command_id = self.next_command_id("compact")
let cmd : @puppetry.CompactCommand = {
command_id,
target_run_id: run_id,
target_turn_id: turn_id,
trigger: @kernel.Manual,
}
match self.mailbox.enqueue_compact(cmd) {
Accepted(_) =>
@port.CommandOutcome::Success(
feedback="compact queued, will execute at the next safe point",
structured=None,
ui_hint=Some(@port.UiHint::Toast),
)
RejectedStale(reason~, ..) =>
@port.CommandOutcome::Failure(
reason="compact rejected: " + reason,
)
RejectedQueueFull(depth~, ..) =>
@port.CommandOutcome::Failure(
reason="compact queue full (depth=\{depth})",
)
_ =>
@port.CommandOutcome::Failure(reason="compact already requested")
}
}
"fork-here" if !self.fork_enabled =>
raise @error.CommandError::NotFound(id)
"fork-here" => {
// Parse optional `index` from args. Default is the current
// transcript length (fork at the tail).
let index = parse_fork_index(args)
let command_id = self.next_command_id("fork")
let cmd : @puppetry.ForkCommand = {
command_id,
target_run_id: run_id,
target_turn_id: turn_id,
fork_index: index,
}
match self.mailbox.enqueue_fork(cmd) {
Accepted(_) =>
@port.CommandOutcome::Success(
feedback="fork queued at index \{index}, will execute at the next safe point",
structured=None,
ui_hint=Some(@port.UiHint::Toast),
)
RejectedStale(reason~, ..) =>
@port.CommandOutcome::Failure(reason="fork rejected: " + reason)
RejectedQueueFull(depth~, ..) =>
@port.CommandOutcome::Failure(
reason="fork queue full (depth=\{depth})",
)
_ => @port.CommandOutcome::Failure(reason="fork already requested")
}
}
other => raise @error.CommandError::NotFound(other)
}
_ =>
// No active run: nothing for the pump to consume. Tell the caller
// so they can surface it (or queue an out-of-band compact/fork,
// which M3.5 does not deliver).
@port.CommandOutcome::Failure(
reason="no active run: " +
id +
" can only be invoked while a turn is in flight",
)
}
}
///|
#warnings("-unused_value")
extend BuiltinCommandPort with @port.CommandPort::{commands, invoke}
///|
/// Parse the optional `index` argument for `/fork-here`. Recognises:
/// - `{"index": N}` (object form)
/// - `N` (bare number, used by positional slash parsers)
/// Defaults to `0` when absent or unparseable (the caller can override
/// by passing a positive index). A future iteration can return the
/// current transcript length when omitted, but that requires the
/// transcript reference the BuiltinCommandPort does not hold today.
fn parse_fork_index(args : Json) -> Int {
match args {
Json::Object(map) =>
if map.contains("index") {
match map["index"] {
Json::Number(n, ..) => n.to_int()
_ => 0
}
} else {
0
}
Json::Number(n, ..) => n.to_int()
_ => 0
}
}
///|
/// Build the agent's full command list: the builtins
/// (`/compact`, `/fork-here`) registered first, followed by the extension
/// commands. Extensions that declare a command with id `compact` or
/// `fork-here` are dropped from the list — they override the builtins by
/// being the only one with that id (D5.4). This keeps the BuiltinCommandPort
/// as a sensible default that products can replace.
///
/// D3-Q1: the builtin port is a per-Agent singleton (constructed once in
/// `Agent::Agent`), so its `seq` counter stays monotonic across turns.
/// This method merely borrows it into the per-turn command list — it does
/// NOT reconstruct the port.
fn compose_agent_commands(
commands : Array[&@port.CommandPort],
builtin_command_port : BuiltinCommandPort,
) -> Array[&@port.CommandPort] {
let builtin : &@port.CommandPort = builtin_command_port as &@port.CommandPort
let extension_ids : Map[String, Unit] = Map::from_array([])
for cmd_port in commands {
for def in cmd_port.commands() {
extension_ids[def.id] = ()
}
}
// If any extension declares `compact` or `fork-here`, the builtin is
// skipped — the extension version wins. Otherwise the builtin is the
// default.
builtin_command_port.compact_enabled = !extension_ids.contains("compact")
builtin_command_port.fork_enabled = !extension_ids.contains("fork-here")
let result : Array[&@port.CommandPort] = []
if builtin_command_port.compact_enabled || builtin_command_port.fork_enabled {
result.push(builtin)
}
for cmd in commands {
result.push(cmd)
}
result
}
///|
pub fn Agent::commands(self : Agent) -> Array[&@port.CommandPort] {
compose_agent_commands(
self.runtime.commands,
self.runtime.builtin_command_port,
)
}