Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Alert ¶
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).
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.
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 ¶
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.