hooks

package
v0.0.2 Latest Latest
Warning

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

Go to latest
Published: Sep 9, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package hooks defines in-process extension points for the turn loop. Today it covers tool gating (approve/deny) and tool lifecycle observation; the interfaces are deliberately transport-agnostic so an external implementation (e.g. an Aether-ACL-driven approver, or an OTel observer) can satisfy the same contracts later without changing the turn loop.

Index

Constants

View Source
const (
	DecisionAllow = "allow"
	DecisionDeny  = "deny"
)

Variables

View Source
var (
	ErrInvalidHookCommand  = errors.New("hooks: invalid command hook")
	ErrHookTimeout         = errors.New("hooks: command hook timed out")
	ErrMalformedHookOutput = errors.New("hooks: malformed hook JSON output")
)

Functions

func ExecutionBranchIDFrom added in v0.0.2

func ExecutionBranchIDFrom(ctx context.Context) (string, bool)

ExecutionBranchIDFrom returns the turn branch carried on ctx.

func ExecutionLedgerDisabled added in v0.0.2

func ExecutionLedgerDisabled(ctx context.Context) bool

func NotifyTurn

func NotifyTurn(ctx context.Context, ev TurnEvent, observers []TurnObserver)

NotifyTurn fans an event out to every observer, best-effort (nil-safe).

func WithExecutionBranchID added in v0.0.2

func WithExecutionBranchID(ctx context.Context, branchID string) context.Context

WithExecutionBranchID installs the stable branch selected for one turn.

func WithRecursionGuard

func WithRecursionGuard(ctx context.Context) context.Context

func WithoutExecutionLedger added in v0.0.2

func WithoutExecutionLedger(ctx context.Context) context.Context

WithoutExecutionLedger marks an ephemeral turn whose observer signals must not be written to durable operational history.

Types

type AllowList

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

AllowList is a ToolApprover permitting only the named tools. An empty list permits everything (it is treated as "no restriction"), which is how an absent command allowed-tools frontmatter behaves.

func NewAllowList

func NewAllowList(names []string, reason string) *AllowList

NewAllowList builds an AllowList from tool names. The reason is surfaced to the model on denial.

func (*AllowList) ApproveTool

func (a *AllowList) ApproveTool(_ context.Context, call ToolCall) Decision

ApproveTool allows the call when the list is empty (no restriction) or the tool is listed.

type CWDEvent

type CWDEvent struct {
	OldCWD string `json:"old_cwd"`
	NewCWD string `json:"new_cwd"`
}

type CWDWatcher

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

func NewCWDWatcher

func NewCWDWatcher() (*CWDWatcher, error)

func (*CWDWatcher) Poll

func (w *CWDWatcher) Poll(ctx context.Context) (CWDEvent, bool, error)

type CommandHook

type CommandHook struct {
	Name    string
	Event   EventName
	Command []string
	Timeout time.Duration
	Env     map[string]string
}

type Config

type Config struct {
	DefaultTimeout time.Duration
	EnvAllowlist   []string
}

type Decision

type Decision struct {
	Allow  bool
	Reason string // human-readable; surfaced to the model when denied
}

Decision is an approver's verdict on a tool call.

func Allow

func Allow() Decision

Allow returns an allowing decision.

func Approve

func Approve(ctx context.Context, call ToolCall, approvers []ToolApprover) Decision

Approve runs approvers in order and returns the first denial, or Allow when none object. nil/empty approver slices allow everything.

func Deny

func Deny(reason string) Decision

Deny returns a denying decision with a reason.

type EventName

type EventName string
const (
	EventPreToolUse         EventName = "PreToolUse"
	EventPostToolUse        EventName = "PostToolUse"
	EventPostToolUseFailure EventName = "PostToolUseFailure"
	EventCommand            EventName = "Command"
	EventUserPromptSubmit   EventName = "UserPromptSubmit"
	EventSessionStart       EventName = "SessionStart"
	EventSessionEnd         EventName = "SessionEnd"
	EventTaskCreated        EventName = "TaskCreated"
	EventTaskCompleted      EventName = "TaskCompleted"
	EventPreCompact         EventName = "PreCompact"
	EventPostCompact        EventName = "PostCompact"
	EventCWDChanged         EventName = "CwdChanged"
	EventFileChanged        EventName = "FileChanged"
	EventToolLifecycle      EventName = "ToolLifecycle"
)

type Executor

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

func NewExecutor

func NewExecutor(cfg Config) *Executor

func (*Executor) Dispatch

func (e *Executor) Dispatch(ctx context.Context, inv Invocation, hooks []CommandHook) ([]Result, error)

type FileChangeKind

type FileChangeKind string
const (
	FileChangeAdd    FileChangeKind = "add"
	FileChangeChange FileChangeKind = "change"
	FileChangeUnlink FileChangeKind = "unlink"
)

type FileEvent

type FileEvent struct {
	Path string         `json:"file_path"`
	Kind FileChangeKind `json:"event"`
}

type HookOutput

type HookOutput struct {
	Decision   string            `json:"decision,omitempty"`
	Reason     string            `json:"reason,omitempty"`
	Message    string            `json:"message,omitempty"`
	Env        map[string]string `json:"env,omitempty"`
	WatchPaths []string          `json:"watch_paths,omitempty"`
}

type Invocation

type Invocation struct {
	Event EventName       `json:"event"`
	Input json.RawMessage `json:"input,omitempty"`
	Tool  *ToolCall       `json:"tool,omitempty"`
}

type PollingWatcher

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

func NewPollingWatcher

func NewPollingWatcher(paths []string) (*PollingWatcher, error)

func (*PollingWatcher) Poll

func (w *PollingWatcher) Poll(ctx context.Context) ([]FileEvent, error)

type Result

type Result struct {
	HookName string
	Event    EventName
	ExitCode int
	Stdout   string
	Stderr   string
	Output   HookOutput
	Blocked  bool
	Reason   string
	Skipped  bool
}

type RetryStrategy added in v0.0.2

type RetryStrategy string

RetryStrategy names what the runner is about to do about a failed provider call. It is the discriminator a renderer needs to word the notice: a model switch is a different user-facing story from waiting out a rate limit.

const (
	// RetryTrimContext drops the oldest history and retries the same model.
	RetryTrimContext RetryStrategy = "trim_context"
	// RetryBackoff waits RetryIn and retries the same model.
	RetryBackoff RetryStrategy = "backoff"
	// RetryFallbackModel switches to a different model; RetryIn is zero.
	RetryFallbackModel RetryStrategy = "fallback_model"
)

type Runtime

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

func NewRuntime

func NewRuntime(cfg Config, hooks []CommandHook) *Runtime

func (*Runtime) ApproveTool

func (r *Runtime) ApproveTool(ctx context.Context, call ToolCall) Decision

func (*Runtime) Dispatch

func (r *Runtime) Dispatch(ctx context.Context, inv Invocation) ([]Result, error)

func (*Runtime) EmitToolEvent

func (r *Runtime) EmitToolEvent(ctx context.Context, event tools.ToolEvent) error

func (*Runtime) ObserveTurn

func (r *Runtime) ObserveTurn(ctx context.Context, ev TurnEvent)

ObserveTurn makes a command-hook Runtime a TurnObserver: it bridges the turn phases that HAVE a command-hook event to that event's external hooks. Phases with no corresponding event (the model-call boundaries) are dropped — the in-process TurnObserver seam still delivers them. This is what "populates" the previously-unwired UserPromptSubmit + PostCompact command-hook events.

func (*Runtime) ToolFinished

func (r *Runtime) ToolFinished(ctx context.Context, call ToolCall, isError bool, err error)

func (*Runtime) ToolStarted

func (r *Runtime) ToolStarted(ctx context.Context, call ToolCall)

type ToolApprover

type ToolApprover interface {
	ApproveTool(ctx context.Context, call ToolCall) Decision
}

ToolApprover decides whether a tool call may execute. The first denial wins. External approvers (e.g. Aether-ACL-driven) implement this same interface.

type ToolCall

type ToolCall struct {
	CallID string
	Name   string
	Args   json.RawMessage
	Addr   protocol.MessageAddress
}

ToolCall is the subset of a tool invocation a hook inspects.

type ToolObserver

type ToolObserver interface {
	ToolStarted(ctx context.Context, call ToolCall)
	ToolFinished(ctx context.Context, call ToolCall, isError bool, err error)
}

ToolObserver observes the tool lifecycle without a veto. Audit/telemetry (e.g. OTel spans) implement this.

type ToolResultObserver added in v0.0.2

type ToolResultObserver interface {
	ToolResult(ctx context.Context, call ToolCall, result tools.Result, err error)
}

ToolResultObserver is an optional richer companion to ToolObserver. The turn loop calls it only after an admitted tool actually ran, allowing evidence and provenance consumers to inspect bounded result metadata without changing the existing lifecycle interface or receiving tool payload content.

type TurnEvent

type TurnEvent struct {
	Phase     TurnPhase
	Addr      protocol.MessageAddress
	Iteration int    // tool-loop round: 0 before the first model call
	Model     string // model in use for this step, when known
	Err       error  // set on model_call_finished / turn_finished on error
	MessageID string // stable transcript message reference, when known
	// OperationID distinguishes repeated reference/recovery events within one
	// branch. Reference points at an authority-owned record without copying it.
	OperationID string
	Reference   *tools.ResultReference

	// Attempt is the 1-based ordinal of the retry about to be made, within its
	// own strategy's budget. Set on PhaseProviderRetry.
	Attempt int
	// RetryIn is the wait the runner will observe before retrying. Zero for
	// strategies that retry immediately. Set on PhaseProviderRetry.
	RetryIn time.Duration
	// Strategy is what the runner is about to do about the failure. Set on
	// PhaseProviderRetry.
	Strategy RetryStrategy
	// FailureKind is the classified provider failure ("rate_limit",
	// "context_overflow", …), empty when the error was not a classified
	// ProviderError. Set on PhaseProviderRetry.
	FailureKind string
	// NextModel is the model the runner is switching to. Set only for
	// RetryFallbackModel.
	NextModel string
}

TurnEvent is a turn-lifecycle signal delivered to TurnObservers. It carries enough to CORRELATE and snapshot (addr, iteration, model); the message content itself lives in the history store (persisted per step via SaveHistory). Err is set on the *Finished phases when the step/turn errored.

type TurnObserver

type TurnObserver interface {
	ObserveTurn(ctx context.Context, ev TurnEvent)
}

TurnObserver observes the turn lifecycle without a veto — audit, telemetry, checkpoint/sync. Best-effort: it must not block the turn, and a nil observer or empty slice is a no-op. Peer to ToolObserver (which covers the tool sub-lifecycle). A single method keyed by Phase keeps it cheap to implement (a consumer switches on the phases it cares about) and lets new phases be added without breaking implementers — the same reason the messaging spec discriminates stream events by a "phase"/"event" field.

type TurnPhase

type TurnPhase string

TurnPhase identifies a point in a turn's lifecycle. It is the discriminator for TurnEvent, mirroring how EventName discriminates command-hook invocations.

const (
	PhaseTurnStarted        TurnPhase = "turn_started"
	PhaseUserPromptSubmit   TurnPhase = "user_prompt_submit"
	PhaseModelCallStarted   TurnPhase = "model_call_started"
	PhaseModelCallFinished  TurnPhase = "model_call_finished"
	PhaseCompacted          TurnPhase = "compacted"
	PhaseResourceReferenced TurnPhase = "resource_referenced"
	PhaseRecoveryStarted    TurnPhase = "recovery_started"
	// PhaseProviderRetry fires when a provider call failed and the runner has
	// decided to try again — before the wait, so an observer can show the user
	// what is happening while it happens. Distinct from PhaseRecoveryStarted,
	// which is about resuming a turn after a process restart.
	PhaseProviderRetry TurnPhase = "provider_retry"
	PhaseTurnFinished  TurnPhase = "turn_finished"
)

Jump to

Keyboard shortcuts

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