blob: 706ff0c5e3002528a8952528203e683f3d3f6f10 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { randomUUID } from 'node:crypto';
import type { SessionHeader } from '@maka/core/session';
import { RuntimeContextCompactError } from '@maka/runtime/runtime-kernel';
import {
RuntimeHostedRootConflictError,
RuntimeHostedRootUnavailableError,
} from '@maka/runtime/message-authority';
import { type SessionManager } from '@maka/runtime/session-manager';
import { isSessionNotFoundError } from '@maka/storage/execution-stores';
import type {
ContextCompactInput,
ContextCompactResult,
ContextDiagnosticsQueryInput,
OperationOutcome,
} from '../protocol/index.js';
import type { ContextOperationHandlerMap } from './operation-dispatcher.js';
import { runtimeHostExternalTurnUnavailableReason } from './host-session-availability.js';
import type {
HostedExecutionAdmission,
HostedExecutionAuthority,
HostedExecutionIdentity,
} from './hosted-execution-authority.js';
import { isHostedExecutionTerminal } from './hosted-execution-authority.js';
type ContextRuntime = Pick<
SessionManager,
'compactSession' | 'getContextDiagnostics' | 'listTurns' | 'preflightContextCompaction'
>;
type ContextExecutionAuthority = Pick<
HostedExecutionAuthority,
'admit' | 'lookup' | 'prepare' | 'reconcile'
>;
export interface HostContextCoordinatorOptions {
readonly runtime: ContextRuntime;
readonly executions: ContextExecutionAuthority;
readonly sessions: {
readHeaderSnapshot(sessionId: string): Promise<SessionHeader>;
};
readonly requestDrain: () => void;
readonly newId?: () => string;
}
/** Owns Interactive context inspection and explicit compaction policy. */
export class HostContextCoordinator {
readonly handlers: ContextOperationHandlerMap = {
'context.diagnostics.query': (input) => this.#queryDiagnostics(input),
'context.compact': (input) => this.#compact(input),
};
readonly #runtime: ContextRuntime;
readonly #executions: ContextExecutionAuthority;
readonly #sessions: HostContextCoordinatorOptions['sessions'];
readonly #requestDrain: () => void;
readonly #newId: () => string;
constructor(options: HostContextCoordinatorOptions) {
this.#runtime = options.runtime;
this.#executions = options.executions;
this.#sessions = options.sessions;
this.#requestDrain = options.requestDrain;
this.#newId = options.newId ?? randomUUID;
}
async #queryDiagnostics(
input: ContextDiagnosticsQueryInput,
): Promise<OperationOutcome<'context.diagnostics.query'>> {
try {
await this.#sessions.readHeaderSnapshot(input.sessionId);
return {
ok: true,
result: await this.#runtime.getContextDiagnostics(input.sessionId),
};
} catch (error) {
if (isSessionNotFoundError(error)) return notFound('Session does not exist');
throw error;
}
}
async #compact(input: ContextCompactInput): Promise<OperationOutcome<'context.compact'>> {
let prepared:
| Extract<ReturnType<ContextExecutionAuthority['prepare']>, { kind: 'prepared' }>['admission']
| undefined;
try {
const existing = await this.#executions.lookup(input.sessionId, input.turnId);
if (existing && existing.descriptor.kind !== 'context_compact') {
return operationConflict('Turn identity belongs to a different execution kind');
}
if (existing) {
const admitted = await this.#executions.admit(this.#admission(input, existing));
return {
ok: true,
result: this.#compactResult(admitted.snapshot),
};
}
let preparation = this.#executions.prepare(input.sessionId);
if (preparation.kind === 'busy' && preparation.execution) {
const active = await this.#executions.reconcile(preparation.execution);
if (isHostedExecutionTerminal(active)) {
await preparation.whenIdle;
preparation = this.#executions.prepare(input.sessionId);
}
}
if (preparation.kind === 'busy') {
return sessionBusy('Session already has an active or pending root Turn');
}
if (preparation.kind === 'unavailable') {
return operationUnavailable(preparation.reason);
}
prepared = preparation.admission;
const rejected = await this.#preflight(input);
if (rejected) return rejected;
const identity: HostedExecutionIdentity = {
sessionId: input.sessionId,
turnId: input.turnId,
runId: this.#newId(),
userMessageId: null,
descriptor: { kind: 'context_compact' },
};
const admitted = await prepared.admit(this.#admission(input, identity));
return {
ok: true,
result: this.#compactResult(admitted.snapshot),
};
} catch (error) {
if (error instanceof RuntimeHostedRootConflictError) {
return sessionBusy(error.message);
}
if (error instanceof RuntimeHostedRootUnavailableError) {
return operationUnavailable(error.message);
}
this.#requestDrain();
throw error;
} finally {
prepared?.release();
}
}
#compactResult(turn: import('../protocol/index.js').TurnSnapshot): ContextCompactResult {
if (turn.status === 'completed') {
if (!turn.contextCompactionOutcome) {
return {
kind: 'finished',
turn,
outcome: { kind: 'failed', reason: 'missing_durable_outcome' },
};
}
return {
kind: 'finished',
turn,
outcome: turn.contextCompactionOutcome,
};
}
if (turn.status === 'failed' || turn.status === 'cancelled') {
return {
kind: 'finished',
turn,
outcome: {
kind: 'failed',
reason: turn.status === 'failed' ? turn.failureClass : turn.abortSource,
},
};
}
return { kind: 'started', turn };
}
async #preflight(
input: ContextCompactInput,
): Promise<OperationOutcome<'context.compact'> | undefined> {
let header: SessionHeader;
try {
header = await this.#sessions.readHeaderSnapshot(input.sessionId);
} catch (error) {
if (isSessionNotFoundError(error)) return notFound('Session does not exist');
throw error;
}
if (header.isArchived) {
return sessionArchived('Cannot compact an archived Session');
}
const unavailableReason = runtimeHostExternalTurnUnavailableReason(header);
if (unavailableReason) return operationUnavailable(unavailableReason);
if (
(await this.#runtime.listTurns(input.sessionId)).some((turn) => turn.turnId === input.turnId)
) {
return operationConflict('Turn identity already exists');
}
try {
await this.#runtime.preflightContextCompaction(input.sessionId);
} catch (error) {
if (error instanceof RuntimeContextCompactError) {
return error.code === 'session_busy'
? sessionBusy(error.message)
: operationUnavailable(error.message);
}
throw error;
}
return undefined;
}
#admission(
input: ContextCompactInput,
identity: HostedExecutionIdentity,
): HostedExecutionAdmission {
return {
sessionId: identity.sessionId,
turnId: identity.turnId,
runId: identity.runId,
userMessageId: identity.userMessageId,
execution: identity.descriptor,
content: null,
start: ({ runId, userMessageId, onRunStarted }) => {
if (runId !== identity.runId || userMessageId !== null) {
throw new Error('Hosted Execution changed the context compact identity');
}
return this.#runtime.compactSession(input.sessionId, {
turnId: input.turnId,
hostedRoot: { runId, onRunStarted },
});
},
};
}
}
function notFound(message: string) {
return { ok: false, error: { code: 'not_found', message } } as const;
}
function sessionBusy(message: string) {
return { ok: false, error: { code: 'session_busy', message } } as const;
}
function sessionArchived(message: string) {
return { ok: false, error: { code: 'session_archived', message } } as const;
}
function operationUnavailable(message: string) {
return {
ok: false,
error: { code: 'operation_unavailable', message },
} as const;
}
function operationConflict(message: string) {
return { ok: false, error: { code: 'operation_conflict', message } } as const;
}