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:
- Define the state variable
- Add it to the system prompt
- Register a tool to modify it
- Update the prompt when it changes
- Validate the new value
useKnob does all five in one line.
Basic Usage
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:
// 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 + Constraints | Semantic 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:
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:
<Knobs />Render Prop
Custom section rendering, tool auto-registered:
<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:
<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:
- Type check (number, string, boolean)
- Options check (if
optionsdefined) - Range check (min/max for numbers)
- Length/pattern check (for strings)
- Custom
validatefunction (if provided)
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):
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.
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:
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
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:
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:
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.