# Agentick Hooks React-like hooks for building agentic applications with Agentick. ## Agent Loop Control The most important hooks for agent development are the lifecycle hooks that control how your agent loops through ticks (model calls). ### useContinuation The primary hook for implementing agent loops with custom termination conditions. `result.shouldContinue` reflects the framework's current decision — `true` if tool calls are pending or messages are queued, `false` otherwise. When multiple callbacks are registered, each sees the accumulated decision from prior callbacks (chained). Return nothing to defer. Return `true`/`false`, an object `{ stop/continue: true, reason? }`, or call `result.stop()`/`result.continue()` to override. ```tsx import { useContinuation } from "@agentick/core"; function MyAgent() { // Veto: stop even if framework would continue useContinuation((result) => { if (result.tick >= 10) return { stop: true, reason: "max-ticks" }; }); // Defer: no return = accept current shouldContinue useContinuation((result) => { logger.info(`tick ${result.tick}, continuing: ${result.shouldContinue}`); }); // Boolean shorthand useContinuation((result) => !result.text?.includes("")); // Imperative methods with reasons useContinuation((result) => { if (result.text?.includes("")) result.stop("task-complete"); }); return ; } ``` ### TickResult The `TickResult` object passed to continuation callbacks contains: | Property | Type | Description | | ------------------- | -------------------- | -------------------------------------------------------- | | `shouldContinue` | `boolean` | Current continuation decision (chained across callbacks) | | `tick` | `number` | Current tick number | | `text` | `string?` | Combined text from assistant response | | `content` | `ContentBlock[]` | Raw content blocks from response | | `toolCalls` | `ToolCall[]` | Tool calls made this tick | | `toolResults` | `ToolResult[]` | Results from tool execution | | `stopReason` | `string?` | Model's stop reason (e.g., "end_turn") | | `usage` | `UsageStats?` | Token usage statistics | | `timeline` | `COMTimelineEntry[]` | Timeline entries for this tick | | `stop(reason?)` | `function` | Request execution to stop | | `continue(reason?)` | `function` | Request execution to continue | ### Callback Return Values The callback can influence continuation in four ways: 1. **Return void**: No opinion — accept current `shouldContinue` (framework or prior callbacks) 2. **Return boolean**: `true` = continue, `false` = stop 3. **Return object**: `{ stop: true, reason? }` or `{ continue: true, reason? }` 4. **Call methods**: `result.stop(reason?)` or `result.continue(reason?)` ```tsx // Defer (no return = accept framework decision) useContinuation((r) => { logger.info(`continuing: ${r.shouldContinue}`); }); // Boolean shorthand useContinuation((r) => !r.text?.includes("")); // Object with reason useContinuation((r) => { if (r.tick > 50) return { stop: true, reason: "budget" }; }); // Async verification useContinuation(async (r) => { const verified = await verifyWithModel(r.text); return verified ? false : true; }); ``` ## Common Agent Patterns ### Done Tool Pattern Let the model explicitly signal completion by calling a tool: ```tsx import { Tool, useContinuation } from "@agentick/core"; function AgentWithDoneTool() { const [isDone, setIsDone] = useState(false); useContinuation(() => !isDone); return ( <> { setIsDone(true); return [{ type: "text", text: `Task complete: ${summary}` }]; }} /> ); } ``` ### Token-Based Completion Parse the model's response for a completion marker: ```tsx function AgentWithDoneMarker() { useContinuation((result) => { if (result.text?.includes("") || result.text?.includes("TASK COMPLETE")) { return { stop: true, reason: "done-marker-found" }; } // No return = defer to framework (continues if tool calls pending) }); return ; } ``` ### Async Verification Use another model or external service to verify completion: ```tsx function AgentWithVerification() { useContinuation(async (result) => { // Skip verification if framework is already continuing (tool calls pending) if (result.shouldContinue) return; // Verify with a cheaper/faster model const isComplete = await verifyCompletion(result.text); if (!isComplete) return { continue: true, reason: "verification-pending" }; }); return ; } ``` ### Max Ticks with Reason Veto continuation with a reason: ```tsx function AgentWithLimits() { useContinuation((result) => { if (result.tick >= 20) return { stop: true, reason: "max-ticks-exceeded" }; if (result.usage && result.usage.totalTokens > 100000) return { stop: true, reason: "token-budget-exceeded" }; // No return = defer to framework }); return ; } ``` ## Lifecycle Hooks All lifecycle hooks follow the pattern: data first, COM (context) last. ### useOnMount Run code when the component mounts: ```tsx function AgentWithSetup() { useOnMount((ctx) => { console.log("Component mounted"); ctx.setState("initialized", true); }); return ; } ``` ### useOnUnmount Run code when the component unmounts: ```tsx function AgentWithCleanup() { useOnUnmount((ctx) => { console.log("Component unmounting"); // Cleanup resources }); return ; } ``` ### useOnTickStart Run code at the start of each tick (before compilation): ```tsx function AgentWithSetup() { useOnTickStart((tickState) => { console.log(`Tick ${tickState.tick} starting!`); }); // With COM access useOnTickStart((tickState, ctx) => { ctx.setState("lastTickStart", tickState.tick); }); return ; } ``` > **Timing:** `useOnTickStart` fires on every tick the component is alive, including the tick in which it mounts. Newly-mounted components receive a catch-up call after their first render. ### useOnTickEnd Lower-level hook for post-tick processing (useContinuation is built on this): ```tsx function AgentWithTelemetry() { useOnTickEnd((result) => { // Log tick metrics analytics.track("tick_complete", { tick: result.tick, tokens: result.usage?.totalTokens, toolCalls: result.toolCalls.length, }); // Don't affect continuation (return void) }); return ; } ``` ### useAfterCompile Inspect compiled context before sending to model, optionally request recompilation: ```tsx function AgentWithContextManagement() { useAfterCompile((compiled, ctx) => { // Estimate tokens const tokens = estimateTokens(compiled); if (tokens > MAX_CONTEXT_TOKENS) { // Summarize old messages summarizeOldMessages(ctx); ctx.requestRecompile("context-too-large"); } }); return ; } ``` ### useOnExecutionEnd Run code when execution completes (after all ticks, before snapshot persistence): ```tsx function AgentWithCleanup() { useOnExecutionEnd((ctx) => { ctx.setState("lastCompleted", Date.now()); }); return ; } ``` > **Timing:** `useOnExecutionEnd` fires once per `send()` call, after the tick loop exits but before the session snapshot is persisted. State changes here are captured in the snapshot. ## State Hooks ### useState React's useState, re-exported for convenience: ```tsx const [count, setCount] = useState(0); const [state, dispatch] = useReducer(reducer, initial); ``` ### useSignal Reactive state that triggers recompilation on change: ```tsx const count = useSignal(0); const doubled = useComputed(() => count.value * 2); // Update triggers recompile count.value++; ``` ### useComState State stored in the COM, accessible to all components. Returns a Signal (not a tuple). Automatically re-renders when state is modified externally (e.g. from a tool handler). ```tsx function ComponentA() { const value = useComState("shared-key", "initial"); console.log(value()); // or value.value — read current value value.set("updated"); // write new value } function ComponentB() { const value = useComState("shared-key", "initial"); // Same COM state return
Value: {value()}
; } ``` #### Snapshot Persistence COM state entries are included in session snapshots by default. On restore, values are applied before the component tree renders, so `useComState` reads the persisted value instead of reinitializing. Set `{ persist: false }` for transient state that shouldn't survive session restore: ```tsx // Transient state — don't persist across sessions const isExpanded = useComState("ui:expanded", false, { persist: false }); ``` Values must be JSON-serializable. Non-serializable values are silently skipped during snapshot creation. ## Context Hooks ### useCom Access the Context Object Model: ```tsx const ctx = useCom(); ctx.addSection({ id: "context", content: "..." }); ctx.setState("key", value); ``` ### useTickState Access current tick information: ```tsx const state = useTickState(); console.log(`Tick ${state.tick}`); console.log(state.timeline); // Session's full timeline ``` ## Message Hooks ### useOnMessage React to incoming messages (for interactive agents): ```tsx useOnMessage((message, ctx, state) => { if (message.type === "user") { console.log("User said:", message.content); } }); ``` ### useQueuedMessages Access messages queued for this tick: ```tsx const messages = useQueuedMessages(); for (const msg of messages) { // Process queued messages } ``` ## Timeline Hooks ### useTimeline Direct read/write access to the session's timeline (the append-only source of truth): ```tsx function MyAgent() { const timeline = useTimeline(); // Read entries console.log(`${timeline.entries.length} messages in history`); // Replace entire timeline (e.g., after summarization) timeline.set([summaryEntry, ...recentEntries]); // Transform timeline via function timeline.update((entries) => entries.slice(-10)); // Keep last 10 } ``` The timeline is the session's complete conversation history. Use `` component props (`limit`, `maxTokens`, `roles`) for non-destructive context management. Use `useTimeline().set()` / `.update()` for destructive mutations (e.g., context compression with summarization). ### useConversationHistory Read-only access to the full timeline, without needing a ``: ```tsx function HistoryViewer() { const history = useConversationHistory(); return
Messages: {history.length}
; } ``` ## Resolve Hooks ### useResolved Access data loaded by the `resolve` configuration during session restore (Layer 2). ```tsx function MyAgent() { const greeting = useResolved("greeting"); const userData = useResolved("userData"); return ( <> {greeting && {greeting}} ); } ``` `useResolved` returns `undefined` for keys that weren't resolved (or when no resolve is configured). Results are set once during restore and are read-only thereafter. ## Data Hooks ### useData Fetch and cache data with dependency-based refresh: ```tsx // Refetch when location changes const weather = useData("weather", () => fetchWeather(location), [location]); // Refetch every tick by including tick in deps const { tick } = useTickState(); const status = useData("status", fetchStatus, [tick]); // Cache forever (no deps) const config = useData("config", fetchConfig); ``` #### Snapshot Persistence `useData` cache entries are included in session snapshots by default. On restore, cached values are applied without re-fetching. Set `{ persist: false }` to exclude large datasets, frequently-changing data, or values already stored elsewhere. These entries will re-fetch on restore as if the session were starting fresh. ```tsx // Large data — re-fetch on restore instead of persisting const embeddings = useData("embeddings", () => fetchEmbeddings(query), [query], { persist: false }); ``` Values must be JSON-serializable. Non-serializable values (functions, circular references, etc.) are silently skipped during snapshot creation. ## Knobs Knobs are model-visible, model-settable reactive state. Think of them as **form controls for models** — the same way HTML inputs bridge humans to application state, knobs bridge models to application state. The model sees primitive values (string, number, boolean) and can change them via a `set_knob` tool. An optional resolve callback maps the primitive to a rich application value. ### knob() — Config-level Descriptor Create a knob descriptor for use in config objects. Detected by `isKnob()`. ```tsx import { knob } from "@agentick/core"; const config = { mode: knob("broad", { description: "Operating mode", options: ["broad", "deep"] }), model: knob("gpt-4", { description: "Model", options: ["gpt-4", "gpt-5"] }, (v) => openai(v)), citations: knob(true, { description: "Whether to include citations" }), }; ``` ### useKnob() — Component-level Hook Create a live knob inside a component. Returns `[value, setter]`. ```tsx import { useKnob, Knobs } from "@agentick/core"; function Agent() { // Simple — mode is "broad" or "deep" const [mode, setMode] = useKnob("mode", "broad", { description: "Operating mode", options: ["broad", "deep"], }); // With resolver — model sees "gpt-4", you get openai("gpt-4") const [model, setModel] = useKnob("model", "gpt-4", { description: "Model" }, (v) => openai(v)); // From descriptor const desc = knob("broad", { description: "Mode", options: ["broad", "deep"] }); const [modeFromDesc] = useKnob("mode", desc); return ( <>
Mode: {mode}
); } ``` ### Type-Safe Constraints Constraints are conditional on the value type: ```tsx // Numbers: min, max, step useKnob("temp", 0.7, { description: "Temperature", min: 0, max: 2, step: 0.1 }); // Strings: maxLength, pattern useKnob("code", "abc", { description: "Code", maxLength: 10, pattern: "^[a-z]+$" }); // Booleans: no constraints (just a toggle) useKnob("verbose", true, { description: "Verbose output" }); // All types: options, group, required, validate useKnob("mode", "quick", { description: "Research depth", options: ["quick", "deep"], group: "Behavior", required: true, validate: (v) => (v !== "invalid" ? true : "Cannot use 'invalid'"), }); ``` Semantic types are inferred automatically: `[toggle]`, `[range]`, `[number]`, `[select]`, `[text]`. ### `` — Three Rendering Modes The `` component always registers the `set_knob` tool. It provides three modes for rendering the knobs section: #### Mode 1: Default rendering Place `` in the tree. Renders a model-visible section with all knobs grouped and typed. ```tsx function Agent() { useKnob("temp", 0.7, { description: "Temperature", group: "Model", min: 0, max: 2 }); useKnob("mode", "quick", { description: "Depth", group: "Behavior", options: ["quick", "deep"] }); useKnob("verbose", true, { description: "Verbose output" }); return ( <> ); } ``` Produces a section like: ``` verbose [toggle]: true — Verbose output ### Model temp [range]: 0.7 — Temperature (0 - 2) ### Behavior mode [select]: "quick" — Depth (options: "quick", "deep") ``` #### Mode 2: Render prop Pass a function as children to control section rendering. Receives `KnobGroup[]`. ```tsx function Agent() { useKnob("temp", 0.7, { description: "Temperature", group: "Model", min: 0, max: 2 }); useKnob("mode", "quick", { description: "Depth", options: ["quick", "deep"] }); return ( <> {(groups) => (
{groups .flatMap((g) => g.knobs) .map((k) => `${k.name}=${k.value}`) .join("\n")}
)}
); } ``` The `set_knob` tool is still registered automatically. You control only the section output. #### Mode 3: Provider + Context Full custom rendering. `` registers the tool and exposes knob data via React context. Use `` or `useKnobsContext()` to consume. ```tsx import { useKnobsContext, type KnobInfo, type KnobGroup } from "@agentick/core"; function MyKnobDisplay() { const { knobs, groups, get } = useKnobsContext(); const temp = get("temp"); return (
Temperature: {temp?.value} | Total knobs: {knobs.length}
); } function Agent() { useKnob("temp", 0.7, { description: "Temperature", min: 0, max: 2 }); useKnob("mode", "quick", { description: "Depth", options: ["quick", "deep"] }); return ( <> ); } ``` `` provides built-in rendering with optional customization: ```tsx // Default section (same as output) // Custom per-knob rendering (
{knob.name}: {knob.value}
)} />
// Custom per-group rendering (
{group.knobs.map(k => `${k.name}=${k.value}`).join(", ")}
)} />
``` ### KnobInfo Read-only snapshot of a knob, passed to render props and available via context: | Field | Type | Description | | ---------------------- | ------------------------------------------------------- | --------------------------------- | | `name` | `string` | Knob name | | `description` | `string` | Human/model-readable summary | | `value` | `string \| number \| boolean` | Current primitive value | | `defaultValue` | `string \| number \| boolean` | Initial value | | `semanticType` | `"toggle" \| "range" \| "number" \| "select" \| "text"` | Inferred from value + constraints | | `valueType` | `"string" \| "number" \| "boolean"` | Primitive type | | `group` | `string?` | Group name | | `options` | `(string \| number \| boolean)[]?` | Valid values (select/enum) | | `min`, `max`, `step` | `number?` | Number constraints | | `maxLength`, `pattern` | `string? / number?` | String constraints | | `required` | `boolean?` | Whether value is required | ### KnobsContextValue Returned by `useKnobsContext()`: | Field | Type | Description | | -------- | ----------------------------------------- | ------------------------------- | | `knobs` | `KnobInfo[]` | All knobs (flat list) | | `groups` | `KnobGroup[]` | Knobs grouped; `""` = ungrouped | | `get` | `(name: string) => KnobInfo \| undefined` | Lookup a knob by name | ### Hooks | Hook | Description | | --------------------------- | ----------------------------------------------------- | | `useKnobsContext()` | Access knob context (throws outside `Knobs.Provider`) | | `useKnobsContextOptional()` | Access knob context (returns null outside provider) | ### Momentary Knobs Momentary knobs auto-reset to their default value at the end of each execution. Use them for lazy-loaded context that the model expands on demand, with automatic token reclamation. ```tsx import { knob, useKnob, Knobs } from "@agentick/core"; // Config-level descriptor const planningWorkflow = knob.momentary(false, { description: "Account planning workflow", }); // Or inline with useKnob function Agent() { const [showPlanning] = useKnob("planning", false, { description: "Account planning workflow", momentary: true, }); return ( <> {showPlanning && (
...
)} ); } ``` Momentary knobs display as `[momentary toggle]` with a `(resets after use)` hint in the model-visible section. The model sets the knob to expand context, acts on it, and the knob resets at execution end — before the snapshot is persisted, so restored sessions start clean. ### When no knobs exist All three modes render nothing when no knobs are registered — no tool, no section, no context. `` still renders its children (just without context). ## Gates (Continuation Conditions) Gates are knob-backed continuation conditions. A gate is a named checkpoint that activates when a condition is met, blocks the model from completing until it actively addresses the gate, and auto-renders instructions when active. Gates compose two existing primitives — a knob (three-state: `inactive`/`active`/`deferred`) and a continuation callback (blocks exit when engaged). The model controls the gate via `set_knob`. ### gate() — Descriptor Factory Create a gate descriptor at module level: ```tsx import { gate } from "@agentick/core"; const verificationGate = gate({ description: "Verify your changes before completing", instructions: `VERIFICATION PENDING: You've modified files. Run appropriate checks (typecheck, tests, lint). Clear the verification gate when satisfied. Set to "deferred" if you plan to verify after other work.`, activateWhen: (result) => result.toolCalls.some((tc) => ["write_file", "edit_file"].includes(tc.name)), }); ``` ### useGate() — Hook Wire the gate into a component. Returns `GateState` with the gate's current state and an auto-rendered `` element. ```tsx import { useGate, Knobs } from "@agentick/core"; function CodingAgent() { const verification = useGate("verification", verificationGate); return ( <> You are a coding agent. {verification.element} ); } ``` ### Three States | State | `active` | `deferred` | `engaged` | Blocks exit | Shows instructions | | ---------- | -------- | ---------- | --------- | ----------- | ------------------ | | `inactive` | `false` | `false` | `false` | No | No | | `active` | `true` | `false` | `true` | Yes | Yes (Ephemeral) | | `deferred` | `false` | `true` | `true` | Yes | No | ### How It Works ``` Tick N: Model edits files └─ tick end: activateWhen fires → knob = "active" └─ continuation: model would stop → gate forces continue Tick N+1: Model gets another turn └─ sees Ephemeral: "VERIFICATION PENDING: ..." └─ runs shell commands (typecheck, tests) └─ calls set_knob(name="verification", value="inactive") Tick N+1 ends: └─ gate is inactive → no continuation forced → execution completes ``` The gate only forces continuation when the model would otherwise **stop**. During multi-step work (tool calls pending, tasks in progress), `shouldContinue` is already `true` — the gate is irrelevant. It only catches the exit. ### Defer The model can set a gate to `"deferred"` to acknowledge it without addressing it immediately. Deferred gates: - Still block exit (the model can't slip past) - Don't show the instructions Ephemeral (saves tokens) - Un-defer to `"active"` when the model would stop — forcing one more turn with full instructions visible ```tsx // Model defers during a multi-step refactor: // set_knob(name="verification", value="deferred") // ... continues working ... // When model tries to stop → gate un-defers → instructions reappear // Model runs verification → clears gate → execution ends ``` ### Activation Rules - `activateWhen` only fires when the gate is `inactive` - Once engaged (`active` or `deferred`), the model controls it via `set_knob` - If the model clears the gate AND `activateWhen` fires in the same tick, the gate re-activates - Activation doesn't fire from `deferred` state — preventing unwanted re-engagement ### GateState Returned by `useGate()`: | Field | Type | Description | | ---------- | --------------------- | ----------------------------------------- | | `active` | `boolean` | Gate is in `"active"` state | | `deferred` | `boolean` | Gate is in `"deferred"` state | | `engaged` | `boolean` | `active \|\| deferred` — gate is blocking | | `clear()` | `() => void` | Set gate to `"inactive"` | | `defer()` | `() => void` | Set gate to `"deferred"` | | `element` | `JSX.Element \| null` | Ephemeral with instructions (when active) | ### GateDescriptor Passed to `gate()` and `useGate()`: | Field | Type | Description | | -------------- | --------------------------------- | ------------------------------------- | | `description` | `string` | Shown in knob menu | | `instructions` | `string` | Ephemeral content when gate is active | | `activateWhen` | `(result: TickResult) => boolean` | Condition that triggers activation | ### Multiple Gates Gates are independent. Multiple gates can be active simultaneously, and each must be cleared individually before the model can exit: ```tsx function Agent() { const verification = useGate("verification", verificationGate); const review = useGate("review", reviewGate); return ( <> {verification.element} {review.element} ); } ``` ### Design Philosophy Gates are a **nudge, not a jail**. The model CAN call `set_knob(name="verification", value="inactive")` without actually verifying. That's by design — we trust the model's judgment. The gate ensures the model must actively decide to skip verification rather than accidentally forgetting it. The framework provides the gate. The model provides the intelligence. ## Priority System When multiple components call `stop()` or `continue()`, the COM uses a priority system: 1. **Stop requests** take precedence over continue requests 2. **Higher priority** wins within each category (default priority: 0) 3. **Reasons** are preserved for debugging ```tsx // High-priority stop (will override normal continues) result.stop({ reason: "critical-error", priority: 100 }); // Normal priority continue result.continue({ reason: "still-working", priority: 0 }); ```