Skip to documentation
Documentation navigation

Documentation navigation

Documentation / guides

Commit Boundary

Describe the event.StepDone commit boundary for a completed conceptual Harness Step.

developer

event.StepDone is the durable commit record for a completed conceptual Step. It is emitted by the loop actor at the same boundary where the finalized Step group is appended to the loop’s committed history. A StepDone event is therefore an authoritative record, not an optimistic progress notification.

Public record

type StepDone struct {
	enduring
	loopScoped
	Header
	Messages content.AgenticMessages `json:"messages,omitempty"`
}

The Header.Coordinates identity contract is strict. A valid StepDone needs non-zero SessionID, LoopID, TurnID, StepID, and EventID; StepID without TurnID is invalid. event.ValidateEvent also requires the message-group shape described in Tool Calls and Results.

What commits

For a text-only Step, the commit candidate contains one assistant message. For a tool Step, it contains the assistant message followed by all tool-result messages. The actor performs these operations in order:

  1. stamp the StepDone event with its producer coordinates and EventID;
  2. preflight the durable context mutation;
  3. run the checked durable event boundary;
  4. append the candidate messages to committed loop history only after the boundary reports success; and
  5. acknowledge the parked Turn goroutine.

The history clone and the event payload are independent allocations. A consumer cannot mutate the live history by modifying StepDone.Messages, and the runtime cannot mutate the payload after it has been published.

%%{init: {"theme":"dark"}}%%
stateDiagram-v2
    [*] --> Streaming
    Streaming --> Materialized: EOF with usable output
    Materialized --> ToolBatch: tool uses present
    Materialized --> CommitCandidate: no tool uses
    ToolBatch --> CommitCandidate: results appended
    CommitCandidate --> DurableBoundary
    DurableBoundary --> Committed: append succeeds
    Committed --> StepDone
    DurableBoundary --> Discarded: context canceled or append fails
    Discarded --> TurnInterrupted: cancellation boundary
    Discarded --> TurnFailed: non-cancellation boundary failure
    StepDone --> [*]

Rollback scope

Rollback is step-granular, not whole-Turn. If the provider fails, the response is empty, a tool batch is canceled, or the commit handshake is canceled, the in-flight Step is discarded and no StepDone is emitted for it. Any earlier StepDone records in the same Turn remain committed. The enclosing Turn then publishes TurnFailed for a non-cancellation error or TurnInterrupted when the Turn context was canceled.

This is the important distinction:

ObservationMeaning
TokenDelta seen, no StepDoneLive output existed, but this Step did not reach the durable boundary.
StepDone seenThe finalized group committed and is part of loop history.
TurnFailed after earlier StepDoneEarlier Steps remain durable; only the current incomplete Step rolled back.
TurnInterrupted during commitThe current group did not commit; already committed groups remain.

Consumer pattern

package example

import (
	"errors"
	"fmt"

	"github.com/looprig/harness/pkg/event"
)

func requireCommittedStep(ev event.Event) error {
	step, ok := ev.(event.StepDone)
	if !ok {
		return fmt.Errorf("not a committed step: %T", ev)
	}
	if err := event.ValidateEvent(step); err != nil {
		var invalid *event.InvalidEventError
		if errors.As(err, &invalid) {
			return fmt.Errorf("invalid StepDone field %s: %w", invalid.Field, err)
		}
		return err
	}
	return nil
}

Do not synthesize a missing StepDone from TokenDelta values. If you need replay or restore, consume the enduring event stream or journal. event.MarshalEvent has dedicated StepDone encoding and fails closed on malformed messages.

Source and proof

← back to documentation