Make what the model knows follow the active tools
Write a before_agent_start prompt section from pi.getActiveTools(), and bound a settle guard so it nudges exactly once.
Registering run_checks has not told the model it exists. This example derives
prompt guidance from pi.getActiveTools() and bounds a settle guard to a single
nudge — all observed over a scripted model, not a real one.
Run it
Use Pi 1.0.4 with the example repository’s test runner. These are excerpts of the canonical extensions:
pi.on("before_agent_start", (event) => {
// getActiveTools(), not getAllTools(). getAllTools() reports every
// *registered* tool, so a tool the session disabled would still be
// described here — and the model would be told to use something it
// cannot call. The active set is what is declared to the model.
const active = pi.getActiveTools();
if (!active.includes("run_checks")) {
// Delete rather than leave stale: a section written for a previous
// turn outlives the condition that produced it.
delete event.systemPromptOptions.sections.review_guard;
return;
}
event.systemPromptOptions.sections.review_guard =
"- `run_checks` runs this repository's type check and unit tests. " +
"Use it instead of running a command yourself before reporting on a change. " +
"- Force pushes are blocked by an extension. If a push is refused, do not retry with a different form.";
});The section tracks the active set, not the registered one: pi.getAllTools()
reports every tool the session has defined, so a disabled tool would still be
described. The same handler can write a tool_guidance section for the built-in
tools, dropping it when nothing matches.
Insisting a step happens before settle:
pi.on("agent_before_settle", async (event) => {
const ranChecks = event.context.contextMessages.some(
(m) => m.role === "toolResult" && m.toolName === "run_checks",
);
if (ranChecks) return;
// Nudge once. Without this, a model that never runs the checks is asked
// again after every answer, forever.
const alreadyNudged = event.context.contextMessages.some(
(m) => m.role === "custom" && m.customType === "review-guard",
);
if (alreadyNudged) return;
return {
entries: [
{
type: "custom_message",
customType: "review-guard",
content: "You reported on a change without running `run_checks`. Run it now and revise.",
display: false,
},
],
continue: true,
};
});Expected outcome
| Prompt state | Provider’s system message |
|---|---|
run_checks active |
review_guard section naming the tool |
run_checks registered but not active |
Deleted; the tool is not described |
Model answers without running run_checks |
One continuation, then settle — exactly two requests |
Mechanism and limitations
Documented: before_agent_start exposes event.systemPromptOptions.sections;
agent_before_settle may return { entries, continue: true } for one more
request; extensions.md warns an unconditional continuation can loop.
Observed: knowledge.test.ts drives each handler through an AgentSession
with a scripted provider and inspects what the provider receives — a
tool_guidance section arrives as a named <tool_guidance>…</tool_guidance>
block. The probe pins the accessor disagreement: run_checks is registered in
every session, yet only the active ones see the guidance. The settle guard asks
exactly once — a model that never runs the checks produces two requests, and one
that does is not asked. On 1.0.4 the section did not appear in
session.systemPrompt; check the request, not that accessor.
This is the code around the model, not a real model following it: a prompt section
can describe run_checks, it cannot force the call.
Understand this example
Copy this prompt into your AI tool. No code runs here.
Apply this example
Copy this prompt into your AI tool. No code runs here.