rowboat/apps/cli/src/application/lib/agent.ts

214 lines
7.1 KiB
TypeScript
Raw Normal View History

2025-11-07 11:42:10 +05:30
import { Message, MessageList } from "../entities/message.js";
2025-10-28 13:17:06 +05:30
import { z } from "zod";
2025-11-07 11:42:10 +05:30
import { Step, StepInputT, StepOutputT } from "./step.js";
2025-10-28 13:17:06 +05:30
import { openai } from "@ai-sdk/openai";
2025-11-07 11:42:10 +05:30
import { google } from "@ai-sdk/google";
import { generateText, ModelMessage, stepCountIs, streamText, tool, Tool, ToolSet, jsonSchema } from "ai";
import { Agent, AgentTool } from "../entities/agent.js";
2025-10-28 13:17:06 +05:30
import { WorkDir } from "../config/config.js";
import fs from "fs";
import path from "path";
2025-11-07 11:42:10 +05:30
import { loadWorkflow } from "./utils.js";
const BashTool = tool({
description: "Run a command in the shell",
inputSchema: z.object({
command: z.string(),
}),
});
const AskHumanTool = tool({
description: "Ask the human for input",
inputSchema: z.object({
question: z.string(),
}),
});
function mapAgentTool(t: z.infer<typeof AgentTool>): Tool {
switch (t.type) {
case "mcp":
return tool({
name: t.name,
description: t.description,
inputSchema: jsonSchema(t.inputSchema),
});
case "workflow":
const workflow = loadWorkflow(t.name);
if (!workflow) {
throw new Error(`Workflow ${t.name} not found`);
}
return tool({
name: t.name,
description: workflow.description,
inputSchema: z.object({
message: z.string().describe("The message to send to the workflow"),
}),
});
case "builtin":
switch (t.name) {
case "bash":
return BashTool;
default:
throw new Error(`Unknown builtin tool: ${t.name}`);
}
}
}
2025-10-28 13:17:06 +05:30
function convertFromMessages(messages: z.infer<typeof Message>[]): ModelMessage[] {
const result: ModelMessage[] = [];
for (const msg of messages) {
switch (msg.role) {
case "assistant":
if (typeof msg.content === 'string') {
result.push({
role: "assistant",
content: msg.content,
});
} 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,
};
}
}),
});
}
break;
case "system":
result.push({
role: "system",
content: msg.content,
});
break;
case "user":
result.push({
role: "user",
content: msg.content,
});
break;
2025-11-07 11:42:10 +05:30
case "tool":
result.push({
role: "tool",
content: [
{
type: "tool-result",
toolCallId: msg.toolCallId,
toolName: msg.toolName,
output: {
type: "text",
value: msg.content,
},
},
],
});
break;
2025-10-28 13:17:06 +05:30
}
}
return result;
}
2025-11-07 11:42:10 +05:30
export class AgentNode implements Step {
2025-10-28 13:17:06 +05:30
private id: string;
2025-11-07 11:42:10 +05:30
private background: boolean;
private agent: z.infer<typeof Agent>;
2025-10-28 13:17:06 +05:30
2025-11-07 11:42:10 +05:30
constructor(id: string, background: boolean) {
2025-10-28 13:17:06 +05:30
this.id = id;
2025-11-07 11:42:10 +05:30
this.background = background;
2025-10-28 13:17:06 +05:30
const agentPath = path.join(WorkDir, "agents", `${id}.json`);
const agent = fs.readFileSync(agentPath, "utf8");
2025-11-07 11:42:10 +05:30
this.agent = Agent.parse(JSON.parse(agent));
}
tools(): Record<string, z.infer<typeof AgentTool>> {
return this.agent.tools ?? {};
2025-10-28 13:17:06 +05:30
}
2025-11-07 11:42:10 +05:30
async* execute(input: StepInputT): StepOutputT {
// console.log("\n\n\t>>>>\t\tinput", JSON.stringify(input));
const tools: ToolSet = {};
2025-11-08 09:12:02 +05:30
// if (!this.background) {
// tools["ask-human"] = AskHumanTool;
// }
2025-11-07 11:42:10 +05:30
for (const [name, tool] of Object.entries(this.agent.tools ?? {})) {
try {
tools[name] = mapAgentTool(tool);
} catch (error) {
console.error(`Error mapping tool ${name}:`, error);
continue;
}
}
// console.log("\n\n\t>>>>\t\ttools", JSON.stringify(tools, null, 2));
const { fullStream } = streamText({
model: openai("gpt-4.1"),
// model: google("gemini-2.5-pro"),
2025-10-28 13:17:06 +05:30
messages: convertFromMessages(input),
2025-11-07 11:42:10 +05:30
system: this.agent.instructions,
2025-10-28 13:17:06 +05:30
stopWhen: stepCountIs(1),
2025-11-07 11:42:10 +05:30
tools,
2025-10-28 13:17:06 +05:30
});
for await (const event of fullStream) {
2025-11-07 11:42:10 +05:30
// console.log("\n\n\t>>>>\t\tstream event", JSON.stringify(event));
2025-10-28 13:17:06 +05:30
switch (event.type) {
case "reasoning-start":
yield {
type: "reasoning-start",
};
break;
case "reasoning-delta":
yield {
type: "reasoning-delta",
delta: event.text,
};
break;
case "reasoning-end":
yield {
type: "reasoning-end",
};
break;
case "text-start":
yield {
type: "text-start",
};
break;
case "text-delta":
yield {
type: "text-delta",
delta: event.text,
};
break;
case "tool-call":
yield {
type: "tool-call",
toolCallId: event.toolCallId,
toolName: event.toolName,
input: event.input,
};
break;
case "finish":
yield {
type: "usage",
usage: event.totalUsage,
};
break;
default:
// console.warn("Unknown event type", event);
continue;
}
}
}
}