mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 00:03:08 +00:00
fix(remote): deliver stdin-after-start prompt to interpreter-wrapped agents (aider/vibe)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { sendFollowupPromptWhenAgentReady } from './agent-followup-delivery'
|
||||
import {
|
||||
inspectRuntimeTerminalProcess,
|
||||
sendRuntimePtyInputVerified
|
||||
} from '@/runtime/runtime-terminal-inspection'
|
||||
import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config'
|
||||
|
||||
vi.mock('@/runtime/runtime-terminal-inspection', () => ({
|
||||
inspectRuntimeTerminalProcess: vi.fn(),
|
||||
sendRuntimePtyInputVerified: vi.fn()
|
||||
}))
|
||||
|
||||
// The interpreter-wrapped agents that deliver their prompt over stdin after the
|
||||
// process starts. These are pip console-scripts, so the PTY foreground comm is
|
||||
// python/python3 — never the agent's own name.
|
||||
const INTERPRETER_WRAPPED_AGENTS = [
|
||||
{ agent: 'aider', expectedProcess: TUI_AGENT_CONFIG.aider.expectedProcess },
|
||||
{ agent: 'mistral-vibe', expectedProcess: TUI_AGENT_CONFIG['mistral-vibe'].expectedProcess }
|
||||
] as const
|
||||
|
||||
describe('sendFollowupPromptWhenAgentReady — interpreter-wrapped agents', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.stubGlobal('globalThis', globalThis)
|
||||
// Deliver the prompt write eagerly so the test does not depend on retries.
|
||||
vi.mocked(sendRuntimePtyInputVerified).mockResolvedValue(true)
|
||||
})
|
||||
|
||||
it('sanity: config keeps aider/vibe as stdin-after-start with python-style expected process', () => {
|
||||
expect(TUI_AGENT_CONFIG.aider.promptInjectionMode).toBe('stdin-after-start')
|
||||
expect(TUI_AGENT_CONFIG['mistral-vibe'].promptInjectionMode).toBe('stdin-after-start')
|
||||
})
|
||||
|
||||
for (const { agent, expectedProcess } of INTERPRETER_WRAPPED_AGENTS) {
|
||||
it(`types the prompt once ${agent} is up behind a python3 wrapper with a live child`, async () => {
|
||||
// The console-script agent is running: foreground comm is python3 and the
|
||||
// PTY has a non-shell child. The exact agent name never appears.
|
||||
vi.mocked(inspectRuntimeTerminalProcess).mockResolvedValue({
|
||||
foregroundProcess: 'python3',
|
||||
hasChildProcesses: true
|
||||
})
|
||||
|
||||
const delivered = await sendFollowupPromptWhenAgentReady({
|
||||
ptyId: 'pty-1',
|
||||
expectedProcess,
|
||||
prompt: 'ship it',
|
||||
settings: null
|
||||
})
|
||||
|
||||
expect(delivered).toBe(true)
|
||||
expect(sendRuntimePtyInputVerified).toHaveBeenCalledWith(null, 'pty-1', 'ship it\r')
|
||||
})
|
||||
|
||||
it(`still refuses to type into a bare ${agent} shell foreground`, async () => {
|
||||
// No agent yet: foreground is a plain shell with no non-shell child. The
|
||||
// guard must NOT write user text into an arbitrary shell.
|
||||
vi.mocked(inspectRuntimeTerminalProcess).mockResolvedValue({
|
||||
foregroundProcess: 'zsh',
|
||||
hasChildProcesses: false
|
||||
})
|
||||
|
||||
const delivered = await sendFollowupPromptWhenAgentReady({
|
||||
ptyId: 'pty-1',
|
||||
expectedProcess,
|
||||
prompt: 'ship it',
|
||||
settings: null
|
||||
})
|
||||
|
||||
expect(delivered).toBe(false)
|
||||
expect(sendRuntimePtyInputVerified).not.toHaveBeenCalled()
|
||||
})
|
||||
}
|
||||
|
||||
it('types immediately when the resolver already returns the agent name (local ps path)', async () => {
|
||||
// On local desktop the ps-table resolver usually resolves python3 → aider
|
||||
// before we poll; the exact-match path must keep working.
|
||||
vi.mocked(inspectRuntimeTerminalProcess).mockResolvedValue({
|
||||
foregroundProcess: 'aider',
|
||||
hasChildProcesses: true
|
||||
})
|
||||
|
||||
const delivered = await sendFollowupPromptWhenAgentReady({
|
||||
ptyId: 'pty-1',
|
||||
expectedProcess: 'aider',
|
||||
prompt: 'ship it',
|
||||
settings: null
|
||||
})
|
||||
|
||||
expect(delivered).toBe(true)
|
||||
expect(sendRuntimePtyInputVerified).toHaveBeenCalledWith(null, 'pty-1', 'ship it\r')
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,11 @@ import {
|
||||
inspectRuntimeTerminalProcess,
|
||||
sendRuntimePtyInputVerified
|
||||
} from '@/runtime/runtime-terminal-inspection'
|
||||
import { isExpectedAgentProcess } from '../../../shared/agent-process-recognition'
|
||||
import {
|
||||
isAgentForegroundWrapperProcess,
|
||||
isExpectedAgentProcess
|
||||
} from '../../../shared/agent-process-recognition'
|
||||
import { isShellProcess } from '../../../shared/shell-process-detection'
|
||||
import type { GlobalSettings } from '../../../shared/types'
|
||||
|
||||
type RuntimeOwnerSettings = Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined
|
||||
@@ -25,7 +29,7 @@ export async function sendFollowupPromptWhenAgentReady(args: {
|
||||
}
|
||||
|
||||
// Why: delayed follow-ups must not type into an arbitrary shell. Require a
|
||||
// positive expected-process match before writing user/task text to the PTY.
|
||||
// positive readiness signal before writing user/task text to the PTY.
|
||||
async function waitForAgentForeground(
|
||||
ptyId: string,
|
||||
expectedProcess: string,
|
||||
@@ -41,6 +45,20 @@ async function waitForAgentForeground(
|
||||
if (isExpectedAgentProcess(foreground, expectedProcess)) {
|
||||
return true
|
||||
}
|
||||
// Why: interpreter-wrapped agents (aider, mistral-vibe are pip console
|
||||
// scripts) surface a python/node foreground comm, so the exact-name check
|
||||
// never matches — locally when the ps-table resolver can't pin the child,
|
||||
// and over SSH when the relay falls back to the bare interpreter name. If
|
||||
// the foreground is a known agent wrapper (not a shell) with a live
|
||||
// non-shell child, the agent has taken over the PTY and can accept input.
|
||||
if (
|
||||
attempt >= 4 &&
|
||||
isAgentForegroundWrapperProcess(foreground) &&
|
||||
!isShellProcess(foreground) &&
|
||||
process.hasChildProcesses
|
||||
) {
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
// Ignore transient PTY inspection failures and keep polling.
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user