The SDK is the preferred path. ACP also generates a deterministic long-form client plus an instruction prompt you can paste into a coding agent, or you can wire the pieces yourself.
Wrap the call you already have
wrapAgentRun refuses to run while the agent is paused or stopped, times the call, reads token counts and the answer text from OpenAI, Anthropic and Google Gemini responses, and reports the run. Errors are reported and rethrown. In Python the same wrapper is wrap_agent_run (and wrap_agent_run_async), which raises AgentPausedError without calling your function.
agent-control-panel · run wrapper
import { acp } from "./acp";
// Your application supplies the fallback and provider call.
const systemPrompt = (await acp.fetchPrompt()) || DEFAULT_PROMPT;
const model = acp.getEffectiveModel(DEFAULT_MODEL);
const answer = await acp.wrapAgentRun(
() => callYourModel({ model, systemPrompt, input }),
{ inputSummary: input.slice(0, 500), modelUsed: model }
);
// For a run you time yourself, use reportRun instead.
// Include fullInput/fullOutput and promptVersion explicitly
// when you need recorded content and version attribution.
Python · run wrapper
from agent_control_panel import AgentPausedError
def handle(question: str) -> str:
try:
return acp.wrap_agent_run(
lambda: my_agent(question, system_prompt=acp.fetch_prompt() or DEFAULT_PROMPT),
user_input=question,
model_used=acp.get_effective_model("gpt-4o-mini"),
)
except AgentPausedError:
return "This assistant is paused. Please try again later."
Batching
Runs buffer and flush every 5 seconds, or immediately at 50 buffered. The buffer is capped at 200; excess drops oldest first.
Retries
A failed flush retries four times with jittered backoff, then drops the batch and logs the count. Reporting never throws into your code.
Shutdown
The client flushes on beforeExit and on acp.stop(), so the last runs of a process still arrive.
Paste to your coding agent
The onboarding prompt tells the agent exactly which files to create, which env vars to read, and which calls to wrap. It does not ask the agent to invent an API.
Instructions for the coding agent
1. Create acp-client.js with the code provided by Agent Control Panel.
2. Read ACP_URL, ACP_API_KEY and ACP_WEBHOOK_SECRET from the environment.
3. Register every existing agent id in the agentState map.
4. Wrap each agent's model call in wrapAgentRun(agentId, fallbackPrompt, runFn, defaultModel).
5. Mount POST /api/acp/webhook and GET /api/acp/status.
6. Do not change agent behavior, prompts, or model defaults.
Manual: prompt fetch and run guard
Two pieces matter. The prompt fetch resolves the current version with a cache fallback. The run wrapper checks paused and stopped state before the model is called.
acp-client.js · run guard
// ─── Agent Run Wrapper ───────────────────────────────────────────────────
// runFn receives (prompt, effectiveModel) so you can use the model override
async function wrapAgentRun(agentId, fallbackPrompt, runFn, defaultModel, options) {
const state = agentState[agentId];
if (!state) throw new Error(`Unknown agent: ${agentId}`);
if (state.paused) {
return { error: "Agent is paused by Agent Control Panel", paused: true };
}
if (state.stopped) {
return { error: "Agent is stopped by Agent Control Panel", stopped: true };
}
const { prompt, version } = await fetchPrompt(agentId, fallbackPrompt);
const effectiveModel = getEffectiveModel(agentId, defaultModel || "");
state.status = "running";
const startTime = Date.now();
try {
const result = await runFn(prompt, effectiveModel);
state.status = "idle";
state.runsSinceLast++;
state.tokensSinceLast += (result.tokensTotal || 0);
reportRun(agentId, { status: "success", /* … */ prompt_version: version });
return result;
} catch (err) {
state.status = "error";
state.errorsSinceLast++;
reportRun(agentId, { status: "error", error_message: err.message, prompt_version: version });
throw err;
}
}
Fallback order for prompts: live fetch, then the local cache, then the prompt already hardcoded in your app. A failed fetch never blocks a run.