Skip to main content
reference

Watcher tools

Five tools let a personality (a directory of files that decides the agent's tools, memory, and model) set a standing intent: "when this changes, tell me, until then". A watcher runs a deterministic differ on a schedule with no model involved, and on a change it delivers a short summary to a channel, wakes its personality, or both. Every watcher has limits, so no agent-created watcher can wake a personality without end.

Source​

PartFile
Toolsextensions/tools-watchers/src/index.ts (createWatcherTools)
Manager, limits and validationextensions/watchers/src/index.ts (WatcherManager, validateWatcherInput, effectiveMaxFires)
WiringCreateAgentLoopOptions.watcherManager in packages/wiring/src/index.ts

Tests: extensions/watchers/src/__tests__/limits.test.ts and extensions/tools-watchers/src/__tests__/limits.test.ts.

Availability​

The tools are registered only when a WatcherManager is wired, which ethos gateway start and ethos serve do. ethos chat wires none. A personality sees them when its toolset.yaml lists them (toolset group watchers):

# ~/.ethos/personalities/<id>/toolset.yaml
- watcher_create
- watcher_list
- watcher_pause
- watcher_resume
- watcher_delete

Watchers are stored in ~/.ethos/watchers/watchers.json. Their checks ride the cron scheduler's 60-second tick as system jobs, so no second poller runs.

Tools​

ToolArgumentsNeeds a person-started turnWhat it does
watcher_createsee belowYesCreates a watcher owned by the calling personality.
watcher_listnoneNoLists the calling personality's watchers with kind, target, interval, actions, remaining budget, cooldown, expiry, and why a stopped watcher stopped.
watcher_pauseidNoPauses a watcher. Its last-seen state is kept.
watcher_resumeidYesResumes a paused watcher. Refused when the owner is at the per-owner cap.
watcher_deleteidYesDeletes a watcher, its schedule and its state.

"Needs a person-started turn" means the call is refused unless ToolContext.initiator === 'user'. A watcher wake, a cron turn, or any surface that does not say who started the turn gets … needs a turn a person started. watcher_pause and watcher_list stay open because they cannot add a fire. Enforced by personRequired in extensions/tools-watchers/src/index.ts.

watcher_create​

FieldTypeRequiredDescription
idstringyesLowercase letters, digits and hyphens. Unique.
kindfile | http | rss | processyesfile — content hash; http — ETag or content; rss — new item GUIDs; process — alive or dead.
targetstringyesA file path (file), a URL (http, rss), or a pid-file path, process name or PID (process).
interval_secondsnumberyesPoll interval. Minimum 60.
deliver{ platform, chat_id }one of deliver / wakeSends the change summary verbatim to that chat. No model turn.
wake{ personality_id, prompt_prefix? }one of deliver / wakeWakes the calling personality with the summary as untrusted context. personality_id must be the caller.
limits.expires_atstringnoISO-8601 date-time with a zone (Z or ±hh:mm), e.g. 2026-10-01T09:00:00Z. A bare date, a zone-less time or free text is refused. After it, the watcher pauses instead of firing.
limits.cooldown_secondsintegernoMinimum seconds between fires, >= 0. A change inside the cooldown advances the watcher's state but fires nothing.
limits.max_firesintegernoFires allowed before the watcher pauses. 1 to 100; default 20. 0 (unlimited) is refused.
watcher_create({
id: "release-feed",
kind: "rss",
target: "https://github.com/ethosagent/ethos/releases.atom",
interval_seconds: 3600,
wake: { personality_id: "scout", prompt_prefix: "Summarise what changed." },
limits: { expires_at: "2026-12-31T00:00:00Z", cooldown_seconds: 21600, max_fires: 10 }
})

watcher_list then shows the budget on the watcher's line:

release-feed [rss] https://github.com/ethosagent/ethos/releases.atom — every 3600s, wake → scout, 10 of 10 fires left, cooldown 21600s, expires 2026-12-31T00:00:00Z (active)

Limits​

LimitValueEnforced by
Default fire budget20 (DEFAULT_WATCHER_MAX_FIRES) for a watcher with an ownereffectiveMaxFires
Largest budget an agent can set100 (MAX_AGENT_WATCHER_FIRES)watcher_create, then validateWatcherInput
Active watchers per personality10 enabled (MAX_WATCHERS_PER_OWNER); paused ones do not countWatcherManager.createWatcher, WatcherManager.resumeWatcher
Unlimited (max_fires: 0)Only a record with no ownervalidateWatcherInput

Notes:

  • What counts as a fire. A change that delivered a message or started a wake turn. A fire is reserved before the delivery runs and returned when nothing went out: a withheld delivery, no callback wired, or a wake that reports it started no turn. A delivery or wake that throws keeps the fire, because a turn may have run first.
  • Where limits are checked. Expiry and a spent budget are checked on every tick, before the differ runs, so a watcher whose target never changes again still pauses once it expires. The cooldown and the budget are checked again when a fire is reserved. All of it is in WatcherManager.dispatchChange, tick and claimFire.
  • A stopped watcher. Expiry or a spent budget pauses the watcher and records why (stopped), and watcher_list prints that reason. watcher_resume clears the reason but not the fires used, so a watcher with a spent budget pauses again on its next tick. Delete it and create a new one to start over.
  • Watchers with no owner. watcher_create always records the calling personality as owner. A record with no owner — written before owners were recorded, or added to watchers.json by hand — keeps the unlimited behaviour it had before limits existed, and is the only kind that may carry maxFires: 0.
  • Records written before limits existed. They load unchanged. One with an owner gets the default budget of 20.
  • Ownership. watcher_pause, watcher_resume and watcher_delete act only on a watcher the calling personality owns; on a shared turn, only on one a shared turn created. Anything else returns Watcher not found: <id>, the same as an id that does not exist.

Errors​

Every refusal is code: 'input_invalid'.

Message (excerpt)Cause
watcher_create needs a turn a person startedCalled from a wake, cron or other system turn. The same for watcher_resume and watcher_delete.
limits.max_fires must be at least 1max_fires: 0 from an agent.
limits.max_fires must be at most 100Budget over the agent ceiling.
limits.expiresAt "…" is not an ISO-8601 date-time with a zoneNo zone, a bare date, or free text.
limits.cooldownSeconds must be an integer >= 0A negative or fractional cooldown.
personality "<id>" already has 10 active watchersThe per-owner cap. Pause or delete one first.
wake.personality_id must be the calling personalityA watcher tried to wake a different personality.
Watcher not found: <id>The id does not exist, or the caller does not own it.

Known limitations​

  • The file lock is in-process only. WatcherManager.serialize queues each read-modify-write of watchers.json inside one process. Two processes on one state directory (ethos serve beside ethos gateway start) can still interleave and lose an update: a fire count, a pause, a new record.
  • A run that has read untrusted content cannot create a watcher with a wake; see Post-read tool downgrade.

See also​