Skip to content

Commit 2dcaa1c

Browse files
feat: CodeCortex AI — autonomous terminal coding agent with pip package
1 parent df9df2a commit 2dcaa1c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+5899
-37
lines changed

.ai-agent/config.toml

Lines changed: 0 additions & 5 deletions
This file was deleted.

.codecortex-ai/config.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
hooks_enabled = false
2+
3+
[model]
4+
name = "openrouter/hunter-alpha"
5+
temperature = 0

.gitignore

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ desktop.ini
3030
# Sessions & Logs
3131
hook.log
3232
*.log
33+
sessions/
3334

34-
# Git
35-
.git/
35+
# CodeCortex AI Pip build artifacts
36+
CodeCortex AI Pip/dist/
37+
CodeCortex AI Pip/build/
38+
CodeCortex AI Pip/*.egg-info/
39+
CodeCortex AI Pip/codecortex/*.egg-info/
40+
41+
# User config
42+
.codecortex/

CodeCortex AI Pip/README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# CodeCortex AI
2+
3+
**Autonomous coding agent for the terminal** — powered by LLMs.
4+
5+
## Installation
6+
7+
```bash
8+
pip install codecortex-ai
9+
```
10+
11+
## Quick Start
12+
13+
```bash
14+
codecortex
15+
```
16+
17+
On first run, you'll be prompted for:
18+
- **API Key** (required) — your OpenAI, OpenRouter, or any compatible provider key
19+
- **Base URL** (optional) — leave blank for OpenAI, or enter a custom endpoint
20+
21+
Configuration is saved to `~/.codecortex/config.json`.
22+
23+
## Usage
24+
25+
```bash
26+
# Interactive mode
27+
codecortex
28+
29+
# Single prompt
30+
codecortex "refactor this function to use async/await"
31+
32+
# Specify working directory
33+
codecortex --cwd ./my-project
34+
35+
# Reset saved config
36+
codecortex --reset
37+
```
38+
39+
## Supported Providers
40+
41+
Any OpenAI-compatible API works:
42+
- **OpenAI** — leave Base URL empty (default)
43+
- **OpenRouter**`https://openrouter.ai/api/v1`
44+
- **Anthropic via proxy** — any compatible endpoint
45+
- **Local models** (Ollama, LM Studio) — `http://localhost:11434/v1`
46+
47+
## Commands
48+
49+
Inside the interactive session:
50+
51+
| Command | Description |
52+
|-------------|--------------------------------|
53+
| `/help` | Show help |
54+
| `/config` | Show current configuration |
55+
| `/model` | View or change model |
56+
| `/approval` | View or change approval policy |
57+
| `/tools` | List available tools |
58+
| `/mcp` | List MCP servers |
59+
| `/stats` | Show session statistics |
60+
| `/save` | Save current session |
61+
| `/sessions` | List saved sessions |
62+
| `/resume` | Resume a saved session |
63+
| `/clear` | Clear conversation |
64+
| `/exit` | Quit |
65+
66+
## License
67+
68+
MIT
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""CodeCortex AI — autonomous coding agent for the terminal."""
2+
3+
__version__ = "0.1.0"

CodeCortex AI Pip/codecortex/agent/__init__.py

Whitespace-only changes.
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
from __future__ import annotations
2+
from typing import AsyncGenerator, Awaitable, Callable
3+
from codecortex.agent.events import AgentEvent, AgentEventType
4+
from codecortex.agent.session import Session
5+
from codecortex.client.response import StreamEventType, TokenUsage, ToolCall, ToolResultMessage
6+
from codecortex.config.config import Config
7+
from codecortex.prompts.system import create_loop_breaker_prompt
8+
from codecortex.tools.base import ToolConfirmation
9+
10+
11+
class Agent:
12+
def __init__(
13+
self,
14+
config: Config,
15+
confirmation_callback: Callable[[ToolConfirmation], bool] | None = None,
16+
):
17+
self.config = config
18+
self.session: Session | None = Session(self.config)
19+
self.session.approval_manager.confirmation_callback = confirmation_callback
20+
21+
async def run(self, message: str):
22+
await self.session.hook_system.trigger_before_agent(message)
23+
yield AgentEvent.agent_start(message)
24+
self.session.context_manager.add_user_message(message)
25+
26+
final_response: str | None = None
27+
28+
async for event in self._agentic_loop():
29+
yield event
30+
31+
if event.type == AgentEventType.TEXT_COMPLETE:
32+
final_response = event.data.get("content")
33+
34+
await self.session.hook_system.trigger_after_agent(message, final_response)
35+
yield AgentEvent.agent_end(final_response)
36+
37+
async def _agentic_loop(self) -> AsyncGenerator[AgentEvent, None]:
38+
max_turns = self.config.max_turns
39+
40+
for turn_num in range(max_turns):
41+
self.session.increment_turn()
42+
response_text = ""
43+
44+
# check for context overflow
45+
if self.session.context_manager.needs_compression():
46+
summary, usage = await self.session.chat_compactor.compress(
47+
self.session.context_manager
48+
)
49+
50+
if summary:
51+
self.session.context_manager.replace_with_summary(summary)
52+
self.session.context_manager.set_latest_usage(usage)
53+
self.session.context_manager.add_usage(usage)
54+
55+
tool_schemas = self.session.tool_registry.get_schemas()
56+
57+
tool_calls: list[ToolCall] = []
58+
usage: TokenUsage | None = None
59+
60+
async for event in self.session.client.chat_completion(
61+
self.session.context_manager.get_messages(),
62+
tools=tool_schemas if tool_schemas else None,
63+
):
64+
if event.type == StreamEventType.TEXT_DELTA:
65+
if event.text_delta:
66+
content = event.text_delta.content
67+
response_text += content
68+
yield AgentEvent.text_delta(content)
69+
elif event.type == StreamEventType.TOOL_CALL_COMPLETE:
70+
if event.tool_call:
71+
tool_calls.append(event.tool_call)
72+
elif event.type == StreamEventType.ERROR:
73+
yield AgentEvent.agent_error(
74+
event.error or "Unknown error occurred.",
75+
)
76+
elif event.type == StreamEventType.MESSAGE_COMPLETE:
77+
usage = event.usage
78+
79+
self.session.context_manager.add_assistant_message(
80+
response_text or None,
81+
(
82+
[
83+
{
84+
"id": tc.call_id,
85+
"type": "function",
86+
"function": {
87+
"name": tc.name,
88+
"arguments": str(tc.arguments),
89+
},
90+
}
91+
for tc in tool_calls
92+
]
93+
if tool_calls
94+
else None
95+
),
96+
)
97+
if response_text:
98+
yield AgentEvent.text_complete(response_text)
99+
self.session.loop_detector.record_action(
100+
"response",
101+
text=response_text,
102+
)
103+
104+
if not tool_calls:
105+
if usage:
106+
self.session.context_manager.set_latest_usage(usage)
107+
self.session.context_manager.add_usage(usage)
108+
109+
self.session.context_manager.prune_tool_outputs()
110+
return
111+
112+
tool_call_results: list[ToolResultMessage] = []
113+
114+
for tool_call in tool_calls:
115+
yield AgentEvent.tool_call_start(
116+
tool_call.call_id,
117+
tool_call.name,
118+
tool_call.arguments,
119+
)
120+
121+
self.session.loop_detector.record_action(
122+
"tool_call",
123+
tool_name=tool_call.name,
124+
args=tool_call.arguments,
125+
)
126+
127+
result = await self.session.tool_registry.invoke(
128+
tool_call.name,
129+
tool_call.arguments,
130+
self.config.cwd,
131+
self.session.hook_system,
132+
self.session.approval_manager,
133+
)
134+
135+
yield AgentEvent.tool_call_complete(
136+
tool_call.call_id,
137+
tool_call.name,
138+
result,
139+
)
140+
141+
tool_call_results.append(
142+
ToolResultMessage(
143+
tool_call_id=tool_call.call_id,
144+
content=result.to_model_output(),
145+
is_error=not result.success,
146+
)
147+
)
148+
149+
for tool_result in tool_call_results:
150+
self.session.context_manager.add_tool_result(
151+
tool_result.tool_call_id,
152+
tool_result.content,
153+
)
154+
155+
loop_detection_error = self.session.loop_detector.check_for_loop()
156+
if loop_detection_error:
157+
loop_prompt = create_loop_breaker_prompt(loop_detection_error)
158+
self.session.context_manager.add_user_message(loop_prompt)
159+
160+
if usage:
161+
self.session.context_manager.set_latest_usage(usage)
162+
self.session.context_manager.add_usage(usage)
163+
164+
self.session.context_manager.prune_tool_outputs()
165+
yield AgentEvent.agent_error(f"Maximum turns ({max_turns}) reached")
166+
167+
async def __aenter__(self) -> Agent:
168+
await self.session.initialize()
169+
return self
170+
171+
async def __aexit__(
172+
self,
173+
exc_type,
174+
exc_val,
175+
exc_tb,
176+
) -> None:
177+
if self.session and self.session.client and self.session.mcp_manager:
178+
await self.session.client.close()
179+
await self.session.mcp_manager.shutdown()
180+
self.session = None
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
from __future__ import annotations
2+
from enum import Enum
3+
from dataclasses import dataclass, field
4+
from typing import Any
5+
6+
from codecortex.client.response import TokenUsage
7+
from codecortex.tools.base import ToolResult
8+
9+
10+
class AgentEventType(str, Enum):
11+
# Agent lifecycle
12+
AGENT_START = "agent_start"
13+
AGENT_END = "agent_end"
14+
AGENT_ERROR = "agent_error"
15+
16+
# Tool calls
17+
TOOL_CALL_START = "tool_call_start"
18+
TOOL_CALL_COMPLETE = "tool_call_complete"
19+
20+
# Text streaming
21+
TEXT_DELTA = "text_delta"
22+
TEXT_COMPLETE = "text_complete"
23+
24+
25+
@dataclass
26+
class AgentEvent:
27+
type: AgentEventType
28+
data: dict[str, Any] = field(default_factory=dict)
29+
30+
@classmethod
31+
def agent_start(cls, message: str) -> AgentEvent:
32+
return cls(
33+
type=AgentEventType.AGENT_START,
34+
data={"message": message},
35+
)
36+
37+
@classmethod
38+
def agent_end(
39+
cls,
40+
response: str | None = None,
41+
usage: TokenUsage | None = None,
42+
) -> AgentEvent:
43+
return cls(
44+
type=AgentEventType.AGENT_END,
45+
data={
46+
"response": response,
47+
"usage": usage.__dict__ if usage else None,
48+
},
49+
)
50+
51+
@classmethod
52+
def agent_error(
53+
cls,
54+
error: str,
55+
details: dict[str, Any] | None = None,
56+
) -> AgentEvent:
57+
return cls(
58+
type=AgentEventType.AGENT_ERROR,
59+
data={"error": error, "details": details or {}},
60+
)
61+
62+
@classmethod
63+
def text_delta(cls, content: str) -> AgentEvent:
64+
return cls(
65+
type=AgentEventType.TEXT_DELTA,
66+
data={"content": content},
67+
)
68+
69+
@classmethod
70+
def text_complete(cls, content: str) -> AgentEvent:
71+
return cls(
72+
type=AgentEventType.TEXT_COMPLETE,
73+
data={"content": content},
74+
)
75+
76+
@classmethod
77+
def tool_call_start(cls, call_id: str, name: str, arguments: dict[str, Any]):
78+
return cls(
79+
type=AgentEventType.TOOL_CALL_START,
80+
data={
81+
"call_id": call_id,
82+
"name": name,
83+
"arguments": arguments,
84+
},
85+
)
86+
87+
@classmethod
88+
def tool_call_complete(
89+
cls,
90+
call_id: str,
91+
name: str,
92+
result: ToolResult,
93+
):
94+
return cls(
95+
type=AgentEventType.TOOL_CALL_COMPLETE,
96+
data={
97+
"call_id": call_id,
98+
"name": name,
99+
"success": result.success,
100+
"output": result.output,
101+
"error": result.error,
102+
"metadata": result.metadata,
103+
"diff": result.diff.to_diff() if result.diff else None,
104+
"truncated": result.truncated,
105+
"exit_code": result.exit_code,
106+
},
107+
)

0 commit comments

Comments
 (0)