Remove native classes from TS runtime

This commit is contained in:
elpresidank 2026-06-01 20:26:47 -05:00
parent 952daf325d
commit dca2786828
79 changed files with 7622 additions and 6703 deletions

View file

@ -14,13 +14,14 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import {
FlowProcessor,
ConsumerSpec,
ProducerSpec,
makeFlowProcessor,
makeConsumerSpec,
makeProducerSpec,
makeFlowProcessorProgram,
errorMessage,
type ProcessorConfig,
type FlowContext,
type FlowProcessorRuntime,
type ToolRequest,
type ToolResponse,
type EffectConfigHandler,
@ -281,41 +282,35 @@ const onMcpToolRequest = Effect.fn("McpToolService.onRequest")(function* (
});
export const makeMcpToolSpecs = (): ReadonlyArray<Spec<McpToolRuntime>> => [
new ConsumerSpec<ToolRequest, McpToolHandlerError, McpToolRuntime>(
makeConsumerSpec<ToolRequest, McpToolHandlerError, McpToolRuntime>(
"mcp-tool-request",
onMcpToolRequest,
),
new ProducerSpec<ToolResponse>("mcp-tool-response"),
makeProducerSpec<ToolResponse>("mcp-tool-response"),
];
export const makeMcpToolConfigHandlers = (): ReadonlyArray<
EffectConfigHandler<never, McpToolRuntime>
> => [onMcpConfig];
export class McpToolService extends FlowProcessor<McpToolRuntime> {
private readonly runtime = Effect.runSync(makeMcpToolRuntime);
export type McpToolService = FlowProcessorRuntime<McpToolRuntime>;
constructor(config: ProcessorConfig) {
super(config);
for (const spec of makeMcpToolSpecs()) {
this.registerSpecification(spec);
}
this.registerConfigHandler((config, version) =>
Effect.runPromise(onMcpConfig(config, version).pipe(
Effect.provideService(McpToolRuntime, this.runtime),
)),
);
}
override startEffect() {
return super.startEffect().pipe(
Effect.provideService(McpToolRuntime, this.runtime),
);
}
export function makeMcpToolService(config: ProcessorConfig): McpToolService {
const runtime = Effect.runSync(makeMcpToolRuntime);
const service = makeFlowProcessor(config, {
specifications: makeMcpToolSpecs(),
provide: (effect) => effect.pipe(Effect.provideService(McpToolRuntime, runtime)),
});
service.registerConfigHandler((pushedConfig, version) =>
Effect.runPromise(onMcpConfig(pushedConfig, version).pipe(
Effect.provideService(McpToolRuntime, runtime),
)),
);
return service;
}
export const McpToolService = makeMcpToolService;
export const program = makeFlowProcessorProgram<ProcessorConfig, never, McpToolRuntime>({
id: "mcp-tool",
specs: () => makeMcpToolSpecs(),

View file

@ -1,7 +1,7 @@
// ReAct agent -- barrel exports
export { AgentService } from "./service.js";
export { StreamingReActParser } from "./parser.js";
export { makeStreamingReActParser, type StreamingReActParser } from "./parser.js";
export { buildReActPrompt } from "./prompt.js";
export {
createKnowledgeQueryTool,

View file

@ -22,57 +22,75 @@ const MARKERS = [
// Longest marker prefix for partial-match detection
const MAX_MARKER_LEN = Math.max(...MARKERS.map((m) => m.prefix.length));
export class StreamingReActParser {
private state: ReActState = "initial";
private buffer = "";
private onThought: (text: string) => void;
private onAction: (name: string) => void;
private onActionInput: (input: string) => void;
private onFinalAnswer: (text: string) => void;
export interface StreamingReActParser {
readonly feed: (text: string) => void;
readonly flush: () => void;
}
constructor(
onThought: (text: string) => void,
onAction: (name: string) => void,
onActionInput: (input: string) => void,
onFinalAnswer: (text: string) => void,
) {
this.onThought = onThought;
this.onAction = onAction;
this.onActionInput = onActionInput;
this.onFinalAnswer = onFinalAnswer;
}
export function makeStreamingReActParser(
onThought: (text: string) => void,
onAction: (name: string) => void,
onActionInput: (input: string) => void,
onFinalAnswer: (text: string) => void,
): StreamingReActParser {
let state: ReActState = "initial";
let buffer = "";
/**
* Feed a chunk of LLM output text into the parser.
* Accumulates in a buffer and processes complete lines.
*/
feed(text: string): void {
this.buffer += text;
this.processBuffer(false);
}
const emitContent = (content: string): void => {
if (content.length === 0) return;
/**
* Flush any remaining buffered content at the end of output.
*/
flush(): void {
this.processBuffer(true);
// Emit any remaining buffer content in the current state
if (this.buffer.trim().length > 0) {
this.emitContent(this.buffer);
this.buffer = "";
switch (state) {
case "thought":
onThought(content);
break;
case "action":
onAction(content);
break;
case "action_input":
onActionInput(content);
break;
case "final_answer":
onFinalAnswer(content);
break;
case "initial":
// Content before any marker -- treat as thought
state = "thought";
onThought(content);
break;
case "complete":
break;
}
}
};
private processBuffer(isFinal: boolean): void {
const processLine = (line: string): void => {
const trimmed = line.trimStart();
// Check if this line starts a new section
for (const marker of MARKERS) {
if (trimmed.startsWith(marker.prefix)) {
const content = trimmed.slice(marker.prefix.length).trim();
state = marker.state;
emitContent(content);
return;
}
}
// Otherwise, this is continuation content for the current state
if (trimmed.length > 0) {
emitContent(trimmed);
}
};
const processBuffer = (isFinal: boolean): void => {
// Process complete lines (terminated by newline)
while (true) {
const newlineIdx = this.buffer.indexOf("\n");
const newlineIdx = buffer.indexOf("\n");
if (newlineIdx === -1) {
// No complete line yet.
// If not final, check for partial marker match at the end and wait.
if (!isFinal) {
// If the remaining buffer could be the start of a marker, wait for more input.
const trimmed = this.buffer.trimStart();
const trimmed = buffer.trimStart();
if (trimmed.length > 0 && trimmed.length < MAX_MARKER_LEN) {
const couldBeMarker = MARKERS.some((m) =>
m.prefix.startsWith(trimmed),
@ -86,54 +104,29 @@ export class StreamingReActParser {
break;
}
const line = this.buffer.slice(0, newlineIdx);
this.buffer = this.buffer.slice(newlineIdx + 1);
this.processLine(line);
const line = buffer.slice(0, newlineIdx);
buffer = buffer.slice(newlineIdx + 1);
processLine(line);
}
}
};
private processLine(line: string): void {
const trimmed = line.trimStart();
/**
* Feed a chunk of LLM output text into the parser.
* Accumulates in a buffer and processes complete lines.
*/
const feed = (text: string): void => {
buffer += text;
processBuffer(false);
};
// Check if this line starts a new section
for (const marker of MARKERS) {
if (trimmed.startsWith(marker.prefix)) {
const content = trimmed.slice(marker.prefix.length).trim();
this.state = marker.state;
this.emitContent(content);
return;
}
const flush = (): void => {
processBuffer(true);
// Emit any remaining buffer content in the current state
if (buffer.trim().length > 0) {
emitContent(buffer);
buffer = "";
}
};
// Otherwise, this is continuation content for the current state
if (trimmed.length > 0) {
this.emitContent(trimmed);
}
}
private emitContent(content: string): void {
if (content.length === 0) return;
switch (this.state) {
case "thought":
this.onThought(content);
break;
case "action":
this.onAction(content);
break;
case "action_input":
this.onActionInput(content);
break;
case "final_answer":
this.onFinalAnswer(content);
break;
case "initial":
// Content before any marker -- treat as thought
this.state = "thought";
this.onThought(content);
break;
case "complete":
break;
}
}
return { feed, flush };
}

View file

@ -17,14 +17,15 @@
*/
import {
FlowProcessor,
ConsumerSpec,
ProducerSpec,
RequestResponseSpec,
makeFlowProcessor,
makeConsumerSpec,
makeProducerSpec,
makeRequestResponseSpec,
makeFlowProcessorProgram,
errorMessage,
type ProcessorConfig,
type FlowContext,
type FlowProcessorRuntime,
type AgentRequest,
type AgentResponse,
type TextCompletionRequest,
@ -488,32 +489,32 @@ const onAgentRequest = Effect.fn("AgentService.onRequest")(function* (
});
export const makeAgentSpecs = (): ReadonlyArray<Spec<AgentRuntime>> => [
new ConsumerSpec<AgentRequest, AgentHandlerError, AgentRuntime>(
makeConsumerSpec<AgentRequest, AgentHandlerError, AgentRuntime>(
"agent-request",
onAgentRequest,
),
new ProducerSpec<AgentResponse>("agent-response"),
new RequestResponseSpec<TextCompletionRequest, TextCompletionResponse>(
makeProducerSpec<AgentResponse>("agent-response"),
makeRequestResponseSpec<TextCompletionRequest, TextCompletionResponse>(
"llm",
"text-completion-request",
"text-completion-response",
),
new RequestResponseSpec<GraphRagRequest, GraphRagResponse>(
makeRequestResponseSpec<GraphRagRequest, GraphRagResponse>(
"graph-rag",
"graph-rag-request",
"graph-rag-response",
),
new RequestResponseSpec<DocumentRagRequest, DocumentRagResponse>(
makeRequestResponseSpec<DocumentRagRequest, DocumentRagResponse>(
"doc-rag",
"document-rag-request",
"document-rag-response",
),
new RequestResponseSpec<TriplesQueryRequest, TriplesQueryResponse>(
makeRequestResponseSpec<TriplesQueryRequest, TriplesQueryResponse>(
"triples",
"triples-request",
"triples-response",
),
new RequestResponseSpec<ToolRequest, ToolResponse>(
makeRequestResponseSpec<ToolRequest, ToolResponse>(
"mcp-tool",
"mcp-tool-request",
"mcp-tool-response",
@ -524,32 +525,25 @@ export const makeAgentConfigHandlers = (): ReadonlyArray<
EffectConfigHandler<never, AgentRuntime>
> => [onToolsConfig];
export class AgentService extends FlowProcessor<AgentRuntime> {
private readonly runtime = Effect.runSync(makeAgentRuntime);
export type AgentService = FlowProcessorRuntime<AgentRuntime>;
constructor(config: ProcessorConfig) {
super(config);
for (const spec of makeAgentSpecs()) {
this.registerSpecification(spec);
}
this.registerConfigHandler((config, version) =>
Effect.runPromise(onToolsConfig(config, version).pipe(
Effect.provideService(AgentRuntime, this.runtime),
)),
);
console.log("[AgentService] Service initialized");
}
override startEffect() {
return super.startEffect().pipe(
Effect.provideService(AgentRuntime, this.runtime),
);
}
export function makeAgentService(config: ProcessorConfig): AgentService {
const runtime = Effect.runSync(makeAgentRuntime);
const service = makeFlowProcessor(config, {
specifications: makeAgentSpecs(),
provide: (effect) => effect.pipe(Effect.provideService(AgentRuntime, runtime)),
});
service.registerConfigHandler((pushedConfig, version) =>
Effect.runPromise(onToolsConfig(pushedConfig, version).pipe(
Effect.provideService(AgentRuntime, runtime),
)),
);
console.log("[AgentService] Service initialized");
return service;
}
export const AgentService = makeAgentService;
/**
* Simple line-based parser for ReAct LLM output.
*