feat(connectors): pending prompts, answers and turn events behind experimentalConnectors (B3) (#32160)

This commit is contained in:
Jinwoo Hong
2026-10-23 15:31:49 -07:00
committed by GitHub
parent ec5cfbad37
commit 2c53a550ff
22 changed files with 1547 additions and 23 deletions
+7 -1
View File
@@ -212,7 +212,13 @@ export const HANDLER_GROUPS: readonly HandlerGroup[] = [
},
{
name: 'connector',
keys: ['connector start', 'connector send'],
keys: [
'connector start',
'connector send',
'connector prompt',
'connector answer',
'connector watch'
],
load: async () => (await import('./handlers/connector.js')).CONNECTOR_HANDLERS
},
{
+25 -1
View File
@@ -1,6 +1,11 @@
import { describe, expect, it } from 'vitest'
import { RuntimeClientError } from '../runtime-client'
import { callConnector, formatConnectorStart, formatDelivery } from './connector'
import {
callConnector,
formatConnectorStart,
formatDelivery,
formatPendingPrompt
} from './connector'
function refusingClient(code: string) {
return {
@@ -41,4 +46,23 @@ describe('orca connector', () => {
'refused: agent_asleep_in_window'
)
})
it('prints a prompt with its id and options, and an unknown kind as one to answer in Orca', () => {
expect(
formatPendingPrompt({
kind: 'approval',
promptId: 'p_1',
tool: 'Bash',
summary: 'ls',
options: [
{ id: 'allow_once', label: 'Allow once' },
{ id: 'reject_once', label: 'Reject' }
]
})
).toBe('approval Bash: ls\n prompt id: p_1\n allow_once: Allow once\n reject_once: Reject')
expect(formatPendingPrompt({ kind: 'free-text', promptId: 'p_2' })).toBe(
'unsupported (answer in Orca)\n prompt id: p_2'
)
expect(formatPendingPrompt(null)).toBe('no pending prompt')
})
})
+73 -8
View File
@@ -1,5 +1,9 @@
import type { CommandHandler, HandlerContext } from '../dispatch'
import { getOptionalStringFlag, getRequiredStringFlag } from '../flags'
import {
getOptionalNonNegativeIntegerFlag,
getOptionalStringFlag,
getRequiredStringFlag
} from '../flags'
import { printResult } from '../format'
import { RuntimeClientError } from '../runtime-client'
import { getRequiredWorktreeSelector } from '../selectors'
@@ -26,10 +30,11 @@ export function formatConnectorStart(initial: ConnectorDelivery): string {
export async function callConnector<TResult>(
client: Pick<HandlerContext['client'], 'call'>,
method: string,
params: Record<string, unknown>
params: Record<string, unknown>,
options?: { timeoutMs: number }
) {
try {
return await client.call<TResult>(method, params)
return await client.call<TResult>(method, params, options)
} catch (error) {
if (error instanceof RuntimeClientError && error.code === 'method_not_found') {
throw new RuntimeClientError(
@@ -41,12 +46,43 @@ export async function callConnector<TResult>(
}
}
function conversationParams(flags: HandlerContext['flags']) {
return {
source: 'dev',
externalId: getRequiredStringFlag(flags, 'conversation'),
deliveryId: getRequiredStringFlag(flags, 'delivery-id')
type ConnectorPrompt = {
kind: string
promptId: string
tool?: string
summary?: string
question?: string
options?: { id: string; label: string }[]
}
/** A kind this CLI does not know reads as one only Orca can answer, as the contract asks. */
export function formatPendingPrompt(prompt: ConnectorPrompt | null): string {
if (!prompt) {
return 'no pending prompt'
}
const known = prompt.kind === 'approval' || prompt.kind === 'question'
const head =
prompt.kind === 'approval'
? `approval ${prompt.tool}${prompt.summary ? `: ${prompt.summary}` : ''}`
: prompt.kind === 'question'
? `question: ${prompt.question}`
: 'unsupported (answer in Orca)'
const options = known ? (prompt.options ?? []) : []
return [
head,
` prompt id: ${prompt.promptId}`,
...options.map((option) => ` ${option.id}: ${option.label}`)
].join('\n')
}
const DEFAULT_WATCH_MS = 30_000
function conversation(flags: HandlerContext['flags']) {
return { source: 'dev', externalId: getRequiredStringFlag(flags, 'conversation') }
}
function conversationParams(flags: HandlerContext['flags']) {
return { ...conversation(flags), deliveryId: getRequiredStringFlag(flags, 'delivery-id') }
}
export const CONNECTOR_HANDLERS: Record<string, CommandHandler> = {
@@ -82,5 +118,34 @@ export const CONNECTOR_HANDLERS: Record<string, CommandHandler> = {
text: getRequiredStringFlag(flags, 'text')
})
printResult(result, json, formatDelivery)
},
'connector prompt': async ({ flags, client, json }) => {
const result = await callConnector<{ prompt: ConnectorPrompt | null }>(
client,
'connector.pendingPrompt',
conversation(flags)
)
printResult(result, json, (value) => formatPendingPrompt(value.prompt))
},
'connector answer': async ({ flags, client, json }) => {
const result = await callConnector<ConnectorDelivery>(client, 'connector.answer', {
...conversation(flags),
promptId: getRequiredStringFlag(flags, 'prompt-id'),
optionId: getRequiredStringFlag(flags, 'option')
})
printResult(result, json, formatDelivery)
},
'connector watch': async ({ flags, client, json }) => {
const forMs = getOptionalNonNegativeIntegerFlag(flags, 'for-ms') ?? DEFAULT_WATCH_MS
const result = await callConnector<{ snapshot: unknown; events: unknown[] }>(
client,
'connector.watch',
{ forMs },
// The host holds the call open for the whole window.
{ timeoutMs: forMs + 15_000 }
)
printResult(result, json, (value) =>
[value.snapshot, ...value.events].map((event) => JSON.stringify(event)).join('\n')
)
}
}
+33
View File
@@ -38,5 +38,38 @@ export const CONNECTOR_COMMAND_SPECS: CommandSpec[] = [
...CONNECTOR_NOTES,
'Held while the agent is asking for permission, and sent once it is answered.'
]
},
{
path: ['connector', 'prompt'],
hidden: true,
summary: "Show what a dev connector conversation's agent is waiting on",
usage: 'orca connector prompt --conversation <id> [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'conversation'],
notes: [
CONNECTOR_NOTES[0],
'An approval or a one-question single choice can be answered here; anything else reads unsupported, to answer in Orca.'
]
},
{
path: ['connector', 'answer'],
hidden: true,
summary: "Answer a dev connector conversation's pending prompt",
usage: 'orca connector answer --conversation <id> --prompt-id <id> --option <id> [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'conversation', 'prompt-id', 'option'],
notes: [
CONNECTOR_NOTES[0],
'Refused unless --prompt-id is the prompt still pending in the same agent process, so a late answer never types into whatever came next.'
]
},
{
path: ['connector', 'watch'],
hidden: true,
summary: 'Collect connector conversation events for a while',
usage: 'orca connector watch [--for-ms <ms>] [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'for-ms'],
notes: [
CONNECTOR_NOTES[0],
'Prints a snapshot of every linked conversation, then each event (linked, prompt, turn-started, turn-ended) seen in the window; --for-ms defaults to 30000.'
]
}
]