mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 08:02:35 +00:00
185 lines
7.5 KiB
TypeScript
185 lines
7.5 KiB
TypeScript
// A chat write that did not happen, as a fact: what the person was doing, and what the host said.
|
|
// Saved with a queued message, so it holds only what stays true after a reload; the words are
|
|
// chosen from it when it is shown (`agent-session-refusal-notice.ts`).
|
|
|
|
import {
|
|
readAgentSessionRefusalDetails,
|
|
type AgentSessionRefusalReason
|
|
} from './agent-session-refusal-details'
|
|
import type { AgentSessionRewindReason } from './agent-session-rewind'
|
|
import {
|
|
isAgentSessionWireRefusalCode,
|
|
readAgentSessionRefusalReference,
|
|
type AgentSessionOwnerVerdict,
|
|
type AgentSessionRefusalReference,
|
|
type AgentSessionWireRefusal,
|
|
type AgentSessionWireRefusalCode
|
|
} from './agent-session-wire-refusals'
|
|
|
|
/** What the person was doing, which decides what the notice says did not happen. `send` says
|
|
* only that the message was not sent; `composer-send` also says to send it again, for a message
|
|
* that went back to the composer. `read-history` is opening the chat to show its history, which
|
|
* writes nothing. */
|
|
export type AgentSessionWriteKind =
|
|
| 'read-history'
|
|
| 'send'
|
|
| 'composer-send'
|
|
| 'stop'
|
|
| 'stop-task'
|
|
| 'stop-tasks'
|
|
| 'answer'
|
|
| 'option'
|
|
| 'command'
|
|
| 'goal'
|
|
|
|
/** The kind of write an `agentSession.*` call stands for. */
|
|
export function agentSessionWriteKindForMethod(
|
|
fingerprintMethod: string,
|
|
fields: Record<string, unknown>
|
|
): AgentSessionWriteKind {
|
|
if (fingerprintMethod === 'agentSession.send') {
|
|
return 'send'
|
|
}
|
|
if (fingerprintMethod === 'agentSession.cancel') {
|
|
// A background-task stop never asked the agent to stop.
|
|
if (fields.scope !== 'background-tasks') {
|
|
return 'stop'
|
|
}
|
|
return typeof fields.taskId === 'string' ? 'stop-task' : 'stop-tasks'
|
|
}
|
|
if (fingerprintMethod.startsWith('agentSession.respondTo')) {
|
|
return 'answer'
|
|
}
|
|
if (fingerprintMethod === 'agentSession.setOption') {
|
|
return 'option'
|
|
}
|
|
if (fingerprintMethod === 'agentSession.threadGoal') {
|
|
return 'goal'
|
|
}
|
|
return 'command'
|
|
}
|
|
|
|
/** What a saved refusal keeps beside its reason: facts about the refused operation that stay true
|
|
* after a reload. Never the fence, revision or resolution, which move, nor any provider text. */
|
|
type DurableRefusalFacts = {
|
|
agent_session_operation_invalid: { rewindReason?: AgentSessionRewindReason }
|
|
agent_session_operation_unknown: { rewindReason?: AgentSessionRewindReason }
|
|
/** A snapshot of the owner's state from when it was refused. */
|
|
agent_session_ownership_unknown: { ownerVerdict?: AgentSessionOwnerVerdict }
|
|
}
|
|
|
|
const DURABLE_FACT_KEYS: Partial<Record<AgentSessionWireRefusalCode, readonly string[]>> = {
|
|
agent_session_operation_invalid: ['rewindReason'],
|
|
agent_session_operation_unknown: ['rewindReason'],
|
|
agent_session_ownership_unknown: ['ownerVerdict']
|
|
} satisfies { [C in keyof DurableRefusalFacts]: readonly (keyof DurableRefusalFacts[C])[] }
|
|
|
|
export type AgentSessionWriteRefusalDetails<C extends AgentSessionWireRefusalCode> = {
|
|
reason?: AgentSessionRefusalReason<C>
|
|
} & (C extends keyof DurableRefusalFacts ? DurableRefusalFacts[C] : Record<never, never>)
|
|
|
|
/** A refused write: its code, and what the host said about it that outlives the reply. */
|
|
export type AgentSessionWriteRefusal = {
|
|
[C in AgentSessionWireRefusalCode]: {
|
|
kind: 'refused'
|
|
code: C
|
|
/** Absent from older hosts and from entries saved before refusals carried a reason. */
|
|
details?: AgentSessionWriteRefusalDetails<C>
|
|
}
|
|
}[AgentSessionWireRefusalCode]
|
|
|
|
/** Why a write did not happen, or that nothing proves it did not. */
|
|
export type AgentSessionWriteFailure =
|
|
| AgentSessionWriteRefusal
|
|
/** The request failed without a refusal before the host ran it, so nothing is known about why. */
|
|
| { kind: 'failed' }
|
|
/** The request failed where the host may already have run it (a timeout, a lost connection, an
|
|
* error inside the method). */
|
|
| { kind: 'unconfirmed' }
|
|
|
|
/** The refusal as a write keeps it. Details are read against the code, so a reason or verdict
|
|
* another build added is dropped and the code's own words stand. */
|
|
function agentSessionWriteRefusal(
|
|
code: AgentSessionWireRefusalCode,
|
|
details: unknown
|
|
): AgentSessionWriteRefusal {
|
|
const durable: readonly string[] = ['reason', ...(DURABLE_FACT_KEYS[code] ?? [])]
|
|
const kept = Object.fromEntries(
|
|
Object.entries(readAgentSessionRefusalDetails(code, details) ?? {}).filter(([key]) =>
|
|
durable.includes(key)
|
|
)
|
|
)
|
|
const refusal = {
|
|
kind: 'refused',
|
|
code,
|
|
...(Object.keys(kept).length > 0 ? { details: kept } : {})
|
|
}
|
|
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: `kept` holds only a reason and facts read against `code` and allowed for it above.
|
|
return refusal as AgentSessionWriteRefusal
|
|
}
|
|
|
|
export function agentSessionRefusalFailure(
|
|
refusal: Pick<AgentSessionWireRefusal, 'code' | 'details'>
|
|
): AgentSessionWriteRefusal {
|
|
// A newer host can send a code this client has never heard of; its details mean nothing here.
|
|
return isAgentSessionWireRefusalCode(refusal.code)
|
|
? agentSessionWriteRefusal(refusal.code, refusal.details)
|
|
: { kind: 'refused', code: refusal.code }
|
|
}
|
|
|
|
/** A request that threw, from the RPC error code the host answered with (undefined when none came
|
|
* back). Only a host that turned it away before running the method proves the write did not
|
|
* happen. */
|
|
export function agentSessionRpcErrorFailure(code: string | undefined): AgentSessionWriteFailure {
|
|
if (code === 'method_not_found' || code === 'method_not_supported') {
|
|
return { kind: 'refused', code: 'structured_agent_session_unsupported' }
|
|
}
|
|
return code === 'invalid_argument' || code === 'unauthorized'
|
|
? { kind: 'refused', code: 'agent_session_operation_invalid' }
|
|
: { kind: 'unconfirmed' }
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === 'object' && value !== null
|
|
}
|
|
|
|
/** The refusal a failed request's error carries in its data. Takes both shapes a client meets: an
|
|
* RPC call's thrown error, whose payload is on `response.error`, and the payload a stream hands
|
|
* its error callback. Undefined from an older host, or for a failure that was not a refusal. */
|
|
export function readAgentSessionErrorRefusal(
|
|
error: unknown
|
|
): AgentSessionRefusalReference | undefined {
|
|
const payload =
|
|
isRecord(error) && isRecord(error.response) && isRecord(error.response.error)
|
|
? error.response.error
|
|
: error
|
|
const data = isRecord(payload) ? payload.data : undefined
|
|
return isRecord(data) ? readAgentSessionRefusalReference(data.refusal) : undefined
|
|
}
|
|
|
|
/** What to say about a request that threw: the host's refusal when its error carried one, else
|
|
* what the RPC error code proves. Words only: whether the write may have happened stays the
|
|
* caller's own classification. */
|
|
export function agentSessionThrownFailure(
|
|
error: unknown,
|
|
rpcCode: string | undefined
|
|
): AgentSessionWriteFailure {
|
|
const refusal = readAgentSessionErrorRefusal(error)
|
|
return refusal ? agentSessionRefusalFailure(refusal) : agentSessionRpcErrorFailure(rpcCode)
|
|
}
|
|
|
|
/** A saved failure, or undefined when it is not one this build wrote. */
|
|
export function parseAgentSessionWriteFailure(
|
|
value: unknown
|
|
): AgentSessionWriteFailure | undefined {
|
|
if (typeof value !== 'object' || value === null || !('kind' in value)) {
|
|
return undefined
|
|
}
|
|
if (value.kind === 'failed') {
|
|
return { kind: 'failed' }
|
|
}
|
|
return value.kind === 'refused' && 'code' in value && isAgentSessionWireRefusalCode(value.code)
|
|
? agentSessionWriteRefusal(value.code, 'details' in value ? value.details : undefined)
|
|
: undefined
|
|
}
|