command

package
v0.33.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 10, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

README

pkg/command

pkg/command defines the sealed command union for the loop actor. Command is a sealed interface: only types in this package can implement it (the isCommand method is unexported). The actor accepts commands on its Commands channel and on its priority PriorityCommands lane.

What is command?

  • A sealed Command interface with CommandHeader() returning Header (id, agency, route).
  • Submit commands — fire-and-forget; their outcome is published as a typed event, never replied on a per-command channel:
    • UserInput — submit a user turn to a loop.
    • SubagentResult — deliver a delegate's result back to its parent.
    • CancelQueuedInput — drop a queued input without running it.
    • ProvideUserInput — answer a UserInputRequested gate.
    • Compact — request a per-loop context compaction.
  • Control commands — carry a buffered(1) Ack channel so the actor's send never stalls:
    • InterruptAck chan bool; true iff a running turn was cancelled.
    • ShutdownAck chan error; nil on clean exit.
  • Routing helpersGateRoute (loop + tool-execution addressing for a gate reply), Route (the loop-side match key), ApproveAction (exactly the three approval actions; the single validation source shared by the strict wire decoder and the session route).
  • Header / CommandName / CommandField / InvalidCommandError for contract violations by internal callers.

How to use

Consumers don't construct commands directly; they call Session methods that produce them:

session.Submit(ctx, blocks)                 // → command.UserInput
session.SubmitToLoop(ctx, loopID, blocks)  // → command.UserInput routed to loopID
session.Interrupt(ctx)                      // → command.Interrupt (priority)
session.Shutdown(ctx)                       // → command.Shutdown (priority)
session.RespondGate(ctx, gateResponse)      // → command.ApproveToolCall / DenyToolCall / ...

A loop backend (see pkg/loop.Backend) consumes commands off its CommandSink(). The native actor's Commands channel is unbuffered; priority commands travel on a separate bounded lane so an Interrupt or Shutdown is never stuck behind a saturated submit lane.

Sibling packages

  • pkg/identityidentity.Coordinates, identity.Cause, identity.Agency embedded in Header and GateRoute.
  • pkg/event — the events the actor publishes in reply to submit commands; the event.Reply set whose ReplyTo() is the command id.
  • pkg/gategate.ApprovalAction, gate.GateResponse, gate.CloseReason, mirrored verbatim by the approve/deny commands.

How it is designed

   Consumer ──► Session ──► command.UserInput / Interrupt / Shutdown / ...
                                  │
                                  │  CommandSink (Commands chan)
                                  ▼
                       ┌──────────────────────┐
                       │ Loop actor            │
                       │  select:             │
                       │   priorityCommands ──┤  ◀── Interrupt, Shutdown
                       │   commands         ──┤  ◀── submit / control
                       │   gateReg          ──┤  ◀── pending gate registrations
                       │   snapshots       ──┤  ◀── committed-state queries
                       └──────────┬───────────┘
                                  │
                                  │ outcomes published as events
                                  ▼
                                pkg/event  (via pkg/hub)
Sealed by construction

Command is interface { isCommand(); CommandHeader() Header }. The isCommand method is unexported, so only types in this package can implement it — the set of commands is closed at compile time. Adding a new command requires a matching change to the actor's select and to the session dispatch; nothing else can mint one.

Strict wire decoding

The marshal/unmarshal pair (marshal.go / validate.go) is the single strict decoder for commands crossing the wire (e.g. an HTTP gate response). ParseApprovalAction is the one validation source shared by DecodeApprovalAction and the session route, so anything but the three exact actions fails closed — a malformed or token-bearing record can neither be journaled nor restored.

Documentation

Index

Constants

View Source
const (
	CommandCancelDelegateRequest CommandName  = "CancelDelegateRequest"
	CancelDelegateRequestAck     CommandField = "Ack"
)
View Source
const (
	CommandInterrupt CommandName  = "Interrupt"
	InterruptAck     CommandField = "Ack"
)
View Source
const (
	CommandSetLoopMode         CommandName  = "SetLoopMode"
	CommandChangeLoopInference CommandName  = "ChangeLoopInference"
	SetLoopModeAck             CommandField = "Ack"
	ChangeLoopInferenceAck     CommandField = "Ack"
)
View Source
const (
	CommandReplaceLoopExternalTools CommandName  = "ReplaceLoopExternalTools"
	ReplaceLoopExternalToolsAck     CommandField = "Ack"
	ReplaceLoopExternalToolsSource  CommandField = "Source"
)
View Source
const (
	CommandProcessNotification CommandName  = "ProcessNotification"
	FieldNotification          CommandField = "Notification"
)

CommandProcessNotification/FieldNotification name this command for CommandValidationError, alongside the shared vocabulary in validate.go.

View Source
const (
	CommandShutdown CommandName  = "Shutdown"
	ShutdownAck     CommandField = "Ack"
)
View Source
const (
	CommandUserInput         CommandName = "UserInput"
	CommandSubagentResult    CommandName = "SubagentResult"
	CommandCancelQueuedInput CommandName = "CancelQueuedInput"
	CommandApproveToolCall   CommandName = "ApproveToolCall"
	CommandDenyToolCall      CommandName = "DenyToolCall"
	CommandProvideUserInput  CommandName = "ProvideUserInput"
	CommandCompact           CommandName = "Compact"
	CommandUnknown           CommandName = "Command"

	FieldCommandID             CommandField = "CommandID"
	FieldSessionID             CommandField = "SessionID"
	FieldLoopID                CommandField = "LoopID"
	FieldTargetCommandID       CommandField = "TargetCommandID"
	FieldTargetLoopID          CommandField = "TargetLoopID"
	FieldBackgroundHandBack    CommandField = "BackgroundHandBack"
	FieldDelegateDeliveryPhase CommandField = "DelegateDeliveryPhase"
	FieldToolExecutionID       CommandField = "ToolExecutionID"
	FieldAgency                CommandField = "Agency"
	FieldAction                CommandField = "Action"
)

Command/field names for CommandValidationError. The CommandName/CommandField types are reused from command.go (the existing InvalidCommandError vocabulary).

Variables

This section is empty.

Functions

func MarshalCommand

func MarshalCommand(cmd Command) ([]byte, error)

MarshalCommand encodes a Command into the durable intent-log wire envelope: a JSON object carrying a "type" discriminator (== the CommandName naming source, the package's single source of truth), a "v" schema version, the embedded Header fields, and the type-specific payload. It fails closed on a type outside the sealed union (UnknownCommandTypeError). The transient ack channels (Interrupt.Ack/Shutdown.Ack, tagged json:"-") never serialize. The content blocks (UserInput.Blocks/SubagentResult.Blocks) are a sealed-interface slice with no general codec, so they are delegated to content.MarshalBlocks; every other field round-trips through encoding/json.

func ValidateCommand

func ValidateCommand(cmd Command) error

ValidateCommand checks cmd against the ID fill matrix and returns a typed *CommandValidationError on the first violation, nil when cmd satisfies every invariant. CommandID is required on every command; per-type addressing rules then apply (UserInput's machine delegate target/phase marker, SubagentResult/ CancelQueuedInput coordinates, and the gate-reply GateRoute). Interrupt and Shutdown are session-wide control commands with no addressing today, so they are not validated here (their Ack-channel contract is checked by their own Validate). Fail-secure: a command type outside the addressed set passes only the universal CommandID check.

Types

type ApproveToolCall

type ApproveToolCall struct {
	Header
	// GateRoute locates the loop (the session dispatches by LoopID) and names the
	// pending gate (ToolExecutionID), which the actor matches against.
	GateRoute
	// Action is gate.ApprovalApprove or gate.ApprovalApproveAlwaysWorkspace.
	// Any other value — including gate.ApprovalDeny, which travels on
	// DenyToolCall — fails ValidateCommand closed, so a malformed or legacy
	// record can never decode into an approval.
	Action gate.ApprovalAction `json:"action"`
}

ApproveToolCall approves the pending tool call identified by ToolExecutionID with exactly one of the two approve actions. It is a fire-and-route control command: the actor routes it by GateToolExecutionID to the permission gate blocked on that call, so there is no Ack (the gate's unblocking and the subsequent ToolCallStarted event are the observable effect, not a reply on this command).

The command carries no scope, no grant tokens, and no persistence payload: gate.ApprovalApprove approves once and writes nothing, while gate.ApprovalApproveAlwaysWorkspace additionally instructs the evaluator to atomically persist the displayed reusable rule candidates before execution. Fresh execution-bound grants are minted by the evaluator AFTER the decision and travel only in the prepared execution contract, never on this wire.

func (ApproveToolCall) GateToolExecutionID

func (c ApproveToolCall) GateToolExecutionID() uuid.UUID

GateToolExecutionID returns the tool-call id this command targets, so the actor can route it to the matching pending gate.

type CancelDelegateRequest

type CancelDelegateRequest struct {
	Header
	identity.Coordinates
	TargetCommandID uuid.UUID                   `json:"target_command_id,omitzero"`
	Ack             chan<- DelegateCancelResult `json:"-"`
}

CancelDelegateRequest atomically cancels one managed request on one loop.

func (CancelDelegateRequest) Validate

func (c CancelDelegateRequest) Validate() error

type CancelQueuedInput

type CancelQueuedInput struct {
	Header
	identity.Coordinates
	TargetCommandID uuid.UUID `json:"target_command_id,omitzero"`
}

CancelQueuedInput retracts a still-queued submit (a UserInput/SubagentResult that received an InputQueued disposition). It is routed to the loop like the gate commands; the loop resolves it against its OWN inbox (race-free, since the actor is the sole owner of the queue), so there is no session-side TOCTOU.

Coordinates selects the target loop; TargetCommandID names the queued submit to remove (it is the submit command's Header.CommandID, returned in InputQueued.Cause.CommandID).

It is fire-and-forget — there is no Ack. Its outcome is observable as events, not a point-to-point reply: when the input was still queued the loop publishes the Enduring event.InputCancelled{CancelClientRetracted} keyed by TargetCommandID. When it had already started or folded into a turn — or was never queued — the retract is a pure no-op: the issuer infers "already committed / unknown" from the event.TurnStarted / event.TurnFoldedInto it already saw for that command.

type ChangeLoopInference

type ChangeLoopInference struct {
	Header
	Model     model.Model             `json:"model,omitzero"`
	Effort    model.Effort            `json:"effort,omitzero"`
	SetModel  bool                    `json:"set_model,omitzero"`
	SetEffort bool                    `json:"set_effort,omitzero"`
	Ack       chan<- LoopChangeResult `json:"-"` // live reply channel; no JSON representation
}

ChangeLoopInference changes only the secret-free model descriptor and/or the inference effort. Like SetLoopMode it is a CONTROL command on a live reply channel — its durable record is the event.LoopInferenceChanged the actor emits — so it is not in the wire codec. SetModel/SetEffort select which of Model/Effort the batch changes; the whole batch is validated atomically by the actor before anything is applied. The change takes effect at the NEXT turn boundary; a running turn keeps the model/effort it started under. Ack is required and must be non-nil and buffered(1).

func (ChangeLoopInference) Validate

func (c ChangeLoopInference) Validate() error

Validate checks the reply-channel contract: Ack must be present AND buffered (cap >= 1), like SetLoopMode. The model/effort VALUES are validated by the actor against the loop definition (atomically), not here.

type Command

type Command interface {
	CommandHeader() Header
	// contains filtered or unexported methods
}

Command is a sealed interface for all loop commands. Only types in this package can implement it.

func UnmarshalCommand

func UnmarshalCommand(data []byte) (Command, error)

UnmarshalCommand decodes a durable intent-log wire envelope back into a concrete Command. It fails closed on the untrusted restore boundary: input over the byte cap → CommandLimitError; malformed envelope → CommandDecodeError; an unknown or missing "type" tag → UnknownCommandTypeError; a malformed payload for a known type → CommandDecodeError. A successfully decoded command is validated against the ID fill matrix (ValidateCommand), so a structurally-valid but semantically- invalid record is rejected rather than resurrected. The transient ack channels are never on the wire, so a restored Interrupt/Shutdown carries a nil Ack.

type CommandDecodeError

type CommandDecodeError struct {
	Type  CommandName
	Cause error
}

CommandDecodeError wraps a failure to unmarshal serialized command bytes (malformed JSON, wrong field types) once a known "type" tag has been read, or the initial envelope probe failure (nil/garbage/non-object), in which case Type is empty.

func (*CommandDecodeError) Error

func (e *CommandDecodeError) Error() string

func (*CommandDecodeError) Unwrap

func (e *CommandDecodeError) Unwrap() error

type CommandEncodeError

type CommandEncodeError struct {
	Type  CommandName
	Cause error
}

CommandEncodeError wraps a failure to marshal a command's payload (a json.Marshal failure, or a delegated content-block codec failure on the marshal path).

func (*CommandEncodeError) Error

func (e *CommandEncodeError) Error() string

func (*CommandEncodeError) Unwrap

func (e *CommandEncodeError) Unwrap() error

type CommandField

type CommandField string
const ReplaceLoopExternalToolsTools CommandField = "Tools"

ReplaceLoopExternalToolsTools names the Tools/Identities alignment violation.

type CommandLimitError

type CommandLimitError struct {
	Got int
	Max int
}

CommandLimitError is returned when serialized input exceeds the envelope byte cap at the untrusted decode boundary.

func (*CommandLimitError) Error

func (e *CommandLimitError) Error() string

type CommandName

type CommandName string

type CommandValidationError

type CommandValidationError struct {
	Command CommandName
	Field   CommandField
	Rule    Rule
}

CommandValidationError reports that a command violates the ID fill matrix: Field names the offending identity/addressing field and Rule says why (required). It is a typed package-API error so a journal/test can errors.As it to inspect the exact violation rather than parse a string. It is distinct from InvalidCommandError (which guards required-channel contracts like Interrupt.Ack) so the two failure modes never alias at a call site.

func (*CommandValidationError) Error

func (e *CommandValidationError) Error() string

type Compact

type Compact struct {
	Header
	identity.Coordinates
}

Compact requests conversation compaction for one exact loop. Agency records whether the trusted dispatcher admitted a manual or automatic request.

type DelegateCancelResult

type DelegateCancelResult uint8

DelegateCancelResult is the actor-authoritative outcome of a targeted request cancellation. It is transient control-plane state and is never serialized.

const (
	DelegateCancelNoop DelegateCancelResult = iota
	DelegateCancelQueued
	DelegateCancelActive
)

type DelegateDeliveryPhase

type DelegateDeliveryPhase string

DelegateDeliveryPhase is the durable phase marker on a machine delegate UserInput. The command record remains the source of truth for the request payload and request id; this marker lets restore distinguish an accepted intent from the one fallback admission that was already journaled. The zero value is intentionally unset for ordinary interactive input and legacy records.

const (
	// DelegateDeliveryPhaseIntent means the exact command record was accepted
	// durably but has not yet been admitted to the actor path.
	DelegateDeliveryPhaseIntent DelegateDeliveryPhase = "intent"
	// DelegateDeliveryPhaseFallbackQueued means the exact command record was
	// durably admitted once as the normal fallback; restore may re-admit that
	// same command id if no opening/cancellation event exists.
	DelegateDeliveryPhaseFallbackQueued DelegateDeliveryPhase = "fallback_queued"
)

func (DelegateDeliveryPhase) Valid

func (p DelegateDeliveryPhase) Valid() bool

Valid reports whether p is one of the non-zero durable delegate phases.

type DenyToolCall

type DenyToolCall struct {
	Header
	// GateRoute locates the loop (the session dispatches by LoopID) and names the
	// pending gate (ToolExecutionID), which the actor matches against.
	GateRoute
}

DenyToolCall denies a pending tool call identified by ToolExecutionID. Like ApproveToolCall it is a fire-and-route control command with no Ack: the actor routes it by GateToolExecutionID to the permission gate, which fails the call closed (fail-secure). Denial carries no scope — nothing is ever persisted on a deny.

func (DenyToolCall) GateToolExecutionID

func (c DenyToolCall) GateToolExecutionID() uuid.UUID

GateToolExecutionID returns the tool-call id this command targets, so the actor can route it to the matching pending gate.

type GateRoute

type GateRoute struct {
	identity.Coordinates
	GateID          uuid.UUID `json:"gate_id,omitzero"`
	ToolExecutionID uuid.UUID `json:"tool_execution_id,omitzero"`
}

GateRoute is the routing key for a gate reply. identity.Coordinates locates the target loop — the session dispatches by GateRoute.LoopID — and ToolExecutionID names the pending gate the loop matches against (its pendingGates key). It is embedded in the gate commands so a reply carries both the dispatch target (the loop) and the match key (the tool call), replacing the former route-to-primary.

A zero coordinate means "unspecified at this granularity": a GateRoute with only LoopID set addresses a loop without naming a turn. The loop resolves the gate by ToolExecutionID, so the coordinates are the addressing envelope, not the match key.

type Header struct {
	CommandID uuid.UUID       `json:"command_id,omitzero"` // fresh per command instance, stamped by the sender before send
	Cause     identity.Cause  `json:"cause,omitzero"`      // the direct cause of this command; zero = root (user-initiated)
	Agency    identity.Agency `json:"agency,omitzero"`     // who issued this command; AgencyMachine (zero) by default

	// CreatedAt is when this command was created, stamped at the session dispatch
	// boundary from the injected clock (mirrors event.Header.CreatedAt: minted at
	// creation, not delivery). It is the journal's creation timestamp for the
	// intent-log record and round-trips through the command codec as-is. Zero means
	// the sender supplied none (omitzero drops it), so an in-process command that is
	// never persisted is unaffected.
	CreatedAt time.Time `json:"created_at,omitzero"`
}

Header is the correlation/idempotency metadata embedded in every command. The sender stamps the fields before sending the command (the session does this via newCommandID); zero-valued fields mean the sender supplied none.

func (Header) CommandHeader

func (h Header) CommandHeader() Header

CommandHeader is promoted onto every command that embeds Header.

type Interrupt

type Interrupt struct {
	Header
	Ack chan<- bool `json:"-"` // live reply channel; no JSON representation
}

Interrupt cancels the running turn. Ack receives true if a turn was cancelled, false if idle or the session is already shutting down. Ack is required and must be non-nil.

func (Interrupt) Validate

func (c Interrupt) Validate() error

Validate checks that all required fields are non-nil.

type InvalidCommandError

type InvalidCommandError struct {
	Command CommandName
	Field   CommandField
}

InvalidCommandError is returned when an internal caller violates a command contract.

func (*InvalidCommandError) Error

func (e *InvalidCommandError) Error() string

type LoopChangeResult

type LoopChangeResult struct {
	Err    error
	Mode   string
	Model  model.Model
	Effort model.Effort
}

LoopChangeResult is the loop actor's synchronous reply to a SetLoopMode or ChangeLoopInference command. Err is the typed failure (nil on success); on success Mode/Model/Effort report the EFFECTIVE mode, secret-free model descriptor, and effort the actor committed — the values the NEXT turn will start under. The session controller updates its live Handle view from these committed values so Handle.Mode()/Model() reflect the current selection. It rides a live channel and is never serialized (the ack channel is json:"-").

type LoopTerminatedError

type LoopTerminatedError struct{ Cause error }

LoopTerminatedError is sent on Shutdown.Ack when the loop's root context was cancelled before the actor finished cleanup.

func (*LoopTerminatedError) Error

func (e *LoopTerminatedError) Error() string

func (*LoopTerminatedError) Unwrap

func (e *LoopTerminatedError) Unwrap() error

type LoopToolsResult

type LoopToolsResult struct {
	Err        error
	Generation string
	Installed  int
}

LoopToolsResult is the loop actor's synchronous reply to ReplaceLoopExternalTools. Err is the typed failure (nil on success); on success Generation echoes the installed generation and Installed reports how many external tools that source now contributes to the next turn. It rides a live channel and is never serialized.

type ProcessNotification

type ProcessNotification struct {
	Header
	Notification tool.ProcessCompletionNotification `json:"notification"`

	// Result is the transient, optional live disposition channel: nil for a
	// restore-reconstructed redelivery (fire-and-forget — restore seeds the
	// loop directly rather than replaying this command live) or for a decoded
	// wire record (json:"-": it never rides the durable payload), and a
	// caller-owned buffered channel for a live NotifyProcessCompletion call
	// awaiting the loop's disposition.
	Result chan<- ProcessNotificationResult `json:"-"`
}

ProcessNotification is the sealed, durable wire command that carries Task 4's metadata-only tool.ProcessCompletionNotification DTO to its owning loop. It is deliberately as narrow as the DTO it wraps: it adds only the generic command envelope (Header) and a transient live disposition channel — never a command, output, stdin, host path, OS PID, or any other free-form field. Restore reconstructs it directly from the durable intent log (round-tripping through the sealed codec below) to detect and re-enqueue undelivered notifications.

Header.CommandID and Notification.CommandID carry the SAME stable, pre-persisted id Tools allocates before a completion can be published. Header.CommandID is the generic envelope field the journal's idempotency index and the intent-log CommandRecord key on (mirroring every other sealed command); Notification.CommandID is Task 4's own DTO field. ValidateCommand rejects any disagreement fail-secure rather than silently preferring one.

type ProcessNotificationResult

type ProcessNotificationResult uint8

ProcessNotificationResult is the transient, non-serialized outcome of dispatching a ProcessNotification to its owning loop (Task 24C). It answers "what happened to THIS delivery attempt", never a durable record:

  • ProcessNotificationAccepted: the durable append genuinely persisted a NEW frame (or no-persistence/headless mode has no durable concept at all) and the loop took ownership of the notification.
  • ProcessNotificationDuplicate: the append's idempotency id already named an IDENTICAL durable record (a same-CommandID retry) — no second frame was written; the loop may still have been (re)delivered the value, which is harmless because a completion notification is metadata-only.
  • ProcessNotificationCollision: the append's idempotency id already named a durable record with a DIFFERENT persisted payload — a genuine id reuse bug or forged retry. Never dispatched to the loop.
  • ProcessNotificationStopped: delivery could not complete right now — the owning loop has exited, its bounded live notification set is full, or the addressed loop cannot accept process notifications at all (a foreign engine). The durable command (once appended) remains authoritative: the caller may retry dispatch later with the SAME CommandID.
const (
	ProcessNotificationAccepted ProcessNotificationResult = iota + 1
	ProcessNotificationDuplicate
	ProcessNotificationCollision
	ProcessNotificationStopped
)

type ProvideUserInput

type ProvideUserInput struct {
	Header
	// GateRoute locates the loop (the session dispatches by LoopID) and names the
	// pending gate (ToolExecutionID), which the actor matches against.
	GateRoute
	Answer string `json:"answer,omitempty"`
}

ProvideUserInput supplies the user's Answer to a pending AskUser request identified by ToolExecutionID. Like the approve/deny pair it is a fire-and-route control command with no Ack: the actor routes it by GateToolExecutionID to the user-input gate blocked on that call, which delivers Answer to the waiting tool.

func (ProvideUserInput) GateToolExecutionID

func (c ProvideUserInput) GateToolExecutionID() uuid.UUID

GateToolExecutionID returns the tool-call id this command targets, so the actor can route it to the matching pending gate.

type ReplaceLoopExternalTools

type ReplaceLoopExternalTools struct {
	Header
	Source     string                       `json:"source,omitzero"`
	Generation string                       `json:"generation,omitzero"`
	Tools      []tool.InvokableTool         `json:"-"` // live values; no JSON representation
	Identities []event.ExternalToolIdentity `json:"-"` // durable projection of Tools
	Ack        chan<- LoopToolsResult       `json:"-"` // live reply channel
}

ReplaceLoopExternalTools atomically REPLACES one source's external tool slot on a loop. Like SetLoopMode it is a CONTROL command carried on a live reply channel, not a journaled wire command: its durable, replayable record is the event.LoopExternalToolsetChanged the actor emits, so it is deliberately absent from the intent-log codec. The change takes effect at the NEXT turn boundary; a running turn keeps the toolset it started under.

Tools are ALREADY BUILT. Building is the session's job, not the actor's: the session owns the loop's tool.Bindings (the actor only ever receives a BoundDefinition), and an external factory may perform I/O — building on the actor goroutine would stall the loop and, with it, the very idle detection this feature depends on. Identities is the matching durable identity projection, computed from the built tools by the same caller so the emitted record cannot drift from what is installed; it is index-aligned with Tools.

An empty Tools is legal: it clears the source's slot. Ack is required and must be non-nil and buffered(1).

func (ReplaceLoopExternalTools) Validate

func (c ReplaceLoopExternalTools) Validate() error

Validate checks the actor-protecting structural contract: Ack must be present AND buffered (cap >= 1) — the actor replies with a single non-blocking direct send, so an unbuffered Ack would wedge it — and Tools must be index-aligned with Identities, since the actor installs Tools while the journal records Identities and a mismatch would make the durable record a lie. Source is required here because it names the slot the actor would otherwise replace ambiguously. The tool VALUES and the name-collision rules are validated by the caller and the actor, not here.

type Rule

type Rule string

Rule is the human-readable invariant a CommandValidationError records, so the caller learns WHY a field is wrong, not just which.

const (
	// RuleRequired: the field must be non-zero for this command.
	RuleRequired Rule = "must be set"
	RuleInvalid  Rule = "is invalid"
)

type SetLoopMode

type SetLoopMode struct {
	Header
	Mode string                  `json:"mode,omitzero"`
	Ack  chan<- LoopChangeResult `json:"-"` // live reply channel; no JSON representation
}

SetLoopMode selects one predeclared loop mode. It is a CONTROL command carried on a live reply channel (Ack), not a journaled wire command: its durable, replayable record is the event.LoopModeChanged the actor emits, which restore folds — so it is deliberately absent from the intent-log codec. The actor validates the mode name against the loop's bound definition, emits the enduring event, and applies the change at the NEXT turn boundary; the running turn keeps the mode it started under. An empty Mode names the base mode. Ack is required and must be non-nil and buffered(1).

func (SetLoopMode) Validate

func (c SetLoopMode) Validate() error

Validate checks the reply-channel contract: Ack must be present AND buffered (cap >= 1). The actor delivers the reply with a single non-blocking direct send, so an unbuffered Ack would wedge the actor; both violations are typed so a caller can errors.As them.

type Shutdown

type Shutdown struct {
	Header
	Ack chan<- error `json:"-"` // live reply channel; no JSON representation
}

Shutdown cancels the running turn (if any), delivers its terminal event, and exits the actor. Ack receives nil after clean exit, or *LoopTerminatedError if the loop's root context was cancelled before cleanup completed. Ack is required and must be non-nil.

func (Shutdown) Validate

func (c Shutdown) Validate() error

Validate checks that all required fields are non-nil.

type SubagentResult

type SubagentResult struct {
	Header                               // command.Header; Cause.LoopID = CHILD loop; Agency = AgencyMachine
	identity.Coordinates                 // addresses the PARENT loop (delivery target)
	Blocks               []content.Block `json:"blocks,omitempty"`
}

SubagentResult delivers a finished subagent's output to its parent loop (the hand-back). It shares UserInput's submit semantics — the parent loop's events go to the session fan-in.

It carries TWO loop ids with distinct jobs:

  • The embedded identity.Coordinates addresses the PARENT loop — the delivery target. The session dispatches the command to loops[Coordinates.LoopID].
  • Header.Cause.LoopID is the CHILD loop that produced the result. When the parent folds the result into a turn, the loop stamps this Cause.LoopID onto any start/queue/fold/return event the submit causes, which releases the parent's quiescence wake token on the publish path.

Header.Agency stays AgencyMachine (the zero default): a hand-back is machine-originated, never user.

A SubagentResult is NEVER rejected, so its wake token is ALWAYS released by a published Enduring event (TurnStarted/TurnFoldedInto, or InputCancelled if the loop ends before it commits) — there is no off-publish-path reconciliation anymore.

type UnbufferedAckError

type UnbufferedAckError struct {
	Command CommandName
	Field   CommandField
}

UnbufferedAckError reports that a loop-change command's live reply channel is present but unbuffered (cap < 1). The loop actor replies with a single non-blocking direct send, so an unbuffered Ack would wedge it; the contract requires a buffered(1) channel. It is distinct from InvalidCommandError (a MISSING channel) so the two failure modes never alias.

func (*UnbufferedAckError) Error

func (e *UnbufferedAckError) Error() string

type UnknownCommandTypeError

type UnknownCommandTypeError struct{ Type CommandName }

UnknownCommandTypeError is returned by UnmarshalCommand when the envelope's "type" tag names no concrete command (including the empty/missing tag), or by MarshalCommand when a foreign concrete type is handed in (one not in classifyCommand's sealed union). The restore path is an untrusted boundary; callers fail secure on this error rather than guess a concrete command to reconstruct.

func (*UnknownCommandTypeError) Error

func (e *UnknownCommandTypeError) Error() string

type UserInput

type UserInput struct {
	Header
	Blocks []content.Block `json:"blocks,omitempty"`
	// NoFold requests a DISTINCT non-folding turn: the input still queues behind a
	// running turn, but it NEVER folds into that turn at a tool-continuation boundary —
	// it starts its OWN turn (Cause.CommandID = this command's id) when the running turn
	// finishes. It is the delegation follow-up path, where each request is an independent
	// question/answer correlated by command id; the interactive submit path leaves it
	// false so ordinary input keeps its fold-into-turn semantics.
	NoFold bool `json:"no_fold,omitzero"`
	// TargetLoopID durably carries the dispatch target for machine NoFold or phased
	// delegate requests because storage replay cannot recover CommandRecord's
	// transport-only loop.
	TargetLoopID uuid.UUID `json:"target_loop_id,omitzero"`
	// BackgroundHandBack durably marks the narrow managed-delegation request shape that
	// requires automatic background parent hand-back after the child terminal commits.
	// Legacy no-fold hand-backs remain valid; a foldable hand-back must carry a valid
	// non-zero DelegateDeliveryPhase alongside its durable target identity. Foreground
	// delegate requests and ordinary user input leave it false.
	BackgroundHandBack bool `json:"background_hand_back,omitzero"`
	// DelegateDeliveryPhase is the durable phase marker for machine MessageAgent
	// delivery. Intent and fallback_queued are journaled together with this exact
	// command record, so the actor payload and fallback phase cannot diverge.
	DelegateDeliveryPhase DelegateDeliveryPhase `json:"delegate_delivery_phase,omitzero"`
	// Accepted is the transient durable-acceptance ack used only by managed delegate
	// sends. It is never serialized; prepared starts use LoopStarted.InitialRequestID.
	Accepted chan error `json:"-"`
}

UserInput is interactive input. The loop decides its outcome; the caller never assumes a turn was created. Submit commands DO NOT carry a context (no Ctx field): a queued input can start much later, fold, be cancelled, or be returned, so the loop derives the turn context from its own loopCtx only when a turn actually starts. A UserInput may queue behind a running turn (it later folds into a tool-continuation request or starts a later turn).

The loop announces the outcome by PUBLISHING a typed Reply event onto the normal session fan-in (event.TurnStarted / event.InputQueued / event.TurnRejected, each carrying Cause.CommandID == this command's id), NOT a point-to-point reply: every submit observes its outcome on the session event fan-in.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL