alpiecode 10.0.1__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.
Files changed (58) hide show
  1. {alpiecode-10.0.1 → alpiecode-10.0.2}/PKG-INFO +1 -1
  2. {alpiecode-10.0.1 → alpiecode-10.0.2}/pyproject.toml +1 -1
  3. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/alpiecode.egg-info/PKG-INFO +1 -1
  4. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/alpiecode.egg-info/SOURCES.txt +8 -1
  5. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/__init__.py +1 -1
  6. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/agent.py +37 -0
  7. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/cache.py +117 -0
  8. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/context.py +33 -1
  9. alpiecode-10.0.2/src/codeagent/metrics.py +83 -0
  10. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/orchestrator.py +38 -0
  11. alpiecode-10.0.2/src/codeagent/system1.py +279 -0
  12. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/tools.py +15 -1
  13. alpiecode-10.0.2/test/test_benchmark_system1.py +90 -0
  14. alpiecode-10.0.2/test/test_cache_reads.py +66 -0
  15. alpiecode-10.0.2/test/test_context_filter.py +57 -0
  16. alpiecode-10.0.2/test/test_metrics.py +46 -0
  17. alpiecode-10.0.2/test/test_system1.py +84 -0
  18. {alpiecode-10.0.1 → alpiecode-10.0.2}/README.md +0 -0
  19. {alpiecode-10.0.1 → alpiecode-10.0.2}/setup.cfg +0 -0
  20. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/alpiecode/__init__.py +0 -0
  21. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  22. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/alpiecode.egg-info/entry_points.txt +0 -0
  23. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/alpiecode.egg-info/requires.txt +0 -0
  24. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/alpiecode.egg-info/top_level.txt +0 -0
  25. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/backends/__init__.py +0 -0
  26. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/backends/base.py +0 -0
  27. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/backends/local_backend.py +0 -0
  28. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/backends/openai_backend.py +0 -0
  29. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/cli.py +0 -0
  30. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/client.py +0 -0
  31. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/compaction.py +0 -0
  32. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/config.py +0 -0
  33. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/discovery.py +0 -0
  34. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/doctor.py +0 -0
  35. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/executor.py +0 -0
  36. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/extension/alpiecode.vsix +0 -0
  37. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/git_ops.py +0 -0
  38. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/github.py +0 -0
  39. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/guardian.py +0 -0
  40. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/guardrails.py +0 -0
  41. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/ipython_ext.py +0 -0
  42. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/local_model.py +0 -0
  43. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/media.py +0 -0
  44. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/memory.py +0 -0
  45. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/progress.py +0 -0
  46. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/prompt.py +0 -0
  47. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/rephraser.py +0 -0
  48. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/server.py +0 -0
  49. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/session.py +0 -0
  50. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/updater.py +0 -0
  51. {alpiecode-10.0.1 → alpiecode-10.0.2}/src/codeagent/vscode_installer.py +0 -0
  52. {alpiecode-10.0.1 → alpiecode-10.0.2}/test/test_audit_fixes.py +0 -0
  53. {alpiecode-10.0.1 → alpiecode-10.0.2}/test/test_chat_mode.py +0 -0
  54. {alpiecode-10.0.1 → alpiecode-10.0.2}/test/test_claude_codex_capabilities.py +0 -0
  55. {alpiecode-10.0.1 → alpiecode-10.0.2}/test/test_offline_mode.py +0 -0
  56. {alpiecode-10.0.1 → alpiecode-10.0.2}/test/test_phase1.py +0 -0
  57. {alpiecode-10.0.1 → alpiecode-10.0.2}/test/test_phase2.py +0 -0
  58. {alpiecode-10.0.1 → alpiecode-10.0.2}/test/test_phase3.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 10.0.1
3
+ Version: 10.0.2
4
4
  Summary: AlpieCode
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "alpiecode"
7
- version = "10.0.1"
7
+ version = "10.0.2"
8
8
  description = "AlpieCode"
9
9
  requires-python = ">=3.9"
10
10
  dependencies = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 10.0.1
3
+ Version: 10.0.2
4
4
  Summary: AlpieCode
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.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.1"
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": 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