refactor(providers): centralize message handling in provider module

Move provider-specific normalizeMessage and fetchHistory logic out of the legacy
server/providers adapters and into the refactored provider classes so callers can
depend on the main provider contract instead of parallel adapter plumbing.

Add a providers service to resolve concrete providers through the registry and
delegate message normalization/history loading from realtime handlers and the
unified messages route. Add shared TypeScript message/history types and normalized
message helpers so provider implementations and callers use the same contract.

Remove the old adapter registry/files now that Claude, Codex, Cursor, and Gemini
implement the required behavior directly.
This commit is contained in:
Haileyesus
2026-04-17 14:22:29 +03:00
parent 1a6eb57043
commit 7832429011
25 changed files with 1468 additions and 1286 deletions

View File

@@ -1,7 +1,10 @@
import type {
FetchHistoryOptions,
FetchHistoryResult,
LLMProvider,
McpScope,
McpTransport,
NormalizedMessage,
ProviderMcpServer,
UpsertProviderMcpServerInput,
} from '@/shared/types.js';
@@ -30,11 +33,16 @@ export interface IProviderMcpRuntime {
}>;
}
/**
* Provider contract that both SDK and CLI families implement.
* Main provider contract for CLI and SDK integrations.
*
* Each concrete provider owns its MCP runtime plus the provider-specific logic
* for converting native events/history into the app's normalized message shape.
*/
export interface IProvider {
readonly id: LLMProvider;
readonly mcp: IProviderMcpRuntime;
}
normalizeMessage(raw: unknown, sessionId: string | null): NormalizedMessage[];
fetchHistory(sessionId: string, options?: FetchHistoryOptions): Promise<FetchHistoryResult>;
}

View File

@@ -20,6 +20,93 @@ export type LLMProvider = 'claude' | 'codex' | 'gemini' | 'cursor';
// ---------------------------------------------------------------------------------------------
export type MessageKind =
| 'text'
| 'tool_use'
| 'tool_result'
| 'thinking'
| 'stream_delta'
| 'stream_end'
| 'error'
| 'complete'
| 'status'
| 'permission_request'
| 'permission_cancelled'
| 'session_created'
| 'interactive_prompt'
| 'task_notification';
/**
* Provider-neutral message event emitted over REST and realtime transports.
*
* Providers all produce their own native SDK/CLI event shapes, so this type keeps
* the common envelope strict while allowing provider-specific details to ride
* along as optional properties.
*/
export type NormalizedMessage = {
id: string;
sessionId: string;
timestamp: string;
provider: LLMProvider;
kind: MessageKind;
role?: 'user' | 'assistant';
content?: string;
images?: unknown;
toolName?: string;
toolInput?: unknown;
toolId?: string;
toolResult?: {
content?: string;
isError?: boolean;
toolUseResult?: unknown;
};
isError?: boolean;
text?: string;
tokens?: number;
canInterrupt?: boolean;
requestId?: string;
input?: unknown;
context?: unknown;
reason?: string;
newSessionId?: string;
status?: string;
summary?: string;
tokenBudget?: unknown;
subagentTools?: unknown;
toolUseResult?: unknown;
sequence?: number;
rowid?: number;
[key: string]: unknown;
};
/**
* Pagination and provider lookup options for reading persisted session history.
*/
export type FetchHistoryOptions = {
/** Claude project folder name. Required by Claude history lookup. */
projectName?: string;
/** Absolute workspace path. Required by Cursor to compute its chat hash. */
projectPath?: string;
/** Page size. `null` means all messages. */
limit?: number | null;
/** Pagination offset from the newest messages. */
offset?: number;
};
/**
* Provider-neutral history result returned by the unified messages endpoint.
*/
export type FetchHistoryResult = {
messages: NormalizedMessage[];
total: number;
hasMore: boolean;
offset: number;
limit: number | null;
tokenUsage?: unknown;
};
// ---------------------------------------------------------------------------------------------
export type AppErrorOptions = {
code?: string;
statusCode?: number;

View File

@@ -1,10 +1,25 @@
import { randomUUID } from 'node:crypto';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import type { NextFunction, Request, RequestHandler, Response } from 'express';
import type { ApiErrorShape, ApiSuccessShape, AppErrorOptions } from '@/shared/types.js';
import type {
ApiErrorShape,
ApiSuccessShape,
AppErrorOptions,
NormalizedMessage,
} from '@/shared/types.js';
type NormalizedMessageInput =
{
kind: NormalizedMessage['kind'];
provider: NormalizedMessage['provider'];
id?: string | null;
sessionId?: string | null;
timestamp?: string | null;
} & Record<string, unknown>;
export function createApiSuccessResponse<TData>(
data: TData,
@@ -55,6 +70,33 @@ export class AppError extends Error {
// -------------------------------------------------------------------------------------------
// ------------------------ Normalized provider message helpers ------------------------
/**
* Generates a stable unique id for normalized provider messages.
*/
export function generateMessageId(prefix = 'msg'): string {
return `${prefix}_${randomUUID()}`;
}
/**
* Creates a normalized provider message and fills the shared envelope fields.
*
* Provider adapters and live SDK handlers pass through provider-specific fields,
* while this helper guarantees every emitted event has an id, session id,
* timestamp, and provider marker.
*/
export function createNormalizedMessage(fields: NormalizedMessageInput): NormalizedMessage {
return {
...fields,
id: fields.id || generateMessageId(fields.kind),
sessionId: fields.sessionId || '',
timestamp: fields.timestamp || new Date().toISOString(),
provider: fields.provider,
};
}
// -------------------------------------------------------------------------------------------
// ------------------------ The following are mainly for provider MCP runtimes ------------------------
/**
* Safely narrows an unknown value to a plain object record.