event

package
v0.28.0 Latest Latest
Warning

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

Go to latest
Published: Aug 20, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

README

pkg/event

pkg/event defines the sealed union of rig, session, loop, turn, step, and tool events. Enduring rig-control and workspace transitions are durable replay inputs; ephemeral streaming events are never persisted.

Every concrete event embeds:

  • a Header (producer identity: Coordinates, Cause, Agency);
  • exactly one lifecycle mixin (ephemeral, enduring, or terminal — supplying Class() and EndsTurn()); and
  • exactly one scope mixin (sessionScoped or loopScoped — supplying Scope()).

The compile-time assertions in doc.go pin the sealed union: every concrete event type must satisfy Event, and the list is the authoritative enumeration. Adding a new event without adding it there is harmless, but removing a type or breaking its interface satisfaction fails the build here first.

What is event?

  • A typed Event interface with isEvent (unexported, so only types in this package can implement it) plus Class, Scope, EndsTurn, and EventHeader.
  • Three lifecycle classes:
    • ClassEphemeral — streaming events (TokenDelta, ToolCallStarted, ToolCallCompleted, …). Never persisted.
    • ClassEnduring — rig-control and workspace transitions (ConfigurationAdopted, WorkspaceCheckpointed, ForeignSessionBound, CompactionStarted, …). The durable replay inputs.
    • ClassTerminal — turn/step outcomes (TurnDone, TurnFailed, TurnInterrupted, StepDone). Mark EndsTurn() true when they end a turn.
  • Two scopes:
    • ScopeSession — events the session as a whole owns (started, idle, stopped, workspace, restore, integration status).
    • ScopeLoop — events a particular loop owns (started, idle, mode changed, compaction, turn/step/tool lifecycle).
  • A Delivery value: an Event paired with its JournalSeq (zero for an ephemeral event that was never persisted, the append sequence for an enduring one). The journal sequence rides alongside the event without ever entering the event codec, so a live SSE consumer can stamp id:<journal_seq> without the seq being part of the wire form.
  • An EventFilter (match by class, scope, kind, or specific types) used by Session.SubscribeEvents.
  • A Reply interface: a small, sealed set of command-resolution events (TurnStarted, InputQueued, TurnRejected, TurnFoldedInto, InputCancelled, CompactWaiterResolved, CompactWaiterRejected) whose ReplyTo() returns the embedded Header.Cause.CommandID. The set is asserted in tests so the seven stay sealed.

How to use

You usually don't construct events directly — the runtime and the session emit them. You read them through a subscription:

sub, _ := session.SubscribeEvents(nil)
for delivery := range sub.Events() {
    switch ev := delivery.Event.(type) {
    case event.TurnStarted:
        // a turn began; ev.Header.Coordinates.TurnID is its id
    case event.TokenDelta:
        // streaming assistant text; ev.Chunk carries the delta
    case event.ToolCallStarted:
        // a tool call is about to run
    case event.ToolCallCompleted:
        // a tool call finished
    case event.PermissionRequested:
        // a gate is open; answer with session.RespondGate
    case event.TurnDone:
        // a turn completed; ev.Header carries the causation id
    case event.IntegrationStatus:
        // an external integration (MCP, …) reported state
    }
    if delivery.JournalSeq > 0 {
        // an enduring event was persisted at this journal sequence
    }
}

Filters narrow the stream:

turnsOnly, _ := session.SubscribeEvents(event.EventFilter{
    MatchTurns: true,
    MatchScope: event.ScopeLoop,
})

Sibling packages

  • pkg/identityCoordinates, Cause, Agency, embedded in event.Header.
  • pkg/command — the commands whose outcomes some events reply to (event.Reply).
  • pkg/hub — the fan-in that delivers events.
  • pkg/journal — the durable writer that sequences enduring events.

How it is designed

              one concrete event type
                       │
   ┌───────────────────┴───────────────────┐
   │ Header (identity, cause, agency)       │
   │  exactly one lifecycle mixin            │
   │    ephemeral | enduring | terminal       │
   │  exactly one scope mixin                 │
   │    sessionScoped | loopScoped            │
   └───────────────────┬───────────────────┘
                       │
                       │  satisfies
                       ▼
                    Event
            ┌──────────┴───────────┐
            │  Class() / Scope()    │
            │  EndsTurn()           │
            │  EventHeader()        │
            └──────────┬───────────┘
                       │
        ┌──────────────┴──────────────┐
        ▼                             ▼
   fan-in: pkg/hub            durable: pkg/journal
   (class-aware overflow)    (only ClassEnduring persisted)
Why the mixins

The "exactly one of each" rule is enforced from both sides:

  • Embedding two of a kind makes the promoted selector ambiguous — the file does not compile.
  • Embedding zero leaves the method missing — the type does not satisfy Event.

So any count other than one fails the compile-time assertions in doc.go first.

Drift, integration, and compaction events

A few event groups carry extra structure worth knowing about:

  • DriftAssessment / ConfigurationAdopted — at restore time the rig runs a RestoreDecider against the assessment and records the decision as a durable ConfigurationAdopted. The assessment compares the live loop fingerprint to the journal-recorded one and classifies each change as Info or Warn.
  • IntegrationStatus — an integration is any live external capability a session runs alongside its loops (MCP, a language server, a plugin host). Harness does not implement one; this event is the coarse, protocol-neutral way one reports how it's doing (Starting/Ready/Degraded/Failed/Closed).
  • CompactionStarted / CompactionCommitted / CompactionRejected — per-loop context compaction lifecycle, with the context basis (revision, through-event-id) the compactor ran against. CompactionCommitted may carry the additive, backward-compatible Retained field: the exact user-anchored suffix protected by selection, including structurally complete tool-use/result pairs. On restore, the conversation becomes [Summary, Retained...]; the summary is the one derived message (DerivedPrefix == 1), while human-authored retained content keeps its provenance. Older events omit Retained and therefore restore as summary-only.

Documentation

Overview

Package event defines the sealed union of rig, session, loop, turn, step, and tool events. Enduring rig-control and workspace transitions are durable replay inputs; ephemeral streaming events are never persisted.

Every concrete event embeds a Header (producer identity), exactly one lifecycle mixin (ephemeral, enduring, or terminal — supplying Class()/EndsTurn()), and exactly one scope mixin (sessionScoped or loopScoped — supplying Scope()). The compile-time assertions below pin the sealed union: every concrete event type must satisfy Event, and the list is the authoritative enumeration of the union. Adding a new event without adding it here is harmless, but removing a type or breaking its interface satisfaction fails the build here first.

Index

Constants

View Source
const (
	LoopRestoreTombstoneRuntimeMismatch    = "runtime_mismatch"
	LoopRestoreTombstoneRuntimeUnavailable = "runtime_unavailable"
)

LoopRestoreTombstoneCategory is the bounded reason a restored child was kept in the durable topology without a live backend.

View Source
const (
	// MaxIntegrationSourceBytes caps Source.
	MaxIntegrationSourceBytes = 64
	// MaxIntegrationNameBytes caps Name.
	MaxIntegrationNameBytes = 128
	// MaxIntegrationDetailBytes caps Detail.
	MaxIntegrationDetailBytes = 512
)

Maximum sizes for the free-text and identifier fields. They exist because an integration is not part of this module and its inputs are not this module's to trust: a server-influenced name or message must not be able to grow an event without bound. See ValidateEvent, which enforces them at the publish boundary.

View Source
const (
	// KindEmptyResponse classifies *EmptyResponseError.
	KindEmptyResponse = "empty_response"
	// KindToolLimit classifies *ToolLimitError (the runaway guard).
	KindToolLimit = "tool_limit"
	// KindTurnPanic classifies *TurnPanicError (a recovered turn-goroutine panic).
	KindTurnPanic = "turn_panic"
	// KindUnknown is the fail-open fallback for any error the switch does not
	// recognize, including the open-ended provider/stream errors. It is stable: a
	// caller can rely on "unknown" for an unrecognized cause.
	KindUnknown = "unknown"
)

Stable kind strings ErrKind projects the event package's own TurnFailed.Err causes to. They are part of the durable wire contract: never rename one (old journals carry the old string); only add new constants. Provider/stream errors that flow through streamFailure are not enumerated here on purpose — the event package is a leaf (it must not import the LLM provider layer), so they fall back to KindUnknown while their full Error() text is still preserved in RestoredError.Message.

View Source
const (

	// MaxConfigMessageLen and MaxConfigActorLen bound the durable, partly
	// user-authored audit fields. They are exported so the restore constructor can
	// TRUNCATE a decider's over-long Message/Actor before building the adoption (a
	// long audit note must never brick a restore); the validator here still rejects
	// an over-long field on a hand-crafted, decoded journal record.
	MaxConfigMessageLen = 4096
	MaxConfigActorLen   = 1024
)

Bounds for ConfigurationAdopted's durable, partly user-authored payload: a hostile or buggy decision must not be able to append an unbounded record to the journal, and a legacy (SchemaVersion 0) manifest projection is never persisted.

View Source
const (
	WorkflowActivityRunStarted      WorkflowActivityKind = "run_started"
	WorkflowActivityVertexCompleted WorkflowActivityKind = "vertex_completed"
	WorkflowActivityRunInterrupted  WorkflowActivityKind = "run_interrupted"
	WorkflowActivityRunResumed      WorkflowActivityKind = "run_resumed"
	WorkflowActivityRunCompleted    WorkflowActivityKind = "run_completed"
	WorkflowActivityRunCancelled    WorkflowActivityKind = "run_cancelled"
	WorkflowActivityRunFailed       WorkflowActivityKind = "run_failed"

	// Kind-prefixed aliases keep the enum discoverable for callers that group
	// constants by the declared type name.
	WorkflowActivityKindRunStarted      = WorkflowActivityRunStarted
	WorkflowActivityKindVertexCompleted = WorkflowActivityVertexCompleted
	WorkflowActivityKindRunInterrupted  = WorkflowActivityRunInterrupted
	WorkflowActivityKindRunResumed      = WorkflowActivityRunResumed
	WorkflowActivityKindRunCompleted    = WorkflowActivityRunCompleted
	WorkflowActivityKindRunCancelled    = WorkflowActivityRunCancelled
	WorkflowActivityKindRunFailed       = WorkflowActivityRunFailed
)
View Source
const (
	MaxWorkflowNameBytes            = 64
	MaxWorkflowVersionBytes         = 64
	MaxWorkflowVertexLabelBytes     = 128
	MaxWorkflowActivityMessageBytes = 512
	MaxWorkflowActivityProgress     = 1_000_000
)

Bounds for fields originating in workflow definitions or execution state. They are byte limits at the durable boundary; UTF-8 validity is checked separately so a multi-byte rune can never be split by a producer.

View Source
const ManifestSchemaVersion uint32 = 3

ManifestSchemaVersion is the current ConfigManifest schema version. Bumping it changes the canonical encoding, which changes every fingerprint — restore therefore never treats raw fingerprint inequality across schema versions as drift (see AssessDrift) and records a one-time baseline upgrade instead.

v2 adds both PermissionReviewPolicyRev (the review-policy-identity-drift- while-enabled fix) and HookPolicyRev (the operation-hooks feature) to the canonical encoding. Neither shipped independently — both landed in the same merge before v2 was ever persisted anywhere — so v2 is defined as carrying both fields together, not as two separate single-field bumps. v3 adds the explicit runtime identity fields. v1 and v2 canonical encodings remain immutable so previously persisted fingerprints remain verifiable.

Variables

This section is empty.

Functions

func CompactWaiterReplyID

func CompactWaiterReplyID(attempt CompactAttemptID, commandID uuid.UUID, resolved bool) uuid.UUID

CompactWaiterReplyID derives the idempotency key for one per-command outcome.

func ErrKind

func ErrKind(err error) string

ErrKind maps a concrete error to its stable kind string via an errors.As switch over the known in-package TurnFailed.Err causes. It is what the event codec uses to project TurnFailed.Err into RestoredError.Kind on marshal. An already-restored *RestoredError re-projects to its own Kind (idempotent re-marshal); a nil or unrecognized error yields KindUnknown. errors.As (not a bare type switch) so a wrapped known cause is still classified by its concrete type.

func MarshalEvent

func MarshalEvent(ev Event) ([]byte, error)

MarshalEvent encodes an Enduring event into the durable wire envelope: a JSON object carrying a "type" discriminator (== the classify name, the package's single naming source of truth), a "v" schema version, the embedded Header fields, and the type-specific payload. It fails closed on an Ephemeral event (EphemeralNotPersistableError) and on a type outside the sealed union (UnknownEventTypeError). The interface-valued fields that have no general codec (PermissionRequested.Request, TurnFailed.Err, RestoreErrored.Err) are projected onto durable forms; every other field round-trips through encoding/json.

func ShouldDeliver

func ShouldDeliver(filter EventFilter, ev Event) bool

ShouldDeliver reports whether ev passes filter for one subscriber. Session-scoped events (including the durable WorkflowActivity timeline) always deliver, bypassing LoopScope. Loop-scoped events are matched by the class-appropriate LoopScope against the producing loop's Header.LoopID. It is evaluated at fan-out, before the bounded send, so a filtered-out firehose never enters the egress buffer.

func ValidLoopRestoreTombstoneCategory

func ValidLoopRestoreTombstoneCategory(category string) bool

func ValidateEvent

func ValidateEvent(ev Event) error

ValidateEvent checks ev against the ID fill matrix and returns a typed *InvalidEventError on the first violation, nil when ev satisfies every invariant. EventID is required on every event; the per-type profile then pins the required and must-be-zero coordinates (and ToolExecutionID for tool-interaction and permission-review events). Fail-secure: an event whose concrete type is not in the sealed union is invalid with FieldType/RuleUnknownType — the caller learns the type is unknown, not that some coordinate is missing.

Types

type ActiveLoopChanged

type ActiveLoopChanged struct {
	Header
	PreviousLoopID uuid.UUID `json:"previous_loop_id,omitzero"`
	ActiveLoopID   uuid.UUID `json:"active_loop_id"`
	// contains filtered or unexported fields
}

ActiveLoopChanged records the session's selected loop. The new selection is observable only after this session-scoped transition is durable.

func (ActiveLoopChanged) Class

func (ActiveLoopChanged) Class() Class

func (ActiveLoopChanged) EndsTurn

func (ActiveLoopChanged) EndsTurn() bool

func (ActiveLoopChanged) Scope

func (ActiveLoopChanged) Scope() Scope

type AgentRuntime

type AgentRuntime struct {
	Harness         string `json:"harness"`
	Profile         string `json:"profile"`
	CredentialMode  string `json:"credential_mode"`
	Source          string `json:"source,omitempty"`
	SelectionKind   string `json:"selection_kind,omitempty"`
	ModelAlias      string `json:"model_alias"`
	SmallModelAlias string `json:"small_model_alias,omitempty"`
	ACPSessionID    string `json:"acp_session_id,omitempty"`
}

AgentRuntime is the bounded, secret-free identity of the runtime selected for a loop. It is additive so legacy LoopStarted records decode with nil.

type BasisPoints

type BasisPoints uint16

BasisPoints is a percentage scaled by 100. Values above 10_000 are invalid.

const FullScaleBasisPoints BasisPoints = 10_000

type CancelReason

type CancelReason uint8

CancelReason explains why a queued input left the loop queue without committing. It is carried by InputCancelled.

const (
	CancelClientRetracted CancelReason = iota
	CancelTurnInterrupted
	CancelTurnFailed
)

type Class

type Class uint8

Class is the delivery class of an event. It is semantic — "is this event reconstructable from a later authoritative event?" — not a transport flag, which is why it belongs on the event rather than on the transport.

const (
	Ephemeral Class = iota // reconstructable from a later authoritative event -> droppable
	Enduring               // authoritative transition/payload -> never silently dropped
)

type Clock

type Clock func() time.Time

Clock and IDGen are injected so tests are deterministic (mirrors session's injected idGenerator seam): the Factory mints from these rather than calling time.Now/uuid.New directly, so a test can pin both. IDGen returns an error so a crypto/rand failure propagates (matching session's idGenerator func() (uuid.UUID, error)) rather than being swallowed.

type CompactAttemptID

type CompactAttemptID uuid.UUID

func (CompactAttemptID) IsZero

func (id CompactAttemptID) IsZero() bool

func (CompactAttemptID) MarshalText

func (id CompactAttemptID) MarshalText() ([]byte, error)

func (*CompactAttemptID) UnmarshalText

func (id *CompactAttemptID) UnmarshalText(text []byte) error

type CompactRejectReason

type CompactRejectReason uint8
const (
	CompactRejectUnspecified CompactRejectReason = iota
	CompactRejectControlLaneFull
	CompactRejectShuttingDown
	CompactRejectInterrupted
	CompactRejectCanceled
	CompactRejectStaleBasis
	CompactRejectProgressPublication
	CompactRejectUnavailable
	CompactRejectExecutionFailed
	CompactRejectInvalidSummary
	CompactRejectContextCountFailed
	CompactRejectSummaryTooLarge
	CompactRejectInternal
	CompactRejectContextLimitUnknown
	CompactRejectRetainedTailTooLarge
)

func (CompactRejectReason) Valid

func (r CompactRejectReason) Valid() bool

type CompactWaiterRejected

type CompactWaiterRejected struct {
	Header
	AttemptID CompactAttemptID    `json:"attempt_id"`
	Reason    CompactRejectReason `json:"reason"`
	// contains filtered or unexported fields
}

func (CompactWaiterRejected) Class

func (CompactWaiterRejected) Class() Class

func (CompactWaiterRejected) EndsTurn

func (CompactWaiterRejected) EndsTurn() bool

func (CompactWaiterRejected) Scope

func (CompactWaiterRejected) Scope() Scope

type CompactWaiterResolved

type CompactWaiterResolved struct {
	Header
	AttemptID        CompactAttemptID `json:"attempt_id"`
	CommittedEventID uuid.UUID        `json:"committed_event_id"`
	// contains filtered or unexported fields
}

func (CompactWaiterResolved) Class

func (CompactWaiterResolved) Class() Class

func (CompactWaiterResolved) EndsTurn

func (CompactWaiterResolved) EndsTurn() bool

func (CompactWaiterResolved) Scope

func (CompactWaiterResolved) Scope() Scope

type CompactionCommitted

type CompactionCommitted struct {
	Header
	AttemptID        CompactAttemptID        `json:"attempt_id"`
	WaiterCommandIDs []uuid.UUID             `json:"waiter_command_ids"`
	Reason           CompactionReason        `json:"reason"`
	Basis            ContextBasis            `json:"basis"`
	Summary          *content.UserMessage    `json:"summary"`
	Retained         content.AgenticMessages `json:"retained,omitempty"`
	PostContext      ContextMeasurement      `json:"post_context"`
	Duration         time.Duration           `json:"duration,omitzero"`
	// contains filtered or unexported fields
}

func (CompactionCommitted) Class

func (CompactionCommitted) Class() Class

func (CompactionCommitted) EndsTurn

func (CompactionCommitted) EndsTurn() bool

func (CompactionCommitted) MarshalJSON

func (value CompactionCommitted) MarshalJSON() ([]byte, error)

MarshalJSON gives CompactionCommitted's additive retained graph the same tagged wire shape as the existing message-bearing events. Validation remains at ValidateEvent/MarshalEvent, so direct JSON marshaling retains the package's established behavior of encoding the value without revalidating all fields.

func (CompactionCommitted) Scope

func (CompactionCommitted) Scope() Scope

func (*CompactionCommitted) UnmarshalJSON

func (value *CompactionCommitted) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes both legacy summary-only records and the additive retained message graph. Empty or omitted retained arrays normalize to nil, matching omitempty's fixed point and keeping old event values stable.

type CompactionReason

type CompactionReason uint8
const (
	CompactionReasonUnspecified CompactionReason = iota
	CompactionReasonManual
	CompactionReasonAutomatic
)

func (CompactionReason) Valid

func (r CompactionReason) Valid() bool

type CompactionRejected

type CompactionRejected struct {
	Header
	AttemptID        CompactAttemptID    `json:"attempt_id"`
	WaiterCommandIDs []uuid.UUID         `json:"waiter_command_ids"`
	Reason           CompactionReason    `json:"reason"`
	Basis            ContextBasis        `json:"basis"`
	RejectReason     CompactRejectReason `json:"reject_reason"`
	Duration         time.Duration       `json:"duration,omitzero"`
	// contains filtered or unexported fields
}

func (CompactionRejected) Class

func (CompactionRejected) Class() Class

func (CompactionRejected) EndsTurn

func (CompactionRejected) EndsTurn() bool

func (CompactionRejected) Scope

func (CompactionRejected) Scope() Scope

type CompactionStarted

type CompactionStarted struct {
	Header
	AttemptID CompactAttemptID `json:"attempt_id"`
	Reason    CompactionReason `json:"reason"`
	Basis     ContextBasis     `json:"basis"`
	// contains filtered or unexported fields
}

func (CompactionStarted) Class

func (CompactionStarted) Class() Class

func (CompactionStarted) EndsTurn

func (CompactionStarted) EndsTurn() bool

func (CompactionStarted) Scope

func (CompactionStarted) Scope() Scope

type ConfigEpoch

type ConfigEpoch uint64

ConfigEpoch orders the configurations explicitly adopted within one Session. SessionStarted is epoch 1; each ConfigurationAdopted increments it.

type ConfigFingerprint

type ConfigFingerprint struct {
	// TopologyRev is the digest of ordered loop definitions, primer roots, active
	// primer, and delegation edges owned by the rig.
	TopologyRev string `json:"topology_rev,omitzero"`
	// AgentKind names the application and Loop role this session ran (e.g. "carbon:carbon").
	// It is empty for a caller that does not inject a kind (a non-swarm/legacy session).
	AgentKind string `json:"agent_kind,omitzero"`
	// ModelID is the model identifier the session ran against (the llm.Model.Name).
	ModelID string `json:"model_id,omitzero"`
	// SystemPromptRev is a content digest (hex sha256) of the system prompt text, so
	// a prompt change is detectable without persisting the prompt itself.
	SystemPromptRev string `json:"system_prompt_rev,omitzero"`
	// ToolPolicyRev is a content digest (hex sha256) over the tool set's stable
	// identity (its sorted tool names), so a tool-set change is detectable without
	// persisting the tool definitions.
	ToolPolicyRev string `json:"tool_policy_rev,omitzero"`
	// RuntimeSkills records whether the untrusted, human-gated workspace skill source
	// was enabled for this session. A session must not silently resume under a
	// different skill-trust mode, so the flag is part of the fingerprint. It is the
	// MODE only — the flag alone does NOT distinguish two repos' .skills/, which is
	// what WorkspaceRoot is for.
	RuntimeSkills bool `json:"runtime_skills,omitzero"`
	// WorkspaceRoot is the canonical absolute workspace-root id (filepath.Clean of the
	// absolute root). It binds the session to the repo whose .skills/ (and file tools)
	// it ran against, so a session cannot silently resume under a different repo's
	// workspace. Empty for a caller that does not inject a root.
	WorkspaceRoot string `json:"workspace_root,omitzero"`
	// AgentAdapter identifies the foreign-agent adapter that backed this session
	// (e.g. "claude"). Empty for a native session. A session must not silently resume
	// under a different foreign adapter, so it is part of the fingerprint.
	AgentAdapter string `json:"agent_adapter,omitzero"`
	// PermissionPosture is the non-interactive permission mode the foreign agent ran
	// under (e.g. "default", "acceptEdits"). Empty for a native session. A change in
	// posture is a behavior change that must not resume unnoticed.
	PermissionPosture string `json:"permission_posture,omitzero"`
	// NativePermissionPolicyRev is an opaque content digest (hex sha256) of the
	// NATIVE permission and access configuration, computed and injected by the
	// composition root (e.g. over the selected access profiles and rule policy).
	// Harness only compares it. Empty for a foreign session (which uses
	// PermissionPosture) or a caller that does not inject it. A change is a
	// behavior change that must not resume unnoticed.
	NativePermissionPolicyRev string `json:"native_permission_policy_rev,omitzero"`
	// ExternalCapabilityRev is a content digest over the identity of the EXTERNAL
	// capabilities an application attached to this session — tools, prompts and
	// resources served by processes Harness does not own, such as MCP servers.
	// Empty means the session had none, which is what makes the field additive:
	// a journal written before it existed decodes it empty and compares Equal to
	// a live config that also has none.
	//
	// Harness neither computes nor interprets it. It is supplied by the
	// composition root, which is the only layer that knows what it attached; the
	// canonical producer today is github.com/looprig/mcp's
	// mcpharness.Manager.ConfigDigest. Harness's part of the contract is the two
	// properties every other Rev field here has: it is a digest, so it carries
	// identity and not the configuration — a server's credentials, headers, and
	// environment must never reach a journal — and it is compared, never parsed.
	//
	// It is ONE opaque string rather than a structured manifest deliberately.
	// The richer model — per-binding manifests, configuration epochs, and typed
	// drift — is specified in docs/plans/2026-07-16-session-versioning-migration-design.md
	// and is NOT implemented. Until it is, external capability drift is reported
	// through the same one-shot mechanism as every other config change: a
	// fingerprint mismatch at restore, which sessionruntime's
	// WithAllowConfigMismatch decides on. A field that promised more than that
	// would be a promise nothing here keeps.
	ExternalCapabilityRev string `json:"external_capability_rev,omitzero"`
	// RuntimeProfile is the secret-free backend profile selected for this bound
	// loop. Empty preserves native and legacy callers.
	RuntimeProfile string `json:"runtime_profile,omitzero"`
	// RuntimeCatalogRev identifies the parent-scoped runtime catalog snapshot.
	RuntimeCatalogRev string `json:"runtime_catalog_rev,omitzero"`
	// RuntimeIdentityRev is an opaque hex digest of the selected runtime tuple.
	// It is produced by loop.BoundDefinition.RuntimeIdentity().Digest() and
	// covers the model alias, target key, effective effort (including canonical
	// none), profile, and catalog without persisting raw model descriptors.
	RuntimeIdentityRev string `json:"runtime_identity_rev,omitzero"`
}

ConfigFingerprint is the stable identity of the agent configuration a session started under, stamped onto SessionStarted so a durable journal can detect when a restore is being attempted against a materially changed config (a different model, system prompt, tool policy, skill-trust mode, workspace, foreign adapter, or permission posture). It is a fingerprint, not the config itself: each field is either a verbatim identifier (AgentKind, ModelID, WorkspaceRoot, AgentAdapter, PermissionPosture), a content digest (SystemPromptRev, ToolPolicyRev), or a mode flag (RuntimeSkills) — never the raw prompt text or tool definitions — so it is safe to persist and compare without leaking definition internals. Package rig freezes the fingerprint from its registered loop definitions, topology, and composition fields; this package only defines the durable value and its equality.

The fields evolve ADDITIVELY: every field is omitzero, so an old journal record that predates a field decodes it as the zero value and compares Equal to a record that also leaves it empty — a session persisted before a field was added restores without a spurious mismatch.

func (ConfigFingerprint) Equal

func (f ConfigFingerprint) Equal(other ConfigFingerprint) bool

Equal reports whether two fingerprints identify the same configuration: true iff every field is equal. It is the comparison a restore uses to decide whether the persisted config still matches the live one. New fields are additive (omitzero), so an old record's empty new field equals a current record that also leaves it empty.

type ConfigManifest

type ConfigManifest struct {
	SchemaVersion   uint32              `json:"schema_version"`
	AgentKind       string              `json:"agent_kind,omitzero"`
	TopologyRev     string              `json:"topology_rev,omitzero"`
	ModelID         string              `json:"model_id,omitzero"`
	SystemPromptRev string              `json:"system_prompt_rev,omitzero"`
	Tools           []ToolManifestEntry `json:"tools,omitzero"`
	RuntimeSkills   bool                `json:"runtime_skills,omitzero"`
	WorkspaceRoot   string              `json:"workspace_root,omitzero"`
	WorkspaceTrust  string              `json:"workspace_trust,omitzero"`
	AgentAdapter    string              `json:"agent_adapter,omitzero"`
	// PermissionPosture is the foreign-agent posture string; native sessions
	// use NativePermissionPolicyRev + PermissionStrictness instead.
	PermissionPosture         string          `json:"permission_posture,omitzero"`
	NativePermissionPolicyRev string          `json:"native_permission_policy_rev,omitzero"`
	PermissionStrictness      StrictnessLevel `json:"permission_strictness,omitzero"`
	// PermissionReviewConfigured reports only whether ANY permission-review
	// classifier was registered for this session — never the classifier or
	// policy identity itself (the classifier SET's identity stays folded into
	// TopologyRev, for detecting drift among already-enabled classifiers,
	// classified Info like the rest of TopologyRev). It exists so AssessDrift
	// has a directionally-comparable signal for the one transition an opaque
	// digest can't distinguish: classifiers going from unconfigured to
	// configured across a restore, which must never resume silently (design
	// §21).
	PermissionReviewConfigured bool `json:"permission_review_configured,omitzero"`
	// PermissionReviewPolicyRev is the review POLICY's own Revision label
	// (gate.PermissionReviewPolicy.Revision — e.g. "strict-policy-v1"),
	// carried SEPARATELY from TopologyRev (which also folds this same value
	// in, alongside classifier identity, for backward-compatible digest
	// coverage). Without this dedicated field, a policy-identity change while
	// classifiers stay configured on both sides is visible only as an opaque
	// TopologyRev digest difference — classified DriftInfo like any other
	// topology change and silently auto-accepted by DefaultPolicyDecider, so
	// a session opened under a strict review policy (e.g. MaximumAutoRisk:
	// low) could restore under a looser one (e.g. default MaximumAutoRisk:
	// high) with no warning at all. AssessDrift compares this field
	// DIRECTLY (Warn on any change) whenever PermissionReviewConfigured is
	// true on both sides — the same kind of directional fix already applied
	// to PermissionReviewConfigured itself for the disabled->enabled
	// transition, extended to cover an already-enabled reviewer's policy
	// identity changing underneath it.
	PermissionReviewPolicyRev string          `json:"permission_review_policy_rev,omitzero"`
	ConfinementRev            string          `json:"confinement_rev,omitzero"`
	ConfinementStrictness     StrictnessLevel `json:"confinement_strictness,omitzero"`
	ExternalCapabilityRev     string          `json:"external_capability_rev,omitzero"`
	HookPolicyRev             string          `json:"hook_policy_rev,omitzero"`
	RuntimeProfile            string          `json:"runtime_profile,omitzero"`
	RuntimeCatalogRev         string          `json:"runtime_catalog_rev,omitzero"`
	// RuntimeIdentityRev is the opaque hex digest of the selected runtime tuple;
	// raw provider, model, effort, alias, endpoint, and credential values never
	// enter the durable manifest.
	RuntimeIdentityRev string `json:"runtime_identity_rev,omitzero"`
	// AppFields are application-defined, secret-free compatibility fields.
	// Canonically encoded in sorted key order.
	AppFields map[string]string `json:"app_fields,omitzero"`
	// contains filtered or unexported fields
}

ConfigManifest is the canonical, bounded, secret-free description of the behavior a Session runs under. It is a strict superset of the legacy ConfigFingerprint (see ManifestFromLegacy) and the input to both the SHA-256 fingerprint and typed drift assessment. Credentials, raw prompts, tool schemas, and environment contents never enter a manifest.

SchemaVersion 0 marks a legacy projection built by ManifestFromLegacy; it is never persisted and never fingerprinted.

func ManifestFromLegacy

func ManifestFromLegacy(f ConfigFingerprint) ConfigManifest

ManifestFromLegacy projects a legacy ConfigFingerprint into a partial manifest for drift assessment against a live candidate. SchemaVersion 0 marks the projection: it is never persisted, never fingerprinted, and limits assessment to the fields the legacy fingerprint can distinguish (tool identity is names-only; permission and confinement are digest-only, so their changes classify Warn).

func (ConfigManifest) Fingerprint

func (m ConfigManifest) Fingerprint() string

Fingerprint is SHA-256 over the canonical encoding: explicit domain, schema version, stable field order, length-delimited values, deterministic collection ordering. Equal fingerprints of the same SchemaVersion identify behaviorally identical configurations.

func (ConfigManifest) ToolNamesRev

func (m ConfigManifest) ToolNamesRev() string

ToolNamesRev reproduces the legacy names-only tool digest from the manifest's tool entries, so a full manifest can be compared against a legacy baseline. It MUST stay byte-identical to rig's toolPolicyRev (sorted names joined by \n).

type ConfigurationAdopted

type ConfigurationAdopted struct {
	Header
	Epoch               ConfigEpoch    `json:"epoch"`
	PreviousFingerprint string         `json:"previous_fingerprint,omitzero"`
	AdoptedFingerprint  string         `json:"adopted_fingerprint"`
	Manifest            ConfigManifest `json:"manifest"`
	Drift               []DriftChange  `json:"drift,omitzero"`
	Source              DecisionSource `json:"source"`
	Actor               string         `json:"actor,omitzero"`
	AppVersion          string         `json:"app_version,omitzero"`
	// Message is durable user-authored data, not an instruction: it never gains
	// authority during future prompt construction.
	Message string `json:"message,omitzero"`
	// contains filtered or unexported fields
}

ConfigurationAdopted commits a new configuration epoch: the durable record of an accepted restore drift or a one-time baseline upgrade. It is appended under the restore lease after the decision validates and before RestoreDone; the latest SessionStarted or ConfigurationAdopted is the baseline for the next restore's drift assessment. Like SessionStarted it is session-scoped and Enduring — Header.SessionID is set, LoopID/TurnID/StepID are zero, and the SessionID rides in the Header (there is no standalone SessionID field).

func (ConfigurationAdopted) Class

func (ConfigurationAdopted) Class() Class

func (ConfigurationAdopted) EndsTurn

func (ConfigurationAdopted) EndsTurn() bool

func (ConfigurationAdopted) Scope

func (ConfigurationAdopted) Scope() Scope

type ContextBasis

type ContextBasis struct {
	Revision       ContextRevision `json:"revision"`
	ThroughEventID uuid.UUID       `json:"through_event_id"`
}

ContextBasis identifies the exact durable context included in a measurement.

type ContextField

type ContextField string

ContextField identifies an invalid structural measurement field.

const (
	ContextFieldRevision           ContextField = "Revision"
	ContextFieldThroughEventID     ContextField = "ThroughEventID"
	ContextFieldModel              ContextField = "Model"
	ContextFieldRequestFingerprint ContextField = "RequestFingerprint"
	ContextFieldInputLimit         ContextField = "InputLimit"
	ContextFieldQuality            ContextField = "Quality"
)

type ContextMeasured

type ContextMeasured struct {
	Header
	Measurement ContextMeasurement `json:"measurement"`
	// contains filtered or unexported fields
}

ContextMeasured durably publishes the latest authoritative measurement.

func (ContextMeasured) Class

func (ContextMeasured) Class() Class

func (ContextMeasured) EndsTurn

func (ContextMeasured) EndsTurn() bool

func (ContextMeasured) Scope

func (ContextMeasured) Scope() Scope

type ContextMeasurement

type ContextMeasurement struct {
	Basis              ContextBasis              `json:"basis"`
	Model              model.ModelKey            `json:"model"`
	RequestFingerprint [32]byte                  `json:"request_fingerprint"`
	InputTokens        content.TokenCount        `json:"input_tokens"`
	InputLimit         content.TokenCount        `json:"input_limit"`
	Quality            contextcount.CountQuality `json:"quality"`
}

ContextMeasurement is one authoritative complete-request input count.

func (ContextMeasurement) Validate

func (m ContextMeasurement) Validate() error

Validate checks structural invariants without treating over-limit occupancy as malformed: raw counts above InputLimit remain valid audit evidence.

type ContextPressure

type ContextPressure struct {
	Header
	Measurement ContextMeasurement `json:"measurement"`
	Occupancy   BasisPoints        `json:"occupancy"`
	Previous    PressureLevel      `json:"previous"`
	Current     PressureLevel      `json:"current"`
	// contains filtered or unexported fields
}

ContextPressure is a droppable public level-change signal.

func (ContextPressure) Class

func (ContextPressure) Class() Class

func (ContextPressure) EndsTurn

func (ContextPressure) EndsTurn() bool

func (ContextPressure) Scope

func (ContextPressure) Scope() Scope

type ContextRevision

type ContextRevision uint64

ContextRevision is the loop-local revision of the committed active context. Zero is invalid because it cannot identify a committed context mutation.

type ContextValidationError

type ContextValidationError struct {
	Field ContextField
	Cause error
}

ContextValidationError reports malformed replayable context metadata.

func (*ContextValidationError) Error

func (e *ContextValidationError) Error() string

func (*ContextValidationError) Unwrap

func (e *ContextValidationError) Unwrap() error

type DecisionSource

type DecisionSource string

DecisionSource records who or what accepted a configuration adoption.

const (
	DecisionSourceUser     DecisionSource = "user"
	DecisionSourcePolicy   DecisionSource = "policy"
	DecisionSourceOperator DecisionSource = "operator"
	// DecisionSourceMigration is stamped only by Harness itself when a Phase 2
	// migration adopts a configuration; a RestoreDecider never produces it.
	DecisionSourceMigration DecisionSource = "migration"
)

func (DecisionSource) Valid

func (s DecisionSource) Valid() bool

Valid reports whether the source is one of the four closed DecisionSource values. An adoption record with any other source is malformed.

type DelegateDeliveryState

type DelegateDeliveryState string

DelegateDeliveryState is the durable resolution vocabulary for a delegated message's delivery attempt. The zero value is intentionally not a state: an intent and a fallback queue are represented by the exact journaled command record, while these values record only the foreign-delivery outcomes that must survive replay.

const (
	// DelegateDeliverySteerAttemptReserved is appended before a foreign adapter
	// may admit the steering request to its writer. It is an intermediate state:
	// restore may correlate a later turn terminal or replay a durable fallback;
	// only an intent-only reservation is repaired as resolved_unknown.
	DelegateDeliverySteerAttemptReserved DelegateDeliveryState = "steer_attempt_reserved"
	// DelegateDeliveryResolvedUnknown records an ambiguous delivery result. It
	// is terminal for automatic delivery recovery: the request may have reached
	// the adapter, so no fallback is allowed.
	DelegateDeliveryResolvedUnknown DelegateDeliveryState = "resolved_unknown"
	// DelegateDeliveryResolvedUntrackable records an adapter lifecycle breach
	// that delivered outside the host-owned turn contract. No synthetic turn or
	// automatic fallback may follow it.
	DelegateDeliveryResolvedUntrackable DelegateDeliveryState = "resolved_untrackable"
)

func (DelegateDeliveryState) Valid

func (s DelegateDeliveryState) Valid() bool

Valid reports whether s is one of the closed durable delivery states.

type DelegateDeliveryStateChanged

type DelegateDeliveryStateChanged struct {
	Header
	RequestID    uuid.UUID             `json:"request_id"`
	TargetLoopID uuid.UUID             `json:"target_loop_id"`
	State        DelegateDeliveryState `json:"state"`
	// contains filtered or unexported fields
}

DelegateDeliveryStateChanged is the durable state transition for a foreign delegate delivery attempt. It deliberately carries only the request id, the target loop, and the closed state vocabulary in its payload. The event never embeds command.UserInput (which would create an event↔command import cycle), broker tokens, or model-visible origin/session identity. The existing v1 event envelope versions this record on the journal wire.

It is session-scoped because the session owns the reservation and adjudicates adapter delivery; TargetLoopID is explicit so the state remains addressable without pretending the target loop produced the event.

func (DelegateDeliveryStateChanged) Class

func (DelegateDeliveryStateChanged) Class() Class

func (DelegateDeliveryStateChanged) EndsTurn

func (DelegateDeliveryStateChanged) EndsTurn() bool

func (DelegateDeliveryStateChanged) Scope

func (DelegateDeliveryStateChanged) Scope() Scope

type DelegateRequestAccepted

type DelegateRequestAccepted struct {
	Header // Cause.CommandID=request, Coordinates.LoopID=target child
	// contains filtered or unexported fields
}

DelegateRequestAccepted is the durable actor-side acceptance of a follow-up machine NoFold request, emitted before it can queue or start.

func (DelegateRequestAccepted) Class

func (DelegateRequestAccepted) Class() Class

func (DelegateRequestAccepted) EndsTurn

func (DelegateRequestAccepted) EndsTurn() bool

func (DelegateRequestAccepted) Scope

func (DelegateRequestAccepted) Scope() Scope

type Delivery

type Delivery struct {
	Event      Event
	JournalSeq uint64
}

Delivery is one fan-in delivery: the event plus its durable journal sequence. JournalSeq is 0 for Ephemeral deliveries (never persisted, never sequenced) and the strictly-monotonic append sequence for Enduring deliveries. It rides only the LIVE delivery path — it is never part of the persisted event codec, so the durable envelope stays byte-compatible.

type DriftAssessment

type DriftAssessment struct {
	Changes         []DriftChange `json:"changes,omitzero"`
	BaselineUpgrade bool          `json:"baseline_upgrade,omitzero"`
}

DriftAssessment is the typed comparison of the latest adopted baseline against the candidate live manifest.

func AssessDrift

func AssessDrift(baseline, candidate ConfigManifest) DriftAssessment

AssessDrift compares baseline (the latest adopted manifest, possibly a legacy projection) against candidate (the frozen live manifest). Direction- sensitive categories classify Info when the posture tightened, Warn when it broadened, and Warn when direction is unknowable (an opaque digest-only change) — fail secure. The returned Changes are deterministically ordered.

func (DriftAssessment) AnyWarn

func (a DriftAssessment) AnyWarn() bool

AnyWarn reports whether any change requires an explicit decision under default policy.

type DriftCategory

type DriftCategory string

DriftCategory names the manifest field family a change belongs to.

const (
	DriftTool          DriftCategory = "tool"
	DriftModel         DriftCategory = "model"
	DriftPrompt        DriftCategory = "prompt"
	DriftTopology      DriftCategory = "topology"
	DriftExternal      DriftCategory = "external"
	DriftConfinement   DriftCategory = "confinement"
	DriftPermission    DriftCategory = "permission"
	DriftWorkspace     DriftCategory = "workspace"
	DriftTrust         DriftCategory = "trust"
	DriftAgentKind     DriftCategory = "agent_kind"
	DriftAgentName     DriftCategory = "agent_name"
	DriftAdapter       DriftCategory = "adapter"
	DriftRuntimeSkills DriftCategory = "runtime_skills"
	DriftHookPolicy    DriftCategory = "hook_policy"
	DriftRuntime       DriftCategory = "runtime"
	DriftApp           DriftCategory = "app"
)

type DriftChange

type DriftChange struct {
	Category DriftCategory `json:"category"`
	Field    string        `json:"field,omitzero"`
	Old      string        `json:"old,omitzero"`
	New      string        `json:"new,omitzero"`
	Severity DriftSeverity `json:"severity"`
}

DriftChange is one typed configuration change: safe identities only (names, digests, levels), never raw configuration.

type DriftSeverity

type DriftSeverity string

DriftSeverity is the two-tier classification of one configuration change, answering one question: does the change expand what the session can touch? Severity is advisory input to application policy, not authority.

const (
	DriftInfo DriftSeverity = "info"
	DriftWarn DriftSeverity = "warn"
)

type EmptyResponseError

type EmptyResponseError struct{}

EmptyResponseError is the TurnFailed.Err cause when a provider returns a successful stream that contains no text or thinking content.

func (EmptyResponseError) Error

func (EmptyResponseError) Error() string

type EphemeralNotPersistableError

type EphemeralNotPersistableError struct{ Type string }

EphemeralNotPersistableError is returned by MarshalEvent when handed any Ephemeral event. The Ephemeral set is never persisted — it self-heals from a later authoritative event and TokenDelta.Chunk has no durable codec — so the marshaler fails closed rather than emit a lossy record. Type is the classify name of the rejected event so a caller learns exactly which event it tried to persist.

func (*EphemeralNotPersistableError) Error

type Event

type Event interface {
	Class() Class
	Scope() Scope
	EndsTurn() bool // turn-terminal: the last event this turn's per-turn stream carries
	EventHeader() Header
	Visibility() EventVisibility
	// contains filtered or unexported methods
}

Event is the sealed root of every loop event. Every concrete event embeds a Header, exactly one lifecycle mixin (ephemeral, enduring, or terminal), and exactly one scope mixin (sessionScoped or loopScoped). The lifecycle mixin supplies Class()/EndsTurn() and the scope mixin supplies Scope(), so the hub gets its delivery policy and consumers get producer identity without a transport-only envelope or a concrete type switch. Embedding two lifecycle mixins or two scope mixins makes the selectors ambiguous and the type stops satisfying Event, so the "exactly one of each" rule is enforced by the compiler.

func UnmarshalEvent

func UnmarshalEvent(data []byte) (Event, error)

UnmarshalEvent decodes a durable wire envelope back into a concrete Event. It fails closed on the untrusted restore boundary: input over the byte cap → EventLimitError; malformed envelope → EventDecodeError; an unknown or missing "type" tag → UnknownEventTypeError; a malformed payload for a known type → EventDecodeError. A successfully decoded event is validated against the ID fill matrix (ValidateEvent), so a structurally-valid but semantically-invalid record is rejected rather than resurrected.

type EventDecodeError

type EventDecodeError struct {
	Type  string
	Cause error
}

EventDecodeError wraps a failure to unmarshal serialized event bytes (malformed JSON, wrong field types) once a known "type" tag has been read.

func (*EventDecodeError) Error

func (e *EventDecodeError) Error() string

func (*EventDecodeError) Unwrap

func (e *EventDecodeError) Unwrap() error

type EventEncodeError

type EventEncodeError struct {
	Type  string
	Cause error
}

EventEncodeError wraps a failure to marshal an event's payload (a json.Marshal failure, or a delegated content/tool codec failure on the marshal path).

func (*EventEncodeError) Error

func (e *EventEncodeError) Error() string

func (*EventEncodeError) Unwrap

func (e *EventEncodeError) Unwrap() error

type EventFilter

type EventFilter struct {
	Ephemeral LoopScope // TokenDelta + tool lifecycle (ToolCallStarted/Completed) delivery
	Enduring  LoopScope // loop-produced StepDone, gates, terminals
}

EventFilter is a subscriber's declared interest: which loop producers it wants events from, separated by class. It is declared interest (deterministic, evaluated at fan-out before the bounded send), distinct from backpressure drop. An agent's token firehose excluded by Ephemeral never even enters that subscriber's egress buffer.

type EventLimitError

type EventLimitError struct {
	Got int
	Max int
}

EventLimitError is returned when an event codec input or output exceeds its cap.

func (*EventLimitError) Error

func (e *EventLimitError) Error() string

type EventName

type EventName string

EventName is the concrete event type name an InvalidEventError points at.

type EventVisibility

type EventVisibility uint8

EventVisibility controls whether an event may enter ordinary product streams. Public is deliberately zero so legacy records remain public and byte-stable.

const (
	Public EventVisibility = iota
	Internal
)

func (EventVisibility) Valid

func (v EventVisibility) Valid() bool

Valid reports whether visibility belongs to the closed durable domain.

type ExternalToolIdentity

type ExternalToolIdentity struct {
	Name         string `json:"name"`
	SchemaDigest string `json:"schema_digest"`
}

ExternalToolIdentity is the durable, secret-free identity of ONE external tool installed into a loop's external slot. Name is the model-facing tool name; SchemaDigest is the hex SHA-256 of the tool's compacted argument JSON Schema. The schema itself is deliberately NOT recorded: an external schema is attacker- or third-party-supplied and may carry descriptions, defaults, or examples that embed secrets, so only its digest crosses into the journal.

type Factory

type Factory struct {
	// contains filtered or unexported fields
}

Factory mints fresh event Headers, stamping each with a new EventID and the current CreatedAt at creation time. It is the single creation seam every Enduring event flows through so the journal sees a stable idempotency key and creation timestamp.

func NewFactory

func NewFactory(newID IDGen, now Clock) *Factory

NewFactory wires the id generator and clock the Factory mints from.

func (*Factory) NewHeader

func (f *Factory) NewHeader() (Header, error)

NewHeader mints a fresh EventID + CreatedAt onto an empty Header. Callers fill Coordinates/Cause. It is Stamp of the zero Header.

func (*Factory) Stamp

func (f *Factory) Stamp(h Header) (Header, error)

Stamp returns a COPY of h with a fresh EventID and CreatedAt, preserving the caller's existing Coordinates and Cause (the producer set those before stamping). A crypto/rand failure from newID is propagated, never swallowed, and no partial Header escapes — the returned Header carries a zero EventID on error.

func (*Factory) StampCompactWaiterRejected

func (f *Factory) StampCompactWaiterRejected(ev CompactWaiterRejected) (Header, error)

StampCompactWaiterRejected stamps CreatedAt while preserving and verifying a rejected waiter's deterministic EventID. It cannot inject an arbitrary ID.

func (*Factory) StampCompactWaiterResolved

func (f *Factory) StampCompactWaiterResolved(ev CompactWaiterResolved) (Header, error)

StampCompactWaiterResolved stamps CreatedAt while preserving and verifying a resolved waiter's deterministic EventID. It cannot inject an arbitrary ID.

func (*Factory) StampWorkflowActivity

func (f *Factory) StampWorkflowActivity(ev WorkflowActivity, deterministicID uuid.UUID) (WorkflowActivity, error)

StampWorkflowActivity stamps a fully specified WorkflowActivity with the caller's stable source activity ID. Unlike Stamp, it never calls newID: retry logic must be able to submit the same journal identity again. An explicit CreatedAt is preserved so a deterministic producer can make the complete journal payload byte-identical across retries; a zero CreatedAt uses the factory clock for ordinary callers. The body is validated after stamping so the returned value is safe to send through the normal Hub/journal path.

type FieldName

type FieldName string

FieldName is the identity/body field an InvalidEventError points at.

const (
	FieldEventID            FieldName = "EventID"
	FieldSessionID          FieldName = "SessionID"
	FieldLoopID             FieldName = "LoopID"
	FieldTurnID             FieldName = "TurnID"
	FieldStepID             FieldName = "StepID"
	FieldToolExecutionID    FieldName = "ToolExecutionID"
	FieldConsistency        FieldName = "Consistency"
	FieldTrigger            FieldName = "Trigger"
	FieldCause              FieldName = "Cause"
	FieldCommandID          FieldName = "CommandID"
	FieldRequestID          FieldName = "RequestID"
	FieldActiveLoopID       FieldName = "ActiveLoopID"
	FieldTargetLoopID       FieldName = "TargetLoopID"
	FieldCategory           FieldName = "Category"
	FieldModel              FieldName = "Model"
	FieldModelKey           FieldName = "ModelKey"
	FieldContextLimits      FieldName = "ContextLimits"
	FieldEffort             FieldName = "Effort"
	FieldUsage              FieldName = "Usage"
	FieldMessages           FieldName = "Messages"
	FieldVisibility         FieldName = "Visibility"
	FieldDefinition         FieldName = "Definition"
	FieldRunID              FieldName = "RunID"
	FieldRuntime            FieldName = "Runtime"
	FieldAgentRuntime       FieldName = "AgentRuntime"
	FieldACPSessionID       FieldName = "ACPSessionID"
	FieldDuration           FieldName = "Duration"
	FieldStage              FieldName = "Stage"
	FieldReasonCode         FieldName = "ReasonCode"
	FieldAttemptID          FieldName = "AttemptID"
	FieldReason             FieldName = "Reason"
	FieldRejectReason       FieldName = "RejectReason"
	FieldWaiterCommandIDs   FieldName = "WaiterCommandIDs"
	FieldSummary            FieldName = "Summary"
	FieldRetained           FieldName = "Retained"
	FieldPostContext        FieldName = "PostContext"
	FieldCommittedEventID   FieldName = "CommittedEventID"
	FieldSource             FieldName = "Source"
	FieldActor              FieldName = "Actor"
	FieldGeneration         FieldName = "Generation"
	FieldTools              FieldName = "Tools"
	FieldProcess            FieldName = "Process"
	FieldGateID             FieldName = "GateID"
	FieldClassifier         FieldName = "Classifier"
	FieldClassifierRevision FieldName = "ClassifierRevision"
	FieldStatus             FieldName = "Status"
	FieldRisk               FieldName = "Risk"
	FieldAuthorization      FieldName = "Authorization"
	FieldCategories         FieldName = "Categories"
	FieldAutoApproved       FieldName = "AutoApproved"
	// FieldIntegrationName names IntegrationStatus.Name. It is not spelled
	// "FieldName": that identifier is this file's FieldName TYPE.
	FieldIntegrationName    FieldName = "Name"
	FieldState              FieldName = "State"
	FieldDetail             FieldName = "Detail"
	FieldEpoch              FieldName = "Epoch"
	FieldAdoptedFingerprint FieldName = "AdoptedFingerprint"
	FieldManifest           FieldName = "Manifest"
	FieldDrift              FieldName = "Drift"
	FieldMessage            FieldName = "Message"
	FieldWorkflowName       FieldName = "WorkflowName"
	FieldWorkflowVersion    FieldName = "WorkflowVersion"
	FieldActivityKind       FieldName = "ActivityKind"
	FieldOccurredAt         FieldName = "OccurredAt"
	FieldVertexID           FieldName = "VertexID"
	FieldVertexLabel        FieldName = "VertexLabel"
	FieldProgress           FieldName = "Progress"
	// FieldType names the whole event (not one coordinate) on the fail-secure
	// unknown-type path, paired with RuleUnknownType.
	FieldType FieldName = "Type"
)

Identity / body field names, named so an InvalidEventError reads precisely.

type ForeignSessionBound

type ForeignSessionBound struct {
	Header
	ForeignSID string `json:"foreign_sid"`
	// contains filtered or unexported fields
}

ForeignSessionBound records the foreign agent session id for adapters that cannot accept a pre-minted id at LoopStarted time. It is loop-scoped and Enduring because restore needs it to resume the foreign session.

func (ForeignSessionBound) Class

func (ForeignSessionBound) Class() Class

func (ForeignSessionBound) EndsTurn

func (ForeignSessionBound) EndsTurn() bool

func (ForeignSessionBound) Scope

func (ForeignSessionBound) Scope() Scope

type GateOpened

type GateOpened struct {
	Header
	Gate gate.Gate `json:"gate,omitzero"`
	// contains filtered or unexported fields
}

GateOpened is the PUBLIC activation event for a gate. It carries the pure public envelope (the Gate) and NO private payload — the typed payload stays server-private inside the GatePreparedRecord. It fans out to SSE/history and makes the gate listable/answerable.

func (GateOpened) Class

func (GateOpened) Class() Class

func (GateOpened) EndsTurn

func (GateOpened) EndsTurn() bool

func (GateOpened) Scope

func (GateOpened) Scope() Scope

type GatePrepared

type GatePrepared struct {
	Header
	Gate gate.Gate `json:"gate,omitzero"`
	// contains filtered or unexported fields
}

GatePrepared is the private/internal prepared projection inside a GatePreparedRecord. It is durable so restore can validate a later GateOpened, but it is NOT fanned out to SSE/history and does not make the gate answerable. It must never be appended through NewEventRecord or hub.PublishEvent; it is only valid inside journal.GatePreparedRecord.

func (GatePrepared) Class

func (GatePrepared) Class() Class

func (GatePrepared) EndsTurn

func (GatePrepared) EndsTurn() bool

func (GatePrepared) Scope

func (GatePrepared) Scope() Scope

type GateResolved

type GateResolved struct {
	Header
	GateID gate.ID `json:"gate_id,omitzero"`
	// Resolver is the self-contained scope discriminator the DECODER needs to pick
	// this record's identity profile. GatePrepared and GateOpened embed the full
	// gate.Gate (whose Resolver already names the owner), but GateResolved carries
	// only the GateID and coordinates — so without this field a decoded GateResolved
	// could not tell a host-owned gate (SessionID required; loop/turn/step optional)
	// from a loop-owned one (full step profile). It is additive and omitempty: an old
	// record written before this field existed decodes with an empty Resolver and is
	// validated under the strict loop-owned profile, matching every gate record that
	// could ever restore before this change.
	Resolver gate.ResolverKind   `json:"resolver,omitempty"`
	Reason   gate.CloseReason    `json:"reason,omitempty"`
	Action   string              `json:"action,omitempty"`
	Source   gate.ResponseSource `json:"source,omitzero"`
	// Audit is a sealed interface (gate.ResponseAudit) with no general JSON codec,
	// so it is excluded from direct serialization — like PermissionRequested.Request
	// — and projected through gate.MarshalResponseAudit by the marshaler.
	Audit gate.ResponseAudit `json:"-"`
	// contains filtered or unexported fields
}

GateResolved is the SINGLE atomic close-with-answer record. The decision Action stays in the clear (for a permission gate it is one of the three exact gate.ApprovalAction strings); Reason is the close reason; per-kind Audit is redaction-aware (requirement and candidate descriptions, never grant tokens, never raw tool arguments). A non-answer close (abandon/owner) sets Reason with Action="".

func (GateResolved) Class

func (GateResolved) Class() Class

func (GateResolved) EndsTurn

func (GateResolved) EndsTurn() bool

func (GateResolved) Scope

func (GateResolved) Scope() Scope
type Header struct {
	// Coordinates is the producer's location in the hierarchy: SessionID on every
	// event; LoopID for loop-scoped events; TurnID for turn events; StepID for
	// step/tool scoped events. Session-scoped events leave LoopID/TurnID/StepID zero.
	identity.Coordinates

	// AgentName is the immutable attribution name of the agent driving the producing
	// loop, stamped at loop creation onto its LoopStarted (and carried on the loop's
	// other events via the same Header). It is empty (omitzero) for a plain loop and for
	// any record persisted before AgentName existed, so old journals stay byte-compatible
	// — the field serializes additively with no new codec case. Restore validates the
	// root loop's stamped name against the configured primary's name.
	AgentName identity.AgentName `json:"agent_name,omitzero"`

	// EventID identifies this event. Header carries this identity directly; detailed
	// wiring is sequenced after the journal follow-on.
	EventID uuid.UUID `json:"event_id,omitzero"`

	// CreatedAt is when this event was created (minted at creation, not delivery).
	// It is the journal's creation timestamp for every Enduring event.
	CreatedAt time.Time `json:"created_at,omitzero"`

	// Cause is the direct cause of this event. For UserInput/SubagentResult
	// resolution events (TurnStarted, TurnFoldedInto, InputCancelled, InputQueued,
	// TurnRejected), Cause.CommandID is the submit command id. For an event caused by
	// a SubagentResult, Cause.LoopID is the producing agent's loop id (its
	// quiescence wake token). Cause.Agency surfaces who caused it, but ONLY the turn-
	// resolution events stamp it — TurnStarted, TurnFoldedInto, and InputCancelled
	// (per design §444-446); InputQueued and TurnRejected carry Cause.CommandID but
	// NOT Cause.Agency.
	Cause identity.Cause `json:"cause,omitzero"`

	// EventVisibility is omitted for Public so pre-visibility journals remain a
	// byte-for-byte fixed point. Internal events always persist the non-zero tag.
	EventVisibility EventVisibility `json:"visibility,omitzero"`
}

Header is the producer identity stamped on every event. The producer (the loop for loop events, the session for session events) fills it in; fan-in, filter, and journal consumers read it without a transport-only envelope.

func (Header) EventHeader

func (h Header) EventHeader() Header

EventHeader returns the embedded Header so every event satisfies Event without per-type boilerplate.

func (Header) ReplyTo

func (h Header) ReplyTo() uuid.UUID

ReplyTo returns the id of the command this event answers (its Cause.CommandID). It is promoted onto every Reply event via the embedded Header.

func (Header) Visibility

func (h Header) Visibility() EventVisibility

Visibility returns the event's durable product-delivery classification.

type HustleCompleted

type HustleCompleted struct {
	Header
	Run      HustleRunDescriptor `json:"run"`
	Duration time.Duration       `json:"duration,omitzero"`
	Usage    *content.Usage      `json:"usage,omitempty"`
	// contains filtered or unexported fields
}

HustleCompleted durably records one successful terminal outcome.

func (HustleCompleted) Class

func (HustleCompleted) Class() Class

func (HustleCompleted) EndsTurn

func (HustleCompleted) EndsTurn() bool

func (HustleCompleted) Scope

func (HustleCompleted) Scope() Scope

type HustleFailed

type HustleFailed struct {
	Header
	Run        HustleRunDescriptor `json:"run"`
	Duration   time.Duration       `json:"duration,omitzero"`
	Stage      hustle.Stage        `json:"stage"`
	ReasonCode hustle.ReasonCode   `json:"reason_code"`
	Usage      *content.Usage      `json:"usage,omitempty"`
	// contains filtered or unexported fields
}

HustleFailed durably records one bounded, security-safe failure outcome.

func (HustleFailed) Class

func (HustleFailed) Class() Class

func (HustleFailed) EndsTurn

func (HustleFailed) EndsTurn() bool

func (HustleFailed) Scope

func (HustleFailed) Scope() Scope

type HustleRunDescriptor

type HustleRunDescriptor struct {
	Definition hustle.DefinitionDescriptor `json:"definition"`
	RunID      hustle.RunID                `json:"run_id"`
	Runtime    ModelRuntime                `json:"runtime,omitzero"`
}

HustleRunDescriptor is the secret-free identity and resolved runtime of one invocation. Started events carry a zero Runtime; terminal events carry the resolved runtime whenever model resolution succeeded.

type HustleStarted

type HustleStarted struct {
	Header
	Run HustleRunDescriptor `json:"run"`
	// contains filtered or unexported fields
}

HustleStarted durably records ownership before scheduler eligibility.

func (HustleStarted) Class

func (HustleStarted) Class() Class

func (HustleStarted) EndsTurn

func (HustleStarted) EndsTurn() bool

func (HustleStarted) Scope

func (HustleStarted) Scope() Scope

type IDGen

type IDGen func() (uuid.UUID, error)

type InputCancelled

type InputCancelled struct {
	Header
	TurnIndex TurnIndex            `json:"turn_index,omitzero"`
	Reason    CancelReason         `json:"reason,omitzero"`
	Message   *content.UserMessage `json:"message,omitzero"`
	// contains filtered or unexported fields
}

InputCancelled is emitted when a queued input leaves the loop queue without committing — client retract, or a return after an abnormal turn end. Header.Cause.CommandID is the submit command id; Header.TurnID is the active turn that caused a return, or zero for a pure client retract outside a turn. Message is the returned/retracted user message.

func (InputCancelled) Class

func (InputCancelled) Class() Class

func (InputCancelled) EndsTurn

func (InputCancelled) EndsTurn() bool

func (InputCancelled) Scope

func (InputCancelled) Scope() Scope

type InputQueued

type InputQueued struct {
	Header
	// contains filtered or unexported fields
}

InputQueued is the Ephemeral Reply event for a UserInput accepted into the loop inbox but not yet assigned to a turn (it later resolves to TurnStarted, TurnFoldedInto, or InputCancelled). Header.Cause.CommandID is the submit command id. Ephemeral: it self-heals — the authoritative resolution event still follows if this is dropped.

func (InputQueued) Class

func (InputQueued) Class() Class

func (InputQueued) EndsTurn

func (InputQueued) EndsTurn() bool

func (InputQueued) Scope

func (InputQueued) Scope() Scope

type IntegrationState

type IntegrationState uint8

IntegrationState is how an integration is doing, in the only granularity a consumer outside it can act on. The zero value is not a state.

It is five values on purpose. An integration's own lifecycle is invariably richer — the MCP client alone distinguishes configured, starting, authenticating, discovering, ready, degraded, reconnecting, failed, closing, and closed — and mirroring that here would be this package learning one integration's model and imposing it on the next. The projection is lossy by design; an integration that needs its full state machine seen renders it from its own status surface, which it still owns.

const (
	// IntegrationStarting is an integration coming up. It is not serving yet,
	// and it is not a fault: everything is starting once.
	IntegrationStarting IntegrationState = iota + 1
	// IntegrationReady is an integration serving normally.
	IntegrationReady
	// IntegrationDegraded is an integration still serving, but with less than
	// it advertised — a lost connection it is retrying, a capability that
	// failed. It is distinct from Failed because the difference is actionable:
	// a degraded integration may recover on its own.
	IntegrationDegraded
	// IntegrationFailed is an integration not serving. It may still recover.
	IntegrationFailed
	// IntegrationClosed is an integration that has shut down. It is terminal
	// for this Session.
	IntegrationClosed
)

func (IntegrationState) String

func (s IntegrationState) String() string

String returns a stable lowercase identifier, or "unknown".

func (IntegrationState) Valid

func (s IntegrationState) Valid() bool

Valid reports whether s is a declared state. It fails closed on the zero value and on anything outside the set, which is what makes an unset State a rejected event rather than a silently rendered "starting".

type IntegrationStatus

type IntegrationStatus struct {
	Header
	// Source names the kind of integration, e.g. "mcp". It is the namespace
	// Name lives in: two integrations may both call a binding "github".
	Source string `json:"source"`
	// Name identifies the one integration within Source, e.g. a binding name.
	Name string `json:"name"`
	// State is how it is doing.
	State IntegrationState `json:"state"`
	// Detail is bounded, redacted explanatory text, or empty. It is
	// diagnostics, never an instruction and never a fact the host should act
	// on programmatically — that is what State is for.
	Detail string `json:"detail,omitempty"`
	// contains filtered or unexported fields
}

IntegrationStatus reports the live state of one external integration.

It is Ephemeral, which is a claim about what it means rather than a convenience: a status self-heals — the next one supersedes it completely, and a consumer that missed three learns the truth from the fourth. It is also the only honest classification available. An integration is a LIVE resource that a restore reconstructs by reconnecting, never by replaying bytes, so a journaled status could only ever describe a connection that no longer exists. Persisting one would invite a restored Session to render a server as "ready" before anything had dialled it.

Every field is safe to journal even though none of it is: Source and Name are the integration's own validated identifiers, State is a closed enum, and Detail is bounded prose the integration has already redacted. Credentials, tokens, URLs with secrets, raw arguments, and server content must never appear in Detail — an integration that cannot say what is wrong without one says less.

func (IntegrationStatus) Class

func (IntegrationStatus) Class() Class

func (IntegrationStatus) EndsTurn

func (IntegrationStatus) EndsTurn() bool

func (IntegrationStatus) Scope

func (IntegrationStatus) Scope() Scope

type InvalidEventError

type InvalidEventError struct {
	Event EventName
	Field FieldName
	Rule  Rule
}

InvalidEventError reports that an event violates the ID fill matrix: Field names the offending identity/body field and Rule says whether it was required or had to be zero. 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.

func (*InvalidEventError) Error

func (e *InvalidEventError) Error() string

type LegacyRuntimeMigrationError

type LegacyRuntimeMigrationError struct {
	Type   string
	Field  string
	Reason string
}

LegacyRuntimeMigrationError reports a v1 lifecycle payload that cannot be migrated to the current ModelRuntime representation without guessing. Valid pre-runtime LoopInferenceChanged records always carry model; accepting a record with neither model nor runtime would silently erase the selected model.

func (*LegacyRuntimeMigrationError) Error

type LoopAgentSessionBound

type LoopAgentSessionBound struct {
	Header
	ACPSessionID string `json:"acp_session_id"`
	// contains filtered or unexported fields
}

LoopAgentSessionBound records the durable foreign agent session binding for a loop.

func (LoopAgentSessionBound) Class

func (LoopAgentSessionBound) Class() Class

func (LoopAgentSessionBound) EndsTurn

func (LoopAgentSessionBound) EndsTurn() bool

func (LoopAgentSessionBound) Scope

func (LoopAgentSessionBound) Scope() Scope

type LoopExternalToolsetChanged

type LoopExternalToolsetChanged struct {
	Header
	Source     string                 `json:"source"`
	Generation string                 `json:"generation"`
	Tools      []ExternalToolIdentity `json:"tools,omitempty"`
	// contains filtered or unexported fields
}

LoopExternalToolsetChanged durably records that a loop's external tool slot for one Source was REPLACED, effective at the loop's next turn boundary. It records identity only (Source + Generation + per-tool name/schema digest) — never the tool factories and never the schemas themselves.

It is deliberately NOT a restore input: external tools are live resources (an MCP connection cannot be rebuilt from journal bytes), so a restored loop comes up with an EMPTY external slot and the composing application re-installs. The event exists for audit, drift detection, and catalog projection — replay folds it for its runtime identity only, never to reconstruct tools.

func (LoopExternalToolsetChanged) Class

func (LoopExternalToolsetChanged) Class() Class

func (LoopExternalToolsetChanged) EndsTurn

func (LoopExternalToolsetChanged) EndsTurn() bool

func (LoopExternalToolsetChanged) Scope

func (LoopExternalToolsetChanged) Scope() Scope

type LoopIdle

type LoopIdle struct {
	Header
	// contains filtered or unexported fields
}

LoopIdle is emitted when a loop parks with no active turn. Header.SessionID and Header.LoopID are set; TurnID/StepID are zero. It drives session quiescence.

func (LoopIdle) Class

func (LoopIdle) Class() Class

func (LoopIdle) EndsTurn

func (LoopIdle) EndsTurn() bool

func (LoopIdle) Scope

func (LoopIdle) Scope() Scope

type LoopInferenceChanged

type LoopInferenceChanged struct {
	Header
	Runtime ModelRuntime `json:"runtime,omitzero"`
	// contains filtered or unexported fields
}

LoopInferenceChanged durably selects the resolved secret-free runtime for this loop at the next turn boundary.

func (LoopInferenceChanged) Class

func (LoopInferenceChanged) Class() Class

func (LoopInferenceChanged) EndsTurn

func (LoopInferenceChanged) EndsTurn() bool

func (LoopInferenceChanged) Scope

func (LoopInferenceChanged) Scope() Scope

type LoopModeChanged

type LoopModeChanged struct {
	Header
	PreviousMode string       `json:"previous_mode,omitzero"`
	Mode         string       `json:"mode,omitzero"`
	Runtime      ModelRuntime `json:"runtime,omitzero"`
	// contains filtered or unexported fields
}

LoopModeChanged durably selects one predeclared loop mode.

func (LoopModeChanged) Class

func (LoopModeChanged) Class() Class

func (LoopModeChanged) EndsTurn

func (LoopModeChanged) EndsTurn() bool

func (LoopModeChanged) Scope

func (LoopModeChanged) Scope() Scope

type LoopRestoreTombstoned

type LoopRestoreTombstoned struct {
	Header
	Category string `json:"category"`
	// contains filtered or unexported fields
}

LoopRestoreTombstoned records that a non-root child survived restore as a closed, failed registry entry because its durable runtime could not be re-authorized against the current runtime catalog.

func (LoopRestoreTombstoned) Class

func (LoopRestoreTombstoned) Class() Class

func (LoopRestoreTombstoned) EndsTurn

func (LoopRestoreTombstoned) EndsTurn() bool

func (LoopRestoreTombstoned) Scope

func (LoopRestoreTombstoned) Scope() Scope

type LoopScope

type LoopScope struct {
	All   bool                   // deliver from every loop
	Loops map[uuid.UUID]struct{} // when !All, only these loop ids
}

LoopScope selects which loops a class of events is delivered from. All is a short-circuit "every loop"; otherwise only loop ids present in Loops match.

func (LoopScope) Matches

func (s LoopScope) Matches(loopID uuid.UUID) bool

Matches reports whether loopID is in scope. All short-circuits to true; with a nil or empty Loops set and All false, nothing matches.

type LoopStarted

type LoopStarted struct {
	Header
	// Runtime is the initial resolved model identity, limits, and effort. It is
	// durable so restore and catalog repair never consult a mutable catalog.
	Runtime      ModelRuntime  `json:"runtime,omitzero"`
	AgentRuntime *AgentRuntime `json:"agent_runtime,omitempty"`
	// ParentToolUseID is the durable provider tool-use id of the agent tool call
	// that spawned this loop (content.ToolUseBlock.ID), empty for loops not spawned by
	// a tool call (e.g. the primary/root). It is the durable carrier that correlates a
	// child loop back to its parent tool call across persist/restore; omitzero so old
	// journal records without the field decode to "".
	ParentToolUseID string `json:"parent_tool_use_id,omitzero"`
	// ForeignSID is the foreign agent's session id this loop is bound to, for
	// foreign-engine loops only; empty for native loops. It is the durable handle
	// used to --resume the foreign session across turns and across restore. omitzero
	// so old journal records (and native loops) decode to "". Mirrors
	// ParentToolUseID: identity metadata carried on the loop's start event.
	ForeignSID string `json:"foreign_sid,omitzero"`
	// InitialMode is the validated mode selected when the loop was constructed.
	// Empty identifies the base mode and preserves legacy records.
	InitialMode string `json:"initial_mode,omitzero"`
	// InitialRequestID proves the prepared delegate's initial command was accepted
	// before this durable loop-creation commit. Zero for roots/plain loops.
	InitialRequestID uuid.UUID `json:"initial_request_id,omitzero"`
	// DisplayName is the loop's user-facing presentation label, empty when the loop
	// declared none (consumers fall back to Header.AgentName). omitzero so old journal
	// records decode to "".
	DisplayName string `json:"display_name,omitzero"`
	// Description is the loop's user-facing description, empty when none declared.
	Description string `json:"description,omitzero"`
	// contains filtered or unexported fields
}

LoopStarted is published by Session.NewLoop when a loop is registered. Header.Coordinates is the NEW loop (SessionID+LoopID set; TurnID/StepID zero). Header.Cause.Coordinates is the spawning loop/turn/step (zero for the primary = root); Cause.Agency = AgencyMachine. It is the durable loop-tree record for subscribers active at creation time.

func (LoopStarted) Class

func (LoopStarted) Class() Class

func (LoopStarted) EndsTurn

func (LoopStarted) EndsTurn() bool

func (LoopStarted) Scope

func (LoopStarted) Scope() Scope

type ModelRuntime

type ModelRuntime struct {
	Key       model.ModelKey      `json:"key"`
	Limits    model.ContextLimits `json:"limits"`
	Effort    model.Effort        `json:"effort,omitzero"`
	APIFormat model.APIFormat     `json:"api_format,omitzero"`
	BaseURL   string              `json:"base_url,omitzero"`
}

ModelRuntime is the single durable, secret-free description of the model runtime selected for a loop. It deliberately excludes endpoint and catalog data so journal replay and catalog repair do not depend on mutable config.

APIFormat and BaseURL identify the declared transport a selection resolved against at the moment it was accepted. A zero/absent value on either field means "use the definition's declared base transport" — this is both the interpretation for every pre-existing journal record written before these fields existed, and the default for any caller that does not yet populate them. Both fields are unconstrained by validateModelRuntime: they always originate from a model.Model that already passed model.Validate() at the point a live change accepted it, so re-validating them here would be redundant, not additional safety.

type PermissionDecided

type PermissionDecided struct {
	Header
	ToolExecutionID uuid.UUID                `json:"tool_execution_id,omitzero"`
	Effect          PermissionDecisionEffect `json:"effect,omitempty"`
	Reason          string                   `json:"reason,omitempty"`
	Subject         string                   `json:"subject,omitempty"`
	Audit           string                   `json:"audit,omitempty"`
	// contains filtered or unexported fields
}

PermissionDecided is emitted for a non-gated permission decision. Subject and Audit are redacted summaries; grant tokens and raw args must never appear here.

func (PermissionDecided) Class

func (PermissionDecided) Class() Class

func (PermissionDecided) EndsTurn

func (PermissionDecided) EndsTurn() bool

func (PermissionDecided) Scope

func (PermissionDecided) Scope() Scope

type PermissionDecisionEffect

type PermissionDecisionEffect string

PermissionDecisionEffect is the durable approve/deny outcome for a non-gated permission decision. Ask is intentionally absent: gated asks are represented by GateOpened/GateResolved, not PermissionDecided.

const (
	PermissionEffectApprove PermissionDecisionEffect = "approve"
	PermissionEffectDeny    PermissionDecisionEffect = "deny"
)

type PermissionRequested

type PermissionRequested struct {
	Header
	ToolExecutionID uuid.UUID `json:"tool_execution_id,omitzero"`
	// Request is validated at the durable boundary (tool.ValidateRequest on
	// marshal, the strict gate.DecodeRequest on unmarshal), so a malformed or
	// token-bearing record can neither be journaled nor restored. It is
	// projected by the marshaler rather than serialized directly.
	Request tool.Request `json:"-"`
	// contains filtered or unexported fields
}

PermissionRequested is emitted when a tool call needs interactive approval. The per-turn stream (TUI) renders the typed prepared Request — the summary plus the displayed unmet requirement and candidate descriptions. The wire carries the same typed request through the strict request decoder; it never carries grant tokens or raw tool arguments (tool.Request has neither).

func (PermissionRequested) Class

func (PermissionRequested) Class() Class

func (PermissionRequested) EndsTurn

func (PermissionRequested) EndsTurn() bool

func (PermissionRequested) Scope

func (PermissionRequested) Scope() Scope

type PermissionReviewCompleted

type PermissionReviewCompleted struct {
	Header
	GateID             gate.ID                   `json:"gate_id,omitzero"`
	ToolExecutionID    uuid.UUID                 `json:"tool_execution_id,omitzero"`
	Classifier         hustle.Name               `json:"classifier,omitzero"`
	ClassifierRevision string                    `json:"classifier_revision,omitzero"`
	Status             gate.ReviewStatus         `json:"status,omitzero"`
	Risk               gate.ReviewRisk           `json:"risk,omitzero"`
	Authorization      gate.ReviewAuthorization  `json:"authorization,omitzero"`
	Categories         []gate.ReviewRiskCategory `json:"categories,omitzero"`
	AutoApproved       bool                      `json:"auto_approved,omitzero"`
	// contains filtered or unexported fields
}

PermissionReviewCompleted records a classifier's closed terminal audit status. It deliberately excludes prompt, evidence, model output, rationale, gate candidates, and grant material.

func (PermissionReviewCompleted) Class

func (PermissionReviewCompleted) Class() Class

func (PermissionReviewCompleted) EndsTurn

func (PermissionReviewCompleted) EndsTurn() bool

func (PermissionReviewCompleted) Scope

func (PermissionReviewCompleted) Scope() Scope

type PermissionReviewStarted

type PermissionReviewStarted struct {
	Header
	GateID             gate.ID     `json:"gate_id,omitzero"`
	ToolExecutionID    uuid.UUID   `json:"tool_execution_id,omitzero"`
	Classifier         hustle.Name `json:"classifier,omitzero"`
	ClassifierRevision string      `json:"classifier_revision,omitzero"`
	// contains filtered or unexported fields
}

PermissionReviewStarted records that one classifier began reviewing an already-open permission gate. It carries only stable identity and revision metadata; permission subject data and classifier inputs are deliberately absent from the durable event.

func (PermissionReviewStarted) Class

func (PermissionReviewStarted) Class() Class

func (PermissionReviewStarted) EndsTurn

func (PermissionReviewStarted) EndsTurn() bool

func (PermissionReviewStarted) Scope

func (PermissionReviewStarted) Scope() Scope

type PressureLevel

type PressureLevel uint8

PressureLevel is the closed current-context pressure domain.

const (
	PressureUnknown PressureLevel = iota
	PressureNormal
	PressureCompact
	PressureHardLimit
)

type ProcessBackgrounded

type ProcessBackgrounded struct {
	Header
	Process tool.ProcessLifecycleMetadata `json:"process"`
	// contains filtered or unexported fields
}

ProcessBackgrounded durably records the transition from a foreground tool call to a session-owned background process.

func (ProcessBackgrounded) Class

func (ProcessBackgrounded) Class() Class

func (ProcessBackgrounded) EndsTurn

func (ProcessBackgrounded) EndsTurn() bool

func (ProcessBackgrounded) Scope

func (ProcessBackgrounded) Scope() Scope

type ProcessCompleted

type ProcessCompleted struct {
	Header
	Process tool.ProcessLifecycleMetadata `json:"process"`
	// contains filtered or unexported fields
}

ProcessCompleted durably records one terminal supervised-process outcome.

func (ProcessCompleted) Class

func (ProcessCompleted) Class() Class

func (ProcessCompleted) EndsTurn

func (ProcessCompleted) EndsTurn() bool

func (ProcessCompleted) Scope

func (ProcessCompleted) Scope() Scope

type ProcessLost

type ProcessLost struct {
	Header
	Process tool.ProcessLifecycleMetadata `json:"process"`
	// contains filtered or unexported fields
}

ProcessLost durably records a previously nonterminal local process that cannot be reattached after restore.

func (ProcessLost) Class

func (ProcessLost) Class() Class

func (ProcessLost) EndsTurn

func (ProcessLost) EndsTurn() bool

func (ProcessLost) Scope

func (ProcessLost) Scope() Scope

type ProcessStarted

type ProcessStarted struct {
	Header
	Process tool.ProcessLifecycleMetadata `json:"process"`
	// contains filtered or unexported fields
}

ProcessStarted durably records that a supervised process reached running.

func (ProcessStarted) Class

func (ProcessStarted) Class() Class

func (ProcessStarted) EndsTurn

func (ProcessStarted) EndsTurn() bool

func (ProcessStarted) Scope

func (ProcessStarted) Scope() Scope

type ProcessStopRequested

type ProcessStopRequested struct {
	Header
	Process tool.ProcessLifecycleMetadata `json:"process"`
	// contains filtered or unexported fields
}

ProcessStopRequested durably records a nonterminal portable stop request.

func (ProcessStopRequested) Class

func (ProcessStopRequested) Class() Class

func (ProcessStopRequested) EndsTurn

func (ProcessStopRequested) EndsTurn() bool

func (ProcessStopRequested) Scope

func (ProcessStopRequested) Scope() Scope

type RejectReason

type RejectReason uint8

RejectReason explains why a UserInput submit was refused (carried by TurnRejected). It is the single source of truth for submit-rejection reasons — the loop publishes it on the event stream; there is no command-side copy (the former command.Disposition reply, with its command.RejectReason, was removed).

const (
	// RejectUnspecified is the zero-value sentinel: NOT a reason the loop ever
	// produces. It exists so a zero-valued TurnRejected{} does not masquerade as a
	// real reason and so the json:"reason,omitzero" tag only drops a genuinely-empty
	// reason (every real reason below is non-zero and always serializes). A
	// RejectReason that compares equal to this came from a zero value, not a decision.
	RejectUnspecified RejectReason = iota
	// RejectQueueFull: the loop's inbox is at capacity.
	RejectQueueFull
	// RejectShuttingDown: the loop is shutting down and accepts no new input.
	RejectShuttingDown
	// RejectInternal: a transient internal failure (e.g. id generation); the loop is
	// healthy and the caller MAY retry.
	RejectInternal
)

type Reply

type Reply interface {
	Event

	ReplyTo() uuid.UUID // == Header.Cause.CommandID: the command this answers
	// contains filtered or unexported methods
}

Reply is an event that is the direct outcome of a command, delivered on the normal fan-in (classed Ephemeral/Enduring like any other event — NOT a point-to-point channel). It is the typed replacement for the command.Disposition reply: an issuer recognises "the answer to my command" via ReplyTo() == its command id.

type RestoreDone

type RestoreDone struct {
	Header
	// contains filtered or unexported fields
}

RestoreDone marks a successful end of a session restore from the durable journal — the session is reconstructed and ready to resume. It is session-scoped and Enduring; Header.SessionID is set, LoopID/TurnID/StepID are zero.

func (RestoreDone) Class

func (RestoreDone) Class() Class

func (RestoreDone) EndsTurn

func (RestoreDone) EndsTurn() bool

func (RestoreDone) Scope

func (RestoreDone) Scope() Scope

type RestoreErrored

type RestoreErrored struct {
	Header
	// Err is the typed cause of the restore failure; tagged json:"-" because an
	// error value has no stable codec (mirrors TurnFailed.Err). Callers inspect it
	// in-memory via errors.As; a journal records the failure via the event itself.
	Err error `json:"-"`
	// contains filtered or unexported fields
}

RestoreErrored marks a failed session restore from the durable journal. Err carries the typed cause; like TurnFailed.Err an error value cannot round-trip through encoding/json, so it is tagged json:"-" — callers read it in-memory via errors.As, and the durable codec's projection lands in a later phase. It is session-scoped and Enduring; Header.SessionID is set, LoopID/TurnID/StepID are zero.

func (RestoreErrored) Class

func (RestoreErrored) Class() Class

func (RestoreErrored) EndsTurn

func (RestoreErrored) EndsTurn() bool

func (RestoreErrored) Scope

func (RestoreErrored) Scope() Scope

type RestoreStarted

type RestoreStarted struct {
	Header
	// contains filtered or unexported fields
}

RestoreStarted marks the beginning of a session restore from the durable journal. Like SessionStarted it is session-scoped and Enduring (an authoritative session-lifecycle transition, never a turn-ender); Header.SessionID is set, LoopID/TurnID/StepID are zero.

func (RestoreStarted) Class

func (RestoreStarted) Class() Class

func (RestoreStarted) EndsTurn

func (RestoreStarted) EndsTurn() bool

func (RestoreStarted) Scope

func (RestoreStarted) Scope() Scope

type RestoredError

type RestoredError struct {
	Kind    string `json:"kind"`
	Message string `json:"message"`
}

RestoredError is the leaf error a restored TurnFailed (or RestoreErrored) carries in place of its original typed cause. A live error value has no stable JSON codec — its concrete type, fields, and wrapping chain cannot round-trip through encoding/json — so the event codec projects TurnFailed.Err to a {kind,message} pair on marshal and reconstructs a *RestoredError on unmarshal. Kind is the stable classification (see ErrKind); Message is the original Error() text, preserved verbatim so no human-readable detail is lost even when the concrete type is not. RestoredError deliberately does not implement ModelFacingError: ordinary and legacy journal records must never become model-facing merely because their message or kind happens to look safe.

func (*RestoredError) Error

func (e *RestoredError) Error() string

Error renders "<kind>: <message>", mirroring how the original typed cause read.

type RestoredModelFacingError

type RestoredModelFacingError struct {
	Kind    string `json:"kind"`
	Message string `json:"message"`
	Detail  string `json:"-"`
}

RestoredModelFacingError is the distinct restore form for an error that was explicitly marked safe before it crossed the durable event boundary. Detail is kept separate from Kind and Message so the restore path never infers safety from arbitrary legacy error text.

func (*RestoredModelFacingError) Error

func (e *RestoredModelFacingError) Error() string

func (*RestoredModelFacingError) ModelFacingError

func (e *RestoredModelFacingError) ModelFacingError() string

ModelFacingError returns the already-normalized, bounded detail persisted by the event codec. Callers still bound it at their presentation boundary.

type Rule

type Rule string

Rule is the human-readable invariant an InvalidEventError records, so the caller learns WHY the field is wrong (required vs must-be-zero), not just which.

const (
	// RuleRequired: the field must be non-zero for this event.
	RuleRequired Rule = "must be set"
	// RuleMustBeZero: the field must be zero for this event's scope.
	RuleMustBeZero Rule = "must be zero"
	// RuleUnknownType: the event's concrete type is not in the sealed union, so it
	// fails fail-secure (the journal/restore caller must reject it rather than guess
	// an identity contract for it).
	RuleUnknownType Rule = "is not a known event type"
	// RuleInvalid: the field contains a value outside its closed domain.
	RuleInvalid Rule = "is invalid"
)

type Scope

type Scope uint8

Scope is whether an event is session-global or produced by one loop.

const (
	ScopeSession Scope = iota
	ScopeLoop
)

type SessionActive

type SessionActive struct {
	Header
	// contains filtered or unexported fields
}

SessionActive marks the Idle -> Active edge of the session quiescence model.

func (SessionActive) Class

func (SessionActive) Class() Class

func (SessionActive) EndsTurn

func (SessionActive) EndsTurn() bool

func (SessionActive) Scope

func (SessionActive) Scope() Scope

type SessionIdle

type SessionIdle struct {
	Header
	// contains filtered or unexported fields
}

SessionIdle marks the Active -> Idle edge of the session quiescence model.

func (SessionIdle) Class

func (SessionIdle) Class() Class

func (SessionIdle) EndsTurn

func (SessionIdle) EndsTurn() bool

func (SessionIdle) Scope

func (SessionIdle) Scope() Scope

type SessionStarted

type SessionStarted struct {
	Header
	Config   ConfigFingerprint `json:"config,omitzero"`
	Manifest ConfigManifest    `json:"manifest,omitzero"`
	// contains filtered or unexported fields
}

SessionStarted is published when the session's primary loop actor starts. Header.SessionID is set; LoopID/TurnID/StepID are zero. Config is the fingerprint of the agent configuration the session started under (model/system-prompt/tool policy), stamped at construction so a durable journal can detect a config change on restore.

Manifest is the richer, canonical description of the same configuration. During the deprecation window BOTH are populated: Config remains the legacy comparison baseline while Manifest carries the additive fields (workspace trust, permission and confinement strictness, application fields, and per-tool schema identity) that typed drift assessment consumes. It is additive (omitzero): a journal record that predates the field decodes it as the zero ConfigManifest and never spuriously mismatches.

func (SessionStarted) Class

func (SessionStarted) Class() Class

func (SessionStarted) EndsTurn

func (SessionStarted) EndsTurn() bool

func (SessionStarted) Scope

func (SessionStarted) Scope() Scope

type SessionStopped

type SessionStopped struct {
	Header
	// contains filtered or unexported fields
}

SessionStopped marks the session phase transition on Shutdown.

func (SessionStopped) Class

func (SessionStopped) Class() Class

func (SessionStopped) EndsTurn

func (SessionStopped) EndsTurn() bool

func (SessionStopped) Scope

func (SessionStopped) Scope() Scope

type SnapshotConsistency

type SnapshotConsistency uint8

SnapshotConsistency describes whether harness-managed workspace mutations could overlap a snapshot walk. Unknown exists only to decode legacy checkpoint events.

const (
	SnapshotConsistencyUnknown SnapshotConsistency = iota
	SnapshotQuiescent
	SnapshotFuzzy
)

type SnapshotTriggerKind

type SnapshotTriggerKind uint8

SnapshotTriggerKind records the policy boundary that requested a snapshot. Unknown exists only to decode legacy checkpoint events.

const (
	SnapshotTriggerKindUnknown SnapshotTriggerKind = iota
	SnapshotTriggerManual
	SnapshotTriggerIdle
	SnapshotTriggerInterrupt
	SnapshotTriggerTurnDone
	SnapshotTriggerStepDone
	SnapshotTriggerSeed
)

type StepDone

type StepDone struct {
	Header
	Messages content.AgenticMessages `json:"messages,omitempty"`
	// contains filtered or unexported fields
}

StepDone is the enduring event emitted when a step's finalized group is committed: the step's single AIMessage followed by its ToolResultMessages. It is emitted at the actor-owned commit point, once the commit handshake lands, so it never claims a commit that did not happen — Messages is exactly what entered history.

It is emitted for a TRUNCATED step too. When a stream fails after the model has already delivered content, the loop commits the safe prefix of that response (text and sealed reasoning; never a partial or unpaired tool call) so the content the user watched arrive is not silently discarded. Such a group is a lone AIMessage whose last block is the truncation notice, and the turn still ends on TurnFailed. A consumer that needs to distinguish the two reads the turn terminal; a consumer that only renders history sees the notice. A step that decoded nothing usable commits nothing and emits no StepDone at all.

func (StepDone) Class

func (StepDone) Class() Class

func (StepDone) EndsTurn

func (StepDone) EndsTurn() bool

func (StepDone) Scope

func (StepDone) Scope() Scope

type StrictnessLevel

type StrictnessLevel uint8

StrictnessLevel is an ordered security posture supplied by the composition root: higher is stricter. Zero means "unknown" — the posture exists only as an opaque digest, so a change cannot be direction-classified and drift assessment fails secure (Warn). Harness compares levels; it never computes them.

type Subscription

type Subscription interface {
	Events() <-chan Delivery
	Close() error
	Err() error
}

Subscription is the consumer-facing handle to a session event fan-in: the read+teardown contract a TUI/CLI (or a future journal) depends on, independent of the concrete subscription implementation. It lives here in the (leaf) event package — the one package every event producer and consumer already imports — so neither side has to depend on the concrete hub type to name the contract (Dependency Inversion). The session hub's *EventSubscription satisfies it structurally.

Events yields the filtered fan-in stream and closes on Close or on a hub-forced loss; Close is the consumer's intentional, idempotent teardown; Err reports the typed termination cause (nil for an intentional Close, the loss error for a hub-forced drop).

type TokenDelta

type TokenDelta struct {
	Header
	TurnIndex TurnIndex `json:"turn_index,omitzero"`
	// Chunk is content.Chunk, a sealed interface that is never serialized (chunks
	// have no wire codec — see content.Chunk); TokenDelta is an Ephemeral streaming
	// delta that never reaches the journal, so the field is tagged json:"-".
	Chunk content.Chunk `json:"-"`
	// contains filtered or unexported fields
}

TokenDelta is emitted for each streaming chunk from the LLM. TokenDelta and the ToolCallStarted/ToolCallCompleted events (in tool.go) are the Ephemeral events.

func (TokenDelta) Class

func (TokenDelta) Class() Class

func (TokenDelta) EndsTurn

func (TokenDelta) EndsTurn() bool

func (TokenDelta) Scope

func (TokenDelta) Scope() Scope

type ToolCallCompleted

type ToolCallCompleted struct {
	Header
	ToolExecutionID uuid.UUID `json:"tool_execution_id,omitzero"`
	IsError         bool      `json:"is_error,omitzero"`
	ResultPreview   string    `json:"result_preview,omitempty"`
	// contains filtered or unexported fields
}

ToolCallCompleted is emitted when a tool finishes. ResultPreview is the capped tool output for the TUI.

func (ToolCallCompleted) Class

func (ToolCallCompleted) Class() Class

func (ToolCallCompleted) EndsTurn

func (ToolCallCompleted) EndsTurn() bool

func (ToolCallCompleted) Scope

func (ToolCallCompleted) Scope() Scope

type ToolCallStarted

type ToolCallStarted struct {
	Header
	ToolExecutionID uuid.UUID `json:"tool_execution_id,omitzero"`
	ToolName        string    `json:"tool_name,omitempty"`
	Summary         string    `json:"summary,omitempty"`
	// contains filtered or unexported fields
}

ToolCallStarted is emitted when an approved tool begins executing. Summary is capped at construction (never raw args).

func (ToolCallStarted) Class

func (ToolCallStarted) Class() Class

func (ToolCallStarted) EndsTurn

func (ToolCallStarted) EndsTurn() bool

func (ToolCallStarted) Scope

func (ToolCallStarted) Scope() Scope

type ToolLimitError

type ToolLimitError struct {
	Iterations    int
	MaxIterations int
	Calls         int
	MaxCalls      int
}

ToolLimitError is the TurnFailed.Err cause when the agentic loop's runaway guard fires: the model requested another tool batch after either the per-turn iteration cap (LLM<->tool round-trips) or the total-call cap was exceeded. It is typed and secret-free (it carries only the counts), so it is safe to surface un-redacted in TurnFailed.Err — it never embeds raw messages or tool arguments. Callers may errors.As it to distinguish a runaway stop from a provider/network failure.

func (*ToolLimitError) Error

func (e *ToolLimitError) Error() string

type ToolManifestEntry

type ToolManifestEntry struct {
	Name            string `json:"name"`
	InputSchemaRev  string `json:"input_schema_rev,omitzero"`
	OutputSchemaRev string `json:"output_schema_rev,omitzero"`
}

ToolManifestEntry is one model-facing tool's stable identity: its name plus content digests of its input and output schemas. Digests, never schemas — the manifest carries identity, not definitions.

type TurnDone

type TurnDone struct {
	Header
	TurnIndex TurnIndex `json:"turn_index,omitzero"`
	// Message is the complete AI response.
	Message *content.AIMessage `json:"message,omitzero"`
	// Usage is the checked sum of every completed request in this turn. Loop
	// cumulative accounting folds StepDone only, so this projection is never
	// added a second time.
	Usage content.Usage `json:"usage,omitzero"`
	// contains filtered or unexported fields
}

TurnDone is the terminal success event for a turn.

func (TurnDone) Class

func (TurnDone) Class() Class

func (TurnDone) EndsTurn

func (TurnDone) EndsTurn() bool

func (TurnDone) Scope

func (TurnDone) Scope() Scope

type TurnFailed

type TurnFailed struct {
	Header
	TurnIndex TurnIndex `json:"turn_index,omitzero"`
	// Err is the typed cause; an error value cannot round-trip through
	// encoding/json (no codec, no stable shape), so it is tagged json:"-" to keep
	// the journal output clean rather than emitting garbage. Callers read it
	// in-memory via errors.As; a journal records the failure via the event itself.
	Err error `json:"-"`
	// contains filtered or unexported fields
}

TurnFailed is the terminal event for non-cancellation LLM/provider errors. Err carries the typed cause; callers may errors.As it to inspect and retry.

func (TurnFailed) Class

func (TurnFailed) Class() Class

func (TurnFailed) EndsTurn

func (TurnFailed) EndsTurn() bool

func (TurnFailed) Scope

func (TurnFailed) Scope() Scope

type TurnFoldedInto

type TurnFoldedInto struct {
	Header
	TurnIndex TurnIndex            `json:"turn_index,omitzero"`
	Message   *content.UserMessage `json:"message,omitzero"`
	// contains filtered or unexported fields
}

TurnFoldedInto is emitted when queued input folds into a mandatory tool-continuation request. Header.Cause.CommandID is the submit command id; Header.Cause.LoopID is set for a SubagentResult hand-back. Message is the folded user message.

func (TurnFoldedInto) Class

func (TurnFoldedInto) Class() Class

func (TurnFoldedInto) EndsTurn

func (TurnFoldedInto) EndsTurn() bool

func (TurnFoldedInto) Scope

func (TurnFoldedInto) Scope() Scope

type TurnIndex

type TurnIndex int

TurnIndex identifies a turn within one loop. Each loop numbers its own turns from 0; it is not unique across loops in a multi-loop session.

type TurnInterrupted

type TurnInterrupted struct {
	Header
	TurnIndex TurnIndex `json:"turn_index,omitzero"`
	// contains filtered or unexported fields
}

TurnInterrupted is the terminal event when the turn context is cancelled.

func (TurnInterrupted) Class

func (TurnInterrupted) Class() Class

func (TurnInterrupted) EndsTurn

func (TurnInterrupted) EndsTurn() bool

func (TurnInterrupted) Scope

func (TurnInterrupted) Scope() Scope

type TurnPanicError

type TurnPanicError struct{ Detail string }

TurnPanicError is the TurnFailed.Err cause when the turn goroutine panics. Detail is the recovered value rendered as a string.

func (*TurnPanicError) Error

func (e *TurnPanicError) Error() string

type TurnRejected

type TurnRejected struct {
	Header
	Reason RejectReason `json:"reason,omitzero"`
	// contains filtered or unexported fields
}

TurnRejected is the Enduring Reply event for a UserInput the loop refused (queue-full, shutting-down, or a transient internal failure). Enduring: a rejected user message must never silently vanish. Header.Cause.CommandID is the submit command id.

func (TurnRejected) Class

func (TurnRejected) Class() Class

func (TurnRejected) EndsTurn

func (TurnRejected) EndsTurn() bool

func (TurnRejected) Scope

func (TurnRejected) Scope() Scope

type TurnStarted

type TurnStarted struct {
	Header
	TurnIndex TurnIndex            `json:"turn_index,omitzero"`
	Message   *content.UserMessage `json:"message,omitzero"`
	// contains filtered or unexported fields
}

TurnStarted is emitted when runLoop commits a turn's initial UserMessage. It is the first enduring turn event. Header.Cause.CommandID is the submit command id. Message is the exact UserMessage committed as the first message of the turn.

func (TurnStarted) Class

func (TurnStarted) Class() Class

func (TurnStarted) EndsTurn

func (TurnStarted) EndsTurn() bool

func (TurnStarted) Scope

func (TurnStarted) Scope() Scope

type UnknownEventTypeError

type UnknownEventTypeError struct{ Type string }

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

func (*UnknownEventTypeError) Error

func (e *UnknownEventTypeError) Error() string

type UnknownMessageRoleError

type UnknownMessageRoleError struct{ Role string }

UnknownMessageRoleError is returned by the message-slice decoder when a message's "role" names no concrete Conversation type (including an empty/missing role). The Conversation union is sealed and discriminated by role; an unknown role fails closed rather than guess a concrete message to reconstruct.

func (*UnknownMessageRoleError) Error

func (e *UnknownMessageRoleError) Error() string

type UnsupportedSchemaError

type UnsupportedSchemaError struct {
	Kind    string // "event"
	Version uint32
}

UnsupportedSchemaError reports a durable record whose schema version is newer than this binary supports. It is the dispatch hook a future migration layer catches; today it is a hard, typed restore failure.

func (*UnsupportedSchemaError) Error

func (e *UnsupportedSchemaError) Error() string

type UserInputRequested

type UserInputRequested struct {
	Header
	ToolExecutionID uuid.UUID `json:"tool_execution_id,omitzero"`
	Question        string    `json:"question,omitempty"`
	Choices         []string  `json:"choices,omitempty"`
	// contains filtered or unexported fields
}

UserInputRequested is emitted when a tool (AskUser) needs free-form input. The per-turn stream gets the full Question and Choices for rendering.

func (UserInputRequested) Class

func (UserInputRequested) Class() Class

func (UserInputRequested) EndsTurn

func (UserInputRequested) EndsTurn() bool

func (UserInputRequested) Scope

func (UserInputRequested) Scope() Scope

type WorkflowActivity

type WorkflowActivity struct {
	Header

	RunID             uuid.UUID            `json:"run_id"`
	WorkflowName      string               `json:"workflow_name"`
	WorkflowVersion   string               `json:"workflow_version"`
	Kind              WorkflowActivityKind `json:"kind"`
	Status            WorkflowRunStatus    `json:"status"`
	VertexID          uuid.UUID            `json:"vertex_id,omitzero"`
	VertexLabel       string               `json:"vertex_label,omitempty"`
	CompletedVertices uint32               `json:"completed_vertices,omitzero"`
	TotalVertices     uint32               `json:"total_vertices,omitzero"`
	Message           string               `json:"message,omitempty"`
	OccurredAt        time.Time            `json:"occurred_at"`
	// contains filtered or unexported fields
}

WorkflowActivity is the public, durable session notification for one safe workflow transition. It carries identifiers and bounded display metadata only: no checkpoint state, policy text, model output, document fragment, or metadata map belongs in this event.

The EventID is normally a deterministic source activity ID supplied through Factory.StampWorkflowActivity. That specialized path is the only factory path that preserves an externally derived identity; generic Factory.Stamp continues to mint fresh IDs for ordinary event construction.

func (WorkflowActivity) Class

func (WorkflowActivity) Class() Class

func (WorkflowActivity) EndsTurn

func (WorkflowActivity) EndsTurn() bool

func (WorkflowActivity) Scope

func (WorkflowActivity) Scope() Scope

type WorkflowActivityKind

type WorkflowActivityKind string

WorkflowActivityKind is the closed lifecycle vocabulary projected from a workflow run. It deliberately describes safe user-facing milestones rather than exposing Flow's internal checkpoint or state shape.

func (WorkflowActivityKind) Valid

func (k WorkflowActivityKind) Valid() bool

Valid reports whether k is one of the durable activity kinds.

type WorkflowRunStatus

type WorkflowRunStatus string

WorkflowRunStatus is the bounded status projection carried by an activity. The failed value is included for workflow-host failures that occur after a durable run exists; Flow itself may represent recoverable execution errors as an interrupted run.

const (
	WorkflowRunStatusRunning     WorkflowRunStatus = "running"
	WorkflowRunStatusInterrupted WorkflowRunStatus = "interrupted"
	WorkflowRunStatusCompleted   WorkflowRunStatus = "completed"
	WorkflowRunStatusCancelled   WorkflowRunStatus = "cancelled"
	WorkflowRunStatusFailed      WorkflowRunStatus = "failed"
)

func (WorkflowRunStatus) Valid

func (s WorkflowRunStatus) Valid() bool

Valid reports whether s is one of the durable workflow statuses.

type WorkspaceCheckpointed

type WorkspaceCheckpointed struct {
	Header
	Ref         string              `json:"ref"`
	Consistency SnapshotConsistency `json:"consistency"`
	Trigger     SnapshotTriggerKind `json:"trigger"`
	// contains filtered or unexported fields
}

WorkspaceCheckpointed records that the session's workspace was durably snapshotted as Ref at this point in the event order. It is session-scoped and Enduring — the resume token's pointer to the workspace store; Header.SessionID is set, LoopID/TurnID/StepID are zero. Ref is an opaque "v1:sha256:<hex>" string (typed as workspacestore.Ref at the producer; pkg/event stays dependency-light).

func (WorkspaceCheckpointed) Class

func (WorkspaceCheckpointed) Class() Class

func (WorkspaceCheckpointed) EndsTurn

func (WorkspaceCheckpointed) EndsTurn() bool

func (WorkspaceCheckpointed) Scope

func (WorkspaceCheckpointed) Scope() Scope

type WorkspaceRestored

type WorkspaceRestored struct {
	Header
	Ref string `json:"ref"`
	// contains filtered or unexported fields
}

WorkspaceRestored records that the live workspace was replaced from Ref and that Ref is now the effective durable restore point.

func (WorkspaceRestored) Class

func (WorkspaceRestored) Class() Class

func (WorkspaceRestored) EndsTurn

func (WorkspaceRestored) EndsTurn() bool

func (WorkspaceRestored) Scope

func (WorkspaceRestored) Scope() Scope

Jump to

Keyboard shortcuts

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