@agentics.org/sparc2
Version:
SPARC 2.0 - Autonomous Vector Coding Agent + MCP. SPARC 2.0, vectorized AI code analysis, is an intelligent coding agent framework built to automate and streamline software development. It combines secure execution environments, and version control into a
388 lines (335 loc) • 10.9 kB
text/typescript
/**
* Agent module for SPARC 2.0
* Provides the main agent functionality for analyzing and modifying code
*/
import OpenAI from "npm:openai";
import { logDebug, logError, logInfo } from "../logger.ts";
import { applyDiff, computeDiff } from "../diff/diffTracker.ts";
import {
createCheckpoint,
createCommit,
getCurrentBranch,
isRepoClean,
rollbackChanges,
} from "../git/gitIntegration.ts";
import { executeCode } from "../sandbox/codeInterpreter.ts";
import { indexDiffEntry } from "../vector/vectorStore.ts";
/**
* File to process interface
*/
export interface FileToProcess {
/** Path to the file */
path: string;
/** Original content of the file */
originalContent: string;
}
/**
* Agent options
*/
export interface AgentOptions {
/** Model to use for the agent */
model?: string;
/** Mode for the agent (automatic, semi, manual, custom) */
mode?: "automatic" | "semi" | "manual" | "custom" | "interactive";
/** Diff mode (file, function) */
diffMode?: "file" | "function";
/** Processing mode (sequential, parallel, concurrent, swarm) */
processing?: "sequential" | "parallel" | "concurrent" | "swarm";
/** Path to the agent configuration file */
configPath?: string;
}
/**
* Result of a modification operation
*/
export interface ModificationResult {
/** Path to the file */
path: string;
/** Original content of the file */
originalContent: string;
/** Modified content of the file */
modifiedContent: string;
/** Commit hash if changes were committed */
commitHash?: string;
}
/**
* SPARC 2.0 Agent class
* Provides methods for analyzing and modifying code
*/
export class SPARC2Agent {
private openai!: OpenAI; // Using definite assignment assertion
private model: string;
private mode: "automatic" | "semi" | "manual" | "custom" | "interactive";
private diffMode: "file" | "function";
private processing: "sequential" | "parallel" | "concurrent" | "swarm";
/**
* Create a new SPARC 2.0 Agent
* @param options Agent options
*/
constructor(options: AgentOptions = {}) {
this.model = options.model || "gpt-4o";
this.mode = options.mode || "automatic";
this.diffMode = options.diffMode || "file";
this.processing = options.processing || "sequential";
}
/**
* Initialize the agent
*/
async init(): Promise<void> {
// Initialize OpenAI client
const apiKey = Deno.env.get("OPENAI_API_KEY");
if (!apiKey) {
throw new Error("OPENAI_API_KEY environment variable not set");
}
this.openai = new OpenAI({
apiKey,
});
await logInfo("SPARC2 Agent initialized", {
model: this.model,
mode: this.mode,
diffMode: this.diffMode,
processing: this.processing,
});
}
/**
* Analyze and compute diff between two versions of code
* @param path Path to the file
* @param oldContent Previous version of code
* @param newContent New version of code
* @returns Diff text
*/
async analyzeAndDiff(path: string, oldContent: string, newContent: string): Promise<string> {
// Compute diff
const diff = computeDiff(oldContent, newContent, this.diffMode);
// If there are no changes, return empty string
if (diff.changedLines === 0) {
await logInfo(`No changes detected for ${path}`);
return "";
}
// Log diff
await logInfo(`Computed diff for ${path}`, {
changedLines: diff.changedLines,
diffMode: this.diffMode,
});
// Index diff for later search
await indexDiffEntry({
id: new Date().toISOString(),
file: path,
diff: diff.diffText,
metadata: {
changedLines: diff.changedLines,
diffMode: this.diffMode,
timestamp: new Date().toISOString(),
},
});
return diff.diffText;
}
/**
* Apply changes to a file and commit them
* @param path Path to the file
* @param message Commit message
* @returns Commit hash
*/
async applyChanges(path: string, message: string): Promise<string> {
// Get current branch
const branch = await getCurrentBranch();
// Create commit
const commitHash = await createCommit(branch, path, message);
await logInfo(`Applied changes to ${path}`, {
branch,
commitHash,
});
return commitHash;
}
/**
* Create a checkpoint
* @param name Name of the checkpoint
* @returns Checkpoint hash
*/
async createCheckpoint(name: string): Promise<string> {
const hash = await createCheckpoint(name);
await logInfo(`Created checkpoint: ${name}`, {
hash,
});
return hash;
}
/**
* Rollback changes
* @param target Target to rollback to (commit hash, branch, etc.)
* @param type Type of rollback (checkpoint, temporal)
*/
async rollback(target: string, type: "checkpoint" | "temporal"): Promise<void> {
await rollbackChanges(target, type);
await logInfo(`Rolled back to ${type}: ${target}`);
}
/**
* Check if the repository is clean
* @returns True if the repository is clean
*/
async isRepoClean(): Promise<boolean> {
return isRepoClean();
}
/**
* Plan and execute a task
* @param task Task description
* @param files Files to process
* @returns Results of the modifications
*/
async planAndExecute(task: string, files: FileToProcess[]): Promise<ModificationResult[]> {
// Check if repo is clean
const clean = await this.isRepoClean();
if (!clean) {
await logInfo("Repository is not clean, creating checkpoint");
// Sanitize the task name for Git tag (max 50 chars, alphanumeric and hyphens only)
const sanitizedTask = task.replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase().substring(0, 50);
// Note: A timestamp will be automatically added by the createCheckpoint function to ensure uniqueness
await this.createCheckpoint(`pre-${sanitizedTask}`);
}
// Log task
await logInfo(`Planning and executing task: ${task}`, {
files: files.map((f) => f.path),
mode: this.mode,
diffMode: this.diffMode,
processing: this.processing,
});
// Create prompt
const prompt = this.createPrompt(task, files);
// Call OpenAI API
const completion = await this.openai.chat.completions.create({
model: this.model,
messages: [
{
role: "system",
content:
"You are a helpful AI assistant that analyzes code and suggests improvements. Provide your analysis and improvements in a structured format.",
},
{
role: "user",
content: prompt,
},
],
temperature: 0.2,
});
// Extract response
const response = completion.choices[0].message.content;
if (!response) {
throw new Error("No response from OpenAI API");
}
// Process response
const results = await this.processResponse(response, files);
// Log results
await logInfo(`Completed task: ${task}`, {
files: results.map((r) => r.path),
changedFiles: results.filter((r) => r.originalContent !== r.modifiedContent).length,
});
return results;
}
/**
* Create a prompt for the OpenAI API
* @param task Task description
* @param files Files to process
* @returns Prompt text
*/
private createPrompt(task: string, files: FileToProcess[]): string {
let prompt = `Task: ${task}\n\n`;
prompt += "Files to analyze and modify:\n\n";
for (const file of files) {
prompt += `File: ${file.path}\n\`\`\`\n${file.originalContent}\n\`\`\`\n\n`;
}
prompt +=
"Please analyze the code and suggest improvements. For each file you want to modify, provide the full updated code in the following format:\n\n";
prompt += "Analysis:\n[Your analysis here]\n\n";
prompt += "Plan:\n[Your plan here]\n\n";
prompt += "File: [file path]\n```[language]\n[full updated code]\n```\n";
return prompt;
}
/**
* Process the response from the OpenAI API
* @param response Response text
* @param files Files that were processed
* @returns Results of the modifications
*/
private async processResponse(
response: string,
files: FileToProcess[],
): Promise<ModificationResult[]> {
const results: ModificationResult[] = [];
// Create a map of file paths to their original content
const fileMap = new Map<string, string>();
for (const file of files) {
fileMap.set(file.path, file.originalContent);
}
// Extract code blocks from the response
const codeBlockRegex = /File: ([^\n]+)\n```(?:[^\n]+)?\n([\s\S]*?)\n```/g;
let match;
while ((match = codeBlockRegex.exec(response)) !== null) {
const [, path, code] = match;
const trimmedPath = path.trim();
// Check if the file exists in the input files
if (!fileMap.has(trimmedPath)) {
await logWarn(`File not found in input: ${trimmedPath}`);
continue;
}
const originalContent = fileMap.get(trimmedPath)!;
const modifiedContent = code.trim();
// Compute diff
const diff = await this.analyzeAndDiff(trimmedPath, originalContent, modifiedContent);
// If there are changes, write the file and commit
if (diff) {
// Write the file
await Deno.writeTextFile(trimmedPath, modifiedContent);
// Commit the changes
const commitHash = await this.applyChanges(
trimmedPath,
`SPARC2: Updated ${trimmedPath}`,
);
// Add to results
results.push({
path: trimmedPath,
originalContent,
modifiedContent,
commitHash,
});
} else {
// No changes, just add to results
results.push({
path: trimmedPath,
originalContent,
modifiedContent: originalContent,
});
}
}
return results;
}
/**
* Execute code in a sandbox
* @param code Code to execute
* @param options Execution options
* @returns Execution result
*/
async executeCode(
code: string,
options?: "python" | "javascript" | "typescript" | {
language?: "python" | "javascript" | "typescript";
stream?: boolean;
},
) {
let language: "python" | "javascript" | "typescript" = "javascript";
let stream = true;
if (typeof options === "string") {
language = options;
} else if (options && typeof options === "object") {
language = options.language || language;
stream = options.stream !== undefined ? options.stream : stream;
}
await logInfo(`Executing code`, { language, codeLength: code.length });
return executeCode(code, { language, stream });
}
}
/**
* Log a warning message
* @param message Message to log
* @param metadata Additional metadata
*/
async function logWarn(message: string, metadata: Record<string, unknown> = {}) {
await logDebug(message, metadata);
}