alpiecode 10.0.0__tar.gz → 10.0.2__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.
- {alpiecode-10.0.0 → alpiecode-10.0.2}/PKG-INFO +2 -2
- {alpiecode-10.0.0 → alpiecode-10.0.2}/README.md +1 -1
- {alpiecode-10.0.0 → alpiecode-10.0.2}/pyproject.toml +2 -2
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/alpiecode.egg-info/PKG-INFO +2 -2
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/alpiecode.egg-info/SOURCES.txt +8 -1
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/__init__.py +1 -1
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/agent.py +37 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/cache.py +117 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/context.py +33 -1
- alpiecode-10.0.2/src/codeagent/metrics.py +83 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/orchestrator.py +38 -0
- alpiecode-10.0.2/src/codeagent/system1.py +279 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/tools.py +15 -1
- alpiecode-10.0.2/test/test_benchmark_system1.py +90 -0
- alpiecode-10.0.2/test/test_cache_reads.py +66 -0
- alpiecode-10.0.2/test/test_context_filter.py +57 -0
- alpiecode-10.0.2/test/test_metrics.py +46 -0
- alpiecode-10.0.2/test/test_system1.py +84 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/setup.cfg +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/alpiecode/__init__.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/alpiecode.egg-info/dependency_links.txt +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/alpiecode.egg-info/entry_points.txt +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/alpiecode.egg-info/requires.txt +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/alpiecode.egg-info/top_level.txt +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/backends/__init__.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/backends/base.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/backends/local_backend.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/backends/openai_backend.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/cli.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/client.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/compaction.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/config.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/discovery.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/doctor.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/executor.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/extension/alpiecode.vsix +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/git_ops.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/github.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/guardian.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/guardrails.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/ipython_ext.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/local_model.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/media.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/memory.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/progress.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/prompt.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/rephraser.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/server.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/session.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/updater.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/src/codeagent/vscode_installer.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/test/test_audit_fixes.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/test/test_chat_mode.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/test/test_claude_codex_capabilities.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/test/test_offline_mode.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/test/test_phase1.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/test/test_phase2.py +0 -0
- {alpiecode-10.0.0 → alpiecode-10.0.2}/test/test_phase3.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alpiecode
|
|
3
|
-
Version: 10.0.
|
|
4
|
-
Summary: AlpieCode
|
|
3
|
+
Version: 10.0.2
|
|
4
|
+
Summary: AlpieCode
|
|
5
5
|
Requires-Python: >=3.9
|
|
6
6
|
Requires-Dist: huggingface_hub>=0.20.0
|
|
7
7
|
Requires-Dist: rich>=13.0
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# AlpieCode
|
|
2
2
|
|
|
3
3
|
[](https://www.python.org/)
|
|
4
4
|
[](https://github.com/169Pi/codeagent-poc)
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "alpiecode"
|
|
7
|
-
version = "10.0.
|
|
8
|
-
description = "AlpieCode
|
|
7
|
+
version = "10.0.2"
|
|
8
|
+
description = "AlpieCode"
|
|
9
9
|
requires-python = ">=3.9"
|
|
10
10
|
dependencies = [
|
|
11
11
|
"huggingface_hub>=0.20.0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alpiecode
|
|
3
|
-
Version: 10.0.
|
|
4
|
-
Summary: AlpieCode
|
|
3
|
+
Version: 10.0.2
|
|
4
|
+
Summary: AlpieCode
|
|
5
5
|
Requires-Python: >=3.9
|
|
6
6
|
Requires-Dist: huggingface_hub>=0.20.0
|
|
7
7
|
Requires-Dist: rich>=13.0
|
|
@@ -26,12 +26,14 @@ src/codeagent/ipython_ext.py
|
|
|
26
26
|
src/codeagent/local_model.py
|
|
27
27
|
src/codeagent/media.py
|
|
28
28
|
src/codeagent/memory.py
|
|
29
|
+
src/codeagent/metrics.py
|
|
29
30
|
src/codeagent/orchestrator.py
|
|
30
31
|
src/codeagent/progress.py
|
|
31
32
|
src/codeagent/prompt.py
|
|
32
33
|
src/codeagent/rephraser.py
|
|
33
34
|
src/codeagent/server.py
|
|
34
35
|
src/codeagent/session.py
|
|
36
|
+
src/codeagent/system1.py
|
|
35
37
|
src/codeagent/tools.py
|
|
36
38
|
src/codeagent/updater.py
|
|
37
39
|
src/codeagent/vscode_installer.py
|
|
@@ -41,9 +43,14 @@ src/codeagent/backends/local_backend.py
|
|
|
41
43
|
src/codeagent/backends/openai_backend.py
|
|
42
44
|
src/codeagent/extension/alpiecode.vsix
|
|
43
45
|
test/test_audit_fixes.py
|
|
46
|
+
test/test_benchmark_system1.py
|
|
47
|
+
test/test_cache_reads.py
|
|
44
48
|
test/test_chat_mode.py
|
|
45
49
|
test/test_claude_codex_capabilities.py
|
|
50
|
+
test/test_context_filter.py
|
|
51
|
+
test/test_metrics.py
|
|
46
52
|
test/test_offline_mode.py
|
|
47
53
|
test/test_phase1.py
|
|
48
54
|
test/test_phase2.py
|
|
49
|
-
test/test_phase3.py
|
|
55
|
+
test/test_phase3.py
|
|
56
|
+
test/test_system1.py
|
|
@@ -17,7 +17,7 @@ Supports:
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from typing import Optional, Any, Dict
|
|
19
19
|
|
|
20
|
-
__version__ = "10.0.
|
|
20
|
+
__version__ = "10.0.2"
|
|
21
21
|
|
|
22
22
|
def run(task: str, workdir: str = ".", reasoning_level: str = "thinking", **kwargs):
|
|
23
23
|
"""Run an autonomous coding task programmatically."""
|
|
@@ -432,6 +432,43 @@ def run_agent(
|
|
|
432
432
|
style="dim"
|
|
433
433
|
)
|
|
434
434
|
|
|
435
|
+
elif event.type == "system1_intervention" and verbose:
|
|
436
|
+
data = event.data
|
|
437
|
+
action = data.get("action", "")
|
|
438
|
+
err_type = data.get("error_type", "")
|
|
439
|
+
directive = data.get("directive", "")
|
|
440
|
+
if HAS_RICH:
|
|
441
|
+
console.print(Panel(
|
|
442
|
+
f"[bold yellow]⚡ System 1 (Jev) Intervention[/bold yellow] (Turn {data.get('turn')})\n"
|
|
443
|
+
f"• Action: [cyan]{action}[/cyan] | Error Type: [red]{err_type}[/red]\n"
|
|
444
|
+
f"• Directive: {directive}",
|
|
445
|
+
border_style="yellow",
|
|
446
|
+
padding=(0, 1)
|
|
447
|
+
))
|
|
448
|
+
else:
|
|
449
|
+
print(f"\n⚡ [System 1 Intervention] {action}: {directive}")
|
|
450
|
+
|
|
451
|
+
elif event.type == "metrics" and verbose:
|
|
452
|
+
data = event.data
|
|
453
|
+
if HAS_RICH:
|
|
454
|
+
console.print(Panel(
|
|
455
|
+
f"⏱️ [bold]Latency:[/bold] {data.get('total_task_latency_s', 0)}s | "
|
|
456
|
+
f"🧠 [bold]LLM Calls:[/bold] {data.get('llm_call_count', 0)} ({data.get('llm_inference_time_ms', 0)}ms)\n"
|
|
457
|
+
f"🛠️ [bold]Tools Time:[/bold] {data.get('total_tool_execution_ms', 0)}ms ({data.get('tool_call_count', 0)} calls) | "
|
|
458
|
+
f"📦 [bold]Context Prep:[/bold] {data.get('context_prep_time_ms', 0)}ms | "
|
|
459
|
+
f"⚡ [bold]Cache Hits:[/bold] {data.get('cache_hits', 0)}",
|
|
460
|
+
title="📊 Task Performance Summary",
|
|
461
|
+
border_style="cyan",
|
|
462
|
+
padding=(0, 1)
|
|
463
|
+
))
|
|
464
|
+
else:
|
|
465
|
+
print(
|
|
466
|
+
f"\n📊 [Metrics] Task Latency: {data.get('total_task_latency_s')}s | "
|
|
467
|
+
f"LLM Calls: {data.get('llm_call_count')} | "
|
|
468
|
+
f"Tools Time: {data.get('total_tool_execution_ms')}ms | "
|
|
469
|
+
f"Cache Hits: {data.get('cache_hits')}"
|
|
470
|
+
)
|
|
471
|
+
|
|
435
472
|
elif event.type == "safety_ceiling" and verbose:
|
|
436
473
|
if HAS_RICH:
|
|
437
474
|
console.print(
|
|
@@ -193,3 +193,120 @@ def get_cache() -> ResponseCache:
|
|
|
193
193
|
if _CACHE is None:
|
|
194
194
|
_CACHE = ResponseCache()
|
|
195
195
|
return _CACHE
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ── File Read & Test Result Caching ─────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
class FileReadCache:
|
|
201
|
+
"""In-memory cache for repeated file reads keyed by (path, mtime, size, start_line, end_line)."""
|
|
202
|
+
|
|
203
|
+
def __init__(self, max_entries: int = 1000):
|
|
204
|
+
self.max_entries = max_entries
|
|
205
|
+
self._cache: Dict[str, Dict[str, Any]] = {}
|
|
206
|
+
self.hits = 0
|
|
207
|
+
self.misses = 0
|
|
208
|
+
|
|
209
|
+
def get(self, file_path: Path, start_line: Optional[int] = None, end_line: Optional[int] = None) -> Optional[str]:
|
|
210
|
+
try:
|
|
211
|
+
st = file_path.stat()
|
|
212
|
+
key = f"{file_path.resolve()}:{st.st_mtime}:{st.st_size}:{start_line}:{end_line}"
|
|
213
|
+
except OSError:
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
entry = self._cache.get(key)
|
|
217
|
+
if entry is not None:
|
|
218
|
+
self.hits += 1
|
|
219
|
+
return entry["content"]
|
|
220
|
+
|
|
221
|
+
self.misses += 1
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
def put(self, file_path: Path, content: str, start_line: Optional[int] = None, end_line: Optional[int] = None) -> None:
|
|
225
|
+
try:
|
|
226
|
+
st = file_path.stat()
|
|
227
|
+
key = f"{file_path.resolve()}:{st.st_mtime}:{st.st_size}:{start_line}:{end_line}"
|
|
228
|
+
except OSError:
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
if len(self._cache) >= self.max_entries:
|
|
232
|
+
keys = list(self._cache.keys())[: max(1, self.max_entries // 5)]
|
|
233
|
+
for k in keys:
|
|
234
|
+
self._cache.pop(k, None)
|
|
235
|
+
|
|
236
|
+
self._cache[key] = {"content": content, "cached_at": time.time()}
|
|
237
|
+
|
|
238
|
+
def invalidate(self, file_path: Optional[Path] = None) -> None:
|
|
239
|
+
if file_path is None:
|
|
240
|
+
self._cache.clear()
|
|
241
|
+
return
|
|
242
|
+
prefix = f"{file_path.resolve()}:"
|
|
243
|
+
keys_to_del = [k for k in self._cache if k.startswith(prefix)]
|
|
244
|
+
for k in keys_to_del:
|
|
245
|
+
self._cache.pop(k, None)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class TestResultCache:
|
|
249
|
+
"""Caches test command execution results when workspace files are unchanged."""
|
|
250
|
+
__test__ = False
|
|
251
|
+
|
|
252
|
+
def __init__(self, max_entries: int = 100):
|
|
253
|
+
self.max_entries = max_entries
|
|
254
|
+
self._cache: Dict[str, Dict[str, Any]] = {}
|
|
255
|
+
self._workspace_versions: Dict[str, float] = {}
|
|
256
|
+
self.hits = 0
|
|
257
|
+
self.misses = 0
|
|
258
|
+
|
|
259
|
+
def _is_test_cmd(self, cmd: str) -> bool:
|
|
260
|
+
c = cmd.strip().lower()
|
|
261
|
+
return (
|
|
262
|
+
"pytest" in c
|
|
263
|
+
or "unittest" in c
|
|
264
|
+
or "npm test" in c
|
|
265
|
+
or "cargo test" in c
|
|
266
|
+
or "go test" in c
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def get(self, workdir: Path, cmd: str) -> Optional[str]:
|
|
270
|
+
if not self._is_test_cmd(cmd):
|
|
271
|
+
return None
|
|
272
|
+
version = self._workspace_versions.get(str(workdir.resolve()), 0.0)
|
|
273
|
+
key = f"{workdir.resolve()}:{cmd.strip()}:{version}"
|
|
274
|
+
entry = self._cache.get(key)
|
|
275
|
+
if entry is not None:
|
|
276
|
+
self.hits += 1
|
|
277
|
+
return f"[Cached test result]\n{entry['content']}"
|
|
278
|
+
self.misses += 1
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
def put(self, workdir: Path, cmd: str, result: str) -> None:
|
|
282
|
+
if not self._is_test_cmd(cmd):
|
|
283
|
+
return
|
|
284
|
+
version = self._workspace_versions.get(str(workdir.resolve()), 0.0)
|
|
285
|
+
key = f"{workdir.resolve()}:{cmd.strip()}:{version}"
|
|
286
|
+
self._cache[key] = {"content": result, "cached_at": time.time()}
|
|
287
|
+
|
|
288
|
+
def invalidate(self, workdir: Optional[Path] = None) -> None:
|
|
289
|
+
if workdir is None:
|
|
290
|
+
self._cache.clear()
|
|
291
|
+
self._workspace_versions.clear()
|
|
292
|
+
return
|
|
293
|
+
w_key = str(workdir.resolve())
|
|
294
|
+
self._workspace_versions[w_key] = time.time()
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
_FILE_CACHE: Optional[FileReadCache] = None
|
|
298
|
+
_TEST_CACHE: Optional[TestResultCache] = None
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def get_file_cache() -> FileReadCache:
|
|
302
|
+
global _FILE_CACHE
|
|
303
|
+
if _FILE_CACHE is None:
|
|
304
|
+
_FILE_CACHE = FileReadCache()
|
|
305
|
+
return _FILE_CACHE
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def get_test_cache() -> TestResultCache:
|
|
309
|
+
global _TEST_CACHE
|
|
310
|
+
if _TEST_CACHE is None:
|
|
311
|
+
_TEST_CACHE = TestResultCache()
|
|
312
|
+
return _TEST_CACHE
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import re
|
|
1
2
|
"""
|
|
2
3
|
Conversation context manager for AlpieCode.
|
|
3
4
|
|
|
@@ -198,10 +199,11 @@ class ContextManager:
|
|
|
198
199
|
) -> None:
|
|
199
200
|
"""Append a tool execution result and record metadata."""
|
|
200
201
|
self.invalidate_cache()
|
|
202
|
+
filtered_content = filter_noisy_tool_output(content, tool_name=tool_name)
|
|
201
203
|
msg = {
|
|
202
204
|
"role": "tool",
|
|
203
205
|
"tool_call_id": tool_call_id,
|
|
204
|
-
"content":
|
|
206
|
+
"content": filtered_content,
|
|
205
207
|
}
|
|
206
208
|
self._messages.append(dict(msg))
|
|
207
209
|
self._raw_messages.append(copy.deepcopy(msg))
|
|
@@ -415,3 +417,33 @@ class ContextManager:
|
|
|
415
417
|
self._cached_summary = extract_conversation_summary(self._messages, self._metadata)
|
|
416
418
|
return True
|
|
417
419
|
return False
|
|
420
|
+
|
|
421
|
+
def filter_noisy_tool_output(content: str, tool_name: Optional[str] = None) -> str:
|
|
422
|
+
"""
|
|
423
|
+
Filter noisy tool output to conserve LLM context window.
|
|
424
|
+
Extracts relevant errors/failures and condenses passing test spam.
|
|
425
|
+
"""
|
|
426
|
+
if not content or len(content) < 300:
|
|
427
|
+
return content
|
|
428
|
+
|
|
429
|
+
# 1. Pytest / Unittest clean pass compression
|
|
430
|
+
if "passed in" in content and ("test session starts" in content or "collected" in content):
|
|
431
|
+
if "FAILED" not in content and "ERROR" not in content and "failed" not in content:
|
|
432
|
+
m = re.search(r"(=+\s*\d+\s+passed.*in\s+[\d\.]+s\s*=+)", content)
|
|
433
|
+
if m:
|
|
434
|
+
return f"[Clean Test Output]\n{m.group(1)}"
|
|
435
|
+
else:
|
|
436
|
+
# Tests had failures: strip passing test lines, keep failure traceback
|
|
437
|
+
lines = content.splitlines()
|
|
438
|
+
filtered = [
|
|
439
|
+
line for line in lines
|
|
440
|
+
if not (" PASSED" in line and ("[" in line and "%]" in line))
|
|
441
|
+
]
|
|
442
|
+
return "\n".join(filtered)
|
|
443
|
+
|
|
444
|
+
if "Ran " in content and " tests in " in content and "OK" in content and "FAILED" not in content:
|
|
445
|
+
m = re.search(r"(Ran \d+ tests? in [\d\.]+s\s+OK)", content)
|
|
446
|
+
if m:
|
|
447
|
+
return f"[Clean Test Output]\n{m.group(1)}"
|
|
448
|
+
|
|
449
|
+
return content
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Performance metrics tracking for AlpieCode.
|
|
3
|
+
|
|
4
|
+
Tracks per-task metrics:
|
|
5
|
+
- LLM call count
|
|
6
|
+
- Total task latency
|
|
7
|
+
- Tool execution time
|
|
8
|
+
- Context preparation time
|
|
9
|
+
- Cache hits
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class TaskMetrics:
|
|
19
|
+
task_id: str = ""
|
|
20
|
+
start_time: float = field(default_factory=time.time)
|
|
21
|
+
end_time: Optional[float] = None
|
|
22
|
+
llm_call_count: int = 0
|
|
23
|
+
total_task_latency_ms: float = 0.0
|
|
24
|
+
total_tool_execution_ms: float = 0.0
|
|
25
|
+
context_prep_time_ms: float = 0.0
|
|
26
|
+
llm_inference_time_ms: float = 0.0
|
|
27
|
+
tool_call_count: int = 0
|
|
28
|
+
cache_hits: int = 0
|
|
29
|
+
errors_encountered: int = 0
|
|
30
|
+
|
|
31
|
+
def start(self) -> "TaskMetrics":
|
|
32
|
+
self.start_time = time.time()
|
|
33
|
+
return self
|
|
34
|
+
|
|
35
|
+
def finish(self) -> "TaskMetrics":
|
|
36
|
+
self.end_time = time.time()
|
|
37
|
+
self.total_task_latency_ms = (self.end_time - self.start_time) * 1000.0
|
|
38
|
+
return self
|
|
39
|
+
|
|
40
|
+
def record_llm_call(self, duration_ms: float = 0.0) -> None:
|
|
41
|
+
self.llm_call_count += 1
|
|
42
|
+
self.llm_inference_time_ms += duration_ms
|
|
43
|
+
|
|
44
|
+
def record_tool_call(self, duration_ms: float = 0.0) -> None:
|
|
45
|
+
self.tool_call_count += 1
|
|
46
|
+
self.total_tool_execution_ms += duration_ms
|
|
47
|
+
|
|
48
|
+
def record_context_prep(self, duration_ms: float = 0.0) -> None:
|
|
49
|
+
self.context_prep_time_ms += duration_ms
|
|
50
|
+
|
|
51
|
+
def record_cache_hit(self) -> None:
|
|
52
|
+
self.cache_hits += 1
|
|
53
|
+
|
|
54
|
+
def record_error(self) -> None:
|
|
55
|
+
self.errors_encountered += 1
|
|
56
|
+
|
|
57
|
+
def summary(self) -> Dict[str, Any]:
|
|
58
|
+
latency_ms = self.total_task_latency_ms
|
|
59
|
+
if self.end_time is None:
|
|
60
|
+
latency_ms = (time.time() - self.start_time) * 1000.0
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
"task_id": self.task_id,
|
|
64
|
+
"llm_call_count": self.llm_call_count,
|
|
65
|
+
"tool_call_count": self.tool_call_count,
|
|
66
|
+
"cache_hits": self.cache_hits,
|
|
67
|
+
"total_task_latency_ms": round(latency_ms, 2),
|
|
68
|
+
"total_task_latency_s": round(latency_ms / 1000.0, 2),
|
|
69
|
+
"total_tool_execution_ms": round(self.total_tool_execution_ms, 2),
|
|
70
|
+
"context_prep_time_ms": round(self.context_prep_time_ms, 2),
|
|
71
|
+
"llm_inference_time_ms": round(self.llm_inference_time_ms, 2),
|
|
72
|
+
"errors_encountered": self.errors_encountered,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
def format_log_line(self) -> str:
|
|
76
|
+
s = self.summary()
|
|
77
|
+
return (
|
|
78
|
+
f"📊 [Metrics] Task completed in {s['total_task_latency_s']}s | "
|
|
79
|
+
f"LLM calls: {s['llm_call_count']} ({s['llm_inference_time_ms']}ms) | "
|
|
80
|
+
f"Tools: {s['tool_call_count']} ({s['total_tool_execution_ms']}ms) | "
|
|
81
|
+
f"Context prep: {s['context_prep_time_ms']}ms | "
|
|
82
|
+
f"Cache hits: {s['cache_hits']}"
|
|
83
|
+
)
|
|
@@ -54,6 +54,9 @@ from .progress import ProgressMonitor
|
|
|
54
54
|
from .rephraser import PromptRephraser
|
|
55
55
|
from .prompt import PromptBuilder, classify_task
|
|
56
56
|
from .session import Session, SessionManager
|
|
57
|
+
from .metrics import TaskMetrics
|
|
58
|
+
from .system1 import get_system1_controller, ActionCategory
|
|
59
|
+
import time
|
|
57
60
|
|
|
58
61
|
|
|
59
62
|
@dataclass
|
|
@@ -145,6 +148,9 @@ class AgentOrchestrator:
|
|
|
145
148
|
"""Run full agent task loop. Yields AgentEvents."""
|
|
146
149
|
|
|
147
150
|
# ── Phase 0: Discovery — pre-compute task intelligence ──
|
|
151
|
+
task_metrics = TaskMetrics(task_id=session.id).start()
|
|
152
|
+
system1 = get_system1_controller()
|
|
153
|
+
system1.reset()
|
|
148
154
|
task_context = build_task_context(task, session.workdir)
|
|
149
155
|
complexity = complexity or task_context.complexity
|
|
150
156
|
|
|
@@ -191,6 +197,9 @@ class AgentOrchestrator:
|
|
|
191
197
|
if cached.get("reasoning"):
|
|
192
198
|
yield AgentEvent("thinking", {"content": cached["reasoning"]})
|
|
193
199
|
yield AgentEvent("message", {"content": cached["response"]})
|
|
200
|
+
task_metrics.record_cache_hit()
|
|
201
|
+
task_metrics.finish()
|
|
202
|
+
yield AgentEvent("metrics", task_metrics.summary())
|
|
194
203
|
yield AgentEvent("done", {"summary": cached["response"]})
|
|
195
204
|
return
|
|
196
205
|
|
|
@@ -338,6 +347,7 @@ class AgentOrchestrator:
|
|
|
338
347
|
else:
|
|
339
348
|
max_tokens = 2048 if is_offline else effective_max_tokens
|
|
340
349
|
|
|
350
|
+
llm_call_start = time.time()
|
|
341
351
|
if hasattr(task_backend, "chat_completion_stream"):
|
|
342
352
|
resp = None
|
|
343
353
|
for event_type, data in task_backend.chat_completion_stream(
|
|
@@ -444,6 +454,7 @@ class AgentOrchestrator:
|
|
|
444
454
|
# Execute final tool calls before finishing
|
|
445
455
|
results = session.executor.execute_tool_calls(tool_calls)
|
|
446
456
|
for res in results:
|
|
457
|
+
task_metrics.record_tool_call(res.duration_ms)
|
|
447
458
|
yield AgentEvent("tool_result", {
|
|
448
459
|
"turn": turn,
|
|
449
460
|
"id": res.tool_call_id,
|
|
@@ -567,6 +578,8 @@ class AgentOrchestrator:
|
|
|
567
578
|
except Exception:
|
|
568
579
|
pass
|
|
569
580
|
|
|
581
|
+
task_metrics.finish()
|
|
582
|
+
yield AgentEvent("metrics", task_metrics.summary())
|
|
570
583
|
yield AgentEvent("done", {"summary": resp.content})
|
|
571
584
|
return
|
|
572
585
|
|
|
@@ -585,6 +598,7 @@ class AgentOrchestrator:
|
|
|
585
598
|
results = session.executor.execute_tool_calls(tool_calls)
|
|
586
599
|
|
|
587
600
|
for res in results:
|
|
601
|
+
task_metrics.record_tool_call(res.duration_ms)
|
|
588
602
|
yield AgentEvent("tool_result", {
|
|
589
603
|
"turn": turn,
|
|
590
604
|
"id": res.tool_call_id,
|
|
@@ -594,6 +608,26 @@ class AgentOrchestrator:
|
|
|
594
608
|
})
|
|
595
609
|
session.context.add_tool_result(res.tool_call_id, res.content)
|
|
596
610
|
|
|
611
|
+
# ── System 1 Jev Decision Plane Evaluation ──
|
|
612
|
+
for tc, res in zip(tool_calls, results):
|
|
613
|
+
tc_args = tc.arguments if isinstance(tc.arguments, dict) else {}
|
|
614
|
+
if isinstance(tc.arguments, str):
|
|
615
|
+
try:
|
|
616
|
+
import json as _json
|
|
617
|
+
tc_args = _json.loads(tc.arguments)
|
|
618
|
+
except Exception:
|
|
619
|
+
tc_args = {}
|
|
620
|
+
decision = system1.evaluate_turn(tc.name, tc_args, res.content, turn)
|
|
621
|
+
if decision.directive:
|
|
622
|
+
session.context.add_user_message(decision.directive)
|
|
623
|
+
yield AgentEvent("system1_intervention", {
|
|
624
|
+
"turn": turn,
|
|
625
|
+
"action": decision.action.value,
|
|
626
|
+
"error_type": decision.error_type.value,
|
|
627
|
+
"directive": decision.directive,
|
|
628
|
+
"confidence": decision.confidence,
|
|
629
|
+
})
|
|
630
|
+
|
|
597
631
|
# Track touched files and commands for walkthrough
|
|
598
632
|
for tc in tool_calls:
|
|
599
633
|
if tc.name in ("write_file", "edit_file", "apply_patch"):
|
|
@@ -641,11 +675,15 @@ class AgentOrchestrator:
|
|
|
641
675
|
yield AgentEvent("message", {"content": resp.content})
|
|
642
676
|
|
|
643
677
|
extract_and_save_memories(session.workdir, getattr(session.context, 'all_messages', session.context.messages))
|
|
678
|
+
task_metrics.finish()
|
|
679
|
+
yield AgentEvent("metrics", task_metrics.summary())
|
|
644
680
|
yield AgentEvent("done", {"summary": resp.content})
|
|
645
681
|
return
|
|
646
682
|
|
|
647
683
|
# Empty response
|
|
648
684
|
extract_and_save_memories(session.workdir, getattr(session.context, 'all_messages', session.context.messages))
|
|
685
|
+
task_metrics.finish()
|
|
686
|
+
yield AgentEvent("metrics", task_metrics.summary())
|
|
649
687
|
yield AgentEvent("done", {"summary": "Task completed."})
|
|
650
688
|
return
|
|
651
689
|
|