blob: c9a0cdf08563bc1d1c257599e688a4a29377b11b [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 {
INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID,
preservesHostedExecutionEnvironment,
RUNTIME_HOST_PROTOCOL_VERSION,
type HostedExecutionProjection,
type HostedExecutionStartInput,
} from '../protocol/index.js';
import { connectOwnedRuntimeHost } from './connect-or-spawn.js';
import type { RuntimeHostConnection } from './connection.js';
import { configureHostedExecutionTarget } from './hosted-execution-target.js';
export interface RunHostedExecutionInput {
readonly rootPath: string;
readonly execution: HostedExecutionClientStartInput;
readonly baseUrl?: string;
readonly signal?: AbortSignal;
readonly hostSettlementTimeoutMs?: number;
}
type HostedExecutionClientStartInput = Omit<HostedExecutionStartInput, 'session'> & {
readonly session: Omit<HostedExecutionStartInput['session'], 'modelTarget'> & {
readonly modelTarget:
| { readonly kind: 'default' }
| {
readonly kind: 'explicit';
readonly connectionSlug: string;
readonly model: string;
};
};
};
interface RunHostedExecutionDependencies {
readonly connectOwnedRuntimeHost: typeof connectOwnedRuntimeHost;
}
const defaultDependencies: RunHostedExecutionDependencies = { connectOwnedRuntimeHost };
export async function runHostedExecution(
input: RunHostedExecutionInput,
): Promise<HostedExecutionProjection> {
return runHostedExecutionWithDependencies(input, defaultDependencies);
}
export async function runHostedExecutionWithDependencies(
input: RunHostedExecutionInput,
dependencies: RunHostedExecutionDependencies,
): Promise<HostedExecutionProjection> {
if (input.signal?.aborted) {
return indeterminate(input.execution.executionId, 'Hosted execution was cancelled');
}
const initial = await dependencies.connectOwnedRuntimeHost({
rootPath: input.rootPath,
protocol: {
min: RUNTIME_HOST_PROTOCOL_VERSION,
max: RUNTIME_HOST_PROTOCOL_VERSION,
},
compositionId: INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID,
...(input.signal ? { signal: input.signal } : {}),
});
if (initial.kind !== 'connected') {
if (input.signal?.aborted) {
return indeterminate(input.execution.executionId, 'Hosted execution was cancelled');
}
const cause = initial.kind === 'failed' ? initial.reason : initial.kind;
return indeterminate(input.execution.executionId, `Runtime Host did not start: ${cause}`);
}
let connected: Extract<
Awaited<ReturnType<typeof connectOwnedRuntimeHost>>,
{ kind: 'connected' }
> = initial;
let projection: HostedExecutionProjection;
try {
input.signal?.throwIfAborted();
const target = input.execution.session.modelTarget;
let exactTarget: HostedExecutionStartInput['session']['modelTarget'] = { kind: 'default' };
if (target.kind === 'explicit') {
if (!input.baseUrl) throw new Error('Explicit model target requires baseUrl');
const configured = await configureHostedExecutionTarget(
connected.connection,
{
connectionSlug: target.connectionSlug,
model: target.model,
baseUrl: input.baseUrl,
},
input.signal,
);
exactTarget = {
kind: 'explicit',
connectionId: configured.connectionId,
connectionSlug: configured.connectionSlug,
model: target.model,
};
if (configured.changed) {
await connected.connection.close().catch(() => undefined);
if (!(await connected.host.settle(input.hostSettlementTimeoutMs ?? 15_000))) {
return indeterminate(input.execution.executionId, 'Runtime Host did not exit cleanly');
}
const reconnected = await dependencies.connectOwnedRuntimeHost({
rootPath: input.rootPath,
protocol: {
min: RUNTIME_HOST_PROTOCOL_VERSION,
max: RUNTIME_HOST_PROTOCOL_VERSION,
},
compositionId: INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID,
...(input.signal ? { signal: input.signal } : {}),
});
if (reconnected.kind !== 'connected') {
if (input.signal?.aborted) {
return indeterminate(input.execution.executionId, 'Hosted execution was cancelled');
}
const cause = reconnected.kind === 'failed' ? reconnected.reason : reconnected.kind;
return indeterminate(input.execution.executionId, `Runtime Host did not start: ${cause}`);
}
connected = reconnected;
}
}
projection = await executeHostedExecution(
connected.connection,
{
...input.execution,
session: { ...input.execution.session, modelTarget: exactTarget },
},
input.signal,
);
} catch {
projection = input.signal?.aborted
? indeterminate(input.execution.executionId, 'Hosted execution was cancelled')
: indeterminate(
input.execution.executionId,
'Runtime Host connection failed before execution settlement',
);
} finally {
await connected.connection.close().catch(() => undefined);
}
if (preservesHostedExecutionEnvironment(projection)) {
connected.host.releaseToEnvironment();
return projection;
}
const clean = await connected.host.settle(input.hostSettlementTimeoutMs ?? 15_000);
return clean
? projection
: indeterminate(input.execution.executionId, 'Runtime Host did not exit cleanly');
}
async function executeHostedExecution(
connection: Pick<RuntimeHostConnection, 'request'>,
execution: HostedExecutionStartInput,
signal: AbortSignal | undefined,
): Promise<HostedExecutionProjection> {
const cancel = () => {
void connection
.request('hosted.execution.cancel', { executionId: execution.executionId })
.catch(() => undefined);
};
signal?.addEventListener('abort', cancel, { once: true });
if (signal?.aborted) cancel();
try {
return await connection.request('hosted.execution.start', execution);
} finally {
signal?.removeEventListener('abort', cancel);
}
}
function indeterminate(executionId: string, failureReason: string): HostedExecutionProjection {
return { executionId, kind: 'indeterminate', failureReason };
}