Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/__tests__/ConfigService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ describe("ConfigService", () => {
],
},
referenceExamples: {}, // Added referenceExamples
timeoutSeconds: 0,
};

(fs.existsSync as jest.Mock).mockReturnValue(true);
Expand Down
55 changes: 41 additions & 14 deletions src/commands/run.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Args, Command, Flags } from "@oclif/core";
import { CrackedAgent, CrackedAgentOptions } from "@services/CrackedAgent";
import {
CrackedAgent,
CrackedAgentOptions,
ExecutionResult,
} from "@services/CrackedAgent";
import { LLMProviderType } from "@services/LLM/LLMProvider";
import { ModelManager } from "@services/LLM/ModelManager";
import { OpenRouterAPI } from "@services/LLMProviders/OpenRouter/OpenRouterAPI";
Expand All @@ -16,15 +20,19 @@ export class Run extends Command {
"$ run 'Add error handling'",
"$ run --interactive # Start interactive mode",
"$ run --init # Initialize configuration",
"$ run 'Add tests' --timeout 300 # Set timeout to 5 minutes",
];

static flags = {
init: Flags.boolean({
description: "Initialize a default crkdrc.json configuration file",
exclusive: ["interactive"],
}),
timeout: Flags.integer({
description: "Set timeout for the operation in seconds",
exclusive: ["init"],
}),
};

static args = {
message: Args.string({
description: "Message describing the operation to perform",
Expand Down Expand Up @@ -110,6 +118,7 @@ export class Run extends Command {
...config,
options: this.parseOptions(config.options || ""),
provider: config.provider as LLMProviderType,
timeout: (flags.timeout ?? config.timeoutSeconds ?? 0) * 1000, // Convert seconds to milliseconds
};

// Validate provider
Expand All @@ -132,22 +141,40 @@ export class Run extends Command {
console.log("Press Enter to start the stream...");
this.rl.once("line", async () => {
try {
const result = await agent.execute(args.message!, options);
if (!options.stream && result) {
this.log(result.response);
if (result.actions?.length) {
this.log("\nExecuted Actions:");
result.actions.forEach(({ action, result }) => {
this.log(`\nAction: ${action}`);
this.log(`Result: ${JSON.stringify(result, null, 2)}`);
});
const executePromise = agent.execute(
args.message!,
options,
) as Promise<ExecutionResult>;

if (options.timeout > 0) {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(
new Error(
`Operation timed out after ${options.timeout / 1000} seconds`,
),
);
}, options.timeout);
});

try {
await Promise.race([executePromise, timeoutPromise]);
} catch (error) {
this.sessionManager.cleanup();
if (error.message.includes("timed out")) {
console.error("\n" + error.message);
process.exit(1);
}
throw error;
}
} else {
await executePromise;
}
this.sessionManager.cleanup();

process.exit(0);
} catch (error) {
this.sessionManager.cleanup();
this.error((error as Error).message);
console.error("Error:", error);
process.exit(1);
}
});
}
Expand Down
2 changes: 2 additions & 0 deletions src/services/ConfigService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ const configSchema = z.object({
referenceExamples: z.record(z.string(), z.string()).optional().default({}),
projectLanguage: z.string().default("typescript"),
packageManager: z.string().default("yarn"),
timeoutSeconds: z.number().optional().default(0), // Add timeout property
});

export type Config = z.infer<typeof configSchema>;
Expand Down Expand Up @@ -209,6 +210,7 @@ export class ConfigService {
myService: "src/services/MyService.ts",
anotherKey: "path/to/some/other/example.ts",
},
timeoutSeconds: 0, // Add default timeout
};
fs.writeFileSync(
this.CONFIG_PATH,
Expand Down
2 changes: 2 additions & 0 deletions src/services/CrackedAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { PhaseManager } from "./LLM/PhaseManager";

export interface CrackedAgentOptions {
root?: string;
timeout: number;
instructionsPath?: string;
instructions?: string;
provider?: LLMProviderType;
Expand Down Expand Up @@ -98,6 +99,7 @@ export class CrackedAgent {
clearContext: false,
autoScaler: false,
...options,
timeout: 0,
};

this.debugLogger.setDebug(finalOptions.debug);
Expand Down
3 changes: 3 additions & 0 deletions src/services/LLM/__tests__/PhaseManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ describe("PhaseManager", () => {
lockFiles: ["package-lock.json"],
},
referenceExamples: {},
timeoutSeconds: 0,
};

beforeAll(() => {
Expand Down Expand Up @@ -143,6 +144,7 @@ describe("PhaseManager", () => {
lockFiles: ["package-lock.json"],
},
referenceExamples: {},
timeoutSeconds: 0,
};

jest.spyOn(configService, "getConfig").mockReturnValue(customConfig);
Expand Down Expand Up @@ -200,6 +202,7 @@ describe("PhaseManager", () => {
lockFiles: ["package-lock.json"],
},
referenceExamples: {},
timeoutSeconds: 0,
};

jest.spyOn(configService, "getConfig").mockReturnValue(emptyConfig);
Expand Down
18 changes: 18 additions & 0 deletions src/services/LLMProviders/OpenRouter/OpenRouterAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class OpenRouterAPI implements ILLMProvider {
private retryDelay: number = 1000;
private stream: any;
private aborted: boolean = false;
private timeout: number = 0;

constructor(
private htmlEntityDecoder: HtmlEntityDecoder,
Expand Down Expand Up @@ -389,6 +390,19 @@ export class OpenRouterAPI implements ILLMProvider {
currentModel,
);

this.timeout > 0
? new Promise<never>((_, reject) => {
setTimeout(() => {
console.error(
"\nOperation timed out in",
this.timeout / 1000,
"seconds",
);
process.exit(0);
}, this.timeout);
})
: null;

const streamOperation = async () => {
const response = await this.makeRequest(
"/chat/completions",
Expand Down Expand Up @@ -522,4 +536,8 @@ export class OpenRouterAPI implements ILLMProvider {
this.stream = null;
}
}

updateTimeout(timeout: number) {
this.timeout = timeout;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,13 @@ describe("OpenRouterAPI", () => {
});
});
describe("Streaming", () => {
// beforeEach(() => {
// jest.useFakeTimers();
// });

// afterEach(() => {
// jest.useRealTimers();
// });
it("should handle streaming messages correctly", async () => {
const mockStreamData = [
'data: {"choices": [{"delta": {"content": "Hel"}}]}\n',
Expand Down Expand Up @@ -316,6 +323,35 @@ describe("OpenRouterAPI", () => {
expect(callback).not.toHaveBeenCalled();
});

it("should not timeout when timeout is 0", async () => {
const mockStreamData = [
'data: {"choices": [{"delta": {"content": "Hel"}}]}\n',
'data: {"choices": [{"delta": {"content": "lo"}}]}\n',
'data: {"choices": [{"delta": {"content": "!"}}]}\n',
"data: [DONE]\n",
];

const mockStream = new Readable({
read() {
mockStreamData.forEach((chunk) => {
this.push(Buffer.from(chunk));
});
this.push(null);
},
});

postSpy.mockResolvedValue({ data: mockStream });
openRouterAPI.updateTimeout(0); // No timeout

const callback = jest.fn();
await openRouterAPI.streamMessage("gpt-4", "Hi", callback);

expect(callback).toHaveBeenCalledTimes(3);
expect(callback).toHaveBeenNthCalledWith(1, "Hel");
expect(callback).toHaveBeenNthCalledWith(2, "lo");
expect(callback).toHaveBeenNthCalledWith(3, "!");
});

it("should handle aborted stream", async () => {
const mockStreamData = [
'data: {"choices": [{"delta": {"content": "Hel"}}]}\n',
Expand Down
18 changes: 18 additions & 0 deletions src/services/__tests__/CrackedAgent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ describe("CrackedAgent", () => {
clearContext: false,
autoScaler: false,
instructionsPath: "path/to/instructions",
timeout: 0,
};

await crackedAgent.execute("Mock message", options);
Expand All @@ -113,6 +114,7 @@ describe("CrackedAgent", () => {
clearContext: false,
autoScaler: false,
instructions: "Custom instructions",
timeout: 0,
};

await crackedAgent.execute("Mock message", options);
Expand All @@ -130,6 +132,7 @@ describe("CrackedAgent", () => {
debug: false,
clearContext: false,
autoScaler: false,
timeout: 0,
};

await crackedAgent.execute("Mock message", options);
Expand All @@ -146,6 +149,7 @@ describe("CrackedAgent", () => {
debug: false,
clearContext: true,
autoScaler: false,
timeout: 0,
};

await crackedAgent.execute("Mock message", options);
Expand All @@ -160,6 +164,7 @@ describe("CrackedAgent", () => {
debug: false,
clearContext: false,
autoScaler: false,
timeout: 0,
};

await crackedAgent.execute("Mock message", options);
Expand All @@ -174,6 +179,7 @@ describe("CrackedAgent", () => {
debug: false,
clearContext: false,
autoScaler: false,
timeout: 0,
};

await crackedAgent.execute("Mock message", options);
Expand All @@ -189,6 +195,7 @@ describe("CrackedAgent", () => {
debug: true,
clearContext: false,
autoScaler: false,
timeout: 0,
};

await crackedAgent.execute("Mock message", options);
Expand All @@ -203,6 +210,7 @@ describe("CrackedAgent", () => {
debug: false,
clearContext: false,
autoScaler: false,
timeout: 0,
};

await crackedAgent.execute("Mock message", options);
Expand All @@ -217,6 +225,7 @@ describe("CrackedAgent", () => {
debug: false,
clearContext: false,
autoScaler: false,
timeout: 0,
};

await crackedAgent.execute("Mock message", options);
Expand All @@ -234,6 +243,7 @@ describe("CrackedAgent", () => {
clearContext: false,
autoScaler: false,
root: "/custom/root",
timeout: 0,
};

await crackedAgent.execute("Mock message", options);
Expand All @@ -253,6 +263,7 @@ describe("CrackedAgent", () => {
clearContext: false,
autoScaler: false,
options: { key: "value" },
timeout: 0,
};

await crackedAgent.execute("Mock message", options);
Expand All @@ -271,6 +282,7 @@ describe("CrackedAgent", () => {
debug: false,
clearContext: false,
autoScaler: false,
timeout: 0,
};

await crackedAgent.execute("", options);
Expand All @@ -289,6 +301,7 @@ describe("CrackedAgent", () => {
debug: false,
clearContext: false,
autoScaler: false,
timeout: 0,
};

await crackedAgent.execute(undefined as unknown as string, options);
Expand All @@ -308,6 +321,7 @@ describe("CrackedAgent", () => {
clearContext: false,
autoScaler: false,
instructionsPath: "",
timeout: 0,
};

await crackedAgent.execute("Mock message", options);
Expand All @@ -326,6 +340,7 @@ describe("CrackedAgent", () => {
clearContext: false,
autoScaler: false,
instructionsPath: undefined as unknown as string,
timeout: 0,
};

await crackedAgent.execute("Mock message", options);
Expand All @@ -344,6 +359,7 @@ describe("CrackedAgent", () => {
clearContext: false,
autoScaler: false,
instructions: "",
timeout: 0,
};

await crackedAgent.execute("Mock message", options);
Expand All @@ -362,6 +378,7 @@ describe("CrackedAgent", () => {
clearContext: false,
autoScaler: false,
instructions: undefined as unknown as string,
timeout: 0,
};

await crackedAgent.execute("Mock message", options);
Expand All @@ -380,6 +397,7 @@ describe("CrackedAgent", () => {
clearContext: false,
autoScaler: false,
options: {},
timeout: 0,
};

await crackedAgent.execute("Mock message", options);
Expand Down
Loading
Loading