Skip to content

feat(github): plugin-defined reactive behavior hooks #435

@sentry-junior

Description

@sentry-junior

Summary

Design and implement a plugin-defined reactive behavior system for the GitHub channel, enabling Junior to respond to GitHub events beyond explicit @junior mentions. Builds on #311 (the @mention entry point) by adding a behavior arbitration layer, new plugin hooks, and constrained execution modes.

The core concept: GitHub is a peer channel to Slack with its own system prompt, tools, and response surface. Plugins declare named behaviors and claim them in response to specific GitHub events. Core arbitrates claims and runs at most one agent per event.

Background

Issue #311 covers Phase 1: @junior mention in a comment → full agent turn → GitHub comment reply. This issue covers the architecture needed to support reactive event handling — where GitHub events from other bots or CI systems trigger predefined plugin behaviors.

Two distinct turn types both respond on GitHub:

Explicit mention Reactive event
Trigger @junior in comment Bot comment, CI result, etc.
Mode conversation reactive_behavior
Agency Full — user invoked Predefined behavior, constrained
Direct push Allowed if user asks Never
Create PR Yes Draft only, if configured
Opt-in No Yes — repo-scoped, default off

Proposal

New plugin hooks

Add two hooks to AgentPluginHooks in @sentry/junior-plugin-api:

behaviors() — plugins declare named behavior definitions at startup. Auditable and validatable at startup time.

behaviors?(ctx: BehaviorRegistrationContext): Record<string, AgentBehaviorDefinition>;

webhookEvent() — invoked per GitHub webhook delivery. Returns a claim to invoke a named behavior, or ignored/rejected. No agent.dispatch in context — hooks propose, core disposes.

webhookEvent?(ctx: WebhookEventHookContext): Promise<WebhookEventResult | void>;

Return type is a claim referencing a declared behavior:

type WebhookEventResult =
  | { kind: "ignored"; reason?: string }
  | { kind: "rejected"; httpStatus: 400 | 401 | 403 | 422; reason?: string }
  | { kind: "claim"; behaviorId: string; idempotencyKey: string; input: string; variables?: Record<string, unknown>; target?: GitHubResponseTarget };

Behavior definitions

Behavior definitions are declared at startup and include constrained execution policy:

interface AgentBehaviorDefinition {
  channel: "github";
  mode: "conversation" | "reactive_behavior";
  prompt: { profile: string; systemPromptAddon?: string };
  toolPolicy: ToolPolicy;       // enforced by runner, not prompt guidance
  limits?: { maxModelTurns?: number; maxToolCalls?: number };
  constraints?: { allowDirectPush: false; requireDraftPullRequest?: boolean };
  response: { destination: "source_thread" | "source_comment"; format?: "github_markdown" };
}

Extended tools() hook

Extend the existing tools() context with channel and run so plugins can conditionally register tools:

interface ToolRegistrationHookContext {
  channel: AgentChannelContext;  // { kind: "github", owner, repo, ... } or { kind: "slack", ... }
  run: AgentRunContext;          // { mode: "conversation" | "reactive_behavior", behaviorId?, toolPolicy? }
  // ...existing Slack compat fields preserved
}

Multi-plugin safety: claim arbitration

Core collects all plugin claims before scheduling:

  • 0 claims → ACK, no run
  • 1 claim → validate + schedule exactly one run
  • 2+ claims → ACK (prevent retry storm), log conflict, alert, no run

Storage-level uniqueness enforced on (source.platform, source.sourceEventId) for webhook-originated runs.

Delivery abstraction

Refactor dispatch runner to use a delivery adapter interface, replacing hardcoded Slack post calls:

  • SlackDeliveryAdapter — wraps existing Slack post logic
  • GitHubDeliveryAdapter — posts comments/PRs via GitHub App installation token

Reactive behavior opt-in

Default: disabled everywhere. Reactive behaviors require explicit repo-scoped enablement:

interface GitHubBehaviorEnablement {
  enabled: boolean;
  owner: string; repo: string;
  plugin: string; behaviorId: string;
  eventTypes: string[];
  actorAllowlist?: string[];  // which bots/apps can trigger
  bodyPatterns?: string[];
}

Implementation phases

Phase 1 (prerequisite — #311): GitHub adapter, @junior mention handling, GitHubDeliveryAdapter, GitHub system prompt.

Phase 2: behaviors() and webhookEvent() hooks, behavior arbiter, claim arbitration, storage uniqueness, behavior definitions validated at startup.

Phase 3 (gated on runner enforcement): Runner-level tool policy filtering + executor-level rejection, channel/run context in beforeToolExecute/sandboxPrepare, GitHub plugin's first reactive behavior (considerAndAddress for CI bot findings), opt-in config model.

Stop conditions for Phase 3

  • Runner must filter tools by toolPolicy before model exposure
  • Executor must reject disallowed tool calls at runtime (not prompt-only)
  • GitHub write credentials must not be injected in reactive mode
  • Multiple claim arbitration must be enforced before shipping
  • Reactive behaviors must be repo-scoped opt-in, default off

Related

Action taken on behalf of David Cramer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions