Skip to content

Knobs & Controls

Knobs are model-visible, model-settable reactive state. One hook call creates a value, renders it as a form control the model can see, and registers a tool for the model to change it.

This is the killer feature of the reconciler approach. Without a component model, you'd need to manually:

  1. Define the state variable
  2. Add it to the system prompt
  3. Register a tool to modify it
  4. Update the prompt when it changes
  5. Validate the new value

useKnob does all five in one line.

Basic Usage

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

  return (
    <>
      <System>Analyze the top {depth} results for each query.</System>
      <SearchTool maxResults={depth} />
      <Knobs />
      <Timeline />
    </>
  );
}

The model sees something like:

### Knobs
- search_depth [range]: 3 (min: 1, max: 10) — Number of search results to analyze
  Use set_knob to change.

And has access to a set_knob tool to modify it.

Type-Safe Constraints

Constraints are inferred from the value type:

tsx
// Number knobs: min, max, step
const [temp] = useKnob("temperature", 0.7, { min: 0, max: 2, step: 0.1 });

// String knobs: maxLength, pattern
const [format] = useKnob("output_format", "markdown", {
  maxLength: 20,
  pattern: "^(markdown|html|text)$",
});

// Boolean knobs: rendered as toggles
const [verbose] = useKnob("verbose", false, {
  description: "Include detailed explanations",
});

// Enum knobs: rendered as selects
const [style] = useKnob("writing_style", "academic", {
  options: ["academic", "casual", "technical", "creative"],
  description: "Output writing style",
});

Semantic Types

Agentick infers a semantic type from the value and constraints:

Value + ConstraintsSemantic Type
boolean[toggle]
number with min+max[range]
number without range[number]
Any type with options[select]
string[text]

Groups

Organize knobs into named groups:

tsx
const [temp] = useKnob("temperature", 0.7, {
  group: "Model Settings",
  min: 0,
  max: 2,
});
const [maxTokens] = useKnob("max_tokens", 1000, {
  group: "Model Settings",
  min: 100,
  max: 4000,
});
const [style] = useKnob("style", "helpful", {
  group: "Behavior",
  options: ["helpful", "concise", "creative"],
});

Groups render with ### GroupName headers in the model context.

Rendering Modes

Default

<Knobs /> renders a section and registers the set_knob tool:

tsx
<Knobs />

Render Prop

Custom section rendering, tool auto-registered:

tsx
<Knobs>
  {(groups) => (
    <Section id="my-knobs">
      {groups.map((g) => `## ${g.name}\n${g.knobs.map((k) => k.display).join("\n")}`).join("\n")}
    </Section>
  )}
</Knobs>

Provider

Full control — provider registers the tool, you handle rendering:

tsx
<Knobs.Provider>
  <MyCustomKnobUI />
</Knobs.Provider>;

// In MyCustomKnobUI:
function MyCustomKnobUI() {
  const { knobs, groups, get } = useKnobsContext();
  // Full custom rendering
}

Validation

The set_knob tool handler validates automatically:

  1. Type check (number, string, boolean)
  2. Options check (if options defined)
  3. Range check (min/max for numbers)
  4. Length/pattern check (for strings)
  5. Custom validate function (if provided)
tsx
const [priority] = useKnob("priority", 5, {
  min: 1,
  max: 10,
  validate: (v) => (v % 1 === 0 ? true : "Must be a whole number"),
});

Config-Level Knobs

For declaring knobs outside components (e.g., in createAgent config):

tsx
import { knob } from "agentick";

const agent = createAgent({
  knobs: {
    temperature: knob(0.7, { min: 0, max: 2 }),
    style: knob("helpful", { options: ["helpful", "concise"] }),
  },
});

knob() returns a KnobDescriptor — a branded object detected by isKnob() at runtime.

Momentary Knobs

Momentary knobs auto-reset to their default value at the end of each execution. The model sets the knob to expand context, acts on it, and the knob resets automatically — reclaiming tokens.

tsx
import { knob, useKnob, Knobs } from "agentick";

// Config-level
const planningWorkflow = knob.momentary(false, {
  description: "Account planning workflow",
});

// Or inline
function Agent() {
  const [showPlanning] = useKnob("planning", false, {
    description: "Account planning workflow",
    momentary: true,
  });

  return (
    <>
      <Knobs />
      {showPlanning && (
        <Section id="planning" audience="model">
          ...
        </Section>
      )}
      <Timeline />
    </>
  );
}

The model sees planning [momentary toggle]: false — Account planning workflow (resets after use). The reset happens after the tick loop but before the snapshot is persisted, so restored sessions always start clean.

Read-Only Knobs

Read-only knobs are model-visible but not model-settable. The knob renders in the knobs section so the model can read the state, but set_knob rejects writes by name, and group writes skip read-only members. Only application code can change the value:

tsx
function Agent() {
  const [phase, setPhase] = useKnob("phase", "collecting", {
    description: "Pipeline phase (managed by the application)",
    options: ["collecting", "processing", "done"],
    readOnly: true,
  });
  // ... call setPhase() from tool handlers as work progresses
}

The model sees phase [select]: "collecting" — Pipeline phase (managed by the application) (options: "collecting", "processing", "done", read-only). Verified gates use this internally: their state should inform the model, never be settable by it.

Conditional Context (Accordion Pattern)

Like accordions in a UI — the model sees section headers and expands what it needs. Combine useKnob + momentary + conditional rendering:

Toggle

tsx
function Agent() {
  const [showDocs] = useKnob("show_docs", false, {
    description: "Expand the full API reference",
    momentary: true,
  });

  return (
    <>
      <System>Set show_docs when you need the full reference.</System>
      <Section id="api-ref" audience="model">
        {showDocs ? fullApiDocs : "API Reference (set show_docs to expand)"}
      </Section>
      <Knobs />
      <Timeline />
    </>
  );
}

Model sees the collapsed placeholder → sets the knob → reads the content → answers. After the execution, momentary resets the knob and the section collapses. Tokens reclaimed automatically.

Mutual Exclusion

Only one section open at a time:

tsx
const sections = ["api", "billing", "troubleshooting"] as const;
const content: Record<string, string> = {
  /* ... */
};

function SupportAgent() {
  const [active] = useKnob("section", "none", {
    options: ["none", ...sections],
    description: "Which documentation section to expand",
    momentary: true,
  });

  return (
    <>
      <System>Expand a section when you need it.</System>
      {sections.map((s) => (
        <Section key={s} id={s} audience="model">
          {active === s ? content[s] : `${s} (expand to read)`}
        </Section>
      ))}
      <Knobs />
      <Timeline />
    </>
  );
}

The model sees all headers at once, picks the one it needs. Momentary reset means every turn starts with a clean table of contents.

Detail Levels

Progressive disclosure with a number knob:

tsx
const [detail] = useKnob("detail_level", 1, {
  min: 1,
  max: 3,
  description: "1=headers, 2=summaries, 3=full content",
  momentary: true,
});

return (
  <Section id="context" audience="model">
    {detail >= 1 && headers}
    {detail >= 2 && summaries}
    {detail >= 3 && fullContent}
  </Section>
);

The pattern works with any knob type — booleans for toggles, enums for mutual exclusion, numbers for granularity. The model controls its own attention window.

Released under the ISC License.