Documentation / guides
Submit and Admission
Describe how session.Session.Submit admits input to a conceptual Harness Turn and produces TurnStarted.
Submission and Turn execution are separate boundaries. Session.Submit sends a human-authored command.UserInput to the current active loop and returns the minted input ID. It does not wait for TurnStarted, a queue decision, or a terminal event. Session.SubmitToLoop has the same semantics but addresses one specific registered loop.
Public submit contract
// From github.com/looprig/harness/pkg/session.
type Session interface {
SessionID() uuid.UUID
ActiveLoop() loop.Handle
Loop(uuid.UUID) (loop.Handle, bool)
Submit(context.Context, []content.Block) (uuid.UUID, error)
SubmitToLoop(context.Context, uuid.UUID, []content.Block) (uuid.UUID, error)
// Other data-plane and control-plane methods follow in the public interface.
}
The returned UUID is the submit command’s Header.CommandID. Resolution events copy it to Header.Cause.CommandID, and every event.Reply exposes it through ReplyTo().
The context.Context bounds the handoff to the loop command channel. Once the loop accepts the command, the Turn derives its own context from the loop lifetime. Canceling the submit context after the send does not cancel an admitted Turn.
Admission outcomes
The actor decides against its own queue and lifecycle state, so there is no session-side time-of-check/time-of-use race:
| Loop state | Result event | Class | Meaning |
|---|---|---|---|
| Idle | event.TurnStarted | Enduring Reply | Input was committed as the Turn’s initial user message. |
| Running, waiting for execution admission, or compaction-blocked | event.InputQueued | Ephemeral Reply | Input was accepted into the actor-owned inbox and awaits resolution. |
| Inbox at capacity | event.TurnRejected{Reason: event.RejectQueueFull} | Enduring Reply | Input was not admitted. |
| Shutting down | event.TurnRejected{Reason: event.RejectShuttingDown} | Enduring Reply | Input was not admitted. |
| Transient actor or ID failure | event.TurnRejected{Reason: event.RejectInternal} | Enduring Reply | Loop is healthy; caller may retry. |
RejectUnspecified is the zero sentinel and is not produced by the runtime. A successful method return means only that the command was handed to the loop. A non-nil method error means no usable correlation ID is returned: the UUID is zero for context cancellation, an exited loop, an unknown target, session fault, or ID generation failure.
Submit to a selected loop
Submit samples the active loop once. SubmitToLoop does not follow later active-loop changes; it targets the UUID passed by the caller. Both public methods stamp identity.AgencyUser onto the command, so the resulting TurnStarted, TurnFoldedInto, or InputCancelled carries that agency in Header.Cause.Agency.
%%{init: {"theme":"dark"}}%%
sequenceDiagram
participant C as caller
participant S as session
participant L as target loop actor
participant F as session fan-in
C->>S: Submit(ctx, blocks)
S->>S: sample active loop and mint InputID
S->>L: command.UserInput
L-->>S: handoff complete
S-->>C: InputID, nil
alt idle
L->>F: TurnStarted{Cause.CommandID: InputID}
else busy
L->>F: InputQueued{Cause.CommandID: InputID}
L->>F: TurnFoldedInto or later TurnStarted or InputCancelled
else refused
L->>F: TurnRejected{Cause.CommandID: InputID}
end
Correlate the answer
package example
import (
"context"
"fmt"
"github.com/looprig/harness/pkg/event"
"github.com/looprig/harness/pkg/session"
)
func submitAndCorrelate(ctx context.Context, live session.Session) error {
sub, err := live.SubscribeEvents(event.EventFilter{Enduring: event.LoopScope{All: true}})
if err != nil {
return err
}
defer sub.Close()
inputID, err := live.Submit(ctx, nil)
if err != nil {
return fmt.Errorf("handoff failed: %w", err)
}
for delivery := range sub.Events() {
reply, ok := delivery.Event.(event.Reply)
if !ok || reply.ReplyTo() != inputID {
continue
}
switch reply.(type) {
case event.TurnStarted, event.TurnFoldedInto, event.InputCancelled, event.TurnRejected:
fmt.Printf("input %v resolved as %T\n", inputID, reply)
return nil
}
}
return sub.Err()
}
This example requests only enduring events. It will not see InputQueued, which is intentionally ephemeral. If the UI needs the immediate queue acknowledgement, include Ephemeral: event.LoopScope{All: true} in the filter and continue waiting for the later authoritative resolution event.