Example

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.

Full source and test .

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.