ci/woodpecker/push/publish Pipeline failed
Co-authored-by: shaggy <[email protected]>
174 lines
6.1 KiB
TypeScript
174 lines
6.1 KiB
TypeScript
import { Injectable, type OnModuleInit } from '@nestjs/common';
|
|
import { HarnessRegistry } from '../harness/harness.registry.js';
|
|
import {
|
|
isHarnessConversationServiceAvailable,
|
|
type HarnessConversationServiceBinding,
|
|
} from '../harness/harness.tokens.js';
|
|
import type {
|
|
ChatRuntime,
|
|
ChatRuntimeMode,
|
|
LegacyBrowserMessagePayload,
|
|
LegacyEmbeddedChatPort,
|
|
LegacyRuntimeResult,
|
|
LegacyRuntimeStream,
|
|
LegacySessionPresentation,
|
|
LegacySocketTurnLease,
|
|
OwnedConversationContext,
|
|
VerifiedDiscordIngressContext,
|
|
VerifiedDiscordTurnLease,
|
|
} from './chat-runtime.js';
|
|
import { ChatRuntimeUnavailableError, resolveChatRuntimeMode } from './chat-runtime.js';
|
|
|
|
/** The fixed fail-closed result for a legacy browser operation issued under `pi-rpc`. */
|
|
const RUNTIME_UNSUPPORTED = {
|
|
ok: false as const,
|
|
code: 'runtime_unsupported' as const,
|
|
retryable: false as const,
|
|
};
|
|
|
|
/**
|
|
* Resolves the one live {@link ChatRuntime} for this process and enforces the
|
|
* `pi-rpc` readiness preconditions at module init — before the gateway accepts
|
|
* traffic. It never falls back from `pi-rpc` to embedded execution: an unmet
|
|
* `pi-rpc` precondition is a typed startup failure ({@link ChatRuntimeUnavailableError}),
|
|
* and until `onModuleInit` selects a runtime, {@link active} throws rather than
|
|
* exposing any runtime — a failed `pi-rpc` init can never leak the embedded one.
|
|
*/
|
|
@Injectable()
|
|
export class ChatRuntimeRouter implements OnModuleInit, LegacyEmbeddedChatPort {
|
|
private readonly mode: ChatRuntimeMode;
|
|
|
|
/** The single resolved runtime. Undefined until a successful `onModuleInit`. */
|
|
private resolved: ChatRuntime | undefined;
|
|
|
|
constructor(
|
|
private readonly harnessRegistry: HarnessRegistry,
|
|
private readonly conversationService: HarnessConversationServiceBinding,
|
|
private readonly embedded: ChatRuntime,
|
|
private readonly harness: ChatRuntime,
|
|
mode: ChatRuntimeMode = resolveChatRuntimeMode(),
|
|
) {
|
|
this.mode = mode;
|
|
}
|
|
|
|
onModuleInit(): void {
|
|
if (this.mode === 'legacy') {
|
|
// Legacy ignores the pi-rpc preconditions entirely and always runs embedded.
|
|
this.resolved = this.embedded;
|
|
return;
|
|
}
|
|
|
|
// pi-rpc: both preconditions are hard startup failures, checked in a fixed order.
|
|
if (!this.harnessRegistry.has('pi')) {
|
|
this.resolved = undefined;
|
|
throw new ChatRuntimeUnavailableError('adapter_unavailable');
|
|
}
|
|
if (!isHarnessConversationServiceAvailable(this.conversationService)) {
|
|
this.resolved = undefined;
|
|
throw new ChatRuntimeUnavailableError('conversation_service_unavailable');
|
|
}
|
|
|
|
this.resolved = this.harness;
|
|
}
|
|
|
|
get active(): ChatRuntime {
|
|
if (this.resolved === undefined) {
|
|
// Reached only if init has not run or failed closed; never expose a runtime here.
|
|
throw new Error('The chat runtime is not available: startup did not resolve a runtime.');
|
|
}
|
|
return this.resolved;
|
|
}
|
|
|
|
/**
|
|
* The process-wide mode, available before {@link onModuleInit}. Production handlers read
|
|
* this to fail a legacy browser turn closed under `pi-rpc` *before* parsing the payload as
|
|
* either browser-legacy input or a Discord envelope — never to branch into a fallback.
|
|
*/
|
|
get runtimeMode(): ChatRuntimeMode {
|
|
return this.mode;
|
|
}
|
|
|
|
/**
|
|
* The embedded runtime narrowed to its port. Only reached on the legacy path (and for the
|
|
* verified-Discord op in both modes), where the injected runtime is always a real
|
|
* `EmbeddedChatRuntime`. The router spec constructs the router with a bare `{ kind }` stub
|
|
* but never invokes a port op, so this narrowing is never exercised against the stub.
|
|
*/
|
|
private get embeddedPort(): LegacyEmbeddedChatPort {
|
|
return this.embedded as unknown as LegacyEmbeddedChatPort;
|
|
}
|
|
|
|
// --- LegacyEmbeddedChatPort: legacy browser operations fail closed under pi-rpc ---
|
|
|
|
completeLegacyRestTurn(
|
|
context: OwnedConversationContext,
|
|
input: Readonly<{ content: string }>,
|
|
): Promise<
|
|
LegacyRuntimeResult<Readonly<{ text: string; presentation: LegacySessionPresentation }>>
|
|
> {
|
|
if (this.mode === 'pi-rpc') {
|
|
return Promise.resolve(RUNTIME_UNSUPPORTED);
|
|
}
|
|
return this.embeddedPort.completeLegacyRestTurn(context, input);
|
|
}
|
|
|
|
prepareLegacySocketTurn(
|
|
context: OwnedConversationContext,
|
|
input: LegacyBrowserMessagePayload,
|
|
stream: LegacyRuntimeStream,
|
|
): Promise<LegacyRuntimeResult<LegacySocketTurnLease>> {
|
|
if (this.mode === 'pi-rpc') {
|
|
return Promise.resolve(RUNTIME_UNSUPPORTED);
|
|
}
|
|
return this.embeddedPort.prepareLegacySocketTurn(context, input, stream);
|
|
}
|
|
|
|
setLegacyThinking(
|
|
context: OwnedConversationContext,
|
|
level: string,
|
|
): LegacyRuntimeResult<LegacySessionPresentation> {
|
|
if (this.mode === 'pi-rpc') {
|
|
return RUNTIME_UNSUPPORTED;
|
|
}
|
|
return this.embeddedPort.setLegacyThinking(context, level);
|
|
}
|
|
|
|
abortLegacyTurn(context: OwnedConversationContext): Promise<LegacyRuntimeResult<void>> {
|
|
if (this.mode === 'pi-rpc') {
|
|
return Promise.resolve(RUNTIME_UNSUPPORTED);
|
|
}
|
|
return this.embeddedPort.abortLegacyTurn(context);
|
|
}
|
|
|
|
applyLegacyModelOverride(
|
|
context: OwnedConversationContext,
|
|
modelId: string,
|
|
): LegacyRuntimeResult<LegacySessionPresentation> {
|
|
if (this.mode === 'pi-rpc') {
|
|
return RUNTIME_UNSUPPORTED;
|
|
}
|
|
return this.embeddedPort.applyLegacyModelOverride(context, modelId);
|
|
}
|
|
|
|
readLegacySessionPresentation(
|
|
context: OwnedConversationContext,
|
|
): LegacyRuntimeResult<LegacySessionPresentation> {
|
|
if (this.mode === 'pi-rpc') {
|
|
return RUNTIME_UNSUPPORTED;
|
|
}
|
|
return this.embeddedPort.readLegacySessionPresentation(context);
|
|
}
|
|
|
|
/**
|
|
* Verified Discord ingress bypasses browser mode: it is embedded-only in BOTH modes and
|
|
* never reaches the harness or routing-engine selection. It is reached only through a
|
|
* {@link VerifiedDiscordIngressContext}, which exists only after every ingress check.
|
|
*/
|
|
dispatchVerifiedDiscordIngress(
|
|
context: VerifiedDiscordIngressContext,
|
|
stream: LegacyRuntimeStream,
|
|
): Promise<LegacyRuntimeResult<VerifiedDiscordTurnLease>> {
|
|
return this.embeddedPort.dispatchVerifiedDiscordIngress(context, stream);
|
|
}
|
|
}
|