2026-02-26 11:00:09 +01:00
import { createInterface } from "node:readline" ;
import type {
AcpRuntimeCapabilities ,
AcpRuntimeDoctorReport ,
AcpRuntime ,
AcpRuntimeEnsureInput ,
AcpRuntimeErrorCode ,
AcpRuntimeEvent ,
AcpRuntimeHandle ,
AcpRuntimeStatus ,
AcpRuntimeTurnInput ,
PluginLogger ,
2026-03-17 22:58:43 -07:00
} from "../runtime-api.js" ;
import { AcpRuntimeError } from "../runtime-api.js" ;
2026-03-08 03:15:30 +00:00
import { toAcpMcpServers , type ResolvedAcpxPluginConfig } from "./config.js" ;
2026-03-13 16:24:58 +00:00
import { checkAcpxVersion , type AcpxVersionCheckResult } from "./ensure.js" ;
2026-03-01 09:31:41 +01:00
import {
parseJsonLines ,
parsePromptEventLine ,
toAcpxErrorEvent ,
} from "./runtime-internals/events.js" ;
2026-03-08 03:15:30 +00:00
import {
buildMcpProxyAgentCommand ,
resolveAcpxAgentCommand ,
} from "./runtime-internals/mcp-agent-command.js" ;
2026-02-26 11:00:09 +01:00
import {
resolveSpawnFailure ,
2026-03-01 23:56:58 +00:00
type SpawnCommandCache ,
type SpawnCommandOptions ,
2026-03-02 01:31:23 +00:00
type SpawnResolutionEvent ,
2026-02-26 11:00:09 +01:00
spawnAndCollect ,
spawnWithResolvedCommand ,
waitForExit ,
} from "./runtime-internals/process.js" ;
import {
asOptionalString ,
asTrimmedString ,
buildPermissionArgs ,
deriveAgentFromSessionKey ,
isRecord ,
type AcpxHandleState ,
type AcpxJsonObject ,
} from "./runtime-internals/shared.js" ;
export const ACPX_BACKEND_ID = "acpx" ;
const ACPX_RUNTIME_HANDLE_PREFIX = "acpx:v1:" ;
const DEFAULT_AGENT_FALLBACK = "codex" ;
2026-03-05 20:17:50 -05:00
const ACPX_EXIT_CODE_PERMISSION_DENIED = 5 ;
2026-02-26 11:00:09 +01:00
const ACPX_CAPABILITIES : AcpRuntimeCapabilities = {
controls : [ "session/set_mode" , "session/set_config_option" , "session/status" ] ,
} ;
2026-03-13 16:24:58 +00:00
type AcpxHealthCheckResult =
| {
ok : true ;
versionCheck : Extract < AcpxVersionCheckResult , { ok : true } > ;
}
| {
ok : false ;
failure :
| {
kind : "version-check" ;
versionCheck : Extract < AcpxVersionCheckResult , { ok : false } > ;
}
| {
kind : "help-check" ;
result : Awaited < ReturnType < typeof spawnAndCollect > > ;
}
| {
kind : "exception" ;
error : unknown ;
} ;
} ;
2026-03-05 20:17:50 -05:00
function formatPermissionModeGuidance ( ) : string {
return "Configure plugins.entries.acpx.config.permissionMode to one of: approve-reads, approve-all, deny-all." ;
}
function formatAcpxExitMessage ( params : {
stderr : string ;
exitCode : number | null | undefined ;
} ) : string {
const stderr = params . stderr . trim ( ) ;
if ( params . exitCode === ACPX_EXIT_CODE_PERMISSION_DENIED ) {
return [
stderr || "Permission denied by ACP runtime (acpx)." ,
"ACPX blocked a write/exec permission request in a non-interactive session." ,
formatPermissionModeGuidance ( ) ,
] . join ( " " ) ;
}
return stderr || ` acpx exited with code ${ params . exitCode ? ? "unknown" } ` ;
}
2026-03-17 17:27:52 +01:00
function summarizeLogText ( text : string , maxChars = 240 ) : string {
const normalized = text . trim ( ) . replace ( /\s+/g , " " ) ;
if ( ! normalized ) {
return "" ;
}
if ( normalized . length <= maxChars ) {
return normalized ;
}
return ` ${ normalized . slice ( 0 , maxChars ) } ... ` ;
}
function findSessionIdentifierEvent ( events : AcpxJsonObject [ ] ) : AcpxJsonObject | undefined {
return events . find (
( event ) = >
asOptionalString ( event . agentSessionId ) ||
asOptionalString ( event . acpxSessionId ) ||
asOptionalString ( event . acpxRecordId ) ,
) ;
}
2026-02-26 11:00:09 +01:00
export function encodeAcpxRuntimeHandleState ( state : AcpxHandleState ) : string {
const payload = Buffer . from ( JSON . stringify ( state ) , "utf8" ) . toString ( "base64url" ) ;
return ` ${ ACPX_RUNTIME_HANDLE_PREFIX } ${ payload } ` ;
}
export function decodeAcpxRuntimeHandleState ( runtimeSessionName : string ) : AcpxHandleState | null {
const trimmed = runtimeSessionName . trim ( ) ;
if ( ! trimmed . startsWith ( ACPX_RUNTIME_HANDLE_PREFIX ) ) {
return null ;
}
const encoded = trimmed . slice ( ACPX_RUNTIME_HANDLE_PREFIX . length ) ;
if ( ! encoded ) {
return null ;
}
try {
const raw = Buffer . from ( encoded , "base64url" ) . toString ( "utf8" ) ;
const parsed = JSON . parse ( raw ) as unknown ;
if ( ! isRecord ( parsed ) ) {
return null ;
}
const name = asTrimmedString ( parsed . name ) ;
const agent = asTrimmedString ( parsed . agent ) ;
const cwd = asTrimmedString ( parsed . cwd ) ;
const mode = asTrimmedString ( parsed . mode ) ;
const acpxRecordId = asOptionalString ( parsed . acpxRecordId ) ;
const backendSessionId = asOptionalString ( parsed . backendSessionId ) ;
const agentSessionId = asOptionalString ( parsed . agentSessionId ) ;
if ( ! name || ! agent || ! cwd ) {
return null ;
}
if ( mode !== "persistent" && mode !== "oneshot" ) {
return null ;
}
return {
name ,
agent ,
cwd ,
mode ,
. . . ( acpxRecordId ? { acpxRecordId } : { } ) ,
. . . ( backendSessionId ? { backendSessionId } : { } ) ,
. . . ( agentSessionId ? { agentSessionId } : { } ) ,
} ;
} catch {
return null ;
}
}
export class AcpxRuntime implements AcpRuntime {
private healthy = false ;
private readonly logger? : PluginLogger ;
private readonly queueOwnerTtlSeconds : number ;
2026-03-01 23:56:58 +00:00
private readonly spawnCommandCache : SpawnCommandCache = { } ;
2026-03-08 03:15:30 +00:00
private readonly mcpProxyAgentCommandCache = new Map < string , string > ( ) ;
2026-03-01 23:56:58 +00:00
private readonly spawnCommandOptions : SpawnCommandOptions ;
2026-03-02 01:31:23 +00:00
private readonly loggedSpawnResolutions = new Set < string > ( ) ;
2026-02-26 11:00:09 +01:00
constructor (
private readonly config : ResolvedAcpxPluginConfig ,
opts ? : {
logger? : PluginLogger ;
queueOwnerTtlSeconds? : number ;
} ,
) {
this . logger = opts ? . logger ;
const requestedQueueOwnerTtlSeconds = opts ? . queueOwnerTtlSeconds ;
this . queueOwnerTtlSeconds =
typeof requestedQueueOwnerTtlSeconds === "number" &&
Number . isFinite ( requestedQueueOwnerTtlSeconds ) &&
requestedQueueOwnerTtlSeconds >= 0
? requestedQueueOwnerTtlSeconds
: this . config . queueOwnerTtlSeconds ;
2026-03-01 23:56:58 +00:00
this . spawnCommandOptions = {
strictWindowsCmdWrapper : this.config.strictWindowsCmdWrapper ,
cache : this.spawnCommandCache ,
2026-03-02 01:31:23 +00:00
onResolved : ( event ) = > {
this . logSpawnResolution ( event ) ;
} ,
2026-03-01 23:56:58 +00:00
} ;
2026-02-26 11:00:09 +01:00
}
isHealthy ( ) : boolean {
return this . healthy ;
}
2026-03-02 01:31:23 +00:00
private logSpawnResolution ( event : SpawnResolutionEvent ) : void {
const key = ` ${ event . command } :: ${ event . strictWindowsCmdWrapper ? "strict" : "compat" } :: ${ event . resolution } ` ;
if ( event . cacheHit || this . loggedSpawnResolutions . has ( key ) ) {
return ;
}
this . loggedSpawnResolutions . add ( key ) ;
this . logger ? . debug ? . (
` acpx spawn resolver: command= ${ event . command } mode= ${ event . strictWindowsCmdWrapper ? "strict" : "compat" } resolution= ${ event . resolution } ` ,
) ;
}
2026-03-13 16:24:58 +00:00
private async checkVersion ( ) : Promise < AcpxVersionCheckResult > {
return await checkAcpxVersion ( {
2026-02-26 11:00:09 +01:00
command : this.config.command ,
cwd : this.config.cwd ,
2026-02-28 10:37:02 +01:00
expectedVersion : this.config.expectedVersion ,
2026-03-10 18:50:10 -03:00
stripProviderAuthEnvVars : this.config.stripProviderAuthEnvVars ,
2026-03-01 23:56:58 +00:00
spawnOptions : this.spawnCommandOptions ,
2026-02-26 11:00:09 +01:00
} ) ;
2026-03-13 16:24:58 +00:00
}
private async runHelpCheck ( ) : Promise < Awaited < ReturnType < typeof spawnAndCollect > > > {
return await spawnAndCollect (
{
command : this.config.command ,
args : [ "--help" ] ,
cwd : this.config.cwd ,
stripProviderAuthEnvVars : this.config.stripProviderAuthEnvVars ,
} ,
this . spawnCommandOptions ,
) ;
}
private async checkHealth ( ) : Promise < AcpxHealthCheckResult > {
const versionCheck = await this . checkVersion ( ) ;
2026-02-26 11:00:09 +01:00
if ( ! versionCheck . ok ) {
2026-03-13 16:24:58 +00:00
return {
ok : false ,
failure : {
kind : "version-check" ,
versionCheck ,
} ,
} ;
2026-02-26 11:00:09 +01:00
}
try {
2026-03-13 16:24:58 +00:00
const result = await this . runHelpCheck ( ) ;
if ( result . error != null || ( result . code ? ? 0 ) !== 0 ) {
return {
ok : false ,
failure : {
kind : "help-check" ,
result ,
} ,
} ;
}
return {
ok : true ,
versionCheck ,
} ;
} catch ( error ) {
return {
ok : false ,
failure : {
kind : "exception" ,
error ,
2026-03-01 23:56:58 +00:00
} ,
2026-03-13 16:24:58 +00:00
} ;
2026-02-26 11:00:09 +01:00
}
}
2026-03-13 16:24:58 +00:00
async probeAvailability ( ) : Promise < void > {
const result = await this . checkHealth ( ) ;
this . healthy = result . ok ;
}
2026-03-17 17:27:52 +01:00
private async createNamedSession ( params : {
agent : string ;
cwd : string ;
sessionName : string ;
resumeSessionId? : string ;
} ) : Promise < AcpxJsonObject [ ] > {
const command = params . resumeSessionId
? [
"sessions" ,
"new" ,
"--name" ,
params . sessionName ,
"--resume-session" ,
params . resumeSessionId ,
]
: [ "sessions" , "new" , "--name" , params . sessionName ] ;
return await this . runControlCommand ( {
args : await this . buildVerbArgs ( {
agent : params.agent ,
cwd : params.cwd ,
command ,
} ) ,
cwd : params.cwd ,
fallbackCode : "ACP_SESSION_INIT_FAILED" ,
} ) ;
}
private async shouldReplaceEnsuredSession ( params : {
sessionName : string ;
agent : string ;
cwd : string ;
} ) : Promise < boolean > {
const args = await this . buildVerbArgs ( {
agent : params.agent ,
cwd : params.cwd ,
command : [ "status" , "--session" , params . sessionName ] ,
} ) ;
let events : AcpxJsonObject [ ] ;
try {
events = await this . runControlCommand ( {
args ,
cwd : params.cwd ,
fallbackCode : "ACP_SESSION_INIT_FAILED" ,
ignoreNoSession : true ,
} ) ;
} catch ( error ) {
this . logger ? . warn ? . (
` acpx ensureSession status probe failed: session= ${ params . sessionName } cwd= ${ params . cwd } error= ${ summarizeLogText ( error instanceof Error ? error.message : String ( error ) ) || "<empty>" } ` ,
) ;
return false ;
}
const noSession = events . some ( ( event ) = > toAcpxErrorEvent ( event ) ? . code === "NO_SESSION" ) ;
if ( noSession ) {
this . logger ? . warn ? . (
` acpx ensureSession replacing missing named session: session= ${ params . sessionName } cwd= ${ params . cwd } ` ,
) ;
return true ;
}
const detail = events . find ( ( event ) = > ! toAcpxErrorEvent ( event ) ) ;
const status = asTrimmedString ( detail ? . status ) ? . toLowerCase ( ) ;
if ( status === "dead" ) {
const summary = summarizeLogText ( asOptionalString ( detail ? . summary ) ? ? "" ) ;
this . logger ? . warn ? . (
` acpx ensureSession replacing dead named session: session= ${ params . sessionName } cwd= ${ params . cwd } status= ${ status } summary= ${ summary || "<empty>" } ` ,
) ;
return true ;
}
return false ;
}
private async recoverEnsureFailure ( params : {
sessionName : string ;
agent : string ;
cwd : string ;
error : unknown ;
} ) : Promise < AcpxJsonObject [ ] | null > {
const errorMessage = summarizeLogText (
params . error instanceof Error ? params.error.message : String ( params . error ) ,
) ;
this . logger ? . warn ? . (
` acpx ensureSession probing named session after ensure failure: session= ${ params . sessionName } cwd= ${ params . cwd } error= ${ errorMessage || "<empty>" } ` ,
) ;
const args = await this . buildVerbArgs ( {
agent : params.agent ,
cwd : params.cwd ,
command : [ "status" , "--session" , params . sessionName ] ,
} ) ;
let events : AcpxJsonObject [ ] ;
try {
events = await this . runControlCommand ( {
args ,
cwd : params.cwd ,
fallbackCode : "ACP_SESSION_INIT_FAILED" ,
ignoreNoSession : true ,
} ) ;
} catch ( statusError ) {
this . logger ? . warn ? . (
` acpx ensureSession status fallback failed: session= ${ params . sessionName } cwd= ${ params . cwd } error= ${ summarizeLogText ( statusError instanceof Error ? statusError.message : String ( statusError ) ) || "<empty>" } ` ,
) ;
return null ;
}
const noSession = events . some ( ( event ) = > toAcpxErrorEvent ( event ) ? . code === "NO_SESSION" ) ;
if ( noSession ) {
this . logger ? . warn ? . (
` acpx ensureSession creating named session after ensure failure and missing status: session= ${ params . sessionName } cwd= ${ params . cwd } ` ,
) ;
return await this . createNamedSession ( {
agent : params.agent ,
cwd : params.cwd ,
sessionName : params.sessionName ,
} ) ;
}
const detail = events . find ( ( event ) = > ! toAcpxErrorEvent ( event ) ) ;
const status = asTrimmedString ( detail ? . status ) ? . toLowerCase ( ) ;
if ( status === "dead" ) {
this . logger ? . warn ? . (
` acpx ensureSession replacing dead named session after ensure failure: session= ${ params . sessionName } cwd= ${ params . cwd } ` ,
) ;
return await this . createNamedSession ( {
agent : params.agent ,
cwd : params.cwd ,
sessionName : params.sessionName ,
} ) ;
}
if ( status === "alive" || findSessionIdentifierEvent ( events ) ) {
this . logger ? . warn ? . (
` acpx ensureSession reusing live named session after ensure failure: session= ${ params . sessionName } cwd= ${ params . cwd } status= ${ status || "unknown" } ` ,
) ;
return events ;
}
return null ;
}
2026-02-26 11:00:09 +01:00
async ensureSession ( input : AcpRuntimeEnsureInput ) : Promise < AcpRuntimeHandle > {
const sessionName = asTrimmedString ( input . sessionKey ) ;
if ( ! sessionName ) {
throw new AcpRuntimeError ( "ACP_SESSION_INIT_FAILED" , "ACP session key is required." ) ;
}
const agent = asTrimmedString ( input . agent ) ;
if ( ! agent ) {
throw new AcpRuntimeError ( "ACP_SESSION_INIT_FAILED" , "ACP agent id is required." ) ;
}
const cwd = asTrimmedString ( input . cwd ) || this . config . cwd ;
const mode = input . mode ;
2026-03-10 02:36:13 -07:00
const resumeSessionId = asTrimmedString ( input . resumeSessionId ) ;
2026-03-17 17:27:52 +01:00
let events : AcpxJsonObject [ ] ;
if ( resumeSessionId ) {
events = await this . createNamedSession ( {
agent ,
cwd ,
sessionName ,
resumeSessionId ,
} ) ;
} else {
try {
events = await this . runControlCommand ( {
args : await this . buildVerbArgs ( {
agent ,
cwd ,
command : [ "sessions" , "ensure" , "--name" , sessionName ] ,
} ) ,
cwd ,
fallbackCode : "ACP_SESSION_INIT_FAILED" ,
} ) ;
} catch ( error ) {
const recovered = await this . recoverEnsureFailure ( {
sessionName ,
agent ,
cwd ,
error ,
} ) ;
if ( ! recovered ) {
throw error ;
}
events = recovered ;
}
}
if ( events . length === 0 ) {
this . logger ? . warn ? . (
` acpx ensureSession returned no events after sessions ensure: session= ${ sessionName } agent= ${ agent } cwd= ${ cwd } ` ,
) ;
}
let ensuredEvent = findSessionIdentifierEvent ( events ) ;
2026-03-03 22:15:28 -08:00
2026-03-17 17:27:52 +01:00
if (
ensuredEvent &&
! resumeSessionId &&
( await this . shouldReplaceEnsuredSession ( {
sessionName ,
agent ,
cwd ,
} ) )
) {
events = await this . createNamedSession ( {
2026-03-08 03:15:30 +00:00
agent ,
cwd ,
2026-03-17 17:27:52 +01:00
sessionName ,
2026-03-08 03:15:30 +00:00
} ) ;
2026-03-17 17:27:52 +01:00
if ( events . length === 0 ) {
this . logger ? . warn ? . (
` acpx ensureSession returned no events after replacing dead session: session= ${ sessionName } agent= ${ agent } cwd= ${ cwd } ` ,
) ;
}
ensuredEvent = findSessionIdentifierEvent ( events ) ;
}
if ( ! ensuredEvent && ! resumeSessionId ) {
events = await this . createNamedSession ( {
agent ,
2026-03-03 22:15:28 -08:00
cwd ,
2026-03-17 17:27:52 +01:00
sessionName ,
2026-03-03 22:15:28 -08:00
} ) ;
2026-03-17 17:27:52 +01:00
if ( events . length === 0 ) {
this . logger ? . warn ? . (
` acpx ensureSession returned no events after sessions new: session= ${ sessionName } agent= ${ agent } cwd= ${ cwd } ` ,
) ;
}
ensuredEvent = findSessionIdentifierEvent ( events ) ;
2026-03-10 02:36:13 -07:00
}
if ( ! ensuredEvent ) {
throw new AcpRuntimeError (
"ACP_SESSION_INIT_FAILED" ,
resumeSessionId
? ` ACP session init failed: 'sessions new --resume-session' returned no session identifiers for ${ sessionName } . `
: ` ACP session init failed: neither 'sessions ensure' nor 'sessions new' returned valid session identifiers for ${ sessionName } . ` ,
) ;
2026-03-03 22:15:28 -08:00
}
2026-02-26 11:00:09 +01:00
const acpxRecordId = ensuredEvent ? asOptionalString ( ensuredEvent . acpxRecordId ) : undefined ;
const agentSessionId = ensuredEvent ? asOptionalString ( ensuredEvent . agentSessionId ) : undefined ;
const backendSessionId = ensuredEvent
? asOptionalString ( ensuredEvent . acpxSessionId )
: undefined ;
return {
sessionKey : input.sessionKey ,
backend : ACPX_BACKEND_ID ,
runtimeSessionName : encodeAcpxRuntimeHandleState ( {
name : sessionName ,
agent ,
cwd ,
mode ,
. . . ( acpxRecordId ? { acpxRecordId } : { } ) ,
. . . ( backendSessionId ? { backendSessionId } : { } ) ,
. . . ( agentSessionId ? { agentSessionId } : { } ) ,
} ) ,
cwd ,
. . . ( acpxRecordId ? { acpxRecordId } : { } ) ,
. . . ( backendSessionId ? { backendSessionId } : { } ) ,
. . . ( agentSessionId ? { agentSessionId } : { } ) ,
} ;
}
async * runTurn ( input : AcpRuntimeTurnInput ) : AsyncIterable < AcpRuntimeEvent > {
const state = this . resolveHandleState ( input . handle ) ;
2026-03-08 03:15:30 +00:00
const args = await this . buildPromptArgs ( {
2026-02-26 11:00:09 +01:00
agent : state.agent ,
sessionName : state.name ,
cwd : state.cwd ,
} ) ;
const cancelOnAbort = async ( ) = > {
await this . cancel ( {
handle : input.handle ,
reason : "abort-signal" ,
} ) . catch ( ( err ) = > {
this . logger ? . warn ? . ( ` acpx runtime abort-cancel failed: ${ String ( err ) } ` ) ;
} ) ;
} ;
const onAbort = ( ) = > {
void cancelOnAbort ( ) ;
} ;
if ( input . signal ? . aborted ) {
await cancelOnAbort ( ) ;
return ;
}
if ( input . signal ) {
input . signal . addEventListener ( "abort" , onAbort , { once : true } ) ;
}
2026-03-01 23:56:58 +00:00
const child = spawnWithResolvedCommand (
{
command : this.config.command ,
args ,
cwd : state.cwd ,
2026-03-10 18:50:10 -03:00
stripProviderAuthEnvVars : this.config.stripProviderAuthEnvVars ,
2026-03-01 23:56:58 +00:00
} ,
this . spawnCommandOptions ,
) ;
2026-02-26 11:00:09 +01:00
child . stdin . on ( "error" , ( ) = > {
// Ignore EPIPE when the child exits before stdin flush completes.
} ) ;
2026-03-09 22:32:32 +01:00
if ( input . attachments && input . attachments . length > 0 ) {
const blocks : unknown [ ] = [ ] ;
if ( input . text ) {
blocks . push ( { type : "text" , text : input.text } ) ;
}
for ( const attachment of input . attachments ) {
if ( attachment . mediaType . startsWith ( "image/" ) ) {
blocks . push ( { type : "image" , mimeType : attachment.mediaType , data : attachment.data } ) ;
}
}
child . stdin . end ( blocks . length > 0 ? JSON . stringify ( blocks ) : input . text ) ;
} else {
child . stdin . end ( input . text ) ;
}
2026-02-26 11:00:09 +01:00
let stderr = "" ;
child . stderr . on ( "data" , ( chunk ) = > {
stderr += String ( chunk ) ;
} ) ;
let sawDone = false ;
let sawError = false ;
const lines = createInterface ( { input : child.stdout } ) ;
try {
for await ( const line of lines ) {
2026-03-01 09:31:41 +01:00
const parsed = parsePromptEventLine ( line ) ;
2026-02-26 11:00:09 +01:00
if ( ! parsed ) {
continue ;
}
if ( parsed . type === "done" ) {
2026-02-28 15:15:28 +01:00
if ( sawDone ) {
continue ;
}
2026-02-26 11:00:09 +01:00
sawDone = true ;
}
if ( parsed . type === "error" ) {
sawError = true ;
}
yield parsed ;
}
const exit = await waitForExit ( child ) ;
if ( exit . error ) {
const spawnFailure = resolveSpawnFailure ( exit . error , state . cwd ) ;
if ( spawnFailure === "missing-command" ) {
this . healthy = false ;
throw new AcpRuntimeError (
"ACP_BACKEND_UNAVAILABLE" ,
` acpx command not found: ${ this . config . command } ` ,
{ cause : exit.error } ,
) ;
}
if ( spawnFailure === "missing-cwd" ) {
throw new AcpRuntimeError (
"ACP_TURN_FAILED" ,
` ACP runtime working directory does not exist: ${ state . cwd } ` ,
{ cause : exit.error } ,
) ;
}
throw new AcpRuntimeError ( "ACP_TURN_FAILED" , exit . error . message , { cause : exit.error } ) ;
}
if ( ( exit . code ? ? 0 ) !== 0 && ! sawError ) {
yield {
type : "error" ,
2026-03-05 20:17:50 -05:00
message : formatAcpxExitMessage ( {
stderr ,
exitCode : exit.code ,
} ) ,
2026-02-26 11:00:09 +01:00
} ;
return ;
}
if ( ! sawDone && ! sawError ) {
yield { type : "done" } ;
}
} finally {
lines . close ( ) ;
if ( input . signal ) {
input . signal . removeEventListener ( "abort" , onAbort ) ;
}
}
}
getCapabilities ( ) : AcpRuntimeCapabilities {
return ACPX_CAPABILITIES ;
}
2026-03-04 10:52:28 +01:00
async getStatus ( input : {
handle : AcpRuntimeHandle ;
signal? : AbortSignal ;
} ) : Promise < AcpRuntimeStatus > {
2026-02-26 11:00:09 +01:00
const state = this . resolveHandleState ( input . handle ) ;
2026-03-08 03:15:30 +00:00
const args = await this . buildVerbArgs ( {
agent : state.agent ,
cwd : state.cwd ,
command : [ "status" , "--session" , state . name ] ,
} ) ;
2026-02-26 11:00:09 +01:00
const events = await this . runControlCommand ( {
2026-03-08 03:15:30 +00:00
args ,
2026-02-26 11:00:09 +01:00
cwd : state.cwd ,
fallbackCode : "ACP_TURN_FAILED" ,
ignoreNoSession : true ,
2026-03-04 10:52:28 +01:00
signal : input.signal ,
2026-02-26 11:00:09 +01:00
} ) ;
2026-03-01 09:31:41 +01:00
const detail = events . find ( ( event ) = > ! toAcpxErrorEvent ( event ) ) ? ? events [ 0 ] ;
2026-02-26 11:00:09 +01:00
if ( ! detail ) {
return {
summary : "acpx status unavailable" ,
} ;
}
const status = asTrimmedString ( detail . status ) || "unknown" ;
const acpxRecordId = asOptionalString ( detail . acpxRecordId ) ;
const acpxSessionId = asOptionalString ( detail . acpxSessionId ) ;
const agentSessionId = asOptionalString ( detail . agentSessionId ) ;
const pid = typeof detail . pid === "number" && Number . isFinite ( detail . pid ) ? detail.pid : null ;
const summary = [
` status= ${ status } ` ,
acpxRecordId ? ` acpxRecordId= ${ acpxRecordId } ` : null ,
acpxSessionId ? ` acpxSessionId= ${ acpxSessionId } ` : null ,
pid != null ? ` pid= ${ pid } ` : null ,
]
. filter ( Boolean )
. join ( " " ) ;
return {
summary ,
. . . ( acpxRecordId ? { acpxRecordId } : { } ) ,
. . . ( acpxSessionId ? { backendSessionId : acpxSessionId } : { } ) ,
. . . ( agentSessionId ? { agentSessionId } : { } ) ,
details : detail ,
} ;
}
async setMode ( input : { handle : AcpRuntimeHandle ; mode : string } ) : Promise < void > {
const state = this . resolveHandleState ( input . handle ) ;
const mode = asTrimmedString ( input . mode ) ;
if ( ! mode ) {
throw new AcpRuntimeError ( "ACP_TURN_FAILED" , "ACP runtime mode is required." ) ;
}
2026-03-08 03:15:30 +00:00
const args = await this . buildVerbArgs ( {
agent : state.agent ,
cwd : state.cwd ,
command : [ "set-mode" , mode , "--session" , state . name ] ,
} ) ;
2026-02-26 11:00:09 +01:00
await this . runControlCommand ( {
2026-03-08 03:15:30 +00:00
args ,
2026-02-26 11:00:09 +01:00
cwd : state.cwd ,
fallbackCode : "ACP_TURN_FAILED" ,
} ) ;
}
async setConfigOption ( input : {
handle : AcpRuntimeHandle ;
key : string ;
value : string ;
} ) : Promise < void > {
const state = this . resolveHandleState ( input . handle ) ;
const key = asTrimmedString ( input . key ) ;
const value = asTrimmedString ( input . value ) ;
if ( ! key || ! value ) {
throw new AcpRuntimeError ( "ACP_TURN_FAILED" , "ACP config option key/value are required." ) ;
}
2026-03-08 03:15:30 +00:00
const args = await this . buildVerbArgs ( {
agent : state.agent ,
cwd : state.cwd ,
command : [ "set" , key , value , "--session" , state . name ] ,
} ) ;
2026-02-26 11:00:09 +01:00
await this . runControlCommand ( {
2026-03-08 03:15:30 +00:00
args ,
2026-02-26 11:00:09 +01:00
cwd : state.cwd ,
fallbackCode : "ACP_TURN_FAILED" ,
} ) ;
}
async doctor ( ) : Promise < AcpRuntimeDoctorReport > {
2026-03-13 16:24:58 +00:00
const result = await this . checkHealth ( ) ;
if ( ! result . ok && result . failure . kind === "version-check" ) {
const { versionCheck } = result . failure ;
2026-02-26 11:00:09 +01:00
this . healthy = false ;
const details = [
2026-02-28 10:37:02 +01:00
versionCheck . expectedVersion ? ` expected= ${ versionCheck . expectedVersion } ` : null ,
2026-02-26 11:00:09 +01:00
versionCheck . installedVersion ? ` installed= ${ versionCheck . installedVersion } ` : null ,
] . filter ( ( detail ) : detail is string = > Boolean ( detail ) ) ;
return {
ok : false ,
code : "ACP_BACKEND_UNAVAILABLE" ,
message : versionCheck.message ,
installCommand : versionCheck.installCommand ,
details ,
} ;
}
2026-03-13 16:24:58 +00:00
if ( ! result . ok && result . failure . kind === "help-check" ) {
const { result : helpResult } = result . failure ;
this . healthy = false ;
if ( helpResult . error ) {
const spawnFailure = resolveSpawnFailure ( helpResult . error , this . config . cwd ) ;
2026-02-26 11:00:09 +01:00
if ( spawnFailure === "missing-command" ) {
return {
ok : false ,
code : "ACP_BACKEND_UNAVAILABLE" ,
message : ` acpx command not found: ${ this . config . command } ` ,
2026-02-28 10:37:02 +01:00
installCommand : this.config.installCommand ,
2026-02-26 11:00:09 +01:00
} ;
}
if ( spawnFailure === "missing-cwd" ) {
return {
ok : false ,
code : "ACP_BACKEND_UNAVAILABLE" ,
message : ` ACP runtime working directory does not exist: ${ this . config . cwd } ` ,
} ;
}
return {
ok : false ,
code : "ACP_BACKEND_UNAVAILABLE" ,
2026-03-13 16:24:58 +00:00
message : helpResult.error.message ,
details : [ String ( helpResult . error ) ] ,
2026-02-26 11:00:09 +01:00
} ;
}
return {
2026-03-13 16:24:58 +00:00
ok : false ,
code : "ACP_BACKEND_UNAVAILABLE" ,
message :
helpResult . stderr . trim ( ) || ` acpx exited with code ${ helpResult . code ? ? "unknown" } ` ,
2026-02-26 11:00:09 +01:00
} ;
2026-03-13 16:24:58 +00:00
}
if ( ! result . ok ) {
2026-02-26 11:00:09 +01:00
this . healthy = false ;
2026-03-13 16:28:31 +00:00
const failure = result . failure ;
2026-02-26 11:00:09 +01:00
return {
ok : false ,
code : "ACP_BACKEND_UNAVAILABLE" ,
2026-03-13 16:24:58 +00:00
message :
2026-03-13 16:28:31 +00:00
failure . kind === "exception"
? failure . error instanceof Error
? failure . error . message
: String ( failure . error )
: "acpx backend unavailable" ,
2026-02-26 11:00:09 +01:00
} ;
}
2026-03-13 16:24:58 +00:00
this . healthy = true ;
return {
ok : true ,
message : ` acpx command available ( ${ this . config . command } , version ${ result . versionCheck . version } ${ this . config . expectedVersion ? ` , expected ${ this . config . expectedVersion } ` : "" } ) ` ,
} ;
2026-02-26 11:00:09 +01:00
}
async cancel ( input : { handle : AcpRuntimeHandle ; reason? : string } ) : Promise < void > {
const state = this . resolveHandleState ( input . handle ) ;
2026-03-08 03:15:30 +00:00
const args = await this . buildVerbArgs ( {
agent : state.agent ,
cwd : state.cwd ,
command : [ "cancel" , "--session" , state . name ] ,
} ) ;
2026-02-26 11:00:09 +01:00
await this . runControlCommand ( {
2026-03-08 03:15:30 +00:00
args ,
2026-02-26 11:00:09 +01:00
cwd : state.cwd ,
fallbackCode : "ACP_TURN_FAILED" ,
ignoreNoSession : true ,
} ) ;
}
async close ( input : { handle : AcpRuntimeHandle ; reason : string } ) : Promise < void > {
const state = this . resolveHandleState ( input . handle ) ;
2026-03-08 03:15:30 +00:00
const args = await this . buildVerbArgs ( {
agent : state.agent ,
cwd : state.cwd ,
command : [ "sessions" , "close" , state . name ] ,
} ) ;
2026-02-26 11:00:09 +01:00
await this . runControlCommand ( {
2026-03-08 03:15:30 +00:00
args ,
2026-02-26 11:00:09 +01:00
cwd : state.cwd ,
fallbackCode : "ACP_TURN_FAILED" ,
ignoreNoSession : true ,
} ) ;
}
private resolveHandleState ( handle : AcpRuntimeHandle ) : AcpxHandleState {
const decoded = decodeAcpxRuntimeHandleState ( handle . runtimeSessionName ) ;
if ( decoded ) {
return decoded ;
}
const legacyName = asTrimmedString ( handle . runtimeSessionName ) ;
if ( ! legacyName ) {
throw new AcpRuntimeError (
"ACP_SESSION_INIT_FAILED" ,
"Invalid acpx runtime handle: runtimeSessionName is missing." ,
) ;
}
return {
name : legacyName ,
agent : deriveAgentFromSessionKey ( handle . sessionKey , DEFAULT_AGENT_FALLBACK ) ,
cwd : this.config.cwd ,
mode : "persistent" ,
} ;
}
2026-03-08 03:15:30 +00:00
private async buildPromptArgs ( params : {
agent : string ;
sessionName : string ;
cwd : string ;
} ) : Promise < string [ ] > {
const prefix = [
2026-02-26 11:00:09 +01:00
"--format" ,
"json" ,
"--json-strict" ,
"--cwd" ,
params . cwd ,
. . . buildPermissionArgs ( this . config . permissionMode ) ,
"--non-interactive-permissions" ,
this . config . nonInteractivePermissions ,
] ;
if ( this . config . timeoutSeconds ) {
2026-03-08 03:15:30 +00:00
prefix . push ( "--timeout" , String ( this . config . timeoutSeconds ) ) ;
}
prefix . push ( "--ttl" , String ( this . queueOwnerTtlSeconds ) ) ;
return await this . buildVerbArgs ( {
agent : params.agent ,
cwd : params.cwd ,
command : [ "prompt" , "--session" , params . sessionName , "--file" , "-" ] ,
prefix ,
} ) ;
}
private async buildVerbArgs ( params : {
agent : string ;
cwd : string ;
command : string [ ] ;
prefix? : string [ ] ;
} ) : Promise < string [ ] > {
const prefix = params . prefix ? ? [ "--format" , "json" , "--json-strict" , "--cwd" , params . cwd ] ;
const agentCommand = await this . resolveRawAgentCommand ( {
agent : params.agent ,
cwd : params.cwd ,
} ) ;
if ( ! agentCommand ) {
return [ . . . prefix , params . agent , . . . params . command ] ;
2026-02-26 11:00:09 +01:00
}
2026-03-08 03:15:30 +00:00
return [ . . . prefix , "--agent" , agentCommand , . . . params . command ] ;
}
private async resolveRawAgentCommand ( params : {
agent : string ;
cwd : string ;
} ) : Promise < string | null > {
if ( Object . keys ( this . config . mcpServers ) . length === 0 ) {
return null ;
}
const cacheKey = ` ${ params . cwd } :: ${ params . agent } ` ;
const cached = this . mcpProxyAgentCommandCache . get ( cacheKey ) ;
if ( cached ) {
return cached ;
}
const targetCommand = await resolveAcpxAgentCommand ( {
acpxCommand : this.config.command ,
cwd : params.cwd ,
agent : params.agent ,
2026-03-10 18:50:10 -03:00
stripProviderAuthEnvVars : this.config.stripProviderAuthEnvVars ,
2026-03-08 03:15:30 +00:00
spawnOptions : this.spawnCommandOptions ,
} ) ;
const resolved = buildMcpProxyAgentCommand ( {
targetCommand ,
mcpServers : toAcpMcpServers ( this . config . mcpServers ) ,
} ) ;
this . mcpProxyAgentCommandCache . set ( cacheKey , resolved ) ;
return resolved ;
2026-02-26 11:00:09 +01:00
}
private async runControlCommand ( params : {
args : string [ ] ;
cwd : string ;
fallbackCode : AcpRuntimeErrorCode ;
ignoreNoSession? : boolean ;
2026-03-04 10:52:28 +01:00
signal? : AbortSignal ;
2026-02-26 11:00:09 +01:00
} ) : Promise < AcpxJsonObject [ ] > {
2026-03-01 23:56:58 +00:00
const result = await spawnAndCollect (
{
command : this.config.command ,
args : params.args ,
cwd : params.cwd ,
2026-03-10 18:50:10 -03:00
stripProviderAuthEnvVars : this.config.stripProviderAuthEnvVars ,
2026-03-01 23:56:58 +00:00
} ,
this . spawnCommandOptions ,
2026-03-04 10:52:28 +01:00
{
signal : params.signal ,
} ,
2026-03-01 23:56:58 +00:00
) ;
2026-02-26 11:00:09 +01:00
if ( result . error ) {
const spawnFailure = resolveSpawnFailure ( result . error , params . cwd ) ;
if ( spawnFailure === "missing-command" ) {
this . healthy = false ;
throw new AcpRuntimeError (
"ACP_BACKEND_UNAVAILABLE" ,
` acpx command not found: ${ this . config . command } ` ,
{ cause : result.error } ,
) ;
}
if ( spawnFailure === "missing-cwd" ) {
throw new AcpRuntimeError (
params . fallbackCode ,
` ACP runtime working directory does not exist: ${ params . cwd } ` ,
{ cause : result.error } ,
) ;
}
throw new AcpRuntimeError ( params . fallbackCode , result . error . message , { cause : result.error } ) ;
}
const events = parseJsonLines ( result . stdout ) ;
2026-03-01 09:31:41 +01:00
const errorEvent = events . map ( ( event ) = > toAcpxErrorEvent ( event ) ) . find ( Boolean ) ? ? null ;
2026-02-26 11:00:09 +01:00
if ( errorEvent ) {
if ( params . ignoreNoSession && errorEvent . code === "NO_SESSION" ) {
return events ;
}
throw new AcpRuntimeError (
params . fallbackCode ,
errorEvent . code ? ` ${ errorEvent . code } : ${ errorEvent . message } ` : errorEvent . message ,
) ;
}
if ( ( result . code ? ? 0 ) !== 0 ) {
throw new AcpRuntimeError (
params . fallbackCode ,
2026-03-05 20:17:50 -05:00
formatAcpxExitMessage ( {
stderr : result.stderr ,
exitCode : result.code ,
} ) ,
2026-02-26 11:00:09 +01:00
) ;
}
return events ;
}
}