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): cursor → acpx. Any other listed name falls back to driver kind acpx with config.agent set to that name.
{
"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.
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
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. |
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 } if loadAcpxRuntime() resolves; otherwise { available: false, reason: string }.
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. |
pi.on("session_shutdown", async () => {
stopCliSessionReaper();
await registry.teardownAll();
cliInstances.clear();
acpxInstances.clear();
});