mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 08:03:12 +00:00
refactor(agent-hooks): home the open-tool-call event set with its siblings
Sits next to TOOL_PROGRESS_HOOK_EVENTS, the existing hook-event-name set read by a main-process transition guard, so the concept keeps one home. Adds pi/prime-agent coverage and drives the contrast pair over the real loopback hook listener, which is what stamps hookEventName for a locally launched agent.
This commit is contained in:
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AgentInterruptInputIntent } from '../../shared/agent-interrupt-intent'
|
||||
import type { EnrichedAgentHookEventPayload } from './server/server-types'
|
||||
import { AgentHookServer, _internals } from './server'
|
||||
import { PANE } from './server.test-fixtures'
|
||||
import { buildBody, PANE, postHookEvent } from './server.test-fixtures'
|
||||
|
||||
const { getCohortAtEmitMock, trackMock } = vi.hoisted(() => ({
|
||||
getCohortAtEmitMock: vi.fn(),
|
||||
@@ -156,6 +156,25 @@ describe('navigation Escape during an open tool call', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
['pi', 'tool_call'],
|
||||
['prime-agent', 'tool_execution_start']
|
||||
])('leaves %s work running when Escape closes an overlay mid-%s', (agentType, hookEventName) => {
|
||||
const server = new AgentHookServer()
|
||||
ingest(server, {
|
||||
source: agentType,
|
||||
hookEventName,
|
||||
state: 'working',
|
||||
prompt: 'refactor the parser',
|
||||
agentType,
|
||||
toolName: 'shell'
|
||||
})
|
||||
|
||||
vi.setSystemTime(1_200)
|
||||
expect(pressInterruptKey(server, 'plain-escape')).toBe(false)
|
||||
expect(server.getStatusSnapshotForPane(PANE)[0]).toMatchObject({ state: 'working' })
|
||||
})
|
||||
|
||||
it('still infers a Claude Escape once the tool call has closed', () => {
|
||||
const server = new AgentHookServer()
|
||||
ingest(server, {
|
||||
@@ -305,3 +324,47 @@ describe('navigation Escape during an open tool call', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/** The guard has to hold on the path a real agent CLI uses, not only on ingestRemote: the
|
||||
* loopback listener is what stamps `hookEventName` for a locally launched agent. */
|
||||
describe('navigation Escape over the loopback hook listener', () => {
|
||||
it.each([
|
||||
['PreToolUse', false, 'working'],
|
||||
['PostToolUse', true, 'done']
|
||||
])(
|
||||
'a Claude row last seen on %s answers %s to a single Escape',
|
||||
async (hookEventName, expectedInference, expectedState) => {
|
||||
vi.useRealTimers()
|
||||
const server = new AgentHookServer()
|
||||
await server.start({ env: 'production' })
|
||||
try {
|
||||
await postHookEvent(
|
||||
server,
|
||||
buildBody({ hook_event_name: 'UserPromptSubmit', prompt: 'migrate the schema' })
|
||||
)
|
||||
await postHookEvent(
|
||||
server,
|
||||
buildBody({
|
||||
hook_event_name: 'PreToolUse',
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'pnpm migrate' }
|
||||
})
|
||||
)
|
||||
if (hookEventName === 'PostToolUse') {
|
||||
await postHookEvent(
|
||||
server,
|
||||
buildBody({ hook_event_name: 'PostToolUse', tool_name: 'Bash' })
|
||||
)
|
||||
}
|
||||
|
||||
expect(pressInterruptKey(server, 'plain-escape')).toBe(expectedInference)
|
||||
expect(server.getStatusSnapshotForPane(PANE)[0]).toMatchObject({
|
||||
state: expectedState,
|
||||
...(expectedInference ? { interrupted: true } : {})
|
||||
})
|
||||
} finally {
|
||||
server.stop()
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -17,6 +17,18 @@ export const TOOL_PROGRESS_HOOK_EVENTS = new Set([
|
||||
'PostToolUse',
|
||||
'PostToolUseFailure'
|
||||
])
|
||||
|
||||
// Why: the events that OPEN a provider tool call, for the agent types whose TUI also binds a bare
|
||||
// Escape to navigation. A row still sitting on one of these has not seen its closing event
|
||||
// (PostToolUse / tool_execution_end), so hook silence there is the tool running, not an interrupt.
|
||||
// Closed by construction: Claude ships PreToolUse as its only tool-opening hook, and Orca itself
|
||||
// generates the pi/omp/prime-agent extension (main/pi/agent-status-handler-source.ts), so neither
|
||||
// provider can introduce an opener without a change here.
|
||||
export const OPEN_TOOL_CALL_HOOK_EVENTS: ReadonlySet<string> = new Set([
|
||||
'PreToolUse',
|
||||
'tool_call',
|
||||
'tool_execution_start'
|
||||
])
|
||||
export const AGENT_PROMPT_SENT_AGENT_KINDS = new Set<AgentKind>(AGENT_KIND_VALUES)
|
||||
|
||||
// Why: bound file growth from PTYs that never re-attach; 7 days is the "still relevant?" horizon beyond which entries shouldn't resurrect on hydrate.
|
||||
|
||||
@@ -14,10 +14,12 @@ import {
|
||||
import { AGENT_STATUS_STALE_AFTER_MS, type AgentType } from '../../../shared/agent-status-types'
|
||||
import type { EnrichedAgentHookEventPayload } from './server-types'
|
||||
import { equivalentInterruptAgentType, isValidPaneKey } from './server-status-identity'
|
||||
import { OPEN_TOOL_CALL_HOOK_EVENTS } from './server-constants'
|
||||
import { AgentHookServerRowOwnership } from './server-row-ownership'
|
||||
|
||||
// Why: these TUIs also close an overlay on a bare Escape (Claude's /btw composer, OMP/Pi's
|
||||
// focused-child and settings views), so one press cannot mean "interrupt" on its own (#13547, #9208).
|
||||
// Sibling policy for agents whose first Escape is a cancel lives in the double-Escape gate below.
|
||||
const ESCAPE_ALSO_NAVIGATES_AGENT_TYPES: ReadonlySet<AgentType> = new Set([
|
||||
'claude',
|
||||
'omp',
|
||||
@@ -25,15 +27,6 @@ const ESCAPE_ALSO_NAVIGATES_AGENT_TYPES: ReadonlySet<AgentType> = new Set([
|
||||
'prime-agent'
|
||||
])
|
||||
|
||||
// Why: hook events that OPEN a provider tool call. Their closing event (PostToolUse /
|
||||
// tool_execution_end) replaces the row, so a row still sitting on one of these means the tool
|
||||
// is running now.
|
||||
const OPEN_TOOL_CALL_HOOK_EVENTS: ReadonlySet<string> = new Set([
|
||||
'PreToolUse',
|
||||
'tool_call',
|
||||
'tool_execution_start'
|
||||
])
|
||||
|
||||
/** Hook silence during an open tool call is evidence the tool is still running, not that the
|
||||
* turn was interrupted — so an ambiguous single Escape may not retire the row. */
|
||||
function isNavigationEscapeDuringOpenToolCall(
|
||||
|
||||
Reference in New Issue
Block a user