server for rowboatx

This commit is contained in:
Ramnique Singh 2025-12-02 13:24:58 +05:30
parent ae877e70ae
commit 9ad6331fbc
38 changed files with 2223 additions and 1088 deletions

View file

@ -1,4 +1,4 @@
import { Agent, ToolAttachment } from "../entities/agent.js";
import { Agent, ToolAttachment } from "../../agents/agents.js";
import z from "zod";
import { CopilotInstructions } from "./instructions.js";
import { BuiltinTools } from "../lib/builtin-tools.js";

View file

@ -1,5 +1,5 @@
import { skillCatalog } from "./skills/index.js";
import { WorkDir as BASE_DIR } from "../config/config.js";
import { WorkDir as BASE_DIR } from "../../config/config.js";
export const CopilotInstructions = `You are an intelligent workflow assistant helping users manage their workflows in ${BASE_DIR}. You can also help the user with general tasks.

View file

@ -1,63 +0,0 @@
import path from "path";
import fs from "fs";
import { McpServerConfig } from "../entities/mcp.js";
import { ModelConfig } from "../entities/models.js";
import { z } from "zod";
import { homedir } from "os";
// Resolve app root relative to compiled file location (dist/...)
export const WorkDir = path.join(homedir(), ".rowboat");
let modelConfig: z.infer<typeof ModelConfig> | null = null;
const baseMcpConfig: z.infer<typeof McpServerConfig> = {
mcpServers: {
}
};
function ensureMcpConfig() {
const configPath = path.join(WorkDir, "config", "mcp.json");
if (!fs.existsSync(configPath)) {
fs.writeFileSync(configPath, JSON.stringify(baseMcpConfig, null, 2));
}
}
function ensureDirs() {
const ensure = (p: string) => { if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); };
ensure(WorkDir);
ensure(path.join(WorkDir, "agents"));
ensure(path.join(WorkDir, "config"));
ensureMcpConfig();
}
function loadMcpServerConfig(): z.infer<typeof McpServerConfig> {
const configPath = path.join(WorkDir, "config", "mcp.json");
if (!fs.existsSync(configPath)) return { mcpServers: {} };
const config = fs.readFileSync(configPath, "utf8");
return McpServerConfig.parse(JSON.parse(config));
}
export async function getModelConfig(): Promise<z.infer<typeof ModelConfig> | null> {
if (modelConfig) {
return modelConfig;
}
const configPath = path.join(WorkDir, "config", "models.json");
try {
const config = await fs.promises.readFile(configPath, "utf8");
modelConfig = ModelConfig.parse(JSON.parse(config));
return modelConfig;
} catch (error) {
console.error(`Warning! model config not found!`);
return null;
}
}
export async function updateModelConfig(config: z.infer<typeof ModelConfig>) {
modelConfig = config;
const configPath = path.join(WorkDir, "config", "models.json");
await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2));
}
ensureDirs();
const { mcpServers } = loadMcpServerConfig();
export const McpServers = mcpServers;

View file

@ -1,101 +0,0 @@
import path from "path";
import fs from "fs";
import { WorkDir } from "./config.js";
export const SECURITY_CONFIG_PATH = path.join(WorkDir, "config", "security.json");
const DEFAULT_ALLOW_LIST = [
"cat",
"curl",
"date",
"echo",
"grep",
"jq",
"ls",
"pwd",
"yq",
"whoami"
]
let cachedAllowList: string[] | null = null;
let cachedMtimeMs: number | null = null;
function ensureSecurityConfig() {
if (!fs.existsSync(SECURITY_CONFIG_PATH)) {
fs.writeFileSync(
SECURITY_CONFIG_PATH,
JSON.stringify(DEFAULT_ALLOW_LIST, null, 2) + "\n",
"utf8",
);
}
}
function normalizeList(commands: unknown[]): string[] {
const seen = new Set<string>();
for (const entry of commands) {
if (typeof entry !== "string") continue;
const normalized = entry.trim().toLowerCase();
if (!normalized) continue;
seen.add(normalized);
}
return Array.from(seen);
}
function parseSecurityPayload(payload: unknown): string[] {
if (Array.isArray(payload)) {
return normalizeList(payload);
}
if (payload && typeof payload === "object") {
const maybeObject = payload as Record<string, unknown>;
if (Array.isArray(maybeObject.allowedCommands)) {
return normalizeList(maybeObject.allowedCommands);
}
const dynamicList = Object.entries(maybeObject)
.filter(([, value]) => Boolean(value))
.map(([key]) => key);
return normalizeList(dynamicList);
}
return [];
}
function readAllowList(): string[] {
ensureSecurityConfig();
try {
const configContent = fs.readFileSync(SECURITY_CONFIG_PATH, "utf8");
const parsed = JSON.parse(configContent);
return parseSecurityPayload(parsed);
} catch (error) {
console.warn(`Failed to read security config at ${SECURITY_CONFIG_PATH}: ${error instanceof Error ? error.message : error}`);
return DEFAULT_ALLOW_LIST;
}
}
export function getSecurityAllowList(): string[] {
ensureSecurityConfig();
try {
const stats = fs.statSync(SECURITY_CONFIG_PATH);
if (cachedAllowList && cachedMtimeMs === stats.mtimeMs) {
return cachedAllowList;
}
const allowList = readAllowList();
cachedAllowList = allowList;
cachedMtimeMs = stats.mtimeMs;
return allowList;
} catch {
cachedAllowList = null;
cachedMtimeMs = null;
return readAllowList();
}
}
export function resetSecurityAllowListCache() {
cachedAllowList = null;
cachedMtimeMs = null;
}

View file

@ -1,35 +0,0 @@
import { z } from "zod";
export const BaseTool = z.object({
name: z.string(),
});
export const BuiltinTool = BaseTool.extend({
type: z.literal("builtin"),
});
export const McpTool = BaseTool.extend({
type: z.literal("mcp"),
description: z.string(),
inputSchema: z.any(),
mcpServerName: z.string(),
});
export const AgentAsATool = BaseTool.extend({
type: z.literal("agent"),
});
export const ToolAttachment = z.discriminatedUnion("type", [
BuiltinTool,
McpTool,
AgentAsATool,
]);
export const Agent = z.object({
name: z.string(),
provider: z.string().optional(),
model: z.string().optional(),
description: z.string(),
instructions: z.string(),
tools: z.record(z.string(), ToolAttachment).optional(),
});

View file

@ -1,12 +0,0 @@
import z from "zod"
import { Agent } from "./agent.js"
import { McpServerDefinition } from "./mcp.js"
export const Example = z.object({
id: z.string(),
instructions: z.string().optional(),
description: z.string().optional(),
entryAgent: z.string().optional(),
agents: z.array(Agent).optional(),
mcpServers: z.record(z.string(), McpServerDefinition).optional(),
});

View file

@ -1,63 +0,0 @@
import { z } from "zod";
import { ProviderOptions } from "./message.js";
const BaseEvent = z.object({
providerOptions: ProviderOptions.optional(),
})
export const LlmStepStreamReasoningStartEvent = BaseEvent.extend({
type: z.literal("reasoning-start"),
});
export const LlmStepStreamReasoningDeltaEvent = BaseEvent.extend({
type: z.literal("reasoning-delta"),
delta: z.string(),
});
export const LlmStepStreamReasoningEndEvent = BaseEvent.extend({
type: z.literal("reasoning-end"),
});
export const LlmStepStreamTextStartEvent = BaseEvent.extend({
type: z.literal("text-start"),
});
export const LlmStepStreamTextDeltaEvent = BaseEvent.extend({
type: z.literal("text-delta"),
delta: z.string(),
});
export const LlmStepStreamTextEndEvent = BaseEvent.extend({
type: z.literal("text-end"),
});
export const LlmStepStreamToolCallEvent = BaseEvent.extend({
type: z.literal("tool-call"),
toolCallId: z.string(),
toolName: z.string(),
input: z.any(),
});
export const LlmStepStreamFinishStepEvent = z.object({
type: z.literal("finish-step"),
finishReason: z.enum(["stop", "tool-calls", "length", "content-filter", "error", "other", "unknown"]),
usage: z.object({
inputTokens: z.number().optional(),
outputTokens: z.number().optional(),
totalTokens: z.number().optional(),
reasoningTokens: z.number().optional(),
cachedInputTokens: z.number().optional(),
}),
providerOptions: ProviderOptions.optional(),
});
export const LlmStepStreamEvent = z.union([
LlmStepStreamReasoningStartEvent,
LlmStepStreamReasoningDeltaEvent,
LlmStepStreamReasoningEndEvent,
LlmStepStreamTextStartEvent,
LlmStepStreamTextDeltaEvent,
LlmStepStreamTextEndEvent,
LlmStepStreamToolCallEvent,
LlmStepStreamFinishStepEvent,
]);

View file

@ -1,20 +0,0 @@
import { z } from "zod";
export const StdioMcpServerConfig = z.object({
type: z.literal("stdio").optional(),
command: z.string(),
args: z.array(z.string()).optional(),
env: z.record(z.string(), z.string()).optional(),
});
export const HttpMcpServerConfig = z.object({
type: z.literal("http").optional(),
url: z.string(),
headers: z.record(z.string(), z.string()).optional(),
});
export const McpServerDefinition = z.union([StdioMcpServerConfig, HttpMcpServerConfig]);
export const McpServerConfig = z.object({
mcpServers: z.record(z.string(), McpServerDefinition),
});

View file

@ -1,67 +0,0 @@
import { z } from "zod";
export const ProviderOptions = z.record(z.string(), z.record(z.string(), z.json()));
export const TextPart = z.object({
type: z.literal("text"),
text: z.string(),
providerOptions: ProviderOptions.optional(),
});
export const ReasoningPart = z.object({
type: z.literal("reasoning"),
text: z.string(),
providerOptions: ProviderOptions.optional(),
});
export const ToolCallPart = z.object({
type: z.literal("tool-call"),
toolCallId: z.string(),
toolName: z.string(),
arguments: z.any(),
providerOptions: ProviderOptions.optional(),
});
export const AssistantContentPart = z.union([
TextPart,
ReasoningPart,
ToolCallPart,
]);
export const UserMessage = z.object({
role: z.literal("user"),
content: z.string(),
providerOptions: ProviderOptions.optional(),
});
export const AssistantMessage = z.object({
role: z.literal("assistant"),
content: z.union([
z.string(),
z.array(AssistantContentPart),
]),
providerOptions: ProviderOptions.optional(),
});
export const SystemMessage = z.object({
role: z.literal("system"),
content: z.string(),
providerOptions: ProviderOptions.optional(),
});
export const ToolMessage = z.object({
role: z.literal("tool"),
content: z.string(),
toolCallId: z.string(),
toolName: z.string(),
providerOptions: ProviderOptions.optional(),
});
export const Message = z.discriminatedUnion("role", [
AssistantMessage,
SystemMessage,
ToolMessage,
UserMessage,
]);
export const MessageList = z.array(Message);

View file

@ -1,27 +0,0 @@
import z from "zod";
export const Flavor = z.enum([
"rowboat [free]",
"aigateway",
"anthropic",
"google",
"ollama",
"openai",
"openai-compatible",
"openrouter",
]);
export const Provider = z.object({
flavor: Flavor,
apiKey: z.string().optional(),
baseURL: z.string().optional(),
headers: z.record(z.string(), z.string()).optional(),
});
export const ModelConfig = z.object({
providers: z.record(z.string(), Provider),
defaults: z.object({
provider: z.string(),
model: z.string(),
}),
});

View file

@ -1,85 +0,0 @@
import { LlmStepStreamEvent } from "./llm-step-events.js";
import { Message, ToolCallPart } from "./message.js";
import { Agent } from "./agent.js";
import z from "zod";
const BaseRunEvent = z.object({
ts: z.iso.datetime().optional(),
subflow: z.array(z.string()),
});
export const StartEvent = BaseRunEvent.extend({
type: z.literal("start"),
runId: z.string(),
agentName: z.string(),
});
export const SpawnSubFlowEvent = BaseRunEvent.extend({
type: z.literal("spawn-subflow"),
agentName: z.string(),
toolCallId: z.string(),
});
export const LlmStreamEvent = BaseRunEvent.extend({
type: z.literal("llm-stream-event"),
event: LlmStepStreamEvent,
});
export const MessageEvent = BaseRunEvent.extend({
type: z.literal("message"),
message: Message,
});
export const ToolInvocationEvent = BaseRunEvent.extend({
type: z.literal("tool-invocation"),
toolName: z.string(),
input: z.string(),
});
export const ToolResultEvent = BaseRunEvent.extend({
type: z.literal("tool-result"),
toolName: z.string(),
result: z.any(),
});
export const AskHumanRequestEvent = BaseRunEvent.extend({
type: z.literal("ask-human-request"),
toolCallId: z.string(),
query: z.string(),
});
export const AskHumanResponseEvent = BaseRunEvent.extend({
type: z.literal("ask-human-response"),
toolCallId: z.string(),
response: z.string(),
});
export const ToolPermissionRequestEvent = BaseRunEvent.extend({
type: z.literal("tool-permission-request"),
toolCall: ToolCallPart,
});
export const ToolPermissionResponseEvent = BaseRunEvent.extend({
type: z.literal("tool-permission-response"),
toolCallId: z.string(),
response: z.enum(["approve", "deny"]),
});
export const RunErrorEvent = BaseRunEvent.extend({
type: z.literal("error"),
error: z.string(),
});
export const RunEvent = z.union([
StartEvent,
SpawnSubFlowEvent,
LlmStreamEvent,
MessageEvent,
ToolInvocationEvent,
ToolResultEvent,
AskHumanRequestEvent,
AskHumanResponseEvent,
ToolPermissionRequestEvent,
ToolPermissionResponseEvent,
RunErrorEvent,
]);

View file

@ -1,681 +0,0 @@
import { jsonSchema, ModelMessage, modelMessageSchema } from "ai";
import fs from "fs";
import path from "path";
import { getModelConfig, WorkDir } from "../config/config.js";
import { Agent, ToolAttachment } from "../entities/agent.js";
import { AssistantContentPart, AssistantMessage, Message, MessageList, ProviderOptions, ToolCallPart, ToolMessage, UserMessage } from "../entities/message.js";
import { runIdGenerator } from "./run-id-gen.js";
import { LanguageModel, stepCountIs, streamText, tool, Tool, ToolSet } from "ai";
import { z } from "zod";
import { getProvider } from "./models.js";
import { LlmStepStreamEvent } from "../entities/llm-step-events.js";
import { execTool } from "./exec-tool.js";
import { AskHumanRequestEvent, RunEvent, ToolPermissionRequestEvent, ToolPermissionResponseEvent } from "../entities/run-events.js";
import { BuiltinTools } from "./builtin-tools.js";
import { CopilotAgent } from "../assistant/agent.js";
import { isBlocked } from "./command-executor.js";
export async function mapAgentTool(t: z.infer<typeof ToolAttachment>): Promise<Tool> {
switch (t.type) {
case "mcp":
return tool({
name: t.name,
description: t.description,
inputSchema: jsonSchema(t.inputSchema),
});
case "agent":
const agent = await loadAgent(t.name);
if (!agent) {
throw new Error(`Agent ${t.name} not found`);
}
return tool({
name: t.name,
description: agent.description,
inputSchema: z.object({
message: z.string().describe("The message to send to the workflow"),
}),
});
case "builtin":
if (t.name === "ask-human") {
return tool({
description: "Ask a human before proceeding",
inputSchema: z.object({
question: z.string().describe("The question to ask the human"),
}),
});
}
const match = BuiltinTools[t.name];
if (!match) {
throw new Error(`Unknown builtin tool: ${t.name}`);
}
return tool({
description: match.description,
inputSchema: match.inputSchema,
});
}
}
export class RunLogger {
private logFile: string;
private fileHandle: fs.WriteStream;
ensureRunsDir() {
const runsDir = path.join(WorkDir, "runs");
if (!fs.existsSync(runsDir)) {
fs.mkdirSync(runsDir, { recursive: true });
}
}
constructor(runId: string) {
this.ensureRunsDir();
this.logFile = path.join(WorkDir, "runs", `${runId}.jsonl`);
this.fileHandle = fs.createWriteStream(this.logFile, {
flags: "a",
encoding: "utf8",
});
}
log(event: z.infer<typeof RunEvent>) {
if (event.type !== "llm-stream-event") {
this.fileHandle.write(JSON.stringify(event) + "\n");
}
}
close() {
this.fileHandle.close();
}
}
export class StreamStepMessageBuilder {
private parts: z.infer<typeof AssistantContentPart>[] = [];
private textBuffer: string = "";
private reasoningBuffer: string = "";
private providerOptions: z.infer<typeof ProviderOptions> | undefined = undefined;
flushBuffers() {
// skip reasoning
// if (this.reasoningBuffer) {
// this.parts.push({ type: "reasoning", text: this.reasoningBuffer });
// this.reasoningBuffer = "";
// }
if (this.textBuffer) {
this.parts.push({ type: "text", text: this.textBuffer });
this.textBuffer = "";
}
}
ingest(event: z.infer<typeof LlmStepStreamEvent>) {
switch (event.type) {
case "reasoning-start":
case "reasoning-end":
case "text-start":
case "text-end":
this.flushBuffers();
break;
case "reasoning-delta":
this.reasoningBuffer += event.delta;
break;
case "text-delta":
this.textBuffer += event.delta;
break;
case "tool-call":
this.parts.push({
type: "tool-call",
toolCallId: event.toolCallId,
toolName: event.toolName,
arguments: event.input,
providerOptions: event.providerOptions,
});
break;
case "finish-step":
this.providerOptions = event.providerOptions;
break;
}
}
get(): z.infer<typeof AssistantMessage> {
this.flushBuffers();
return {
role: "assistant",
content: this.parts,
providerOptions: this.providerOptions,
};
}
}
function normaliseAskHumanToolCall(message: z.infer<typeof AssistantMessage>) {
if (typeof message.content === "string") {
return;
}
let askHumanToolCall: z.infer<typeof ToolCallPart> | null = null;
const newParts = [];
for (const part of message.content as z.infer<typeof AssistantContentPart>[]) {
if (part.type === "tool-call" && part.toolName === "ask-human") {
if (!askHumanToolCall) {
askHumanToolCall = part;
} else {
(askHumanToolCall as z.infer<typeof ToolCallPart>).arguments += "\n" + part.arguments;
}
break;
} else {
newParts.push(part);
}
}
if (askHumanToolCall) {
newParts.push(askHumanToolCall);
}
message.content = newParts;
}
export async function loadAgent(id: string): Promise<z.infer<typeof Agent>> {
if (id === "copilot") {
return CopilotAgent;
}
const agentPath = path.join(WorkDir, "agents", `${id}.json`);
const agent = fs.readFileSync(agentPath, "utf8");
return Agent.parse(JSON.parse(agent));
}
export function convertFromMessages(messages: z.infer<typeof Message>[]): ModelMessage[] {
const result: ModelMessage[] = [];
for (const msg of messages) {
const { providerOptions } = msg;
switch (msg.role) {
case "assistant":
if (typeof msg.content === 'string') {
result.push({
role: "assistant",
content: msg.content,
providerOptions,
});
} else {
result.push({
role: "assistant",
content: msg.content.map(part => {
switch (part.type) {
case 'text':
return part;
case 'reasoning':
return part;
case 'tool-call':
return {
type: 'tool-call',
toolCallId: part.toolCallId,
toolName: part.toolName,
input: part.arguments,
providerOptions: part.providerOptions,
};
}
}),
providerOptions,
});
}
break;
case "system":
result.push({
role: "system",
content: msg.content,
providerOptions,
});
break;
case "user":
result.push({
role: "user",
content: msg.content,
providerOptions,
});
break;
case "tool":
result.push({
role: "tool",
content: [
{
type: "tool-result",
toolCallId: msg.toolCallId,
toolName: msg.toolName,
output: {
type: "text",
value: msg.content,
},
},
],
providerOptions,
});
break;
}
}
// doing this because: https://github.com/OpenRouterTeam/ai-sdk-provider/issues/262
return JSON.parse(JSON.stringify(result));
}
async function buildTools(agent: z.infer<typeof Agent>): Promise<ToolSet> {
const tools: ToolSet = {};
for (const [name, tool] of Object.entries(agent.tools ?? {})) {
try {
tools[name] = await mapAgentTool(tool);
} catch (error) {
console.error(`Error mapping tool ${name}:`, error);
continue;
}
}
return tools;
}
export class AgentState {
logger: RunLogger | null = null;
runId: string | null = null;
agent: z.infer<typeof Agent> | null = null;
agentName: string;
messages: z.infer<typeof MessageList> = [];
lastAssistantMsg: z.infer<typeof AssistantMessage> | null = null;
subflowStates: Record<string, AgentState> = {};
toolCallIdMap: Record<string, z.infer<typeof ToolCallPart>> = {};
pendingToolCalls: Record<string, true> = {};
pendingToolPermissionRequests: Record<string, z.infer<typeof ToolPermissionRequestEvent>> = {};
pendingAskHumanRequests: Record<string, z.infer<typeof AskHumanRequestEvent>> = {};
allowedToolCallIds: Record<string, true> = {};
deniedToolCallIds: Record<string, true> = {};
constructor(agentName: string, runId?: string) {
this.agentName = agentName;
this.runId = runId || runIdGenerator.next();
this.logger = new RunLogger(this.runId);
if (!runId) {
this.logger.log({
type: "start",
runId: this.runId,
agentName: this.agentName,
subflow: [],
});
}
}
getPendingPermissions(): z.infer<typeof ToolPermissionRequestEvent>[] {
const response: z.infer<typeof ToolPermissionRequestEvent>[] = [];
for (const [id, subflowState] of Object.entries(this.subflowStates)) {
for (const perm of subflowState.getPendingPermissions()) {
response.push({
...perm,
subflow: [id, ...perm.subflow],
});
}
}
for (const perm of Object.values(this.pendingToolPermissionRequests)) {
response.push({
...perm,
subflow: [],
});
}
return response;
}
getPendingAskHumans(): z.infer<typeof AskHumanRequestEvent>[] {
const response: z.infer<typeof AskHumanRequestEvent>[] = [];
for (const [id, subflowState] of Object.entries(this.subflowStates)) {
for (const ask of subflowState.getPendingAskHumans()) {
response.push({
...ask,
subflow: [id, ...ask.subflow],
});
}
}
for (const ask of Object.values(this.pendingAskHumanRequests)) {
response.push({
...ask,
subflow: [],
});
}
return response;
}
finalResponse(): string {
if (!this.lastAssistantMsg) {
return '';
}
if (typeof this.lastAssistantMsg.content === "string") {
return this.lastAssistantMsg.content;
}
return this.lastAssistantMsg.content.reduce((acc, part) => {
if (part.type === "text") {
return acc + part.text;
}
return acc;
}, "");
}
ingest(event: z.infer<typeof RunEvent>) {
if (event.subflow.length > 0) {
const { subflow, ...rest } = event;
this.subflowStates[subflow[0]].ingest({
...rest,
subflow: subflow.slice(1),
});
return;
}
switch (event.type) {
case "message":
this.messages.push(event.message);
if (event.message.content instanceof Array) {
for (const part of event.message.content) {
if (part.type === "tool-call") {
this.toolCallIdMap[part.toolCallId] = part;
this.pendingToolCalls[part.toolCallId] = true;
}
}
}
if (event.message.role === "tool") {
const message = event.message as z.infer<typeof ToolMessage>;
delete this.pendingToolCalls[message.toolCallId];
}
if (event.message.role === "assistant") {
this.lastAssistantMsg = event.message;
}
break;
case "spawn-subflow":
this.subflowStates[event.toolCallId] = new AgentState(event.agentName);
break;
case "tool-permission-request":
this.pendingToolPermissionRequests[event.toolCall.toolCallId] = event;
break;
case "tool-permission-response":
switch (event.response) {
case "approve":
this.allowedToolCallIds[event.toolCallId] = true;
break;
case "deny":
this.deniedToolCallIds[event.toolCallId] = true;
break;
}
delete this.pendingToolPermissionRequests[event.toolCallId];
break;
case "ask-human-request":
this.pendingAskHumanRequests[event.toolCallId] = event;
break;
case "ask-human-response":
// console.error('im here', this.agentName, this.runId, event.subflow);
const ogEvent = this.pendingAskHumanRequests[event.toolCallId];
this.messages.push({
role: "tool",
content: JSON.stringify({
userResponse: event.response,
}),
toolCallId: ogEvent.toolCallId,
toolName: this.toolCallIdMap[ogEvent.toolCallId]!.toolName,
});
delete this.pendingAskHumanRequests[ogEvent.toolCallId];
break;
}
}
ingestAndLog(event: z.infer<typeof RunEvent>) {
this.ingest(event);
this.logger!.log(event);
}
*ingestAndLogAndYield(event: z.infer<typeof RunEvent>): Generator<z.infer<typeof RunEvent>, void, unknown> {
this.ingestAndLog(event);
yield event;
}
}
export async function* streamAgent(state: AgentState): AsyncGenerator<z.infer<typeof RunEvent>, void, unknown> {
// get model config
const modelConfig = await getModelConfig();
if (!modelConfig) {
throw new Error("Model config not found");
}
// set up agent
const agent = await loadAgent(state.agentName);
// set up tools
const tools = await buildTools(agent);
// set up provider + model
const provider = await getProvider(agent.provider);
const model = provider.languageModel(agent.model || modelConfig.defaults.model);
let loopCounter = 0;
while (true) {
// console.error(`loop counter: ${loopCounter++}`)
// if last response is from assistant and text, so exit
const lastMessage = state.messages[state.messages.length - 1];
if (lastMessage
&& lastMessage.role === "assistant"
&& (typeof lastMessage.content === "string"
|| !lastMessage.content.some(part => part.type === "tool-call")
)
) {
// console.error("Nothing to do, exiting (a.)")
return;
}
// execute any pending tool calls
for (const toolCallId of Object.keys(state.pendingToolCalls)) {
const toolCall = state.toolCallIdMap[toolCallId];
// if ask-human, skip
if (toolCall.toolName === "ask-human") {
continue;
}
// if tool has been denied, deny
if (state.deniedToolCallIds[toolCallId]) {
yield* state.ingestAndLogAndYield({
type: "message",
message: {
role: "tool",
content: "Unable to execute this tool: Permission was denied.",
toolCallId: toolCallId,
toolName: toolCall.toolName,
},
subflow: [],
});
continue;
}
// if permission is pending on this tool call, allow execution
if (state.pendingToolPermissionRequests[toolCallId]) {
continue;
}
// execute approved tool
yield* state.ingestAndLogAndYield({
type: "tool-invocation",
toolName: toolCall.toolName,
input: JSON.stringify(toolCall.arguments),
subflow: [],
});
let result: any = null;
if (agent.tools![toolCall.toolName].type === "agent") {
let subflowState = state.subflowStates[toolCallId];
for await (const event of streamAgent(subflowState)) {
yield* state.ingestAndLogAndYield({
...event,
subflow: [toolCallId, ...event.subflow],
});
}
if (!subflowState.getPendingAskHumans().length && !subflowState.getPendingPermissions().length) {
result = subflowState.finalResponse();
}
} else {
result = await execTool(agent.tools![toolCall.toolName], toolCall.arguments);
}
if (result) {
const resultMsg: z.infer<typeof ToolMessage> = {
role: "tool",
content: JSON.stringify(result),
toolCallId: toolCall.toolCallId,
toolName: toolCall.toolName,
};
yield* state.ingestAndLogAndYield({
type: "tool-result",
toolName: toolCall.toolName,
result: result,
subflow: [],
});
yield* state.ingestAndLogAndYield({
type: "message",
message: resultMsg,
subflow: [],
});
}
}
// if pending state, exit
if (state.getPendingAskHumans().length || state.getPendingPermissions().length) {
// console.error("pending asks or permissions, exiting (b.)")
return;
}
// if current message state isn't runnable, exit
if (state.messages.length === 0 || state.messages[state.messages.length - 1].role === "assistant") {
// console.error("current message state isn't runnable, exiting (c.)")
return;
}
// run one LLM turn.
// stream agent response and build message
const messageBuilder = new StreamStepMessageBuilder();
for await (const event of streamLlm(
model,
state.messages,
agent.instructions,
tools,
)) {
messageBuilder.ingest(event);
yield* state.ingestAndLogAndYield({
type: "llm-stream-event",
event: event,
subflow: [],
});
}
// build and emit final message from agent response
const message = messageBuilder.get();
yield* state.ingestAndLogAndYield({
type: "message",
message,
subflow: [],
});
// if there were any ask-human calls, emit those events
if (message.content instanceof Array) {
for (const part of message.content) {
if (part.type === "tool-call") {
const underlyingTool = agent.tools![part.toolName];
if (underlyingTool.type === "builtin" && underlyingTool.name === "ask-human") {
yield* state.ingestAndLogAndYield({
type: "ask-human-request",
toolCallId: part.toolCallId,
query: part.arguments.question,
subflow: [],
});
}
if (underlyingTool.type === "builtin" && underlyingTool.name === "executeCommand") {
// if command is blocked, then seek permission
if (isBlocked(part.arguments.command)) {
yield* state.ingestAndLogAndYield({
type: "tool-permission-request",
toolCall: part,
subflow: [],
});
}
}
if (underlyingTool.type === "agent" && underlyingTool.name) {
yield* state.ingestAndLogAndYield({
type: "spawn-subflow",
agentName: underlyingTool.name,
toolCallId: part.toolCallId,
subflow: [],
});
yield* state.ingestAndLogAndYield({
type: "message",
message: {
role: "user",
content: part.arguments.message,
},
subflow: [part.toolCallId],
});
}
}
}
}
}
}
async function* streamLlm(
model: LanguageModel,
messages: z.infer<typeof MessageList>,
instructions: string,
tools: ToolSet,
): AsyncGenerator<z.infer<typeof LlmStepStreamEvent>, void, unknown> {
const { fullStream } = streamText({
model,
messages: convertFromMessages(messages),
system: instructions,
tools,
stopWhen: stepCountIs(1),
});
for await (const event of fullStream) {
// console.log("\n\n\t>>>>\t\tstream event", JSON.stringify(event));
switch (event.type) {
case "reasoning-start":
yield {
type: "reasoning-start",
providerOptions: event.providerMetadata,
};
break;
case "reasoning-delta":
yield {
type: "reasoning-delta",
delta: event.text,
providerOptions: event.providerMetadata,
};
break;
case "reasoning-end":
yield {
type: "reasoning-end",
providerOptions: event.providerMetadata,
};
break;
case "text-start":
yield {
type: "text-start",
providerOptions: event.providerMetadata,
};
break;
case "text-delta":
yield {
type: "text-delta",
delta: event.text,
providerOptions: event.providerMetadata,
};
break;
case "tool-call":
yield {
type: "tool-call",
toolCallId: event.toolCallId,
toolName: event.toolName,
input: event.input,
providerOptions: event.providerMetadata,
};
break;
case "finish-step":
yield {
type: "finish-step",
usage: event.usage,
finishReason: event.finishReason,
providerOptions: event.providerMetadata,
};
break;
default:
// console.warn("Unknown event type", event);
continue;
}
}
}
export const MappedToolCall = z.object({
toolCall: ToolCallPart,
agentTool: ToolAttachment,
});

View file

@ -1,14 +1,13 @@
import { z, ZodType } from "zod";
import * as fs from "fs/promises";
import * as path from "path";
import { WorkDir as BASE_DIR } from "../config/config.js";
import { WorkDir as BASE_DIR } from "../../config/config.js";
import { executeCommand } from "./command-executor.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { Client } from "@modelcontextprotocol/sdk/client";
import { resolveSkill, availableSkills } from "../assistant/skills/index.js";
import { McpServerDefinition, McpServerConfig } from "../entities/mcp.js";
import { executeTool, listServers, listTools } from "../../mcp/mcp.js";
import container from "../../di/container.js";
import { IMcpConfigRepo } from "../..//mcp/repo.js";
import { McpServerDefinition } from "../../mcp/mcp.js";
const BuiltinToolsSchema = z.record(z.string(), z.object({
description: z.string(),
@ -310,109 +309,33 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
description: 'Add or update an MCP server in the configuration with validation. This ensures the server definition is valid before saving.',
inputSchema: z.object({
serverName: z.string().describe('Name/alias for the MCP server'),
serverType: z.enum(['stdio', 'http']).describe('Type of MCP server: "stdio" for command-based or "http" for HTTP/SSE-based'),
command: z.string().optional().describe('Command to execute (required for stdio type, e.g., "npx", "python", "node")'),
args: z.array(z.string()).optional().describe('Command arguments (optional, for stdio type)'),
env: z.record(z.string(), z.string()).optional().describe('Environment variables (optional, for stdio type)'),
url: z.string().optional().describe('HTTP/SSE endpoint URL (required for http type)'),
headers: z.record(z.string(), z.string()).optional().describe('HTTP headers (optional, for http type)'),
config: McpServerDefinition,
}),
execute: async ({ serverName, serverType, command, args, env, url, headers }: {
execute: async ({ serverName, config }: {
serverName: string;
serverType: 'stdio' | 'http';
command?: string;
args?: string[];
env?: Record<string, string>;
url?: string;
headers?: Record<string, string>;
config: z.infer<typeof McpServerDefinition>;
}) => {
try {
// Build server definition based on type
let serverDef: any;
if (serverType === 'stdio') {
if (!command) {
return {
success: false,
message: 'For stdio type servers, "command" is required. Example: "npx" or "python"',
validationErrors: ['Missing required field: command'],
};
}
serverDef = {
type: 'stdio',
command,
...(args ? { args } : {}),
...(env ? { env } : {}),
};
} else if (serverType === 'http') {
if (!url) {
return {
success: false,
message: 'For http type servers, "url" is required. Example: "http://localhost:3000/sse"',
validationErrors: ['Missing required field: url'],
};
}
serverDef = {
type: 'http',
url,
...(headers ? { headers } : {}),
};
} else {
return {
success: false,
message: `Invalid serverType: ${serverType}. Must be "stdio" or "http"`,
validationErrors: [`Invalid serverType: ${serverType}`],
};
}
// Validate against Zod schema
const validationResult = McpServerDefinition.safeParse(serverDef);
const validationResult = McpServerDefinition.safeParse(config);
if (!validationResult.success) {
return {
success: false,
message: 'Server definition failed validation. Check the errors below.',
validationErrors: validationResult.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`),
providedDefinition: serverDef,
providedDefinition: config,
};
}
// Read existing config
const configPath = path.join(BASE_DIR, 'config', 'mcp.json');
let currentConfig: z.infer<typeof McpServerConfig> = { mcpServers: {} };
try {
const content = await fs.readFile(configPath, 'utf-8');
currentConfig = McpServerConfig.parse(JSON.parse(content));
} catch (error: any) {
if (error?.code !== 'ENOENT') {
return {
success: false,
message: `Failed to read existing MCP config: ${error.message}`,
};
}
// File doesn't exist, use empty config
}
// Check if server already exists
const isUpdate = !!currentConfig.mcpServers[serverName];
// Add/update server
currentConfig.mcpServers[serverName] = validationResult.data;
// Write back to file
await fs.mkdir(path.dirname(configPath), { recursive: true });
await fs.writeFile(configPath, JSON.stringify(currentConfig, null, 2), 'utf-8');
const repo = container.resolve<IMcpConfigRepo>('mcpConfigRepo');
await repo.upsert(serverName, config);
return {
success: true,
message: `MCP server '${serverName}' ${isUpdate ? 'updated' : 'added'} successfully`,
serverName,
serverType,
isUpdate,
configuration: validationResult.data,
};
} catch (error) {
return {
success: false,
message: `Failed to add MCP server: ${error instanceof Error ? error.message : 'Unknown error'}`,
error: `Failed to update MCP server: ${error instanceof Error ? error.message : 'Unknown error'}`,
};
}
},
@ -421,47 +344,17 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
listMcpServers: {
description: 'List all available MCP servers from the configuration',
inputSchema: z.object({}),
execute: async (): Promise<{ success: boolean, servers: any[], count: number, message: string }> => {
execute: async () => {
try {
const configPath = path.join(BASE_DIR, 'config', 'mcp.json');
// Check if config exists
try {
await fs.access(configPath);
} catch {
return {
success: true,
servers: [],
count: 0,
message: 'No MCP servers configured yet',
};
}
const content = await fs.readFile(configPath, 'utf-8');
const config = JSON.parse(content);
const servers = Object.keys(config.mcpServers || {}).map(name => {
const server = config.mcpServers[name];
return {
name,
type: 'command' in server ? 'stdio' : 'http',
command: server.command,
url: server.url,
};
});
const result = await listServers();
return {
success: true,
servers,
count: servers.length,
message: `Found ${servers.length} MCP server(s)`,
result,
count: Object.keys(result.mcpServers).length,
};
} catch (error) {
return {
success: false,
servers: [],
count: 0,
message: `Failed to list MCP servers: ${error instanceof Error ? error.message : 'Unknown error'}`,
error: `Failed to list MCP servers: ${error instanceof Error ? error.message : 'Unknown error'}`,
};
}
},
@ -471,69 +364,19 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
description: 'List all available tools from a specific MCP server',
inputSchema: z.object({
serverName: z.string().describe('Name of the MCP server to query'),
cursor: z.string().optional(),
}),
execute: async ({ serverName }: { serverName: string }) => {
execute: async ({ serverName, cursor }: { serverName: string, cursor?: string }) => {
try {
const configPath = path.join(BASE_DIR, 'config', 'mcp.json');
const content = await fs.readFile(configPath, 'utf-8');
const config = JSON.parse(content);
const mcpConfig = config.mcpServers[serverName];
if (!mcpConfig) {
return {
success: false,
message: `MCP server '${serverName}' not found in configuration`,
};
}
// Create transport based on config type
let transport;
if ('command' in mcpConfig) {
transport = new StdioClientTransport({
command: mcpConfig.command,
args: mcpConfig.args || [],
env: mcpConfig.env || {},
});
} else {
try {
transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url));
} catch {
transport = new SSEClientTransport(new URL(mcpConfig.url));
}
}
// Create and connect client
const client = new Client({
name: 'rowboat-copilot',
version: '1.0.0',
});
await client.connect(transport);
// List available tools
const toolsList = await client.listTools();
// Close connection
client.close();
transport.close();
const tools = toolsList.tools.map((t: any) => ({
name: t.name,
description: t.description || 'No description',
inputSchema: t.inputSchema,
}));
const result = await listTools(serverName, cursor);
return {
success: true,
serverName,
tools,
count: tools.length,
message: `Found ${tools.length} tool(s) in MCP server '${serverName}'`,
result,
count: result.tools.length,
};
} catch (error) {
return {
success: false,
message: `Failed to list MCP tools: ${error instanceof Error ? error.message : 'Unknown error'}`,
error: `Failed to list MCP tools: ${error instanceof Error ? error.message : 'Unknown error'}`,
};
}
},
@ -547,108 +390,19 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
arguments: z.record(z.string(), z.any()).optional().describe('Arguments to pass to the tool (as key-value pairs matching the tool\'s input schema). MUST include all required parameters from the tool\'s inputSchema.'),
}),
execute: async ({ serverName, toolName, arguments: args = {} }: { serverName: string, toolName: string, arguments?: Record<string, any> }) => {
let transport: any;
let client: any;
try {
const configPath = path.join(BASE_DIR, 'config', 'mcp.json');
const content = await fs.readFile(configPath, 'utf-8');
const config = JSON.parse(content);
const mcpConfig = config.mcpServers[serverName];
if (!mcpConfig) {
return {
success: false,
message: `MCP server '${serverName}' not found in configuration. Use listMcpServers to see available servers.`,
};
}
// Create transport based on config type
if ('command' in mcpConfig) {
transport = new StdioClientTransport({
command: mcpConfig.command,
args: mcpConfig.args || [],
env: mcpConfig.env || {},
});
} else {
try {
transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url));
} catch {
transport = new SSEClientTransport(new URL(mcpConfig.url));
}
}
// Create and connect client
client = new Client({
name: 'rowboat-copilot',
version: '1.0.0',
});
await client.connect(transport);
// Get tool list to validate the tool exists and check schema
const toolsList = await client.listTools();
const toolDef = toolsList.tools.find((t: any) => t.name === toolName);
if (!toolDef) {
await client.close();
transport.close();
return {
success: false,
message: `Tool '${toolName}' not found in server '${serverName}'. Use listMcpTools to see available tools.`,
availableTools: toolsList.tools.map((t: any) => t.name),
};
}
// Validate required parameters
const inputSchema = toolDef.inputSchema;
if (inputSchema && inputSchema.required && Array.isArray(inputSchema.required)) {
const missingParams = inputSchema.required.filter((param: string) => !(param in args));
if (missingParams.length > 0) {
await client.close();
transport.close();
return {
success: false,
message: `Missing required parameters: ${missingParams.join(', ')}`,
requiredParameters: inputSchema.required,
providedArguments: Object.keys(args),
toolSchema: inputSchema,
hint: `Use listMcpTools to see the full schema for '${toolName}' and ensure all required parameters are included in the arguments field.`,
};
}
}
// Call the tool
const result = await client.callTool({
name: toolName,
arguments: args,
});
// Close connection
await client.close();
transport.close();
const result = await executeTool(serverName, toolName, args);
return {
success: true,
serverName,
toolName,
result: result.content,
result,
message: `Successfully executed tool '${toolName}' from server '${serverName}'`,
};
} catch (error) {
// Ensure cleanup
try {
if (client) await client.close();
if (transport) transport.close();
} catch (cleanupError) {
// Ignore cleanup errors
}
return {
success: false,
message: `Failed to execute MCP tool: ${error instanceof Error ? error.message : 'Unknown error'}`,
serverName,
toolName,
error: `Failed to execute MCP tool: ${error instanceof Error ? error.message : 'Unknown error'}`,
hint: 'Use listMcpTools to verify the tool exists and check its schema. Ensure all required parameters are provided in the arguments field.',
};
}

View file

@ -0,0 +1,38 @@
import { RunEvent } from "../../entities/run-events.js";
import z from "zod";
export interface IBus {
publish(event: z.infer<typeof RunEvent>): Promise<void>;
// subscribe accepts a handler to handle events
// and returns a function to unsubscribe
subscribe(runId: string, handler: (event: z.infer<typeof RunEvent>) => Promise<void>): Promise<() => void>;
}
export class InMemoryBus implements IBus {
private subscribers: Map<string, ((event: z.infer<typeof RunEvent>) => Promise<void>)[]> = new Map();
async publish(event: z.infer<typeof RunEvent>): Promise<void> {
console.log(this.subscribers);
const pending: Promise<void>[] = [];
for (const subscriber of this.subscribers.get(event.runId) || []) {
pending.push(subscriber(event));
}
for (const subscriber of this.subscribers.get('*') || []) {
pending.push(subscriber(event));
}
console.log(pending.length);
await Promise.all(pending);
}
async subscribe(runId: string, handler: (event: z.infer<typeof RunEvent>) => Promise<void>): Promise<() => void> {
if (!this.subscribers.has(runId)) {
this.subscribers.set(runId, []);
}
this.subscribers.get(runId)!.push(handler);
console.log(this.subscribers);
return () => {
this.subscribers.get(runId)!.splice(this.subscribers.get(runId)!.indexOf(handler), 1);
};
}
}

View file

@ -1,6 +1,6 @@
import { exec, execSync } from 'child_process';
import { promisify } from 'util';
import { getSecurityAllowList, SECURITY_CONFIG_PATH } from '../config/security.js';
import { getSecurityAllowList, SECURITY_CONFIG_PATH } from '../../config/security.js';
const execPromise = promisify(exec);
const COMMAND_SPLIT_REGEX = /(?:\|\||&&|;|\||\n)/;

View file

@ -1,53 +1,10 @@
import { ToolAttachment } from "../entities/agent.js";
import { ToolAttachment } from "../../agents/agents.js";
import { z } from "zod";
import { McpServers } from "../config/config.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import { Client } from "@modelcontextprotocol/sdk/client";
import { BuiltinTools } from "./builtin-tools.js";
import { executeTool } from "../../mcp/mcp.js";
async function execMcpTool(agentTool: z.infer<typeof ToolAttachment> & { type: "mcp" }, input: any): Promise<any> {
// load mcp configuration from the tool
const mcpConfig = McpServers[agentTool.mcpServerName];
if (!mcpConfig) {
throw new Error(`MCP server ${agentTool.mcpServerName} not found`);
}
// create transport
let transport: Transport;
if ("command" in mcpConfig) {
transport = new StdioClientTransport({
command: mcpConfig.command,
args: mcpConfig.args,
env: mcpConfig.env,
});
} else {
// first try streamable http transport
try {
transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url));
} catch (error) {
// if that fails, try sse transport
transport = new SSEClientTransport(new URL(mcpConfig.url));
}
}
if (!transport) {
throw new Error(`No transport found for ${agentTool.mcpServerName}`);
}
// create client
const client = new Client({
name: 'rowboatx',
version: '1.0.0',
});
await client.connect(transport);
// call tool
const result = await client.callTool({ name: agentTool.name, arguments: input });
client.close();
transport.close();
const result = await executeTool(agentTool.mcpServerName, agentTool.name, input);
return result;
}

View file

@ -1,19 +1,23 @@
class RunIdGenerator {
export interface IMonotonicallyIncreasingIdGenerator {
next(): Promise<string>;
}
export class IdGen implements IMonotonicallyIncreasingIdGenerator {
private lastMs = 0;
private seq = 0;
private readonly pid: string;
private readonly hostTag: string;
constructor(hostTag: string = "") {
constructor() {
this.pid = String(process.pid).padStart(7, "0");
this.hostTag = hostTag ? `-${hostTag}` : "";
this.hostTag = "";
}
/**
* Returns an ISO8601-based, lexicographically sortable id string.
* Example: 2025-11-11T04-36-29Z-0001234-h1-000
*/
next(): string {
async next(): Promise<string> {
const now = Date.now();
const ms = now >= this.lastMs ? now : this.lastMs; // monotonic clamp
this.seq = ms === this.lastMs ? this.seq + 1 : 0;
@ -27,6 +31,4 @@ class RunIdGenerator {
const seqStr = String(this.seq).padStart(3, "0");
return `${iso}-${this.pid}${this.hostTag}-${seqStr}`;
}
}
export const runIdGenerator = new RunIdGenerator();
}

View file

@ -1,31 +0,0 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
export async function getMcpClient(serverUrl: string, serverName: string): Promise<Client> {
let client: Client | undefined = undefined;
const baseUrl = new URL(serverUrl);
// Try to connect using Streamable HTTP transport
try {
client = new Client({
name: 'streamable-http-client',
version: '1.0.0'
});
const transport = new StreamableHTTPClientTransport(baseUrl);
await client.connect(transport);
console.log(`[MCP] Connected using Streamable HTTP transport to ${serverName}`);
return client;
} catch (error) {
// If that fails with a 4xx error, try the older SSE transport
console.log(`[MCP] Streamable HTTP connection failed, falling back to SSE transport for ${serverName}`);
client = new Client({
name: 'sse-client',
version: '1.0.0'
});
const sseTransport = new SSEClientTransport(baseUrl);
await client.connect(sseTransport);
console.log(`[MCP] Connected using SSE transport to ${serverName}`);
return client;
}
}

View file

@ -0,0 +1,44 @@
import z from "zod";
import { IMonotonicallyIncreasingIdGenerator } from "./id-gen.js";
const EnqueuedMessage = z.object({
messageId: z.string(),
message: z.string(),
});
export interface IMessageQueue {
enqueue(runId: string, message: string): Promise<string>;
dequeue(runId: string): Promise<z.infer<typeof EnqueuedMessage> | null>;
}
export class InMemoryMessageQueue implements IMessageQueue {
private store: Record<string, z.infer<typeof EnqueuedMessage>[]> = {};
private idGenerator: IMonotonicallyIncreasingIdGenerator;
constructor({
idGenerator,
}: {
idGenerator: IMonotonicallyIncreasingIdGenerator;
}) {
this.idGenerator = idGenerator;
}
async enqueue(runId: string, message: string): Promise<string> {
if (!this.store[runId]) {
this.store[runId] = [];
}
const id = await this.idGenerator.next();
this.store[runId].push({
messageId: id,
message,
});
return id;
}
async dequeue(runId: string): Promise<z.infer<typeof EnqueuedMessage> | null> {
if (!this.store[runId]) {
return null;
}
return this.store[runId].shift() ?? null;
}
}

View file

@ -1,90 +0,0 @@
import { ProviderV2 } from "@ai-sdk/provider";
import { createGateway } from "ai";
import { createOpenAI } from "@ai-sdk/openai";
import { createGoogleGenerativeAI } from "@ai-sdk/google";
import { createAnthropic } from "@ai-sdk/anthropic";
import { createOllama } from "ollama-ai-provider-v2";
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import { getModelConfig } from "../config/config.js";
const providerMap: Record<string, ProviderV2> = {};
export async function getProvider(name: string = ""): Promise<ProviderV2> {
// get model conf
const modelConfig = await getModelConfig();
if (!modelConfig) {
throw new Error("Model config not found");
}
if (!name) {
name = modelConfig.defaults.provider;
}
if (providerMap[name]) {
return providerMap[name];
}
const providerConfig = modelConfig.providers[name];
if (!providerConfig) {
throw new Error(`Provider ${name} not found`);
}
const { apiKey, baseURL, headers } = providerConfig;
switch (providerConfig.flavor) {
case "rowboat [free]":
providerMap[name] = createGateway({
apiKey: "rowboatx",
baseURL: "https://ai-gateway.rowboatlabs.com/v1/ai",
});
break;
case "openai":
providerMap[name] = createOpenAI({
apiKey,
baseURL,
headers,
});
break;
case "aigateway":
providerMap[name] = createGateway({
apiKey,
baseURL,
headers
});
break;
case "anthropic":
providerMap[name] = createAnthropic({
apiKey,
baseURL,
headers
});
break;
case "google":
providerMap[name] = createGoogleGenerativeAI({
apiKey,
baseURL,
headers
});
break;
case "ollama":
providerMap[name] = createOllama({
baseURL,
headers
});
break;
case "openai-compatible":
providerMap[name] = createOpenAICompatible({
name,
apiKey,
baseURL : baseURL || "",
headers,
});
break;
case "openrouter":
providerMap[name] = createOpenRouter({
apiKey,
baseURL,
headers
});
break;
default:
throw new Error(`Provider ${name} not found`);
}
return providerMap[name];
}

View file

@ -1,13 +0,0 @@
import { MessageList } from "../entities/message.js";
import { LlmStepStreamEvent } from "../entities/llm-step-events.js";
import { z } from "zod";
import { ToolAttachment } from "../entities/agent.js";
export type StepInputT = z.infer<typeof MessageList>;
export type StepOutputT = AsyncGenerator<z.infer<typeof LlmStepStreamEvent>, void, unknown>;
export interface Step {
execute(input: StepInputT): StepOutputT;
tools(): Record<string, z.infer<typeof ToolAttachment>>;
}

View file

@ -1,6 +1,6 @@
import { z } from "zod";
import { RunEvent } from "../entities/run-events.js";
import { LlmStepStreamEvent } from "../entities/llm-step-events.js";
import { RunEvent } from "../../entities/run-events.js";
import { LlmStepStreamEvent } from "../../entities/llm-step-events.js";
export interface StreamRendererOptions {
showHeaders?: boolean;