nexcoder 0.1.3__tar.gz → 0.1.5__tar.gz
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.
- {nexcoder-0.1.3 → nexcoder-0.1.5}/PKG-INFO +1 -1
- {nexcoder-0.1.3 → nexcoder-0.1.5}/pyproject.toml +1 -1
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/__init__.py +3 -1
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/agent.py +2 -3
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/api_client.py +42 -11
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/cli.py +1 -1
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/config.py +1 -1
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/planner.py +17 -3
- {nexcoder-0.1.3 → nexcoder-0.1.5}/.github/workflows/ci.yml +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/.github/workflows/publish.yml +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/.gitignore +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/CHANGELOG.md +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/LICENSE +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/README.md +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/context.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/exceptions.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/indexer/__init__.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/indexer/index.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/indexer/parser.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/indexer/scanner.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/memory/__init__.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/memory/decisions.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/memory/errors.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/memory/project.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/py.typed +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/reviewer.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/safety.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/test_runner.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/tools/__init__.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/tools/file_ops.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/tools/git_ops.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/tools/search.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/src/nex/tools/shell.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/tests/conftest.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/tests/test_agent.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/tests/test_chat.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/tests/test_cli.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/tests/test_context.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/tests/test_errors.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/tests/test_indexer.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/tests/test_memory.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/tests/test_rate_limiter.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/tests/test_safety.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/tests/test_test_runner.py +0 -0
- {nexcoder-0.1.3 → nexcoder-0.1.5}/tests/test_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nexcoder
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: The coding agent that remembers — AI coding assistant with persistent memory and error learning.
|
|
5
5
|
Project-URL: Homepage, https://github.com/nex-ai/nex-ai
|
|
6
6
|
Project-URL: Repository, https://github.com/nex-ai/nex-ai
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nexcoder"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.5"
|
|
8
8
|
description = "The coding agent that remembers — AI coding assistant with persistent memory and error learning."
|
|
9
9
|
readme = {file = "README.md", content-type = "text/markdown"}
|
|
10
10
|
license = "MIT"
|
|
@@ -438,9 +438,10 @@ class Agent:
|
|
|
438
438
|
haiku_model = self._nex_config.haiku_model
|
|
439
439
|
|
|
440
440
|
planner = Planner(self._client, haiku_model=haiku_model)
|
|
441
|
+
rate_limiter = RateLimiter(tokens_per_minute=token_rate_limit)
|
|
441
442
|
|
|
442
443
|
console.print("\n[bold]Decomposing task into subtasks...[/bold]")
|
|
443
|
-
subtasks = await planner.plan(self._config.task, project_memory)
|
|
444
|
+
subtasks = await planner.plan(self._config.task, project_memory, rate_limiter)
|
|
444
445
|
|
|
445
446
|
console.print(f"\n[bold]Running task:[/bold] {self._config.task}")
|
|
446
447
|
rate_info = f"Rate limit: {token_rate_limit} tokens/min | " if token_rate_limit else ""
|
|
@@ -449,8 +450,6 @@ class Agent:
|
|
|
449
450
|
f"{rate_info}"
|
|
450
451
|
f"Max iterations: {self._config.max_iterations}[/dim]\n"
|
|
451
452
|
)
|
|
452
|
-
|
|
453
|
-
rate_limiter = RateLimiter(tokens_per_minute=token_rate_limit)
|
|
454
453
|
budget = 20_000
|
|
455
454
|
if self._nex_config:
|
|
456
455
|
budget = self._nex_config.subtask_token_budget
|
|
@@ -122,6 +122,30 @@ class RateLimiter:
|
|
|
122
122
|
return
|
|
123
123
|
|
|
124
124
|
|
|
125
|
+
def _extract_retry_after(exc: Exception, default: float = 60.0) -> float:
|
|
126
|
+
"""Extract retry-after seconds from an Anthropic API error.
|
|
127
|
+
|
|
128
|
+
Inspects the exception's response headers for a ``retry-after`` value.
|
|
129
|
+
Falls back to *default* if the header is missing or unparseable.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
exc: The exception raised by the Anthropic SDK.
|
|
133
|
+
default: Fallback wait time in seconds.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Number of seconds to wait before retrying.
|
|
137
|
+
"""
|
|
138
|
+
resp = getattr(exc, "response", None)
|
|
139
|
+
if resp is not None:
|
|
140
|
+
header = getattr(resp, "headers", {}).get("retry-after")
|
|
141
|
+
if header:
|
|
142
|
+
try:
|
|
143
|
+
return max(float(header), 1.0)
|
|
144
|
+
except (ValueError, TypeError):
|
|
145
|
+
pass
|
|
146
|
+
return default
|
|
147
|
+
|
|
148
|
+
|
|
125
149
|
class AnthropicClient:
|
|
126
150
|
"""Async wrapper around the Anthropic API.
|
|
127
151
|
|
|
@@ -140,7 +164,7 @@ class AnthropicClient:
|
|
|
140
164
|
self,
|
|
141
165
|
api_key: str,
|
|
142
166
|
default_model: str = "claude-sonnet-4-20250514",
|
|
143
|
-
max_retries: int =
|
|
167
|
+
max_retries: int = 5,
|
|
144
168
|
) -> None:
|
|
145
169
|
"""Initialize the Anthropic client.
|
|
146
170
|
|
|
@@ -196,7 +220,9 @@ class AnthropicClient:
|
|
|
196
220
|
kwargs["tools"] = tools
|
|
197
221
|
|
|
198
222
|
last_error: Exception | None = None
|
|
199
|
-
|
|
223
|
+
# 429 rate limits need more retries with longer waits than server errors
|
|
224
|
+
max_attempts = self._max_retries + 1
|
|
225
|
+
for attempt in range(max_attempts):
|
|
200
226
|
try:
|
|
201
227
|
response = await self._client.messages.create(**kwargs)
|
|
202
228
|
|
|
@@ -230,16 +256,21 @@ class AnthropicClient:
|
|
|
230
256
|
except Exception as exc:
|
|
231
257
|
last_error = exc
|
|
232
258
|
status_code = getattr(exc, "status_code", None)
|
|
259
|
+
is_rate_limit = status_code == 429
|
|
260
|
+
is_retryable = status_code in (500, 502, 503, 529)
|
|
261
|
+
|
|
262
|
+
if (is_rate_limit or is_retryable) and attempt < max_attempts - 1:
|
|
263
|
+
if is_rate_limit:
|
|
264
|
+
# Rate limits: extract retry-after from response headers,
|
|
265
|
+
# or default to 60s (the full rate-limit window).
|
|
266
|
+
wait = _extract_retry_after(exc, default=60.0)
|
|
267
|
+
else:
|
|
268
|
+
# Server errors: short exponential backoff
|
|
269
|
+
wait = float(2**attempt)
|
|
233
270
|
|
|
234
|
-
# Retry on rate limit or server errors
|
|
235
|
-
if status_code in (429, 500, 502, 503, 529) and attempt < self._max_retries:
|
|
236
|
-
wait = 2**attempt
|
|
237
|
-
retry_after = getattr(exc, "retry_after", None)
|
|
238
|
-
if retry_after:
|
|
239
|
-
wait = max(wait, float(retry_after))
|
|
240
271
|
console.print(
|
|
241
|
-
f"[yellow]API error {status_code}, retrying in {wait}s "
|
|
242
|
-
f"(attempt {attempt + 1}/{
|
|
272
|
+
f"[yellow]API error {status_code}, retrying in {wait:.0f}s "
|
|
273
|
+
f"(attempt {attempt + 1}/{max_attempts - 1})...[/yellow]"
|
|
243
274
|
)
|
|
244
275
|
await asyncio.sleep(wait)
|
|
245
276
|
continue
|
|
@@ -250,7 +281,7 @@ class AnthropicClient:
|
|
|
250
281
|
) from exc
|
|
251
282
|
|
|
252
283
|
raise APIError(
|
|
253
|
-
f"Failed after {
|
|
284
|
+
f"Failed after {max_attempts - 1} retries: {last_error}",
|
|
254
285
|
)
|
|
255
286
|
|
|
256
287
|
async def close(self) -> None:
|
|
@@ -8,7 +8,7 @@ from typing import Any
|
|
|
8
8
|
|
|
9
9
|
from rich.console import Console
|
|
10
10
|
|
|
11
|
-
from nex.api_client import AnthropicClient
|
|
11
|
+
from nex.api_client import AnthropicClient, RateLimiter
|
|
12
12
|
|
|
13
13
|
console = Console(stderr=True)
|
|
14
14
|
|
|
@@ -60,25 +60,36 @@ class Planner:
|
|
|
60
60
|
self._client = api_client
|
|
61
61
|
self._model = haiku_model
|
|
62
62
|
|
|
63
|
-
async def plan(
|
|
63
|
+
async def plan(
|
|
64
|
+
self,
|
|
65
|
+
task: str,
|
|
66
|
+
project_context: str,
|
|
67
|
+
rate_limiter: RateLimiter | None = None,
|
|
68
|
+
) -> list[Subtask]:
|
|
64
69
|
"""Decompose a task into subtasks.
|
|
65
70
|
|
|
66
71
|
Args:
|
|
67
72
|
task: The user's task description.
|
|
68
73
|
project_context: Project memory and relevant context.
|
|
74
|
+
rate_limiter: Optional rate limiter to pace the API call.
|
|
69
75
|
|
|
70
76
|
Returns:
|
|
71
77
|
List of Subtask instances sorted by priority.
|
|
72
78
|
"""
|
|
79
|
+
user_content = f"Project context:\n{project_context}\n\nTask: {task}"
|
|
73
80
|
messages: list[dict[str, Any]] = [
|
|
74
81
|
{
|
|
75
82
|
"role": "user",
|
|
76
|
-
"content":
|
|
83
|
+
"content": user_content,
|
|
77
84
|
}
|
|
78
85
|
]
|
|
79
86
|
|
|
80
87
|
console.print("[dim]Planning task decomposition...[/dim]")
|
|
81
88
|
|
|
89
|
+
if rate_limiter is not None:
|
|
90
|
+
estimated = len(user_content) // 4
|
|
91
|
+
await rate_limiter.wait_if_needed(estimated)
|
|
92
|
+
|
|
82
93
|
response = await self._client.send_message(
|
|
83
94
|
messages=messages,
|
|
84
95
|
system=_PLANNER_SYSTEM_PROMPT,
|
|
@@ -86,6 +97,9 @@ class Planner:
|
|
|
86
97
|
max_tokens=2048,
|
|
87
98
|
)
|
|
88
99
|
|
|
100
|
+
if rate_limiter is not None:
|
|
101
|
+
rate_limiter.record(response.input_tokens)
|
|
102
|
+
|
|
89
103
|
# Extract text response
|
|
90
104
|
text = ""
|
|
91
105
|
for block in response.content:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|