mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 08:02:35 +00:00
refactor(native-chat): the client's outbox settlement gets a name the host's send settlement doesn't share
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
||||
type StructuredAgentSessionOutboxEntry
|
||||
} from './structured-agent-session-outbox'
|
||||
import { reconcileStructuredAgentSessionOutboxWithQueue } from './structured-agent-session-draft-hand-off'
|
||||
import { settleStructuredAgentSessionSendAnswer } from './structured-agent-session-send-settlement'
|
||||
import { settleStructuredAgentSessionSendAnswer } from './structured-agent-session-outbox-settlement'
|
||||
import { hasUnsentStructuredAgentSessionOutboxEntry } from './structured-agent-session-outbox-stop-withdrawal'
|
||||
|
||||
const entry: StructuredAgentSessionOutboxEntry = {
|
||||
|
||||
+11
-11
@@ -7,13 +7,13 @@ import {
|
||||
type StructuredAgentSessionOutboxEntry
|
||||
} from './structured-agent-session-outbox'
|
||||
import {
|
||||
applyStructuredAgentSessionSendSettlement,
|
||||
applyStructuredAgentSessionOutboxSettlement,
|
||||
settleStructuredAgentSessionEntryFromJournal,
|
||||
settleStructuredAgentSessionSendAnswer,
|
||||
structuredAgentSessionEntryOutlivedHostWindow,
|
||||
type StructuredAgentSessionSendAnswer,
|
||||
type StructuredAgentSessionSendSettlementContext
|
||||
} from './structured-agent-session-send-settlement'
|
||||
type StructuredAgentSessionOutboxSettlementContext
|
||||
} from './structured-agent-session-outbox-settlement'
|
||||
import { AGENT_SESSION_MAX_OPERATION_REPLAY_AGE_MS } from './agent-session-host-authority'
|
||||
import { DISPATCH_REJECTED_CANCELLED } from './structured-agent-session-dispatch-rejection'
|
||||
import {
|
||||
@@ -87,16 +87,16 @@ function refused(
|
||||
}
|
||||
}
|
||||
|
||||
const FIRST: StructuredAgentSessionSendSettlementContext = {
|
||||
const FIRST: StructuredAgentSessionOutboxSettlementContext = {
|
||||
firstAttempt: true,
|
||||
answersProve: true,
|
||||
journalHasRow: false
|
||||
}
|
||||
const RESEND_PROVING: StructuredAgentSessionSendSettlementContext = {
|
||||
const RESEND_PROVING: StructuredAgentSessionOutboxSettlementContext = {
|
||||
...FIRST,
|
||||
firstAttempt: false
|
||||
}
|
||||
const RESEND_OLD_HOST: StructuredAgentSessionSendSettlementContext = {
|
||||
const RESEND_OLD_HOST: StructuredAgentSessionOutboxSettlementContext = {
|
||||
...RESEND_PROVING,
|
||||
answersProve: false
|
||||
}
|
||||
@@ -588,18 +588,18 @@ describe('entries an older build saved are migrated, never sent again', () => {
|
||||
describe('applying a settlement', () => {
|
||||
it('records and returns leave the outbox; only a return hands the entry back, with its words', () => {
|
||||
const outbox = [entry(), entry({ clientMessageId: 'next' })]
|
||||
expect(applyStructuredAgentSessionSendSettlement(outbox, ID, { kind: 'recorded' })).toEqual({
|
||||
expect(applyStructuredAgentSessionOutboxSettlement(outbox, ID, { kind: 'recorded' })).toEqual({
|
||||
entries: [outbox[1]],
|
||||
returned: null
|
||||
})
|
||||
expect(
|
||||
applyStructuredAgentSessionSendSettlement(outbox, ID, {
|
||||
applyStructuredAgentSessionOutboxSettlement(outbox, ID, {
|
||||
kind: 'returned',
|
||||
words: ['tryAgain']
|
||||
})
|
||||
).toEqual({ entries: [outbox[1]], returned: { entry: outbox[0], words: ['tryAgain'] } })
|
||||
expect(
|
||||
applyStructuredAgentSessionSendSettlement(outbox, ID, { kind: 'withdrawn' }).returned
|
||||
applyStructuredAgentSessionOutboxSettlement(outbox, ID, { kind: 'withdrawn' }).returned
|
||||
).toEqual({
|
||||
entry: outbox[0],
|
||||
words: null
|
||||
@@ -609,10 +609,10 @@ describe('applying a settlement', () => {
|
||||
it('no answer keeps it under the same id, in doubt; pending keeps it dispatching', () => {
|
||||
const outbox = [entry({ state: 'dispatching' })]
|
||||
expect(
|
||||
applyStructuredAgentSessionSendSettlement(outbox, ID, { kind: 'unanswered' }).entries[0]
|
||||
applyStructuredAgentSessionOutboxSettlement(outbox, ID, { kind: 'unanswered' }).entries[0]
|
||||
).toMatchObject({ clientMessageId: ID, state: 'unconfirmed' })
|
||||
expect(
|
||||
applyStructuredAgentSessionSendSettlement(outbox, ID, { kind: 'pending' }).entries[0]
|
||||
applyStructuredAgentSessionOutboxSettlement(outbox, ID, { kind: 'pending' }).entries[0]
|
||||
).toMatchObject({ clientMessageId: ID, state: 'dispatching' })
|
||||
})
|
||||
})
|
||||
+18
-16
@@ -27,7 +27,7 @@ import { structuredAgentSessionStillSendingWords } from './structured-agent-sess
|
||||
import type { StructuredAgentSessionOutboxEntry } from './structured-agent-session-outbox'
|
||||
import { structuredAgentSessionEntryAwaitsSettlement } from './structured-agent-session-outbox-admission'
|
||||
|
||||
export type StructuredAgentSessionSendSettlement =
|
||||
export type StructuredAgentSessionOutboxSettlement =
|
||||
/** Case 1: the host's row (or card) holds the message from here. */
|
||||
| { kind: 'recorded' }
|
||||
/** Case 1, not final yet: the host wrote the row and has not handed it to the agent. The entry
|
||||
@@ -48,7 +48,7 @@ export type StructuredAgentSessionSendAnswer =
|
||||
* carried, if any, and the RPC error code. */
|
||||
| { kind: 'thrown'; refusal: AgentSessionWriteRefusal | undefined; rpcCode: string | undefined }
|
||||
|
||||
export type StructuredAgentSessionSendSettlementContext = {
|
||||
export type StructuredAgentSessionOutboxSettlementContext = {
|
||||
/** No earlier attempt under this id went out from anywhere, so nothing can hold it but this
|
||||
* attempt. Only an older host needs it: it may refuse a resent id before looking it up. */
|
||||
firstAttempt: boolean
|
||||
@@ -64,13 +64,15 @@ export type StructuredAgentSessionSendSettlementContext = {
|
||||
export const STRUCTURED_AGENT_SESSION_SEND_UNCONFIRMED_WORDS: readonly AgentSessionWriteNoticePart[] =
|
||||
['sendOutcomeLost']
|
||||
|
||||
function returnedFor(refusal: AgentSessionWriteRefusal): StructuredAgentSessionSendSettlement {
|
||||
function returnedFor(refusal: AgentSessionWriteRefusal): StructuredAgentSessionOutboxSettlement {
|
||||
return { kind: 'returned', words: agentSessionWriteNoticeParts(refusal, 'composer-send') }
|
||||
}
|
||||
|
||||
/** Case 3 for a refusal that proves nothing here: why it is held, and that Orca keeps sending it.
|
||||
* "Outcome unknown" is doubt like any lost answer, so it says nothing, thrown or returned. */
|
||||
function stillSendingFor(refusal: AgentSessionWriteRefusal): StructuredAgentSessionSendSettlement {
|
||||
function stillSendingFor(
|
||||
refusal: AgentSessionWriteRefusal
|
||||
): StructuredAgentSessionOutboxSettlement {
|
||||
return refusal.code === 'agent_session_operation_unknown'
|
||||
? { kind: 'unanswered' }
|
||||
: { kind: 'unanswered', words: structuredAgentSessionStillSendingWords(refusal) }
|
||||
@@ -78,8 +80,8 @@ function stillSendingFor(refusal: AgentSessionWriteRefusal): StructuredAgentSess
|
||||
|
||||
/** Words for an id no resend can settle: what an earlier attempt left, if anything, is in the chat. */
|
||||
function settledByJournal(
|
||||
context: StructuredAgentSessionSendSettlementContext
|
||||
): StructuredAgentSessionSendSettlement {
|
||||
context: StructuredAgentSessionOutboxSettlementContext
|
||||
): StructuredAgentSessionOutboxSettlement {
|
||||
return context.journalHasRow
|
||||
? { kind: 'recorded' }
|
||||
: { kind: 'returned', words: [...STRUCTURED_AGENT_SESSION_SEND_UNCONFIRMED_WORDS] }
|
||||
@@ -87,8 +89,8 @@ function settledByJournal(
|
||||
|
||||
function refusalSettlement(
|
||||
refusal: AgentSessionWriteRefusal,
|
||||
context: StructuredAgentSessionSendSettlementContext
|
||||
): StructuredAgentSessionSendSettlement {
|
||||
context: StructuredAgentSessionOutboxSettlementContext
|
||||
): StructuredAgentSessionOutboxSettlement {
|
||||
const reason = refusal.details?.reason
|
||||
if (refusal.code === 'agent_session_operation_unknown') {
|
||||
// A rewind the host refused before writing anything is settled; every other unknown is doubt.
|
||||
@@ -120,8 +122,8 @@ function refusalSettlement(
|
||||
* and only on a first attempt: an earlier one may have landed before the host turned this away. */
|
||||
function thrownSettlement(
|
||||
answer: Extract<StructuredAgentSessionSendAnswer, { kind: 'thrown' }>,
|
||||
context: StructuredAgentSessionSendSettlementContext
|
||||
): StructuredAgentSessionSendSettlement {
|
||||
context: StructuredAgentSessionOutboxSettlementContext
|
||||
): StructuredAgentSessionOutboxSettlement {
|
||||
if (answer.refusal) {
|
||||
return stillSendingFor(answer.refusal)
|
||||
}
|
||||
@@ -136,8 +138,8 @@ function thrownSettlement(
|
||||
export function settleStructuredAgentSessionSendAnswer(
|
||||
answer: StructuredAgentSessionSendAnswer,
|
||||
clientMessageId: string,
|
||||
context: StructuredAgentSessionSendSettlementContext
|
||||
): StructuredAgentSessionSendSettlement {
|
||||
context: StructuredAgentSessionOutboxSettlementContext
|
||||
): StructuredAgentSessionOutboxSettlement {
|
||||
if (answer.kind === 'thrown') {
|
||||
return thrownSettlement(answer, context)
|
||||
}
|
||||
@@ -188,7 +190,7 @@ export function structuredAgentSessionEntryOutlivedHostWindow(
|
||||
/** A row the host holds for the send. */
|
||||
function settleStructuredAgentSessionSendRow(
|
||||
submission: AgentJournalSubmission
|
||||
): StructuredAgentSessionSendSettlement {
|
||||
): StructuredAgentSessionOutboxSettlement {
|
||||
if (submission.dispatchState === 'pending') {
|
||||
return { kind: 'pending' }
|
||||
}
|
||||
@@ -243,7 +245,7 @@ function readThrough(
|
||||
export function settleStructuredAgentSessionEntryFromJournal(
|
||||
entry: StructuredAgentSessionOutboxEntry,
|
||||
reading: StructuredAgentSessionJournalReading
|
||||
): StructuredAgentSessionSendSettlement | null {
|
||||
): StructuredAgentSessionOutboxSettlement | null {
|
||||
// The host handed it off as a queued draft, in whatever state: the card carries it.
|
||||
if (
|
||||
reading.submissions.some((candidate) => candidate.queuedMessageId === entry.clientMessageId)
|
||||
@@ -298,10 +300,10 @@ export type StructuredAgentSessionSettledOutbox = {
|
||||
} | null
|
||||
}
|
||||
|
||||
export function applyStructuredAgentSessionSendSettlement(
|
||||
export function applyStructuredAgentSessionOutboxSettlement(
|
||||
entries: readonly StructuredAgentSessionOutboxEntry[],
|
||||
clientMessageId: string,
|
||||
settlement: StructuredAgentSessionSendSettlement
|
||||
settlement: StructuredAgentSessionOutboxSettlement
|
||||
): StructuredAgentSessionSettledOutbox {
|
||||
const entry = entries.find((candidate) => candidate.clientMessageId === clientMessageId)
|
||||
if (!entry) {
|
||||
@@ -26,7 +26,7 @@ function goesOutOnItsOwn(
|
||||
* - A message that never went out is withdrawn here; its text goes back to the composer.
|
||||
* - One already on its way is stamped with the Stop's own id and never sent again: a resend onto the
|
||||
* session the user stopped could start a turn. Its own answer, its journal row or the Stop's
|
||||
* answer settles it (structured-agent-session-send-settlement).
|
||||
* answer settles it (structured-agent-session-outbox-settlement).
|
||||
* - One the host holds a row for is the journal's to settle.
|
||||
*/
|
||||
export function stopStructuredAgentSessionOutbox(
|
||||
|
||||
@@ -15,9 +15,9 @@ import {
|
||||
import { admitStructuredAgentSessionOutboxEntry } from './structured-agent-session-outbox-admission'
|
||||
import { structuredAgentSessionEntryAttempt } from './structured-agent-session-outbox-delivery'
|
||||
import {
|
||||
applyStructuredAgentSessionSendSettlement,
|
||||
applyStructuredAgentSessionOutboxSettlement,
|
||||
settleStructuredAgentSessionSendAnswer
|
||||
} from './structured-agent-session-send-settlement'
|
||||
} from './structured-agent-session-outbox-settlement'
|
||||
import { stopStructuredAgentSessionOutbox } from './structured-agent-session-outbox-stop-withdrawal'
|
||||
|
||||
function entry(sentDelivery?: 'queue-if-active') {
|
||||
@@ -151,7 +151,7 @@ describe('outbox queue delivery', () => {
|
||||
)
|
||||
const stopped = stopStructuredAgentSessionOutbox([staged], [], 'client-1', 'stop-1').entries
|
||||
for (const entries of [stopped, [staged]]) {
|
||||
const settled = applyStructuredAgentSessionSendSettlement(entries, 'client-1', settlement)
|
||||
const settled = applyStructuredAgentSessionOutboxSettlement(entries, 'client-1', settlement)
|
||||
expect(settled.entries).toEqual([])
|
||||
expect(settled.returned?.entry.clientMessageId).toBe('client-1')
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { parseStructuredAgentSessionOutboxQueueFields } from './structured-agent
|
||||
|
||||
/** `queued`: waits to go out. `dispatching`: out, or held by the host as a row it has not handed
|
||||
* to the agent yet. `unconfirmed`: no answer yet, so the same id goes again
|
||||
* (structured-agent-session-send-settlement). */
|
||||
* (structured-agent-session-outbox-settlement). */
|
||||
export type StructuredAgentSessionOutboxState = 'queued' | 'dispatching' | 'unconfirmed'
|
||||
|
||||
/** The Stop that outran a send already on its way: by its own id, and where the host's journal
|
||||
@@ -147,7 +147,7 @@ export function stageStructuredAgentSessionOutboxEntryForSend(
|
||||
/**
|
||||
* The outbox as the journal reads it: an entry the host holds a row for leaves once that row has
|
||||
* settled (the row shows it from there), and stays out while it is pending. A view's reading; the
|
||||
* outbox hook settles the stored copy (structured-agent-session-send-settlement).
|
||||
* outbox hook settles the stored copy (structured-agent-session-outbox-settlement).
|
||||
*/
|
||||
export function reconcileStructuredAgentSessionOutbox(
|
||||
entries: readonly StructuredAgentSessionOutboxEntry[],
|
||||
|
||||
Reference in New Issue
Block a user