Skip to main content

Reaction Engine

The ReactionEngine provides automatic event handling with retry logic, escalation, and configurable responses. It is inspired by the agent-orchestrator's reaction system.

Concept​

When something goes wrong — a task fails, a member dies, a member gets stuck — the reaction engine automatically fires configured responses. If the response doesn't resolve the issue after enough retries, it escalates.

Triggers​

TriggerWhen it fires
task-failedA task execution fails
task-stuckA task makes no progress
member-unresponsiveA member stops sending heartbeats
member-stuckA member is alive but not progressing
member-deadA member dies (3+ consecutive failures)
all-completeAll tasks in a team are done
mass-deathMultiple members die within a window

Actions​

ActionEffect
notifySend a notification (via notify handler)
send-to-agentSend a message to a specific agent
reassignReassign the task to another member
restart-memberRestart the failed member
auto-mergeAuto-merge results (for completed tasks)
customCall a custom handler function

Configuration​

from agent_teams.reactions import (
ReactionConfig,
ReactionEngine,
ReactionAction,
ReactionPriority,
ReactionTrigger,
)

engine = ReactionEngine()

# Notify on task failure with up to 3 retries
engine.add_rule(ReactionConfig(
trigger=ReactionTrigger.TASK_FAILED,
action=ReactionAction.NOTIFY,
priority=ReactionPriority.ACTION,
max_retries=3,
escalate_after_retries=2,
message_template="Task {target_id} failed",
))

# Restart unresponsive members
engine.add_rule(ReactionConfig(
trigger=ReactionTrigger.MEMBER_UNRESPONSIVE,
action=ReactionAction.RESTART_MEMBER,
max_retries=2,
))

# Reassign tasks from dead members
engine.add_rule(ReactionConfig(
trigger=ReactionTrigger.MEMBER_DEAD,
action=ReactionAction.REASSIGN,
))

Handlers​

Wire up handlers so the engine knows how to execute each action:

async def notify(trigger, target_id, priority, message, context):
print(f"[{priority.value}] {message}")

async def send_message(target_id, message, context):
await channel.send(target_id, message)

async def reassign(target_id, context):
await manager.reassign_task(target_id)

async def restart(target_id, context):
await manager.restart_member(target_id)

engine.set_notify_handler(notify)
engine.set_send_handler(send_message)
engine.set_reassign_handler(reassign)
engine.set_restart_handler(restart)

Firing Reactions​

# Fire a reaction (typically called by the health monitor or task executor)
await engine.fire(ReactionTrigger.TASK_FAILED, "task-123")

# Fire returns True if an action was executed, False otherwise
result = await engine.fire(ReactionTrigger.MEMBER_DEAD, "agent-2")

Retry and Escalation​

The engine tracks reaction state per trigger+target pair:

  • Retries: Each fire() increments the attempt counter for that trigger/target. When attempt_count > max_retries, the reaction stops.
  • Escalation: After escalate_after_retries attempts or escalate_after_seconds elapsed, the reaction escalates to URGENT priority and notifies.
# After 2 retries, escalate to urgent
engine.add_rule(ReactionConfig(
trigger=ReactionTrigger.TASK_STUCK,
action=ReactionAction.NOTIFY,
priority=ReactionPriority.WARNING,
max_retries=5,
escalate_after_retries=2,
escalate_after_seconds=1800, # or 30 minutes
))

Resolving Reactions​

When the issue is fixed (e.g., the task succeeds on retry), resolve the reaction state:

engine.resolve(ReactionTrigger.TASK_FAILED, "task-123")

This prevents further retries on the resolved trigger/target pair.

Custom Handlers​

For application-specific logic, use the CUSTOM action:

async def auto_merge_results(target_id, context):
"""Custom handler to merge results when all tasks complete."""
results = context.get("results", [])
merged = combine_results(results)
await store.save(target_id, merged)

engine.add_rule(ReactionConfig(
trigger=ReactionTrigger.ALL_COMPLETE,
action=ReactionAction.CUSTOM,
custom_handler=auto_merge_results,
))