Skip to documentation
Documentation navigation

Documentation navigation

Documentation / guides

Hook operations

Choose the exact runtime operations a hook observes.

developer

Operation domain

hook.Operation is a uint8 with these exact constants:

ConstantValueGuardableMeaning
OperationTurn1yesOne loop turn.
OperationStep2noOne turn-local inference/tool step.
OperationInference3yesProvider inference boundary.
OperationCompaction4yesOne transcript-compaction attempt.
OperationToolCall5yesSemantic model tool call, including permission resolution.
OperationGateWait6noTime blocked waiting for a gate answer.
OperationToolExecution7noApproved tool execution only.
OperationJournalAppend8noOne bounded durable record append.

Operation.Valid() recognizes only values 1 through 8. Operation.Guardable() is the capability check used by ValidateSet; an Around observer may target any valid operation, while a Guard may target only the four yes rows.

Call shape

hook.Call is the immutable start snapshot:

FieldTypeMeaning
Operationhook.OperationSelects the operation and the one legal payload pointer.
StartedAttime.TimeRuntime-owned start time.
Coordinatesidentity.CoordinatesSession, loop, turn, and step location.
AgentNameidentity.AgentNameLoop attribution.
Causeidentity.CauseDirect causal edge that started this operation.
Turn, Step, Inference, Compaction, ToolCall, GateWait, ToolExecution, JournalAppendpointersExactly one is non-nil and must match Operation.

ValidateCall returns *hook.CallError with CallUnknownOperation or CallInvalidPayload. The runner clones the call before passing it to each callback. It also clones the Call embedded in hook.Result; Result.Err is the trusted in-process error and is intentionally not deep-cloned.

Payloads

The operation payloads expose these exact fields:

PayloadFields
TurnDataIndex event.TurnIndex, Input *content.UserMessage
StepDataIndex hook.StepIndex
InferenceDataRequest *inference.Request, AIMessage *content.AIMessage, StreamResult *stream.StreamResult
CompactionDataAttemptID event.CompactAttemptID, Input *loop.CompactionInput, Output *loop.CompactionOutput
ToolCallDataToolExecutionID uuid.UUID, ToolUseID string, ToolName string, Summary string, ArgsJSON json.RawMessage, PermissionEffect event.PermissionDecisionEffect, PermissionReason string, Result *tool.ToolResult, ResultPreview string, IsError bool
GateWaitDataGateID gate.ID, Kind gate.Kind, Resolver gate.ResolverKind, Blocks gate.Blocks, Effect gate.Effect, Answer *gate.Answer
ToolExecutionDataToolExecutionID uuid.UUID, ToolUseID string, ToolName string, ArgsJSON json.RawMessage, Result *tool.ToolResult, ResultPreview string, IsError bool
JournalAppendDataFamily hook.RecordFamily, RecordID string

RecordFamily is closed: RecordEvent, RecordCommand, RecordGatePrepared, and RecordFence. The journal hook sees the family and bounded record identity, not serialized bytes. The runtime uses JournalAppend to attach durable append work to the active operation’s causal context.

Outcomes

hook.Outcome is closed and starts at one: OutcomeCompleted, OutcomeDenied, OutcomeFailed, and OutcomeCanceled. The runtime supplies a valid outcome in hook.Result.Outcome; OutcomeDenied is the terminal value for a guard refusal. A finish callback must treat Result.Err as trusted process-local data and redact or classify it before crossing a logging, telemetry, or network boundary.

This is a compile-realistic observer that records only bounded fields:

package main

import (
	"context"
	"time"

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

func turnObserver() hook.Around {
	return hook.Around{
		Operation: hook.OperationTurn,
		Begin: func(ctx context.Context, call hook.Call) (context.Context, hook.FinishFunc) {
			started := call.StartedAt
			return ctx, func(result hook.Result) {
				_ = started
				_ = result.Outcome.Valid()
				_ = time.Since(result.EndedAt)
			}
		},
	}
}

See the public definitions in pkg/hook/hook.go and pkg/hook/data.go, with operation nesting exercised by pkg/rig/hooks_integration_test.go.

Source and proof

← back to documentation