Skip to content

Hooks

Agentick hooks follow React conventions with AI-specific extensions. They manage state, lifecycle, and reactive behavior within agent components.

State Hooks

useState

Identical to React's useState. State persists across ticks within an execution.

tsx
const [count, setCount] = useState(0);

useSignal

Reactive signal — updates propagate without re-rendering the full component tree.

tsx
const count = useSignal(0);
count.value++; // Triggers targeted update
console.log(count.value); // Current value

useKnob

Creates model-visible, model-settable reactive state. See Knobs for full documentation.

tsx
const [depth, setDepth] = useKnob("search_depth", 3, {
  min: 1,
  max: 10,
  description: "Number of results to analyze",
});

Lifecycle Hooks

All lifecycle hooks follow the "data first, ctx last" convention.

useOnMount

Runs once when the component first mounts (tick 1).

tsx
useOnMount((ctx) => {
  console.log("Agent initialized");
});

useOnTickStart

Runs at the start of each tick, including the mount tick. Newly-mounted components receive a catch-up call after their first render.

tsx
useOnTickStart((tickState, ctx) => {
  console.log(`Starting tick ${tickState.tickNumber}`);
});

useOnTickEnd

Runs after each tick completes (including the mount tick).

tsx
useOnTickEnd((result, ctx) => {
  console.log(`Tick ended. Response: ${result.response}`);
});

useAfterCompile

Runs after the compiler produces the model input, before the model call. Useful for inspection or telemetry.

tsx
useAfterCompile((compiled, ctx) => {
  console.log(`Sending ${compiled.messages.length} messages`);
});

useContinuation

Controls whether execution continues with another tick. result.shouldContinue shows the framework's decision. Return nothing to defer, or override. See Controlling the Tick Loop below.

tsx
useContinuation((result) => {
  if (result.tick >= 10) return { stop: true, reason: "max-ticks" };
  // No return = defer to framework
});

useOnExecutionEnd

Runs once when execution completes (after all ticks finish). Fires before the session snapshot is persisted, so state changes here are captured cleanly. Always fires — even on abort or error.

tsx
useOnExecutionEnd((ctx) => {
  console.log("Execution complete");
  ctx.setState("lastCompleted", Date.now());
});

useOnMessage

Fires when a new message is added to the timeline.

tsx
useOnMessage((message, ctx, state) => {
  if (message.role === "assistant") {
    logResponse(message.content);
  }
});

Controlling the Tick Loop

An execution is a sequence of ticks. Each tick is one model API call. By default, execution continues when the model returns tool calls or messages are queued, and stops otherwise.

You control this with useContinuation. The result.shouldContinue property shows the framework's current decision (including overrides from prior callbacks in the chain). Return nothing to defer, or override.

Default behavior

No tool calls and no queued messages → stop. Tool calls or queued messages → continue.

useContinuation

Override the default with a boolean, object, imperative methods, or defer:

tsx
// Defer: no return = accept framework decision
useContinuation((result) => {
  logger.info(`tick ${result.tick}, continuing: ${result.shouldContinue}`);
});

// Boolean shorthand
useContinuation((result) => !result.text?.includes("<DONE>"));

// Object with reason
useContinuation((result) => {
  if (result.tick >= 10) return { stop: true, reason: "max-ticks" };
});

// Imperative methods
useContinuation((result) => {
  if (result.text?.includes("<DONE>")) result.stop("task-complete");
});

result.stop() and result.continue()

These methods are available on the TickResult passed to both useContinuation and useOnTickEnd. They accept an optional reason string or options object:

tsx
result.stop("task-complete");
result.stop({ reason: "verified", status: "completed" });

result.continue("verification-pending");
result.continue({ reason: "retry", priority: 10 });

Stopping from useOnTickEnd

You can also control the loop from useOnTickEnd — useful when you want to run side effects and control flow in the same callback:

tsx
useOnTickEnd((result, ctx) => {
  // Log the result
  saveToDatabase(result.response);

  // Stop if the agent found what it was looking for
  if (result.response?.includes("FOUND")) {
    result.stop("target-found");
  }
});

Tick limit

Set maxTicks on the input to cap the number of ticks per execution:

tsx
await session.send({
  messages: [...],
  maxTicks: 5,  // Hard limit: stop after 5 ticks regardless
});

Signature Reference

typescript
useOnMount((ctx) => {});
useOnTickStart((tickState, ctx) => {});
useOnTickEnd((result, ctx) => {});
useAfterCompile((compiled, ctx) => {});
useOnExecutionEnd((ctx) => {});
useContinuation((result, ctx) => boolean | void); // result.shouldContinue shows framework default
useOnMessage((message, ctx, state) => {});

Released under the ISC License.