blob: d9f74758007a51bd00879d4274c1801a34266be2 [file]
import { decodeAgentRunHeader, type AgentRunHeader } from './agent-run.js';
import { encodeCanonicalRuntimeEvent } from './canonical-runtime-event.js';
import { sha256Hex } from './local-memory.js';
import { isRecord } from './record-schema.js';
import type { RuntimeEvent } from './runtime-event.js';
import { stableJsonStringify } from './tool-args-identity.js';
export type RuntimeBoundaryDigest = `sha256:${string}`;
export interface RuntimePrefixIdentityV1 {
sessionId: string;
invocationId: string;
runId: string;
turnId: string;
}
export interface RuntimePrefixPositionV1 {
lastEventSeq: number;
eventCount: number;
lastEventId: string;
}
export interface RuntimePrefixRowV1 {
eventSeq: number;
event: RuntimeEvent;
}
export interface ImmutableRuntimePrefixV1 {
protocol: 'immutable_runtime_prefix_v1';
identity: RuntimePrefixIdentityV1;
position: RuntimePrefixPositionV1;
prefixDigest: RuntimeBoundaryDigest;
events: readonly RuntimeEvent[];
}
export interface RuntimePrefixSegmentV1 {
protocol: 'runtime_prefix_segment_v1';
identity: RuntimePrefixIdentityV1;
position: RuntimePrefixPositionV1;
prefixDigest: RuntimeBoundaryDigest;
}
export interface RuntimeBoundaryCursorV1 {
protocol: 'runtime_boundary_cursor_v1';
segments: readonly [RuntimePrefixSegmentV1, ...RuntimePrefixSegmentV1[]];
manifestDigest: RuntimeBoundaryDigest;
}
export interface ContinuationClaimV1 {
protocol: 'continuation_claim_v1';
claimId: string;
boundaryDigest: RuntimeBoundaryDigest;
boundary: RuntimeBoundaryCursorV1;
providerProjectionVersion: 1;
providerReplayDigest: RuntimeBoundaryDigest;
target: {
sessionId: string;
invocationId: string;
runId: string;
turnId: string;
};
/** Exact pre-provider target Run header used by both normal admission and crash repair. */
targetRunHeader: AgentRunHeader;
claimedAt: number;
}
export function buildImmutableRuntimePrefix(
identity: RuntimePrefixIdentityV1,
rows: readonly RuntimePrefixRowV1[],
): ImmutableRuntimePrefixV1 {
const canonicalRows = canonicalizePrefixRows(identity, rows);
const last = canonicalRows.at(-1);
if (!last) throw new Error('immutable RuntimeEvent prefix is empty');
return {
protocol: 'immutable_runtime_prefix_v1',
identity: { ...identity },
position: {
lastEventSeq: last.eventSeq,
eventCount: canonicalRows.length,
lastEventId: last.event.id,
},
prefixDigest: digestCanonicalRuntimePrefix(identity, canonicalRows),
events: canonicalRows.map((row) => row.event),
};
}
export function digestRuntimePrefix(
identity: RuntimePrefixIdentityV1,
rows: readonly RuntimePrefixRowV1[],
): RuntimeBoundaryDigest {
return digestCanonicalRuntimePrefix(identity, canonicalizePrefixRows(identity, rows));
}
export function runtimePrefixSegment(prefix: ImmutableRuntimePrefixV1): RuntimePrefixSegmentV1 {
if (prefix.protocol !== 'immutable_runtime_prefix_v1') {
throw new Error('Invalid immutable RuntimeEvent prefix protocol');
}
const rebuilt = buildImmutableRuntimePrefix(
prefix.identity,
prefix.events.map((event, index) => ({ eventSeq: index + 1, event })),
);
if (stableJsonStringify(rebuilt.position) !== stableJsonStringify(prefix.position)) {
throw new Error('Immutable RuntimeEvent prefix position mismatch');
}
if (rebuilt.prefixDigest !== prefix.prefixDigest) {
throw new Error('Immutable RuntimeEvent prefix digest mismatch');
}
return decodeRuntimePrefixSegment({
protocol: 'runtime_prefix_segment_v1',
identity: rebuilt.identity,
position: rebuilt.position,
prefixDigest: rebuilt.prefixDigest,
});
}
export function createRuntimeBoundaryCursor(
segments: readonly [RuntimePrefixSegmentV1, ...RuntimePrefixSegmentV1[]],
): RuntimeBoundaryCursorV1 {
const canonicalSegments = segments.map(decodeRuntimePrefixSegment) as [
RuntimePrefixSegmentV1,
...RuntimePrefixSegmentV1[],
];
const sessionId = canonicalSegments[0].identity.sessionId;
const invocationIds = new Set<string>();
const runIds = new Set<string>();
const turnIds = new Set<string>();
for (const segment of canonicalSegments) {
if (segment.identity.sessionId !== sessionId) {
throw new Error('Runtime boundary segments must belong to the same session');
}
if (runIds.has(segment.identity.runId)) {
throw new Error('Runtime boundary lineage contains a duplicate runId');
}
runIds.add(segment.identity.runId);
if (invocationIds.has(segment.identity.invocationId)) {
throw new Error('Runtime boundary lineage contains a duplicate invocationId');
}
invocationIds.add(segment.identity.invocationId);
if (turnIds.has(segment.identity.turnId)) {
throw new Error('Runtime boundary lineage contains a duplicate turnId');
}
turnIds.add(segment.identity.turnId);
}
return {
protocol: 'runtime_boundary_cursor_v1',
segments: canonicalSegments,
manifestDigest: digestRuntimeBoundaryManifest(canonicalSegments),
};
}
export function digestRuntimeBoundaryManifest(
segments: readonly [RuntimePrefixSegmentV1, ...RuntimePrefixSegmentV1[]],
): RuntimeBoundaryDigest {
const canonicalSegments = segments.map(decodeRuntimePrefixSegment);
const json = stableJsonStringify({
protocol: 'runtime_boundary_cursor_v1',
segments: canonicalSegments,
});
const bytes = concatBytes(
lengthPrefixed(utf8('maka.runtime-boundary-manifest.v1')),
lengthPrefixed(utf8(json)),
);
return `sha256:${sha256Hex(bytes)}`;
}
export function decodeRuntimePrefixSegment(value: unknown): RuntimePrefixSegmentV1 {
if (
!isRecord(value) ||
!hasExactKeys(value, ['protocol', 'identity', 'position', 'prefixDigest']) ||
value.protocol !== 'runtime_prefix_segment_v1'
) {
throw new Error('Invalid RuntimeEvent prefix segment');
}
return {
protocol: 'runtime_prefix_segment_v1',
identity: decodePrefixIdentity(value.identity),
position: decodePrefixPosition(value.position),
prefixDigest: decodeBoundaryDigest(value.prefixDigest),
};
}
export function decodeRuntimeBoundaryCursor(value: unknown): RuntimeBoundaryCursorV1 {
if (
!isRecord(value) ||
!hasExactKeys(value, ['protocol', 'segments', 'manifestDigest']) ||
value.protocol !== 'runtime_boundary_cursor_v1' ||
!Array.isArray(value.segments) ||
value.segments.length === 0
) {
throw new Error('Invalid RuntimeEvent boundary cursor');
}
const segments = value.segments.map(decodeRuntimePrefixSegment) as [
RuntimePrefixSegmentV1,
...RuntimePrefixSegmentV1[],
];
const cursor = createRuntimeBoundaryCursor(segments);
const manifestDigest = decodeBoundaryDigest(value.manifestDigest);
if (cursor.manifestDigest !== manifestDigest) {
throw new Error('RuntimeEvent boundary manifest digest mismatch');
}
return cursor;
}
export function decodeContinuationClaim(value: unknown): ContinuationClaimV1 {
if (
!isRecord(value) ||
!hasExactKeys(value, [
'protocol',
'claimId',
'boundaryDigest',
'boundary',
'providerProjectionVersion',
'providerReplayDigest',
'target',
'targetRunHeader',
'claimedAt',
]) ||
value.protocol !== 'continuation_claim_v1' ||
!isNonEmptyString(value.claimId) ||
!isRecord(value.target) ||
!hasExactKeys(value.target, ['sessionId', 'invocationId', 'runId', 'turnId']) ||
!isNonEmptyString(value.target.sessionId) ||
!isNonEmptyString(value.target.invocationId) ||
!isNonEmptyString(value.target.runId) ||
!isNonEmptyString(value.target.turnId) ||
value.providerProjectionVersion !== 1 ||
!Number.isSafeInteger(value.claimedAt) ||
(value.claimedAt as number) < 0
) {
throw new Error('Invalid continuation claim');
}
const boundary = decodeRuntimeBoundaryCursor(value.boundary);
const boundaryDigest = decodeBoundaryDigest(value.boundaryDigest);
const providerReplayDigest = decodeBoundaryDigest(value.providerReplayDigest);
if (boundaryDigest !== boundary.manifestDigest) {
throw new Error('Continuation claim boundary digest mismatch');
}
const source = boundary.segments.at(-1)!;
if (value.target.sessionId !== source.identity.sessionId) {
throw new Error('Continuation claim target session differs from source boundary');
}
const targetRunId = value.target.runId;
if (boundary.segments.some((segment) => segment.identity.runId === targetRunId)) {
throw new Error('Continuation claim target runId reuses source identity');
}
const targetInvocationId = value.target.invocationId;
if (boundary.segments.some((segment) => segment.identity.invocationId === targetInvocationId)) {
throw new Error('Continuation claim target invocationId reuses source identity');
}
const targetTurnId = value.target.turnId;
if (boundary.segments.some((segment) => segment.identity.turnId === targetTurnId)) {
throw new Error('Continuation claim target turnId reuses source identity');
}
const targetRunHeader = decodeAgentRunHeader(value.targetRunHeader);
const continuationSource = targetRunHeader.continuationSource;
if (
targetRunHeader.runId !== targetRunId ||
targetRunHeader.invocationId !== targetInvocationId ||
targetRunHeader.sessionId !== value.target.sessionId ||
targetRunHeader.turnId !== targetTurnId ||
targetRunHeader.status !== 'created' ||
targetRunHeader.createdAt !== value.claimedAt ||
targetRunHeader.updatedAt !== value.claimedAt ||
targetRunHeader.completedAt !== undefined ||
targetRunHeader.failureClass !== undefined ||
targetRunHeader.failureMessage !== undefined ||
!continuationSource ||
!('protocol' in continuationSource) ||
continuationSource.protocol !== 'continuation_source_v2' ||
continuationSource.claimId !== value.claimId ||
continuationSource.boundaryDigest !== boundaryDigest ||
continuationSource.sourceInvocationId !== source.identity.invocationId ||
continuationSource.sourceRunId !== source.identity.runId ||
continuationSource.sourceTurnId !== source.identity.turnId ||
continuationSource.sourceRuntimeEventHighWater !== source.position.lastEventSeq ||
continuationSource.sourcePrefixDigest !== source.prefixDigest ||
continuationSource.replayManifestDigest !== boundary.manifestDigest
) {
throw new Error('Continuation claim target Run header mismatch');
}
return {
protocol: 'continuation_claim_v1',
claimId: value.claimId,
boundaryDigest,
boundary,
providerProjectionVersion: 1,
providerReplayDigest,
target: {
sessionId: value.target.sessionId,
invocationId: value.target.invocationId,
runId: value.target.runId,
turnId: value.target.turnId,
},
targetRunHeader,
claimedAt: value.claimedAt as number,
};
}
function canonicalizePrefixRows(
identity: RuntimePrefixIdentityV1,
rows: readonly RuntimePrefixRowV1[],
): RuntimePrefixRowV1[] {
const canonicalIdentity = decodePrefixIdentity(identity);
const canonicalRows: RuntimePrefixRowV1[] = [];
for (const [index, row] of rows.entries()) {
const expectedEventSeq = index + 1;
if (
!Number.isSafeInteger(row.eventSeq) ||
row.eventSeq <= 0 ||
row.eventSeq !== expectedEventSeq
) {
throw new Error(
`immutable RuntimeEvent event_seq gap: expected ${expectedEventSeq}, received ${String(row.eventSeq)}`,
);
}
const event = encodeCanonicalRuntimeEvent(row.event).event;
if (event.partial === true) {
throw new Error(`immutable RuntimeEvent prefix contains partial snapshot ${event.id}`);
}
if (
event.sessionId !== canonicalIdentity.sessionId ||
event.invocationId !== canonicalIdentity.invocationId ||
event.runId !== canonicalIdentity.runId ||
event.turnId !== canonicalIdentity.turnId
) {
throw new Error(`immutable RuntimeEvent identity mismatch for ${event.id}`);
}
canonicalRows.push({ eventSeq: row.eventSeq, event });
}
return canonicalRows;
}
function digestCanonicalRuntimePrefix(
identity: RuntimePrefixIdentityV1,
rows: readonly RuntimePrefixRowV1[],
): RuntimeBoundaryDigest {
const chunks = [
lengthPrefixed(utf8('maka.runtime-prefix.v1')),
lengthPrefixed(utf8(stableJsonStringify(identity))),
];
for (const row of rows) {
chunks.push(uint64be(row.eventSeq));
chunks.push(lengthPrefixed(utf8(encodeCanonicalRuntimeEvent(row.event).json)));
}
return `sha256:${sha256Hex(concatBytes(...chunks))}`;
}
function decodePrefixIdentity(value: unknown): RuntimePrefixIdentityV1 {
if (
!isRecord(value) ||
!hasExactKeys(value, ['sessionId', 'invocationId', 'runId', 'turnId']) ||
!isNonEmptyString(value.sessionId) ||
!isNonEmptyString(value.invocationId) ||
!isNonEmptyString(value.runId) ||
!isNonEmptyString(value.turnId)
) {
throw new Error('Invalid RuntimeEvent prefix identity');
}
return {
sessionId: value.sessionId,
invocationId: value.invocationId,
runId: value.runId,
turnId: value.turnId,
};
}
function decodePrefixPosition(value: unknown): RuntimePrefixPositionV1 {
if (
!isRecord(value) ||
!hasExactKeys(value, ['lastEventSeq', 'eventCount', 'lastEventId']) ||
!Number.isSafeInteger(value.lastEventSeq) ||
(value.lastEventSeq as number) <= 0 ||
!Number.isSafeInteger(value.eventCount) ||
value.eventCount !== value.lastEventSeq ||
!isNonEmptyString(value.lastEventId)
) {
throw new Error('Invalid RuntimeEvent prefix position');
}
return {
lastEventSeq: value.lastEventSeq as number,
eventCount: value.eventCount as number,
lastEventId: value.lastEventId,
};
}
function decodeBoundaryDigest(value: unknown): RuntimeBoundaryDigest {
if (typeof value !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(value)) {
throw new Error('Invalid RuntimeEvent boundary digest');
}
return value as RuntimeBoundaryDigest;
}
function utf8(value: string): Uint8Array {
return new TextEncoder().encode(value);
}
function lengthPrefixed(bytes: Uint8Array): Uint8Array {
return concatBytes(uint64be(bytes.byteLength), bytes);
}
function uint64be(value: number): Uint8Array {
if (!Number.isSafeInteger(value) || value < 0) {
throw new Error('RuntimeEvent boundary length is not a safe integer');
}
const bytes = new Uint8Array(8);
new DataView(bytes.buffer).setBigUint64(0, BigInt(value), false);
return bytes;
}
function concatBytes(...chunks: readonly Uint8Array[]): Uint8Array {
const result = new Uint8Array(chunks.reduce((length, chunk) => length + chunk.byteLength, 0));
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.byteLength;
}
return result;
}
function hasExactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
const actual = Object.keys(value).sort();
const expected = [...keys].sort();
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
}
function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.length > 0;
}