mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-01 09:29:38 +02:00
refactor(ts): complete legacy host removal — drop fastify/commander/zod, delete MCP SDK server, remove ManagedRuntime facades
Finishes the remaining EFFECT_NATIVE_REWRITE_PLAN stages in one verified slice: - fastify, @fastify/websocket, commander, zod removed from all package manifests - legacy @modelcontextprotocol/sdk stdio server deleted; effect/unstable/ai McpServer is canonical - no ManagedRuntime or Effect.runPromise program facades remain in production source - gateway server/rpc-contract and client rpc/socket moved onto Effect v4 native http/rpc/socket layers Gates (force-run, no cache): check:tsgo, build, test (96 tests / 11 tasks) all green. Native-class inventory: zero blocking production classes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a26463afc1
commit
cf12defcd8
30 changed files with 1506 additions and 456 deletions
66
ts/bun.lock
66
ts/bun.lock
|
|
@ -27,14 +27,6 @@
|
|||
"name": "@trustgraph/base",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@effect/ai-anthropic": "4.0.0-beta.78",
|
||||
"@effect/ai-openai": "4.0.0-beta.78",
|
||||
"@effect/ai-openrouter": "4.0.0-beta.78",
|
||||
"@effect/atom-react": "4.0.0-beta.78",
|
||||
"@effect/openapi-generator": "4.0.0-beta.78",
|
||||
"@effect/opentelemetry": "4.0.0-beta.78",
|
||||
"@effect/platform-browser": "4.0.0-beta.78",
|
||||
"@effect/platform-bun": "4.0.0-beta.78",
|
||||
"effect": "4.0.0-beta.78",
|
||||
"nats": "^2.29.0",
|
||||
},
|
||||
|
|
@ -52,16 +44,10 @@
|
|||
"tg": "dist/index.js",
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/ai-anthropic": "4.0.0-beta.78",
|
||||
"@effect/ai-openai": "4.0.0-beta.78",
|
||||
"@effect/ai-openrouter": "4.0.0-beta.78",
|
||||
"@effect/atom-react": "4.0.0-beta.78",
|
||||
"@effect/openapi-generator": "4.0.0-beta.78",
|
||||
"@effect/opentelemetry": "4.0.0-beta.78",
|
||||
"@effect/platform-browser": "4.0.0-beta.78",
|
||||
"@effect/platform-bun": "4.0.0-beta.78",
|
||||
"@trustgraph/base": "workspace:*",
|
||||
"@trustgraph/client": "workspace:*",
|
||||
"effect": "4.0.0-beta.78",
|
||||
"ws": "^8.18.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -97,21 +83,13 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@effect/ai-anthropic": "4.0.0-beta.78",
|
||||
"@effect/ai-openai": "4.0.0-beta.78",
|
||||
"@effect/ai-openrouter": "4.0.0-beta.78",
|
||||
"@effect/atom-react": "4.0.0-beta.78",
|
||||
"@effect/openapi-generator": "4.0.0-beta.78",
|
||||
"@effect/opentelemetry": "4.0.0-beta.78",
|
||||
"@effect/platform-browser": "4.0.0-beta.78",
|
||||
"@effect/platform-bun": "4.0.0-beta.78",
|
||||
"@effect/platform-node": "4.0.0-beta.78",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.78",
|
||||
"@effect/tsgo": "0.14.0",
|
||||
"@effect/vitest": "4.0.0-beta.78",
|
||||
"@mistralai/mistralai": "^1.0.0",
|
||||
"@modelcontextprotocol/sdk": "^1.12.0",
|
||||
"@qdrant/js-client-rest": "^1.13.0",
|
||||
"@trustgraph/base": "workspace:*",
|
||||
"@trustgraph/client": "workspace:*",
|
||||
"effect": "4.0.0-beta.78",
|
||||
"falkordb": "^5.0.0",
|
||||
"ollama": "^0.6.3",
|
||||
|
|
@ -129,20 +107,11 @@
|
|||
"name": "@trustgraph/mcp",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@effect/ai-anthropic": "4.0.0-beta.78",
|
||||
"@effect/ai-openai": "4.0.0-beta.78",
|
||||
"@effect/ai-openrouter": "4.0.0-beta.78",
|
||||
"@effect/atom-react": "4.0.0-beta.78",
|
||||
"@effect/openapi-generator": "4.0.0-beta.78",
|
||||
"@effect/opentelemetry": "4.0.0-beta.78",
|
||||
"@effect/platform-browser": "4.0.0-beta.78",
|
||||
"@effect/platform-bun": "4.0.0-beta.78",
|
||||
"@effect/platform-node": "4.0.0-beta.78",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.78",
|
||||
"@effect/tsgo": "0.14.0",
|
||||
"@effect/vitest": "4.0.0-beta.78",
|
||||
"@trustgraph/base": "workspace:*",
|
||||
"@trustgraph/client": "workspace:*",
|
||||
"effect": "4.0.0-beta.78",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/vitest": "4.0.0-beta.78",
|
||||
|
|
@ -155,21 +124,11 @@
|
|||
"name": "@trustgraph/workbench",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@effect/ai-anthropic": "4.0.0-beta.78",
|
||||
"@effect/ai-openai": "4.0.0-beta.78",
|
||||
"@effect/ai-openrouter": "4.0.0-beta.78",
|
||||
"@effect/atom-react": "4.0.0-beta.78",
|
||||
"@effect/openapi-generator": "4.0.0-beta.78",
|
||||
"@effect/opentelemetry": "4.0.0-beta.78",
|
||||
"@effect/platform-browser": "4.0.0-beta.78",
|
||||
"@effect/platform-bun": "4.0.0-beta.78",
|
||||
"@effect/platform-node": "4.0.0-beta.78",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.78",
|
||||
"@effect/tsgo": "0.14.0",
|
||||
"@effect/vitest": "4.0.0-beta.78",
|
||||
"@tanstack/react-query": "^5.75.0",
|
||||
"@trustgraph/client": "workspace:*",
|
||||
"clsx": "^2.1.0",
|
||||
"effect": "4.0.0-beta.78",
|
||||
"lucide-react": "^0.513.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
|
|
@ -178,7 +137,6 @@
|
|||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.6.0",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"zustand": "^5.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/vitest": "4.0.0-beta.78",
|
||||
|
|
@ -234,16 +192,8 @@
|
|||
|
||||
"@effect/ai-anthropic": ["@effect/ai-anthropic@4.0.0-beta.78", "", { "peerDependencies": { "effect": "^4.0.0-beta.78" } }, "sha512-FZzwvKx2k+UIDJyv+FWMtC2CKRxJMWTII+z8EmMcT6tw1uzkkn+ouIyJ32AU+lfveKScNV1SXL4ml3VdcYGy/g=="],
|
||||
|
||||
"@effect/ai-openai": ["@effect/ai-openai@4.0.0-beta.78", "", { "peerDependencies": { "effect": "^4.0.0-beta.78" } }, "sha512-tx953rRkLqW2BeEkWK12/nRBPO0b1eS6pI+2YyWI0nQvX2JTTijrGlBv/qDVa5kxDkLm63+tA04xnxgZMlA8NA=="],
|
||||
|
||||
"@effect/ai-openrouter": ["@effect/ai-openrouter@4.0.0-beta.78", "", { "peerDependencies": { "effect": "^4.0.0-beta.78" } }, "sha512-9GIRU9stAnDU5EJ5ZghUWrQXaE+rECCWI/eKVfYeC7UqjZmmmJmTcEbid3tvz2NMsnvIn0ymeKsJAohWCys39w=="],
|
||||
|
||||
"@effect/atom-react": ["@effect/atom-react@4.0.0-beta.78", "", { "peerDependencies": { "effect": "^4.0.0-beta.78", "react": "^19.2.4", "scheduler": "*" } }, "sha512-cgxDXJaD0wlbQXbp6tiEmmY+yajwurB0ynkFG20RVucvH4LsQMB3ogiHe0mt42wGggfbVYMEDxgBpQdqDRY8yA=="],
|
||||
|
||||
"@effect/openapi-generator": ["@effect/openapi-generator@4.0.0-beta.78", "", { "peerDependencies": { "@effect/platform-node": "^4.0.0-beta.78", "effect": "^4.0.0-beta.78" }, "bin": { "openapigen": "dist/bin.js" } }, "sha512-qhKRcZCNQ5b0Klrct+AC/tPQgIDBxVsD0MkQLIzqvLU3qRHaNd5yHo7kxFf/DuhCyyL++xZfbHsPdq3VdLIByg=="],
|
||||
|
||||
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.78", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.78" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-OJGnlNkxfhUmZ/8aLIfQly8ic2tntcnwidAP0BdrTUKa1/sbZjq5xTrhVUjvmehFra2Thsef0k4UPTgsOrBG1A=="],
|
||||
|
||||
"@effect/platform-browser": ["@effect/platform-browser@4.0.0-beta.78", "", { "dependencies": { "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^4.0.0-beta.78" } }, "sha512-8r9MVuZ8xJRyVyi+C8SKSYLbMsHr7qOiUgLV6lKMECuAWyMhlbK/7Ka9SQGr0ZPqOe5ShLEvV7DevnGkG+owAQ=="],
|
||||
|
||||
"@effect/platform-bun": ["@effect/platform-bun@4.0.0-beta.78", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.78" }, "peerDependencies": { "effect": "^4.0.0-beta.78" } }, "sha512-lmPCL1G7SlkCWCguX3rDPS7kKuvJ/AN4pjS7IXb/5SoauHPd67iUdc1ZbB7o6lwTChJaIfWNNPkUWygiaUeJiA=="],
|
||||
|
|
@ -382,8 +332,6 @@
|
|||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="],
|
||||
|
||||
"@pdf-lib/standard-fonts": ["@pdf-lib/standard-fonts@1.0.0", "", { "dependencies": { "pako": "^1.0.6" } }, "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA=="],
|
||||
|
||||
"@pdf-lib/upng": ["@pdf-lib/upng@1.0.1", "", { "dependencies": { "pako": "^1.0.10" } }, "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ=="],
|
||||
|
|
@ -478,10 +426,6 @@
|
|||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.2", "", { "dependencies": { "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "tailwindcss": "4.2.2" }, "peerDependencies": { "vite": "6.4.1" } }, "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w=="],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.96.2", "", {}, "sha512-hzI6cTVh4KNRk8UtoIBS7Lv9g6BnJPXvBKsvYH1aGWvv0347jT3BnSvztOE+kD76XGvZnRC/t6qdW1CaIfwCeA=="],
|
||||
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.96.2", "", { "dependencies": { "@tanstack/query-core": "5.96.2" }, "peerDependencies": { "react": "19.2.4" } }, "sha512-sYyzzJT4G0g02azzJ8o55VFFV31XvFpdUpG+unxS0vSaYsJnSPKGoI6WdPwUucJL1wpgGfwfmntNX/Ub1uOViA=="],
|
||||
|
||||
"@trustgraph/base": ["@trustgraph/base@workspace:packages/base"],
|
||||
|
||||
"@trustgraph/cli": ["@trustgraph/cli@workspace:packages/cli"],
|
||||
|
|
@ -1242,8 +1186,6 @@
|
|||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "3.25.76" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
||||
|
||||
"zustand": ["zustand@5.0.12", "", { "optionalDependencies": { "@types/react": "19.2.14", "react": "19.2.4" } }, "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"@qdrant/js-client-rest/undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="],
|
||||
|
|
|
|||
|
|
@ -20,14 +20,6 @@
|
|||
"test": "bunx --bun vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/ai-anthropic": "4.0.0-beta.78",
|
||||
"@effect/ai-openai": "4.0.0-beta.78",
|
||||
"@effect/ai-openrouter": "4.0.0-beta.78",
|
||||
"@effect/atom-react": "4.0.0-beta.78",
|
||||
"@effect/openapi-generator": "4.0.0-beta.78",
|
||||
"@effect/opentelemetry": "4.0.0-beta.78",
|
||||
"@effect/platform-browser": "4.0.0-beta.78",
|
||||
"@effect/platform-bun": "4.0.0-beta.78",
|
||||
"effect": "4.0.0-beta.78",
|
||||
"nats": "^2.29.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import type { PubSubBackend } from "../backend/types.js";
|
||||
import { makeNatsBackend } from "../backend/nats.js";
|
||||
import { makeNatsBackend, makeNatsBackendScoped } from "../backend/nats.js";
|
||||
import { Cause, Config as EffectConfig, Context, Effect } from "effect";
|
||||
import { processorLifecycleError, type ProcessorLifecycleError } from "../errors.js";
|
||||
import { loadProcessorRuntimeConfig } from "../runtime/config.js";
|
||||
|
|
@ -198,6 +198,27 @@ export function makeAsyncProcessor<
|
|||
return processor;
|
||||
}
|
||||
|
||||
export const makeAsyncProcessorScoped = Effect.fn("makeAsyncProcessorScoped")(function* <
|
||||
RunError = ProcessorLifecycleError,
|
||||
RunRequirements = never,
|
||||
>(
|
||||
config: ProcessorConfig,
|
||||
options: AsyncProcessorRuntimeOptions<RunError, RunRequirements> = {},
|
||||
) {
|
||||
if (config.pubsub !== undefined) {
|
||||
return makeAsyncProcessor(config, options);
|
||||
}
|
||||
|
||||
const pubsub = yield* makeNatsBackendScoped(config.pubsubUrl ?? "nats://localhost:4222");
|
||||
return makeAsyncProcessor(
|
||||
{
|
||||
...config,
|
||||
pubsub,
|
||||
},
|
||||
options,
|
||||
);
|
||||
});
|
||||
|
||||
export type AsyncProcessor<
|
||||
RunError = ProcessorLifecycleError,
|
||||
RunRequirements = never,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export {
|
||||
AsyncProcessor,
|
||||
makeAsyncProcessor,
|
||||
makeAsyncProcessorScoped,
|
||||
type ConfigHandler,
|
||||
type EffectConfigHandler,
|
||||
type AsyncProcessorRuntime,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
type ProcessorLifecycleError,
|
||||
type PubSubError,
|
||||
} from "../errors.js";
|
||||
import { makeNatsBackend } from "../backend/nats.js";
|
||||
import { makeNatsBackendScoped } from "../backend/nats.js";
|
||||
import { makePubSubService, PubSub } from "../backend/pubsub.js";
|
||||
import {
|
||||
ConsumerFactory,
|
||||
|
|
@ -119,18 +119,9 @@ export function makeProcessorProgram<
|
|||
manageProcessSignals: false,
|
||||
} as Config;
|
||||
|
||||
const pubsub = makePubSubService(makeNatsBackend(runtimeConfig.pubsubUrl ?? "nats://localhost:4222"));
|
||||
const backend = yield* makeNatsBackendScoped(runtimeConfig.pubsubUrl ?? "nats://localhost:4222");
|
||||
const pubsub = makePubSubService(backend);
|
||||
const messagingConfig = yield* loadMessagingRuntimeConfig();
|
||||
yield* Effect.addFinalizer(() =>
|
||||
pubsub.close.pipe(
|
||||
Effect.catch((error) =>
|
||||
Effect.logError("[PubSub] Failed to close processor backend", {
|
||||
error: error.message,
|
||||
operation: error.operation,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
const processorEffect = runProcessorScoped<Config, RunError, RunRequirements>(
|
||||
runtimeConfig,
|
||||
options.make,
|
||||
|
|
@ -202,18 +193,9 @@ export function makeFlowProcessorProgram<
|
|||
manageProcessSignals: false,
|
||||
} as Config;
|
||||
|
||||
const pubsub = makePubSubService(makeNatsBackend(runtimeConfig.pubsubUrl ?? "nats://localhost:4222"));
|
||||
const backend = yield* makeNatsBackendScoped(runtimeConfig.pubsubUrl ?? "nats://localhost:4222");
|
||||
const pubsub = makePubSubService(backend);
|
||||
const messagingConfig = yield* loadMessagingRuntimeConfig();
|
||||
yield* Effect.addFinalizer(() =>
|
||||
pubsub.close.pipe(
|
||||
Effect.catch((error) =>
|
||||
Effect.logError("[PubSub] Failed to close processor backend", {
|
||||
error: error.message,
|
||||
operation: error.operation,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const configHandlers = options.configHandlers?.(runtimeConfig);
|
||||
const processorOptions = {
|
||||
|
|
|
|||
|
|
@ -12,16 +12,10 @@
|
|||
"test": "bunx --bun vitest run --passWithNoTests --exclude=dist/**"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/ai-anthropic": "4.0.0-beta.78",
|
||||
"@effect/ai-openai": "4.0.0-beta.78",
|
||||
"@effect/ai-openrouter": "4.0.0-beta.78",
|
||||
"@effect/atom-react": "4.0.0-beta.78",
|
||||
"@effect/openapi-generator": "4.0.0-beta.78",
|
||||
"@effect/opentelemetry": "4.0.0-beta.78",
|
||||
"@effect/platform-browser": "4.0.0-beta.78",
|
||||
"@effect/platform-bun": "4.0.0-beta.78",
|
||||
"@trustgraph/base": "workspace:*",
|
||||
"@trustgraph/client": "workspace:*",
|
||||
"effect": "4.0.0-beta.78",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -7,37 +7,77 @@
|
|||
import { Effect } from "effect";
|
||||
import * as Argument from "effect/unstable/cli/Argument";
|
||||
import * as Command from "effect/unstable/cli/Command";
|
||||
import { cliCommandError, withSocket } from "./util.js";
|
||||
import { cliCommandError, withGatewayClient, type CliCommandError } from "./util.js";
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: {};
|
||||
}
|
||||
|
||||
function stringProperty(source: unknown, key: string): string | undefined {
|
||||
const value = asRecord(source)[key];
|
||||
return typeof value === "string" ? value : undefined;
|
||||
}
|
||||
|
||||
function booleanProperty(source: unknown, key: string): boolean | undefined {
|
||||
const value = asRecord(source)[key];
|
||||
return typeof value === "boolean" ? value : undefined;
|
||||
}
|
||||
|
||||
function responseErrorMessage(source: unknown): string | undefined {
|
||||
const error = asRecord(source).error;
|
||||
if (typeof error === "string") return error;
|
||||
return stringProperty(error, "message");
|
||||
}
|
||||
|
||||
export const agentCommand = Command.make("agent", {
|
||||
question: Argument.string("question").pipe(Argument.withDescription("Question to ask")),
|
||||
}, ({ question }) =>
|
||||
withSocket((socket, opts) =>
|
||||
withGatewayClient((client, opts) =>
|
||||
Effect.gen(function* () {
|
||||
const flow = socket.flow(opts.flow);
|
||||
let streamError: CliCommandError | undefined;
|
||||
|
||||
yield* Effect.callback<void, ReturnType<typeof cliCommandError>>((resume) => {
|
||||
flow.agent(
|
||||
yield* client.runDispatchStream(
|
||||
{
|
||||
scope: "flow",
|
||||
flow: opts.flow,
|
||||
service: "agent",
|
||||
request: {
|
||||
question,
|
||||
(chunk) => {
|
||||
// think — show thought process
|
||||
if (chunk.length > 0) process.stderr.write(chunk);
|
||||
},
|
||||
(chunk) => {
|
||||
// observe — show observations
|
||||
if (chunk.length > 0) process.stderr.write(chunk);
|
||||
},
|
||||
(chunk, complete) => {
|
||||
// answer — print to stdout
|
||||
if (chunk.length > 0) process.stdout.write(chunk);
|
||||
if (complete) {
|
||||
process.stdout.write("\n");
|
||||
resume(Effect.void);
|
||||
}
|
||||
},
|
||||
(err) => resume(Effect.fail(cliCommandError("agent", err))),
|
||||
);
|
||||
});
|
||||
user: opts.user,
|
||||
collection: "default",
|
||||
streaming: true,
|
||||
},
|
||||
},
|
||||
(chunk) => {
|
||||
const resp = asRecord(chunk.response);
|
||||
const chunkType = stringProperty(resp, "chunk_type");
|
||||
const error = chunkType === "error" ? responseErrorMessage(resp) ?? "Unknown agent error" : responseErrorMessage(resp);
|
||||
if (error !== undefined) {
|
||||
streamError = cliCommandError("agent", error);
|
||||
return true;
|
||||
}
|
||||
|
||||
const content = stringProperty(resp, "content") ?? "";
|
||||
const messageComplete = booleanProperty(resp, "end_of_message") === true;
|
||||
const dialogComplete = chunk.complete === true || booleanProperty(resp, "end_of_dialog") === true;
|
||||
|
||||
if (chunkType === "thought" || chunkType === "observation") {
|
||||
if (content.length > 0) process.stderr.write(content);
|
||||
} else if (chunkType === "answer" || chunkType === "final-answer") {
|
||||
if (content.length > 0) process.stdout.write(content);
|
||||
if (messageComplete || dialogComplete) process.stdout.write("\n");
|
||||
}
|
||||
|
||||
return dialogComplete;
|
||||
},
|
||||
{ timeoutMs: 120_000, retries: 2 },
|
||||
);
|
||||
|
||||
if (streamError !== undefined) {
|
||||
return yield* streamError;
|
||||
}
|
||||
}),
|
||||
),
|
||||
).pipe(Command.withDescription("Ask the TrustGraph agent a question"));
|
||||
|
|
|
|||
|
|
@ -2,7 +2,12 @@
|
|||
* Shared CLI utilities.
|
||||
*/
|
||||
|
||||
import { createTrustGraphSocket, type BaseApi } from "@trustgraph/client";
|
||||
import {
|
||||
createTrustGraphSocket,
|
||||
makeTrustGraphGatewayClientScoped,
|
||||
type BaseApi,
|
||||
type TrustGraphGatewayClient,
|
||||
} from "@trustgraph/client";
|
||||
import { Duration, Effect } from "effect";
|
||||
import * as O from "effect/Option";
|
||||
import * as S from "effect/Schema";
|
||||
|
|
@ -109,6 +114,23 @@ export function createSocketEffect(opts: CliOpts): Effect.Effect<BaseApi, CliCom
|
|||
);
|
||||
}
|
||||
|
||||
function gatewayUrlWithToken(opts: CliOpts): string {
|
||||
if (opts.token === undefined || opts.token.length === 0) return opts.gateway;
|
||||
const separator = opts.gateway.includes("?") ? "&" : "?";
|
||||
return `${opts.gateway}${separator}token=${encodeURIComponent(opts.token)}`;
|
||||
}
|
||||
|
||||
export const withGatewayClient = Effect.fn("withGatewayClient")(function* <A, E, R>(
|
||||
use: (client: TrustGraphGatewayClient, opts: CliOpts) => Effect.Effect<A, E, R>,
|
||||
) {
|
||||
const opts = yield* getOpts;
|
||||
return yield* Effect.scoped(
|
||||
makeTrustGraphGatewayClientScoped({ url: gatewayUrlWithToken(opts) }).pipe(
|
||||
Effect.flatMap((client) => use(client, opts)),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
export const withSocket = Effect.fn("withSocket")(function* <A, E, R>(
|
||||
use: (socket: BaseApi, opts: CliOpts) => Effect.Effect<A, E, R>,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,16 @@
|
|||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./rpc/contract": {
|
||||
"types": "./dist/rpc/contract.d.ts",
|
||||
"import": "./dist/rpc/contract.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "bunx --bun tsc",
|
||||
"dev": "tsc --watch",
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ describe("FlowsApi", () => {
|
|||
makeRequest: vi.fn(),
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
flowsApi = new FlowsApi(mockApi as any);
|
||||
flowsApi = FlowsApi(mockApi as any);
|
||||
});
|
||||
|
||||
describe("startFlow", () => {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ describe("workbench API contracts", () => {
|
|||
values: [{ type: "prompt", key: "welcome", value: "hello" }],
|
||||
});
|
||||
|
||||
const result = await new ConfigApi(base).getValues("prompt");
|
||||
const result = await ConfigApi(base).getValues("prompt");
|
||||
|
||||
expect(makeRequest).toHaveBeenCalledWith(
|
||||
"config",
|
||||
|
|
@ -45,7 +45,7 @@ describe("workbench API contracts", () => {
|
|||
],
|
||||
});
|
||||
|
||||
const result = await new ConfigApi(base).getTokenCosts();
|
||||
const result = await ConfigApi(base).getTokenCosts();
|
||||
|
||||
expect(result).toEqual([
|
||||
{ model: "gpt-test", input_price: 0.1, output_price: 0.2 },
|
||||
|
|
@ -55,7 +55,7 @@ describe("workbench API contracts", () => {
|
|||
it("writes and deletes config using Python-style key/value arrays", async () => {
|
||||
const { base, makeRequest } = makeApi();
|
||||
makeRequest.mockResolvedValue({});
|
||||
const config = new ConfigApi(base);
|
||||
const config = ConfigApi(base);
|
||||
|
||||
await config.putConfig([{ type: "tool", key: "search", value: "{}" }]);
|
||||
await config.deleteConfig({ type: "tool", key: "search" });
|
||||
|
|
@ -86,7 +86,7 @@ describe("workbench API contracts", () => {
|
|||
const { base, makeRequest } = makeApi();
|
||||
const document = { id: "doc-1", title: "Document" };
|
||||
const processing = { id: "proc-1", "document-id": "doc-1" };
|
||||
const librarian = new LibrarianApi(base);
|
||||
const librarian = LibrarianApi(base);
|
||||
|
||||
makeRequest
|
||||
.mockResolvedValueOnce({ "document-metadatas": [document] })
|
||||
|
|
@ -101,7 +101,7 @@ describe("workbench API contracts", () => {
|
|||
const document = { id: "doc-1", title: "Document" };
|
||||
makeRequest.mockResolvedValue({ "document-metadata": document });
|
||||
|
||||
const result = await new LibrarianApi(base).getDocumentMetadata("doc-1");
|
||||
const result = await LibrarianApi(base).getDocumentMetadata("doc-1");
|
||||
|
||||
expect(makeRequest).toHaveBeenCalledWith(
|
||||
"librarian",
|
||||
|
|
@ -120,7 +120,7 @@ describe("workbench API contracts", () => {
|
|||
const { base, makeRequest } = makeApi();
|
||||
makeRequest.mockResolvedValue({});
|
||||
|
||||
await new LibrarianApi(base).loadDocument(
|
||||
await LibrarianApi(base).loadDocument(
|
||||
"SGVsbG8=",
|
||||
"text/plain",
|
||||
"Hello",
|
||||
|
|
@ -145,7 +145,7 @@ describe("workbench API contracts", () => {
|
|||
describe("KnowledgeApi", () => {
|
||||
it("lists and loads document embedding cores", async () => {
|
||||
const { base, makeRequest } = makeApi();
|
||||
const knowledge = new KnowledgeApi(base);
|
||||
const knowledge = KnowledgeApi(base);
|
||||
|
||||
makeRequest
|
||||
.mockResolvedValueOnce({ ids: ["de-core"] })
|
||||
|
|
@ -178,7 +178,7 @@ describe("workbench API contracts", () => {
|
|||
const { base, makeRequest } = makeApi();
|
||||
makeRequest.mockResolvedValue({});
|
||||
|
||||
await new KnowledgeApi(base).unloadKgCore("kg-core", "default");
|
||||
await KnowledgeApi(base).unloadKgCore("kg-core", "default");
|
||||
|
||||
expect(makeRequest).toHaveBeenCalledWith(
|
||||
"knowledge",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export * from "./models/namespaces.js";
|
|||
|
||||
// Export socket client
|
||||
export * from "./socket/trustgraph-socket.js";
|
||||
export * from "./socket/effect-rpc-client.js";
|
||||
export * from "./rpc/contract.js";
|
||||
|
||||
// Export WebSocket adapter (isomorphic helpers and types)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Schema as S } from "effect";
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
|
||||
import * as Rpc from "effect/unstable/rpc/Rpc";
|
||||
import * as RpcGroup from "effect/unstable/rpc/RpcGroup";
|
||||
|
||||
|
|
@ -32,3 +33,14 @@ export class DispatchStream extends Rpc.make("DispatchStream", {
|
|||
}) {}
|
||||
|
||||
export const TrustGraphRpcs = RpcGroup.make(Dispatch, DispatchStream);
|
||||
|
||||
export class GatewayWorkbenchHttpApi extends HttpApi.make("trustgraph-gateway-workbench")
|
||||
.add(
|
||||
HttpApiGroup.make("workbench", { topLevel: true }).add(
|
||||
HttpApiEndpoint.post("dispatch", "/api/v1/workbench/dispatch", {
|
||||
payload: DispatchPayload,
|
||||
success: S.Unknown,
|
||||
}),
|
||||
),
|
||||
)
|
||||
{}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Cause, Context, Effect, Fiber, Layer, ManagedRuntime, Stream, SubscriptionRef } from "effect";
|
||||
import { Cause, Context, Effect, Exit, Fiber, Layer, Ref, Scope, Stream, SubscriptionRef } from "effect";
|
||||
import type * as RpcGroup from "effect/unstable/rpc/RpcGroup";
|
||||
import * as RpcClient from "effect/unstable/rpc/RpcClient";
|
||||
import type { RpcClientError } from "effect/unstable/rpc/RpcClientError";
|
||||
|
|
@ -35,24 +35,44 @@ export interface DispatchOptions {
|
|||
readonly retries?: number;
|
||||
}
|
||||
|
||||
export interface TrustGraphGatewayClient {
|
||||
readonly state: Effect.Effect<RpcConnectionState>;
|
||||
readonly changes: Stream.Stream<RpcConnectionState>;
|
||||
readonly subscribe: (
|
||||
listener: (state: RpcConnectionState) => void,
|
||||
) => Effect.Effect<Effect.Effect<void>>;
|
||||
readonly dispatch: (
|
||||
input: DispatchInput,
|
||||
options?: DispatchOptions,
|
||||
) => Effect.Effect<unknown, RpcClientError | DispatchError>;
|
||||
readonly dispatchStream: (
|
||||
input: DispatchInput,
|
||||
options?: DispatchOptions,
|
||||
) => Stream.Stream<DispatchStreamChunk, RpcClientError | DispatchError>;
|
||||
readonly runDispatchStream: (
|
||||
input: DispatchInput,
|
||||
receiver: (chunk: DispatchStreamChunk) => boolean,
|
||||
options?: DispatchOptions,
|
||||
) => Effect.Effect<DispatchStreamChunk | undefined, RpcClientError | DispatchError>;
|
||||
readonly close: Effect.Effect<void>;
|
||||
}
|
||||
|
||||
export class TrustGraphGatewayClientService extends Context.Service<
|
||||
TrustGraphGatewayClientService,
|
||||
TrustGraphGatewayClient
|
||||
>()("@trustgraph/client/socket/effect-rpc-client/TrustGraphGatewayClientService") {}
|
||||
|
||||
export interface TrustGraphGatewayClientOptions {
|
||||
readonly url: string;
|
||||
readonly onConnect?: () => void;
|
||||
readonly onDisconnect?: () => void;
|
||||
readonly stateRef?: SubscriptionRef.SubscriptionRef<RpcConnectionState>;
|
||||
readonly closedRef?: Ref.Ref<boolean>;
|
||||
}
|
||||
|
||||
const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;
|
||||
const DEFAULT_REQUEST_ATTEMPTS = 3;
|
||||
|
||||
type NewableFactory<Args extends readonly unknown[], A extends object> = {
|
||||
new (...args: Args): A;
|
||||
(...args: Args): A;
|
||||
readonly prototype: A;
|
||||
};
|
||||
|
||||
function newableFactory<Args extends readonly unknown[], A extends object>(
|
||||
factory: (...args: Args) => A,
|
||||
): NewableFactory<Args, A> {
|
||||
function Constructor(...args: Args): A {
|
||||
return factory(...args);
|
||||
}
|
||||
return Constructor as unknown as NewableFactory<Args, A>;
|
||||
}
|
||||
|
||||
export interface EffectRpcClient {
|
||||
readonly subscribe: (listener: (state: RpcConnectionState) => void) => () => void;
|
||||
readonly dispatch: (
|
||||
|
|
@ -67,131 +87,207 @@ export interface EffectRpcClient {
|
|||
readonly close: () => Promise<void>;
|
||||
}
|
||||
|
||||
const makeClientLayer = (
|
||||
options: TrustGraphGatewayClientOptions,
|
||||
stateRef: SubscriptionRef.SubscriptionRef<RpcConnectionState>,
|
||||
closedRef: Ref.Ref<boolean>,
|
||||
): Layer.Layer<TrustGraphRpcClientService> => {
|
||||
const setState = (nextState: RpcConnectionState) =>
|
||||
SubscriptionRef.set(stateRef, nextState);
|
||||
|
||||
const socketLayer = Layer.effect(
|
||||
Socket.Socket,
|
||||
Socket.makeWebSocket(options.url, {
|
||||
closeCodeIsError: (code) => code !== 1000,
|
||||
openTimeout: "10 seconds",
|
||||
}),
|
||||
).pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal));
|
||||
|
||||
const hooksLayer = Layer.succeed(
|
||||
RpcClient.ConnectionHooks,
|
||||
RpcClient.ConnectionHooks.of({
|
||||
onConnect: Effect.gen(function* () {
|
||||
yield* setState({ status: "connected" });
|
||||
options.onConnect?.();
|
||||
}),
|
||||
onDisconnect: Effect.gen(function* () {
|
||||
const closed = yield* Ref.get(closedRef);
|
||||
if (!closed) {
|
||||
yield* setState({
|
||||
status: "connecting",
|
||||
lastError: "Disconnected from gateway",
|
||||
});
|
||||
}
|
||||
options.onDisconnect?.();
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const protocolLayer = RpcClient.layerProtocolSocket({
|
||||
retryTransientErrors: true,
|
||||
}).pipe(
|
||||
Layer.provide(socketLayer),
|
||||
Layer.provide(RpcSerialization.layerNdjson),
|
||||
Layer.provide(hooksLayer),
|
||||
);
|
||||
|
||||
return Layer.effect(
|
||||
TrustGraphRpcClientService,
|
||||
RpcClient.make(TrustGraphRpcs),
|
||||
).pipe(Layer.provide(protocolLayer));
|
||||
};
|
||||
|
||||
const makeSubscribeEffect = Effect.fn("makeSubscribeEffect")(function* (
|
||||
stateRef: SubscriptionRef.SubscriptionRef<RpcConnectionState>,
|
||||
scope: Scope.Scope,
|
||||
listener: (state: RpcConnectionState) => void,
|
||||
) {
|
||||
let latest = SubscriptionRef.getUnsafe(stateRef);
|
||||
listener(latest);
|
||||
let replaySeen = false;
|
||||
const fiber = yield* Effect.forkIn(SubscriptionRef.changes(stateRef).pipe(
|
||||
Stream.runForEach((nextState) =>
|
||||
Effect.sync(() => {
|
||||
if (!replaySeen) {
|
||||
replaySeen = true;
|
||||
if (nextState === latest) return;
|
||||
}
|
||||
latest = nextState;
|
||||
listener(nextState);
|
||||
})
|
||||
),
|
||||
), scope);
|
||||
return yield* Effect.succeed(Fiber.interrupt(fiber).pipe(Effect.asVoid));
|
||||
});
|
||||
|
||||
export const makeTrustGraphGatewayClientScoped: (
|
||||
options: TrustGraphGatewayClientOptions,
|
||||
) => Effect.Effect<TrustGraphGatewayClient, never, Scope.Scope> = Effect.fn("makeTrustGraphGatewayClientScoped")(function* (
|
||||
options,
|
||||
) {
|
||||
const stateRef = options.stateRef ?? (yield* SubscriptionRef.make<RpcConnectionState>({ status: "connecting" }));
|
||||
const closedRef = options.closedRef ?? (yield* Ref.make(false));
|
||||
const scope = yield* Scope.Scope;
|
||||
const context = yield* Layer.buildWithScope(makeClientLayer(options, stateRef, closedRef), scope).pipe(
|
||||
Effect.tapCause((cause) =>
|
||||
SubscriptionRef.set(stateRef, {
|
||||
status: "failed",
|
||||
lastError: Cause.pretty(cause),
|
||||
})
|
||||
),
|
||||
);
|
||||
const client = Context.get(context, TrustGraphRpcClientService);
|
||||
|
||||
const close = Effect.gen(function* () {
|
||||
const wasClosed = yield* Ref.getAndSet(closedRef, true);
|
||||
if (!wasClosed) {
|
||||
yield* SubscriptionRef.set(stateRef, { status: "closed" });
|
||||
}
|
||||
});
|
||||
|
||||
yield* Effect.addFinalizer(() => close);
|
||||
|
||||
return {
|
||||
state: SubscriptionRef.get(stateRef),
|
||||
changes: SubscriptionRef.changes(stateRef),
|
||||
subscribe: (listener) => makeSubscribeEffect(stateRef, scope, listener),
|
||||
dispatch: (input, options = {}) =>
|
||||
withDispatchRequestPolicy(client.Dispatch(DispatchPayload.make(input)), options),
|
||||
dispatchStream: (input, options = {}) =>
|
||||
Stream.unwrap(
|
||||
withDispatchRequestPolicy(
|
||||
Effect.succeed(client.DispatchStream(DispatchPayload.make(input))),
|
||||
options,
|
||||
),
|
||||
),
|
||||
runDispatchStream: (input, receiver, options = {}) => {
|
||||
let last: DispatchStreamChunk | undefined;
|
||||
return withDispatchRequestPolicy(
|
||||
client.DispatchStream(DispatchPayload.make(input)).pipe(
|
||||
Stream.runForEachWhile((chunk) =>
|
||||
Effect.suspend(() => {
|
||||
last = chunk;
|
||||
return Effect.succeed(!receiver(chunk));
|
||||
}),
|
||||
),
|
||||
Effect.andThen(() => Effect.succeed(last)),
|
||||
),
|
||||
options,
|
||||
);
|
||||
},
|
||||
close,
|
||||
} satisfies TrustGraphGatewayClient;
|
||||
});
|
||||
|
||||
export const makeTrustGraphGatewayClientLayer = (
|
||||
options: TrustGraphGatewayClientOptions,
|
||||
): Layer.Layer<TrustGraphGatewayClientService> =>
|
||||
Layer.effect(
|
||||
TrustGraphGatewayClientService,
|
||||
makeTrustGraphGatewayClientScoped(options).pipe(
|
||||
Effect.map(TrustGraphGatewayClientService.of),
|
||||
),
|
||||
);
|
||||
|
||||
export function makeEffectRpcClient(
|
||||
url: string,
|
||||
onConnect?: () => void,
|
||||
onDisconnect?: () => void,
|
||||
): EffectRpcClient {
|
||||
const stateRef = Effect.runSync(SubscriptionRef.make<RpcConnectionState>({ status: "connecting" }));
|
||||
let closed = false;
|
||||
|
||||
const setState = (nextState: RpcConnectionState) =>
|
||||
SubscriptionRef.set(stateRef, nextState);
|
||||
|
||||
const makeClientLayer = (): Layer.Layer<TrustGraphRpcClientService> => {
|
||||
const socketLayer = Layer.effect(
|
||||
Socket.Socket,
|
||||
Socket.makeWebSocket(url, {
|
||||
closeCodeIsError: (code) => code !== 1000,
|
||||
openTimeout: "10 seconds",
|
||||
}),
|
||||
).pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal));
|
||||
|
||||
const hooksLayer = Layer.succeed(
|
||||
RpcClient.ConnectionHooks,
|
||||
RpcClient.ConnectionHooks.of({
|
||||
onConnect: Effect.gen(function* () {
|
||||
yield* setState({ status: "connected" });
|
||||
onConnect?.();
|
||||
}),
|
||||
onDisconnect: Effect.gen(function* () {
|
||||
if (!closed) {
|
||||
yield* setState({
|
||||
status: "connecting",
|
||||
lastError: "Disconnected from gateway",
|
||||
});
|
||||
}
|
||||
onDisconnect?.();
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const protocolLayer = RpcClient.layerProtocolSocket({
|
||||
retryTransientErrors: true,
|
||||
}).pipe(
|
||||
Layer.provide(socketLayer),
|
||||
Layer.provide(RpcSerialization.layerNdjson),
|
||||
Layer.provide(hooksLayer),
|
||||
);
|
||||
|
||||
const clientLayer = Layer.effect(
|
||||
TrustGraphRpcClientService,
|
||||
RpcClient.make(TrustGraphRpcs),
|
||||
).pipe(Layer.provide(protocolLayer));
|
||||
|
||||
return clientLayer;
|
||||
const closedRef = Effect.runSync(Ref.make(false));
|
||||
const scope = Effect.runSync(Scope.make());
|
||||
const options: TrustGraphGatewayClientOptions = {
|
||||
url,
|
||||
stateRef,
|
||||
closedRef,
|
||||
...(onConnect === undefined ? {} : { onConnect }),
|
||||
...(onDisconnect === undefined ? {} : { onDisconnect }),
|
||||
};
|
||||
|
||||
const runtime = ManagedRuntime.make(makeClientLayer());
|
||||
const clientPromise = runtime.runPromise(
|
||||
TrustGraphRpcClientService.pipe(
|
||||
Effect.tapCause((cause) =>
|
||||
setState({
|
||||
status: "failed",
|
||||
lastError: Cause.pretty(cause),
|
||||
})
|
||||
),
|
||||
),
|
||||
const clientPromise = Effect.runPromise(
|
||||
makeTrustGraphGatewayClientScoped(options).pipe(Scope.provide(scope)),
|
||||
);
|
||||
|
||||
return {
|
||||
subscribe: (listener) => {
|
||||
let latest = SubscriptionRef.getUnsafe(stateRef);
|
||||
listener(latest);
|
||||
let replaySeen = false;
|
||||
const fiber = Effect.runFork(
|
||||
SubscriptionRef.changes(stateRef).pipe(
|
||||
Stream.runForEach((nextState) =>
|
||||
Effect.sync(() => {
|
||||
if (!replaySeen) {
|
||||
replaySeen = true;
|
||||
if (nextState === latest) return;
|
||||
}
|
||||
latest = nextState;
|
||||
listener(nextState);
|
||||
})
|
||||
),
|
||||
),
|
||||
let unsubscribe: Effect.Effect<void> | undefined;
|
||||
let cancelled = false;
|
||||
listener(SubscriptionRef.getUnsafe(stateRef));
|
||||
void clientPromise.then((client) =>
|
||||
Effect.runPromise(client.subscribe(listener)).then((release) => {
|
||||
if (cancelled) {
|
||||
return Effect.runPromise(release);
|
||||
}
|
||||
unsubscribe = release;
|
||||
})
|
||||
);
|
||||
|
||||
return () => {
|
||||
Effect.runFork(Fiber.interrupt(fiber));
|
||||
cancelled = true;
|
||||
if (unsubscribe !== undefined) {
|
||||
Effect.runFork(unsubscribe);
|
||||
}
|
||||
};
|
||||
},
|
||||
dispatch: (input, options = {}) =>
|
||||
clientPromise.then((client) =>
|
||||
runtime.runPromise(
|
||||
withDispatchRequestPolicy(client.Dispatch(DispatchPayload.make(input)), options),
|
||||
)
|
||||
Effect.runPromise(client.dispatch(input, options))
|
||||
),
|
||||
dispatchStream: (input, receiver, options = {}) => {
|
||||
let last: DispatchStreamChunk | undefined;
|
||||
return clientPromise.then((client) =>
|
||||
runtime.runPromise(
|
||||
withDispatchRequestPolicy(
|
||||
client.DispatchStream(DispatchPayload.make(input)).pipe(
|
||||
Stream.runForEachWhile((chunk) =>
|
||||
Effect.suspend(() => {
|
||||
last = chunk;
|
||||
return Effect.succeed(!receiver(chunk));
|
||||
}),
|
||||
),
|
||||
),
|
||||
options,
|
||||
dispatchStream: (input, receiver, options = {}) =>
|
||||
clientPromise.then((client) =>
|
||||
Effect.runPromise(client.runDispatchStream(input, receiver, options))
|
||||
),
|
||||
close: () =>
|
||||
clientPromise.then((client) =>
|
||||
Effect.runPromise(
|
||||
client.close.pipe(
|
||||
Effect.andThen(Scope.close(scope, Exit.void)),
|
||||
),
|
||||
)
|
||||
).then(() => last);
|
||||
},
|
||||
close: () => {
|
||||
if (closed) return Promise.resolve();
|
||||
closed = true;
|
||||
Effect.runSync(setState({ status: "closed" }));
|
||||
return runtime.dispose();
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export const EffectRpcClient = newableFactory(makeEffectRpcClient);
|
||||
|
||||
export function withDispatchRequestPolicy<A, E, R>(
|
||||
effect: Effect.Effect<A, E, R>,
|
||||
options: DispatchOptions,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Import core types and classes for the TrustGraph API
|
||||
import type { Term, Triple } from "../models/Triple.js";
|
||||
import {
|
||||
EffectRpcClient,
|
||||
type EffectRpcClient,
|
||||
type DispatchInput,
|
||||
type DispatchOptions,
|
||||
type RpcConnectionState,
|
||||
|
|
@ -445,21 +445,6 @@ function makeid(length: number) {
|
|||
);
|
||||
}
|
||||
|
||||
type NewableFactory<Args extends readonly unknown[], A extends object> = {
|
||||
new (...args: Args): A;
|
||||
(...args: Args): A;
|
||||
readonly prototype: A;
|
||||
};
|
||||
|
||||
function newableFactory<Args extends readonly unknown[], A extends object>(
|
||||
factory: (...args: Args) => A,
|
||||
): NewableFactory<Args, A> {
|
||||
function Constructor(...args: Args): A {
|
||||
return factory(...args);
|
||||
}
|
||||
return Constructor as unknown as NewableFactory<Args, A>;
|
||||
}
|
||||
|
||||
/**
|
||||
* BaseApi - Core WebSocket client for TrustGraph API
|
||||
* Manages connection lifecycle, message routing, and provides base request
|
||||
|
|
@ -622,27 +607,27 @@ export function makeBaseApi(
|
|||
|
||||
// Factory methods for creating specialized API instances
|
||||
librarian() {
|
||||
return new LibrarianApi(api);
|
||||
return makeLibrarianApi(api);
|
||||
},
|
||||
|
||||
flows() {
|
||||
return new FlowsApi(api);
|
||||
return makeFlowsApi(api);
|
||||
},
|
||||
|
||||
flow(id: string) {
|
||||
return new FlowApi(api, id);
|
||||
return makeFlowApi(api, id);
|
||||
},
|
||||
|
||||
knowledge() {
|
||||
return new KnowledgeApi(api);
|
||||
return makeKnowledgeApi(api);
|
||||
},
|
||||
|
||||
config() {
|
||||
return new ConfigApi(api);
|
||||
return makeConfigApi(api);
|
||||
},
|
||||
|
||||
collectionManagement() {
|
||||
return new CollectionManagementApi(api);
|
||||
return makeCollectionManagementApi(api);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -739,7 +724,7 @@ export function makeBaseApi(
|
|||
}
|
||||
|
||||
export type BaseApi = ReturnType<typeof makeBaseApi>;
|
||||
export const BaseApi = newableFactory(makeBaseApi);
|
||||
export const BaseApi = makeBaseApi;
|
||||
|
||||
export function makeBaseApiWithRpc(
|
||||
user: string,
|
||||
|
|
@ -1153,7 +1138,7 @@ export function makeLibrarianApi(api: BaseApi) {
|
|||
}
|
||||
|
||||
export type LibrarianApi = ReturnType<typeof makeLibrarianApi>;
|
||||
export const LibrarianApi = newableFactory(makeLibrarianApi);
|
||||
export const LibrarianApi = makeLibrarianApi;
|
||||
|
||||
/**
|
||||
* FlowsApi - Manages processing flows and configuration
|
||||
|
|
@ -1418,7 +1403,7 @@ export function makeFlowsApi(api: BaseApi) {
|
|||
}
|
||||
|
||||
export type FlowsApi = ReturnType<typeof makeFlowsApi>;
|
||||
export const FlowsApi = newableFactory(makeFlowsApi);
|
||||
export const FlowsApi = makeFlowsApi;
|
||||
|
||||
/**
|
||||
* FlowApi - Interface for interacting with a specific flow instance
|
||||
|
|
@ -2205,7 +2190,7 @@ export function makeFlowApi(api: BaseApi, flowId: string) {
|
|||
}
|
||||
|
||||
export type FlowApi = ReturnType<typeof makeFlowApi>;
|
||||
export const FlowApi = newableFactory(makeFlowApi);
|
||||
export const FlowApi = makeFlowApi;
|
||||
|
||||
/**
|
||||
* ConfigApi - Dedicated configuration management interface
|
||||
|
|
@ -2401,7 +2386,7 @@ export function makeConfigApi(api: BaseApi) {
|
|||
}
|
||||
|
||||
export type ConfigApi = ReturnType<typeof makeConfigApi>;
|
||||
export const ConfigApi = newableFactory(makeConfigApi);
|
||||
export const ConfigApi = makeConfigApi;
|
||||
|
||||
/**
|
||||
* KnowledgeApi - Manages knowledge graph cores and data
|
||||
|
|
@ -2568,7 +2553,7 @@ export function makeKnowledgeApi(api: BaseApi) {
|
|||
}
|
||||
|
||||
export type KnowledgeApi = ReturnType<typeof makeKnowledgeApi>;
|
||||
export const KnowledgeApi = newableFactory(makeKnowledgeApi);
|
||||
export const KnowledgeApi = makeKnowledgeApi;
|
||||
|
||||
/**
|
||||
* CollectionManagementApi - Manages collections for organizing documents
|
||||
|
|
@ -2677,7 +2662,7 @@ export function makeCollectionManagementApi(api: BaseApi) {
|
|||
}
|
||||
|
||||
export type CollectionManagementApi = ReturnType<typeof makeCollectionManagementApi>;
|
||||
export const CollectionManagementApi = newableFactory(makeCollectionManagementApi);
|
||||
export const CollectionManagementApi = makeCollectionManagementApi;
|
||||
|
||||
/**
|
||||
* Factory function to create a new TrustGraph WebSocket connection
|
||||
|
|
@ -2690,4 +2675,4 @@ export const createTrustGraphSocket = (
|
|||
user: string,
|
||||
token?: string,
|
||||
socketUrl?: string,
|
||||
): BaseApi => new BaseApi(user, token, socketUrl);
|
||||
): BaseApi => makeBaseApi(user, token, socketUrl);
|
||||
|
|
|
|||
|
|
@ -12,21 +12,13 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@effect/ai-anthropic": "4.0.0-beta.78",
|
||||
"@effect/ai-openai": "4.0.0-beta.78",
|
||||
"@effect/ai-openrouter": "4.0.0-beta.78",
|
||||
"@effect/atom-react": "4.0.0-beta.78",
|
||||
"@effect/openapi-generator": "4.0.0-beta.78",
|
||||
"@effect/opentelemetry": "4.0.0-beta.78",
|
||||
"@effect/platform-browser": "4.0.0-beta.78",
|
||||
"@effect/platform-bun": "4.0.0-beta.78",
|
||||
"@effect/platform-node": "4.0.0-beta.78",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.78",
|
||||
"@effect/tsgo": "0.14.0",
|
||||
"@effect/vitest": "4.0.0-beta.78",
|
||||
"@mistralai/mistralai": "^1.0.0",
|
||||
"@modelcontextprotocol/sdk": "^1.12.0",
|
||||
"@qdrant/js-client-rest": "^1.13.0",
|
||||
"@trustgraph/base": "workspace:*",
|
||||
"@trustgraph/client": "workspace:*",
|
||||
"effect": "4.0.0-beta.78",
|
||||
"falkordb": "^5.0.0",
|
||||
"ollama": "^0.6.3",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { Clock, Effect, Exit, HashMap, HashSet, Option, Random, Scope, Synchroni
|
|||
import {
|
||||
loadMessagingRuntimeConfig,
|
||||
makeNatsBackend,
|
||||
makeNatsBackendScoped,
|
||||
makePubSubService,
|
||||
makeRequestResponseFactoryService,
|
||||
messagingDeliveryError,
|
||||
|
|
@ -401,3 +402,17 @@ export function makeDispatcherManager(config: GatewayConfig): DispatcherManager
|
|||
publishToTopic,
|
||||
};
|
||||
}
|
||||
|
||||
export const makeDispatcherManagerScoped = Effect.fn("makeDispatcherManagerScoped")(function* (
|
||||
config: GatewayConfig,
|
||||
) {
|
||||
if (config.pubsub !== undefined) {
|
||||
return makeDispatcherManager(config);
|
||||
}
|
||||
|
||||
const pubsub = yield* makeNatsBackendScoped(config.natsUrl ?? "nats://localhost:4222");
|
||||
return makeDispatcherManager({
|
||||
...config,
|
||||
pubsub,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,34 +1 @@
|
|||
import { Schema as S } from "effect";
|
||||
import * as Rpc from "effect/unstable/rpc/Rpc";
|
||||
import * as RpcGroup from "effect/unstable/rpc/RpcGroup";
|
||||
|
||||
export class DispatchPayload extends S.Class<DispatchPayload>("DispatchPayload")({
|
||||
scope: S.Literals(["global", "flow"]),
|
||||
service: S.String,
|
||||
flow: S.optionalKey(S.String),
|
||||
request: S.Record(S.String, S.Unknown),
|
||||
}) {}
|
||||
|
||||
export class DispatchStreamChunk extends S.Class<DispatchStreamChunk>("DispatchStreamChunk")({
|
||||
response: S.Unknown,
|
||||
complete: S.Boolean,
|
||||
}) {}
|
||||
|
||||
export class DispatchError extends S.TaggedErrorClass<DispatchError>()("DispatchError", {
|
||||
message: S.String,
|
||||
}) {}
|
||||
|
||||
export class Dispatch extends Rpc.make("Dispatch", {
|
||||
payload: DispatchPayload,
|
||||
success: S.Unknown,
|
||||
error: DispatchError,
|
||||
}) {}
|
||||
|
||||
export class DispatchStream extends Rpc.make("DispatchStream", {
|
||||
payload: DispatchPayload,
|
||||
success: DispatchStreamChunk,
|
||||
error: DispatchError,
|
||||
stream: true,
|
||||
}) {}
|
||||
|
||||
export const TrustGraphRpcs = RpcGroup.make(Dispatch, DispatchStream);
|
||||
export * from "@trustgraph/client/rpc/contract";
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
|
|||
import { Clock, Config, Effect, Exit, Layer, Random, Scope } from "effect";
|
||||
import * as O from "effect/Option";
|
||||
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi";
|
||||
import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
|
||||
import {
|
||||
formatPrometheusMetrics,
|
||||
|
|
@ -19,7 +20,8 @@ import {
|
|||
toTgError,
|
||||
type PubSubBackend,
|
||||
} from "@trustgraph/base";
|
||||
import { makeDispatcherManager, type DispatcherManager } from "./dispatch/manager.js";
|
||||
import { GatewayWorkbenchHttpApi } from "@trustgraph/client/rpc/contract";
|
||||
import { makeDispatcherManagerScoped, type DispatcherManager } from "./dispatch/manager.js";
|
||||
import { makeGatewayRpcServer, type GatewayRpcServer } from "./rpc-server.js";
|
||||
|
||||
export interface GatewayConfig {
|
||||
|
|
@ -134,6 +136,22 @@ const workbenchDispatch = (
|
|||
),
|
||||
);
|
||||
|
||||
const gatewayWorkbenchHttpApiRoutes = (
|
||||
config: GatewayConfig,
|
||||
dispatcher: DispatcherManager,
|
||||
) => {
|
||||
const handlers = HttpApiBuilder.group(
|
||||
GatewayWorkbenchHttpApi,
|
||||
"workbench",
|
||||
(handlers) =>
|
||||
handlers.handleRaw("dispatch", () => workbenchDispatch(config, dispatcher)),
|
||||
);
|
||||
|
||||
return HttpApiBuilder.layer(GatewayWorkbenchHttpApi).pipe(
|
||||
Layer.provide(handlers),
|
||||
);
|
||||
};
|
||||
|
||||
const globalDispatch = (
|
||||
config: GatewayConfig,
|
||||
dispatcher: DispatcherManager,
|
||||
|
|
@ -240,7 +258,7 @@ export const makeGatewayRoutes = (
|
|||
rpcScope: Scope.Scope,
|
||||
) =>
|
||||
Layer.mergeAll(
|
||||
HttpRouter.add("POST", "/api/v1/workbench/dispatch", workbenchDispatch(config, dispatcher)),
|
||||
gatewayWorkbenchHttpApiRoutes(config, dispatcher),
|
||||
HttpRouter.add("POST", "/api/v1/:kind", globalDispatch(config, dispatcher)),
|
||||
HttpRouter.add("POST", "/api/v1/flow/:flow/service/:kind", flowDispatch(config, dispatcher)),
|
||||
HttpRouter.add("POST", "/api/v1/flow/:flow/load", flowLoad(config, dispatcher)),
|
||||
|
|
@ -251,7 +269,7 @@ export const makeGatewayRoutes = (
|
|||
export function createGateway(config: GatewayConfig) {
|
||||
return Layer.effectDiscard(
|
||||
Effect.scoped(Effect.gen(function* () {
|
||||
const dispatcher = makeDispatcherManager(config);
|
||||
const dispatcher = yield* makeDispatcherManagerScoped(config);
|
||||
yield* dispatcher.start.pipe(
|
||||
Effect.mapError((cause) => messagingLifecycleError("gateway", "dispatcher-start", cause)),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"],
|
||||
"references": [
|
||||
{ "path": "../base" }
|
||||
{ "path": "../base" },
|
||||
{ "path": "../client" }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,18 +13,9 @@
|
|||
"dependencies": {
|
||||
"@trustgraph/base": "workspace:*",
|
||||
"@trustgraph/client": "workspace:*",
|
||||
"@effect/platform-node": "4.0.0-beta.78",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.78",
|
||||
"@effect/ai-anthropic": "4.0.0-beta.78",
|
||||
"@effect/ai-openai": "4.0.0-beta.78",
|
||||
"@effect/ai-openrouter": "4.0.0-beta.78",
|
||||
"@effect/atom-react": "4.0.0-beta.78",
|
||||
"@effect/openapi-generator": "4.0.0-beta.78",
|
||||
"@effect/opentelemetry": "4.0.0-beta.78",
|
||||
"@effect/platform-browser": "4.0.0-beta.78",
|
||||
"@effect/platform-bun": "4.0.0-beta.78",
|
||||
"@effect/tsgo": "0.14.0",
|
||||
"@effect/vitest": "4.0.0-beta.78"
|
||||
"@effect/platform-node": "4.0.0-beta.78",
|
||||
"effect": "4.0.0-beta.78"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/vitest": "4.0.0-beta.78",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from "@effect/vitest";
|
||||
import type { BaseApi } from "@trustgraph/client";
|
||||
import { Effect, Layer } from "effect";
|
||||
import { DispatchStreamChunk, type BaseApi, type TrustGraphGatewayClient } from "@trustgraph/client";
|
||||
import { Effect, Layer, Stream } from "effect";
|
||||
import * as S from "effect/Schema";
|
||||
import { McpServer } from "effect/unstable/ai";
|
||||
import * as McpSchema from "effect/unstable/ai/McpSchema";
|
||||
|
|
@ -13,6 +13,7 @@ import {
|
|||
TrustGraphMcpConfig,
|
||||
TrustGraphMcpToolkit,
|
||||
TrustGraphMcpToolkitLive,
|
||||
TrustGraphGateway,
|
||||
TrustGraphSocket,
|
||||
} from "../server-effect.js";
|
||||
|
||||
|
|
@ -124,6 +125,29 @@ const makeFakeSocket = (
|
|||
return { socket, calls };
|
||||
};
|
||||
|
||||
const makeFakeGateway = (): TrustGraphGatewayClient => ({
|
||||
state: Effect.succeed({ status: "connected" }),
|
||||
changes: Stream.empty,
|
||||
subscribe: () => Effect.succeed(Effect.void),
|
||||
dispatch: () => Effect.succeed({}),
|
||||
dispatchStream: () => Stream.empty,
|
||||
runDispatchStream: (_input, receiver) =>
|
||||
Effect.sync(() => {
|
||||
const chunk = DispatchStreamChunk.make({
|
||||
response: {
|
||||
chunk_type: "answer",
|
||||
content: "agent answer",
|
||||
end_of_message: true,
|
||||
end_of_dialog: true,
|
||||
},
|
||||
complete: true,
|
||||
});
|
||||
receiver(chunk);
|
||||
return chunk;
|
||||
}),
|
||||
close: Effect.void,
|
||||
});
|
||||
|
||||
const testConfig = TrustGraphMcpConfig.of({
|
||||
gatewayUrl: "ws://localhost:8088/api/v1/rpc",
|
||||
user: "mcp-test",
|
||||
|
|
@ -147,8 +171,10 @@ const makeNativeTestClientEffect = Effect.fn("makeNativeTestClient")(function*(
|
|||
textCompletion: options.textCompletion,
|
||||
graphRag: options.graphRag,
|
||||
});
|
||||
const gateway = makeFakeGateway();
|
||||
const serverLayer = McpServer.toolkit(TrustGraphMcpToolkit).pipe(
|
||||
Layer.provide(TrustGraphMcpToolkitLive),
|
||||
Layer.provide(Layer.succeed(TrustGraphGateway, TrustGraphGateway.of(gateway))),
|
||||
Layer.provide(Layer.succeed(TrustGraphSocket, TrustGraphSocket.of(socket))),
|
||||
Layer.provide(Layer.succeed(TrustGraphMcpConfig, testConfig)),
|
||||
Layer.provide(McpServer.layerHttp({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import {BunHttpServer, BunRuntime} from "@effect/platform-bun";
|
||||
import {NodeRuntime, NodeStdio} from "@effect/platform-node";
|
||||
import {createTrustGraphSocket, type BaseApi, type Term as ClientTerm} from "@trustgraph/client";
|
||||
import {
|
||||
createTrustGraphSocket,
|
||||
makeTrustGraphGatewayClientScoped,
|
||||
type BaseApi,
|
||||
type Term as ClientTerm,
|
||||
type TrustGraphGatewayClient,
|
||||
} from "@trustgraph/client";
|
||||
import {Config, Context, Effect, Layer} from "effect";
|
||||
import * as O from "effect/Option";
|
||||
import * as Predicate from "effect/Predicate";
|
||||
|
|
@ -1223,6 +1229,12 @@ export interface TrustGraphMcpConfigShape {
|
|||
const readNonEmpty = (value: string | undefined): string | undefined =>
|
||||
value !== undefined && value.length > 0 ? value : undefined
|
||||
|
||||
const gatewayUrlWithToken = (config: TrustGraphMcpConfigShape): string => {
|
||||
if (config.token === undefined || config.token.length === 0) return config.gatewayUrl
|
||||
const separator = config.gatewayUrl.includes("?") ? "&" : "?"
|
||||
return `${config.gatewayUrl}${separator}token=${encodeURIComponent(config.token)}`
|
||||
}
|
||||
|
||||
const parsePort = (raw: string | undefined): number => {
|
||||
if (raw === undefined) {
|
||||
return 3000
|
||||
|
|
@ -1283,6 +1295,19 @@ export class TrustGraphSocket extends Context.Service<TrustGraphSocket, BaseApi>
|
|||
)
|
||||
}
|
||||
|
||||
export class TrustGraphGateway extends Context.Service<TrustGraphGateway, TrustGraphGatewayClient>()(
|
||||
"@trustgraph/mcp/server-effect/TrustGraphGateway",
|
||||
) {
|
||||
static readonly layer = Layer.effect(
|
||||
TrustGraphGateway,
|
||||
Effect.gen(function*() {
|
||||
const config = yield* TrustGraphMcpConfig
|
||||
const client = yield* makeTrustGraphGatewayClientScoped({url: gatewayUrlWithToken(config)})
|
||||
return TrustGraphGateway.of(client)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const toErrorMessage = (cause: unknown): string => {
|
||||
if (Predicate.isError(cause) && cause.message.length > 0) {
|
||||
return cause.message
|
||||
|
|
@ -1296,6 +1321,25 @@ const toErrorMessage = (cause: unknown): string => {
|
|||
return "TrustGraph MCP tool failed"
|
||||
}
|
||||
|
||||
const asRecord = (value: unknown): Record<string, unknown> =>
|
||||
Predicate.isObject(value) && !Array.isArray(value) ? value as Record<string, unknown> : {}
|
||||
|
||||
const stringProperty = (source: unknown, key: string): string | undefined => {
|
||||
const value = asRecord(source)[key]
|
||||
return Predicate.isString(value) ? value : undefined
|
||||
}
|
||||
|
||||
const booleanProperty = (source: unknown, key: string): boolean | undefined => {
|
||||
const value = asRecord(source)[key]
|
||||
return typeof value === "boolean" ? value : undefined
|
||||
}
|
||||
|
||||
const responseErrorMessage = (source: unknown): string | undefined => {
|
||||
const error = asRecord(source).error
|
||||
if (Predicate.isString(error)) return error
|
||||
return stringProperty(error, "message")
|
||||
}
|
||||
|
||||
const decodeJson = S.decodeUnknownEffect(S.Json)
|
||||
const decodeJsonArray = S.decodeUnknownEffect(S.Array(S.Json))
|
||||
|
||||
|
|
@ -1316,10 +1360,59 @@ const decodeJsonArrayOrFail = <E>(
|
|||
const asIriTerm = (value: string | undefined): ClientTerm | undefined =>
|
||||
value !== undefined && value.length > 0 ? {t: "i", i: value} : undefined
|
||||
|
||||
const runAgentTool = Effect.fn("TrustGraphMcpToolkit.agent")(function*(
|
||||
gateway: TrustGraphGatewayClient,
|
||||
config: TrustGraphMcpConfigShape,
|
||||
question: string,
|
||||
) {
|
||||
let fullAnswer = ""
|
||||
let streamError: AgentError | undefined
|
||||
|
||||
yield* gateway.runDispatchStream(
|
||||
{
|
||||
scope: "flow",
|
||||
flow: config.flowId,
|
||||
service: "agent",
|
||||
request: {
|
||||
question,
|
||||
user: config.user,
|
||||
collection: "default",
|
||||
streaming: true,
|
||||
},
|
||||
},
|
||||
(chunk) => {
|
||||
const resp = asRecord(chunk.response)
|
||||
const chunkType = stringProperty(resp, "chunk_type")
|
||||
const error = chunkType === "error"
|
||||
? responseErrorMessage(resp) ?? "Unknown agent error"
|
||||
: responseErrorMessage(resp)
|
||||
if (error !== undefined) {
|
||||
streamError = AgentError.make({message: error})
|
||||
return true
|
||||
}
|
||||
|
||||
if (chunkType === "answer" || chunkType === "final-answer") {
|
||||
fullAnswer += stringProperty(resp, "content") ?? ""
|
||||
}
|
||||
|
||||
return chunk.complete === true || booleanProperty(resp, "end_of_dialog") === true
|
||||
},
|
||||
{timeoutMs: 120_000, retries: 2},
|
||||
).pipe(
|
||||
Effect.mapError((cause) => AgentError.make({message: toErrorMessage(cause)})),
|
||||
)
|
||||
|
||||
if (streamError !== undefined) {
|
||||
return yield* streamError
|
||||
}
|
||||
return AgentSuccess.make({text: fullAnswer})
|
||||
})
|
||||
|
||||
export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
|
||||
Effect.gen(function*() {
|
||||
const config = yield* TrustGraphMcpConfig
|
||||
const socket = yield* TrustGraphSocket
|
||||
const gateway = yield* TrustGraphGateway
|
||||
|
||||
return TrustGraphMcpToolkit.of({
|
||||
text_completion: ({system, prompt}) =>
|
||||
|
|
@ -1354,22 +1447,7 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
|
|||
Effect.map((text) => DocumentRagSuccess.make({text})),
|
||||
),
|
||||
|
||||
agent: ({question}) =>
|
||||
Effect.callback<AgentSuccess, AgentError>((resume) => {
|
||||
let fullAnswer = ""
|
||||
socket.flow(config.flowId).agent(
|
||||
question,
|
||||
() => {},
|
||||
() => {},
|
||||
(chunk, complete) => {
|
||||
fullAnswer += chunk
|
||||
if (complete) {
|
||||
resume(Effect.succeed(AgentSuccess.make({text: fullAnswer})))
|
||||
}
|
||||
},
|
||||
(cause) => resume(Effect.fail(AgentError.make({message: toErrorMessage(cause)}))),
|
||||
)
|
||||
}),
|
||||
agent: ({question}) => runAgentTool(gateway, config, question),
|
||||
|
||||
embeddings: ({text}) =>
|
||||
Effect.tryPromise({
|
||||
|
|
@ -1694,6 +1772,7 @@ const makeTrustGraphMcpHttpLayerFromConfig = (
|
|||
version: config.version,
|
||||
path: config.mcpPath,
|
||||
})),
|
||||
Layer.provide(TrustGraphGateway.layer),
|
||||
Layer.provide(TrustGraphSocket.layer),
|
||||
Layer.provide(Layer.succeed(TrustGraphMcpConfig, TrustGraphMcpConfig.of(config))),
|
||||
)
|
||||
|
|
@ -1713,6 +1792,7 @@ const makeTrustGraphMcpStdioLayerFromConfig = (
|
|||
version: config.version,
|
||||
})),
|
||||
Layer.provide(NodeStdio.layer),
|
||||
Layer.provide(TrustGraphGateway.layer),
|
||||
Layer.provide(TrustGraphSocket.layer),
|
||||
Layer.provide(Layer.succeed(TrustGraphMcpConfig, TrustGraphMcpConfig.of(config))),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,21 +10,11 @@
|
|||
"qa:browser": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/ai-anthropic": "4.0.0-beta.78",
|
||||
"@effect/ai-openai": "4.0.0-beta.78",
|
||||
"@effect/ai-openrouter": "4.0.0-beta.78",
|
||||
"@effect/atom-react": "4.0.0-beta.78",
|
||||
"@effect/openapi-generator": "4.0.0-beta.78",
|
||||
"@effect/opentelemetry": "4.0.0-beta.78",
|
||||
"@effect/platform-browser": "4.0.0-beta.78",
|
||||
"@effect/platform-bun": "4.0.0-beta.78",
|
||||
"@effect/platform-node": "4.0.0-beta.78",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.78",
|
||||
"@effect/tsgo": "0.14.0",
|
||||
"@effect/vitest": "4.0.0-beta.78",
|
||||
"@tanstack/react-query": "^5.75.0",
|
||||
"@trustgraph/client": "workspace:*",
|
||||
"clsx": "^2.1.0",
|
||||
"effect": "4.0.0-beta.78",
|
||||
"lucide-react": "^0.513.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
|
|
@ -32,8 +22,7 @@
|
|||
"react-force-graph-2d": "^1.29.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.6.0",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"zustand": "^5.0.0"
|
||||
"tailwind-merge": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/vitest": "4.0.0-beta.78",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -20,12 +20,12 @@ export function NotificationToasts() {
|
|||
if (notifications.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2" aria-live="polite">
|
||||
<div className="pointer-events-none fixed bottom-4 right-4 z-50 flex flex-col gap-2" aria-live="polite">
|
||||
{notifications.map((n) => (
|
||||
<div
|
||||
key={n.id}
|
||||
className={cn(
|
||||
"flex items-start gap-2 rounded-lg border px-4 py-3 text-sm shadow-lg",
|
||||
"pointer-events-none flex items-start gap-2 rounded-lg border px-4 py-3 text-sm shadow-lg",
|
||||
typeStyles[n.type],
|
||||
)}
|
||||
>
|
||||
|
|
@ -37,7 +37,7 @@ export function NotificationToasts() {
|
|||
</div>
|
||||
<button
|
||||
onClick={() => removeNotification(n.id)}
|
||||
className="shrink-0 opacity-60 hover:opacity-100"
|
||||
className="pointer-events-auto shrink-0 opacity-60 hover:opacity-100"
|
||||
aria-label="Dismiss notification"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
|
|
|
|||
|
|
@ -266,8 +266,10 @@ export default function SettingsPage() {
|
|||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (collectionForm.id.trim().length === 0) return;
|
||||
const collectionId = collectionForm.id.trim();
|
||||
if (collectionId.length === 0) return;
|
||||
createCollection(collectionForm);
|
||||
setField({ key: "collection", value: collectionId });
|
||||
setCollectionForm({ id: "", name: "", description: "", tags: "", submitting: false });
|
||||
setCreateOpen(false);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
type Settings,
|
||||
type WorkbenchApiFactory,
|
||||
} from "@/atoms/workbench";
|
||||
import type { BaseApi } from "@trustgraph/client";
|
||||
import { makeMockBaseApi, qaSettingsFromFixture, type MockWorkbenchFixture } from "@/qa/mock-api";
|
||||
|
||||
export interface WorkbenchQaWindowConfig {
|
||||
|
|
@ -19,6 +20,7 @@ export interface WorkbenchQaWindowConfig {
|
|||
declare global {
|
||||
interface Window {
|
||||
__TRUSTGRAPH_WORKBENCH_QA__?: WorkbenchQaWindowConfig;
|
||||
__TRUSTGRAPH_WORKBENCH_QA_API__?: BaseApi;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -43,6 +45,7 @@ export function getWorkbenchQaInitialValues(): Iterable<readonly [Atom.Atom<unkn
|
|||
const apiFactory: WorkbenchApiFactory = {
|
||||
create: () => api,
|
||||
};
|
||||
window.__TRUSTGRAPH_WORKBENCH_QA_API__ = api;
|
||||
return [
|
||||
[apiFactoryAtom as Atom.Atom<unknown>, apiFactory],
|
||||
[settingsAtom as Atom.Atom<unknown>, qaSettings(fixture)],
|
||||
|
|
|
|||
|
|
@ -25,19 +25,21 @@ export default defineConfig({
|
|||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
"/api/v1/rpc": {
|
||||
target: "ws://localhost:8088/",
|
||||
ws: true,
|
||||
server: isWorkbenchQa
|
||||
? {}
|
||||
: {
|
||||
proxy: {
|
||||
"/api/v1/rpc": {
|
||||
target: "ws://localhost:8088/",
|
||||
ws: true,
|
||||
},
|
||||
"/api/v1": {
|
||||
target: "http://localhost:8088/",
|
||||
},
|
||||
"/otel": {
|
||||
target: "http://localhost:4328/",
|
||||
rewrite: (p) => p.replace(/^\/otel/, ""),
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/v1": {
|
||||
target: "http://localhost:8088/",
|
||||
},
|
||||
"/otel": {
|
||||
target: "http://localhost:4328/",
|
||||
rewrite: (p) => p.replace(/^\/otel/, ""),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ const effectClassPatterns = [
|
|||
/\bContext\.Service\b/,
|
||||
/\bRpc\.make\b/,
|
||||
/\bHttpApi\.make\b/,
|
||||
/\bAtomHttpApi\.Service\b/,
|
||||
/\bAtomRpc\.Service\b/,
|
||||
/\bEffect\.Service\b/,
|
||||
];
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue