refactor(ts): complete legacy host removal — drop fastify/commander/zod, delete MCP SDK server, remove ManagedRuntime facades

Finishes the remaining EFFECT_NATIVE_REWRITE_PLAN stages in one verified slice:
- fastify, @fastify/websocket, commander, zod removed from all package manifests
- legacy @modelcontextprotocol/sdk stdio server deleted; effect/unstable/ai McpServer is canonical
- no ManagedRuntime or Effect.runPromise program facades remain in production source
- gateway server/rpc-contract and client rpc/socket moved onto Effect v4 native http/rpc/socket layers

Gates (force-run, no cache): check:tsgo, build, test (96 tests / 11 tasks) all green.
Native-class inventory: zero blocking production classes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
elpresidank 2026-06-11 06:29:29 -05:00
parent a26463afc1
commit cf12defcd8
30 changed files with 1506 additions and 456 deletions

View file

@ -13,7 +13,7 @@ describe("FlowsApi", () => {
makeRequest: vi.fn(),
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
flowsApi = new FlowsApi(mockApi as any);
flowsApi = FlowsApi(mockApi as any);
});
describe("startFlow", () => {

View file

@ -23,7 +23,7 @@ describe("workbench API contracts", () => {
values: [{ type: "prompt", key: "welcome", value: "hello" }],
});
const result = await new ConfigApi(base).getValues("prompt");
const result = await ConfigApi(base).getValues("prompt");
expect(makeRequest).toHaveBeenCalledWith(
"config",
@ -45,7 +45,7 @@ describe("workbench API contracts", () => {
],
});
const result = await new ConfigApi(base).getTokenCosts();
const result = await ConfigApi(base).getTokenCosts();
expect(result).toEqual([
{ model: "gpt-test", input_price: 0.1, output_price: 0.2 },
@ -55,7 +55,7 @@ describe("workbench API contracts", () => {
it("writes and deletes config using Python-style key/value arrays", async () => {
const { base, makeRequest } = makeApi();
makeRequest.mockResolvedValue({});
const config = new ConfigApi(base);
const config = ConfigApi(base);
await config.putConfig([{ type: "tool", key: "search", value: "{}" }]);
await config.deleteConfig({ type: "tool", key: "search" });
@ -86,7 +86,7 @@ describe("workbench API contracts", () => {
const { base, makeRequest } = makeApi();
const document = { id: "doc-1", title: "Document" };
const processing = { id: "proc-1", "document-id": "doc-1" };
const librarian = new LibrarianApi(base);
const librarian = LibrarianApi(base);
makeRequest
.mockResolvedValueOnce({ "document-metadatas": [document] })
@ -101,7 +101,7 @@ describe("workbench API contracts", () => {
const document = { id: "doc-1", title: "Document" };
makeRequest.mockResolvedValue({ "document-metadata": document });
const result = await new LibrarianApi(base).getDocumentMetadata("doc-1");
const result = await LibrarianApi(base).getDocumentMetadata("doc-1");
expect(makeRequest).toHaveBeenCalledWith(
"librarian",
@ -120,7 +120,7 @@ describe("workbench API contracts", () => {
const { base, makeRequest } = makeApi();
makeRequest.mockResolvedValue({});
await new LibrarianApi(base).loadDocument(
await LibrarianApi(base).loadDocument(
"SGVsbG8=",
"text/plain",
"Hello",
@ -145,7 +145,7 @@ describe("workbench API contracts", () => {
describe("KnowledgeApi", () => {
it("lists and loads document embedding cores", async () => {
const { base, makeRequest } = makeApi();
const knowledge = new KnowledgeApi(base);
const knowledge = KnowledgeApi(base);
makeRequest
.mockResolvedValueOnce({ ids: ["de-core"] })
@ -178,7 +178,7 @@ describe("workbench API contracts", () => {
const { base, makeRequest } = makeApi();
makeRequest.mockResolvedValue({});
await new KnowledgeApi(base).unloadKgCore("kg-core", "default");
await KnowledgeApi(base).unloadKgCore("kg-core", "default");
expect(makeRequest).toHaveBeenCalledWith(
"knowledge",

View file

@ -8,6 +8,7 @@ export * from "./models/namespaces.js";
// Export socket client
export * from "./socket/trustgraph-socket.js";
export * from "./socket/effect-rpc-client.js";
export * from "./rpc/contract.js";
// Export WebSocket adapter (isomorphic helpers and types)

View file

@ -1,4 +1,5 @@
import { Schema as S } from "effect";
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
import * as Rpc from "effect/unstable/rpc/Rpc";
import * as RpcGroup from "effect/unstable/rpc/RpcGroup";
@ -32,3 +33,14 @@ export class DispatchStream extends Rpc.make("DispatchStream", {
}) {}
export const TrustGraphRpcs = RpcGroup.make(Dispatch, DispatchStream);
export class GatewayWorkbenchHttpApi extends HttpApi.make("trustgraph-gateway-workbench")
.add(
HttpApiGroup.make("workbench", { topLevel: true }).add(
HttpApiEndpoint.post("dispatch", "/api/v1/workbench/dispatch", {
payload: DispatchPayload,
success: S.Unknown,
}),
),
)
{}

View file

@ -1,4 +1,4 @@
import { Cause, Context, Effect, Fiber, Layer, ManagedRuntime, Stream, SubscriptionRef } from "effect";
import { Cause, Context, Effect, Exit, Fiber, Layer, Ref, Scope, Stream, SubscriptionRef } from "effect";
import type * as RpcGroup from "effect/unstable/rpc/RpcGroup";
import * as RpcClient from "effect/unstable/rpc/RpcClient";
import type { RpcClientError } from "effect/unstable/rpc/RpcClientError";
@ -35,24 +35,44 @@ export interface DispatchOptions {
readonly retries?: number;
}
export interface TrustGraphGatewayClient {
readonly state: Effect.Effect<RpcConnectionState>;
readonly changes: Stream.Stream<RpcConnectionState>;
readonly subscribe: (
listener: (state: RpcConnectionState) => void,
) => Effect.Effect<Effect.Effect<void>>;
readonly dispatch: (
input: DispatchInput,
options?: DispatchOptions,
) => Effect.Effect<unknown, RpcClientError | DispatchError>;
readonly dispatchStream: (
input: DispatchInput,
options?: DispatchOptions,
) => Stream.Stream<DispatchStreamChunk, RpcClientError | DispatchError>;
readonly runDispatchStream: (
input: DispatchInput,
receiver: (chunk: DispatchStreamChunk) => boolean,
options?: DispatchOptions,
) => Effect.Effect<DispatchStreamChunk | undefined, RpcClientError | DispatchError>;
readonly close: Effect.Effect<void>;
}
export class TrustGraphGatewayClientService extends Context.Service<
TrustGraphGatewayClientService,
TrustGraphGatewayClient
>()("@trustgraph/client/socket/effect-rpc-client/TrustGraphGatewayClientService") {}
export interface TrustGraphGatewayClientOptions {
readonly url: string;
readonly onConnect?: () => void;
readonly onDisconnect?: () => void;
readonly stateRef?: SubscriptionRef.SubscriptionRef<RpcConnectionState>;
readonly closedRef?: Ref.Ref<boolean>;
}
const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;
const DEFAULT_REQUEST_ATTEMPTS = 3;
type NewableFactory<Args extends readonly unknown[], A extends object> = {
new (...args: Args): A;
(...args: Args): A;
readonly prototype: A;
};
function newableFactory<Args extends readonly unknown[], A extends object>(
factory: (...args: Args) => A,
): NewableFactory<Args, A> {
function Constructor(...args: Args): A {
return factory(...args);
}
return Constructor as unknown as NewableFactory<Args, A>;
}
export interface EffectRpcClient {
readonly subscribe: (listener: (state: RpcConnectionState) => void) => () => void;
readonly dispatch: (
@ -67,131 +87,207 @@ export interface EffectRpcClient {
readonly close: () => Promise<void>;
}
const makeClientLayer = (
options: TrustGraphGatewayClientOptions,
stateRef: SubscriptionRef.SubscriptionRef<RpcConnectionState>,
closedRef: Ref.Ref<boolean>,
): Layer.Layer<TrustGraphRpcClientService> => {
const setState = (nextState: RpcConnectionState) =>
SubscriptionRef.set(stateRef, nextState);
const socketLayer = Layer.effect(
Socket.Socket,
Socket.makeWebSocket(options.url, {
closeCodeIsError: (code) => code !== 1000,
openTimeout: "10 seconds",
}),
).pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal));
const hooksLayer = Layer.succeed(
RpcClient.ConnectionHooks,
RpcClient.ConnectionHooks.of({
onConnect: Effect.gen(function* () {
yield* setState({ status: "connected" });
options.onConnect?.();
}),
onDisconnect: Effect.gen(function* () {
const closed = yield* Ref.get(closedRef);
if (!closed) {
yield* setState({
status: "connecting",
lastError: "Disconnected from gateway",
});
}
options.onDisconnect?.();
}),
}),
);
const protocolLayer = RpcClient.layerProtocolSocket({
retryTransientErrors: true,
}).pipe(
Layer.provide(socketLayer),
Layer.provide(RpcSerialization.layerNdjson),
Layer.provide(hooksLayer),
);
return Layer.effect(
TrustGraphRpcClientService,
RpcClient.make(TrustGraphRpcs),
).pipe(Layer.provide(protocolLayer));
};
const makeSubscribeEffect = Effect.fn("makeSubscribeEffect")(function* (
stateRef: SubscriptionRef.SubscriptionRef<RpcConnectionState>,
scope: Scope.Scope,
listener: (state: RpcConnectionState) => void,
) {
let latest = SubscriptionRef.getUnsafe(stateRef);
listener(latest);
let replaySeen = false;
const fiber = yield* Effect.forkIn(SubscriptionRef.changes(stateRef).pipe(
Stream.runForEach((nextState) =>
Effect.sync(() => {
if (!replaySeen) {
replaySeen = true;
if (nextState === latest) return;
}
latest = nextState;
listener(nextState);
})
),
), scope);
return yield* Effect.succeed(Fiber.interrupt(fiber).pipe(Effect.asVoid));
});
export const makeTrustGraphGatewayClientScoped: (
options: TrustGraphGatewayClientOptions,
) => Effect.Effect<TrustGraphGatewayClient, never, Scope.Scope> = Effect.fn("makeTrustGraphGatewayClientScoped")(function* (
options,
) {
const stateRef = options.stateRef ?? (yield* SubscriptionRef.make<RpcConnectionState>({ status: "connecting" }));
const closedRef = options.closedRef ?? (yield* Ref.make(false));
const scope = yield* Scope.Scope;
const context = yield* Layer.buildWithScope(makeClientLayer(options, stateRef, closedRef), scope).pipe(
Effect.tapCause((cause) =>
SubscriptionRef.set(stateRef, {
status: "failed",
lastError: Cause.pretty(cause),
})
),
);
const client = Context.get(context, TrustGraphRpcClientService);
const close = Effect.gen(function* () {
const wasClosed = yield* Ref.getAndSet(closedRef, true);
if (!wasClosed) {
yield* SubscriptionRef.set(stateRef, { status: "closed" });
}
});
yield* Effect.addFinalizer(() => close);
return {
state: SubscriptionRef.get(stateRef),
changes: SubscriptionRef.changes(stateRef),
subscribe: (listener) => makeSubscribeEffect(stateRef, scope, listener),
dispatch: (input, options = {}) =>
withDispatchRequestPolicy(client.Dispatch(DispatchPayload.make(input)), options),
dispatchStream: (input, options = {}) =>
Stream.unwrap(
withDispatchRequestPolicy(
Effect.succeed(client.DispatchStream(DispatchPayload.make(input))),
options,
),
),
runDispatchStream: (input, receiver, options = {}) => {
let last: DispatchStreamChunk | undefined;
return withDispatchRequestPolicy(
client.DispatchStream(DispatchPayload.make(input)).pipe(
Stream.runForEachWhile((chunk) =>
Effect.suspend(() => {
last = chunk;
return Effect.succeed(!receiver(chunk));
}),
),
Effect.andThen(() => Effect.succeed(last)),
),
options,
);
},
close,
} satisfies TrustGraphGatewayClient;
});
export const makeTrustGraphGatewayClientLayer = (
options: TrustGraphGatewayClientOptions,
): Layer.Layer<TrustGraphGatewayClientService> =>
Layer.effect(
TrustGraphGatewayClientService,
makeTrustGraphGatewayClientScoped(options).pipe(
Effect.map(TrustGraphGatewayClientService.of),
),
);
export function makeEffectRpcClient(
url: string,
onConnect?: () => void,
onDisconnect?: () => void,
): EffectRpcClient {
const stateRef = Effect.runSync(SubscriptionRef.make<RpcConnectionState>({ status: "connecting" }));
let closed = false;
const setState = (nextState: RpcConnectionState) =>
SubscriptionRef.set(stateRef, nextState);
const makeClientLayer = (): Layer.Layer<TrustGraphRpcClientService> => {
const socketLayer = Layer.effect(
Socket.Socket,
Socket.makeWebSocket(url, {
closeCodeIsError: (code) => code !== 1000,
openTimeout: "10 seconds",
}),
).pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal));
const hooksLayer = Layer.succeed(
RpcClient.ConnectionHooks,
RpcClient.ConnectionHooks.of({
onConnect: Effect.gen(function* () {
yield* setState({ status: "connected" });
onConnect?.();
}),
onDisconnect: Effect.gen(function* () {
if (!closed) {
yield* setState({
status: "connecting",
lastError: "Disconnected from gateway",
});
}
onDisconnect?.();
}),
}),
);
const protocolLayer = RpcClient.layerProtocolSocket({
retryTransientErrors: true,
}).pipe(
Layer.provide(socketLayer),
Layer.provide(RpcSerialization.layerNdjson),
Layer.provide(hooksLayer),
);
const clientLayer = Layer.effect(
TrustGraphRpcClientService,
RpcClient.make(TrustGraphRpcs),
).pipe(Layer.provide(protocolLayer));
return clientLayer;
const closedRef = Effect.runSync(Ref.make(false));
const scope = Effect.runSync(Scope.make());
const options: TrustGraphGatewayClientOptions = {
url,
stateRef,
closedRef,
...(onConnect === undefined ? {} : { onConnect }),
...(onDisconnect === undefined ? {} : { onDisconnect }),
};
const runtime = ManagedRuntime.make(makeClientLayer());
const clientPromise = runtime.runPromise(
TrustGraphRpcClientService.pipe(
Effect.tapCause((cause) =>
setState({
status: "failed",
lastError: Cause.pretty(cause),
})
),
),
const clientPromise = Effect.runPromise(
makeTrustGraphGatewayClientScoped(options).pipe(Scope.provide(scope)),
);
return {
subscribe: (listener) => {
let latest = SubscriptionRef.getUnsafe(stateRef);
listener(latest);
let replaySeen = false;
const fiber = Effect.runFork(
SubscriptionRef.changes(stateRef).pipe(
Stream.runForEach((nextState) =>
Effect.sync(() => {
if (!replaySeen) {
replaySeen = true;
if (nextState === latest) return;
}
latest = nextState;
listener(nextState);
})
),
),
let unsubscribe: Effect.Effect<void> | undefined;
let cancelled = false;
listener(SubscriptionRef.getUnsafe(stateRef));
void clientPromise.then((client) =>
Effect.runPromise(client.subscribe(listener)).then((release) => {
if (cancelled) {
return Effect.runPromise(release);
}
unsubscribe = release;
})
);
return () => {
Effect.runFork(Fiber.interrupt(fiber));
cancelled = true;
if (unsubscribe !== undefined) {
Effect.runFork(unsubscribe);
}
};
},
dispatch: (input, options = {}) =>
clientPromise.then((client) =>
runtime.runPromise(
withDispatchRequestPolicy(client.Dispatch(DispatchPayload.make(input)), options),
)
Effect.runPromise(client.dispatch(input, options))
),
dispatchStream: (input, receiver, options = {}) => {
let last: DispatchStreamChunk | undefined;
return clientPromise.then((client) =>
runtime.runPromise(
withDispatchRequestPolicy(
client.DispatchStream(DispatchPayload.make(input)).pipe(
Stream.runForEachWhile((chunk) =>
Effect.suspend(() => {
last = chunk;
return Effect.succeed(!receiver(chunk));
}),
),
),
options,
dispatchStream: (input, receiver, options = {}) =>
clientPromise.then((client) =>
Effect.runPromise(client.runDispatchStream(input, receiver, options))
),
close: () =>
clientPromise.then((client) =>
Effect.runPromise(
client.close.pipe(
Effect.andThen(Scope.close(scope, Exit.void)),
),
)
).then(() => last);
},
close: () => {
if (closed) return Promise.resolve();
closed = true;
Effect.runSync(setState({ status: "closed" }));
return runtime.dispose();
},
),
};
}
export const EffectRpcClient = newableFactory(makeEffectRpcClient);
export function withDispatchRequestPolicy<A, E, R>(
effect: Effect.Effect<A, E, R>,
options: DispatchOptions,

View file

@ -1,7 +1,7 @@
// Import core types and classes for the TrustGraph API
import type { Term, Triple } from "../models/Triple.js";
import {
EffectRpcClient,
type EffectRpcClient,
type DispatchInput,
type DispatchOptions,
type RpcConnectionState,
@ -445,21 +445,6 @@ function makeid(length: number) {
);
}
type NewableFactory<Args extends readonly unknown[], A extends object> = {
new (...args: Args): A;
(...args: Args): A;
readonly prototype: A;
};
function newableFactory<Args extends readonly unknown[], A extends object>(
factory: (...args: Args) => A,
): NewableFactory<Args, A> {
function Constructor(...args: Args): A {
return factory(...args);
}
return Constructor as unknown as NewableFactory<Args, A>;
}
/**
* BaseApi - Core WebSocket client for TrustGraph API
* Manages connection lifecycle, message routing, and provides base request
@ -622,27 +607,27 @@ export function makeBaseApi(
// Factory methods for creating specialized API instances
librarian() {
return new LibrarianApi(api);
return makeLibrarianApi(api);
},
flows() {
return new FlowsApi(api);
return makeFlowsApi(api);
},
flow(id: string) {
return new FlowApi(api, id);
return makeFlowApi(api, id);
},
knowledge() {
return new KnowledgeApi(api);
return makeKnowledgeApi(api);
},
config() {
return new ConfigApi(api);
return makeConfigApi(api);
},
collectionManagement() {
return new CollectionManagementApi(api);
return makeCollectionManagementApi(api);
},
};
@ -739,7 +724,7 @@ export function makeBaseApi(
}
export type BaseApi = ReturnType<typeof makeBaseApi>;
export const BaseApi = newableFactory(makeBaseApi);
export const BaseApi = makeBaseApi;
export function makeBaseApiWithRpc(
user: string,
@ -1153,7 +1138,7 @@ export function makeLibrarianApi(api: BaseApi) {
}
export type LibrarianApi = ReturnType<typeof makeLibrarianApi>;
export const LibrarianApi = newableFactory(makeLibrarianApi);
export const LibrarianApi = makeLibrarianApi;
/**
* FlowsApi - Manages processing flows and configuration
@ -1418,7 +1403,7 @@ export function makeFlowsApi(api: BaseApi) {
}
export type FlowsApi = ReturnType<typeof makeFlowsApi>;
export const FlowsApi = newableFactory(makeFlowsApi);
export const FlowsApi = makeFlowsApi;
/**
* FlowApi - Interface for interacting with a specific flow instance
@ -2205,7 +2190,7 @@ export function makeFlowApi(api: BaseApi, flowId: string) {
}
export type FlowApi = ReturnType<typeof makeFlowApi>;
export const FlowApi = newableFactory(makeFlowApi);
export const FlowApi = makeFlowApi;
/**
* ConfigApi - Dedicated configuration management interface
@ -2401,7 +2386,7 @@ export function makeConfigApi(api: BaseApi) {
}
export type ConfigApi = ReturnType<typeof makeConfigApi>;
export const ConfigApi = newableFactory(makeConfigApi);
export const ConfigApi = makeConfigApi;
/**
* KnowledgeApi - Manages knowledge graph cores and data
@ -2568,7 +2553,7 @@ export function makeKnowledgeApi(api: BaseApi) {
}
export type KnowledgeApi = ReturnType<typeof makeKnowledgeApi>;
export const KnowledgeApi = newableFactory(makeKnowledgeApi);
export const KnowledgeApi = makeKnowledgeApi;
/**
* CollectionManagementApi - Manages collections for organizing documents
@ -2677,7 +2662,7 @@ export function makeCollectionManagementApi(api: BaseApi) {
}
export type CollectionManagementApi = ReturnType<typeof makeCollectionManagementApi>;
export const CollectionManagementApi = newableFactory(makeCollectionManagementApi);
export const CollectionManagementApi = makeCollectionManagementApi;
/**
* Factory function to create a new TrustGraph WebSocket connection
@ -2690,4 +2675,4 @@ export const createTrustGraphSocket = (
user: string,
token?: string,
socketUrl?: string,
): BaseApi => new BaseApi(user, token, socketUrl);
): BaseApi => makeBaseApi(user, token, socketUrl);