Functions
build-index
fn (skills: Vec): Str
Build a system-prompt fragment that lists every skill available to
the agent, formatted so an LLM can decide which skills to read
next. Prepended with a short header describing the read_skill
contract.
Example
index ::ai::skill/build-index([tone, escalation])
// => \"\"\"
// Available skills you can read with read_skill(name):
// - customer-tone: Apply our voice (when: reply, marketing)
// - escalation: Decide when to escalate (when: refund, angry)
// \"\"\"
builtin-apply-skill
fn (skills: Vec): ::ai::tool/Tool
fn (skills: Vec, skill-context: Map?): ::ai::tool/Tool
Construct the apply_skill built-in tool over the given skill
set. Like read_skill but lets the model pass a ctx map that
reaches the skill's body-fn (when present).
The arity-2 form takes a skill-context: Map? whose keys are
merged into the model-supplied ctx with harness keys winning.
The harness uses this to thread per-turn data (e.g. attachments)
into skill activation without trusting the model to populate it.
builtin-apply-skill-r
fn (resolver: SkillResolver): ::ai::tool/Tool
fn (resolver: SkillResolver, skill-context: Map?): ::ai::tool/Tool
Resolver-aware variant of builtin-apply-skill. Like
builtin-read-skill-r but lets the model pass a ctx map that
reaches the skill's body-fn (when present).
The arity-2 form takes a skill-context: Map? whose keys are
merged into the model-supplied ctx with harness keys winning.
See ChatOptions.skill-context.
builtin-list-skills
fn (skills: Vec): ::ai::tool/Tool
Construct the list_skills built-in tool over the given skill
set. Used internally by builtins; exposed for callers that want
to wire individual built-ins by hand.
builtin-list-skills-r
fn (resolver: SkillResolver): ::ai::tool/Tool
Resolver-aware variant of builtin-list-skills. The bound
list_skills tool consults the resolver's list-fn on every
invocation, so resolvers that change their output over time (e.g.
a hybrid resolver that re-ranks after new context arrives) stay
in sync with what the model sees.
builtin-read-skill
fn (skills: Vec): ::ai::tool/Tool
Construct the read_skill built-in tool over the given skill set.
Returns the resolved body for the named skill, or an error string
when the name is unknown.
builtin-read-skill-r
fn (resolver: SkillResolver): ::ai::tool/Tool
Resolver-aware variant of builtin-read-skill. Routes name
lookups through resolver.read-fn, so a store-backed resolver
can serve skills it never surfaced in the index.
builtin-search-skills-r
fn (resolver: SkillResolver): ::ai::tool/Tool
Built-in search_skills tool. Only constructed when the resolver
declares a search-fn; built around resolver.search-fn(query, opts) so any backing strategy (substring, embeddings, vendor
APIs) works without changes here.
builtins
fn (skills: Vec): Vec<::ai::tool/Tool>
fn (skills: Vec, skill-context: Map?): Vec<::ai::tool/Tool>
Return the three built-in tools (list_skills, read_skill,
apply_skill) bound over skills. Concatenated into the agent's
tools list by ::ai::chat/run-loop whenever the agent declares
one or more skills.
The arity-2 form forwards skill-context to builtin-apply-skill
so per-turn harness data flows into skill activation. See
ChatOptions.skill-context for the conventional usage.
Equivalent to builtins-with-resolver(in-memory-resolver(skills))
minus the optional search_skills tool. Kept for backwards
compatibility with callers that wire skill toolsets directly.
builtins-with-resolver
fn (resolver: SkillResolver): Vec<::ai::tool/Tool>
fn (resolver: SkillResolver, skill-context: Map?): Vec<::ai::tool/Tool>
Build the per-turn skill toolset from a SkillResolver. Always
emits list_skills, read_skill, and apply_skill; appends a
fourth search_skills tool when the resolver provides a
search-fn. Used by ::ai::chat/run-loop whenever an agent
declares either opts.skills or opts.skill-resolver.
The arity-2 form forwards skill-context to builtin-apply-skill-r
so per-turn harness data flows into skill activation. See
ChatOptions.skill-context for the conventional usage.
effective-resolver
fn (skill-resolver: SkillResolver?, skills: Vec?): SkillResolver?
Resolve which SkillResolver ::ai::chat/run-loop should use on
a given call. Precedence:
opts.skill-resolver(when non-null) wins outright.- Otherwise
opts.skillsis materialized viafor-agentand wrapped inin-memory-resolver. - When neither is set, returns
null—run-loopskips the skill index and skill builtins entirely.
find-skill
fn (skills: Vec, name: Str): Skill?
Look up a skill by name, returning null when no match.
for-agent
fn (entries: Vec): Vec
Normalize an agent's skills: list (a mix of Hot function refs
and pre-built Skill records) into a Vec<Skill> ready for
::ai::chat/run-loop. Per the design, agents must list every
skill function explicitly — there is no namespace glob.
Example
skills ::ai::skill/for-agent([customer-tone, escalation-policy])
from-fn
fn (f: Fn): Skill
Build a Skill from a Hot function annotated with
meta {skill: {...}}. The annotation map is merged with sensible
defaults: name falls back to the qualified function name, when
defaults to [].
Example
tone
meta {skill: {description: "Customer tone", when: ["reply"], body: "..."}}
fn () { {} }
skill ::ai::skill/from-fn(tone)
// => Skill({name: "::myapp/tone", description: "Customer tone", ...})
in-memory-resolver
fn (skills: Vec): SkillResolver
Default SkillResolver: list-fn returns the full skills
vector every turn, read-fn does an in-memory name lookup, and
search-fn is null. Mirrors the pre-resolver behaviour of
::ai::chat/run-loop exactly, so wrapping opts.skills in this
resolver is a no-op for the model.
index-line
fn (skill: Skill): Str
Render a single skill as one line in the resolver index, e.g.
" - customer-tone: Apply our voice (when: reply, marketing)".
Trigger phrases and modalities are comma-joined; missing fields
are omitted. Modalities (when set and non-default) are appended
in their own bracketed clause so the model can choose skills
appropriate to the turn's input kind.
list-skills-of
fn (skills: Vec): Vec
Build the body for a list_skills tool call: a vector of
{name, description, when} maps suitable for an LLM to scan.
load-body
fn (skill: Skill, ctx: Map): Str
Resolve the body of a Skill for a given invocation context.
Resolution order:
- If
skill.body-fnis non-null, call it withctxand return the resulting string. - Otherwise return
skill.body(or""if absent).
Errors from body-fn propagate as Result.Err.
Example
text ::ai::skill/load-body(skill, {topic: "refund"})
ns
Alias of ::ai::skill/
Skills are LLM-judgment-routed prompt augmentations: named bundles
of instructions (and optionally bundled tools) that an agent can
pull into its context on demand. Prompt-only skills can be built
directly as Skill({...}) records. Executable or function-backed
skills can also be declared by adding meta {skill: {...}} to a
Hot function; ::ai::skill/from-fn uses the compiler-harvested
metadata to build the same Skill shape at runtime.
Three built-in tools (list_skills, read_skill, apply_skill)
are auto-added by ::ai::chat/run-loop so the model can discover
and pull skills on demand without exhausting context up front.
Example
customer-tone
meta {skill: {
description: "Apply our customer voice rules",
when: ["customer reply", "marketing copy", "support email"],
body: \"\"\"
Use a warm, plain-language tone. Avoid jargon. Always close
with a clear next step.
\"\"\"
}}
fn () { {} }
generated-tone Skill({
name: "generated-tone",
description: "A prompt-only generated skill",
when: ["generated guidance"],
body: "Use this body directly."
})
Modalities
A skill may declare the input kinds it can act on via
modalities (e.g. ["image", "pdf"]). The resolver index
surfaces this hint so the model picks skills appropriate to the
turn — a vision-only skill is unlikely to be the right pick on a
pure-text turn, and vice versa. null ≡ ["text"]. Modalities
are advisory: the field does not gate dispatch, the model still
decides.
Per-turn context (attachments, sender, transport)
body-fn(ctx: Map) skills receive a ctx argument that the
model populates at the apply_skill(name, ctx) call site. The
harness can also inject per-turn data via
ChatOptions.skill-context, which is merged into ctx with
harness keys winning — so the model cannot spoof harness-owned
values like attachments.
The conventional reserved key is attachments: when the agent
runtime normalizes an inbound message it stamps the inbound
attachments into opts.skill-context.attachments. A skill that
wants to see them simply declares an attachments parameter on
its body-fn — the framework destructures ctx by parameter
name (::hot::internal::mcp/invoke-with-input semantics), so a
skill never has to do its own get(ctx, "attachments").
Static body strings are returned verbatim — the harness does
not interpolate ${...} placeholders into them. If you want
attachment-aware logic, write a body-fn.
receipt-analyzer
meta {skill: {
description: "Extract line items from a receipt",
when: ["receipt", "expense"],
modalities: ["image", "pdf"],
}}
fn (attachments: Vec?): Str {
atts or(attachments, [])
cond {
is-empty(atts) => { "Ask the user to attach a receipt." }
=> {
count length(atts)
`Use any vision tools available to extract line items
from the ${count} attached file(s).`
}
}
}
search-skills-of
fn (skills: Vec, query: Str, opts: Map): Vec
Rank skills against a free-text query using a lightweight
case-insensitive substring scorer. opts.limit caps the result
(default 10); opts.min-score filters out weak hits (default 1).
Returns the highest-scoring Skill records, ties broken by input
order.
searchable-resolver
fn (skills: Vec, opts: Map): SkillResolver
SkillResolver over a fixed Vec<Skill> that also exposes a
substring-rank search-fn. run-loop will surface a fourth
search_skills tool so the model can query for relevant skills
on demand.
opts.always-on (default []) are skills returned from
list-fn every turn regardless of search; the rest of skills
are reachable only via search_skills and read_skill. This
keeps the system-prompt index small while still letting the
model find the long tail.
Types
Skill
Skill type {
name: Str,
description: Str,
when: Vec?,
modalities: Vec?,
body: Str?,
body-fn: Fn?,
tools: Vec<::ai::tool/Tool>?,
requires: Vec?,
fn: Fn?
}
A reusable prompt augmentation. Most fields are optional:
name— unique identifier the model uses withread_skill/apply_skill. Defaults to the qualified Hot function name.description— short summary the model uses to decide when to pull the skill. Required for any useful skill.when— trigger phrases that hint when this skill applies. Joined into the resolver index injected into the system prompt.modalities— input kinds this skill can act on, e.g.["text"],["image", "pdf"]. Surfaced in the resolver index so the model can pick the right skill for a non-text turn. Defaultnull≡["text"]. See "Modalities" in the module doc.body— inline instructions returned byread_skill. May be loaded from a resource via::hot::resource/load-str. Returned verbatim — static bodies are not interpolated.body-fn— alternative tobody: a function(ctx: Map) -> Strthat produces context-aware instructions. The harness merges per-turn data (e.g. inbound attachments) intoctxbefore calling — see "Per-turn context" in the module doc.tools— bundled tools that become available to the model whenever the skill is read.requires— names of other skills this one depends on.fn— back-reference to the Hot function the skill was harvested from. Preserved for callers that need to round-trip to the source function; built-inread_skill/apply_skillresolvebodyorbody-fnrather than calling this function.
SkillResolver
SkillResolver type {
list-fn: Fn,
read-fn: Fn,
search-fn: Fn?,
skills: Vec?
}
Pluggable strategy for picking which skills are visible to the
model on a given turn. ::ai::chat/run-loop calls into a resolver
at three fixed points:
list-fn(ctx)— once per turn, when building the system-prompt skill index. Returns theVec<Skill>to advertise this turn.ctxis a map carrying the turn'smessages,systemprompt, andmodel; resolvers may inspect any of these to decide what to surface (e.g. embed the last user message and rank a store).read-fn(name)— invoked from theread_skillandapply_skillbuilt-in tools when the model asks for a specific skill body. Should returnnullfor unknown names. Note this is decoupled fromlist-fn: a store-backed resolver may know about more skills than it surfaces in any single turn.search-fn(query, opts)— optional. When set,run-loopadds a fourthsearch_skillstool that lets the model query the resolver directly. Useful when the index is too large to inline every turn.
Built-in factories:
in-memory-resolver(skills)— current behaviour, no search.searchable-resolver(skills)— likein-memory-resolverbut with a substring-ranksearch-fnover name/description/when. Pair with a small or emptylist-fnoutput to scale to many skills without inflating the system prompt.
Custom resolvers (Postgres, Pinecone, LLM-judged routing, etc.)
are constructed by passing your own Fn slots to
SkillResolver({...}) directly.