blob: 7779982502a082b981ff87cde141984e978f5b1e [file]
import {
INTERACTION_CLOSURE_REASONS,
interactionCanonicalOutcomesEquivalent,
projectInteractionSandboxBoundaryRequest,
type InteractionAnswer,
type InteractionCanonicalOutcome,
} from '@maka/core';
import type { SandboxBoundaryRequest } from '@maka/core';
import { RuntimeInteractionInvariantError, type RuntimeUserQuestionOutcome } from '@maka/runtime';
import type {
InteractionRecord,
StoredInteractionOutcome,
StoredInteractionRequest,
} from '@maka/storage/interaction-store';
import {
INTERACTION_PENDING_REVISION,
INTERACTION_RESOLVED_REVISION,
INTERACTION_SCHEMA_VERSION,
type InteractionAnsweredSnapshot,
type InteractionPendingSnapshot,
type InteractionSnapshot,
type SessionInteractionProjection,
} from '../protocol/index.js';
export function projectInteractionRecord(record: InteractionRecord): InteractionSnapshot {
const base = {
schemaVersion: INTERACTION_SCHEMA_VERSION,
interactionId: record.request.requestId,
sessionId: record.request.sessionId,
turnId: record.request.turnId,
runId: record.request.runId,
request: record.request.request,
} as const;
const outcome = record.outcome?.outcome;
if (!outcome) {
return {
...base,
revision: INTERACTION_PENDING_REVISION,
status: 'pending',
outcome: null,
};
}
return outcome.kind === 'closure'
? { ...base, revision: INTERACTION_RESOLVED_REVISION, status: 'closed', outcome }
: { ...base, revision: INTERACTION_RESOLVED_REVISION, status: 'answered', outcome };
}
export function projectPendingInteraction(
request: StoredInteractionRequest,
): InteractionPendingSnapshot {
return {
schemaVersion: INTERACTION_SCHEMA_VERSION,
interactionId: request.requestId,
sessionId: request.sessionId,
turnId: request.turnId,
runId: request.runId,
revision: INTERACTION_PENDING_REVISION,
request: request.request,
status: 'pending',
outcome: null,
};
}
export function projectSessionInteractions(
pending: readonly StoredInteractionRequest[],
sandboxBoundaries: readonly SandboxBoundaryRequest[] = [],
): SessionInteractionProjection {
return {
pending: [
...pending.map((request) => ({
createdAt: request.createdAt,
interactionId: request.requestId,
snapshot: projectPendingInteraction(request),
})),
...sandboxBoundaries.map((request) => ({
createdAt: request.createdAt,
interactionId: request.requestId,
snapshot: projectPendingSandboxBoundaryInteraction(request),
})),
]
.sort(
(left, right) =>
left.createdAt - right.createdAt || left.interactionId.localeCompare(right.interactionId),
)
.map(({ snapshot }) => snapshot),
};
}
export function projectSandboxBoundaryInteraction(
request: SandboxBoundaryRequest,
): InteractionSnapshot {
const base = sandboxBoundaryInteractionBase(request);
if (request.status === 'pending') {
return {
...base,
revision: INTERACTION_PENDING_REVISION,
status: 'pending',
outcome: null,
};
}
const closureReason = INTERACTION_CLOSURE_REASONS.find(
(reason) => reason === request.outcomeReason,
);
if (closureReason) {
return {
...base,
revision: INTERACTION_RESOLVED_REVISION,
status: 'closed',
outcome: {
kind: 'closure',
reason: closureReason,
committedAt: requireSandboxBoundarySettledAt(request),
},
};
}
return {
...base,
revision: INTERACTION_RESOLVED_REVISION,
status: 'answered',
outcome: sandboxBoundaryCanonicalOutcome(request),
};
}
export function projectPendingSandboxBoundaryInteraction(
request: SandboxBoundaryRequest,
): InteractionPendingSnapshot {
const snapshot = projectSandboxBoundaryInteraction(request);
if (snapshot.status !== 'pending') {
throw new RuntimeInteractionInvariantError(
`Sandbox boundary Interaction ${request.requestId} is already resolved`,
);
}
return snapshot;
}
export function compareStoredInteractionRequests(
left: StoredInteractionRequest,
right: StoredInteractionRequest,
): number {
return left.createdAt - right.createdAt || left.requestId.localeCompare(right.requestId);
}
export function questionCanonicalOutcome(
answer: Extract<InteractionAnswer, { kind: 'question' }>,
committedAt: number,
): Extract<InteractionCanonicalOutcome, { kind: 'question_answer' }> {
return { kind: 'question_answer', answers: [...answer.answers], committedAt };
}
export function runtimeQuestionOutcome(
outcome: InteractionCanonicalOutcome,
): RuntimeUserQuestionOutcome {
if (outcome.kind === 'closure') {
if (outcome.reason === 'timed_out') {
throw new RuntimeInteractionInvariantError('Question Interaction resolved with a timeout');
}
return { kind: 'closure', reason: outcome.reason };
}
if (outcome.kind !== 'question_answer') {
throw new RuntimeInteractionInvariantError(
'Question Interaction resolved with a permission answer',
);
}
return { kind: 'question_answer', answer: { answers: [...outcome.answers] } };
}
export function answerOutcome(
record: InteractionRecord & { outcome: StoredInteractionOutcome },
candidate: InteractionAnswer,
):
| { readonly ok: true; readonly result: InteractionAnsweredSnapshot }
| {
readonly ok: false;
readonly error: { readonly code: 'already_resolved'; readonly message: string };
} {
const snapshot = projectInteractionRecord(record);
if (snapshot.status === 'closed') {
return {
ok: false,
error: { code: 'already_resolved', message: 'Interaction was already closed' },
};
}
if (snapshot.status !== 'answered') {
throw new RuntimeInteractionInvariantError('Committed Interaction did not project as resolved');
}
const candidateOutcome = canonicalOutcomeForHistoricalAnswer(
candidate,
snapshot.outcome.committedAt,
);
return interactionCanonicalOutcomesEquivalent(snapshot.outcome, candidateOutcome)
? { ok: true, result: snapshot }
: {
ok: false,
error: {
code: 'already_resolved',
message: 'Interaction has a different canonical answer',
},
};
}
function canonicalOutcomeForHistoricalAnswer(
answer: InteractionAnswer,
committedAt: number,
): Exclude<InteractionCanonicalOutcome, { kind: 'closure' }> {
if (answer.kind === 'question') {
return questionCanonicalOutcome(answer, committedAt);
}
if (answer.kind === 'sandbox_boundary') {
throw new RuntimeInteractionInvariantError(
'Sandbox boundary answers require their canonical boundary settlement',
);
}
return answer.decision === 'deny'
? {
kind: 'permission_answer',
decision: 'deny',
rememberForTurn: false,
reviewer: 'user',
committedAt,
}
: {
kind: 'permission_answer',
decision: 'allow',
rememberForTurn: answer.rememberForTurn,
reviewer: 'user',
committedAt,
};
}
function sandboxBoundaryInteractionBase(request: SandboxBoundaryRequest) {
if (!request.turnId || !request.runId) {
throw new RuntimeInteractionInvariantError(
`Sandbox boundary ${request.requestId} has no hosted Run provenance`,
);
}
return {
schemaVersion: INTERACTION_SCHEMA_VERSION,
interactionId: request.requestId,
sessionId: request.sessionId,
turnId: request.turnId,
runId: request.runId,
request: projectInteractionSandboxBoundaryRequest(request),
} as const;
}
function sandboxBoundaryCanonicalOutcome(
request: SandboxBoundaryRequest,
): Extract<InteractionCanonicalOutcome, { kind: 'sandbox_boundary_decision' }> {
if (request.status === 'pending') {
throw new RuntimeInteractionInvariantError(
`Sandbox boundary Interaction ${request.requestId} is still pending`,
);
}
return {
kind: 'sandbox_boundary_decision',
decision: request.status === 'denied' ? 'deny' : 'allow',
status: request.status,
committedAt: requireSandboxBoundarySettledAt(request),
};
}
function requireSandboxBoundarySettledAt(request: SandboxBoundaryRequest): number {
if (request.settledAt === undefined) {
throw new RuntimeInteractionInvariantError(
`Sandbox boundary Interaction ${request.requestId} has no settlement timestamp`,
);
}
return request.settledAt;
}