alchemy 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +324 -0
  2. package/package.json +15 -8
  3. package/src/$.ts +36 -0
  4. package/src/agent/ai.ts +215 -0
  5. package/src/{components/agent → agent}/anthropic.ts +6 -2
  6. package/src/agent/dependencies.ts +62 -0
  7. package/src/agent/file-context.ts +8 -0
  8. package/src/agent/index.ts +9 -0
  9. package/src/agent/model.ts +39 -0
  10. package/src/agent/module.ts +418 -0
  11. package/src/{components/agent → agent}/openai.ts +6 -0
  12. package/src/agent/program.md +38 -0
  13. package/src/agent/program.ts +37 -0
  14. package/src/agent/prompts.ts +20 -0
  15. package/src/alchemize.ts +45 -14
  16. package/src/apply.ts +25 -10
  17. package/src/{components/aws → aws}/bucket.ts +2 -2
  18. package/src/{components/aws → aws}/function.ts +2 -2
  19. package/src/{components/aws → aws}/policy.ts +2 -2
  20. package/src/{components/aws → aws}/queue.ts +1 -1
  21. package/src/{components/aws → aws}/role.ts +2 -2
  22. package/src/{components/aws → aws}/table.ts +2 -2
  23. package/src/destroy.ts +36 -12
  24. package/src/{components/esbuild.ts → esbuild.ts} +1 -1
  25. package/src/{components/fs.ts → fs.ts} +20 -18
  26. package/src/global.ts +7 -14
  27. package/src/index.ts +11 -2
  28. package/src/input.ts +1 -1
  29. package/src/markdown/design.ts +125 -0
  30. package/src/markdown/extract.ts +9 -0
  31. package/src/markdown/index.ts +3 -0
  32. package/src/{components/agent → markdown}/requirements.ts +53 -29
  33. package/src/output.ts +6 -0
  34. package/src/resource.ts +208 -75
  35. package/src/scope.ts +56 -0
  36. package/src/slug.ts +3 -0
  37. package/src/state.ts +66 -38
  38. package/src/typescript/check-omission.ts +47 -0
  39. package/src/typescript/debug-type-errors.ts +42 -0
  40. package/src/typescript/extract.ts +9 -0
  41. package/src/{components/agent → typescript}/index.ts +3 -1
  42. package/src/typescript/install-packages.ts +50 -0
  43. package/src/{components/agent → typescript}/package.ts +44 -27
  44. package/src/typescript/repair-omissions.ts +31 -0
  45. package/src/typescript/repair.ts +45 -0
  46. package/src/{components/agent → typescript}/tsconfig.ts +37 -17
  47. package/src/typescript/typescript.ts +217 -0
  48. package/src/typescript/validate.ts +73 -0
  49. package/test/scope.test.ts +67 -0
  50. package/src/components/agent/agent.ts +0 -45
  51. package/src/components/agent/model.ts +0 -22
  52. package/src/components/agent/typescript.ts +0 -177
  53. /package/src/{components/agent → agent}/scrape.ts +0 -0
  54. /package/src/{components/aws → aws}/index.ts +0 -0
@@ -0,0 +1,217 @@
1
+ import type { CoreMessage } from "ai";
2
+ import { mkdir, writeFile } from "fs/promises";
3
+ import { dirname } from "path";
4
+ import { z } from "zod";
5
+ import { ModelId, generateText, resolveModel } from "../agent";
6
+ import { dependenciesAsMessages } from "../agent/dependencies";
7
+ import { FileContext } from "../agent/file-context";
8
+ import { rm } from "../fs";
9
+ import { type Context, Resource } from "../resource";
10
+ import { checkForCodeOmission } from "./check-omission";
11
+ import { debugTypeErrors } from "./debug-type-errors";
12
+ import { extractTypeScriptCode } from "./extract";
13
+ import { repairTypeScriptCode } from "./repair";
14
+ import { repairCodeOmissions } from "./repair-omissions";
15
+ import { validateTypeScript } from "./validate";
16
+
17
+ export type TypeScriptFileInput = z.infer<typeof TypeScriptFileInput>;
18
+
19
+ const TypeScriptFileInput = z.object({
20
+ /**
21
+ * The ID of the model to use for generating TypeScript code
22
+ * @default "gpt-4o"
23
+ */
24
+ modelId: ModelId.optional(),
25
+
26
+ /**
27
+ * The name of the file to write the code to
28
+ */
29
+ path: z.string(),
30
+
31
+ /**
32
+ * The requirements for the TypeScript file
33
+ */
34
+ requirements: z.string(),
35
+
36
+ /**
37
+ * List of other code files that it depends on
38
+ */
39
+ dependencies: z.array(FileContext).optional(),
40
+
41
+ /**
42
+ * Temperature setting for model generation (higher = more creative, lower = more focused)
43
+ * @default 0.7
44
+ */
45
+ temperature: z.number().optional(),
46
+
47
+ /**
48
+ * Whether to perform TypeScript type checking on the generated code
49
+ * @default false
50
+ */
51
+ typeCheck: z.boolean().optional(),
52
+
53
+ /**
54
+ * Path to the tsconfig.json file to use for validation
55
+ * If not provided, will look for tsconfig.json in the project root
56
+ * Only used if typeCheck is true
57
+ */
58
+ tsconfigPath: z.string().optional(),
59
+
60
+ /**
61
+ * Project root directory. Used to resolve tsconfig.json and module imports
62
+ * If not provided, will use the directory of the target file
63
+ * Only used if typeCheck is true
64
+ */
65
+ projectRoot: z.string().optional(),
66
+ });
67
+
68
+ export type TypeScriptFileOutput = z.infer<typeof TypeScriptFileOutput>;
69
+ export const TypeScriptFileOutput = FileContext;
70
+
71
+ export class TypeScriptFile extends Resource(
72
+ "code::TypeScriptFile",
73
+ async (ctx: Context<TypeScriptFileOutput>, props: TypeScriptFileInput) => {
74
+ if (ctx.event === "delete") {
75
+ await rm(props.path);
76
+ return;
77
+ }
78
+
79
+ console.log("Implementing", props.path);
80
+
81
+ // Get the appropriate model based on the ID
82
+ const model = await resolveModel(props.modelId ?? "gpt-4o");
83
+
84
+ const messages: CoreMessage[] = [
85
+ {
86
+ role: "system" as const,
87
+ content:
88
+ "You are an expert TypeScript developer that generates clean, well-documented code following best practices. " +
89
+ "Your response must follow this exact format:\n" +
90
+ "1. A single sentence describing what the code will do\n" +
91
+ "2. A single TypeScript code block surrounded by ```ts and ``` tags containing the COMPLETE implementation\n" +
92
+ "IMPORTANT: You must ALWAYS provide the ENTIRE file contents, including ALL imports, types, and implementations.\n" +
93
+ "NEVER output partial code or omit any functionality from the original file when making changes.\n" +
94
+ "Do not include any other explanations or multiple code blocks.",
95
+ },
96
+ ...dependenciesAsMessages(props.dependencies),
97
+ {
98
+ role: "user" as const,
99
+ content: `Please generate TypeScript code based on the following specifications:
100
+
101
+ Requirements:
102
+ ${props.requirements}
103
+
104
+ The code should:
105
+ 1. Be well-documented with JSDoc comments
106
+ 2. Follow TypeScript best practices
107
+ 3. Include proper type definitions
108
+ 4. Be ready to use with the provided context files
109
+ 5. Include ALL necessary imports and dependencies
110
+ 6. Contain the COMPLETE implementation with no omissions`,
111
+ },
112
+ ];
113
+
114
+ const maxAttempts = 3;
115
+ let code: string | undefined;
116
+ let typeErrors: string | undefined;
117
+
118
+ // Try generating and validating code up to maxAttempts times
119
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
120
+ try {
121
+ // After 2 failed attempts, consult o3-mini for analysis
122
+ if (attempt === maxAttempts) {
123
+ if (!ctx.quiet) {
124
+ console.log("[TypeScript] Consulting expert for code analysis...");
125
+ }
126
+ const diagnosis = await debugTypeErrors(messages, typeErrors!);
127
+
128
+ messages.push({
129
+ role: "user",
130
+ content: diagnosis,
131
+ });
132
+ }
133
+
134
+ // Generate the TypeScript code
135
+ if (!ctx.quiet) {
136
+ console.log(`[TypeScript] Generating code attempt ${attempt}...`);
137
+ }
138
+ const { text } = await generateText({
139
+ model,
140
+ temperature: Math.max(
141
+ 0.1,
142
+ (props.temperature ?? 0.7) * (1 - attempt * 0.2),
143
+ ),
144
+ messages,
145
+ });
146
+
147
+ code = extractTypeScriptCode(text);
148
+ if (!code) {
149
+ console.log("Format mismatch");
150
+ messages.push(
151
+ {
152
+ role: "assistant" as const,
153
+ content: text,
154
+ },
155
+ {
156
+ role: "user" as const,
157
+ content:
158
+ "Your response did not match the required format. Please provide your response with exactly one sentence summary" +
159
+ " followed by a single TypeScript code block surrounded by ```ts and ``` tags containing the COMPLETE implementation. No other text or explanations.",
160
+ },
161
+ );
162
+ continue;
163
+ }
164
+
165
+ // First check for omissions
166
+ if (!ctx.quiet) {
167
+ console.log("[TypeScript] Checking for code omissions...");
168
+ }
169
+ const hasOmissions = await checkForCodeOmission(code);
170
+ if (hasOmissions) {
171
+ code = await repairCodeOmissions(model, messages);
172
+ }
173
+
174
+ // Ensure the directory exists
175
+ await mkdir(dirname(props.path), { recursive: true });
176
+
177
+ // Write the TypeScript file
178
+ await writeFile(props.path, code);
179
+
180
+ if (props.typeCheck) {
181
+ typeErrors = await validateTypeScript(props.path, {
182
+ tsconfigPath: props.tsconfigPath,
183
+ projectRoot: props.projectRoot,
184
+ });
185
+ if (typeErrors) {
186
+ if (attempt === maxAttempts) {
187
+ throw new Error(
188
+ `Failed to generate type-safe code after maximum attempts. Errors:\n${typeErrors}`,
189
+ );
190
+ } else {
191
+ code = await repairTypeScriptCode(model, typeErrors, messages);
192
+ continue;
193
+ }
194
+ }
195
+ }
196
+
197
+ // If we get here, the code is valid
198
+ return {
199
+ path: props.path,
200
+ content: code,
201
+ };
202
+ } catch (error) {
203
+ // On the last attempt, rethrow the error
204
+ if (attempt === maxAttempts) {
205
+ throw error;
206
+ }
207
+ // Otherwise continue to the next attempt
208
+ if (!ctx.quiet) {
209
+ console.log(`Attempt ${attempt} failed:`, error);
210
+ }
211
+ }
212
+ }
213
+
214
+ // This should never be reached due to the throws above
215
+ throw new Error("Unexpected code generation failure");
216
+ },
217
+ ) {}
@@ -0,0 +1,73 @@
1
+ import { dirname, resolve } from "path";
2
+ import * as ts from "typescript";
3
+
4
+ export interface TypeCheckOptions {
5
+ tsconfigPath?: string;
6
+ projectRoot?: string;
7
+ }
8
+
9
+ /**
10
+ * Validates TypeScript code and returns formatted error messages if there are any type errors
11
+ * @returns Formatted error message if there are errors, undefined if valid
12
+ */
13
+ export async function validateTypeScript(
14
+ filePath: string,
15
+ options: TypeCheckOptions,
16
+ ): Promise<string | undefined> {
17
+ const projectRoot = options.projectRoot ?? dirname(filePath);
18
+ const tsconfigPath =
19
+ options.tsconfigPath ?? resolve(projectRoot, "tsconfig.json");
20
+
21
+ // Load tsconfig.json if it exists
22
+ let compilerOptions: ts.CompilerOptions = {
23
+ target: ts.ScriptTarget.ESNext,
24
+ module: ts.ModuleKind.ESNext,
25
+ strict: true,
26
+ esModuleInterop: true,
27
+ skipLibCheck: true,
28
+ noEmit: true,
29
+ };
30
+
31
+ if (ts.sys.fileExists(tsconfigPath)) {
32
+ const { config, error } = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
33
+ if (error) {
34
+ throw new Error(`Error reading tsconfig.json: ${error.messageText}`);
35
+ }
36
+
37
+ const { options: parsedOptions, errors } = ts.parseJsonConfigFileContent(
38
+ config,
39
+ ts.sys,
40
+ projectRoot,
41
+ );
42
+
43
+ if (errors.length) {
44
+ throw new Error(
45
+ `Error parsing tsconfig.json: ${errors.map((e) => e.messageText).join("\n")}`,
46
+ );
47
+ }
48
+
49
+ compilerOptions = parsedOptions;
50
+ }
51
+
52
+ // Create program
53
+ const program = ts.createProgram({
54
+ rootNames: [filePath],
55
+ options: compilerOptions,
56
+ });
57
+
58
+ // Get diagnostics
59
+ const diagnostics = [
60
+ ...program.getSemanticDiagnostics(),
61
+ ...program.getSyntacticDiagnostics(),
62
+ ];
63
+
64
+ if (diagnostics.length > 0) {
65
+ return ts.formatDiagnosticsWithColorAndContext(diagnostics, {
66
+ getCanonicalFileName: (path) => path,
67
+ getCurrentDirectory: () => projectRoot,
68
+ getNewLine: () => ts.sys.newLine,
69
+ });
70
+ }
71
+
72
+ return undefined;
73
+ }
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { apply } from "../src/apply";
3
+ import type { PolicyDocument } from "../src/components/aws/policy";
4
+ import { Role } from "../src/components/aws/role";
5
+ import { File } from "../src/components/fs";
6
+ import { destroy } from "../src/destroy";
7
+ import { rootScope } from "../src/global";
8
+ import { type Context, Resource } from "../src/resource";
9
+ import { Scope, getScope, withScope } from "../src/scope";
10
+
11
+ describe("Scope", () => {
12
+ it("should maintain scope context and track resources", async () => {
13
+ const testScope = new Scope("test");
14
+
15
+ await withScope(testScope, async () => {
16
+ new File("test-file", "test.txt", "Hello World");
17
+
18
+ const currentScope = getScope();
19
+ expect(currentScope).toEqual(testScope);
20
+ });
21
+ expect(testScope.nodes.size).toBe(1);
22
+ expect(testScope).toBe(testScope);
23
+ expect(testScope).not.toBe(rootScope);
24
+ });
25
+
26
+ it("should handle nested resources with AWS Role", async () => {
27
+ class ServiceResources extends Resource(
28
+ "service-resources",
29
+ async (
30
+ ctx: Context<{ roleArn: string }>,
31
+ props: { roleName: string },
32
+ ) => {
33
+ const assumeRolePolicy: PolicyDocument = {
34
+ Version: "2012-10-17",
35
+ Statement: [
36
+ {
37
+ Effect: "Allow",
38
+ Principal: {
39
+ Service: "lambda.amazonaws.com",
40
+ },
41
+ Action: "sts:AssumeRole",
42
+ },
43
+ ],
44
+ };
45
+
46
+ const role = new Role(`role`, {
47
+ roleName: props.roleName,
48
+ assumeRolePolicy,
49
+ });
50
+
51
+ return {
52
+ roleArn: role.arn,
53
+ };
54
+ },
55
+ ) {}
56
+
57
+ const service = new ServiceResources("test-service", {
58
+ roleName: "alchemy-test-scope-role",
59
+ });
60
+
61
+ // Apply the parent resource which should create the role
62
+ const output = await apply(service);
63
+
64
+ // Clean up
65
+ await destroy(service);
66
+ });
67
+ });
@@ -1,45 +0,0 @@
1
- import type { ZodType } from "zod";
2
- import { type Context, type Provider, Resource } from "../../resource";
3
-
4
- export function Agent<Type extends string, TInput, TOutput>(
5
- type: Type,
6
- {
7
- description,
8
- input,
9
- output,
10
- }: {
11
- description: string;
12
- input: ZodType<TInput>;
13
- output: ZodType<TOutput>;
14
- },
15
- handler: (ctx: Context<TOutput>, props: TInput) => Promise<TOutput | void>,
16
- ): Provider<Type, [TInput], Awaited<TOutput>> & {
17
- description: string;
18
- input: ZodType<TInput>;
19
- output: ZodType<TOutput>;
20
- } {
21
- // @ts-expect-error - we don't care that the props are not statically known, they will be visible outside this scope
22
- return class Agent extends Resource(
23
- type,
24
- async (ctx: Context<TOutput>, props: TInput): Promise<TOutput | void> => {
25
- // Validate input
26
- const validatedInput = input.parse(props);
27
-
28
- // Call handler with validated input
29
- const result = await handler(ctx, validatedInput);
30
-
31
- if (ctx.event === "delete") {
32
- return;
33
- }
34
-
35
- // Validate output
36
- const validatedOutput = output.parse(result);
37
-
38
- return validatedOutput;
39
- },
40
- ) {
41
- static readonly description = description;
42
- static readonly input = input;
43
- static readonly output = output;
44
- };
45
- }
@@ -1,22 +0,0 @@
1
- import { type AnthropicModelId, isAnthropicModel } from "./anthropic";
2
- import { type OpenAIChatModelId, isOpenAIModel } from "./openai";
3
-
4
- export type ModelId = OpenAIChatModelId | AnthropicModelId;
5
-
6
- export async function resolveModel(modelId: ModelId) {
7
- if (isOpenAIModel(modelId)) {
8
- const { openai } = await import("@ai-sdk/openai");
9
- return {
10
- type: "openai" as const,
11
- model: openai(modelId as OpenAIChatModelId),
12
- };
13
- }
14
- if (isAnthropicModel(modelId)) {
15
- const { anthropic } = await import("@ai-sdk/anthropic");
16
- return {
17
- type: "anthropic" as const,
18
- model: anthropic(modelId as AnthropicModelId),
19
- };
20
- }
21
- throw new Error(`Unsupported model ID: ${modelId}`);
22
- }
@@ -1,177 +0,0 @@
1
- import type { CoreMessage } from "ai";
2
- import { generateText } from "ai";
3
- import { mkdir, unlink, writeFile } from "fs/promises";
4
- import { dirname } from "path";
5
- import { z } from "zod";
6
- import { Agent } from "./agent";
7
- import { resolveModel } from "./model";
8
-
9
- export type CodeProps = z.infer<typeof CodeProps>;
10
-
11
- export type File = z.infer<typeof File>;
12
- export const File = z.object({
13
- path: z.string(),
14
- content: z.string(),
15
- });
16
-
17
- const CodeProps = z.object({
18
- /**
19
- * The ID of the model to use for generating TypeScript code
20
- * @default "gpt-4o"
21
- */
22
- modelId: z.string().optional(),
23
-
24
- /**
25
- * The name of the file to write the code to
26
- */
27
- path: z.string(),
28
-
29
- /**
30
- * The requirements for the TypeScript file
31
- */
32
- requirements: z.string(),
33
-
34
- /**
35
- * List of other code files that it depends on
36
- */
37
- dependencies: z.array(File).optional(),
38
-
39
- /**
40
- * Temperature setting for model generation (higher = more creative, lower = more focused)
41
- * @default 0.7
42
- */
43
- temperature: z.number().optional(),
44
- });
45
-
46
- export type CodeOutput = z.infer<typeof CodeOutput>;
47
- export const CodeOutput = File;
48
-
49
- export class TypeScriptFile extends Agent(
50
- "code::TypeScriptFile",
51
- {
52
- description:
53
- "This Agent is responsible for generating TypeScript code based on requirements and context from other code files.",
54
- input: CodeProps,
55
- output: CodeOutput,
56
- },
57
- async (ctx, props) => {
58
- if (ctx.event === "delete") {
59
- await unlink(props.path);
60
- return;
61
- }
62
-
63
- // Get the appropriate model based on the ID
64
- const { model } = await resolveModel(props.modelId ?? "gpt-4o");
65
-
66
- // Generate the TypeScript code with retries
67
- const content = await generateCodeWithRetries(model, props);
68
-
69
- // Ensure the directory exists
70
- await mkdir(dirname(props.path), { recursive: true });
71
-
72
- // Write the TypeScript file
73
- await writeFile(props.path, content);
74
-
75
- return {
76
- path: props.path,
77
- content: content,
78
- };
79
- },
80
- ) {}
81
-
82
- /**
83
- * Attempts to generate valid TypeScript code with retries
84
- */
85
- async function generateCodeWithRetries(
86
- model: any,
87
- props: CodeProps,
88
- maxAttempts = 3,
89
- ): Promise<string> {
90
- const baseMessages: CoreMessage[] = [
91
- {
92
- role: "system",
93
- content:
94
- "You are an expert TypeScript developer that generates clean, well-documented code following best practices. " +
95
- "Your response must follow this exact format:\n" +
96
- "1. A single sentence describing what the code will do\n" +
97
- "2. A single TypeScript code block surrounded by ```ts and ``` tags\n" +
98
- "Do not include any other explanations or multiple code blocks.",
99
- },
100
- {
101
- role: "user",
102
- content: `Please generate TypeScript code based on the following specifications:
103
-
104
- Requirements:
105
- ${props.requirements}
106
-
107
- ${
108
- props.dependencies?.length
109
- ? `Context (other code files):
110
- ${props.dependencies.map((dep) => dep.content).join("\n\n")}`
111
- : ""
112
- }
113
-
114
- The code should:
115
- 1. Be well-documented with JSDoc comments
116
- 2. Follow TypeScript best practices
117
- 3. Include proper type definitions
118
- 4. Be ready to use with the provided context files
119
-
120
- Your response MUST follow this format:
121
- - One sentence summary of what the code will do
122
- - Single TypeScript code block surrounded by \`\`\`ts and \`\`\` tags
123
- - No other explanations or multiple code blocks`,
124
- },
125
- ];
126
-
127
- const messages = [...baseMessages];
128
-
129
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
130
- const { text } = await generateText({
131
- model,
132
- temperature: Math.max(
133
- 0.1,
134
- (props.temperature ?? 0.7) * (1 - attempt * 0.2),
135
- ), // Reduce temperature with each attempt
136
- messages,
137
- });
138
-
139
- const code = extractTypeScriptCode(text);
140
- if (code) {
141
- return code;
142
- }
143
-
144
- if (attempt === maxAttempts) {
145
- throw new Error(
146
- "Failed to generate valid TypeScript code after maximum attempts",
147
- );
148
- }
149
-
150
- // Add the failed attempt and correction request to the message chain
151
- messages.push(
152
- {
153
- role: "assistant",
154
- content: text,
155
- },
156
- {
157
- role: "user",
158
- content:
159
- "Your response did not match the required format. Please provide your response with exactly one sentence summary" +
160
- " followed by a single TypeScript code block surrounded by ```ts and ``` tags. No other text or explanations.",
161
- },
162
- );
163
- }
164
-
165
- // This should never be reached due to the throw above
166
- throw new Error("Unexpected code generation failure");
167
- }
168
-
169
- /**
170
- * Extracts TypeScript code from a markdown code block
171
- * @param text Response text that may contain markdown
172
- * @returns The TypeScript code if found, undefined otherwise
173
- */
174
- function extractTypeScriptCode(text: string): string | undefined {
175
- const matches = text.match(/```(?:ts|typescript)\n([\s\S]*?)```/);
176
- return matches?.[1]?.trim();
177
- }
File without changes
File without changes