Enforce strict Effect tsgo migrations

This commit is contained in:
elpresidank 2026-06-01 23:19:54 -05:00
parent 64fb23e7d0
commit f6878d4dd7
49 changed files with 5547 additions and 3250 deletions

View file

@ -75,7 +75,7 @@ const mcpToolError = (
cause: unknown,
tool?: string,
): McpToolError =>
new McpToolError({
McpToolError.make({
operation,
message: errorMessage(cause),
...(tool === undefined ? {} : { tool }),
@ -166,12 +166,9 @@ const invokeConfiguredTool = Effect.fn("McpToolRuntime.invokeTool")(function* (
const result = yield* Effect.acquireUseRelease(
Effect.tryPromise({
try: async () => {
await client.connect(transport as unknown as Parameters<Client["connect"]>[0]);
return client;
},
try: () => client.connect(transport as unknown as Parameters<Client["connect"]>[0]),
catch: (cause) => mcpToolError("connect", cause, name),
}),
}).pipe(Effect.as(client)),
(connectedClient) =>
Effect.tryPromise({
try: () =>
@ -318,6 +315,6 @@ export const program = makeFlowProcessorProgram<ProcessorConfig, never, McpToolR
layer: () => McpToolRuntimeLive,
});
export async function run(): Promise<void> {
await Effect.runPromise(program);
export function run(): Promise<void> {
return Effect.runPromise(program);
}

View file

@ -132,79 +132,80 @@ const toPromiseRequestor = <TReq, TRes>(
const buildConfiguredTool = (
toolId: string,
data: ToolConfigEntry,
): AgentTool | null => {
const implType = data.type ?? "";
const name = data.name ?? "";
const description = data.description ?? "";
const config = { ...data } as Record<string, unknown>;
): Effect.Effect<AgentTool | null> =>
Effect.gen(function* () {
const implType = data.type ?? "";
const name = data.name ?? "";
const description = data.description ?? "";
const config = { ...data } as Record<string, unknown>;
if (name.length === 0) {
console.warn(`[AgentService] Skipping tool with no name: ${toolId}`);
return null;
}
switch (implType) {
case "knowledge-query":
return {
name,
description:
description.length > 0
? description
: "Query the knowledge graph for information about entities and their relationships.",
args: [{ name: "question", type: "string", description: "The question to ask" }],
config,
execute: async () => "",
};
case "document-query":
return {
name,
description:
description.length > 0
? description
: "Search documents for relevant information.",
args: [{ name: "question", type: "string", description: "The question to search for" }],
config,
execute: async () => "",
};
case "triples-query":
return {
name,
description:
description.length > 0
? description
: "Query for specific triples in the knowledge graph.",
args: [
{ name: "subject", type: "string", description: "Subject entity (optional)" },
{ name: "predicate", type: "string", description: "Predicate/relationship (optional)" },
{ name: "object", type: "string", description: "Object entity (optional)" },
],
config,
execute: async () => "",
};
case "mcp-tool": {
const args: ToolArg[] = (data.arguments ?? []).map((arg) => ({
name: arg.name ?? "",
type: arg.type ?? "string",
description: arg.description ?? "",
}));
return {
name,
description,
args,
config,
execute: async () => "",
};
if (name.length === 0) {
yield* Effect.logWarning(`[AgentService] Skipping tool with no name: ${toolId}`);
return null;
}
default:
console.warn(`[AgentService] Unknown tool type "${implType}" for ${name}`);
return null;
}
};
switch (implType) {
case "knowledge-query":
return {
name,
description:
description.length > 0
? description
: "Query the knowledge graph for information about entities and their relationships.",
args: [{ name: "question", type: "string", description: "The question to ask" }],
config,
execute: () => Promise.resolve(""),
};
case "document-query":
return {
name,
description:
description.length > 0
? description
: "Search documents for relevant information.",
args: [{ name: "question", type: "string", description: "The question to search for" }],
config,
execute: () => Promise.resolve(""),
};
case "triples-query":
return {
name,
description:
description.length > 0
? description
: "Query for specific triples in the knowledge graph.",
args: [
{ name: "subject", type: "string", description: "Subject entity (optional)" },
{ name: "predicate", type: "string", description: "Predicate/relationship (optional)" },
{ name: "object", type: "string", description: "Object entity (optional)" },
],
config,
execute: () => Promise.resolve(""),
};
case "mcp-tool": {
const args: ToolArg[] = (data.arguments ?? []).map((arg) => ({
name: arg.name ?? "",
type: arg.type ?? "string",
description: arg.description ?? "",
}));
return {
name,
description,
args,
config,
execute: () => Promise.resolve(""),
};
}
default:
yield* Effect.logWarning(`[AgentService] Unknown tool type "${implType}" for ${name}`);
return null;
}
});
const loadConfiguredTools = Effect.fn("AgentRuntime.loadConfiguredTools")(function* (
config: Record<string, unknown>,
@ -231,7 +232,7 @@ const loadConfiguredTools = Effect.fn("AgentRuntime.loadConfiguredTools")(functi
continue;
}
const tool = buildConfiguredTool(toolId, decoded.value);
const tool = yield* buildConfiguredTool(toolId, decoded.value);
if (tool === null) continue;
tools.push(tool);
@ -348,7 +349,7 @@ const executeTool = (
): Effect.Effect<string> =>
Effect.tryPromise({
try: () => tool.execute(input),
catch: (cause) => new AgentToolExecutionError({ message: errorMessage(cause) }),
catch: (cause) => AgentToolExecutionError.make({ message: errorMessage(cause) }),
}).pipe(
Effect.catch((error: AgentToolExecutionError) =>
Effect.succeed(`Error executing tool: ${error.message}`),
@ -473,12 +474,12 @@ const onAgentRequest = Effect.fn("AgentService.onRequest")(function* (
}).pipe(
Effect.catch((error: unknown) =>
Effect.logError(`[AgentService] Error processing request ${requestId}`, {
error: error instanceof Error ? error.message : String(error),
error: errorMessage(error),
}).pipe(
Effect.flatMap(() =>
responseProducer.send(requestId, {
chunk_type: "error",
content: `Agent error: ${error instanceof Error ? error.message : String(error)}`,
content: `Agent error: ${errorMessage(error)}`,
end_of_message: true,
end_of_dialog: true,
}),
@ -538,7 +539,7 @@ export function makeAgentService(config: ProcessorConfig): AgentService {
Effect.provideService(AgentRuntime, runtime),
)),
);
console.log("[AgentService] Service initialized");
Effect.runSync(Effect.log("[AgentService] Service initialized"));
return service;
}
@ -629,6 +630,6 @@ export const program = makeFlowProcessorProgram<ProcessorConfig, never, AgentRun
layer: () => AgentRuntimeLive,
});
export async function run(): Promise<void> {
await Effect.runPromise(program);
export function run(): Promise<void> {
return Effect.runPromise(program);
}

View file

@ -18,9 +18,14 @@ import type {
Term,
Triple,
} from "@trustgraph/base";
import { Effect } from "effect";
import * as O from "effect/Option";
import * as S from "effect/Schema";
import type { AgentTool, ToolArg } from "./types.js";
const decodeJsonUnknown = S.decodeUnknownOption(S.UnknownFromJsonString);
/**
* Format a Term to a human-readable string.
*/
@ -41,17 +46,15 @@ function termToString(term: Term): string {
* Parse tool input -- accepts either raw JSON or a plain string question.
*/
function parseQuestion(input: string): string {
try {
const parsed = JSON.parse(input) as Record<string, unknown>;
if (typeof parsed === "object" && parsed !== null && "question" in parsed) {
return String(parsed.question);
}
// If it's a string JSON value, use it directly
if (typeof parsed === "string") {
return parsed;
}
} catch {
// Not valid JSON -- treat as plain text
const decoded = decodeJsonUnknown(input);
if (O.isNone(decoded)) return input;
const parsed = decoded.value;
if (typeof parsed === "object" && parsed !== null && "question" in parsed) {
return String(parsed.question);
}
if (typeof parsed === "string") {
return parsed;
}
return input;
}
@ -83,15 +86,15 @@ export function createKnowledgeQueryTool(
description: "The question to ask the knowledge graph",
},
],
async execute(input: string): Promise<string> {
execute: (input: string): Promise<string> => Effect.runPromise(Effect.gen(function* () {
const question = parseQuestion(input);
console.log(`[KnowledgeQuery] Executing: "${question.slice(0, 60)}..." collection=${collection}`);
yield* Effect.log(`[KnowledgeQuery] Executing: "${question.slice(0, 60)}..." collection=${collection}`);
const request: GraphRagRequest = {
query: question,
...(collection !== undefined ? { collection } : {}),
};
const res = await client.request(request);
console.log(`[KnowledgeQuery] Response (${res.response?.length ?? 0} chars): ${res.error !== undefined ? `ERROR: ${res.error.message}` : `${res.response?.slice(0, 300)}...`}`);
const res = yield* Effect.tryPromise(() => client.request(request));
yield* Effect.log(`[KnowledgeQuery] Response (${res.response?.length ?? 0} chars): ${res.error !== undefined ? `ERROR: ${res.error.message}` : `${res.response?.slice(0, 300)}...`}`);
// Extract explain data if embedded in the response
const rawRes = res as Record<string, unknown>;
@ -100,15 +103,15 @@ export function createKnowledgeQueryTool(
rawRes.explain_triples !== undefined &&
onExplain !== undefined
) {
onExplain({
yield* Effect.sync(() => onExplain({
explainId: (rawRes.explain_id as string) ?? "",
triples: rawRes.explain_triples as Triple[],
});
}));
}
if (res.error !== undefined) return `Error: ${res.error.message}`;
return res.response;
},
})),
};
}
@ -130,16 +133,16 @@ export function createDocumentQueryTool(
description: "The question to search documents for",
},
],
async execute(input: string): Promise<string> {
execute: (input: string): Promise<string> => Effect.runPromise(Effect.gen(function* () {
const question = parseQuestion(input);
const request: DocumentRagRequest = {
query: question,
...(collection !== undefined ? { collection } : {}),
};
const res = await client.request(request);
const res = yield* Effect.tryPromise(() => client.request(request));
if (res.error !== undefined) return `Error: ${res.error.message}`;
return res.response;
},
})),
};
}
@ -152,39 +155,42 @@ function parseTriplesInput(input: string): {
o?: Term;
limit?: number;
} {
try {
const parsed = JSON.parse(input) as Record<string, unknown>;
const toTerm = (val: unknown): Term | undefined => {
if (typeof val === "string") {
return { type: "LITERAL", value: val };
}
if (typeof val === "object" && val !== null && "type" in val) {
return val as Term;
}
return undefined;
};
const result: {
s?: Term;
p?: Term;
o?: Term;
limit?: number;
} = {};
const s = toTerm(parsed.subject ?? parsed.s);
const p = toTerm(parsed.predicate ?? parsed.p);
const o = toTerm(parsed.object ?? parsed.o);
if (s !== undefined) result.s = s;
if (p !== undefined) result.p = p;
if (o !== undefined) result.o = o;
if (typeof parsed.limit === "number") result.limit = parsed.limit;
return result;
} catch {
// If not valid JSON, treat as a subject search
const decoded = decodeJsonUnknown(input);
if (
O.isNone(decoded) ||
typeof decoded.value !== "object" ||
decoded.value === null
) {
return {
s: { type: "LITERAL", value: input },
};
}
const parsed = decoded.value as Record<string, unknown>;
const toTerm = (val: unknown): Term | undefined => {
if (typeof val === "string") {
return { type: "LITERAL", value: val };
}
if (typeof val === "object" && val !== null && "type" in val) {
return val as Term;
}
return undefined;
};
const result: {
s?: Term;
p?: Term;
o?: Term;
limit?: number;
} = {};
const s = toTerm(parsed.subject ?? parsed.s);
const p = toTerm(parsed.predicate ?? parsed.p);
const o = toTerm(parsed.object ?? parsed.o);
if (s !== undefined) result.s = s;
if (p !== undefined) result.p = p;
if (o !== undefined) result.o = o;
if (typeof parsed.limit === "number") result.limit = parsed.limit;
return result;
}
/**
@ -216,7 +222,7 @@ export function createTriplesQueryTool(
description: "The object entity to search for (optional)",
},
],
async execute(input: string): Promise<string> {
execute: (input: string): Promise<string> => Effect.runPromise(Effect.gen(function* () {
const { s, p, o, limit } = parseTriplesInput(input);
const request: TriplesQueryRequest = {
limit: limit ?? 20,
@ -225,7 +231,7 @@ export function createTriplesQueryTool(
...(o !== undefined ? { o } : {}),
...(collection !== undefined ? { collection } : {}),
};
const res = await client.request(request);
const res = yield* Effect.tryPromise(() => client.request(request));
if (res.error !== undefined) return `Error: ${res.error.message}`;
@ -238,7 +244,7 @@ export function createTriplesQueryTool(
`(${termToString(t.s)}) -[${termToString(t.p)}]-> (${termToString(t.o)})`,
);
return lines.join("\n");
},
})),
};
}
@ -258,12 +264,12 @@ export function createMcpTool(
name: toolName,
description,
args,
async execute(input: string): Promise<string> {
const res = await client.request({ name: toolName, parameters: input });
execute: (input: string): Promise<string> => Effect.runPromise(Effect.gen(function* () {
const res = yield* Effect.tryPromise(() => client.request({ name: toolName, parameters: input }));
if (res.error !== undefined) return `Error: ${res.error.message}`;
if (res.text !== undefined) return res.text;
if (res.object !== undefined) return res.object;
return "No content";
},
})),
};
}