refactor(ts): make client gateway effect native

This commit is contained in:
elpresidank 2026-06-11 08:06:31 -05:00
parent 174d636178
commit a7bdbb9257
16 changed files with 1168 additions and 1262 deletions

View file

@ -7,16 +7,12 @@
import { Effect } from "effect"; import { Effect } from "effect";
import * as Argument from "effect/unstable/cli/Argument"; import * as Argument from "effect/unstable/cli/Argument";
import * as Command from "effect/unstable/cli/Command"; import * as Command from "effect/unstable/cli/Command";
import { cliCommandError, withSocket, writeJson } from "./util.js"; import { gatewayDispatch, withGatewayClient, writeJson } from "./util.js";
const show = Command.make("show", {}, () => const show = Command.make("show", {}, () =>
withSocket((socket) => withGatewayClient((client) =>
Effect.gen(function* () { Effect.gen(function* () {
const cfg = socket.config(); const resp = yield* gatewayDispatch(client, "config.show", "config", { operation: "config" }, { timeoutMs: 60000 });
const resp = yield* Effect.tryPromise({
try: () => cfg.getConfigAll(),
catch: (error) => cliCommandError("config.show", error),
});
yield* writeJson(resp); yield* writeJson(resp);
}), }),
), ),
@ -25,19 +21,17 @@ const show = Command.make("show", {}, () =>
const get = Command.make("get", { const get = Command.make("get", {
key: Argument.string("key").pipe(Argument.withDescription("Config key (format: type/key)")), key: Argument.string("key").pipe(Argument.withDescription("Config key (format: type/key)")),
}, ({ key }) => }, ({ key }) =>
withSocket((socket) => withGatewayClient((client) =>
Effect.gen(function* () { Effect.gen(function* () {
const cfg = socket.config(); const parts = key.split("/");
// Support "type/key" format; fall back to using the whole string as key const configKey =
const parts = key.split("/"); parts.length >= 2
const configKey = ? { type: parts[0], key: parts.slice(1).join("/") }
parts.length >= 2 : { type: "config", key };
? { type: parts[0], key: parts.slice(1).join("/") } const resp = yield* gatewayDispatch(client, "config.get", "config", {
: { type: "config", key }; operation: "get",
const resp = yield* Effect.tryPromise({ keys: [configKey],
try: () => cfg.getConfig([configKey]), }, { timeoutMs: 60000 });
catch: (error) => cliCommandError("config.get", error),
});
yield* writeJson(resp); yield* writeJson(resp);
}), }),
), ),
@ -47,18 +41,17 @@ const set = Command.make("set", {
key: Argument.string("key").pipe(Argument.withDescription("Config key (format: type/key)")), key: Argument.string("key").pipe(Argument.withDescription("Config key (format: type/key)")),
value: Argument.string("value").pipe(Argument.withDescription("Config value (JSON)")), value: Argument.string("value").pipe(Argument.withDescription("Config value (JSON)")),
}, ({ key, value }) => }, ({ key, value }) =>
withSocket((socket) => withGatewayClient((client) =>
Effect.gen(function* () { Effect.gen(function* () {
const cfg = socket.config(); const parts = key.split("/");
const parts = key.split("/"); const configEntry =
const configEntry = parts.length >= 2
parts.length >= 2 ? { type: parts[0], key: parts.slice(1).join("/"), value }
? { type: parts[0], key: parts.slice(1).join("/"), value } : { type: "config", key, value };
: { type: "config", key, value }; const resp = yield* gatewayDispatch(client, "config.set", "config", {
const resp = yield* Effect.tryPromise({ operation: "put",
try: () => cfg.putConfig([configEntry]), values: [configEntry],
catch: (error) => cliCommandError("config.set", error), }, { timeoutMs: 60000 });
});
yield* writeJson(resp); yield* writeJson(resp);
}), }),
), ),
@ -70,13 +63,12 @@ const list = Command.make("list", {
Argument.withDefault("config"), Argument.withDefault("config"),
), ),
}, ({ type }) => }, ({ type }) =>
withSocket((socket) => withGatewayClient((client) =>
Effect.gen(function* () { Effect.gen(function* () {
const cfg = socket.config(); const resp = yield* gatewayDispatch(client, "config.list", "config", {
const resp = yield* Effect.tryPromise({ operation: "list",
try: () => cfg.list(type), type,
catch: (error) => cliCommandError("config.list", error), }, { timeoutMs: 60000 });
});
yield* writeJson(resp); yield* writeJson(resp);
}), }),
), ),
@ -85,18 +77,17 @@ const list = Command.make("list", {
const deleteCommand = Command.make("delete", { const deleteCommand = Command.make("delete", {
key: Argument.string("key").pipe(Argument.withDescription("Config key (format: type/key)")), key: Argument.string("key").pipe(Argument.withDescription("Config key (format: type/key)")),
}, ({ key }) => }, ({ key }) =>
withSocket((socket) => withGatewayClient((client) =>
Effect.gen(function* () { Effect.gen(function* () {
const cfg = socket.config(); const parts = key.split("/");
const parts = key.split("/"); const configKey =
const configKey = parts.length >= 2
parts.length >= 2 ? { type: parts[0], key: parts.slice(1).join("/") }
? { type: parts[0], key: parts.slice(1).join("/") } : { type: "config", key };
: { type: "config", key }; const resp = yield* gatewayDispatch(client, "config.delete", "config", {
const resp = yield* Effect.tryPromise({ operation: "delete",
try: () => cfg.deleteConfig(configKey), keys: [configKey],
catch: (error) => cliCommandError("config.delete", error), }, { timeoutMs: 30000 });
});
yield* writeJson(resp); yield* writeJson(resp);
}), }),
), ),

View file

@ -7,7 +7,7 @@
import { Effect } from "effect"; import { Effect } from "effect";
import * as Argument from "effect/unstable/cli/Argument"; import * as Argument from "effect/unstable/cli/Argument";
import * as Command from "effect/unstable/cli/Command"; import * as Command from "effect/unstable/cli/Command";
import { cliCommandError, withSocket, writeJson } from "./util.js"; import { gatewayDispatch, withGatewayClient, writeJson } from "./util.js";
export const embeddingsCommand = Command.make("embeddings", { export const embeddingsCommand = Command.make("embeddings", {
texts: Argument.string("text").pipe( texts: Argument.string("text").pipe(
@ -15,14 +15,13 @@ export const embeddingsCommand = Command.make("embeddings", {
Argument.variadic({ min: 1 }), Argument.variadic({ min: 1 }),
), ),
}, ({ texts }) => }, ({ texts }) =>
withSocket((socket, opts) => withGatewayClient((client, opts) =>
Effect.gen(function* () { Effect.gen(function* () {
const flow = socket.flow(opts.flow); const response = yield* gatewayDispatch(client, "embeddings", "embeddings", {
const vectors = yield* Effect.tryPromise({ texts: Array.from(texts),
try: () => flow.embeddings(Array.from(texts)), }, { flow: opts.flow, timeoutMs: 30000 });
catch: (error) => cliCommandError("embeddings", error), const record = response as Record<string, unknown>;
}); yield* writeJson(record.vectors ?? []);
yield* writeJson(vectors);
}), }),
), ),
).pipe(Command.withDescription("Generate text embeddings")); ).pipe(Command.withDescription("Generate text embeddings"));

View file

@ -9,16 +9,14 @@ import * as S from "effect/Schema";
import * as Argument from "effect/unstable/cli/Argument"; import * as Argument from "effect/unstable/cli/Argument";
import * as Command from "effect/unstable/cli/Command"; import * as Command from "effect/unstable/cli/Command";
import * as Flag from "effect/unstable/cli/Flag"; import * as Flag from "effect/unstable/cli/Flag";
import { cliCommandError, withSocket, writeJson } from "./util.js"; import { cliCommandError, gatewayDispatch, withGatewayClient, writeJson } from "./util.js";
const list = Command.make("list", {}, () => const list = Command.make("list", {}, () =>
withSocket((socket) => withGatewayClient((client) =>
Effect.gen(function* () { Effect.gen(function* () {
const flows = socket.flows(); const response = yield* gatewayDispatch(client, "flow.list", "flow", { operation: "list-flows" }, { timeoutMs: 60000 });
const ids = yield* Effect.tryPromise({ const record = response as Record<string, unknown>;
try: () => flows.getFlows(), const ids = Array.isArray(record["flow-ids"]) ? record["flow-ids"] : [];
catch: (error) => cliCommandError("flow.list", error),
});
yield* writeJson(ids); yield* writeJson(ids);
}), }),
), ),
@ -27,13 +25,16 @@ const list = Command.make("list", {}, () =>
const get = Command.make("get", { const get = Command.make("get", {
id: Argument.string("id").pipe(Argument.withDescription("Flow ID")), id: Argument.string("id").pipe(Argument.withDescription("Flow ID")),
}, ({ id }) => }, ({ id }) =>
withSocket((socket) => withGatewayClient((client) =>
Effect.gen(function* () { Effect.gen(function* () {
const flows = socket.flows(); const response = yield* gatewayDispatch(client, "flow.get", "flow", {
const def = yield* Effect.tryPromise({ operation: "get-flow",
try: () => flows.getFlow(id), "flow-id": id,
catch: (error) => cliCommandError("flow.get", error), }, { timeoutMs: 60000 });
}); const record = response as Record<string, unknown>;
const def = typeof record.flow === "string"
? yield* S.decodeUnknownEffect(S.UnknownFromJsonString)(record.flow)
: record.flow;
yield* writeJson(def); yield* writeJson(def);
}), }),
), ),
@ -56,26 +57,23 @@ const start = Command.make("start", {
Flag.optional, Flag.optional,
), ),
}, ({ id, blueprint, description, parameters }) => }, ({ id, blueprint, description, parameters }) =>
withSocket((socket) => withGatewayClient((client) =>
Effect.gen(function* () { Effect.gen(function* () {
const flows = socket.flows();
const rawParameters = parameters._tag === "Some" ? parameters.value : undefined; const rawParameters = parameters._tag === "Some" ? parameters.value : undefined;
const params = rawParameters !== undefined && rawParameters.length > 0 const params = rawParameters !== undefined && rawParameters.length > 0
? yield* S.decodeUnknownEffect(S.UnknownFromJsonString)(rawParameters).pipe( ? yield* S.decodeUnknownEffect(S.UnknownFromJsonString)(rawParameters).pipe(
Effect.flatMap(S.decodeUnknownEffect(S.Record(S.String, S.Unknown))), Effect.flatMap(S.decodeUnknownEffect(S.Record(S.String, S.Unknown))),
Effect.mapError((error) => cliCommandError("flow.start.parameters", error)), Effect.mapError((error) => cliCommandError("flow.start.parameters", error)),
) )
: undefined; : undefined;
const resp = yield* Effect.tryPromise({ const request = {
try: () => operation: "start-flow",
flows.startFlow( "flow-id": id,
id, "blueprint-name": blueprint,
blueprint, description,
description, ...(params !== undefined && Object.keys(params).length > 0 ? { parameters: params } : {}),
params, };
), const resp = yield* gatewayDispatch(client, "flow.start", "flow", request, { timeoutMs: 30000 });
catch: (error) => cliCommandError("flow.start", error),
});
yield* writeJson(resp); yield* writeJson(resp);
}), }),
), ),
@ -84,13 +82,12 @@ const start = Command.make("start", {
const stop = Command.make("stop", { const stop = Command.make("stop", {
id: Argument.string("id").pipe(Argument.withDescription("Flow ID")), id: Argument.string("id").pipe(Argument.withDescription("Flow ID")),
}, ({ id }) => }, ({ id }) =>
withSocket((socket) => withGatewayClient((client) =>
Effect.gen(function* () { Effect.gen(function* () {
const flows = socket.flows(); const resp = yield* gatewayDispatch(client, "flow.stop", "flow", {
const resp = yield* Effect.tryPromise({ operation: "stop-flow",
try: () => flows.stopFlow(id), "flow-id": id,
catch: (error) => cliCommandError("flow.stop", error), }, { timeoutMs: 30000 });
});
yield* writeJson(resp); yield* writeJson(resp);
}), }),
), ),

View file

@ -9,7 +9,7 @@ import * as O from "effect/Option";
import * as Argument from "effect/unstable/cli/Argument"; import * as Argument from "effect/unstable/cli/Argument";
import * as Command from "effect/unstable/cli/Command"; import * as Command from "effect/unstable/cli/Command";
import * as Flag from "effect/unstable/cli/Flag"; import * as Flag from "effect/unstable/cli/Flag";
import { cliCommandError, withSocket, writeLine } from "./util.js"; import { gatewayDispatch, withGatewayClient, writeLine } from "./util.js";
export const graphRagCommand = Command.make("graph-rag", { export const graphRagCommand = Command.make("graph-rag", {
query: Argument.string("query").pipe(Argument.withDescription("Natural language query")), query: Argument.string("query").pipe(Argument.withDescription("Natural language query")),
@ -26,22 +26,17 @@ export const graphRagCommand = Command.make("graph-rag", {
Flag.optional, Flag.optional,
), ),
}, ({ query, entityLimit, tripleLimit, collection }) => }, ({ query, entityLimit, tripleLimit, collection }) =>
withSocket((socket, opts) => withGatewayClient((client, opts) =>
Effect.gen(function* () { Effect.gen(function* () {
const flow = socket.flow(opts.flow); const response = yield* gatewayDispatch(client, "graph-rag", "graph-rag", {
const response = yield* Effect.tryPromise({ query,
try: () => user: opts.user,
flow.graphRag( collection: O.getOrUndefined(collection) ?? "default",
query, "entity-limit": entityLimit,
{ "triple-limit": tripleLimit,
entityLimit, }, { flow: opts.flow, timeoutMs: 60000 });
tripleLimit, const record = response as Record<string, unknown>;
}, yield* writeLine(typeof record.response === "string" ? record.response : "");
O.getOrUndefined(collection),
),
catch: (error) => cliCommandError("graph-rag", error),
});
yield* writeLine(response);
}), }),
), ),
).pipe(Command.withDescription("Query the knowledge graph using RAG")); ).pipe(Command.withDescription("Query the knowledge graph using RAG"));
@ -57,19 +52,16 @@ export const documentRagCommand = Command.make("document-rag", {
Flag.optional, Flag.optional,
), ),
}, ({ query, docLimit, collection }) => }, ({ query, docLimit, collection }) =>
withSocket((socket, opts) => withGatewayClient((client, opts) =>
Effect.gen(function* () { Effect.gen(function* () {
const flow = socket.flow(opts.flow); const response = yield* gatewayDispatch(client, "document-rag", "document-rag", {
const response = yield* Effect.tryPromise({ query,
try: () => user: opts.user,
flow.documentRag( collection: O.getOrUndefined(collection) ?? "default",
query, "doc-limit": docLimit,
docLimit, }, { flow: opts.flow, timeoutMs: 60000 });
O.getOrUndefined(collection), const record = response as Record<string, unknown>;
), yield* writeLine(typeof record.response === "string" ? record.response : "");
catch: (error) => cliCommandError("document-rag", error),
});
yield* writeLine(response);
}), }),
), ),
).pipe(Command.withDescription("Query documents using RAG")); ).pipe(Command.withDescription("Query documents using RAG"));

View file

@ -4,12 +4,12 @@
* Manages documents stored in the TrustGraph library. * Manages documents stored in the TrustGraph library.
*/ */
import { Effect, Match } from "effect"; import { Clock, Effect, Match } from "effect";
import * as O from "effect/Option"; import * as O from "effect/Option";
import * as Argument from "effect/unstable/cli/Argument"; import * as Argument from "effect/unstable/cli/Argument";
import * as Command from "effect/unstable/cli/Command"; import * as Command from "effect/unstable/cli/Command";
import * as Flag from "effect/unstable/cli/Flag"; import * as Flag from "effect/unstable/cli/Flag";
import { cliCommandError, withSocket, writeJson } from "./util.js"; import { cliCommandError, gatewayDispatch, withGatewayClient, writeJson } from "./util.js";
function basenamePath(filepath: string): string { function basenamePath(filepath: string): string {
const normalized = filepath.replace(/\/+$/, ""); const normalized = filepath.replace(/\/+$/, "");
@ -34,14 +34,14 @@ export function guessMimeType(filepath: string): string {
} }
const list = Command.make("list", {}, () => const list = Command.make("list", {}, () =>
withSocket((socket) => withGatewayClient((client, opts) =>
Effect.gen(function* () { Effect.gen(function* () {
const lib = socket.librarian(); const response = yield* gatewayDispatch(client, "library.list", "librarian", {
const docs = yield* Effect.tryPromise({ operation: "list-documents",
try: () => lib.getDocuments(), user: opts.user,
catch: (error) => cliCommandError("library.list", error), }, { timeoutMs: 60000 });
}); const record = response as Record<string, unknown>;
yield* writeJson(docs); yield* writeJson(record["document-metadatas"] ?? record.documents ?? []);
}), }),
), ),
).pipe(Command.withDescription("List documents in the library")); ).pipe(Command.withDescription("List documents in the library"));
@ -72,9 +72,8 @@ const load = Command.make("load", {
Flag.optional, Flag.optional,
), ),
}, ({ file, title, mimeType, comments, tags, id }) => }, ({ file, title, mimeType, comments, tags, id }) =>
withSocket((socket) => withGatewayClient((client, opts) =>
Effect.gen(function* () { Effect.gen(function* () {
const lib = socket.librarian();
const data = new Uint8Array(yield* Effect.tryPromise({ const data = new Uint8Array(yield* Effect.tryPromise({
try: () => Bun.file(file).arrayBuffer(), try: () => Bun.file(file).arrayBuffer(),
catch: (error) => cliCommandError("library.load.read-file", error), catch: (error) => cliCommandError("library.load.read-file", error),
@ -82,19 +81,25 @@ const load = Command.make("load", {
const b64 = Buffer.from(data).toString("base64"); const b64 = Buffer.from(data).toString("base64");
const resolvedMimeType = O.getOrUndefined(mimeType) ?? guessMimeType(file); const resolvedMimeType = O.getOrUndefined(mimeType) ?? guessMimeType(file);
const resolvedTitle = O.getOrUndefined(title) ?? basenamePath(file); const resolvedTitle = O.getOrUndefined(title) ?? basenamePath(file);
const timestamp = yield* Clock.currentTimeMillis;
const resp = yield* Effect.tryPromise({ const documentId = O.getOrUndefined(id);
try: () => const documentMetadata = {
lib.loadDocument( time: Math.floor(timestamp / 1000),
b64, kind: resolvedMimeType,
resolvedMimeType, title: resolvedTitle,
resolvedTitle, comments,
comments, user: opts.user,
Array.from(tags), tags: Array.from(tags),
O.getOrUndefined(id), "document-type": "source",
), documentType: "source",
catch: (error) => cliCommandError("library.load", error), ...(documentId !== undefined ? { id: documentId } : {}),
}); };
const resp = yield* gatewayDispatch(client, "library.load", "librarian", {
operation: "add-document",
"document-metadata": documentMetadata,
documentMetadata,
content: b64,
}, { timeoutMs: 30000 });
yield* writeJson(resp); yield* writeJson(resp);
}), }),
), ),
@ -107,27 +112,29 @@ const remove = Command.make("remove", {
Flag.optional, Flag.optional,
), ),
}, ({ id, collection }) => }, ({ id, collection }) =>
withSocket((socket) => withGatewayClient((client, opts) =>
Effect.gen(function* () { Effect.gen(function* () {
const lib = socket.librarian(); const resp = yield* gatewayDispatch(client, "library.remove", "librarian", {
const resp = yield* Effect.tryPromise({ operation: "remove-document",
try: () => lib.removeDocument(id, O.getOrUndefined(collection)), "document-id": id,
catch: (error) => cliCommandError("library.remove", error), documentId: id,
}); user: opts.user,
collection: O.getOrUndefined(collection) ?? "default",
}, { timeoutMs: 30000 });
yield* writeJson(resp); yield* writeJson(resp);
}), }),
), ),
).pipe(Command.withDescription("Remove a document from the library")); ).pipe(Command.withDescription("Remove a document from the library"));
const processing = Command.make("processing", {}, () => const processing = Command.make("processing", {}, () =>
withSocket((socket) => withGatewayClient((client, opts) =>
Effect.gen(function* () { Effect.gen(function* () {
const lib = socket.librarian(); const response = yield* gatewayDispatch(client, "library.processing", "librarian", {
const items = yield* Effect.tryPromise({ operation: "list-processing",
try: () => lib.getProcessing(), user: opts.user,
catch: (error) => cliCommandError("library.processing", error), }, { timeoutMs: 60000 });
}); const record = response as Record<string, unknown>;
yield* writeJson(items); yield* writeJson(record["processing-metadatas"] ?? record.processing ?? record["processing-metadata"] ?? []);
}), }),
), ),
).pipe(Command.withDescription("List documents currently being processed")); ).pipe(Command.withDescription("List documents currently being processed"));

View file

@ -9,7 +9,7 @@ import { Effect } from "effect";
import * as O from "effect/Option"; import * as O from "effect/Option";
import * as Command from "effect/unstable/cli/Command"; import * as Command from "effect/unstable/cli/Command";
import * as Flag from "effect/unstable/cli/Flag"; import * as Flag from "effect/unstable/cli/Flag";
import { cliCommandError, withSocket, writeJson } from "./util.js"; import { gatewayDispatch, withGatewayClient, writeJson } from "./util.js";
export const triplesCommand = Command.make("triples", { export const triplesCommand = Command.make("triples", {
subject: Flag.string("subject").pipe( subject: Flag.string("subject").pipe(
@ -37,9 +37,8 @@ export const triplesCommand = Command.make("triples", {
Flag.optional, Flag.optional,
), ),
}, ({ subject, predicate, object, limit, collection }) => }, ({ subject, predicate, object, limit, collection }) =>
withSocket((socket, opts) => withGatewayClient((client, opts) =>
Effect.gen(function* () { Effect.gen(function* () {
const flow = socket.flow(opts.flow);
const subjectValue = O.getOrUndefined(subject); const subjectValue = O.getOrUndefined(subject);
const predicateValue = O.getOrUndefined(predicate); const predicateValue = O.getOrUndefined(predicate);
const objectValue = O.getOrUndefined(object); const objectValue = O.getOrUndefined(object);
@ -53,18 +52,16 @@ export const triplesCommand = Command.make("triples", {
? { t: "i", i: objectValue } ? { t: "i", i: objectValue }
: undefined; : undefined;
const triples = yield* Effect.tryPromise({ const response = yield* gatewayDispatch(client, "triples", "triples", {
try: () => limit,
flow.triplesQuery( user: opts.user,
s, collection: O.getOrUndefined(collection) ?? "default",
p, ...(s !== undefined ? { s } : {}),
o, ...(p !== undefined ? { p } : {}),
limit, ...(o !== undefined ? { o } : {}),
O.getOrUndefined(collection), }, { flow: opts.flow, timeoutMs: 30000 });
), const record = response as Record<string, unknown>;
catch: (error) => cliCommandError("triples", error), yield* writeJson(record.triples ?? record.response ?? []);
});
yield* writeJson(triples);
}), }),
), ),
).pipe(Command.withDescription("Query knowledge graph triples")); ).pipe(Command.withDescription("Query knowledge graph triples"));

View file

@ -3,14 +3,13 @@
*/ */
import type { import type {
BaseApi, DispatchOptions,
TrustGraphGatewayClient, TrustGraphGatewayClient,
} from "@trustgraph/client"; } from "@trustgraph/client";
import { import {
createTrustGraphSocket,
makeTrustGraphGatewayClientScoped, makeTrustGraphGatewayClientScoped,
} from "@trustgraph/client"; } from "@trustgraph/client";
import { Duration, Effect } from "effect"; import { Effect } from "effect";
import * as O from "effect/Option"; import * as O from "effect/Option";
import * as S from "effect/Schema"; import * as S from "effect/Schema";
import * as Command from "effect/unstable/cli/Command"; import * as Command from "effect/unstable/cli/Command";
@ -85,37 +84,6 @@ export const writeJson = (value: unknown) =>
Effect.flatMap(writeLine), Effect.flatMap(writeLine),
); );
/**
* Create a BaseApi socket client and wait for the connection to be established.
* The client auto-connects; we listen for the first "connected/authenticated"
* state before handing it back to the caller.
*/
export function createSocketEffect(opts: CliOpts): Effect.Effect<BaseApi, CliCommandError> {
const socket = createTrustGraphSocket(opts.user, opts.token, opts.gateway);
return Effect.callback<void, CliCommandError>((resume) => {
const unsub = socket.onConnectionStateChange((state) => {
if (state.status === "authenticated" || state.status === "unauthenticated") {
unsub();
resume(Effect.void);
} else if (state.status === "failed") {
unsub();
resume(Effect.fail(cliCommandError("connect", state.lastError ?? "WebSocket connection failed")));
}
});
return Effect.sync(() => {
unsub();
});
}).pipe(
Effect.timeout(Duration.seconds(15)),
Effect.catchTag("TimeoutError", () =>
Effect.fail(cliCommandError("connect", "Timed out waiting for WebSocket connection")),
),
Effect.as(socket),
);
}
function gatewayUrlWithToken(opts: CliOpts): string { function gatewayUrlWithToken(opts: CliOpts): string {
if (opts.token === undefined || opts.token.length === 0) return opts.gateway; if (opts.token === undefined || opts.token.length === 0) return opts.gateway;
const separator = opts.gateway.includes("?") ? "&" : "?"; const separator = opts.gateway.includes("?") ? "&" : "?";
@ -133,16 +101,37 @@ export const withGatewayClient = Effect.fn("withGatewayClient")(function* <A, E,
); );
}); });
export const withSocket = Effect.fn("withSocket")(function* <A, E, R>( export interface GatewayDispatchOptions {
use: (socket: BaseApi, opts: CliOpts) => Effect.Effect<A, E, R>, readonly flow?: string;
readonly timeoutMs?: number;
readonly retries?: number;
}
export const gatewayDispatch = Effect.fn("gatewayDispatch")(function*(
client: TrustGraphGatewayClient,
operation: string,
service: string,
request: Record<string, unknown>,
options: GatewayDispatchOptions = {},
) { ) {
const opts = yield* getOpts; const input = options.flow === undefined
return yield* Effect.acquireUseRelease( ? {
createSocketEffect(opts), scope: "global" as const,
(socket) => use(socket, opts), service,
(socket) => request,
Effect.sync(() => { }
socket.close(); : {
}), scope: "flow" as const,
service,
flow: options.flow,
request,
};
const dispatchOptions: DispatchOptions = {
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
...(options.retries !== undefined ? { retries: options.retries } : {}),
};
return yield* client.dispatch(input, dispatchOptions).pipe(
Effect.mapError((error) => cliCommandError(operation, error)),
); );
}); });

View file

@ -1,42 +1,43 @@
import { Schema as S } from "effect";
// Term type discriminators matching the wire format // Term type discriminators matching the wire format
// i = IRI, b = BLANK node, l = LITERAL, t = TRIPLE (reified) // i = IRI, b = BLANK node, l = LITERAL, t = TRIPLE (reified)
export type TermType = "i" | "b" | "l" | "t"; export type TermType = "i" | "b" | "l" | "t";
export interface IriTerm { export class IriTerm extends S.Class<IriTerm>("IriTerm")({
t: "i"; t: S.Literal("i"),
i: string; i: S.String,
} }, { description: "IRI term in TrustGraph wire triples." }) {}
export interface BlankTerm { export class BlankTerm extends S.Class<BlankTerm>("BlankTerm")({
t: "b"; t: S.Literal("b"),
d: string; d: S.String,
} }, { description: "Blank-node term in TrustGraph wire triples." }) {}
export interface LiteralTerm { export class LiteralTerm extends S.Class<LiteralTerm>("LiteralTerm")({
t: "l"; t: S.Literal("l"),
v: string; v: S.String,
dt?: string; // datatype dt: S.optionalKey(S.String),
ln?: string; // language ln: S.optionalKey(S.String),
} }, { description: "Literal term in TrustGraph wire triples." }) {}
export interface TripleTerm { export class TripleTerm extends S.Class<TripleTerm>("TripleTerm")({
t: "t"; t: S.Literal("t"),
tr?: Triple; tr: S.optionalKey(S.suspend((): S.Codec<Triple, Triple> => Triple)),
} }, { description: "Reified triple term in TrustGraph wire triples." }) {}
export type Term = IriTerm | BlankTerm | LiteralTerm | TripleTerm; export const Term = S.Union([IriTerm, BlankTerm, LiteralTerm, TripleTerm]);
export type Term = typeof Term.Type;
export interface PartialTriple { export class PartialTriple extends S.Class<PartialTriple>("PartialTriple")({
s?: Term; s: S.optionalKey(Term),
p?: Term; p: S.optionalKey(Term),
o?: Term; o: S.optionalKey(Term),
} }, { description: "Partial triple pattern for query wildcards." }) {}
export interface Triple { export class Triple extends S.Class<Triple>("Triple")({
s: Term; s: Term,
p: Term; p: Term,
o: Term; o: Term,
g?: string; // graph (renamed from direc to match backend) g: S.optionalKey(S.String),
} }, { description: "TrustGraph wire triple, optionally scoped to a named graph." }) {}

View file

@ -1,521 +1,505 @@
import type { Term, Triple } from "./Triple.js"; import { Schema as S } from "effect";
import { Term, Triple } from "./Triple.js";
export type Request = object; export type Request = object;
export type Response = object; export type Response = object;
export interface ResponseError {
type?: string;
message: string;
}
export type WireError = object | string; export type WireError = object | string;
export interface RequestMessage { const UnknownRecord = S.Record(S.String, S.Unknown);
id: string; const WireErrorValue = S.Union([S.String, UnknownRecord]);
service: string; const TypedMessageError = S.Struct({
request: Request; message: S.String,
flow?: string; type: S.optionalKey(S.String),
} });
const OptionalMessageError = S.Struct({
message: S.optionalKey(S.String),
});
export interface ApiResponse { const NumberArray = S.Array(S.Finite).pipe(S.mutable);
id: string; const NumberMatrix = S.Array(NumberArray).pipe(S.mutable);
response: Response; const TripleArray = S.Array(Triple).pipe(S.mutable);
} const StringArray = S.Array(S.String).pipe(S.mutable);
export interface Metadata { export class ResponseError extends S.Class<ResponseError>("ResponseError")({
id?: string; type: S.optionalKey(S.String),
metadata?: Triple[]; message: S.String,
user?: string; }, { description: "TrustGraph response error payload." }) {}
collection?: string;
}
export interface EntityEmbeddings { export class RequestMessage extends S.Class<RequestMessage>("RequestMessage")({
entity?: Term; id: S.String,
vectors?: number[][]; service: S.String,
} request: UnknownRecord,
flow: S.optionalKey(S.String),
}, { description: "Envelope sent to a TrustGraph service." }) {}
export interface GraphEmbeddings { export class ApiResponse extends S.Class<ApiResponse>("ApiResponse")({
metadata?: Metadata; id: S.String,
entities?: EntityEmbeddings[]; response: UnknownRecord,
} }, { description: "Envelope returned from a TrustGraph service." }) {}
export interface TextCompletionRequest { export class Metadata extends S.Class<Metadata>("Metadata")({
system: string; id: S.optionalKey(S.String),
prompt: string; metadata: S.optionalKey(TripleArray),
streaming?: boolean; user: S.optionalKey(S.String),
} collection: S.optionalKey(S.String),
}, { description: "Shared request metadata for TrustGraph wire messages." }) {}
export interface TextCompletionResponse { export class EntityEmbeddings extends S.Class<EntityEmbeddings>("EntityEmbeddings")({
response: string; entity: S.optionalKey(Term),
// Streaming fields vectors: S.optionalKey(NumberMatrix),
end_of_stream?: boolean; }, { description: "Embedding vectors associated with a graph entity." }) {}
error?: {
message: string;
type?: string;
};
// Token usage (appears in final message)
in_token?: number;
out_token?: number;
model?: string;
}
export interface GraphRagRequest { export class GraphEmbeddings extends S.Class<GraphEmbeddings>("GraphEmbeddings")({
query: string; metadata: S.optionalKey(Metadata),
user?: string; entities: S.optionalKey(S.Array(EntityEmbeddings).pipe(S.mutable)),
collection?: string; }, { description: "Graph embedding payload grouped by entity." }) {}
"entity-limit"?: number; // Default: 50
"triple-limit"?: number; // Default: 30
"max-subgraph-size"?: number; // Default: 1000
"max-path-length"?: number; // Default: 2
streaming?: boolean;
}
export interface GraphRagResponse { export class TextCompletionRequest extends S.Class<TextCompletionRequest>("TextCompletionRequest")({
response: string; system: S.String,
// Streaming fields prompt: S.String,
chunk?: string; streaming: S.optionalKey(S.Boolean),
end_of_stream?: boolean; }, { description: "Text-completion request payload." }) {}
endOfStream?: boolean;
error?: {
message: string;
type?: string;
};
// Token usage (appears in final message)
in_token?: number;
out_token?: number;
model?: string;
// Explainability fields
message_type?: "chunk" | "explain";
explain_id?: string;
explain_graph?: string;
explain_triples?: unknown[];
end_of_session?: boolean;
}
export interface DocumentRagRequest { export class TextCompletionResponse extends S.Class<TextCompletionResponse>("TextCompletionResponse")({
query: string; response: S.String,
user?: string; end_of_stream: S.optionalKey(S.Boolean),
collection?: string; error: S.optionalKey(TypedMessageError),
"doc-limit"?: number; // Default: 20 in_token: S.optionalKey(S.Finite),
streaming?: boolean; out_token: S.optionalKey(S.Finite),
} model: S.optionalKey(S.String),
}, { description: "Text-completion response payload." }) {}
export interface DocumentRagResponse { export class GraphRagRequest extends S.Class<GraphRagRequest>("GraphRagRequest")({
response: string; query: S.String,
// Streaming fields user: S.optionalKey(S.String),
chunk?: string; collection: S.optionalKey(S.String),
end_of_stream?: boolean; "entity-limit": S.optionalKey(S.Finite),
endOfStream?: boolean; "triple-limit": S.optionalKey(S.Finite),
error?: { "max-subgraph-size": S.optionalKey(S.Finite),
message: string; "max-path-length": S.optionalKey(S.Finite),
type?: string; streaming: S.optionalKey(S.Boolean),
}; }, { description: "Graph RAG request payload." }) {}
// Token usage (appears in final message)
in_token?: number;
out_token?: number;
model?: string;
// Explainability fields
message_type?: "chunk" | "explain";
explain_id?: string;
explain_graph?: string;
end_of_session?: boolean;
}
export interface AgentRequest { export class GraphRagResponse extends S.Class<GraphRagResponse>("GraphRagResponse")({
question: string; response: S.String,
user?: string; chunk: S.optionalKey(S.String),
collection?: string; end_of_stream: S.optionalKey(S.Boolean),
streaming?: boolean; endOfStream: S.optionalKey(S.Boolean),
} error: S.optionalKey(TypedMessageError),
in_token: S.optionalKey(S.Finite),
out_token: S.optionalKey(S.Finite),
model: S.optionalKey(S.String),
message_type: S.optionalKey(S.Literals(["chunk", "explain"])),
explain_id: S.optionalKey(S.String),
explain_graph: S.optionalKey(S.String),
explain_triples: S.optionalKey(S.Array(S.Unknown).pipe(S.mutable)),
end_of_session: S.optionalKey(S.Boolean),
}, { description: "Graph RAG response payload." }) {}
export interface AgentResponse { export class DocumentRagRequest extends S.Class<DocumentRagRequest>("DocumentRagRequest")({
// Streaming response format (new protocol) query: S.String,
chunk_type?: "thought" | "action" | "observation" | "answer" | "final-answer" | "explain" | "error"; user: S.optionalKey(S.String),
content?: string; collection: S.optionalKey(S.String),
end_of_message?: boolean; "doc-limit": S.optionalKey(S.Finite),
end_of_dialog?: boolean; streaming: S.optionalKey(S.Boolean),
}, { description: "Document RAG request payload." }) {}
// Legacy fields for backward compatibility with non-streaming export class DocumentRagResponse extends S.Class<DocumentRagResponse>("DocumentRagResponse")({
thought?: string; response: S.String,
observation?: string; chunk: S.optionalKey(S.String),
answer?: string; end_of_stream: S.optionalKey(S.Boolean),
error?: ResponseError; endOfStream: S.optionalKey(S.Boolean),
error: S.optionalKey(TypedMessageError),
in_token: S.optionalKey(S.Finite),
out_token: S.optionalKey(S.Finite),
model: S.optionalKey(S.String),
message_type: S.optionalKey(S.Literals(["chunk", "explain"])),
explain_id: S.optionalKey(S.String),
explain_graph: S.optionalKey(S.String),
end_of_session: S.optionalKey(S.Boolean),
}, { description: "Document RAG response payload." }) {}
// Token usage (appears in final message) export class AgentRequest extends S.Class<AgentRequest>("AgentRequest")({
in_token?: number; question: S.String,
out_token?: number; user: S.optionalKey(S.String),
model?: string; collection: S.optionalKey(S.String),
streaming: S.optionalKey(S.Boolean),
}, { description: "Agent request payload." }) {}
// Explainability fields export class AgentResponse extends S.Class<AgentResponse>("AgentResponse")({
message_type?: "chunk" | "explain"; chunk_type: S.optionalKey(S.Literals([
explain_id?: string; "thought",
explain_graph?: string; "action",
explain_triples?: unknown[]; "observation",
} "answer",
"final-answer",
"explain",
"error",
])),
content: S.optionalKey(S.String),
end_of_message: S.optionalKey(S.Boolean),
end_of_dialog: S.optionalKey(S.Boolean),
thought: S.optionalKey(S.String),
observation: S.optionalKey(S.String),
answer: S.optionalKey(S.String),
error: S.optionalKey(ResponseError),
in_token: S.optionalKey(S.Finite),
out_token: S.optionalKey(S.Finite),
model: S.optionalKey(S.String),
message_type: S.optionalKey(S.Literals(["chunk", "explain"])),
explain_id: S.optionalKey(S.String),
explain_graph: S.optionalKey(S.String),
explain_triples: S.optionalKey(S.Array(S.Unknown).pipe(S.mutable)),
}, { description: "Agent response payload." }) {}
export interface EmbeddingsRequest { export class EmbeddingsRequest extends S.Class<EmbeddingsRequest>("EmbeddingsRequest")({
texts: string[]; texts: StringArray,
} }, { description: "Batch embeddings request payload." }) {}
export interface EmbeddingsResponse { export class EmbeddingsResponse extends S.Class<EmbeddingsResponse>("EmbeddingsResponse")({
vectors: number[][]; // One vector per input text vectors: NumberMatrix,
} }, { description: "Batch embeddings response payload." }) {}
export interface GraphEmbeddingsQueryRequest { export class GraphEmbeddingsQueryRequest extends S.Class<GraphEmbeddingsQueryRequest>("GraphEmbeddingsQueryRequest")({
vector: number[]; // Single query vector vector: NumberArray,
limit: number; limit: S.Finite,
user?: string; user: S.optionalKey(S.String),
collection?: string; collection: S.optionalKey(S.String),
} }, { description: "Graph embeddings query request payload." }) {}
export interface EntityMatch { export class EntityMatch extends S.Class<EntityMatch>("EntityMatch")({
entity: Term | null; entity: S.NullOr(Term),
score: number; score: S.Finite,
} }, { description: "Scored graph-entity match." }) {}
export interface GraphEmbeddingsQueryResponse { export class GraphEmbeddingsQueryResponse extends S.Class<GraphEmbeddingsQueryResponse>("GraphEmbeddingsQueryResponse")({
entities: EntityMatch[]; entities: S.Array(EntityMatch).pipe(S.mutable),
} }, { description: "Graph embeddings query response payload." }) {}
export interface TriplesQueryRequest { export class TriplesQueryRequest extends S.Class<TriplesQueryRequest>("TriplesQueryRequest")({
s?: Term; s: S.optionalKey(Term),
p?: Term; p: S.optionalKey(Term),
o?: Term; o: S.optionalKey(Term),
g?: string; // Named graph URI filter (plain string, not Term) g: S.optionalKey(S.String),
limit: number; limit: S.Finite,
user?: string; user: S.optionalKey(S.String),
collection?: string; collection: S.optionalKey(S.String),
} }, { description: "Triple pattern query request payload." }) {}
export interface TriplesQueryResponse { export class TriplesQueryResponse extends S.Class<TriplesQueryResponse>("TriplesQueryResponse")({
triples: Triple[]; triples: TripleArray,
/** @deprecated Use `triples` — kept for backward compatibility */ response: S.optionalKey(TripleArray),
response?: Triple[]; }, { description: "Triple pattern query response payload." }) {}
}
export interface RowsQueryRequest { export class RowsQueryRequest extends S.Class<RowsQueryRequest>("RowsQueryRequest")({
query: string; query: S.String,
user?: string; user: S.optionalKey(S.String),
collection?: string; collection: S.optionalKey(S.String),
variables?: Record<string, unknown>; variables: S.optionalKey(UnknownRecord),
operation_name?: string; operation_name: S.optionalKey(S.String),
} }, { description: "Structured rows GraphQL request payload." }) {}
export interface RowsQueryResponse { export class RowsQueryResponse extends S.Class<RowsQueryResponse>("RowsQueryResponse")({
data?: Record<string, unknown>; data: S.optionalKey(UnknownRecord),
errors?: Record<string, unknown>[]; errors: S.optionalKey(S.Array(UnknownRecord).pipe(S.mutable)),
extensions?: Record<string, unknown>; extensions: S.optionalKey(UnknownRecord),
values?: unknown[]; values: S.optionalKey(S.Array(S.Unknown).pipe(S.mutable)),
} }, { description: "Structured rows GraphQL response payload." }) {}
export interface NlpQueryRequest { export class NlpQueryRequest extends S.Class<NlpQueryRequest>("NlpQueryRequest")({
question: string; question: S.String,
max_results?: number; max_results: S.optionalKey(S.Finite),
} }, { description: "Natural-language-to-GraphQL request payload." }) {}
export interface NlpQueryResponse { export class NlpQueryResponse extends S.Class<NlpQueryResponse>("NlpQueryResponse")({
graphql_query?: string; graphql_query: S.optionalKey(S.String),
variables?: Record<string, unknown>; variables: S.optionalKey(UnknownRecord),
detected_schemas?: Record<string, unknown>[]; detected_schemas: S.optionalKey(S.Array(UnknownRecord).pipe(S.mutable)),
confidence?: number; confidence: S.optionalKey(S.Finite),
} }, { description: "Natural-language-to-GraphQL response payload." }) {}
export interface StructuredQueryRequest { export class StructuredQueryRequest extends S.Class<StructuredQueryRequest>("StructuredQueryRequest")({
question: string; question: S.String,
user?: string; user: S.optionalKey(S.String),
collection?: string; collection: S.optionalKey(S.String),
} }, { description: "Structured query request payload." }) {}
export interface StructuredQueryResponse { export class StructuredQueryResponse extends S.Class<StructuredQueryResponse>("StructuredQueryResponse")({
data?: Record<string, unknown>; data: S.optionalKey(UnknownRecord),
errors?: Record<string, unknown>[]; errors: S.optionalKey(S.Array(UnknownRecord).pipe(S.mutable)),
} }, { description: "Structured query response payload." }) {}
export interface RowEmbeddingsQueryRequest { export class RowEmbeddingsQueryRequest extends S.Class<RowEmbeddingsQueryRequest>("RowEmbeddingsQueryRequest")({
vector: number[]; // Single query vector vector: NumberArray,
schema_name: string; schema_name: S.String,
user?: string; user: S.optionalKey(S.String),
collection?: string; collection: S.optionalKey(S.String),
index_name?: string; index_name: S.optionalKey(S.String),
limit?: number; limit: S.optionalKey(S.Finite),
} }, { description: "Row embeddings query request payload." }) {}
export interface RowEmbeddingsMatch { export class RowEmbeddingsMatch extends S.Class<RowEmbeddingsMatch>("RowEmbeddingsMatch")({
index_name: string; index_name: S.String,
index_value: string[]; index_value: StringArray,
text: string; text: S.String,
score: number; score: S.Finite,
} }, { description: "Scored row embeddings match." }) {}
export interface RowEmbeddingsQueryResponse { export class RowEmbeddingsQueryResponse extends S.Class<RowEmbeddingsQueryResponse>("RowEmbeddingsQueryResponse")({
matches?: RowEmbeddingsMatch[]; matches: S.optionalKey(S.Array(RowEmbeddingsMatch).pipe(S.mutable)),
error?: { error: S.optionalKey(TypedMessageError),
message: string; }, { description: "Row embeddings query response payload." }) {}
type?: string;
};
}
export interface LoadDocumentRequest { export class LoadDocumentRequest extends S.Class<LoadDocumentRequest>("LoadDocumentRequest")({
id?: string; id: S.optionalKey(S.String),
data: string; data: S.String,
metadata?: Triple[]; metadata: S.optionalKey(TripleArray),
} }, { description: "Flow-scoped document load request payload." }) {}
export type LoadDocumentResponse = void; export type LoadDocumentResponse = void;
export interface LoadTextRequest { export class LoadTextRequest extends S.Class<LoadTextRequest>("LoadTextRequest")({
id?: string; id: S.optionalKey(S.String),
text: string; text: S.String,
charset?: string; charset: S.optionalKey(S.String),
metadata?: Triple[]; metadata: S.optionalKey(TripleArray),
} }, { description: "Flow-scoped text load request payload." }) {}
export type LoadTextResponse = void; export type LoadTextResponse = void;
export interface DocumentMetadata { export class DocumentMetadata extends S.Class<DocumentMetadata>("DocumentMetadata")({
id?: string; id: S.optionalKey(S.String),
time?: number; time: S.optionalKey(S.Finite),
kind?: string; kind: S.optionalKey(S.String),
title?: string; title: S.optionalKey(S.String),
comments?: string; comments: S.optionalKey(S.String),
metadata?: Triple[]; metadata: S.optionalKey(TripleArray),
user?: string; user: S.optionalKey(S.String),
tags?: string[]; tags: S.optionalKey(StringArray),
parentId?: string; parentId: S.optionalKey(S.String),
documentType?: string; documentType: S.optionalKey(S.String),
"parent-id"?: string; "parent-id": S.optionalKey(S.String),
"document-type"?: string; "document-type": S.optionalKey(S.String),
} }, { description: "Library document metadata payload." }) {}
export interface ProcessingMetadata { export class ProcessingMetadata extends S.Class<ProcessingMetadata>("ProcessingMetadata")({
id?: string; id: S.optionalKey(S.String),
"document-id"?: string; "document-id": S.optionalKey(S.String),
documentId?: string; documentId: S.optionalKey(S.String),
time?: number; time: S.optionalKey(S.Finite),
flow?: string; flow: S.optionalKey(S.String),
user?: string; user: S.optionalKey(S.String),
collection?: string; collection: S.optionalKey(S.String),
tags?: string[]; tags: S.optionalKey(StringArray),
} }, { description: "Library processing metadata payload." }) {}
export interface LibraryRequest { export class LibraryRequest extends S.Class<LibraryRequest>("LibraryRequest")({
operation: string; operation: S.String,
documentId?: string; documentId: S.optionalKey(S.String),
"document-id"?: string; "document-id": S.optionalKey(S.String),
processingId?: string; processingId: S.optionalKey(S.String),
"processing-id"?: string; "processing-id": S.optionalKey(S.String),
"document-metadata"?: DocumentMetadata; "document-metadata": S.optionalKey(DocumentMetadata),
documentMetadata?: DocumentMetadata; documentMetadata: S.optionalKey(DocumentMetadata),
"processing-metadata"?: ProcessingMetadata; "processing-metadata": S.optionalKey(ProcessingMetadata),
content?: string; content: S.optionalKey(S.String),
user?: string; user: S.optionalKey(S.String),
collection?: string; collection: S.optionalKey(S.String),
metadata?: Triple[]; metadata: S.optionalKey(TripleArray),
id?: string; id: S.optionalKey(S.String),
flow?: string; flow: S.optionalKey(S.String),
} }, { description: "Library service request payload." }) {}
export interface LibraryResponse { export class LibraryResponse extends S.Class<LibraryResponse>("LibraryResponse")({
error?: WireError; error: S.optionalKey(WireErrorValue),
"document-metadata"?: DocumentMetadata; "document-metadata": S.optionalKey(DocumentMetadata),
documentMetadata?: DocumentMetadata; documentMetadata: S.optionalKey(DocumentMetadata),
content?: string; content: S.optionalKey(S.String),
"document-metadatas"?: DocumentMetadata[]; "document-metadatas": S.optionalKey(S.Array(DocumentMetadata).pipe(S.mutable)),
documents?: DocumentMetadata[]; documents: S.optionalKey(S.Array(DocumentMetadata).pipe(S.mutable)),
"processing-metadata"?: ProcessingMetadata; "processing-metadata": S.optionalKey(ProcessingMetadata),
"processing-metadatas"?: ProcessingMetadata[]; "processing-metadatas": S.optionalKey(S.Array(ProcessingMetadata).pipe(S.mutable)),
processing?: ProcessingMetadata[]; processing: S.optionalKey(S.Array(ProcessingMetadata).pipe(S.mutable)),
} }, { description: "Library service response payload." }) {}
export interface KnowledgeRequest { export class KnowledgeRequest extends S.Class<KnowledgeRequest>("KnowledgeRequest")({
operation: string; operation: S.String,
user?: string; user: S.optionalKey(S.String),
id?: string; id: S.optionalKey(S.String),
flow?: string; flow: S.optionalKey(S.String),
collection?: string; collection: S.optionalKey(S.String),
triples?: Triple[]; triples: S.optionalKey(TripleArray),
"graph-embeddings"?: GraphEmbeddings; "graph-embeddings": S.optionalKey(GraphEmbeddings),
graphEmbeddings?: GraphEmbeddings; graphEmbeddings: S.optionalKey(GraphEmbeddings),
"document-embeddings"?: unknown; "document-embeddings": S.optionalKey(S.Unknown),
documentEmbeddings?: unknown; documentEmbeddings: S.optionalKey(S.Unknown),
} }, { description: "Knowledge service request payload." }) {}
export interface KnowledgeResponse { export class KnowledgeResponse extends S.Class<KnowledgeResponse>("KnowledgeResponse")({
error?: WireError; error: S.optionalKey(WireErrorValue),
ids?: string[]; ids: S.optionalKey(StringArray),
eos?: boolean; eos: S.optionalKey(S.Boolean),
triples?: Triple[]; triples: S.optionalKey(TripleArray),
"graph-embeddings"?: GraphEmbeddings; "graph-embeddings": S.optionalKey(GraphEmbeddings),
graphEmbeddings?: GraphEmbeddings; graphEmbeddings: S.optionalKey(GraphEmbeddings),
"document-embeddings"?: unknown; "document-embeddings": S.optionalKey(S.Unknown),
documentEmbeddings?: unknown; documentEmbeddings: S.optionalKey(S.Unknown),
} }, { description: "Knowledge service response payload." }) {}
export interface FlowRequest { export class FlowRequest extends S.Class<FlowRequest>("FlowRequest")({
operation: string; operation: S.String,
"blueprint-name"?: string; "blueprint-name": S.optionalKey(S.String),
"blueprint-definition"?: string; "blueprint-definition": S.optionalKey(S.String),
description?: string; description: S.optionalKey(S.String),
"flow-id"?: string; "flow-id": S.optionalKey(S.String),
parameters?: Record<string, unknown>; parameters: S.optionalKey(UnknownRecord),
user?: string; user: S.optionalKey(S.String),
} }, { description: "Flow service request payload." }) {}
export interface FlowResponse { export class FlowResponse extends S.Class<FlowResponse>("FlowResponse")({
"blueprint-names"?: string[]; "blueprint-names": S.optionalKey(StringArray),
"flow-ids"?: string[]; "flow-ids": S.optionalKey(StringArray),
ids?: string[]; ids: S.optionalKey(StringArray),
flow?: string; flow: S.optionalKey(S.String),
"blueprint-definition"?: string; "blueprint-definition": S.optionalKey(S.String),
description?: string; description: S.optionalKey(S.String),
error?: error: S.optionalKey(S.Union([OptionalMessageError, WireErrorValue])),
| { }, { description: "Flow service response payload." }) {}
message?: string;
}
| WireError;
}
export interface PromptRequest { export class PromptRequest extends S.Class<PromptRequest>("PromptRequest")({
id: string; id: S.String,
terms: Record<string, unknown>; terms: UnknownRecord,
streaming?: boolean; streaming: S.optionalKey(S.Boolean),
} }, { description: "Prompt rendering request payload." }) {}
export interface PromptResponse { export class PromptResponse extends S.Class<PromptResponse>("PromptResponse")({
text: string; text: S.String,
// Streaming fields end_of_stream: S.optionalKey(S.Boolean),
end_of_stream?: boolean; error: S.optionalKey(TypedMessageError),
error?: { in_token: S.optionalKey(S.Finite),
message: string; out_token: S.optionalKey(S.Finite),
type?: string; model: S.optionalKey(S.String),
}; }, { description: "Prompt rendering response payload." }) {}
// Token usage (appears in final message)
in_token?: number;
out_token?: number;
model?: string;
}
export type ConfigRequest = object; export type ConfigRequest = object;
export type ConfigResponse = object; export type ConfigResponse = object;
// Chunked Upload Types export class ChunkedUploadDocumentMetadata extends S.Class<ChunkedUploadDocumentMetadata>("ChunkedUploadDocumentMetadata")({
id: S.String,
time: S.Finite,
kind: S.String,
title: S.String,
comments: S.optionalKey(S.String),
metadata: S.optionalKey(TripleArray),
user: S.String,
collection: S.optionalKey(S.String),
tags: S.optionalKey(StringArray),
}, { description: "Document metadata used to begin a chunked upload." }) {}
export interface ChunkedUploadDocumentMetadata { export class BeginUploadRequest extends S.Class<BeginUploadRequest>("BeginUploadRequest")({
id: string; operation: S.Literal("begin-upload"),
time: number; "document-metadata": S.optionalKey(ChunkedUploadDocumentMetadata),
kind: string; documentMetadata: S.optionalKey(ChunkedUploadDocumentMetadata),
title: string; "total-size": S.Finite,
comments?: string; "chunk-size": S.optionalKey(S.Finite),
metadata?: Triple[]; }, { description: "Chunked upload begin request payload." }) {}
user: string;
collection?: string;
tags?: string[];
}
export interface BeginUploadRequest { export class BeginUploadResponse extends S.Class<BeginUploadResponse>("BeginUploadResponse")({
operation: "begin-upload"; "upload-id": S.String,
"document-metadata"?: ChunkedUploadDocumentMetadata; "chunk-size": S.Finite,
documentMetadata?: ChunkedUploadDocumentMetadata; "total-chunks": S.Finite,
"total-size": number; error: S.optionalKey(ResponseError),
"chunk-size"?: number; }, { description: "Chunked upload begin response payload." }) {}
}
export interface BeginUploadResponse { export class UploadChunkRequest extends S.Class<UploadChunkRequest>("UploadChunkRequest")({
"upload-id": string; operation: S.Literal("upload-chunk"),
"chunk-size": number; "upload-id": S.String,
"total-chunks": number; "chunk-index": S.Finite,
error?: ResponseError; content: S.String,
} user: S.String,
}, { description: "Chunked upload chunk request payload." }) {}
export interface UploadChunkRequest { export class UploadChunkResponse extends S.Class<UploadChunkResponse>("UploadChunkResponse")({
operation: "upload-chunk"; "upload-id": S.String,
"upload-id": string; "chunk-index": S.Finite,
"chunk-index": number; "chunks-received": S.Finite,
content: string; // base64-encoded "total-chunks": S.Finite,
user: string; "bytes-received": S.Finite,
} "total-bytes": S.Finite,
error: S.optionalKey(ResponseError),
}, { description: "Chunked upload chunk response payload." }) {}
export interface UploadChunkResponse { export class CompleteUploadRequest extends S.Class<CompleteUploadRequest>("CompleteUploadRequest")({
"upload-id": string; operation: S.Literal("complete-upload"),
"chunk-index": number; "upload-id": S.String,
"chunks-received": number; user: S.String,
"total-chunks": number; }, { description: "Chunked upload completion request payload." }) {}
"bytes-received": number;
"total-bytes": number;
error?: ResponseError;
}
export interface CompleteUploadRequest { export class CompleteUploadResponse extends S.Class<CompleteUploadResponse>("CompleteUploadResponse")({
operation: "complete-upload"; "document-id": S.String,
"upload-id": string; "object-id": S.String,
user: string; error: S.optionalKey(ResponseError),
} }, { description: "Chunked upload completion response payload." }) {}
export interface CompleteUploadResponse { export class GetUploadStatusRequest extends S.Class<GetUploadStatusRequest>("GetUploadStatusRequest")({
"document-id": string; operation: S.Literal("get-upload-status"),
"object-id": string; "upload-id": S.String,
error?: ResponseError; user: S.String,
} }, { description: "Chunked upload status request payload." }) {}
export interface GetUploadStatusRequest { export class GetUploadStatusResponse extends S.Class<GetUploadStatusResponse>("GetUploadStatusResponse")({
operation: "get-upload-status"; "upload-id": S.String,
"upload-id": string; "upload-state": S.Literals(["in-progress", "completed", "expired"]),
user: string; "chunks-received": S.Finite,
} "total-chunks": S.Finite,
"received-chunks": S.Array(S.Finite).pipe(S.mutable),
"missing-chunks": S.Array(S.Finite).pipe(S.mutable),
"bytes-received": S.Finite,
"total-bytes": S.Finite,
error: S.optionalKey(ResponseError),
}, { description: "Chunked upload status response payload." }) {}
export interface GetUploadStatusResponse { export class AbortUploadRequest extends S.Class<AbortUploadRequest>("AbortUploadRequest")({
"upload-id": string; operation: S.Literal("abort-upload"),
"upload-state": "in-progress" | "completed" | "expired"; "upload-id": S.String,
"chunks-received": number; user: S.String,
"total-chunks": number; }, { description: "Chunked upload abort request payload." }) {}
"received-chunks": number[];
"missing-chunks": number[];
"bytes-received": number;
"total-bytes": number;
error?: ResponseError;
}
export interface AbortUploadRequest { export class AbortUploadResponse extends S.Class<AbortUploadResponse>("AbortUploadResponse")({
operation: "abort-upload"; error: S.optionalKey(ResponseError),
"upload-id": string; }, { description: "Chunked upload abort response payload." }) {}
user: string;
}
export interface AbortUploadResponse { export class ListUploadsRequest extends S.Class<ListUploadsRequest>("ListUploadsRequest")({
error?: ResponseError; operation: S.Literal("list-uploads"),
} user: S.String,
}, { description: "Pending uploads list request payload." }) {}
export interface ListUploadsRequest { export class UploadSession extends S.Class<UploadSession>("UploadSession")({
operation: "list-uploads"; "upload-id": S.String,
user: string; "document-id": S.String,
} "document-metadata-json": S.String,
"total-size": S.Finite,
"chunk-size": S.Finite,
"total-chunks": S.Finite,
"chunks-received": S.Finite,
"created-at": S.String,
}, { description: "Pending upload session payload." }) {}
export interface UploadSession { export class ListUploadsResponse extends S.Class<ListUploadsResponse>("ListUploadsResponse")({
"upload-id": string; "upload-sessions": S.Array(UploadSession).pipe(S.mutable),
"document-id": string; error: S.optionalKey(ResponseError),
"document-metadata-json": string; }, { description: "Pending uploads list response payload." }) {}
"total-size": number;
"chunk-size": number;
"total-chunks": number;
"chunks-received": number;
"created-at": string;
}
export interface ListUploadsResponse { export class StreamDocumentRequest extends S.Class<StreamDocumentRequest>("StreamDocumentRequest")({
"upload-sessions": UploadSession[]; operation: S.Literal("stream-document"),
error?: ResponseError; "document-id": S.String,
} "chunk-size": S.optionalKey(S.Finite),
user: S.String,
}, { description: "Document chunk stream request payload." }) {}
export interface StreamDocumentRequest { export class StreamDocumentResponse extends S.Class<StreamDocumentResponse>("StreamDocumentResponse")({
operation: "stream-document"; content: S.String,
"document-id": string; "chunk-index": S.Finite,
"chunk-size"?: number; "total-chunks": S.Finite,
user: string; error: S.optionalKey(ResponseError),
} }, { description: "Document chunk stream response payload." }) {}
export interface StreamDocumentResponse {
content: string; // base64-encoded chunk
"chunk-index": number;
"total-chunks": number;
error?: ResponseError;
}

View file

@ -1,4 +1,4 @@
import { Cause, Context, Effect, Exit, Fiber, Layer, Ref, Scope, Stream, SubscriptionRef } from "effect"; import { Cause, Context, Effect, Fiber, Layer, Ref, Schema as S, Scope, Stream, SubscriptionRef } from "effect";
import type * as RpcGroup from "effect/unstable/rpc/RpcGroup"; import type * as RpcGroup from "effect/unstable/rpc/RpcGroup";
import * as RpcClient from "effect/unstable/rpc/RpcClient"; import * as RpcClient from "effect/unstable/rpc/RpcClient";
import type { RpcClientError } from "effect/unstable/rpc/RpcClientError"; import type { RpcClientError } from "effect/unstable/rpc/RpcClientError";
@ -19,17 +19,17 @@ class TrustGraphRpcClientService extends Context.Service<
export type RpcConnectionStatus = "connecting" | "connected" | "failed" | "closed"; export type RpcConnectionStatus = "connecting" | "connected" | "failed" | "closed";
export interface RpcConnectionState { export class RpcConnectionState extends S.Class<RpcConnectionState>("RpcConnectionState")({
status: RpcConnectionStatus; status: S.Literals(["connecting", "connected", "failed", "closed"]),
lastError?: string; lastError: S.optionalKey(S.String),
} }, { description: "Current Effect RPC gateway connection state." }) {}
export interface DispatchInput { export class DispatchInput extends S.Class<DispatchInput>("DispatchInput")({
scope: "global" | "flow"; scope: S.Literals(["global", "flow"]),
service: string; service: S.String,
flow?: string; flow: S.optionalKey(S.String),
request: Record<string, unknown>; request: S.Record(S.String, S.Unknown),
} }, { description: "TrustGraph gateway dispatch target and request payload." }) {}
export interface DispatchOptions { export interface DispatchOptions {
readonly timeoutMs?: number; readonly timeoutMs?: number;
@ -74,20 +74,6 @@ export interface TrustGraphGatewayClientOptions {
const DEFAULT_REQUEST_TIMEOUT_MS = 10_000; const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;
const DEFAULT_REQUEST_ATTEMPTS = 3; const DEFAULT_REQUEST_ATTEMPTS = 3;
export interface EffectRpcClient {
readonly subscribe: (listener: (state: RpcConnectionState) => void) => () => void;
readonly dispatch: (
input: DispatchInput,
options?: DispatchOptions,
) => Promise<unknown>;
readonly dispatchStream: (
input: DispatchInput,
receiver: (chunk: DispatchStreamChunk) => boolean,
options?: DispatchOptions,
) => Promise<DispatchStreamChunk | undefined>;
readonly close: () => Promise<void>;
}
const makeClientLayer = ( const makeClientLayer = (
options: TrustGraphGatewayClientOptions, options: TrustGraphGatewayClientOptions,
stateRef: SubscriptionRef.SubscriptionRef<RpcConnectionState>, stateRef: SubscriptionRef.SubscriptionRef<RpcConnectionState>,
@ -234,59 +220,12 @@ export function makeEffectRpcClient(
url: string, url: string,
onConnect?: () => void, onConnect?: () => void,
onDisconnect?: () => void, onDisconnect?: () => void,
): EffectRpcClient { ): Effect.Effect<TrustGraphGatewayClient, never, Scope.Scope> {
const stateRef = Effect.runSync(SubscriptionRef.make<RpcConnectionState>({ status: "connecting" })); return makeTrustGraphGatewayClientScoped({
const closedRef = Effect.runSync(Ref.make(false));
const scope = Effect.runSync(Scope.make());
const options: TrustGraphGatewayClientOptions = {
url, url,
stateRef,
closedRef,
...(onConnect === undefined ? {} : { onConnect }), ...(onConnect === undefined ? {} : { onConnect }),
...(onDisconnect === undefined ? {} : { onDisconnect }), ...(onDisconnect === undefined ? {} : { onDisconnect }),
}; });
const clientPromise = Effect.runPromise(
makeTrustGraphGatewayClientScoped(options).pipe(Scope.provide(scope)),
);
return {
subscribe: (listener) => {
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 () => {
cancelled = true;
if (unsubscribe !== undefined) {
Effect.runFork(unsubscribe);
}
};
},
dispatch: (input, options = {}) =>
clientPromise.then((client) =>
Effect.runPromise(client.dispatch(input, 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)),
),
)
),
};
} }
export function withDispatchRequestPolicy<A, E, R>( export function withDispatchRequestPolicy<A, E, R>(

View file

@ -1,16 +1,13 @@
// Import core types and classes for the TrustGraph API // Import core types and classes for the TrustGraph API
import type { Term, Triple } from "../models/Triple.js"; import { Triple } from "../models/Triple.js";
import type { Term } from "../models/Triple.js";
import type { import type {
EffectRpcClient,
DispatchInput, DispatchInput,
DispatchOptions, DispatchOptions,
RpcConnectionState, RpcConnectionState,
} from "./effect-rpc-client.js"; } from "./effect-rpc-client.js";
import {
makeEffectRpcClient,
} from "./effect-rpc-client.js";
import { getDefaultSocketUrl, getRandomValues } from "./websocket-adapter.js"; import { getDefaultSocketUrl, getRandomValues } from "./websocket-adapter.js";
import { Clock, Effect, Fiber, Match, Option, Result, Schema as S, Stream, SubscriptionRef } from "effect"; import { Match, Option, Schema as S } from "effect";
import * as Predicate from "effect/Predicate"; import * as Predicate from "effect/Predicate";
// Import all message types for different services // Import all message types for different services
@ -89,19 +86,32 @@ export interface GraphRagOptions {
pathLength?: number; pathLength?: number;
} }
// Metadata included in final streaming message export interface LegacyRpcClient {
export interface StreamingMetadata { readonly subscribe: (listener: (state: RpcConnectionState) => void) => () => void;
in_token?: number; readonly dispatch: (
out_token?: number; input: DispatchInput,
model?: string; options?: DispatchOptions,
) => Promise<unknown>;
readonly dispatchStream: (
input: DispatchInput,
receiver: (chunk: { readonly response: unknown; readonly complete: boolean }) => boolean,
options?: DispatchOptions,
) => Promise<unknown>;
readonly close: () => Promise<void>;
} }
// Explainability event data // Metadata included in final streaming message
export interface ExplainEvent { export class StreamingMetadata extends S.Class<StreamingMetadata>("StreamingMetadata")({
explainId: string; in_token: S.optionalKey(S.Finite),
explainGraph: string; // Named graph where explain data is stored (e.g., urn:graph:retrieval) out_token: S.optionalKey(S.Finite),
explainTriples?: Triple[]; // Inline subgraph triples (when available) model: S.optionalKey(S.String),
} }, { description: "Token and model metadata attached to a final streaming chunk." }) {}
export class ExplainEvent extends S.Class<ExplainEvent>("ExplainEvent")({
explainId: S.String,
explainGraph: S.String,
explainTriples: S.optionalKey(S.Array(Triple).pipe(S.mutable)),
}, { description: "Explainability graph reference or inline triples for a stream." }) {}
// Configuration constants // Configuration constants
const SOCKET_URL = getDefaultSocketUrl(); // WebSocket endpoint path (isomorphic) const SOCKET_URL = getDefaultSocketUrl(); // WebSocket endpoint path (isomorphic)
@ -155,7 +165,11 @@ function dispatchOptions(
} }
function streamingMetadataFrom(source: unknown): StreamingMetadata | undefined { function streamingMetadataFrom(source: unknown): StreamingMetadata | undefined {
const metadata: StreamingMetadata = {}; const metadata: {
in_token?: number;
out_token?: number;
model?: string;
} = {};
let hasMetadata = false; let hasMetadata = false;
const inToken = numberProperty(source, "in_token"); const inToken = numberProperty(source, "in_token");
@ -185,12 +199,12 @@ function throwIfResponseError(error: ResponseError | undefined): void {
const decodeJsonUnknown = S.decodeUnknownOption(S.UnknownFromJsonString); const decodeJsonUnknown = S.decodeUnknownOption(S.UnknownFromJsonString);
export interface ConfigValueEntry { export class ConfigValueEntry extends S.Class<ConfigValueEntry>("ConfigValueEntry")({
workspace?: string; workspace: S.optionalKey(S.String),
type?: string; type: S.optionalKey(S.String),
key: string; key: S.String,
value: unknown; value: S.Unknown,
} }, { description: "Config key/value entry returned from the TrustGraph config service." }) {}
function asConfigValues(response: unknown): ConfigValueEntry[] { function asConfigValues(response: unknown): ConfigValueEntry[] {
if (response === null || typeof response !== "object") return []; if (response === null || typeof response !== "object") return [];
@ -201,9 +215,12 @@ function asConfigValues(response: unknown): ConfigValueEntry[] {
const item = value as Record<string, unknown>; const item = value as Record<string, unknown>;
const key = item.key; const key = item.key;
if (typeof key !== "string") return []; if (typeof key !== "string") return [];
const entry: ConfigValueEntry = { key, value: item.value }; const entry: ConfigValueEntry = {
if (typeof item.workspace === "string") entry.workspace = item.workspace; key,
if (typeof item.type === "string") entry.type = item.type; value: item.value,
...(typeof item.workspace === "string" ? { workspace: item.workspace } : {}),
...(typeof item.type === "string" ? { type: item.type } : {}),
};
return [entry]; return [entry];
}); });
} }
@ -222,15 +239,11 @@ function parseResponseJson(value: string | undefined, operation: string): unknow
} }
const currentEpochSeconds = (): number => const currentEpochSeconds = (): number =>
Math.floor(Effect.runSync(Clock.currentTimeMillis) / 1000); Math.floor((globalThis.performance.timeOrigin + globalThis.performance.now()) / 1000);
const logClientInfo = (message: string): void => { const logClientInfo = (_message: string): void => {};
Effect.runFork(Effect.log(message));
};
const logClientError = (message: string, error: unknown): void => { const logClientError = (_message: string, _error: unknown): void => {};
Effect.runFork(Effect.logError(message, { error: toErrorMessage(error, message) }));
};
const runLegacyStreamingRequest = ( const runLegacyStreamingRequest = (
operation: string, operation: string,
@ -238,18 +251,9 @@ const runLegacyStreamingRequest = (
request: () => Promise<unknown>, request: () => Promise<unknown>,
onError: (message: string) => void, onError: (message: string) => void,
): Promise<unknown | void> => ): Promise<unknown | void> =>
Effect.runPromise( request().catch((error) => {
Effect.tryPromise({ onError(`${label} request failed: ${toErrorMessage(error, `${operation} failed`)}`);
try: request, });
catch: (error) => socketError(operation, toErrorMessage(error, "Unknown error")),
}).pipe(
Effect.catch((error) =>
Effect.sync(() => {
onError(`${label} request failed: ${error.message}`);
})
),
),
);
const StreamingEnvelopeSchema = S.Struct({ const StreamingEnvelopeSchema = S.Struct({
response: S.optionalKey(S.Unknown), response: S.optionalKey(S.Unknown),
@ -453,34 +457,35 @@ function makeid(length: number) {
* functionality * functionality
*/ */
// Connection state interface for UI consumption // Connection state interface for UI consumption
export interface ConnectionState { export class ConnectionState extends S.Class<ConnectionState>("ConnectionState")({
status: status: S.Literals([
| "connecting" "connecting",
| "connected" "connected",
| "reconnecting" "reconnecting",
| "failed" "failed",
| "authenticated" "authenticated",
| "unauthenticated"; "unauthenticated",
hasApiKey: boolean; ]),
reconnectAttempt?: number; hasApiKey: S.Boolean,
maxAttempts?: number; reconnectAttempt: S.optionalKey(S.Finite),
nextRetryIn?: number; maxAttempts: S.optionalKey(S.Finite),
lastError?: string; nextRetryIn: S.optionalKey(S.Finite),
} lastError: S.optionalKey(S.String),
}, { description: "Workbench-facing TrustGraph gateway connection state." }) {}
export function makeBaseApi( export function makeBaseApi(
user: string, user: string,
token?: string, token: string | undefined,
socketUrl?: string, socketUrl: string | undefined,
rpcFactory: (url: string) => EffectRpcClient = makeEffectRpcClient, rpcFactory: (url: string) => LegacyRpcClient,
) { ) {
let rpc: EffectRpcClient; let rpc: LegacyRpcClient;
const connectionStateRef = Effect.runSync( let unsubscribeRpc: (() => void) | undefined;
SubscriptionRef.make<ConnectionState>({ const connectionStateListeners = new Set<(state: ConnectionState) => void>();
status: "connecting", let connectionState: ConnectionState = {
hasApiKey: isNonEmptyString(token), status: "connecting",
}), hasApiKey: isNonEmptyString(token),
); };
let lastError: string | undefined ; let lastError: string | undefined ;
let rpcState: RpcConnectionState = { status: "connecting" }; let rpcState: RpcConnectionState = { status: "connecting" };
@ -495,27 +500,10 @@ export function makeBaseApi(
* Subscribe to connection state changes for UI updates * Subscribe to connection state changes for UI updates
*/ */
onConnectionStateChange(listener: (state: ConnectionState) => void) { onConnectionStateChange(listener: (state: ConnectionState) => void) {
let latest = SubscriptionRef.getUnsafe(connectionStateRef); connectionStateListeners.add(listener);
listener(latest); notifyConnectionStateListener(listener, connectionState);
let replaySeen = false;
const fiber = Effect.runFork(
SubscriptionRef.changes(connectionStateRef).pipe(
Stream.runForEach((state) =>
Effect.sync(() => {
if (!replaySeen) {
replaySeen = true;
if (state === latest) return;
}
latest = state;
notifyConnectionStateListener(listener, state);
})
),
),
);
// Return unsubscribe function
return () => { return () => {
Effect.runFork(Fiber.interrupt(fiber)); connectionStateListeners.delete(listener);
}; };
}, },
@ -523,13 +511,9 @@ export function makeBaseApi(
* Closes the WebSocket connection and cleans up * Closes the WebSocket connection and cleans up
*/ */
close() { close() {
Effect.runFork( unsubscribeRpc?.();
Effect.tryPromise({ void rpc.close().catch((error) =>
try: () => rpc.close(), logClientError("[socket close error]", socketError("socket-close", toErrorMessage(error, "Socket close failed")))
catch: (error) => socketError("socket-close", toErrorMessage(error, "Socket close failed")),
}).pipe(
Effect.catch((error) => Effect.sync(() => logClientError("[socket close error]", error))),
),
); );
}, },
@ -643,10 +627,8 @@ export function makeBaseApi(
const state: ConnectionState = { const state: ConnectionState = {
status, status,
hasApiKey, hasApiKey,
...(lastError !== undefined ? { lastError } : {}),
}; };
if (lastError !== undefined) {
state.lastError = lastError;
}
return state; return state;
}; };
@ -655,21 +637,24 @@ export function makeBaseApi(
listener: (state: ConnectionState) => void, listener: (state: ConnectionState) => void,
state: ConnectionState, state: ConnectionState,
): void => { ): void => {
const result = Result.try({ try {
try: () => listener(state), listener(state);
catch: (error) => } catch (error) {
logClientError(
"Error in connection state listener",
socketError( socketError(
"connection-state-listener", "connection-state-listener",
toErrorMessage(error, "Error in connection state listener"), toErrorMessage(error, "Error in connection state listener"),
), ),
}); );
if (Result.isFailure(result)) {
logClientError("Error in connection state listener", result.failure);
} }
}; };
const publishConnectionState = () => { const publishConnectionState = () => {
Effect.runSync(SubscriptionRef.set(connectionStateRef, getConnectionState())); connectionState = getConnectionState();
for (const listener of connectionStateListeners) {
notifyConnectionStateListener(listener, connectionState);
}
}; };
const connectionStatusFromRpc = (hasApiKey: boolean): ConnectionState["status"] => const connectionStatusFromRpc = (hasApiKey: boolean): ConnectionState["status"] =>
@ -712,7 +697,7 @@ export function makeBaseApi(
}; };
rpc = rpcFactory(socketUrlWithToken()); rpc = rpcFactory(socketUrlWithToken());
rpc.subscribe((state) => { unsubscribeRpc = rpc.subscribe((state) => {
rpcState = state; rpcState = state;
lastError = state.lastError; lastError = state.lastError;
publishConnectionState(); publishConnectionState();
@ -726,13 +711,12 @@ export function makeBaseApi(
} }
export type BaseApi = ReturnType<typeof makeBaseApi>; export type BaseApi = ReturnType<typeof makeBaseApi>;
export const BaseApi = makeBaseApi;
export function makeBaseApiWithRpc( export function makeBaseApiWithRpc(
user: string, user: string,
token: string | undefined, token: string | undefined,
socketUrl: string | undefined, socketUrl: string | undefined,
rpc: EffectRpcClient, rpc: LegacyRpcClient,
): BaseApi { ): BaseApi {
return makeBaseApi(user, token, socketUrl, () => rpc); return makeBaseApi(user, token, socketUrl, () => rpc);
} }
@ -833,13 +817,9 @@ export function makeLibrarianApi(api: BaseApi) {
tags, tags,
"document-type": "source", "document-type": "source",
documentType: "source", documentType: "source",
...(id !== undefined ? { id } : {}),
...(metadata !== undefined ? { metadata } : {}),
}; };
if (id !== undefined) {
documentMetadata.id = id;
}
if (metadata !== undefined) {
documentMetadata.metadata = metadata;
}
return this.api.makeRequest<LibraryRequest, LibraryResponse>( return this.api.makeRequest<LibraryRequest, LibraryResponse>(
"librarian", "librarian",
@ -929,10 +909,8 @@ export function makeLibrarianApi(api: BaseApi) {
"document-metadata": metadata, "document-metadata": metadata,
documentMetadata: metadata, documentMetadata: metadata,
"total-size": totalSize, "total-size": totalSize,
...(chunkSize !== undefined ? { "chunk-size": chunkSize } : {}),
}; };
if (chunkSize !== undefined) {
request["chunk-size"] = chunkSize;
}
return this.api return this.api
.makeRequest<BeginUploadRequest, BeginUploadResponse>( .makeRequest<BeginUploadRequest, BeginUploadResponse>(
@ -1124,10 +1102,8 @@ export function makeLibrarianApi(api: BaseApi) {
operation: "stream-document", operation: "stream-document",
"document-id": documentId, "document-id": documentId,
user: this.api.user, user: this.api.user,
...(chunkSize !== undefined ? { "chunk-size": chunkSize } : {}),
}; };
if (chunkSize !== undefined) {
request["chunk-size"] = chunkSize;
}
this.api.makeRequestMulti<StreamDocumentRequest, StreamDocumentResponse>( this.api.makeRequestMulti<StreamDocumentRequest, StreamDocumentResponse>(
"librarian", "librarian",
@ -1369,13 +1345,9 @@ export function makeFlowsApi(api: BaseApi) {
"flow-id": id, "flow-id": id,
"blueprint-name": blueprint_name, "blueprint-name": blueprint_name,
description: description, description: description,
...(parameters !== undefined && Object.keys(parameters).length > 0 ? { parameters } : {}),
}; };
// Only include parameters if provided and not empty
if (parameters !== undefined && Object.keys(parameters).length > 0) {
request.parameters = parameters;
}
return this.api return this.api
.makeRequest<FlowRequest, FlowResponse>("flow", request, 30000) .makeRequest<FlowRequest, FlowResponse>("flow", request, 30000)
.then((response) => { .then((response) => {
@ -1447,19 +1419,11 @@ export function makeFlowApi(api: BaseApi, flowId: string) {
query: text, query: text,
user: this.api.user, user: this.api.user,
collection: withDefault(collection, "default"), collection: withDefault(collection, "default"),
...(options?.entityLimit !== undefined ? { "entity-limit": options.entityLimit } : {}),
...(options?.tripleLimit !== undefined ? { "triple-limit": options.tripleLimit } : {}),
...(options?.maxSubgraphSize !== undefined ? { "max-subgraph-size": options.maxSubgraphSize } : {}),
...(options?.pathLength !== undefined ? { "max-path-length": options.pathLength } : {}),
}; };
if (options?.entityLimit !== undefined) {
request["entity-limit"] = options.entityLimit;
}
if (options?.tripleLimit !== undefined) {
request["triple-limit"] = options.tripleLimit;
}
if (options?.maxSubgraphSize !== undefined) {
request["max-subgraph-size"] = options.maxSubgraphSize;
}
if (options?.pathLength !== undefined) {
request["max-path-length"] = options.pathLength;
}
return this.api return this.api
.makeRequest<GraphRagRequest, GraphRagResponse>( .makeRequest<GraphRagRequest, GraphRagResponse>(
@ -1539,10 +1503,8 @@ export function makeFlowApi(api: BaseApi, flowId: string) {
const event: ExplainEvent = { const event: ExplainEvent = {
explainId: explainId ?? "", explainId: explainId ?? "",
explainGraph: stringProperty(resp, "explain_graph") ?? "", explainGraph: stringProperty(resp, "explain_graph") ?? "",
...(explainTriples !== undefined ? { explainTriples } : {}),
}; };
if (explainTriples !== undefined) {
event.explainTriples = explainTriples;
}
onExplain?.(event); onExplain?.(event);
return false; return false;
} }
@ -1635,10 +1597,8 @@ export function makeFlowApi(api: BaseApi, flowId: string) {
const event: ExplainEvent = { const event: ExplainEvent = {
explainId: explainId ?? "", explainId: explainId ?? "",
explainGraph: stringProperty(resp, "explain_graph") ?? "", explainGraph: stringProperty(resp, "explain_graph") ?? "",
...(explainTriples !== undefined ? { explainTriples } : {}),
}; };
if (explainTriples !== undefined) {
event.explainTriples = explainTriples;
}
onExplain?.(event); onExplain?.(event);
// If this message also carries answer text, fall through to chunk handling. // If this message also carries answer text, fall through to chunk handling.
// If it's a standalone explain event (no answer text), stop here. // If it's a standalone explain event (no answer text), stop here.
@ -1668,19 +1628,11 @@ export function makeFlowApi(api: BaseApi, flowId: string) {
user: this.api.user, user: this.api.user,
collection: withDefault(collection, "default"), collection: withDefault(collection, "default"),
streaming: true, streaming: true,
...(options?.entityLimit !== undefined ? { "entity-limit": options.entityLimit } : {}),
...(options?.tripleLimit !== undefined ? { "triple-limit": options.tripleLimit } : {}),
...(options?.maxSubgraphSize !== undefined ? { "max-subgraph-size": options.maxSubgraphSize } : {}),
...(options?.pathLength !== undefined ? { "max-path-length": options.pathLength } : {}),
}; };
if (options?.entityLimit !== undefined) {
request["entity-limit"] = options.entityLimit;
}
if (options?.tripleLimit !== undefined) {
request["triple-limit"] = options.tripleLimit;
}
if (options?.maxSubgraphSize !== undefined) {
request["max-subgraph-size"] = options.maxSubgraphSize;
}
if (options?.pathLength !== undefined) {
request["max-path-length"] = options.pathLength;
}
void runLegacyStreamingRequest( void runLegacyStreamingRequest(
"graph-rag-stream", "graph-rag-stream",
@ -1765,10 +1717,8 @@ export function makeFlowApi(api: BaseApi, flowId: string) {
user: this.api.user, user: this.api.user,
collection: withDefault(collection, "default"), collection: withDefault(collection, "default"),
streaming: true, streaming: true,
...(docLimit !== undefined ? { "doc-limit": docLimit } : {}),
}; };
if (docLimit !== undefined) {
request["doc-limit"] = docLimit;
}
void runLegacyStreamingRequest( void runLegacyStreamingRequest(
"document-rag-stream", "document-rag-stream",
@ -1968,19 +1918,11 @@ export function makeFlowApi(api: BaseApi, flowId: string) {
limit: limit ?? 20, limit: limit ?? 20,
user: this.api.user, user: this.api.user,
collection: withDefault(collection, "default"), collection: withDefault(collection, "default"),
...(s !== undefined ? { s } : {}),
...(p !== undefined ? { p } : {}),
...(o !== undefined ? { o } : {}),
...(graph !== undefined ? { g: graph } : {}),
}; };
if (s !== undefined) {
request.s = s;
}
if (p !== undefined) {
request.p = p;
}
if (o !== undefined) {
request.o = o;
}
if (graph !== undefined) {
request.g = graph;
}
return this.api return this.api
.makeRequest<TriplesQueryRequest, TriplesQueryResponse>( .makeRequest<TriplesQueryRequest, TriplesQueryResponse>(
@ -2005,13 +1947,9 @@ export function makeFlowApi(api: BaseApi, flowId: string) {
) { ) {
const request: LoadDocumentRequest = { const request: LoadDocumentRequest = {
data: document, data: document,
...(id !== undefined ? { id } : {}),
...(metadata !== undefined ? { metadata } : {}),
}; };
if (id !== undefined) {
request.id = id;
}
if (metadata !== undefined) {
request.metadata = metadata;
}
return this.api.makeRequest<LoadDocumentRequest, LoadDocumentResponse>( return this.api.makeRequest<LoadDocumentRequest, LoadDocumentResponse>(
"document-load", "document-load",
@ -2035,16 +1973,10 @@ export function makeFlowApi(api: BaseApi, flowId: string) {
) { ) {
const request: LoadTextRequest = { const request: LoadTextRequest = {
text, text,
...(id !== undefined ? { id } : {}),
...(metadata !== undefined ? { metadata } : {}),
...(charset !== undefined ? { charset } : {}),
}; };
if (id !== undefined) {
request.id = id;
}
if (metadata !== undefined) {
request.metadata = metadata;
}
if (charset !== undefined) {
request.charset = charset;
}
return this.api.makeRequest<LoadTextRequest, LoadTextResponse>( return this.api.makeRequest<LoadTextRequest, LoadTextResponse>(
"text-load", "text-load",
@ -2070,13 +2002,9 @@ export function makeFlowApi(api: BaseApi, flowId: string) {
query, query,
user: this.api.user, user: this.api.user,
collection: withDefault(collection, "default"), collection: withDefault(collection, "default"),
...(variables !== undefined ? { variables } : {}),
...(operationName !== undefined ? { operation_name: operationName } : {}),
}; };
if (variables !== undefined) {
request.variables = variables;
}
if (operationName !== undefined) {
request.operation_name = operationName;
}
return this.api return this.api
.makeRequest<RowsQueryRequest, RowsQueryResponse>( .makeRequest<RowsQueryRequest, RowsQueryResponse>(
@ -2167,12 +2095,9 @@ export function makeFlowApi(api: BaseApi, flowId: string) {
user: this.api.user, user: this.api.user,
collection: withDefault(collection, "default"), collection: withDefault(collection, "default"),
limit: limit ?? 10, limit: limit ?? 10,
...(indexName !== undefined ? { index_name: indexName } : {}),
}; };
if (indexName !== undefined) {
request.index_name = indexName;
}
return this.api return this.api
.makeRequest<RowEmbeddingsQueryRequest, RowEmbeddingsQueryResponse>( .makeRequest<RowEmbeddingsQueryRequest, RowEmbeddingsQueryResponse>(
"row-embeddings", "row-embeddings",
@ -2664,16 +2589,3 @@ export function makeCollectionManagementApi(api: BaseApi) {
export type CollectionManagementApi = ReturnType<typeof makeCollectionManagementApi>; export type CollectionManagementApi = ReturnType<typeof makeCollectionManagementApi>;
export const CollectionManagementApi = makeCollectionManagementApi; export const CollectionManagementApi = makeCollectionManagementApi;
/**
* Factory function to create a new TrustGraph WebSocket connection
* This is the main entry point for using the TrustGraph API
* @param user - User identifier for API requests
* @param token - Optional authentication token for secure connections
* @param socketUrl - Optional WebSocket URL (defaults to /api/v1/rpc for browser, provide full URL for Node.js)
*/
export const createTrustGraphSocket = (
user: string,
token?: string,
socketUrl?: string,
): BaseApi => makeBaseApi(user, token, socketUrl);

View file

@ -1,6 +1,6 @@
import { describe, expect, it } from "@effect/vitest"; import { describe, expect, it } from "@effect/vitest";
import type { BaseApi, TrustGraphGatewayClient } from "@trustgraph/client"; import type { DispatchInput, TrustGraphGatewayClient } from "@trustgraph/client";
import { DispatchStreamChunk, } from "@trustgraph/client"; import { DispatchError, DispatchStreamChunk, } from "@trustgraph/client";
import { Effect, Layer, Stream } from "effect"; import { Effect, Layer, Stream } from "effect";
import * as S from "effect/Schema"; import * as S from "effect/Schema";
import { McpServer } from "effect/unstable/ai"; import { McpServer } from "effect/unstable/ai";
@ -15,7 +15,6 @@ import {
TrustGraphMcpToolkit, TrustGraphMcpToolkit,
TrustGraphMcpToolkitLive, TrustGraphMcpToolkitLive,
TrustGraphGateway, TrustGraphGateway,
TrustGraphSocket,
} from "../server-effect.js"; } from "../server-effect.js";
const expectedToolNames = [ const expectedToolNames = [
@ -61,7 +60,7 @@ interface NativeTestClientOptions {
const decodeJsonText = S.decodeUnknownSync(S.UnknownFromJsonString); const decodeJsonText = S.decodeUnknownSync(S.UnknownFromJsonString);
const makeFakeSocket = ( const makeFakeSocket = (
options: { _options: {
readonly textCompletion?: (() => Promise<string>) | undefined; readonly textCompletion?: (() => Promise<string>) | undefined;
readonly graphRag?: (() => Promise<string>) | undefined; readonly graphRag?: (() => Promise<string>) | undefined;
} = {}, } = {},
@ -71,66 +70,62 @@ const makeFakeSocket = (
graphRag: [], graphRag: [],
}; };
const socket = { return { calls };
close: () => {},
flow: (flowId: string) => {
calls.flowIds.push(flowId);
return {
textCompletion: () => options.textCompletion === undefined
? Promise.resolve("gateway text completion")
: options.textCompletion(),
graphRag: (query: string, ragOptions: unknown, collection?: string) => {
calls.graphRag.push({ query, options: ragOptions, collection });
return options.graphRag === undefined
? Promise.resolve("graph rag answer")
: options.graphRag();
},
documentRag: () => Promise.resolve("document rag answer"),
agent: (
_question: string,
_onThought: () => void,
_onObservation: () => void,
onAnswer: (chunk: string, complete: boolean) => void,
) => onAnswer("agent answer", true),
embeddings: () => Promise.resolve([[0.25, 0.75]]),
triplesQuery: () => Promise.resolve([]),
graphEmbeddingsQuery: () => Promise.resolve([]),
};
},
config: () => ({
getConfigAll: () => Promise.resolve({}),
getConfig: () => Promise.resolve({}),
putConfig: () => Promise.resolve({ ok: true }),
deleteConfig: () => Promise.resolve({ ok: true }),
getPrompts: () => Promise.resolve([]),
getPrompt: () => Promise.resolve({}),
}),
flows: () => ({
getFlows: () => Promise.resolve(["default"]),
getFlow: () => Promise.resolve({}),
startFlow: () => Promise.resolve({ ok: true }),
stopFlow: () => Promise.resolve({ ok: true }),
}),
librarian: () => ({
getDocuments: () => Promise.resolve([]),
loadDocument: () => Promise.resolve({ ok: true }),
removeDocument: () => Promise.resolve({ ok: true }),
}),
knowledge: () => ({
getKnowledgeCores: () => Promise.resolve([]),
deleteKgCore: () => Promise.resolve({ ok: true }),
loadKgCore: () => Promise.resolve({ ok: true }),
}),
} as unknown as BaseApi;
return { socket, calls };
}; };
const makeFakeGateway = (): TrustGraphGatewayClient => ({ const makeFakeGateway = (
calls: FakeSocketCalls,
options: NativeTestClientOptions = {},
): TrustGraphGatewayClient => ({
state: Effect.succeed({ status: "connected" }), state: Effect.succeed({ status: "connected" }),
changes: Stream.empty, changes: Stream.empty,
subscribe: () => Effect.succeed(Effect.void), subscribe: () => Effect.succeed(Effect.void),
dispatch: () => Effect.succeed({}), dispatch: Effect.fn("FakeTrustGraphGateway.dispatch")(function*(input: DispatchInput) {
if (input.flow !== undefined) calls.flowIds.push(input.flow);
if (input.service === "text-completion") {
const response = options.textCompletion === undefined
? "gateway text completion"
: yield* Effect.tryPromise({
try: options.textCompletion,
catch: (cause) => DispatchError.make({
message: cause instanceof Error ? cause.message : String(cause),
}),
});
return { response };
}
if (input.service === "graph-rag") {
calls.graphRag.push({
query: String(input.request.query ?? ""),
options: {
entityLimit: input.request["entity-limit"],
tripleLimit: input.request["triple-limit"],
},
collection: typeof input.request.collection === "string" ? input.request.collection : undefined,
});
const response = options.graphRag === undefined
? "graph rag answer"
: yield* Effect.tryPromise({
try: options.graphRag,
catch: (cause) => DispatchError.make({
message: cause instanceof Error ? cause.message : String(cause),
}),
});
return { response };
}
if (input.service === "document-rag") return { response: "document rag answer" };
if (input.service === "embeddings") return { vectors: [[0.25, 0.75]] };
if (input.service === "triples") return { triples: [] };
if (input.service === "graph-embeddings") return { entities: [] };
if (input.service === "config") return {};
if (input.service === "flow" && input.request.operation === "list-flows") return { "flow-ids": ["default"] };
if (input.service === "flow" && input.request.operation === "get-flow") return { flow: "{}" };
if (input.service === "flow") return { ok: true };
if (input.service === "librarian" && input.request.operation === "list-documents") return { "document-metadatas": [] };
if (input.service === "librarian") return { ok: true };
if (input.service === "knowledge" && input.request.operation === "list-kg-cores") return { ids: [] };
if (input.service === "knowledge") return { ok: true };
return {};
}),
dispatchStream: () => Stream.empty, dispatchStream: () => Stream.empty,
runDispatchStream: (_input, receiver) => runDispatchStream: (_input, receiver) =>
Effect.sync(() => { Effect.sync(() => {
@ -168,15 +163,14 @@ const makeNativeTestClient = (
const makeNativeTestClientEffect = Effect.fn("makeNativeTestClient")(function*( const makeNativeTestClientEffect = Effect.fn("makeNativeTestClient")(function*(
options: NativeTestClientOptions, options: NativeTestClientOptions,
) { ) {
const { socket, calls } = makeFakeSocket({ const { calls } = makeFakeSocket({
textCompletion: options.textCompletion, textCompletion: options.textCompletion,
graphRag: options.graphRag, graphRag: options.graphRag,
}); });
const gateway = makeFakeGateway(); const gateway = makeFakeGateway(calls, options);
const serverLayer = McpServer.toolkit(TrustGraphMcpToolkit).pipe( const serverLayer = McpServer.toolkit(TrustGraphMcpToolkit).pipe(
Layer.provide(TrustGraphMcpToolkitLive), Layer.provide(TrustGraphMcpToolkitLive),
Layer.provide(Layer.succeed(TrustGraphGateway, TrustGraphGateway.of(gateway))), Layer.provide(Layer.succeed(TrustGraphGateway, TrustGraphGateway.of(gateway))),
Layer.provide(Layer.succeed(TrustGraphSocket, TrustGraphSocket.of(socket))),
Layer.provide(Layer.succeed(TrustGraphMcpConfig, testConfig)), Layer.provide(Layer.succeed(TrustGraphMcpConfig, testConfig)),
Layer.provide(McpServer.layerHttp({ Layer.provide(McpServer.layerHttp({
name: "trustgraph", name: "trustgraph",

View file

@ -1,15 +1,15 @@
import {BunHttpServer, BunRuntime} from "@effect/platform-bun"; import {BunHttpServer, BunRuntime} from "@effect/platform-bun";
import {NodeRuntime, NodeStdio} from "@effect/platform-node"; import {NodeRuntime, NodeStdio} from "@effect/platform-node";
import type { import type {
BaseApi, EntityMatch as ClientEntityMatch,
Term as ClientTerm, Term as ClientTerm,
Triple as ClientTriple,
TrustGraphGatewayClient, TrustGraphGatewayClient,
} from "@trustgraph/client"; } from "@trustgraph/client";
import { import {
createTrustGraphSocket,
makeTrustGraphGatewayClientScoped, makeTrustGraphGatewayClientScoped,
} from "@trustgraph/client"; } from "@trustgraph/client";
import {Config, Context, Effect, Layer} from "effect"; import {Clock, Config, Context, Effect, Layer} from "effect";
import * as O from "effect/Option"; import * as O from "effect/Option";
import * as Predicate from "effect/Predicate"; import * as Predicate from "effect/Predicate";
import {McpServer, Tool, Toolkit} from "effect/unstable/ai"; import {McpServer, Tool, Toolkit} from "effect/unstable/ai";
@ -1279,22 +1279,6 @@ export class TrustGraphMcpConfig extends Context.Service<TrustGraphMcpConfig, Tr
) )
} }
export class TrustGraphSocket extends Context.Service<TrustGraphSocket, BaseApi>()(
"@trustgraph/mcp/server-effect/TrustGraphSocket",
) {
static readonly layer = Layer.effect(
TrustGraphSocket,
Effect.gen(function*() {
const config = yield* TrustGraphMcpConfig
const socket = yield* Effect.acquireRelease(
Effect.sync(() => createTrustGraphSocket(config.user, config.token, config.gatewayUrl)),
(socket) => Effect.sync(() => socket.close()),
)
return TrustGraphSocket.of(socket)
}),
)
}
export class TrustGraphGateway extends Context.Service<TrustGraphGateway, TrustGraphGatewayClient>()( export class TrustGraphGateway extends Context.Service<TrustGraphGateway, TrustGraphGatewayClient>()(
"@trustgraph/mcp/server-effect/TrustGraphGateway", "@trustgraph/mcp/server-effect/TrustGraphGateway",
) { ) {
@ -1360,6 +1344,36 @@ const decodeJsonArrayOrFail = <E>(
const asIriTerm = (value: string | undefined): ClientTerm | undefined => const asIriTerm = (value: string | undefined): ClientTerm | undefined =>
value !== undefined && value.length > 0 ? {t: "i", i: value} : undefined value !== undefined && value.length > 0 ? {t: "i", i: value} : undefined
const dispatchGlobal = <E>(
gateway: TrustGraphGatewayClient,
service: string,
request: Record<string, unknown>,
makeError: (cause: unknown) => E,
options: {readonly timeoutMs?: number; readonly retries?: number} = {},
) =>
gateway.dispatch({scope: "global", service, request}, options).pipe(
Effect.map(asRecord),
Effect.mapError(makeError),
)
const dispatchFlow = <E>(
gateway: TrustGraphGatewayClient,
config: TrustGraphMcpConfigShape,
service: string,
request: Record<string, unknown>,
makeError: (cause: unknown) => E,
options: {readonly timeoutMs?: number; readonly retries?: number} = {},
) =>
gateway.dispatch({
scope: "flow",
flow: config.flowId,
service,
request,
}, options).pipe(
Effect.map(asRecord),
Effect.mapError(makeError),
)
const runAgentTool = Effect.fn("TrustGraphMcpToolkit.agent")(function*( const runAgentTool = Effect.fn("TrustGraphMcpToolkit.agent")(function*(
gateway: TrustGraphGatewayClient, gateway: TrustGraphGatewayClient,
config: TrustGraphMcpConfigShape, config: TrustGraphMcpConfigShape,
@ -1411,90 +1425,134 @@ const runAgentTool = Effect.fn("TrustGraphMcpToolkit.agent")(function*(
export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer( export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
Effect.gen(function*() { Effect.gen(function*() {
const config = yield* TrustGraphMcpConfig const config = yield* TrustGraphMcpConfig
const socket = yield* TrustGraphSocket
const gateway = yield* TrustGraphGateway const gateway = yield* TrustGraphGateway
return TrustGraphMcpToolkit.of({ return TrustGraphMcpToolkit.of({
text_completion: ({system, prompt}) => text_completion: ({system, prompt}) =>
Effect.tryPromise({ dispatchFlow(
try: () => socket.flow(config.flowId).textCompletion(system, prompt), gateway,
catch: (cause) => TextCompletionError.make({message: toErrorMessage(cause)}), config,
}).pipe( "text-completion",
Effect.map((text) => TextCompletionSuccess.make({text})), {system, prompt},
(cause) => TextCompletionError.make({message: toErrorMessage(cause)}),
{timeoutMs: 30_000},
).pipe(
Effect.map((response) => TextCompletionSuccess.make({text: stringProperty(response, "response") ?? ""})),
), ),
graph_rag: ({query, entity_limit, triple_limit, collection}) => graph_rag: ({query, entity_limit, triple_limit, collection}) =>
Effect.tryPromise({ dispatchFlow(
try: () => gateway,
socket.flow(config.flowId).graphRag( config,
query, "graph-rag",
{ {
...(entity_limit !== undefined ? {entityLimit: entity_limit} : {}), query,
...(triple_limit !== undefined ? {tripleLimit: triple_limit} : {}), user: config.user,
}, collection: collection ?? "default",
collection, ...(entity_limit !== undefined ? {"entity-limit": entity_limit} : {}),
), ...(triple_limit !== undefined ? {"triple-limit": triple_limit} : {}),
catch: (cause) => GraphRagError.make({message: toErrorMessage(cause)}), },
}).pipe( (cause) => GraphRagError.make({message: toErrorMessage(cause)}),
Effect.map((text) => GraphRagSuccess.make({text})), {timeoutMs: 60_000},
).pipe(
Effect.map((response) => GraphRagSuccess.make({text: stringProperty(response, "response") ?? ""})),
), ),
document_rag: ({query, doc_limit, collection}) => document_rag: ({query, doc_limit, collection}) =>
Effect.tryPromise({ dispatchFlow(
try: () => socket.flow(config.flowId).documentRag(query, doc_limit, collection), gateway,
catch: (cause) => DocumentRagError.make({message: toErrorMessage(cause)}), config,
}).pipe( "document-rag",
Effect.map((text) => DocumentRagSuccess.make({text})), {
query,
user: config.user,
collection: collection ?? "default",
...(doc_limit !== undefined ? {"doc-limit": doc_limit} : {}),
},
(cause) => DocumentRagError.make({message: toErrorMessage(cause)}),
{timeoutMs: 60_000},
).pipe(
Effect.map((response) => DocumentRagSuccess.make({text: stringProperty(response, "response") ?? ""})),
), ),
agent: ({question}) => runAgentTool(gateway, config, question), agent: ({question}) => runAgentTool(gateway, config, question),
embeddings: ({text}) => embeddings: ({text}) =>
Effect.tryPromise({ dispatchFlow(
try: () => socket.flow(config.flowId).embeddings([...text]), gateway,
catch: (cause) => EmbeddingsError.make({message: toErrorMessage(cause)}), config,
}).pipe( "embeddings",
Effect.map((vectors) => EmbeddingsSuccess.make({vectors})), {texts: [...text]},
(cause) => EmbeddingsError.make({message: toErrorMessage(cause)}),
{timeoutMs: 30_000},
).pipe(
Effect.map((response) => EmbeddingsSuccess.make({
vectors: Array.isArray(response.vectors) ? response.vectors as number[][] : [],
})),
), ),
triples_query: ({s, p, o, limit, collection}) => triples_query: ({s, p, o, limit, collection}) =>
Effect.tryPromise({ dispatchFlow(
try: () => gateway,
socket.flow(config.flowId).triplesQuery( config,
asIriTerm(s), "triples",
asIriTerm(p), {
asIriTerm(o), limit: limit ?? 20,
limit, user: config.user,
collection, collection: collection ?? "default",
), ...(asIriTerm(s) !== undefined ? {s: asIriTerm(s)} : {}),
catch: (cause) => TriplesQueryError.make({message: toErrorMessage(cause)}), ...(asIriTerm(p) !== undefined ? {p: asIriTerm(p)} : {}),
}).pipe( ...(asIriTerm(o) !== undefined ? {o: asIriTerm(o)} : {}),
Effect.map((triples) => TriplesQuerySuccess.make({triples})), },
(cause) => TriplesQueryError.make({message: toErrorMessage(cause)}),
{timeoutMs: 30_000},
).pipe(
Effect.map((response) => TriplesQuerySuccess.make({
triples: Array.isArray(response.triples ?? response.response)
? (response.triples ?? response.response) as ClientTriple[]
: [],
})),
), ),
graph_embeddings_query: ({query, limit, collection}) => graph_embeddings_query: ({query, limit, collection}) =>
Effect.tryPromise({ dispatchFlow(
try: () => socket.flow(config.flowId).embeddings([query]), gateway,
catch: (cause) => GraphEmbeddingsQueryError.make({message: toErrorMessage(cause)}), config,
}).pipe( "embeddings",
Effect.flatMap((vectors) => {texts: [query]},
Effect.tryPromise({ (cause) => GraphEmbeddingsQueryError.make({message: toErrorMessage(cause)}),
try: () => socket.flow(config.flowId).graphEmbeddingsQuery( {timeoutMs: 30_000},
vectors[0] ?? [], ).pipe(
limit ?? 10, Effect.flatMap((embeddingResponse) => {
collection, const vectors = Array.isArray(embeddingResponse.vectors) ? embeddingResponse.vectors : []
), const firstVector = Array.isArray(vectors[0]) ? vectors[0] as number[] : []
catch: (cause) => GraphEmbeddingsQueryError.make({message: toErrorMessage(cause)}), return dispatchFlow(
}) gateway,
), config,
Effect.map((entities) => GraphEmbeddingsQuerySuccess.make({entities})), "graph-embeddings",
{
vector: firstVector,
limit: limit ?? 10,
user: config.user,
collection: collection ?? "default",
},
(cause) => GraphEmbeddingsQueryError.make({message: toErrorMessage(cause)}),
{timeoutMs: 30_000},
)
}),
Effect.map((response) => GraphEmbeddingsQuerySuccess.make({
entities: Array.isArray(response.entities) ? response.entities as ClientEntityMatch[] : [],
})),
), ),
get_config_all: () => get_config_all: () =>
Effect.tryPromise({ dispatchGlobal(
try: () => socket.config().getConfigAll(), gateway,
catch: (cause) => GetConfigAllError.make({message: toErrorMessage(cause)}), "config",
}).pipe( {operation: "config"},
(cause) => GetConfigAllError.make({message: toErrorMessage(cause)}),
{timeoutMs: 60_000},
).pipe(
Effect.flatMap((value) => Effect.flatMap((value) =>
decodeJsonOrFail( decodeJsonOrFail(
value, value,
@ -1506,10 +1564,13 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
), ),
get_config: ({keys}) => get_config: ({keys}) =>
Effect.tryPromise({ dispatchGlobal(
try: () => socket.config().getConfig(keys.map(({type, key}) => ({type, key}))), gateway,
catch: (cause) => GetConfigError.make({message: toErrorMessage(cause)}), "config",
}).pipe( {operation: "get", keys: keys.map(({type, key}) => ({type, key}))},
(cause) => GetConfigError.make({message: toErrorMessage(cause)}),
{timeoutMs: 60_000},
).pipe(
Effect.flatMap((value) => Effect.flatMap((value) =>
decodeJsonOrFail( decodeJsonOrFail(
value, value,
@ -1521,10 +1582,13 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
), ),
put_config: ({values}) => put_config: ({values}) =>
Effect.tryPromise({ dispatchGlobal(
try: () => socket.config().putConfig(values.map(({type, key, value}) => ({type, key, value}))), gateway,
catch: (cause) => PutConfigError.make({message: toErrorMessage(cause)}), "config",
}).pipe( {operation: "put", values: values.map(({type, key, value}) => ({type, key, value}))},
(cause) => PutConfigError.make({message: toErrorMessage(cause)}),
{timeoutMs: 60_000},
).pipe(
Effect.flatMap((value) => Effect.flatMap((value) =>
decodeJsonOrFail( decodeJsonOrFail(
value, value,
@ -1536,10 +1600,13 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
), ),
delete_config: ({type, key}) => delete_config: ({type, key}) =>
Effect.tryPromise({ dispatchGlobal(
try: () => socket.config().deleteConfig({type, key}), gateway,
catch: (cause) => DeleteConfigError.make({message: toErrorMessage(cause)}), "config",
}).pipe( {operation: "delete", keys: [{type, key}]},
(cause) => DeleteConfigError.make({message: toErrorMessage(cause)}),
{timeoutMs: 30_000},
).pipe(
Effect.flatMap((value) => Effect.flatMap((value) =>
decodeJsonOrFail( decodeJsonOrFail(
value, value,
@ -1551,21 +1618,29 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
), ),
get_flows: () => get_flows: () =>
Effect.tryPromise({ dispatchGlobal(
try: () => socket.flows().getFlows(), gateway,
catch: (cause) => GetFlowsError.make({message: toErrorMessage(cause)}), "flow",
}).pipe( {operation: "list-flows"},
Effect.map((flow_ids) => GetFlowsSuccess.make({flow_ids})), (cause) => GetFlowsError.make({message: toErrorMessage(cause)}),
{timeoutMs: 60_000},
).pipe(
Effect.map((response) => GetFlowsSuccess.make({
flow_ids: Array.isArray(response["flow-ids"]) ? response["flow-ids"] as string[] : [],
})),
), ),
get_flow: ({flow_id}) => get_flow: ({flow_id}) =>
Effect.tryPromise({ dispatchGlobal(
try: () => socket.flows().getFlow(flow_id), gateway,
catch: (cause) => GetFlowError.make({message: toErrorMessage(cause)}), "flow",
}).pipe( {operation: "get-flow", "flow-id": flow_id},
Effect.flatMap((value) => (cause) => GetFlowError.make({message: toErrorMessage(cause)}),
{timeoutMs: 60_000},
).pipe(
Effect.flatMap((response) =>
decodeJsonOrFail( decodeJsonOrFail(
value, response.flow,
(cause) => GetFlowError.make({message: toErrorMessage(cause)}), (cause) => GetFlowError.make({message: toErrorMessage(cause)}),
).pipe( ).pipe(
Effect.map((flow) => GetFlowSuccess.make({flow})), Effect.map((flow) => GetFlowSuccess.make({flow})),
@ -1574,16 +1649,19 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
), ),
start_flow: ({flow_id, blueprint_name, description, parameters}) => start_flow: ({flow_id, blueprint_name, description, parameters}) =>
Effect.tryPromise({ dispatchGlobal(
try: () => gateway,
socket.flows().startFlow( "flow",
flow_id, {
blueprint_name, operation: "start-flow",
description, "flow-id": flow_id,
parameters === undefined ? undefined : {...parameters}, "blueprint-name": blueprint_name,
), description,
catch: (cause) => StartFlowError.make({message: toErrorMessage(cause)}), ...(parameters === undefined ? {} : {parameters: {...parameters}}),
}).pipe( },
(cause) => StartFlowError.make({message: toErrorMessage(cause)}),
{timeoutMs: 30_000},
).pipe(
Effect.flatMap((value) => Effect.flatMap((value) =>
decodeJsonOrFail( decodeJsonOrFail(
value, value,
@ -1595,10 +1673,13 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
), ),
stop_flow: ({flow_id}) => stop_flow: ({flow_id}) =>
Effect.tryPromise({ dispatchGlobal(
try: () => socket.flows().stopFlow(flow_id), gateway,
catch: (cause) => StopFlowError.make({message: toErrorMessage(cause)}), "flow",
}).pipe( {operation: "stop-flow", "flow-id": flow_id},
(cause) => StopFlowError.make({message: toErrorMessage(cause)}),
{timeoutMs: 30_000},
).pipe(
Effect.flatMap((value) => Effect.flatMap((value) =>
decodeJsonOrFail( decodeJsonOrFail(
value, value,
@ -1610,13 +1691,16 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
), ),
get_documents: () => get_documents: () =>
Effect.tryPromise({ dispatchGlobal(
try: () => socket.librarian().getDocuments(), gateway,
catch: (cause) => GetDocumentsError.make({message: toErrorMessage(cause)}), "librarian",
}).pipe( {operation: "list-documents", user: config.user},
(cause) => GetDocumentsError.make({message: toErrorMessage(cause)}),
{timeoutMs: 60_000},
).pipe(
Effect.flatMap((value) => Effect.flatMap((value) =>
decodeJsonArrayOrFail( decodeJsonArrayOrFail(
value, value["document-metadatas"] ?? value.documents ?? [],
(cause) => GetDocumentsError.make({message: toErrorMessage(cause)}), (cause) => GetDocumentsError.make({message: toErrorMessage(cause)}),
).pipe( ).pipe(
Effect.map((documents) => GetDocumentsSuccess.make({documents})), Effect.map((documents) => GetDocumentsSuccess.make({documents})),
@ -1624,34 +1708,53 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
), ),
), ),
load_document: ({document, mime_type, title, comments, tags, id}) => load_document: Effect.fn("TrustGraphMcpToolkit.load_document")(function*({document, mime_type, title, comments, tags, id}) {
Effect.tryPromise({ const timestamp = yield* Clock.currentTimeMillis
try: () => const metadata = {
socket.librarian().loadDocument( time: Math.floor(timestamp / 1000),
document, kind: mime_type,
mime_type, title,
title, comments: comments ?? "",
comments ?? "", user: config.user,
tags === undefined ? [] : [...tags], tags: tags === undefined ? [] : [...tags],
id, "document-type": "source",
), documentType: "source",
catch: (cause) => LoadDocumentError.make({message: toErrorMessage(cause)}), ...(id === undefined ? {} : {id}),
}).pipe( }
Effect.flatMap((value) => const value = yield* dispatchGlobal(
decodeJsonOrFail( gateway,
value, "librarian",
(cause) => LoadDocumentError.make({message: toErrorMessage(cause)}), {
).pipe( operation: "add-document",
Effect.map((response) => LoadDocumentSuccess.make({response})), "document-metadata": metadata,
) documentMetadata: metadata,
), content: document,
), },
(cause) => LoadDocumentError.make({message: toErrorMessage(cause)}),
{timeoutMs: 30_000},
)
return yield* decodeJsonOrFail(
value,
(cause) => LoadDocumentError.make({message: toErrorMessage(cause)}),
).pipe(
Effect.map((response) => LoadDocumentSuccess.make({response})),
)
}),
remove_document: ({id, collection}) => remove_document: ({id, collection}) =>
Effect.tryPromise({ dispatchGlobal(
try: () => socket.librarian().removeDocument(id, collection), gateway,
catch: (cause) => RemoveDocumentError.make({message: toErrorMessage(cause)}), "librarian",
}).pipe( {
operation: "remove-document",
"document-id": id,
documentId: id,
user: config.user,
collection: collection ?? "default",
},
(cause) => RemoveDocumentError.make({message: toErrorMessage(cause)}),
{timeoutMs: 30_000},
).pipe(
Effect.flatMap((value) => Effect.flatMap((value) =>
decodeJsonOrFail( decodeJsonOrFail(
value, value,
@ -1663,21 +1766,34 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
), ),
get_prompts: () => get_prompts: () =>
Effect.tryPromise({ dispatchGlobal(
try: () => socket.config().getPrompts(), gateway,
catch: (cause) => GetPromptsError.make({message: toErrorMessage(cause)}), "config",
}).pipe( {operation: "config"},
Effect.map((prompts) => GetPromptsSuccess.make({prompts})), (cause) => GetPromptsError.make({message: toErrorMessage(cause)}),
{timeoutMs: 60_000},
).pipe(
Effect.map((response) => {
const promptNs = asRecord(asRecord(response.config).prompt)
const prompts = Object.keys(promptNs)
.filter((key) => key !== "system")
.sort()
.map((id) => ({id, name: id}))
return GetPromptsSuccess.make({prompts})
}),
), ),
get_prompt: ({id}) => get_prompt: ({id}) =>
Effect.tryPromise({ dispatchGlobal(
try: () => socket.config().getPrompt(id), gateway,
catch: (cause) => GetPromptError.make({message: toErrorMessage(cause)}), "config",
}).pipe( {operation: "config"},
Effect.flatMap((value) => (cause) => GetPromptError.make({message: toErrorMessage(cause)}),
{timeoutMs: 60_000},
).pipe(
Effect.flatMap((response) =>
decodeJsonOrFail( decodeJsonOrFail(
value, asRecord(asRecord(response.config).prompt)[id] ?? null,
(cause) => GetPromptError.make({message: toErrorMessage(cause)}), (cause) => GetPromptError.make({message: toErrorMessage(cause)}),
).pipe( ).pipe(
Effect.map((prompt) => GetPromptSuccess.make({prompt})), Effect.map((prompt) => GetPromptSuccess.make({prompt})),
@ -1686,18 +1802,31 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
), ),
get_knowledge_cores: () => get_knowledge_cores: () =>
Effect.tryPromise({ dispatchGlobal(
try: () => socket.knowledge().getKnowledgeCores(), gateway,
catch: (cause) => GetKnowledgeCoresError.make({message: toErrorMessage(cause)}), "knowledge",
}).pipe( {operation: "list-kg-cores", user: config.user},
Effect.map((ids) => GetKnowledgeCoresSuccess.make({ids})), (cause) => GetKnowledgeCoresError.make({message: toErrorMessage(cause)}),
{timeoutMs: 60_000},
).pipe(
Effect.map((response) => GetKnowledgeCoresSuccess.make({
ids: Array.isArray(response.ids) ? response.ids as string[] : [],
})),
), ),
delete_kg_core: ({id, collection}) => delete_kg_core: ({id, collection}) =>
Effect.tryPromise({ dispatchGlobal(
try: () => socket.knowledge().deleteKgCore(id, collection), gateway,
catch: (cause) => DeleteKgCoreError.make({message: toErrorMessage(cause)}), "knowledge",
}).pipe( {
operation: "delete-kg-core",
id,
user: config.user,
collection: collection ?? "default",
},
(cause) => DeleteKgCoreError.make({message: toErrorMessage(cause)}),
{timeoutMs: 30_000},
).pipe(
Effect.flatMap((value) => Effect.flatMap((value) =>
decodeJsonOrFail( decodeJsonOrFail(
value, value,
@ -1709,10 +1838,19 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
), ),
load_kg_core: ({id, flow, collection}) => load_kg_core: ({id, flow, collection}) =>
Effect.tryPromise({ dispatchGlobal(
try: () => socket.knowledge().loadKgCore(id, flow, collection), gateway,
catch: (cause) => LoadKgCoreError.make({message: toErrorMessage(cause)}), "knowledge",
}).pipe( {
operation: "load-kg-core",
id,
flow,
user: config.user,
collection: collection ?? "default",
},
(cause) => LoadKgCoreError.make({message: toErrorMessage(cause)}),
{timeoutMs: 30_000},
).pipe(
Effect.flatMap((value) => Effect.flatMap((value) =>
decodeJsonOrFail( decodeJsonOrFail(
value, value,
@ -1773,7 +1911,6 @@ const makeTrustGraphMcpHttpLayerFromConfig = (
path: config.mcpPath, path: config.mcpPath,
})), })),
Layer.provide(TrustGraphGateway.layer), Layer.provide(TrustGraphGateway.layer),
Layer.provide(TrustGraphSocket.layer),
Layer.provide(Layer.succeed(TrustGraphMcpConfig, TrustGraphMcpConfig.of(config))), Layer.provide(Layer.succeed(TrustGraphMcpConfig, TrustGraphMcpConfig.of(config))),
) )
} }
@ -1793,7 +1930,6 @@ const makeTrustGraphMcpStdioLayerFromConfig = (
})), })),
Layer.provide(NodeStdio.layer), Layer.provide(NodeStdio.layer),
Layer.provide(TrustGraphGateway.layer), Layer.provide(TrustGraphGateway.layer),
Layer.provide(TrustGraphSocket.layer),
Layer.provide(Layer.succeed(TrustGraphMcpConfig, TrustGraphMcpConfig.of(config))), Layer.provide(Layer.succeed(TrustGraphMcpConfig, TrustGraphMcpConfig.of(config))),
) )

View file

@ -3,7 +3,6 @@ import * as BrowserHttpClient from "@effect/platform-browser/BrowserHttpClient";
import * as BrowserKeyValueStore from "@effect/platform-browser/BrowserKeyValueStore"; import * as BrowserKeyValueStore from "@effect/platform-browser/BrowserKeyValueStore";
import type { import type {
GraphRagOptions, GraphRagOptions,
BaseApi,
BeginUploadResponse, BeginUploadResponse,
ChunkedUploadDocumentMetadata, ChunkedUploadDocumentMetadata,
CompleteUploadResponse, CompleteUploadResponse,
@ -18,7 +17,6 @@ import type {
import { import {
DispatchPayload, DispatchPayload,
GatewayWorkbenchHttpApi, GatewayWorkbenchHttpApi,
makeBaseApi,
TrustGraphRpcs, TrustGraphRpcs,
} from "@trustgraph/client"; } from "@trustgraph/client";
import type { Scope, } from "effect"; import type { Scope, } from "effect";
@ -204,10 +202,6 @@ export class Settings extends S.Class<Settings>("Settings")({
featureSwitches: FeatureSwitches, featureSwitches: FeatureSwitches,
}, { description: "Persisted workbench connection and display settings." }) {} }, { description: "Persisted workbench connection and display settings." }) {}
export interface WorkbenchApiFactory {
readonly create: (settings: Settings) => BaseApi;
}
export type Theme = "dark" | "light"; export type Theme = "dark" | "light";
export type ChatMode = "graph-rag" | "document-rag" | "agent"; export type ChatMode = "graph-rag" | "document-rag" | "agent";
@ -764,7 +758,11 @@ function explainTriplesFrom(source: unknown): Triple[] | undefined {
} }
function streamingMetadataFrom(source: unknown): StreamingMetadata | undefined { function streamingMetadataFrom(source: unknown): StreamingMetadata | undefined {
const metadata: StreamingMetadata = {}; const metadata: {
in_token?: number;
out_token?: number;
model?: string;
} = {};
let hasMetadata = false; let hasMetadata = false;
const inToken = numberProperty(source, "in_token"); const inToken = numberProperty(source, "in_token");
@ -804,9 +802,9 @@ function ensureNoGatewayResponseError<A>(operation: string, value: A): Effect.Ef
: Effect.fail(WorkbenchPromiseError.make({ cause: value, message: `${operation}: ${message}` })); : Effect.fail(WorkbenchPromiseError.make({ cause: value, message: `${operation}: ${message}` }));
} }
function qaBaseApi(): BaseApi | undefined { function qaBaseApi(): import("@trustgraph/client").BaseApi | undefined {
if (typeof window === "undefined") return undefined; if (typeof window === "undefined") return undefined;
return (window as Window & { __TRUSTGRAPH_WORKBENCH_QA_API__?: BaseApi }).__TRUSTGRAPH_WORKBENCH_QA_API__; return (window as Window & { __TRUSTGRAPH_WORKBENCH_QA_API__?: import("@trustgraph/client").BaseApi }).__TRUSTGRAPH_WORKBENCH_QA_API__;
} }
function makeWorkbenchGatewayApi(settings: Settings) { function makeWorkbenchGatewayApi(settings: Settings) {
@ -1016,9 +1014,9 @@ function makeWorkbenchGatewayApi(settings: Settings) {
tags, tags,
"document-type": "source", "document-type": "source",
documentType: "source", documentType: "source",
...(id !== undefined ? { id } : {}),
...(metadata !== undefined ? { metadata } : {}),
}; };
if (id !== undefined) documentMetadata.id = id;
if (metadata !== undefined) documentMetadata.metadata = metadata;
return yield* dispatch("librarian", { return yield* dispatch("librarian", {
operation: "add-document", operation: "add-document",
"document-metadata": documentMetadata, "document-metadata": documentMetadata,
@ -1184,10 +1182,8 @@ function makeWorkbenchGatewayApi(settings: Settings) {
const event: ExplainEvent = { const event: ExplainEvent = {
explainId: explainId ?? "", explainId: explainId ?? "",
explainGraph: stringProperty(resp, "explain_graph") ?? "", explainGraph: stringProperty(resp, "explain_graph") ?? "",
...(explainTriples !== undefined ? { explainTriples } : {}),
}; };
if (explainTriples !== undefined) {
event.explainTriples = explainTriples;
}
onExplain?.(event); onExplain?.(event);
if ( if (
stringProperty(resp, "response") === undefined && stringProperty(resp, "response") === undefined &&
@ -1305,10 +1301,8 @@ function makeWorkbenchGatewayApi(settings: Settings) {
const event: ExplainEvent = { const event: ExplainEvent = {
explainId: explainId ?? "", explainId: explainId ?? "",
explainGraph: stringProperty(resp, "explain_graph") ?? "", explainGraph: stringProperty(resp, "explain_graph") ?? "",
...(explainTriples !== undefined ? { explainTriples } : {}),
}; };
if (explainTriples !== undefined) {
event.explainTriples = explainTriples;
}
onExplain?.(event); onExplain?.(event);
return false; return false;
} }
@ -1621,34 +1615,14 @@ export const toggleThemeAtom = Atom.writable(
// Socket lifecycle // Socket lifecycle
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const liveApiFactory: WorkbenchApiFactory = {
create: (settings) =>
makeBaseApi(
settings.user,
settings.apiKey.length > 0 ? settings.apiKey : undefined,
settings.gatewayUrl.length > 0 ? settings.gatewayUrl : undefined,
),
};
export const apiFactoryAtom = Atom.make<WorkbenchApiFactory>(liveApiFactory).pipe(Atom.keepAlive);
export const apiAtom = Atom.make((get) => {
const settings = get(settingsAtom);
const api = get(apiFactoryAtom).create(settings);
get.addFinalizer(() => api.close());
return api;
}).pipe(Atom.keepAlive);
export const connectionStateAtom = Atom.make((get) => { export const connectionStateAtom = Atom.make((get) => {
const api = get(apiAtom); const settings = get(settingsAtom);
const fallback: ConnectionState = { const hasApiKey = settings.apiKey.length > 0;
status: "connecting", const state: ConnectionState = {
hasApiKey: get(settingsAtom).apiKey.length > 0, status: hasApiKey ? "authenticated" : "unauthenticated",
hasApiKey,
}; };
const previous = Option.getOrElse(get.self<ConnectionState>(), () => fallback); return state;
const unsubscribe = api.onConnectionStateChange((state) => get.setSelf(state));
get.addFinalizer(unsubscribe);
return previous;
}).pipe(Atom.keepAlive); }).pipe(Atom.keepAlive);
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View file

@ -2,10 +2,8 @@ import type * as Atom from "effect/unstable/reactivity/Atom";
import type { import type {
FeatureSwitches, FeatureSwitches,
Settings, Settings,
WorkbenchApiFactory,
} from "@/atoms/workbench"; } from "@/atoms/workbench";
import { import {
apiFactoryAtom,
DEFAULT_SETTINGS, DEFAULT_SETTINGS,
flowIdAtom, flowIdAtom,
settingsAtom, settingsAtom,
@ -45,12 +43,8 @@ export function getWorkbenchQaInitialValues(): Iterable<readonly [Atom.Atom<unkn
if (config?.enabled !== true) return undefined; if (config?.enabled !== true) return undefined;
const fixture = config.fixture ?? {}; const fixture = config.fixture ?? {};
const api = makeMockBaseApi(fixture); const api = makeMockBaseApi(fixture);
const apiFactory: WorkbenchApiFactory = {
create: () => api,
};
window.__TRUSTGRAPH_WORKBENCH_QA_API__ = api; window.__TRUSTGRAPH_WORKBENCH_QA_API__ = api;
return [ return [
[apiFactoryAtom as Atom.Atom<unknown>, apiFactory],
[settingsAtom as Atom.Atom<unknown>, qaSettings(fixture)], [settingsAtom as Atom.Atom<unknown>, qaSettings(fixture)],
[flowIdAtom as Atom.Atom<unknown>, config.flowId ?? "default"], [flowIdAtom as Atom.Atom<unknown>, config.flowId ?? "default"],
]; ];

View file

@ -1 +1 @@
{"exemptions":[],"baseline":[{"rule":"no-effect-run","path":"packages/client/src/socket/effect-rpc-client.ts","count":10},{"rule":"no-effect-run","path":"packages/client/src/socket/trustgraph-socket.ts","count":9},{"rule":"no-error-throw","path":"packages/workbench/src/main.tsx","count":1},{"rule":"no-error-throw","path":"scripts/seed-config.ts","count":1},{"rule":"no-error-throw","path":"scripts/seed-demo.ts","count":4},{"rule":"no-error-throw","path":"scripts/seed-flows.ts","count":2},{"rule":"no-error-throw","path":"scripts/test-pipeline.ts","count":2},{"rule":"no-native-fetch","path":"scripts/seed-config.ts","count":1},{"rule":"no-native-fetch","path":"scripts/seed-demo.ts","count":11},{"rule":"no-native-fetch","path":"scripts/seed-flows.ts","count":1},{"rule":"no-native-fetch","path":"scripts/test-pipeline.ts","count":5},{"rule":"no-native-json","path":"scripts/seed-config.ts","count":6},{"rule":"no-native-json","path":"scripts/seed-demo.ts","count":6},{"rule":"no-native-json","path":"scripts/seed-flows.ts","count":3},{"rule":"no-native-json","path":"scripts/test-pipeline.ts","count":6},{"rule":"no-native-sort","path":"packages/client/src/socket/trustgraph-socket.ts","count":2},{"rule":"no-native-sort","path":"packages/flow/src/config/service.ts","count":3},{"rule":"no-native-sort","path":"packages/flow/src/cores/service.ts","count":1},{"rule":"no-native-sort","path":"packages/flow/src/flow-manager/service.ts","count":1},{"rule":"no-native-sort","path":"packages/flow/src/librarian/service.ts","count":1},{"rule":"no-native-sort","path":"packages/flow/src/retrieval/graph-rag.ts","count":1},{"rule":"no-native-sort","path":"packages/workbench/src/atoms/workbench.ts","count":2},{"rule":"no-native-sort","path":"packages/workbench/src/components/chat/explain-graph.tsx","count":1},{"rule":"no-native-sort","path":"packages/workbench/src/pages/graph.tsx","count":1},{"rule":"no-native-sort","path":"packages/workbench/src/qa/mock-api.ts","count":1},{"rule":"no-native-sort","path":"scripts/inventory-native-classes.ts","count":1},{"rule":"no-native-sort","path":"scripts/seed-demo.ts","count":1},{"rule":"no-native-sort","path":"scripts/seed-flows.ts","count":1},{"rule":"no-native-timers","path":"scripts/test-pipeline.ts","count":2},{"rule":"no-node-fs-path","path":"scripts/create-test-pdf.ts","count":1},{"rule":"no-node-fs-path","path":"scripts/inventory-native-classes.ts","count":2},{"rule":"no-process-env","path":"scripts/seed-config.ts","count":2},{"rule":"no-process-env","path":"scripts/seed-demo.ts","count":5},{"rule":"no-process-env","path":"scripts/seed-flows.ts","count":1},{"rule":"no-process-env","path":"scripts/test-pipeline.ts","count":11},{"rule":"no-schema-suffix","path":"packages/base/src/schema/primitives.ts","count":1},{"rule":"schema-first-data","path":"packages/client/src/models/Triple.ts","count":6},{"rule":"schema-first-data","path":"packages/client/src/models/messages.ts","count":58},{"rule":"schema-first-data","path":"packages/client/src/socket/effect-rpc-client.ts","count":2},{"rule":"schema-first-data","path":"packages/client/src/socket/trustgraph-socket.ts","count":4}]} {"exemptions":[],"baseline":[{"rule":"no-error-throw","path":"packages/workbench/src/main.tsx","count":1},{"rule":"no-error-throw","path":"scripts/seed-config.ts","count":1},{"rule":"no-error-throw","path":"scripts/seed-demo.ts","count":4},{"rule":"no-error-throw","path":"scripts/seed-flows.ts","count":2},{"rule":"no-error-throw","path":"scripts/test-pipeline.ts","count":2},{"rule":"no-native-fetch","path":"scripts/seed-config.ts","count":1},{"rule":"no-native-fetch","path":"scripts/seed-demo.ts","count":11},{"rule":"no-native-fetch","path":"scripts/seed-flows.ts","count":1},{"rule":"no-native-fetch","path":"scripts/test-pipeline.ts","count":5},{"rule":"no-native-json","path":"scripts/seed-config.ts","count":6},{"rule":"no-native-json","path":"scripts/seed-demo.ts","count":6},{"rule":"no-native-json","path":"scripts/seed-flows.ts","count":3},{"rule":"no-native-json","path":"scripts/test-pipeline.ts","count":6},{"rule":"no-native-sort","path":"packages/client/src/socket/trustgraph-socket.ts","count":2},{"rule":"no-native-sort","path":"packages/flow/src/config/service.ts","count":3},{"rule":"no-native-sort","path":"packages/flow/src/cores/service.ts","count":1},{"rule":"no-native-sort","path":"packages/flow/src/flow-manager/service.ts","count":1},{"rule":"no-native-sort","path":"packages/flow/src/librarian/service.ts","count":1},{"rule":"no-native-sort","path":"packages/flow/src/retrieval/graph-rag.ts","count":1},{"rule":"no-native-sort","path":"packages/mcp/src/server-effect.ts","count":1},{"rule":"no-native-sort","path":"packages/workbench/src/atoms/workbench.ts","count":2},{"rule":"no-native-sort","path":"packages/workbench/src/components/chat/explain-graph.tsx","count":1},{"rule":"no-native-sort","path":"packages/workbench/src/pages/graph.tsx","count":1},{"rule":"no-native-sort","path":"packages/workbench/src/qa/mock-api.ts","count":1},{"rule":"no-native-sort","path":"scripts/inventory-native-classes.ts","count":1},{"rule":"no-native-sort","path":"scripts/seed-demo.ts","count":1},{"rule":"no-native-sort","path":"scripts/seed-flows.ts","count":1},{"rule":"no-native-timers","path":"scripts/test-pipeline.ts","count":2},{"rule":"no-node-fs-path","path":"scripts/create-test-pdf.ts","count":1},{"rule":"no-node-fs-path","path":"scripts/inventory-native-classes.ts","count":2},{"rule":"no-process-env","path":"scripts/seed-config.ts","count":2},{"rule":"no-process-env","path":"scripts/seed-demo.ts","count":5},{"rule":"no-process-env","path":"scripts/seed-flows.ts","count":1},{"rule":"no-process-env","path":"scripts/test-pipeline.ts","count":11},{"rule":"no-schema-suffix","path":"packages/base/src/schema/primitives.ts","count":1}]}