This commit is contained in:
Ariel Weinberger
2025-07-16 23:05:07 -05:00
parent 12273f087b
commit 9e04fc072e
11 changed files with 371 additions and 324 deletions
+5 -4
View File
@@ -7,18 +7,18 @@ import {
createUIMessageStreamResponse,
TextPart
} from 'ai';
import { createRuntimeContext, WriterType } from '@/lib/ai/mastra/utils/runtime-context';
import { mastra } from '@/lib/ai/mastra';
import { createRuntimeContext, WriterType } from '../../lib/ai/mastra/utils/runtime-context';
import { mastra } from '../../lib/ai/mastra';
import { exec as _exec } from 'child_process';
import { promisify } from 'util';
const exec = promisify(_exec);
import fs from 'fs';
import path from 'path';
import { createImagineClient } from '@/lib/imagine/create-artifact-client';
import { createImagineClient } from '../../lib/imagine/create-artifact-client';
export const handleChatRequest = async (c: Context) => {
const signal = c.req.raw.signal;
const token = c.req.header('X-Imagine-Token');
if (!token) {
return c.json({ error: 'Unauthorized' }, 401);
@@ -95,6 +95,7 @@ export const handleChatRequest = async (c: Context) => {
isFirstMessage: isNewConversation,
signal
});
c.set('runtimeContext', runtimeContext);
const run = await mastra.getWorkflow('codeWorkflow').createRunAsync();
const result = run.stream({
@@ -1,9 +1,9 @@
import { anthropic, AnthropicProviderOptions } from "@ai-sdk/anthropic";
import { anthropic } from "@ai-sdk/anthropic";
import { Agent } from "@mastra/core";
import { fileTools } from "../tools/file-tools";
import { smoothStream } from "ai";
import { RuntimeContextType } from "../utils/runtime-context";
import { AnthropicExtendedProviderOptions } from "@/lib/types";
import { imagineToMastraToolset } from "../tools/imagine-tool";
export const architectAgent = new Agent({
name: "Software Architect Agent",
@@ -16,10 +16,11 @@ NOTE: It is ALWAYS better to use the readMultipleFilesInParallel tool to read mu
// model: anthropic("claude-3-7-sonnet-20250219"),
// model: google("gemini-2.5-pro"),
// model: openai("gpt-4.1-nano"),
tools: {
tools: imagineToMastraToolset({
readFile: fileTools.readFileTool,
readMultipleFilesInParallel: fileTools.readMultipleFilesInParallelTool,
},
listFilesInDirectory: fileTools.listFilesInDirectoryTool,
}),
defaultStreamOptions: ({ runtimeContext }: { runtimeContext: RuntimeContextType }) => {
const abortSignal = runtimeContext.get("signal");
return {
@@ -4,7 +4,7 @@ import { fileTools } from "../tools/file-tools";
import { smoothStream } from "ai";
import z from "zod";
import { RuntimeContextType } from "../utils/runtime-context";
import { vercel } from "@ai-sdk/vercel"
import { imagineToMastraToolset } from "../tools/imagine-tool";
export const developerAgent = new Agent({
name: "Software Developer Agent",
@@ -22,7 +22,7 @@ You must always use the reportDone tool to report to the architect that you are
model: anthropic("claude-sonnet-4-20250514"),
// model: anthropic("claude-3-7-sonnet-20250219"),
// model: vercel("v0-1.5-md"),
tools: {
tools: imagineToMastraToolset({
readFile: fileTools.readFileTool,
writeFile: fileTools.writeFileTool,
listFilesInDirectory: fileTools.listFilesInDirectoryTool,
@@ -45,7 +45,7 @@ You must always use the reportDone tool to report to the architect that you are
return "Thanks for your summary!";
},
}),
},
}),
defaultStreamOptions: ({ runtimeContext }: { runtimeContext: RuntimeContextType }) => {
const abortSignal = runtimeContext.get("signal");
return {
+283 -275
View File
@@ -1,311 +1,319 @@
import { createTool } from "@mastra/core";
import { z } from "zod";
import { createSynapseClient, SynapseHTTPClient } from "@/lib/synapse-http-client";
import { createIdGenerator } from "ai";
import { getWriterFromContext, RuntimeContextType } from "../utils/runtime-context";
import { z } from 'zod';
import { SynapseHTTPClient } from '@/lib/synapse-http-client';
import { createIdGenerator } from 'ai';
import { getWriterFromContext, HonoEnv } from '../utils/runtime-context';
import { createImagineTool } from './imagine-tool';
import { getContext } from 'hono/context-storage';
const readMultipleFilesInParallelTool = createTool({
id: "readMultipleFilesInParallel",
description: "Read multiple files from the repository. This is always encouraged over a single readFile if you know you need to read multiple files.",
inputSchema: z.object({
paths: z.array(z.string()).describe("The paths to the files to read"),
}),
outputSchema: z.object({
files: z.array(z.object({
path: z.string().describe("The path to the file"),
content: z.string().describe("The content of the file"),
})),
}),
execute: async (params) => {
const { context: { paths } } = params;
const runtimeContext = params.runtimeContext as RuntimeContextType;
console.log(`[TOOL - readMultipleFilesInParallel]`, { paths });
const readMultipleFilesInParallelTool = createImagineTool({
id: 'readMultipleFilesInParallel',
description:
'Read multiple files from the repository. This is always encouraged over a single readFile if you know you need to read multiple files.',
inputSchema: z.object({
paths: z.array(z.string()).describe('The paths to the files to read')
}),
outputSchema: z.object({
files: z.array(
z.object({
path: z.string().describe('The path to the file'),
content: z.string().describe('The content of the file')
})
)
}),
execute: async ({ paths }) => {
console.log(`[TOOL - readMultipleFilesInParallel]`, { paths });
const files = await Promise.all(paths.map(async (path) => {
const result = await readFileTool.execute({
context: { path },
runtimeContext,
});
return result;
}));
const files = await Promise.all(
paths.map(async (path) => {
const result = await readFileTool.execute({
path
});
return result;
})
);
return { files };
}
})
const readFileTool = createTool({
id: "readFile",
description: "Read a file from the repository",
inputSchema: z.object({
path: z.string().describe("The path to the file to read"),
}),
outputSchema: z.object({
path: z.string().describe("The path to the file"),
content: z.string().describe("The content of the file"),
}),
execute: async (params) => {
const {
context: { path },
} = params;
console.log("[tool - readFile] params", params.context);
const runtimeContext = params.runtimeContext as RuntimeContextType;
const skipWritingToolCalls = runtimeContext.get("skipWritingToolCalls");
const writer = getWriterFromContext(runtimeContext);
const toolCallId = createIdGenerator({ size: 10 })();
if (!skipWritingToolCalls) {
writer.write({
type: "tool-input-available",
toolCallId,
toolName: "readFile",
input: {
path,
},
});
return { files };
}
const synapse = runtimeContext.get("synapseClient");
const result = await synapse.readFile({ path });
console.log(`[TOOL - readFile] Reading file at path: ${path}`);
if (!skipWritingToolCalls) {
writer.write({
type: "tool-output-available",
toolCallId,
output: {
success: true,
},
});
}
return {
path: result.path,
content: result.content,
};
},
});
const writeFilesTool = createTool({
id: "writeFiles",
description: "Write multiple files to the repository",
inputSchema: z.object({
files: z.array(
z.object({
path: z.string().describe("The path to the file to write"),
content: z.string().describe("The content of the file to write"),
})
),
}),
outputSchema: z.object({
successFiles: z.array(
z.object({ path: z.string().describe("The path to the file") })
),
errorFiles: z.array(
z.object({
path: z.string().describe("The path to the file"),
error: z.string().describe("The error message"),
})
),
}),
execute: async (params) => {
const { context: { files } } = params;
const runtimeContext = params.runtimeContext as RuntimeContextType;
const synapse = runtimeContext.get("synapseClient");
const readFileTool = createImagineTool({
id: 'readFile',
description: 'Read a file from the repository',
inputSchema: z.object({
path: z.string().describe('The path to the file to read')
}),
outputSchema: z.object({
path: z.string().describe('The path to the file'),
content: z.string().describe('The content of the file')
}),
execute: async ({ path }) => {
const runtimeContext = getContext<HonoEnv>().var.runtimeContext;
const skipWritingToolCalls = runtimeContext.get('skipWritingToolCalls');
const writer = getWriterFromContext(runtimeContext);
const toolCallId = createIdGenerator({ size: 10 })();
const { successFiles, errorFiles } = await writeFiles(files, synapse);
if (!skipWritingToolCalls) {
writer.write({
type: 'tool-input-available',
toolCallId,
toolName: 'readFile',
input: {
path
}
});
}
const synapse = runtimeContext.get('synapseClient');
const result = await synapse.readFile({ path });
console.log(`[TOOL - readFile] Reading file at path: ${path}`);
return {
successFiles,
errorFiles,
};
},
if (!skipWritingToolCalls) {
writer.write({
type: 'tool-output-available',
toolCallId,
output: {
success: true
}
});
}
return {
path: result.path,
content: result.content
};
}
});
const writeFileTool = createTool({
id: "writeFile",
description:
"Write a single file to the repository. If the file does not exist, it will be created. If the file exists, it will be overwritten. You must provide the full file contents.",
inputSchema: z.object({
path: z.string().describe("The path to the file to write"),
content: z.string().describe("The content of the file to write"),
}),
outputSchema: z.object({
success: z.boolean().describe("Whether the file was written successfully"),
error: z
.string()
.optional()
.describe("The error message if the file was not written successfully"),
}),
execute: async (params) => {
const {
context: { path, content },
} = params;
const runtimeContext = params.runtimeContext as RuntimeContextType;
const skipWritingToolCalls = runtimeContext.get("skipWritingToolCalls");
const writer = getWriterFromContext(runtimeContext);
const synapse = runtimeContext.get("synapseClient");
const writeFilesTool = createImagineTool({
id: 'writeFiles',
description: 'Write multiple files to the repository',
inputSchema: z.object({
files: z.array(
z.object({
path: z.string().describe('The path to the file to write'),
content: z.string().describe('The content of the file to write')
})
)
}),
outputSchema: z.object({
successFiles: z.array(z.object({ path: z.string().describe('The path to the file') })),
errorFiles: z.array(
z.object({
path: z.string().describe('The path to the file'),
error: z.string().describe('The error message')
})
)
}),
execute: async ({ files }) => {
const runtimeContext = getContext<HonoEnv>().var.runtimeContext;
const synapse = runtimeContext.get('synapseClient');
const toolCallId = createIdGenerator({ size: 10 })();
const { successFiles, errorFiles } = await writeFiles(files, synapse);
if (!skipWritingToolCalls) {
writer.write({
type: "tool-input-available",
toolCallId,
toolName: "writeFile",
input: {
path,
content,
},
});
return {
successFiles,
errorFiles
};
}
});
const { errorFiles } = await writeFiles([{ path, content }], synapse);
const writeFileTool = createImagineTool({
id: 'writeFile',
description:
'Write a single file to the repository. If the file does not exist, it will be created. If the file exists, it will be overwritten. You must provide the full file contents.',
inputSchema: z.object({
path: z.string().describe('The path to the file to write'),
content: z.string().describe('The content of the file to write')
}),
outputSchema: z.object({
success: z.boolean().describe('Whether the file was written successfully'),
error: z
.string()
.optional()
.describe('The error message if the file was not written successfully')
}),
execute: async ({ path, content }) => {
const runtimeContext = getContext<HonoEnv>().var.runtimeContext;
const skipWritingToolCalls = runtimeContext.get('skipWritingToolCalls');
const writer = getWriterFromContext(runtimeContext);
const synapse = runtimeContext.get('synapseClient');
if (errorFiles.length > 0) {
return {
success: false,
error: errorFiles[0].error,
};
} else {
if (!skipWritingToolCalls) {
writer.write({
type: "tool-output-available",
toolCallId,
output: {
success: true,
},
});
}
return {
success: true,
};
const toolCallId = createIdGenerator({ size: 10 })();
if (!skipWritingToolCalls) {
writer.write({
type: 'tool-input-available',
toolCallId,
toolName: 'writeFile',
input: {
path,
content
}
});
}
const { errorFiles } = await writeFiles([{ path, content }], synapse);
if (errorFiles.length > 0) {
return {
success: false,
error: errorFiles[0].error
};
} else {
if (!skipWritingToolCalls) {
writer.write({
type: 'tool-output-available',
toolCallId,
output: {
success: true
}
});
}
return {
success: true
};
}
}
},
});
async function writeFiles(files: { path: string; content: string }[], synapse: SynapseHTTPClient) {
const successFiles: { path: string }[] = [];
const errorFiles: { path: string; error: string }[] = [];
const successFiles: { path: string }[] = [];
const errorFiles: { path: string; error: string }[] = [];
for (const file of files) {
try {
await synapse.createOrUpdateFile({
filepath: file.path,
content: file.content,
});
successFiles.push({ path: file.path });
} catch (error) {
errorFiles.push({
path: file.path,
error: error instanceof Error ? error.message : "Unknown error",
});
for (const file of files) {
try {
await synapse.createOrUpdateFile({
filepath: file.path,
content: file.content
});
successFiles.push({ path: file.path });
} catch (error) {
errorFiles.push({
path: file.path,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
}
return {
successFiles,
errorFiles,
};
return {
successFiles,
errorFiles
};
}
const listFilesInDirectoryTool = createTool({
id: "listFilesInDirectory",
description: "List all files in a directory",
inputSchema: z.object({
path: z
.string()
.default("/")
.describe("The path to the directory to list files from"),
recursive: z
.boolean()
.default(false)
.describe("Whether to list files recursively (dig into subdirectories)"),
}),
outputSchema: z.object({
files: z.array(
z.object({
path: z.string().describe("The path to the file"),
})
),
}),
execute: async (params) => {
const { context: { path, recursive } } = params;
const runtimeContext = params.runtimeContext as RuntimeContextType;
const synapse = runtimeContext.get("synapseClient");
console.log(
`[TOOL - listFilesInDirectory] Listing files in directory at path: ${path}, recursive: ${recursive}`
);
const files = await synapse.listFilesInDir({
dirPath: path || "/",
recursive: recursive || false,
withContent: false,
additionalIgnorePatterns: [],
});
const listFilesInDirectoryTool = createImagineTool({
id: 'listFilesInDirectory',
description: 'List all files in a directory',
inputSchema: z.object({
path: z.string().default('/').describe('The path to the directory to list files from'),
recursive: z
.boolean()
.default(false)
.describe('Whether to list files recursively (dig into subdirectories)')
}),
outputSchema: z.object({
files: z.array(
z.object({
path: z.string().describe('The path to the file')
})
)
}),
execute: async ({ path, recursive }) => {
const runtimeContext = getContext<HonoEnv>().var.runtimeContext;
const synapse = runtimeContext.get('synapseClient');
console.log(
`[TOOL - listFilesInDirectory] Listing files in directory at path: ${path}, recursive: ${recursive}`
);
const files = await synapse.listFilesInDir({
dirPath: path || '/',
recursive: recursive || false,
withContent: false,
additionalIgnorePatterns: []
});
console.log(
`[TOOL - listFilesInDirectory] Found ${files.length} files in directory at path: ${path}`
);
console.log(
`[TOOL - listFilesInDirectory] Found ${files.length} files in directory at path: ${path}`
);
return {
files,
};
},
return {
files
};
}
});
const deleteFileTool = createTool({
id: "deleteFile",
description: "Delete a file from the repository",
inputSchema: z.object({
path: z.string().describe("The path to the file to delete"),
}),
outputSchema: z.object({
success: z.boolean().describe("Whether the file was deleted successfully"),
}),
execute: async (params) => {
const { context: { path } } = params;
const runtimeContext = params.runtimeContext as RuntimeContextType;
const synapse = runtimeContext.get("synapseClient");
await synapse.deleteFile({ filepath: path });
const deleteFileTool = createImagineTool({
id: 'deleteFile',
description: 'Delete a file from the repository',
inputSchema: z.object({
path: z.string().describe('The path to the file to delete')
}),
outputSchema: z.object({
success: z.boolean().describe('Whether the file was deleted successfully')
}),
execute: async ({ path }) => {
const runtimeContext = getContext<HonoEnv>().var.runtimeContext;
const synapse = runtimeContext.get('synapseClient');
await synapse.deleteFile({ filepath: path });
return {
success: true,
};
},
return {
success: true
};
}
});
const moveFileTool = createTool({
id: "moveFile",
description:
"Move a file from one path to another. Also useful when renaming. If the directory does not exist, it will be created.",
inputSchema: z.object({
path: z.string().describe("The path to the file to move"),
newPath: z.string().describe("The new path to the file"),
intent: z
.enum(["move", "rename"])
.describe(
"The intent of the move operation. This will help render the appropriate UI to the user."
),
}),
outputSchema: z.object({
success: z.boolean().describe("Whether the file was moved successfully"),
}),
execute: async (params) => {
const { context: { path, newPath } } = params;
const runtimeContext = params.runtimeContext as RuntimeContextType;
const synapse = runtimeContext.get("synapseClient");
await synapse.updateFilePath({ filepath: path, newPath });
const moveFileTool = createImagineTool({
id: 'moveFile',
description:
'Move a file from one path to another. Also useful when renaming. If the directory does not exist, it will be created.',
inputSchema: z.object({
path: z.string().describe('The path to the file to move'),
newPath: z.string().describe('The new path to the file'),
intent: z
.enum(['move', 'rename'])
.describe(
'The intent of the move operation. This will help render the appropriate UI to the user.'
)
}),
outputSchema: z.object({
success: z.boolean().describe('Whether the file was moved successfully')
}),
execute: async ({ path, newPath }) => {
const runtimeContext = getContext<HonoEnv>().var.runtimeContext;
const synapse = runtimeContext.get('synapseClient');
await synapse.updateFilePath({ filepath: path, newPath });
return {
success: true,
};
},
return {
success: true
};
}
});
export const fileTools = {
readFileTool,
// writeFilesTool,
writeFileTool,
listFilesInDirectoryTool,
deleteFileTool,
moveFileTool,
readMultipleFilesInParallelTool,
readFileTool,
writeFileTool,
listFilesInDirectoryTool,
deleteFileTool,
moveFileTool,
readMultipleFilesInParallelTool
};
// function convertMastraToolToVercelTool<
// TInputSchema extends z.ZodSchema,
// TOutputSchema extends z.ZodSchema
// >(mastraTool: {
// description: string;
// inputSchema: TInputSchema;
// outputSchema: TOutputSchema;
// execute: any;
// }): VercelTool<z.infer<TInputSchema>, z.infer<TOutputSchema>> {
// return {
// description: mastraTool.description,
// inputSchema: mastraTool.inputSchema,
// outputSchema: mastraTool.outputSchema,
// execute: mastraTool.execute,
// } as VercelTool<z.infer<TInputSchema>, z.infer<TOutputSchema>>;
// }
// export const fileToolsVercel = {
// readFileTool: convertMastraToolToVercelTool<typeof readFileTool.inputSchema, typeof readFileTool.outputSchema>(readFileTool)
// };
@@ -0,0 +1,64 @@
import { Tool as MastraTool, ToolExecutionContext } from '@mastra/core';
import z from 'zod';
export type ImagineTool<TInput extends z.ZodSchema, TOutput extends z.ZodSchema> = {
id: string;
description: string;
inputSchema: TInput;
outputSchema: TOutput;
execute: (input: z.infer<TInput>) => Promise<z.infer<TOutput>> | z.infer<TOutput>;
};
export const createImagineTool = <TInput extends z.ZodSchema, TOutput extends z.ZodSchema>({
id,
description,
inputSchema,
outputSchema,
execute
}: {
id: string;
description: string;
inputSchema: TInput;
outputSchema: TOutput;
execute: (input: z.infer<TInput>) => Promise<z.infer<TOutput>> | z.infer<TOutput>;
}): ImagineTool<TInput, TOutput> => {
return {
id,
description,
inputSchema,
outputSchema,
execute
};
};
export const mastraToImagineToolCallAdapter = <
TInput extends z.ZodSchema,
TOutput extends z.ZodSchema
>(
tool: ImagineTool<TInput, TOutput>
): MastraTool<TInput, TOutput, ToolExecutionContext<TInput>> => {
return {
id: tool.id,
description: tool.description,
inputSchema: tool.inputSchema,
outputSchema: tool.outputSchema,
__isMastraTool: true,
execute: async (params: ToolExecutionContext<TInput>) => {
return tool.execute(params.context);
}
};
};
export const imagineToMastraToolset = <
T extends Record<string, ImagineTool<any, any>>
>(
tools: T
): {
[K in keyof T]: T[K] extends ImagineTool<infer TInput, infer TOutput>
? MastraTool<TInput, TOutput, ToolExecutionContext<TInput>>
: never;
} => {
return Object.fromEntries(
Object.entries(tools).map(([key, tool]) => [key, mastraToImagineToolCallAdapter(tool)])
) as any;
};
@@ -1,7 +1,7 @@
import { RuntimeContext } from "@mastra/core/runtime-context";
import { UIMessageStreamWriter } from "ai";
import { createSynapseClient, SynapseHTTPClient } from "@/lib/synapse-http-client";
import { ImagineUIMessage } from "@/shared-types";
import { createSynapseClient, SynapseHTTPClient } from "../../../synapse-http-client";
import { ImagineUIMessage } from "../../../../shared-types";
export type WriterType = UIMessageStreamWriter<ImagineUIMessage>;
export type RuntimeContextPayload = {
@@ -14,6 +14,11 @@ export type RuntimeContextPayload = {
synapseClient: SynapseHTTPClient;
}
export type RuntimeContextType = RuntimeContext<RuntimeContextPayload>;
export type HonoEnv = {
Variables: {
runtimeContext: RuntimeContextType,
}
}
export const cloneRuntimeContext = (runtimeContext: RuntimeContextType, overrides?: Partial<RuntimeContextPayload>): RuntimeContextType => {
const newRuntimeContext = new RuntimeContext<RuntimeContextPayload>();
@@ -13,7 +13,7 @@ import {
getPagesAndComponents,
systemPromptPartsArrayToModelMessages,
} from "../../system-prompt/system-prompt-utils";
import { GitRepositoryUtils } from "@/lib/git-utils";
import { GitRepositoryUtils } from "../../../../lib/git-utils";
import { currentCode } from "../../system-prompt/system-prompt-parts";
import { mastra } from "..";
import { cacheSystemMessage } from "../utils";
@@ -24,7 +24,6 @@ import {
} from "../utils/runtime-context";
import { openai } from "@ai-sdk/openai";
import * as systemPromptParts from "../../system-prompt/system-prompt-parts";
import fs from "fs";
import { anthropic } from "@ai-sdk/anthropic";
const planStep = createStep({
+1 -8
View File
@@ -11,15 +11,8 @@ import { fileURLToPath } from 'url';
import { handleChatRequest } from './handlers/chat/route';
import { getConversation, getConversations } from './handlers/conversation';
import { contextStorage } from "hono/context-storage";
import { RuntimeContextType } from './lib/ai/mastra/utils/runtime-context';
export type HonoEnv = {
Variables: {
runtimeContext: RuntimeContextType,
}
}
const app = new Hono<HonoEnv>();
const app = new Hono();
// Middleware
app.use('*', cors());
+1 -1
View File
@@ -18,4 +18,4 @@ export type ImagineUIDataParts = InferUIDataParts<{
thinking: typeof thinkingUIDataPartSchema;
}>;
export type ImagineUIMessage = UIMessage<never, ImagineUIDataParts, ImagineTools>;
export type ImagineUIMessage = UIMessage<never, ImagineUIDataParts, ImagineTools>;
+1 -1
View File
@@ -20,5 +20,5 @@
}
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
"exclude": ["node_modules", "dist", "tmp"]
}
-24
View File
@@ -1,24 +0,0 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": false,
"outDir": "./dist",
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"declaration": true,
"declarationMap": true,
"jsx": "preserve",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist"]
}