mirror of
https://github.com/stablyai/orca.git
synced 2026-11-14 01:47:24 +00:00
133 lines
4.8 KiB
TypeScript
133 lines
4.8 KiB
TypeScript
import { homedir } from 'node:os'
|
|
import { resolve } from 'node:path'
|
|
import { spawnProcess } from '@orca/process-host'
|
|
import type { PipedProcessSpawner } from '@orca/process-host/process-spec'
|
|
import { isRecord } from '../../shared/agent-status/child-work/agent-status-child-work-value-guards'
|
|
import {
|
|
AcpAgentConnection,
|
|
type AcpAgentConnectionOptions
|
|
} from '../acp/process/acp-agent-connection'
|
|
import {
|
|
AcpAgentError,
|
|
AcpAuthRequiredError,
|
|
AcpSignInPromptError
|
|
} from '../acp/transport/acp-errors'
|
|
import type { AcpSessionStarted, AcpSessionStartOptions } from '../acp/protocol/acp-session-setup'
|
|
import type { PromptRequest } from '../acp/generated/acp-protocol.generated'
|
|
import { readOpenCodePromptFailure } from '../foreign-sqlite-readers/foreign-sqlite-reader-spawn'
|
|
import type { OpenCodePromptFailure } from '../foreign-sqlite-readers/opencode-prompt-failure-result'
|
|
import {
|
|
resolveProviderChildEnv,
|
|
type ProviderProcessLaunch
|
|
} from '../provider-process/provider-process-launch'
|
|
import { resolveOpenCodeDatabasePath } from './opencode-data-directory'
|
|
|
|
const ACCESS_REFUSALS = new Set(['FreeTierError', 'RegionError'])
|
|
|
|
function nativePromptError(failure: OpenCodePromptFailure, original: AcpAgentError): AcpAgentError {
|
|
const access =
|
|
failure.type === 'provider.auth' &&
|
|
failure.status === 403 &&
|
|
ACCESS_REFUSALS.has(failure.responseType ?? '')
|
|
const errorName = access ? 'provider.access' : failure.type
|
|
const message = (access ? failure.responseMessage : undefined) ?? failure.message
|
|
const data = {
|
|
...(isRecord(original.data) ? original.data : {}),
|
|
details: message,
|
|
errorName,
|
|
...(failure.status === undefined ? {} : { status: failure.status }),
|
|
...(failure.resetsAt === undefined ? {} : { resetsAt: failure.resetsAt })
|
|
}
|
|
return !access &&
|
|
(failure.status === 401 || ['provider.auth', 'provider.authentication'].includes(errorName))
|
|
? new AcpAuthRequiredError(
|
|
message,
|
|
data,
|
|
original instanceof AcpAuthRequiredError ? original.authMethods : undefined
|
|
)
|
|
: new AcpAgentError(-32603, message, data)
|
|
}
|
|
|
|
export function createOpenCodeAcpConnection(
|
|
launch: ProviderProcessLaunch,
|
|
options: AcpAgentConnectionOptions = {},
|
|
spawnImpl: PipedProcessSpawner = spawnProcess,
|
|
readFailure: typeof readOpenCodePromptFailure = readOpenCodePromptFailure
|
|
): AcpAgentConnection {
|
|
return new OpenCodeAcpConnection(launch, options, spawnImpl, readFailure)
|
|
}
|
|
|
|
class OpenCodeAcpConnection extends AcpAgentConnection {
|
|
private readonly dbPath: string | null
|
|
private providerSessionId: string | undefined
|
|
private lastRecoveredInput: string | undefined
|
|
private prompting = false
|
|
|
|
constructor(
|
|
launch: ProviderProcessLaunch,
|
|
options: AcpAgentConnectionOptions,
|
|
spawnImpl: PipedProcessSpawner,
|
|
private readonly readFailure: typeof readOpenCodePromptFailure
|
|
) {
|
|
const env = resolveProviderChildEnv(launch, process.env)
|
|
const home = (process.platform === 'win32' ? env.USERPROFILE : env.HOME) || homedir()
|
|
const selected = resolveOpenCodeDatabasePath(env, home)
|
|
const dbPath = selected === null ? null : resolve(launch.cwd ?? process.cwd(), selected)
|
|
super(launch, options, spawnImpl)
|
|
this.dbPath = dbPath
|
|
}
|
|
|
|
override async start(
|
|
options: AcpSessionStartOptions,
|
|
received?: (started: AcpSessionStarted) => void
|
|
) {
|
|
const started = await super.start(options, received)
|
|
this.providerSessionId = started.sessionId
|
|
return started
|
|
}
|
|
|
|
override async prompt(prompt: PromptRequest['prompt'], meta?: PromptRequest['_meta']) {
|
|
if (this.prompting) {
|
|
throw new Error('ACP prompt already in progress')
|
|
}
|
|
this.prompting = true
|
|
const startedAt = Date.now()
|
|
try {
|
|
return await super.prompt(prompt, meta)
|
|
} catch (error) {
|
|
if (
|
|
!(error instanceof AcpAgentError) ||
|
|
error instanceof AcpSignInPromptError ||
|
|
![-32000, -32603].includes(error.code) ||
|
|
!this.dbPath ||
|
|
!this.providerSessionId ||
|
|
this.closed
|
|
) {
|
|
throw error
|
|
}
|
|
const failedAt = Date.now()
|
|
let failure: OpenCodePromptFailure | null = null
|
|
try {
|
|
failure = (
|
|
await this.readFailure({
|
|
dbPath: this.dbPath,
|
|
sessionId: this.providerSessionId,
|
|
startedAt,
|
|
failedAt
|
|
})
|
|
).failure
|
|
} catch {
|
|
// Native history is best effort; never hide the ACP failure when it cannot be read.
|
|
}
|
|
// An exit while reading ends the connection even while its output still drains.
|
|
if (!failure || this.closed || this.exited || failure.inputId === this.lastRecoveredInput) {
|
|
throw error
|
|
}
|
|
this.lastRecoveredInput = failure.inputId
|
|
throw nativePromptError(failure, error)
|
|
} finally {
|
|
this.prompting = false
|
|
}
|
|
}
|
|
}
|