blob: 68edc0d09a2fac624b918df1540ec129fc2648fa [file]
import {
decodeInteractionAnswer,
decodeInteractionCanonicalOutcome,
interactionCanonicalOutcomesEquivalent,
type InteractionAnswer,
type InteractionCanonicalOutcome,
} from '@maka/core/interaction';
import {
RuntimeInteractionInvariantError,
type RuntimePermissionAnswer,
type RuntimePermissionOutcome,
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[],
): SessionInteractionProjection {
return {
pending: [...pending].sort(compareStoredInteractionRequests).map(projectPendingInteraction),
};
}
export function compareStoredInteractionRequests(
left: StoredInteractionRequest,
right: StoredInteractionRequest,
): number {
return left.createdAt - right.createdAt || left.requestId.localeCompare(right.requestId);
}
export function permissionInteractionAnswer(answer: RuntimePermissionAnswer): InteractionAnswer {
return decodeInteractionAnswer({
kind: 'permission',
decision: answer.decision,
rememberForTurn: answer.rememberForTurn ?? false,
});
}
export function permissionCanonicalOutcome(
answer: RuntimePermissionAnswer,
committedAt: number,
): Exclude<InteractionCanonicalOutcome, { kind: 'closure' }> {
const outcome = decodeInteractionCanonicalOutcome({
kind: 'permission_answer',
decision: answer.decision,
rememberForTurn: answer.rememberForTurn ?? false,
reviewer: answer.reviewer ?? 'user',
...(answer.rationale === undefined ? {} : { rationale: answer.rationale }),
...(answer.riskLevel === undefined ? {} : { riskLevel: answer.riskLevel }),
committedAt,
});
if (outcome.kind !== 'permission_answer') {
throw new RuntimeInteractionInvariantError('Runtime permission answer decoded incorrectly');
}
return outcome;
}
export function wireCanonicalOutcome(
answer: InteractionAnswer,
committedAt: number,
): Exclude<InteractionCanonicalOutcome, { kind: 'closure' }> {
if (answer.kind === 'question') {
return { kind: 'question_answer', answers: [...answer.answers], committedAt };
}
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,
};
}
export function runtimePermissionOutcome(
outcome: InteractionCanonicalOutcome,
): RuntimePermissionOutcome {
if (outcome.kind === 'closure') return { kind: 'closure', reason: outcome.reason };
if (outcome.kind !== 'permission_answer') {
throw new RuntimeInteractionInvariantError(
'Permission Interaction resolved with a question answer',
);
}
return {
kind: 'permission_answer',
answer: {
decision: outcome.decision,
rememberForTurn: outcome.rememberForTurn,
reviewer: outcome.reviewer,
...(outcome.rationale === undefined ? {} : { rationale: outcome.rationale }),
...(outcome.riskLevel === undefined ? {} : { riskLevel: outcome.riskLevel }),
},
};
}
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 = wireCanonicalOutcome(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',
},
};
}