Skip to documentation
Documentation navigation

Documentation navigation

Documentation / guides

Submit input

Submit content to the active Loop or target a specific live Loop.

developer

Session.Submit is fire-and-forget admission, not a synchronous turn runner. It returns the fresh command ID once the active loop accepts the command for delivery. The loop then decides whether the input starts a turn, waits in its bounded inbox, folds into a tool continuation, or is rejected. Every outcome is an event on the session fan-in and is correlated by Header.Cause.CommandID.

Public entry points

The data-plane contract exposes both active-loop and exact-loop submission:

type Session interface {
	Submit(context.Context, []content.Block) (uuid.UUID, error)
	SubmitToLoop(context.Context, uuid.UUID, []content.Block) (uuid.UUID, error)
	SubscribeEvents(event.EventFilter) (event.Subscription, error)
}

Submit samples the active loop once and stamps AgencyUser. SubmitToLoop addresses the supplied loop and also represents human-authored input. Neither method returns TurnDone or TurnFailed; a non-nil error means the command was not handed to a loop and the returned ID is the zero UUID. Typical transport errors are *session.SessionError with SessionLoopNotFound, SessionLoopExited, SessionContextDone, or SessionFaulted.

func submit(ctx context.Context, s session.Session) error {
	sub, err := s.SubscribeEvents(event.EventFilter{
		Enduring: event.LoopScope{All: true},
	})
	if err != nil {
		return err
	}
	defer sub.Close()

	commandID, err := s.Submit(ctx, []content.Block{
		&content.TextBlock{Text: "Summarize the latest test failure."},
	})
	if err != nil {
		return err
	}
	for delivery := range sub.Events() {
		if reply, ok := delivery.Event.(event.Reply); ok &&
			reply.ReplyTo() == commandID {
			// InputQueued is provisional. Keep reading until a terminal or
			// resolution event for this command arrives.
			if _, terminal := delivery.Event.(event.TurnRejected); terminal {
				return errors.New("the loop rejected the input")
			}
		}
	}
	return sub.Err()
}

This is compile-realistic consumer code: the caller receives a live session.Session from the composition root and does not construct an internal command. The pkg/session/session.go contract and internal/sessionruntime/submit_test.go are the relevant source and proof.

Command shapes

The durable command type for interactive input is:

type UserInput struct {
	Header
	Blocks                []content.Block `json:"blocks,omitempty"`
	NoFold                bool             `json:"no_fold,omitzero"`
	TargetLoopID          uuid.UUID        `json:"target_loop_id,omitzero"`
	BackgroundHandBack    bool             `json:"background_hand_back,omitzero"`
	DelegateDeliveryPhase DelegateDeliveryPhase `json:"delegate_delivery_phase,omitzero"`
	Accepted              chan error       `json:"-"`
}

The runtime’s machine delegate path also uses this type. The fields have narrow semantics:

FieldMeaning and validation
Blockssealed core content blocks; they are encoded through the content codec
NoFoldmachine-only request for its own later turn; it can still wait behind a running turn
TargetLoopIDdurable machine dispatch target; required for machine NoFold, a background hand-back, or a delivery phase
BackgroundHandBackmachine marker requesting automatic parent hand-back after a managed child completes
DelegateDeliveryPhasemachine-only durable phase, either intent or fallback_queued; ordinary interactive input leaves it empty
Acceptedtransient managed-delegate acceptance channel; never serialized
type DelegateDeliveryPhase string

const (
	DelegateDeliveryPhaseIntent         DelegateDeliveryPhase = "intent"
	DelegateDeliveryPhaseFallbackQueued DelegateDeliveryPhase = "fallback_queued"
)

func (p DelegateDeliveryPhase) Valid() bool

An ordinary interactive UserInput has a command ID and blocks. It does not carry a context. The loop derives the turn context only when it actually starts the turn, so caller cancellation after admission cannot invalidate a future queued item.

What the event stream means

EventClassMeaning
event.InputQueuedEphemeral replyadmitted to the inbox, but not assigned to a turn yet
event.TurnStartedEnduring replythis command’s message became the first message of a new turn
event.TurnFoldedIntoEnduring replythis command was folded into a mandatory tool-continuation request
event.TurnRejectedEnduring replyqueue full, loop shutting down, or transient internal failure
event.InputCancelledEnduring eventa queued item was retracted or returned after an abnormal turn end

InputQueued can be dropped by a bounded subscription because the later resolution is authoritative. TurnStarted, TurnFoldedInto, TurnRejected, and InputCancelled are enduring and journal-backed where applicable. A queue full rejection is event.RejectQueueFull; a retryable internal admission failure is event.RejectInternal.

%%{init: {"theme":"dark"}}%%
stateDiagram-v2
    [*] --> Admitted: UserInput accepted
    Admitted --> Queued: running turn exists
    Admitted --> Started: loop idle
    Queued --> Folded: tool continuation accepts input
    Queued --> Started: next turn boundary
    Queued --> Cancelled: client retract or abnormal return
    Admitted --> Rejected: queue full / shutting down / internal failure
    Started --> [*]
    Folded --> [*]
    Cancelled --> [*]
    Rejected --> [*]

Delegate hand-backs

Managed delegation uses a second submit shape:

type SubagentResult struct {
	Header
	identity.Coordinates
	Blocks []content.Block `json:"blocks,omitempty"`
}

The embedded coordinates name the parent loop that receives the result. The child loop that produced it is Header.Cause.LoopID; these IDs must not be swapped. The hand-back is machine agency by default and is never rejected. Its parent resolution event releases the child wake token even when the parent is ending. The exact two-ID contract is proved by pkg/command/submit_test.go.

Durability and restore

The runtime appends ordinary submit intent before dispatch when a journal is configured. A failed audit append does not block a user input from reaching a healthy loop. Machine delegate requests use their intent and fallback_queued phases so restore can distinguish an accepted durable intent from the one fallback admission that was already journaled. Block payloads and correlation IDs are restored; Accepted is not. On a restore path, the runtime creates a new live delivery channel and routes the exact command ID, rather than pretending that an old channel survived the process boundary.

For cancellation of an admitted-but-not-started item, see Cancel queued input. For the gate commands that can interrupt a parked tool step, see Approve and deny and Provide requested user input.

Source and proof

← back to documentation