@miller-tech/uap 1.110.0 → 1.111.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.110.0",
3
+ "version": "1.111.0",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -401,6 +401,22 @@ except Exception: # pragma: no cover - optional middleware
401
401
  _TOOLCALL_NORMALIZER_OK = False
402
402
 
403
403
 
404
+ def _record_project_telemetry(body, model, usage) -> None:
405
+ """Fail-open per-project routing/cost telemetry: derive the request's project
406
+ dir and append a task_outcomes row to its analytics DB so per-project
407
+ dashboards see the model calls this shared proxy makes. See
408
+ tools/agents/scripts/project_telemetry.py. Never raises."""
409
+ try:
410
+ import sys as _s
411
+ _d = os.path.dirname(os.path.abspath(__file__))
412
+ if _d not in _s.path:
413
+ _s.path.insert(0, _d)
414
+ import project_telemetry as _pt
415
+ _pt.record_from_request(body, model, usage)
416
+ except Exception:
417
+ pass
418
+
419
+
404
420
  def _toolcall_workdir_hint(messages: list, limit: int = 8000) -> str:
405
421
  """Recent text/tool_result content (capped) so derive_workdir can recover the
406
422
  real workdir echoed in command outputs even when the model's own tool-call
@@ -9364,6 +9380,7 @@ async def messages(request: Request):
9364
9380
  upstream_input = anthropic_resp.get("usage", {}).get("input_tokens", 0)
9365
9381
  if upstream_input > 0:
9366
9382
  monitor.last_input_tokens = upstream_input
9383
+ _record_project_telemetry(body, model, anthropic_resp.get("usage", {}))
9367
9384
  if PROXY_FORCE_NON_STREAM:
9368
9385
  logger.info(
9369
9386
  "FORCED NON-STREAM: served stream response via guarded non-stream path"
@@ -9803,6 +9820,7 @@ async def messages(request: Request):
9803
9820
  if upstream_input > 0:
9804
9821
  monitor.last_input_tokens = upstream_input
9805
9822
 
9823
+ _record_project_telemetry(body, model, anthropic_resp.get("usage", {}))
9806
9824
  return anthropic_resp
9807
9825
 
9808
9826
 
@@ -0,0 +1,183 @@
1
+ """Per-project routing/cost telemetry for the shared UAP anthropic proxy.
2
+
3
+ The proxy is a shared singleton that serves *every* project, so per-project
4
+ dashboards (which read ``<project>/agents/data/memory/model_analytics.db`` ->
5
+ ``task_outcomes``) never see the model calls the proxy actually makes. This
6
+ module closes that gap: after each completed response the proxy calls
7
+ ``record_from_request`` with the request body + model + usage; we derive the
8
+ project directory from absolute paths echoed in the request and append one
9
+ ``task_outcomes`` row to that project's analytics DB.
10
+
11
+ Design constraints (it runs in the hot path of a live proxy):
12
+ * fully fail-open — never raise into the caller;
13
+ * bounded work — scans a capped slice of the request, does a few fs stat calls;
14
+ * schema-compatible with src/models/analytics.ts so the existing dashboard
15
+ reads it with no change. Local models are zero-cost, so ``cost=0`` and the
16
+ dashboard computes the counterfactual-vs-frontier saving itself.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ import re
23
+ import sqlite3
24
+ from datetime import datetime, timezone
25
+
26
+ # A UAP project root is identifiable by one of these child markers.
27
+ _PROJECT_MARKERS = ("agents/data/memory", ".uap", ".git")
28
+ _ABS_PATH_RE = re.compile(r"/(?:[\w.\-]+/){2,}[\w.\-]*")
29
+ # Bounds so a huge request can never stall the proxy hot path.
30
+ _MAX_SCAN_CHARS = 24_000
31
+ _MAX_CANDIDATE_PATHS = 40
32
+ _MAX_WALK_UP = 8
33
+
34
+ _CREATE_SQL = (
35
+ "CREATE TABLE IF NOT EXISTS task_outcomes ("
36
+ "modelId TEXT, taskType TEXT, complexity TEXT, success INTEGER, "
37
+ "durationMs INTEGER, tokensIn INTEGER, tokensOut INTEGER, cost REAL, "
38
+ "taskId TEXT, timestamp TEXT)"
39
+ )
40
+
41
+
42
+ def _iter_text(body: dict):
43
+ """Yield string fragments from an Anthropic request body, capped in total."""
44
+ budget = _MAX_SCAN_CHARS
45
+ msgs = body.get("messages") or []
46
+ # Newest messages first — the current workdir shows up in recent tool output.
47
+ for msg in reversed(msgs):
48
+ content = msg.get("content") if isinstance(msg, dict) else None
49
+ if isinstance(content, str):
50
+ frag = content[:budget]
51
+ budget -= len(frag)
52
+ yield frag
53
+ elif isinstance(content, list):
54
+ for block in content:
55
+ if not isinstance(block, dict):
56
+ continue
57
+ for key in ("text", "content", "input"):
58
+ val = block.get(key)
59
+ if isinstance(val, str):
60
+ frag = val[:budget]
61
+ budget -= len(frag)
62
+ yield frag
63
+ elif isinstance(val, (dict, list)):
64
+ frag = str(val)[:budget]
65
+ budget -= len(frag)
66
+ yield frag
67
+ if budget <= 0:
68
+ return
69
+ if budget <= 0:
70
+ return
71
+
72
+
73
+ def _project_root_for(path: str) -> str | None:
74
+ """Walk up from ``path`` to the nearest dir carrying a UAP project marker."""
75
+ d = path if os.path.isdir(path) else os.path.dirname(path)
76
+ for _ in range(_MAX_WALK_UP):
77
+ if not d or d == "/":
78
+ break
79
+ for marker in _PROJECT_MARKERS:
80
+ if os.path.exists(os.path.join(d, marker)):
81
+ return d
82
+ d = os.path.dirname(d)
83
+ return None
84
+
85
+
86
+ def derive_project_dir(body: dict) -> str | None:
87
+ """Best-effort project directory for a request, or None. Never raises."""
88
+ try:
89
+ counts: dict[str, int] = {}
90
+ seen = 0
91
+ for frag in _iter_text(body):
92
+ for m in _ABS_PATH_RE.finditer(frag):
93
+ if seen >= _MAX_CANDIDATE_PATHS:
94
+ break
95
+ seen += 1
96
+ root = _project_root_for(m.group(0))
97
+ if root:
98
+ counts[root] = counts.get(root, 0) + 1
99
+ if seen >= _MAX_CANDIDATE_PATHS:
100
+ break
101
+ if not counts:
102
+ return None
103
+ # Most-referenced project root wins; tie-break to the deepest path.
104
+ return max(counts.items(), key=lambda kv: (kv[1], len(kv[0])))[0]
105
+ except Exception:
106
+ return None
107
+
108
+
109
+ def _analytics_db_path(project_dir: str) -> str:
110
+ return os.path.join(project_dir, "agents", "data", "memory", "model_analytics.db")
111
+
112
+
113
+ def record_task_outcome(
114
+ project_dir: str,
115
+ model: str,
116
+ tokens_in: int,
117
+ tokens_out: int,
118
+ *,
119
+ duration_ms: int = 0,
120
+ cost: float = 0.0,
121
+ task_id: str = "",
122
+ task_type: str = "proxy",
123
+ complexity: str = "medium",
124
+ success: bool = True,
125
+ ) -> bool:
126
+ """Append one task_outcomes row to a project's analytics DB. Never raises.
127
+
128
+ Returns True on write, False if skipped (no dir, no tokens, or error).
129
+ """
130
+ try:
131
+ if not project_dir:
132
+ return False
133
+ db_dir = os.path.join(project_dir, "agents", "data", "memory")
134
+ if not os.path.isdir(db_dir):
135
+ # Only record where the project already has a memory dir — do not
136
+ # scaffold analytics into arbitrary derived directories.
137
+ return False
138
+ if (tokens_in or 0) <= 0 and (tokens_out or 0) <= 0:
139
+ return False
140
+ conn = sqlite3.connect(_analytics_db_path(project_dir), timeout=1.0)
141
+ try:
142
+ conn.execute(_CREATE_SQL)
143
+ conn.execute(
144
+ "INSERT INTO task_outcomes (modelId, taskType, complexity, success, "
145
+ "durationMs, tokensIn, tokensOut, cost, taskId, timestamp) "
146
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
147
+ (
148
+ str(model or "unknown"),
149
+ task_type,
150
+ complexity,
151
+ 1 if success else 0,
152
+ int(duration_ms or 0),
153
+ int(tokens_in or 0),
154
+ int(tokens_out or 0),
155
+ float(cost or 0.0),
156
+ str(task_id or ""),
157
+ datetime.now(timezone.utc).isoformat(),
158
+ ),
159
+ )
160
+ conn.commit()
161
+ finally:
162
+ conn.close()
163
+ return True
164
+ except Exception:
165
+ return False
166
+
167
+
168
+ def record_from_request(body: dict, model: str, usage: dict, *, task_id: str = "") -> bool:
169
+ """Proxy hot-path entry point: derive project + record. Never raises."""
170
+ try:
171
+ project_dir = derive_project_dir(body or {})
172
+ if not project_dir:
173
+ return False
174
+ u = usage or {}
175
+ return record_task_outcome(
176
+ project_dir,
177
+ model,
178
+ int(u.get("input_tokens") or 0),
179
+ int(u.get("output_tokens") or 0),
180
+ task_id=task_id,
181
+ )
182
+ except Exception:
183
+ return False
@@ -0,0 +1,107 @@
1
+ """Tests for project_telemetry: per-project proxy routing/cost telemetry."""
2
+ import importlib.util
3
+ import os
4
+ import sqlite3
5
+ import tempfile
6
+ import unittest
7
+ from pathlib import Path
8
+
9
+
10
+ def _load():
11
+ p = Path(__file__).resolve().parents[1] / "scripts" / "project_telemetry.py"
12
+ spec = importlib.util.spec_from_file_location("project_telemetry", p)
13
+ m = importlib.util.module_from_spec(spec)
14
+ spec.loader.exec_module(m)
15
+ return m
16
+
17
+
18
+ pt = _load()
19
+
20
+
21
+ def _make_project(root: str) -> str:
22
+ os.makedirs(os.path.join(root, "agents", "data", "memory"), exist_ok=True)
23
+ return root
24
+
25
+
26
+ class DeriveProjectDir(unittest.TestCase):
27
+ def test_finds_project_root_from_paths_in_messages(self):
28
+ with tempfile.TemporaryDirectory() as d:
29
+ proj = _make_project(os.path.join(d, "project-x"))
30
+ body = {"messages": [
31
+ {"role": "user", "content": "run the build"},
32
+ {"role": "assistant", "content": [
33
+ {"type": "tool_use", "input": {"file_path": f"{proj}/src/app.ts"}},
34
+ ]},
35
+ {"role": "user", "content": [
36
+ {"type": "tool_result", "content": f"wrote {proj}/src/app.ts and {proj}/README.md"},
37
+ ]},
38
+ ]}
39
+ self.assertEqual(pt.derive_project_dir(body), proj)
40
+
41
+ def test_returns_none_when_no_project_paths(self):
42
+ self.assertIsNone(pt.derive_project_dir({"messages": [
43
+ {"role": "user", "content": "hello, no paths here"}]}))
44
+
45
+ def test_fail_open_on_garbage(self):
46
+ for bad in (None, {}, {"messages": None}, {"messages": [1, 2, 3]}, "nope"):
47
+ self.assertIsNone(pt.derive_project_dir(bad if isinstance(bad, dict) else {"messages": bad}))
48
+
49
+
50
+ class RecordTaskOutcome(unittest.TestCase):
51
+ def _count_and_sums(self, proj):
52
+ db = os.path.join(proj, "agents", "data", "memory", "model_analytics.db")
53
+ c = sqlite3.connect(db)
54
+ row = c.execute("SELECT COUNT(*), SUM(tokensIn), SUM(tokensOut), SUM(cost) FROM task_outcomes").fetchone()
55
+ c.close()
56
+ return row
57
+
58
+ def test_writes_dashboard_compatible_row(self):
59
+ with tempfile.TemporaryDirectory() as d:
60
+ proj = _make_project(os.path.join(d, "p"))
61
+ ok = pt.record_task_outcome(proj, "qwen36-a3b", 1000, 500)
62
+ self.assertTrue(ok)
63
+ n, ti, to, cost = self._count_and_sums(proj)
64
+ self.assertEqual(n, 1)
65
+ self.assertEqual(ti, 1000)
66
+ self.assertEqual(to, 500)
67
+ self.assertEqual(cost, 0.0) # local model is zero-cost; dashboard computes counterfactual
68
+
69
+ def test_skips_when_no_memory_dir(self):
70
+ with tempfile.TemporaryDirectory() as d:
71
+ # no agents/data/memory created
72
+ self.assertFalse(pt.record_task_outcome(os.path.join(d, "bare"), "m", 100, 50))
73
+
74
+ def test_skips_when_zero_tokens(self):
75
+ with tempfile.TemporaryDirectory() as d:
76
+ proj = _make_project(os.path.join(d, "p"))
77
+ self.assertFalse(pt.record_task_outcome(proj, "m", 0, 0))
78
+
79
+ def test_appends_multiple_rows(self):
80
+ with tempfile.TemporaryDirectory() as d:
81
+ proj = _make_project(os.path.join(d, "p"))
82
+ pt.record_task_outcome(proj, "m", 10, 5)
83
+ pt.record_task_outcome(proj, "m", 20, 7)
84
+ n, ti, to, _ = self._count_and_sums(proj)
85
+ self.assertEqual((n, ti, to), (2, 30, 12))
86
+
87
+
88
+ class RecordFromRequest(unittest.TestCase):
89
+ def test_end_to_end_derive_and_write(self):
90
+ with tempfile.TemporaryDirectory() as d:
91
+ proj = _make_project(os.path.join(d, "proj"))
92
+ body = {"messages": [{"role": "user", "content": [
93
+ {"type": "tool_result", "content": f"ls {proj}/src returned app.ts"}]}]}
94
+ ok = pt.record_from_request(body, "qwen36", {"input_tokens": 1234, "output_tokens": 88})
95
+ self.assertTrue(ok)
96
+ c = sqlite3.connect(os.path.join(proj, "agents", "data", "memory", "model_analytics.db"))
97
+ n = c.execute("SELECT COUNT(*) FROM task_outcomes WHERE modelId='qwen36'").fetchone()[0]
98
+ c.close()
99
+ self.assertEqual(n, 1)
100
+
101
+ def test_fail_open_returns_false_never_raises(self):
102
+ self.assertFalse(pt.record_from_request(None, "m", None))
103
+ self.assertFalse(pt.record_from_request({"messages": []}, "m", {"input_tokens": 5}))
104
+
105
+
106
+ if __name__ == "__main__":
107
+ unittest.main()