Advance TS port Effect workbench

This commit is contained in:
elpresidank 2026-06-01 16:22:25 -05:00
parent 92dae8c374
commit 3515106670
116 changed files with 12286 additions and 9584 deletions

View file

@ -21,6 +21,8 @@ import {
ConsumerSpec,
ProducerSpec,
RequestResponseSpec,
makeFlowProcessorProgram,
errorMessage,
type ProcessorConfig,
type FlowContext,
type AgentRequest,
@ -35,8 +37,18 @@ import {
type TriplesQueryResponse,
type ToolRequest,
type ToolResponse,
type EffectConfigHandler,
type EffectRequestOptions,
type EffectRequestResponse,
type FlowRequestOptions,
type FlowRequestor,
type FlowResourceNotFoundError,
type MessagingDeliveryError,
type Spec,
} from "@trustgraph/base";
import { makeProcessorProgram } from "@trustgraph/base";
import { Context, Effect, Layer, Ref } from "effect";
import * as O from "effect/Option";
import * as S from "effect/Schema";
import {
createKnowledgeQueryTool,
@ -51,398 +63,490 @@ import type { AgentTool, ToolArg } from "./types.js";
const MAX_ITERATIONS = 10;
export class AgentService extends FlowProcessor {
/** Config-driven tools; null means "use hardcoded fallback". */
private configuredTools: AgentTool[] | null = null;
class AgentToolExecutionError extends S.TaggedErrorClass<AgentToolExecutionError>()(
"AgentToolExecutionError",
{
message: S.String,
},
) {}
const UnknownRecord = S.Record(S.String, S.Unknown);
const ToolArgumentConfig = S.StructWithRest(
S.Struct({
name: S.optionalKey(S.String),
type: S.optionalKey(S.String),
description: S.optionalKey(S.String),
}),
[UnknownRecord],
);
const ToolConfigEntry = S.StructWithRest(
S.Struct({
type: S.optionalKey(S.String),
name: S.optionalKey(S.String),
description: S.optionalKey(S.String),
arguments: ToolArgumentConfig.pipe(S.Array, S.optionalKey),
}),
[UnknownRecord],
);
type ToolConfigEntry = typeof ToolConfigEntry.Type;
const decodeRawToolConfig = S.decodeUnknownOption(S.Record(S.String, S.String));
const decodeToolConfigEntry = S.decodeUnknownOption(ToolConfigEntry.pipe(S.fromJsonString));
export interface AgentRuntimeService {
readonly configureTools: (
config: Record<string, unknown>,
version: number,
) => Effect.Effect<void>;
readonly getConfiguredTools: Effect.Effect<ReadonlyArray<AgentTool> | null>;
}
export class AgentRuntime extends Context.Service<AgentRuntime, AgentRuntimeService>()(
"@trustgraph/flow/agent/react/service/AgentRuntime",
) {}
const toEffectRequestOptions = <TRes>(
options: FlowRequestOptions<TRes> | undefined,
): EffectRequestOptions<TRes> | undefined => {
if (options === undefined) return undefined;
return {
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
...(options.recipient === undefined
? {}
: {
recipient: (response: TRes) =>
Effect.promise(() => options.recipient?.(response) ?? Promise.resolve(true)),
}),
};
};
const toPromiseRequestor = <TReq, TRes>(
requestor: EffectRequestResponse<TReq, TRes>,
): FlowRequestor<TReq, TRes> => ({
request: (request, options) =>
Effect.runPromise(requestor.request(request, toEffectRequestOptions(options))),
stop: () => Effect.runPromise(requestor.stop),
});
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>;
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 () => "",
};
}
default:
console.warn(`[AgentService] Unknown tool type "${implType}" for ${name}`);
return null;
}
};
const loadConfiguredTools = Effect.fn("AgentRuntime.loadConfiguredTools")(function* (
config: Record<string, unknown>,
version: number,
) {
yield* Effect.log(`[AgentService] Loading tool configuration version ${version}`);
if (!("tool" in config) || typeof config.tool !== "object" || config.tool === null) {
yield* Effect.log("[AgentService] No tool config found, using default tools");
return null;
}
const rawConfig = decodeRawToolConfig(config.tool);
if (O.isNone(rawConfig)) {
yield* Effect.logError("[AgentService] Tool config must be an object of JSON strings");
return null;
}
const tools: AgentTool[] = [];
for (const [toolId, toolValue] of Object.entries(rawConfig.value)) {
const decoded = decodeToolConfigEntry(toolValue);
if (O.isNone(decoded)) {
yield* Effect.logError(`[AgentService] Failed to parse tool config ${toolId}`);
continue;
}
const tool = buildConfiguredTool(toolId, decoded.value);
if (tool === null) continue;
tools.push(tool);
yield* Effect.log(`[AgentService] Registered tool: ${tool.name} (${tool.config?.type ?? "unknown"})`);
}
yield* Effect.log(`[AgentService] ${tools.length} tools loaded from config`);
return tools.length > 0 ? tools : null;
});
export const makeAgentRuntime = Effect.gen(function* () {
const configuredToolsRef = yield* Ref.make<ReadonlyArray<AgentTool> | null>(null);
return AgentRuntime.of({
configureTools: Effect.fn("AgentRuntime.configureTools")(function* (config, version) {
const tools = yield* loadConfiguredTools(config, version);
yield* Ref.set(configuredToolsRef, tools);
}),
getConfiguredTools: Ref.get(configuredToolsRef),
});
});
export const AgentRuntimeLive = Layer.effect(AgentRuntime, makeAgentRuntime);
const onToolsConfig = Effect.fn("AgentService.onToolsConfig")(function* (
config: Record<string, unknown>,
version: number,
) {
const runtime = yield* AgentRuntime;
yield* runtime.configureTools(config, version);
});
const wireTools = Effect.fn("AgentService.wireTools")(function* (
tools: ReadonlyArray<AgentTool>,
flowCtx: FlowContext<AgentRuntime>,
collection: string | undefined,
onExplain: (data: ExplainData) => void,
) {
const graphRag = yield* flowCtx.flow.requestorEffect<GraphRagRequest, GraphRagResponse>("graph-rag");
const docRag = yield* flowCtx.flow.requestorEffect<DocumentRagRequest, DocumentRagResponse>("doc-rag");
const triples = yield* flowCtx.flow.requestorEffect<TriplesQueryRequest, TriplesQueryResponse>("triples");
const mcpTool = yield* flowCtx.flow.requestorEffect<ToolRequest, ToolResponse>("mcp-tool");
return tools.map((tool) => {
const implType = tool.config?.type as string | undefined;
switch (implType) {
case "knowledge-query": {
const live = createKnowledgeQueryTool(
toPromiseRequestor(graphRag),
collection,
onExplain,
);
return { ...tool, execute: live.execute };
}
case "document-query": {
const live = createDocumentQueryTool(
toPromiseRequestor(docRag),
collection,
);
return { ...tool, execute: live.execute };
}
case "triples-query": {
const live = createTriplesQueryTool(
toPromiseRequestor(triples),
collection,
);
return { ...tool, execute: live.execute };
}
case "mcp-tool": {
const live = createMcpTool(
toPromiseRequestor(mcpTool),
tool.name,
tool.description,
tool.args,
);
return { ...tool, execute: live.execute };
}
default:
return tool;
}
});
});
const defaultTools = Effect.fn("AgentService.defaultTools")(function* (
flowCtx: FlowContext<AgentRuntime>,
collection: string | undefined,
onExplain: (data: ExplainData) => void,
) {
const graphRag = yield* flowCtx.flow.requestorEffect<GraphRagRequest, GraphRagResponse>("graph-rag");
const docRag = yield* flowCtx.flow.requestorEffect<DocumentRagRequest, DocumentRagResponse>("doc-rag");
const triples = yield* flowCtx.flow.requestorEffect<TriplesQueryRequest, TriplesQueryResponse>("triples");
return [
createKnowledgeQueryTool(
toPromiseRequestor(graphRag),
collection,
onExplain,
),
createDocumentQueryTool(
toPromiseRequestor(docRag),
collection,
),
createTriplesQueryTool(
toPromiseRequestor(triples),
collection,
),
];
});
const executeTool = (
tool: AgentTool,
input: string,
): Effect.Effect<string> =>
Effect.tryPromise({
try: () => tool.execute(input),
catch: (cause) => new AgentToolExecutionError({ message: errorMessage(cause) }),
}).pipe(
Effect.catch((error: AgentToolExecutionError) =>
Effect.succeed(`Error executing tool: ${error.message}`),
),
);
type AgentHandlerError =
| FlowResourceNotFoundError
| MessagingDeliveryError;
const onAgentRequest = Effect.fn("AgentService.onRequest")(function* (
msg: AgentRequest,
properties: Record<string, string>,
flowCtx: FlowContext<AgentRuntime>,
): Effect.fn.Return<void, AgentHandlerError, AgentRuntime> {
const requestId = properties.id;
if (requestId === undefined || requestId.length === 0) return;
const responseProducer = yield* flowCtx.flow.producerEffect<AgentResponse>("agent-response");
yield* Effect.gen(function* () {
const runtime = yield* AgentRuntime;
const explainEvents: ExplainData[] = [];
const onExplain = (data: ExplainData) => {
explainEvents.push(data);
};
const configuredTools = yield* runtime.getConfiguredTools;
let tools = configuredTools !== null
? yield* wireTools(configuredTools, flowCtx, msg.collection, onExplain)
: yield* defaultTools(flowCtx, msg.collection, onExplain);
tools = filterToolsByGroupAndState(tools, msg.group, msg.state);
const { system, prompt: initialPrompt } = buildReActPrompt(
tools,
msg.question,
);
const llmClient = yield* flowCtx.flow.requestorEffect<
TextCompletionRequest,
TextCompletionResponse
>("llm");
let conversation = initialPrompt;
for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
yield* Effect.log(
`[AgentService] Iteration ${iteration + 1}/${MAX_ITERATIONS} for request ${requestId}`,
);
const llmResponse = yield* llmClient.request({
system,
prompt: conversation,
});
if (llmResponse.error !== undefined) {
yield* responseProducer.send(requestId, {
chunk_type: "error",
content: `LLM error: ${llmResponse.error.message}`,
end_of_dialog: true,
});
return;
}
const text = llmResponse.response;
const parsed = parseReActResponse(text);
if (parsed.thought.length > 0) {
yield* responseProducer.send(requestId, {
chunk_type: "thought",
content: parsed.thought,
end_of_message: true,
});
}
if (parsed.finalAnswer.length > 0) {
for (const explain of explainEvents) {
yield* responseProducer.send(requestId, {
chunk_type: "explain",
content: "",
explain_id: explain.explainId,
explain_triples: explain.triples,
} as AgentResponse);
}
yield* responseProducer.send(requestId, {
chunk_type: "answer",
content: parsed.finalAnswer,
end_of_message: true,
end_of_dialog: true,
});
return;
}
if (parsed.action.length > 0 && parsed.actionInput.length > 0) {
const tool = tools.find((candidate) => candidate.name === parsed.action);
const observation = tool === undefined
? `Unknown tool: ${parsed.action}. Available tools: ${tools.map((candidate) => candidate.name).join(", ")}`
: yield* executeTool(tool, parsed.actionInput);
yield* responseProducer.send(requestId, {
chunk_type: "observation",
content: observation,
end_of_message: true,
});
conversation += `\n${text}\nObservation: ${observation}\n`;
} else if (parsed.finalAnswer.length === 0) {
conversation += `\n${text}\nObservation: You must either use a tool (Action + Action Input) or provide a Final Answer.\n`;
}
}
yield* responseProducer.send(requestId, {
chunk_type: "error",
content:
"Maximum reasoning iterations reached without a final answer. " +
"The agent was unable to complete the task within the allowed steps.",
end_of_message: true,
end_of_dialog: true,
});
}).pipe(
Effect.catch((error: unknown) =>
Effect.logError(`[AgentService] Error processing request ${requestId}`, {
error: error instanceof Error ? error.message : String(error),
}).pipe(
Effect.flatMap(() =>
responseProducer.send(requestId, {
chunk_type: "error",
content: `Agent error: ${error instanceof Error ? error.message : String(error)}`,
end_of_message: true,
end_of_dialog: true,
}),
),
),
),
);
});
export const makeAgentSpecs = (): ReadonlyArray<Spec<AgentRuntime>> => [
new ConsumerSpec<AgentRequest, AgentHandlerError, AgentRuntime>(
"agent-request",
onAgentRequest,
),
new ProducerSpec<AgentResponse>("agent-response"),
new RequestResponseSpec<TextCompletionRequest, TextCompletionResponse>(
"llm",
"text-completion-request",
"text-completion-response",
),
new RequestResponseSpec<GraphRagRequest, GraphRagResponse>(
"graph-rag",
"graph-rag-request",
"graph-rag-response",
),
new RequestResponseSpec<DocumentRagRequest, DocumentRagResponse>(
"doc-rag",
"document-rag-request",
"document-rag-response",
),
new RequestResponseSpec<TriplesQueryRequest, TriplesQueryResponse>(
"triples",
"triples-request",
"triples-response",
),
new RequestResponseSpec<ToolRequest, ToolResponse>(
"mcp-tool",
"mcp-tool-request",
"mcp-tool-response",
),
];
export const makeAgentConfigHandlers = (): ReadonlyArray<
EffectConfigHandler<never, AgentRuntime>
> => [onToolsConfig];
export class AgentService extends FlowProcessor<AgentRuntime> {
private readonly runtime = Effect.runSync(makeAgentRuntime);
constructor(config: ProcessorConfig) {
super(config);
// Consumer: agent requests
this.registerSpecification(
ConsumerSpec.fromPromise<AgentRequest>("agent-request", this.onRequest.bind(this)),
);
for (const spec of makeAgentSpecs()) {
this.registerSpecification(spec);
}
// Producer: agent responses (streaming chunks)
this.registerSpecification(new ProducerSpec<AgentResponse>("agent-response"));
// Request-response clients for tool execution
this.registerSpecification(
new RequestResponseSpec<TextCompletionRequest, TextCompletionResponse>(
"llm",
"text-completion-request",
"text-completion-response",
),
this.registerConfigHandler((config, version) =>
Effect.runPromise(onToolsConfig(config, version).pipe(
Effect.provideService(AgentRuntime, this.runtime),
)),
);
this.registerSpecification(
new RequestResponseSpec<GraphRagRequest, GraphRagResponse>(
"graph-rag",
"graph-rag-request",
"graph-rag-response",
),
);
this.registerSpecification(
new RequestResponseSpec<DocumentRagRequest, DocumentRagResponse>(
"doc-rag",
"document-rag-request",
"document-rag-response",
),
);
this.registerSpecification(
new RequestResponseSpec<TriplesQueryRequest, TriplesQueryResponse>(
"triples",
"triples-request",
"triples-response",
),
);
// MCP tool invocation client
this.registerSpecification(
new RequestResponseSpec<ToolRequest, ToolResponse>(
"mcp-tool",
"mcp-tool-request",
"mcp-tool-response",
),
);
// Register for config-push to build tools dynamically
this.registerConfigHandler(this.onToolsConfig.bind(this));
console.log("[AgentService] Service initialized");
}
// ---------- Config-driven tool registration ----------
private async onToolsConfig(
config: Record<string, unknown>,
version: number,
): Promise<void> {
console.log(`[AgentService] Loading tool configuration version ${version}`);
try {
if (!("tool" in config) || typeof config.tool !== "object" || config.tool === null) {
// No tool config — keep using hardcoded fallback
this.configuredTools = null;
console.log("[AgentService] No tool config found, using default tools");
return;
}
const toolConfig = config.tool as Record<string, string>;
const tools: AgentTool[] = [];
for (const [_toolId, toolValue] of Object.entries(toolConfig)) {
try {
const data = JSON.parse(toolValue) as Record<string, unknown>;
const implType = typeof data["type"] === "string" ? data["type"] : "";
const name = typeof data["name"] === "string" ? data["name"] : "";
const description =
typeof data["description"] === "string" ? data["description"] : "";
if (name.length === 0) {
console.warn(`[AgentService] Skipping tool with no name: ${_toolId}`);
continue;
}
let tool: AgentTool | null = null;
switch (implType) {
case "knowledge-query":
// Will be wired to requestor at request time
tool = {
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: data,
execute: async () => "", // placeholder — wired at request time
};
break;
case "document-query":
tool = {
name,
description:
description.length > 0
? description
: "Search documents for relevant information.",
args: [{ name: "question", type: "string", description: "The question to search for" }],
config: data,
execute: async () => "",
};
break;
case "triples-query":
tool = {
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: data,
execute: async () => "",
};
break;
case "mcp-tool": {
const configArgs = (data["arguments"] as Array<Record<string, string>>) ?? [];
const args: ToolArg[] = configArgs.map((a) => ({
name: a.name ?? "",
type: a.type ?? "string",
description: a.description ?? "",
}));
// Create a placeholder — will be wired to the MCP requestor at request time
tool = {
name,
description,
args,
config: data,
execute: async () => "", // placeholder
};
break;
}
default:
console.warn(`[AgentService] Unknown tool type "${implType}" for ${name}`);
continue;
}
if (tool !== null) {
tools.push(tool);
console.log(`[AgentService] Registered tool: ${name} (${implType})`);
}
} catch (err) {
console.error(`[AgentService] Failed to parse tool config ${_toolId}:`, err);
}
}
this.configuredTools = tools.length > 0 ? tools : null;
console.log(`[AgentService] ${tools.length} tools loaded from config`);
} catch (err) {
console.error("[AgentService] Config reload failed:", err);
}
}
/**
* Wire up tool execute functions with live requestors from the flow context.
* Config-driven tools store placeholders; this replaces them with real impls.
*/
private wireTools(
tools: AgentTool[],
flowCtx: FlowContext,
collection?: string,
onExplain?: (data: ExplainData) => void,
): AgentTool[] {
return tools.map((tool) => {
const implType = tool.config?.["type"] as string | undefined;
switch (implType) {
case "knowledge-query": {
const live = createKnowledgeQueryTool(
flowCtx.flow.requestor<GraphRagRequest, GraphRagResponse>("graph-rag"),
collection,
onExplain,
);
return { ...tool, execute: live.execute };
}
case "document-query": {
const live = createDocumentQueryTool(
flowCtx.flow.requestor<DocumentRagRequest, DocumentRagResponse>("doc-rag"),
collection,
);
return { ...tool, execute: live.execute };
}
case "triples-query": {
const live = createTriplesQueryTool(
flowCtx.flow.requestor<TriplesQueryRequest, TriplesQueryResponse>("triples"),
collection,
);
return { ...tool, execute: live.execute };
}
case "mcp-tool": {
const live = createMcpTool(
flowCtx.flow.requestor<ToolRequest, ToolResponse>("mcp-tool"),
tool.name,
tool.description,
tool.args,
);
return { ...tool, execute: live.execute };
}
default:
return tool;
}
});
}
private async onRequest(
msg: AgentRequest,
properties: Record<string, string>,
flowCtx: FlowContext,
): Promise<void> {
const requestId = properties.id;
if (requestId === undefined || requestId.length === 0) return;
const responseProducer = flowCtx.flow.producer<AgentResponse>("agent-response");
try {
// Accumulate explain data from tool calls for emission after completion
const explainEvents: ExplainData[] = [];
const onExplain = (data: ExplainData) => {
explainEvents.push(data);
};
// Build tools — config-driven or hardcoded fallback
let tools: AgentTool[];
if (this.configuredTools !== null) {
tools = this.wireTools(this.configuredTools, flowCtx, msg.collection, onExplain);
} else {
// Hardcoded fallback (backward compat)
tools = [
createKnowledgeQueryTool(
flowCtx.flow.requestor<GraphRagRequest, GraphRagResponse>("graph-rag"),
msg.collection,
onExplain,
),
createDocumentQueryTool(
flowCtx.flow.requestor<DocumentRagRequest, DocumentRagResponse>("doc-rag"),
msg.collection,
),
createTriplesQueryTool(
flowCtx.flow.requestor<TriplesQueryRequest, TriplesQueryResponse>("triples"),
msg.collection,
),
];
}
// Apply tool filtering by group and state
tools = filterToolsByGroupAndState(tools, msg.group, msg.state);
// Build the ReAct prompt
const { system, prompt: initialPrompt } = buildReActPrompt(
tools,
msg.question,
);
const llmClient = flowCtx.flow.requestor<
TextCompletionRequest,
TextCompletionResponse
>("llm");
// Conversation accumulates the full exchange for multi-turn reasoning
let conversation = initialPrompt;
for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
console.log(
`[AgentService] Iteration ${iteration + 1}/${MAX_ITERATIONS} for request ${requestId}`,
);
// Call LLM (non-streaming for MVP)
const llmResponse = await llmClient.request({
system,
prompt: conversation,
});
if (llmResponse.error !== undefined) {
await responseProducer.send(requestId, {
chunk_type: "error",
content: `LLM error: ${llmResponse.error.message}`,
end_of_dialog: true,
});
return;
}
const text = llmResponse.response;
// Parse the LLM response with simple line-based parsing
const parsed = parseReActResponse(text);
// Send thought chunk
if (parsed.thought.length > 0) {
await responseProducer.send(requestId, {
chunk_type: "thought",
content: parsed.thought,
end_of_message: true,
});
}
// If we got a final answer, emit explain events then send the answer
if (parsed.finalAnswer.length > 0) {
// Emit explain events collected from tool calls
for (const explain of explainEvents) {
await responseProducer.send(requestId, {
chunk_type: "explain",
content: "",
explain_id: explain.explainId,
explain_triples: explain.triples,
} as AgentResponse);
}
await responseProducer.send(requestId, {
chunk_type: "answer",
content: parsed.finalAnswer,
end_of_message: true,
end_of_dialog: true,
});
return;
}
// Execute tool if action was specified
if (parsed.action.length > 0 && parsed.actionInput.length > 0) {
const tool = tools.find((t) => t.name === parsed.action);
let observation: string;
if (tool !== undefined) {
try {
observation = await tool.execute(parsed.actionInput);
} catch (err) {
observation = `Error executing tool: ${err instanceof Error ? err.message : String(err)}`;
}
} else {
observation = `Unknown tool: ${parsed.action}. Available tools: ${tools.map((t) => t.name).join(", ")}`;
}
// Send observation chunk
await responseProducer.send(requestId, {
chunk_type: "observation",
content: observation,
end_of_message: true,
});
// Append the full exchange to conversation for the next iteration
conversation += `\n${text}\nObservation: ${observation}\n`;
} else if (parsed.finalAnswer.length === 0) {
// LLM didn't produce a valid action or final answer -- nudge it
conversation += `\n${text}\nObservation: You must either use a tool (Action + Action Input) or provide a Final Answer.\n`;
}
}
// Max iterations reached without a final answer
await responseProducer.send(requestId, {
chunk_type: "error",
content:
"Maximum reasoning iterations reached without a final answer. " +
"The agent was unable to complete the task within the allowed steps.",
end_of_message: true,
end_of_dialog: true,
});
} catch (err) {
console.error(`[AgentService] Error processing request ${requestId}:`, err);
await responseProducer.send(requestId, {
chunk_type: "error",
content: `Agent error: ${err instanceof Error ? err.message : String(err)}`,
end_of_message: true,
end_of_dialog: true,
});
}
override startEffect() {
return super.startEffect().pipe(
Effect.provideService(AgentRuntime, this.runtime),
);
}
}
@ -524,11 +628,13 @@ function parseReActResponse(text: string): {
};
}
export const program = makeProcessorProgram({
export const program = makeFlowProcessorProgram<ProcessorConfig, never, AgentRuntime>({
id: "agent",
make: (config) => new AgentService(config),
specs: () => makeAgentSpecs(),
configHandlers: () => makeAgentConfigHandlers(),
layer: () => AgentRuntimeLive,
});
export async function run(): Promise<void> {
await AgentService.launch("agent");
await Effect.runPromise(program);
}