ACPX Provider Integration
Note:
extensions/acpx-provider/is a backward-compatible shim for consumers such as pi-sidecar. It exportsdiscoverAcpxModels,mapAcpxDiscoveredModels,modelIdToDisplayName, and a no-op extension entry. Provider registration and streaming run throughextensions/providers/(AcpxDriver+ProviderDriverRegistry). See Configuration & Settings for settings file locations and External AI Agents & CLI for CLI usage of external agents.
acpx_agents
Agents registered as ACPX providers at session start.
| Parameter | Type | Default | Description |
|---|---|---|---|
acpx_agents |
string[] | string |
[] |
Agent identifiers to create via AcpxDriver. Accepts a JSON array or a comma-separated string. Names are trimmed, lowercased, and filtered to /^[a-z0-9_-]+$/. |
Resolution order: project settings → global settings → ACPX_AGENTS env → [].
Driver mapping (ACPX_AGENT_TO_DRIVER, extensions/providers/built-in-drivers.ts): cursor, claude, and gemini all map to driver kind acpx, with config.agent set to the agent name. An agent that is not in the map is skipped with a no driver mapping in ACPX_AGENT_TO_DRIVER warning — it does not fall back to acpx.
{
"acpx_agents": ["cursor"]
}
export ACPX_AGENTS=cursor
Effect: For each agent, extensions/providers/ creates a ProviderInstance, registers provider id acpx-<agent>, and exposes models under that provider.
Note: During extension startup, each agent races
registry.createInstanceagainstDISCOVERY_TIMEOUT_MS(30000). Timed-out creates are torn down if they finish later. Discovery is skipped entirely inside subagent children (discoverAndRegisterAcpxreturns early whenisSubagent), so ACPX providers only exist in the main session.
loadAcpxRuntime()
Resolves the acpx/runtime module. Memoized across calls; failed loads clear the cache.
Module: extensions/acpx-provider/load-runtime.ts
| Return field | Type | Description |
|---|---|---|
createAcpRuntime |
Function |
Builds an ACP runtime for a cwd / session store / agent registry. |
createFileSessionStore |
Function |
Filesystem session store factory. |
createAgentRegistry |
Function |
ACPX agent registry factory. |
Resolution order:
- Global npm root (
npm root -g) packageacpx→dist/runtime.js - Candidate roots:
/usr/local/lib/node_modules,~/.npm-global/lib/node_modules - Package import
acpx/runtime
A test helper, clearAcpxRuntimeCache(), resets both the root list and the memoized promise.
import { loadAcpxRuntime } from "./load-runtime.js";
const { createAcpRuntime, createFileSessionStore, createAgentRegistry } =
await loadAcpxRuntime();
Throws: Error if runtime cannot be resolved (npm install -g acpx or package dependency acpx required).
discoverAcpxModels(agent, cwd?)
Async discovery of available model ids for one ACPX agent. Creates a temporary oneshot session, reads getStatus().models.availableModelIds, closes the session, and deletes the temp state dir.
Module: extensions/acpx-provider/index.ts
| Parameter | Type | Default | Description |
|---|---|---|---|
agent |
string |
(required) | Agent id. Must match /^[a-z0-9_-]+$/i. |
cwd |
string |
process.cwd() |
Working directory for the discovery session. |
Returns: Promise<Array<{ id: string; name: string; provider: string }>>
| Field | Description |
|---|---|
id |
<agent>:<modelId> |
name |
Display name from modelIdToDisplayName(modelId) plus (<agent>) |
provider |
acpx-<agent> |
On discovery failure, returns [] (does not throw). Invalid agent throws Error.
import { discoverAcpxModels } from "pi-orchestrator-config/extensions/acpx-provider";
const models = await discoverAcpxModels("cursor", "/path/to/project");
// [{ id: "cursor:gpt-5.4[...]", name: "Gpt 5.4 (cursor)", provider: "acpx-cursor" }]
Note: Discovery uses
permissionMode: "deny-all"and state under~/.acpx/discover-<pid>-<uid>/.
mapAcpxDiscoveredModels(agent, modelIds)
Maps raw model id strings to pi-ai runtime Model objects for provider registration.
Module: extensions/acpx-provider/runtime-models.ts
| Parameter | Type | Default | Description |
|---|---|---|---|
agent |
string |
(required) | Agent id used in model/provider ids. |
modelIds |
readonly string[] |
(required) | Raw ids from ACPX status / snapshot. |
catalog |
ModelsDevCatalog \| null |
null |
Optional models.dev catalog used to fill in context window, pricing, and capability fields on each model. |
Returns: Model[] with api: "acpx" and provider: "acpx-<agent>".
| Condition | Effect |
|---|---|
modelIds.length === 0 |
Single fallback model { id: "<agent>:default", name: "<agent> (default)" } |
| otherwise | One model per id: { id: "<agent>:<m>", name: "<Display> (<agent>)" } |
import { mapAcpxDiscoveredModels } from "pi-orchestrator-config/extensions/acpx-provider";
const models = mapAcpxDiscoveredModels("cursor", ["gpt-5.4[context=272k]"]);
// id: "cursor:gpt-5.4[context=272k]", name: "Gpt 5.4 (cursor)", provider: "acpx-cursor"
modelIdToDisplayName(modelId)
| Parameter | Type | Default | Description |
|---|---|---|---|
modelId |
string |
(required) | Raw ACPX model id. |
Returns: string — bracket suffix stripped, hyphens → spaces, title-cased.
import { modelIdToDisplayName } from "pi-orchestrator-config/extensions/acpx-provider";
modelIdToDisplayName("gpt-5.4[context=272k]"); // "Gpt 5.4"
buildAmbientLoginAuth(opts)
Builds /login api-key auth that stores a configured marker when isConfigured() is true.
Module: extensions/shared/create-runtime-provider.ts
Wired by: registerAcpxAgent() in extensions/providers/index.ts
| Option | Type | Default | Description |
|---|---|---|---|
displayName |
string |
(required) | Label in the /login prompt (ACPX <agent>). |
isConfigured |
() => boolean \| Promise<boolean> |
(required) | Gate for login/resolve/check. For ACPX: acpxInstances.has(agent). |
sourceLabel |
string |
(required) | Auth source string (e.g. "cursor acpx runtime"). |
Returns: ProviderAuth["apiKey"] for createRuntimeProvider() / createProvider().
import { buildAmbientLoginAuth } from "../shared/create-runtime-provider.js";
// Wired in extensions/providers/index.ts during registerAcpxAgent()
const auth = buildAmbientLoginAuth({
displayName: "ACPX cursor",
isConfigured: () => acpxInstances.has("cursor"),
sourceLabel: "cursor acpx runtime",
});
AcpxDriver
Built-in ProviderDriver (driverKind: "acpx") in extensions/providers/acpx-driver.ts. Listed in BUILT_IN_DRIVERS and created through ProviderDriverRegistry in extensions/providers/index.ts.
Config (AcpxConfig)
| Field | Type | Default | Description |
|---|---|---|---|
agent |
string |
"cursor" |
ACPX agent name passed to ensureSession. |
enabled |
boolean |
true |
Instance enabled flag. |
AcpxDriver.probe(config)
| Parameter | Type | Default | Description |
|---|---|---|---|
config |
AcpxConfig |
(required) | Passed by the registry; availability checks loadAcpxRuntime() only. |
Returns: { available: true } when both checks pass; otherwise { available: false, reason: string }.
Probe performs two checks:
- Agent CLI on
PATH— resolves the binary namedagentforagent: "cursor", otherwise the agent name itself (claude,gemini). Missing binary → unavailable with aCLI binary ... not found on PATHreason. - ACPX runtime —
loadAcpxRuntime()must resolve, otherwise unavailable with anacpx runtime not availablereason.
AcpxDriver.create(input)
| Parameter | Type | Default | Description |
|---|---|---|---|
input.instanceId |
string |
(required) | Registry instance id (acpx-<agent>). |
input.config |
AcpxConfig |
(required) | Agent + enabled. |
input.cwd |
string |
(required) | Project cwd; hashed to 12-char cwdSlug for state/session keys. |
input.displayName |
string |
`ACPX ${agent}` |
UI display name. |
input.enabled |
boolean |
from input | Instance enabled flag. |
Returns: ProviderInstance with managed model snapshot, adapter, and dispose → adapter.stopAll().
Create side effects:
- Runtime state dir:
~/.acpx/pi-<cwdSlug>/ permissionMode: "approve-all"- Initial persistent session key:
pi-<agent>-default - Snapshot refresh uses a separate oneshot discovery helper
const instance = await registry.createInstance(
"acpx-cursor",
{ driver: "acpx", config: { agent: "cursor" }, enabled: true },
projectCwd,
);
Adapter: startSession(opts)
| Option | Type | Default | Description |
|---|---|---|---|
model |
string |
"default" |
ACPX model id (suffix of pi model id after :). |
systemPrompt |
string |
from buildExternalSystemPrompt |
Prompt applied when the handle is first created for that model. |
cwd |
string |
(required by type) | Project cwd (adapter already bound at create). |
Returns: { sessionId, model } where sessionId is pi-<agent>[-<sanitizedModel>]-<cwdSlug>.
Adapter: sendTurn(handle, prompt, opts?)
| Parameter | Type | Default | Description |
|---|---|---|---|
handle |
SessionHandle |
(required) | From startSession. |
prompt |
string |
(required) | Latest user message text only. |
opts.signal |
AbortSignal |
— | Aborts the turn. |
opts.onEvent |
(event) => void |
— | Receives { kind: "text_delta" \| "thinking_delta", text }. |
Returns: TurnResult — { text, thinking?, stopReason?, usage? }.
Stream mapping: ACPX text_delta with stream === "thought" → thinking_delta; other text_delta → text_delta.
const turn = runtime.startTurn({
handle: acpxHandle,
text: prompt,
mode: "prompt",
requestId: `pi-${Date.now()}-${randomSuffix}`,
signal: abortController.signal,
});
for await (const event of turn.events) {
if (event.type === "text_delta" && event.text) {
// event.stream === "thought" → thinking_delta; else → text_delta
}
}
Warning: Only the latest user message is sent each turn. Full historical message arrays are not forwarded to the ACPX runtime.
Adapter: stopSession / stopAll
| Method | Effect |
|---|---|
stopSession(handle) |
runtime.close for that model handle; clears prompt/usage tracking for the key. |
stopAll() |
Closes all handles; clears handle maps. Called from instance dispose. |
Stream bridge (StreamAssembler)
extensions/providers/index.ts wraps adapter.sendTurn and feeds driver events into StreamAssembler (extensions/shared/stream-builder.ts) for native pi assistant streams.
| Step | Action |
|---|---|
| 1 | Resolve instance from acpxInstances |
| 2 | Parse model id: agent:modelId → driver model suffix |
| 3 | adapter.startSession({ model, systemPrompt, cwd }) |
| 4 | extractLatestUserMessage(context) → prompt |
| 5 | adapter.sendTurn(..., { onEvent: assembler.handleEvent }) |
| 6 | assembler.finalize({ finalText, finalThinking, stopReason, usage }) |
runtime.ensureSession(options)
ACPX runtime API used by discovery and the driver adapter.
| Parameter | Type | Default | Description |
|---|---|---|---|
sessionKey |
string |
(required) | Stable key (discover-... or pi-<agent>...-<cwdSlug>). |
agent |
string |
(required) | Target agent id. |
mode |
string |
(required) | "oneshot" for throwaway discovery; "persistent" for stream sessions. |
cwd |
string |
(required) | Session working directory. |
sessionOptions |
object |
omitted if empty | Optional { model?, systemPrompt? }. |
const handle = await runtime.ensureSession({
sessionKey: "pi-cursor-gpt-4-a1b2c3d4e5f6",
agent: "cursor",
mode: "persistent",
cwd: "/home/user/project",
sessionOptions: {
model: "gpt-4",
systemPrompt: "You are being used as a backend LLM...",
},
});
Session shutdown
| Hook | Effect |
|---|---|
session_shutdown |
Stops CLI session reaper; registry.teardownAll() (each ACPX instance dispose → adapter.stopAll()); clears cliInstances and acpxInstances; calls resetProvidersInitialized(). |
Module: extensions/providers/session-shutdown.ts (teardownProvidersOnSessionShutdown)
export async function teardownProvidersOnSessionShutdown(
registry: ProviderDriverRegistry,
cliInstances: Map<string, unknown>,
acpxInstances: Map<string, unknown>,
): Promise<void> {
try {
stopCliSessionReaper();
await registry.teardownAll();
} catch (err) {
log.error("session_shutdown teardown failed", ...);
throw err;
} finally {
// Always clear maps + init guard so a teardown failure cannot leave
// initialized stuck true (would skip registerProvider on /new|/resume|/fork).
cliInstances.clear();
acpxInstances.clear();
resetProvidersInitialized();
}
}
Note: Clearing the initialized guard is what lets providers re-register after
/new,/resume, or/fork. It runs in afinallyblock, so it happens even when teardown throws.