Hire Team

INDUSAGI DOCUMENTATION

Hooks

Preserved documentation snapshot. APIs and capabilities differ by edition and version. Check the selected package before implementation. View pinned source.

View TypeScript Coding Agent source on GitHub
On this page

Hooks are the colon-named event taxonomy an addon subscribes to. They are not a separate system from extensions — in this rebuild, lifecycle observation, payload transformation, and veto are unified into one EventDispatcher that an addon feeds through surface.on(...). The dotted-name "hook factory" model from older docs no longer exists.

The implementation is indus-code-rebuild/src/addons/dispatch/event-dispatcher.ts, with the types declared in src/addons/contract.ts. See extensions for how to register a handler.

Status: The dispatcher is fully implemented and tested, but the events are not yet emitted by the conductor — the engine exists and runs, but the coding-agent loop does not currently dispatch these events through it. There is no --hook flag and no hooks directory. This page documents the contract; see FEATURE_GAP_ROADMAP.md for the integration status.

The Three Handler Kinds

Every handler picks exactly one HookKind, which selects what its run callback returns:

type HookHandler<TPayload> =
  | { kind: "observe";   run(p: TPayload): void | Promise<void> }
  | { kind: "transform"; run(p: TPayload): TPayload | Promise<TPayload> }
  | { kind: "gate";      run(p: TPayload): GateDecision | void | Promise<GateDecision | void> };
Kind Returns Effect
observe nothing fire-and-forget; cannot alter or veto
transform a replacement payload threaded into later handlers and back to the caller
gate a GateDecision { stop: true } short-circuits the event

A GateDecision is { stop: boolean; reason?: string }.

The Events

The full HookEvent set (src/addons/contract.ts). The "Guards" column marks the reserved, gate-bearing events — the ones that front a side-effecting step the host can refuse to take:

Event Guards Description
session:start a session opened
session:end a session closed
turn:start an assistant turn began
turn:end an assistant turn settled
tool:before ✓ straddles a tool execution — a gate blocks the call
tool:after a tool finished — observe/transform the result
chat:params the model request options are being built
chat:message an assistant message was assembled
shell:env the environment for a shell action is being prepared
input:submit ✓ user input entering the loop — a gate drops the turn
context:build the message context is being assembled
compact:build the transcript condense input is being built
compact:before ✓ the transcript is about to be condensed — a gate refuses it

The reserved set is derived from a data table (EVENT_TRAITS in src/addons/dispatch/event-dispatcher.ts), not a separate hand-maintained constant. AddonEventDispatcher.reserved exposes the derived set. A gate handler may attach to any event, but only the reserved events front an action the host is expected to actually stop.

Subscribing

A handler is recorded against an event with surface.on(event, handler) inside an addon's register:

import type { addons } from "indusagi-coding-agent";

const addon: addons.AddonManifest = {
  id: "logger",
  register(surface) {
    // observe: log every assistant turn boundary
    surface.on("turn:start", {
      kind: "observe",
      run() {
        console.log("turn started");
      },
    });

    // transform: prepend context to an assembled assistant message
    surface.on<{ content: { type: string; text?: string }[] }>("chat:message", {
      kind: "transform",
      run(payload) {
        return {
          ...payload,
          content: [{ type: "text", text: "[audited] " }, ...payload.content],
        };
      },
    });

    // gate: refuse a compaction during a critical section
    surface.on("compact:before", {
      kind: "gate",
      run() {
        return { stop: true, reason: "compaction paused" };
      },
    });
  },
};

export default addon;

Dispatch Semantics

When an event is dispatched, the engine walks that event's subscriptions in registration (load) order:

  • observe handlers see the current payload and return nothing.
  • transform handlers fold their return value forward (a transform that throws is isolated and treated as a no-op — the prior payload is kept).
  • The first gate that returns { stop: true } short-circuits the walk; its decision is surfaced as DispatchOutcome.gate.

Because a gate stops the walk, a gate registered earlier wins over a transform registered later — precedence is controlled purely through load order, with no priority field.

interface DispatchOutcome<TPayload> {
  payload: TPayload;     // after every transform ran
  gate?: GateDecision;   // the first stopping decision, if any
}

Fault Isolation

A handler that throws is converted to an AddonFault (kind: "handler") routed to the dispatcher's onFault listeners and then swallowed — observe/transform continue with the prior payload, and a gate fails open (no veto) rather than wedging the agent. A throwing fault listener cannot break fan-out to the others.

Construction Helper

For hosts/tests assembling subscriptions by hand without the full surface builder, subscription(addon, event, handler) mints the recorded EventSubscription shape, and AddonEventDispatcher.from(subscriptions) builds the engine.