mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 09:07:40 +00:00
fix(orchestration): name the provider's words when a worker's preamble is rejected (#35791)
When a structured worker's dispatch preamble is definitively rejected, the
dispatch_preamble_undelivered error now appends the provider's failure detail
("Details: ...") unless the rejection sentence already quotes it. The quoted
text is bounded and normalized by the same quotedProviderDetail helper the
rejection writer uses.
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,8 @@ import type { AgentJournalSubmission } from '../../../../shared/agent-session-jo
|
||||
import type { AgentMessageSource } from '../../../../shared/agent-session/agent-session-message-source'
|
||||
import { getAppEnvironment } from '../../../../shared/app-environment'
|
||||
import { DISPATCH_REJECTED_WRITE_FAILED } from '../../../../shared/structured-agent-session-dispatch-rejection'
|
||||
import { agentSessionFailureFact } from '../../../../shared/agent-session-failure'
|
||||
import { codexDispatchRejection } from '../../../codex/codex-structured-turn-end-settlement'
|
||||
|
||||
const hostRef: { current: unknown } = { current: null }
|
||||
const createSpy = vi.fn()
|
||||
@@ -231,7 +233,7 @@ describe('structured worker session', () => {
|
||||
|
||||
describe('structured worker dispatch preamble', () => {
|
||||
type PreambleHost = Parameters<typeof sendStructuredWorkerPreamble>[0]['host']
|
||||
type Settled = Pick<AgentJournalSubmission, 'dispatchState' | 'reason'>
|
||||
type Settled = Pick<AgentJournalSubmission, 'dispatchState' | 'reason' | 'rejection'>
|
||||
|
||||
function submissionOf(settled: Settled): AgentJournalSubmission {
|
||||
return {
|
||||
@@ -377,6 +379,43 @@ describe('structured worker dispatch preamble', () => {
|
||||
expect(isUnknownWorkerStartOutcome(error, 'dispatch_input')).toBe(false)
|
||||
})
|
||||
|
||||
it('names a provider log detail the rejection sentence leaves out', async () => {
|
||||
const error = await send(
|
||||
hostWithSubmission({
|
||||
dispatchState: 'rejected',
|
||||
...codexDispatchRejection(
|
||||
agentSessionFailureFact('providerRejected', {
|
||||
detail: { text: 'thread/start: unsupported service tier', audience: 'log' },
|
||||
neverReachedAgent: true
|
||||
})
|
||||
)
|
||||
})
|
||||
).catch((thrown: unknown) => thrown)
|
||||
expect(error).toMatchObject({
|
||||
code: 'dispatch_preamble_undelivered',
|
||||
message:
|
||||
"The dispatch preamble was not delivered (providerRejected): Codex didn't accept this message. Details: thread/start: unsupported service tier."
|
||||
})
|
||||
})
|
||||
|
||||
it('does not repeat provider words the rejection sentence already quotes', async () => {
|
||||
const error = await send(
|
||||
hostWithSubmission({
|
||||
dispatchState: 'rejected',
|
||||
...codexDispatchRejection(
|
||||
agentSessionFailureFact('providerRejected', {
|
||||
detail: { text: 'Unsupported service tier.', audience: 'person' },
|
||||
neverReachedAgent: true
|
||||
})
|
||||
)
|
||||
})
|
||||
).catch((thrown: unknown) => thrown)
|
||||
expect(error).toMatchObject({
|
||||
message:
|
||||
"The dispatch preamble was not delivered (providerRejected): Codex didn't accept this message: Unsupported service tier."
|
||||
})
|
||||
})
|
||||
|
||||
it('ends the message with one period whether the reason is a sentence or a marker', async () => {
|
||||
const sentence = await send(
|
||||
hostWithSubmission({
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { isDefinitiveAgentSessionCreateRefusal } from '../../../../shared/agent-session-definitive-refusal'
|
||||
import type { AgentMessageSource } from '../../../../shared/agent-session/agent-session-message-source'
|
||||
import {
|
||||
agentSessionFailureDetailText,
|
||||
readAgentSessionFailureFact
|
||||
} from '../../../../shared/agent-session-failure'
|
||||
import { quotedProviderDetail } from '../../../../shared/agent-session-failure-quoted-detail'
|
||||
import type {
|
||||
AgentJournalMessageItem,
|
||||
AgentJournalSubmission
|
||||
@@ -240,12 +245,17 @@ export function preambleDispatchState(
|
||||
return submission.dispatchState
|
||||
}
|
||||
if (submission?.dispatchState === 'rejected') {
|
||||
// Name the failure detail unless the reason already quotes it.
|
||||
const detail = agentSessionFailureDetailText(readAgentSessionFailureFact(submission.rejection))
|
||||
const quoted = detail ? quotedProviderDetail(detail) : ''
|
||||
const detailClause =
|
||||
quoted && !submission.reason?.includes(quoted) ? ` Details: ${quoted}.` : ''
|
||||
// Definitive rejection gets a typed non-delivery code so the coordinator can retry without a duplicate turn.
|
||||
throw new OrchestrationError(
|
||||
'dispatch_preamble_undelivered',
|
||||
`The dispatch preamble was not delivered${
|
||||
submission.rejection ? ` (${submission.rejection.kind})` : ''
|
||||
}: ${reasonClause(submission.reason)}.`
|
||||
}: ${reasonClause(submission.reason)}.${detailClause}`
|
||||
)
|
||||
}
|
||||
// Only accepted proves delivery; adapter/transport unknown requires an outcome_unknown receipt that tells the coordinator to inspect.
|
||||
|
||||
@@ -8,6 +8,14 @@ import type {
|
||||
/** Person-facing provider text is quoted, but bounded so the sentence stays one. */
|
||||
const MAX_QUOTED_DETAIL_CHARS = 512
|
||||
|
||||
/** Provider text as a failure sentence quotes it. */
|
||||
export function quotedProviderDetail(text: string): string {
|
||||
return text
|
||||
.slice(0, MAX_QUOTED_DETAIL_CHARS)
|
||||
.trim()
|
||||
.replace(/[.\s]+$/, '')
|
||||
}
|
||||
|
||||
/** The quoted lead when the provider wrote the detail for a person; the plain lead otherwise. */
|
||||
export function quotingPersonDetail(
|
||||
say: AgentSessionFailureSay,
|
||||
@@ -16,12 +24,6 @@ export function quotingPersonDetail(
|
||||
detail: ProviderDiagnostic | undefined,
|
||||
values: AgentSessionFailureCopyValues = {}
|
||||
): string {
|
||||
const quoted =
|
||||
detail?.audience === 'person'
|
||||
? detail.text
|
||||
.slice(0, MAX_QUOTED_DETAIL_CHARS)
|
||||
.trim()
|
||||
.replace(/[.\s]+$/, '')
|
||||
: ''
|
||||
const quoted = detail?.audience === 'person' ? quotedProviderDetail(detail.text) : ''
|
||||
return quoted ? say(quotedLead, { ...values, detail: quoted }) : say(lead, values)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user