blob: 620b9066ebd82cb3f3d8f2000b984700b9d3c33f [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 { GoalCheckpoint, GoalControlLease } from '@maka/runtime/goal-state';
import type { GoalTurnAdmission, GoalTurnOutcome } from '@maka/runtime/goal-continuation';
import type { SessionManager } from '@maka/runtime/session-manager';
import type {
HostedExecutionAuthority,
HostedExecutionSnapshot,
} from './hosted-execution-authority.js';
import { waitForHostedExecutionTerminal } from './hosted-execution-wait.js';
type GoalExecutionAuthority = Pick<HostedExecutionAuthority, 'prepare' | 'reconcile' | 'subscribe'>;
type GoalRuntime = Pick<SessionManager, 'sendMessage'>;
export interface HostGoalExecutionCoordinatorOptions {
readonly executions: GoalExecutionAuthority;
readonly runtime: GoalRuntime;
readonly matchesActive: (
sessionId: string,
checkpoint: GoalCheckpoint,
controlLease: GoalControlLease,
) => boolean;
readonly newId?: () => string;
}
/** Owns the Goal Domain's mapping onto Hosted Execution admission and terminal facts. */
export class HostGoalExecutionCoordinator {
readonly #executions: GoalExecutionAuthority;
readonly #runtime: GoalRuntime;
readonly #matchesActive: HostGoalExecutionCoordinatorOptions['matchesActive'];
readonly #newId: () => string;
readonly #prepared = new Set<
Extract<ReturnType<GoalExecutionAuthority['prepare']>, { kind: 'prepared' }>['admission']
>();
#draining = false;
constructor(options: HostGoalExecutionCoordinatorOptions) {
this.#executions = options.executions;
this.#runtime = options.runtime;
this.#matchesActive = options.matchesActive;
this.#newId = options.newId ?? randomUUID;
}
admitTurn(
sessionId: string,
text: string,
checkpoint: GoalCheckpoint,
controlLease: GoalControlLease,
): GoalTurnAdmission {
if (this.#draining) {
return {
kind: 'unavailable',
reason: 'Runtime Host Goal execution is draining.',
};
}
const preparation = this.#executions.prepare(sessionId);
if (preparation.kind === 'busy') return preparation;
if (preparation.kind === 'unavailable') return preparation;
const execution = {
sessionId,
turnId: this.#newId(),
runId: this.#newId(),
};
const userMessageId = this.#newId();
let started = false;
let gateCancelled = false;
const prepared = preparation.admission;
this.#prepared.add(prepared);
return {
kind: 'prepared',
turnId: execution.turnId,
execution,
start: async () => {
if (started) {
return {
kind: 'errored',
turnId: execution.turnId,
reason: 'Goal continuation admission was started more than once.',
};
}
started = true;
if (this.#draining || !this.#matchesActive(sessionId, checkpoint, controlLease)) {
prepared.release();
this.#prepared.delete(prepared);
return {
kind: 'errored',
turnId: execution.turnId,
reason: 'Goal continuation is unavailable for this Session.',
};
}
try {
const initial = await prepared.admit({
...execution,
userMessageId,
execution: { kind: 'goal', goalId: checkpoint.goalId },
content: { text },
admitExecution: async () => {
gateCancelled =
this.#draining || !this.#matchesActive(sessionId, checkpoint, controlLease);
return gateCancelled ? 'cancelled' : 'executing';
},
start: ({ runId, userMessageId: admittedMessageId, onRunStarted }) => {
if (runId !== execution.runId || admittedMessageId !== userMessageId) {
throw new Error('Hosted Execution changed the Goal continuation identity');
}
return this.#runtime.sendMessage(
sessionId,
{
turnId: execution.turnId,
text,
origin: { kind: 'goal', goalId: checkpoint.goalId },
},
{
runId,
userMessageId,
durability: 'required',
onRunStarted: async (startedRunId) => {
if (startedRunId !== runId) {
throw new Error('Runtime changed the Goal continuation Run identity');
}
await onRunStarted();
},
},
);
},
});
const terminal = await waitForHostedExecutionTerminal(
this.#executions,
execution,
initial.snapshot,
{ completion: initial.completion },
);
await initial.settled;
return goalTurnOutcomeFromHostedExecution(terminal);
} catch (error) {
return {
kind: 'errored',
turnId: execution.turnId,
reason: gateCancelled
? 'Goal continuation is unavailable for this Session.'
: errorMessage(error),
};
} finally {
this.#prepared.delete(prepared);
}
},
};
}
beginDrain(): void {
if (this.#draining) return;
this.#draining = true;
for (const prepared of this.#prepared) prepared.release();
this.#prepared.clear();
}
}
export function goalTurnOutcomeFromHostedExecution(
snapshot: HostedExecutionSnapshot,
): GoalTurnOutcome {
if (snapshot.status === 'completed') {
return { kind: 'completed', turnId: snapshot.turnId };
}
if (snapshot.status === 'cancelled') {
return { kind: 'aborted', turnId: snapshot.turnId };
}
if (snapshot.status === 'failed') {
return {
kind: 'errored',
turnId: snapshot.turnId,
reason: `Turn ended with ${snapshot.failureClass}`,
};
}
return {
kind: 'errored',
turnId: snapshot.turnId,
reason: `Turn ended in non-terminal status ${snapshot.status}`,
};
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}