watchdog

package
v2.7.0-dev.5 Latest Latest
Warning

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

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

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Alert

type Alert struct {
	Signal   string
	Severity Severity
	Reason   string
}

Alert is what a triggered signal returns. Fields are operator- facing — the agent's wiring logs Reason verbatim. Signal is the stable string ID the rest of the system can dispatch on (future "auto" mode picks behavior per signal).

func (Alert) String

func (a Alert) String() string

String implements fmt.Stringer for Alert so log lines stay uniform. Format: "[severity] signal: reason".

type DefaultWatchdog

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

DefaultWatchdog is the package-default implementation. Fans observations across the configured signals; Check collects alerts from each.

func NewDefaultWatchdog

func NewDefaultWatchdog() *DefaultWatchdog

NewDefaultWatchdog returns a DefaultWatchdog wired with the default v1 signal set:

  • RepeatedToolCall (threshold 5): N consecutive identical (name, args) tool calls.

Operators wanting different thresholds construct DefaultWatchdog directly with a custom signal list.

func (*DefaultWatchdog) Check

func (w *DefaultWatchdog) Check() []Alert

Check returns any alerts that accumulated since the last Check and resets the buffer. Returns nil (not an empty slice) when no alerts are pending — lets the caller skip work cheaply.

func (*DefaultWatchdog) ObserveToolCall

func (w *DefaultWatchdog) ObserveToolCall(tc ToolCall)

ObserveToolCall fans the observation across every wired signal.

func (*DefaultWatchdog) Reset

func (w *DefaultWatchdog) Reset()

Reset clears alerts + every signal's state. Called on logical session boundaries (e.g. operator-initiated /clear).

type RepeatedToolCallSignal

type RepeatedToolCallSignal struct {
	Threshold int
	// contains filtered or unexported fields
}

RepeatedToolCallSignal trips when the same (name, args) tool call appears Threshold times consecutively. Catches the read_file loop pattern from issue #144 and similar runaway-tool-call shapes.

"Consecutive" is the key word: a → b → a → b → a doesn't trip (no run of identical calls), but a → a → a → a → a does. This matches operator intuition ("the agent is stuck on the same thing") without flagging legitimate patterns like alternating-tool exploration loops.

Caveat from #144: args comparison is literal-string. Tool calls with semantically-equivalent but textually-different args (e.g. "main.go" vs "/workspace/main.go") won't be detected as repeats. Tool-specific canonicalization is a future enhancement; v1 pairs with #147's inbox framing fix to reduce the probability of that subcase reaching the watchdog at all.

func NewRepeatedToolCallSignal

func NewRepeatedToolCallSignal(threshold int) *RepeatedToolCallSignal

NewRepeatedToolCallSignal constructs a signal with the given threshold. Threshold must be ≥ 2 (a "repeated call" requires at least two of the same in a row); values < 2 are clamped to 2 to avoid the degenerate case where every tool call trips the signal.

func (*RepeatedToolCallSignal) Name

func (s *RepeatedToolCallSignal) Name() string

Name implements Signal.

func (*RepeatedToolCallSignal) ObserveToolCall

func (s *RepeatedToolCallSignal) ObserveToolCall(tc ToolCall) *Alert

ObserveToolCall implements Signal. Tracks the running count of consecutive identical calls; emits an alert when count reaches Threshold. Returns nil on subsequent observations within the same run (already-tripped guard) so we don't re-emit on every extra call — operators want one notice per stuck pattern, not one per tool call past the threshold.

func (*RepeatedToolCallSignal) Reset

func (s *RepeatedToolCallSignal) Reset()

Reset implements Signal.

type Severity

type Severity string

Severity classifies the urgency of an alert. Warn is the operator- visible-but-not-action-blocking level v1 emits; the others are reserved for future modes that pause or escalate automatically.

const (
	SeverityWarn     Severity = "warn"
	SeverityCritical Severity = "critical"
)

type Signal

type Signal interface {
	// Name returns the stable signal ID used in Alert.Signal.
	Name() string

	// ObserveToolCall updates the signal's internal state with one
	// tool invocation. Returning a non-nil Alert means the signal
	// tripped on this observation; DefaultWatchdog appends it to
	// the pending-alerts buffer.
	ObserveToolCall(ToolCall) *Alert

	// Reset clears the signal's state. Called from
	// DefaultWatchdog.Reset.
	Reset()
}

Signal is the per-detector interface inside DefaultWatchdog. Each signal owns its own state and decides when to emit an alert. Implementations must be safe to call serially from DefaultWatchdog (which holds a mutex across observations); they do NOT need to be concurrency-safe themselves.

Adding a new signal: implement Signal, append to NewDefaultWatchdog's signal list (or to a constructor variant). No changes to DefaultWatchdog itself.

type ToolCall

type ToolCall struct {
	Name string
	Args string
}

ToolCall is the per-tool-call observation the watchdog needs. Name is the canonical tool name (e.g. "read_file", "mcp.gke.list_clusters"). Args is the JSON-serialized argument blob — compared as a literal string by the v1 repeated-tool-call detector, which means semantically-equivalent calls with different arg formatting (e.g. relative vs absolute file paths) are treated as distinct. Tool-specific canonicalization is a future enhancement; for v1 the framing fix in #147 already reduces the probability of the alternating-path subcase.

type Watchdog

type Watchdog interface {
	// ObserveToolCall records one tool invocation. Called by the
	// agent's event-tap as tool calls stream by; safe to call from
	// any goroutine.
	ObserveToolCall(ToolCall)

	// Check returns alerts triggered since the last Check call and
	// resets the per-call alert buffer. Returns nil when no signal
	// has tripped. Typically called from the agent's post-turn
	// hook; an alert returned here is "for the turn just ended."
	Check() []Alert

	// Reset clears all accumulated state. Called when the agent
	// resets (e.g. via a hypothetical /clear that clears history)
	// so signals don't carry across a logical session boundary.
	Reset()
}

Watchdog observes per-turn telemetry and returns any alerts that triggered during observation. Implementations must be safe for concurrent use — the agent calls Observe* methods from the streaming event handler and Check from the post-turn hook; concurrency is bounded but real.

The interface is intentionally narrow for v1: just tool-call observation + alert reporting. Richer telemetry (turn timing, per-turn cost delta, files-touched diff) can be added as additional Observe* methods as new signals need them.

Jump to

Keyboard shortcuts

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