refactor(chat): route browser chat through one runtime (P3 Slice-Zero Task 5) (#1172)
ci/woodpecker/push/publish Pipeline failed
ci/woodpecker/push/publish Pipeline failed
Co-authored-by: shaggy <[email protected]>
This commit was merged in pull request #1172.
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user