zeno-cli 0.3.4__py3-none-any.whl

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 (69) hide show
  1. zeno_adapters/__init__.py +17 -0
  2. zeno_adapters/_common.py +38 -0
  3. zeno_adapters/anthropic.py +68 -0
  4. zeno_adapters/claude_code.py +101 -0
  5. zeno_adapters/crewai.py +92 -0
  6. zeno_adapters/langgraph.py +49 -0
  7. zeno_adapters/openai.py +108 -0
  8. zeno_cli/__init__.py +1 -0
  9. zeno_cli/_hooks/cc_bridge.py +1016 -0
  10. zeno_cli/doctor.py +535 -0
  11. zeno_cli/hook_install.py +269 -0
  12. zeno_cli/hud/__init__.py +1 -0
  13. zeno_cli/hud/hud_install.py +652 -0
  14. zeno_cli/hud/zeno_attention.py +288 -0
  15. zeno_cli/hud/zeno_cognition.py +457 -0
  16. zeno_cli/hud/zeno_hud.py +496 -0
  17. zeno_cli/interview_invites.py +342 -0
  18. zeno_cli/login.py +241 -0
  19. zeno_cli/main.py +2534 -0
  20. zeno_cli/onboard.py +206 -0
  21. zeno_cli/outreach.py +456 -0
  22. zeno_cli/version.py +67 -0
  23. zeno_cli-0.3.4.dist-info/METADATA +161 -0
  24. zeno_cli-0.3.4.dist-info/RECORD +69 -0
  25. zeno_cli-0.3.4.dist-info/WHEEL +4 -0
  26. zeno_cli-0.3.4.dist-info/entry_points.txt +4 -0
  27. zeno_core/__init__.py +67 -0
  28. zeno_core/analytics.py +193 -0
  29. zeno_core/rtlx_s.py +460 -0
  30. zeno_core/streak.py +178 -0
  31. zeno_core/tlx_s.py +192 -0
  32. zeno_sdk/__init__.py +6 -0
  33. zeno_sdk/_generated/__init__.py +6 -0
  34. zeno_sdk/_generated/client.py +819 -0
  35. zeno_sdk/_migrations/alembic/env.py +33 -0
  36. zeno_sdk/_migrations/alembic/script.py.mako +18 -0
  37. zeno_sdk/_migrations/alembic/versions/0001_initial.py +79 -0
  38. zeno_sdk/_migrations/alembic/versions/0002_cognition_samples.py +53 -0
  39. zeno_sdk/_migrations/alembic/versions/0003_cognition_drivers.py +41 -0
  40. zeno_sdk/_migrations/alembic/versions/0004_transcript_intelligence.py +248 -0
  41. zeno_sdk/_migrations/alembic.ini +35 -0
  42. zeno_sdk/_runtime.py +12 -0
  43. zeno_sdk/adapters/__init__.py +15 -0
  44. zeno_sdk/adapters/anthropic.py +5 -0
  45. zeno_sdk/adapters/claude_code.py +5 -0
  46. zeno_sdk/adapters/crewai.py +5 -0
  47. zeno_sdk/adapters/langgraph.py +5 -0
  48. zeno_sdk/adapters/openai.py +5 -0
  49. zeno_sdk/auth.py +25 -0
  50. zeno_sdk/client.py +87 -0
  51. zeno_sdk/config.py +61 -0
  52. zeno_sdk/daemon.py +72 -0
  53. zeno_sdk/privacy.py +46 -0
  54. zeno_sdk/session.py +179 -0
  55. zeno_sdk/storage.py +487 -0
  56. zeno_sdk/types/__init__.py +121 -0
  57. zeno_session_intel/__init__.py +19 -0
  58. zeno_session_intel/analytics.py +588 -0
  59. zeno_session_intel/compression.py +123 -0
  60. zeno_session_intel/ingest.py +376 -0
  61. zeno_session_intel/model.py +129 -0
  62. zeno_session_intel/parsers/__init__.py +31 -0
  63. zeno_session_intel/parsers/claude_code.py +169 -0
  64. zeno_session_intel/parsers/codex.py +265 -0
  65. zeno_session_intel/parsers/cursor.py +198 -0
  66. zeno_session_intel/prices.py +281 -0
  67. zeno_session_intel/schema.py +277 -0
  68. zeno_session_intel/signals.py +319 -0
  69. zeno_session_intel/taxonomy.py +71 -0
@@ -0,0 +1,319 @@
1
+ """Agent-behavioral signal extraction from a parsed transcript.
2
+
3
+ Ported from agentsview (MIT, see THIRD_PARTY_LICENSES.md): tool-health
4
+ (``internal/signals/toolhealth.go``), context pressure/compaction
5
+ (``internal/signals/context.go``), outcome classification
6
+ (``internal/signals/outcome.go``), and the health score (``internal/signals/score.go``,
7
+ ported verbatim - the penalty constants are the contract).
8
+
9
+ These score *agent* behavior and are stored on ``transcript_sessions``. They are
10
+ ORTHOGONAL to zeno's human-cognition ``attention_score``; the two complement, never
11
+ overlap. Pure computation over a ``ParsedSession``; stdlib-only, never raises.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import re
18
+ from dataclasses import dataclass, field
19
+
20
+ from .model import ParsedSession
21
+ from .taxonomy import normalize_tool_category
22
+
23
+ # tool-failure content heuristics (toolhealth.go IsFailure)
24
+ _GOROUTINE = re.compile(r"goroutine \d+")
25
+ _EXIT_CODE = re.compile(r"exit (status|code) [1-9]\d*")
26
+ _JS_AT = re.compile(r"^\s+at ", re.MULTILINE)
27
+ _BASH_FAIL_SUBSTR = ("command not found", "permission denied", "traceback (most recent call last)")
28
+ _GIVE_UP = (
29
+ "i'm unable to",
30
+ "i am unable to",
31
+ "i can't proceed",
32
+ "i cannot proceed",
33
+ "i don't have access",
34
+ )
35
+ _EDIT_PATH_KEYS = ("file_path", "path", "target_file", "filePath")
36
+
37
+
38
+ @dataclass
39
+ class SessionSignals:
40
+ message_count: int = 0
41
+ user_message_count: int = 0
42
+ total_output_tokens: int = 0
43
+ peak_context_tokens: int = 0
44
+ is_automated: bool = False
45
+ tool_failure_signal_count: int = 0
46
+ tool_retry_count: int = 0
47
+ edit_churn_count: int = 0
48
+ consecutive_failure_max: int = 0
49
+ final_failure_streak: int = 0
50
+ compaction_count: int = 0
51
+ mid_task_compaction_count: int = 0
52
+ compaction_boundaries: list[int] = field(default_factory=list) # message ordinals
53
+ context_pressure_max: float | None = None
54
+ has_tool_calls: bool = False
55
+ has_context_data: bool = False
56
+ ended_with_role: str = ""
57
+ outcome: str = "unknown"
58
+ outcome_confidence: str = "low"
59
+ health_score: int | None = None
60
+ health_grade: str = ""
61
+ penalties: dict[str, int] = field(default_factory=dict)
62
+
63
+
64
+ def _is_failure(category: str, result_content: str, is_error: bool) -> bool:
65
+ """A tool result is a failure if flagged is_error, or its content matches the
66
+ category-specific heuristics (Bash error shapes; Edit/Write 'FAILED')."""
67
+ if is_error:
68
+ return True
69
+ text = result_content or ""
70
+ low = text.lower()
71
+ if category in ("Bash",):
72
+ if any(s in low for s in _BASH_FAIL_SUBSTR):
73
+ return True
74
+ if _GOROUTINE.search(text) or _EXIT_CODE.search(low):
75
+ return True
76
+ if len(_JS_AT.findall(text)) >= 3:
77
+ return True
78
+ if category in ("Edit", "Write") and "FAILED" in text:
79
+ return True
80
+ return False
81
+
82
+
83
+ def _tool_failure_sequence(session: ParsedSession) -> list[bool]:
84
+ """Per-tool-result failure flags in message order. A result is matched to the
85
+ tool_use it answers (by category, via the preceding assistant turn's tool_uses)."""
86
+ # map tool_use_id -> category for results that carry an id; else fall back to
87
+ # the most recent assistant tool_use category.
88
+ id_to_cat: dict[str, str] = {}
89
+ last_cat = "Other"
90
+ seq: list[bool] = []
91
+ for m in session.messages:
92
+ for tu in m.tool_uses:
93
+ cat = normalize_tool_category(tu.name)
94
+ last_cat = cat
95
+ if tu.id:
96
+ id_to_cat[tu.id] = cat
97
+ for tr in m.tool_results:
98
+ cat = id_to_cat.get(tr.tool_use_id, last_cat)
99
+ seq.append(_is_failure(cat, tr.content, tr.is_error))
100
+ return seq
101
+
102
+
103
+ def _retry_count(session: ParsedSession) -> int:
104
+ """Runs of 3+ identical (tool_name, input_json) -> sum(runLen - 1)."""
105
+ calls = [(tu.name, tu.input_json) for m in session.messages for tu in m.tool_uses]
106
+ total = 0
107
+ i = 0
108
+ n = len(calls)
109
+ while i < n:
110
+ j = i + 1
111
+ while j < n and calls[j] == calls[i]:
112
+ j += 1
113
+ run = j - i
114
+ if run >= 3:
115
+ total += run - 1
116
+ i = j
117
+ return total
118
+
119
+
120
+ def _edit_churn_count(session: ParsedSession) -> int:
121
+ """Excess Edit/Write touches: a touch counts as churn when the same file already
122
+ had >=2 touches within the trailing 10-ordinal window."""
123
+ touches: list[tuple[int, str]] = [] # (ordinal, file)
124
+ for m in session.messages:
125
+ for tu in m.tool_uses:
126
+ if normalize_tool_category(tu.name) in ("Edit", "Write"):
127
+ path = _extract_path(tu.input_json)
128
+ if path:
129
+ touches.append((m.ordinal, path))
130
+ churn = 0
131
+ for idx, (ordinal, path) in enumerate(touches):
132
+ prior = [1 for o, p in touches[:idx] if p == path and (ordinal - o) <= 10]
133
+ if len(prior) >= 2:
134
+ churn += 1
135
+ return churn
136
+
137
+
138
+ def _extract_path(input_json: str) -> str:
139
+ if not input_json:
140
+ return ""
141
+ try:
142
+ obj = json.loads(input_json)
143
+ except Exception:
144
+ return ""
145
+ if isinstance(obj, dict):
146
+ for k in _EDIT_PATH_KEYS:
147
+ v = obj.get(k)
148
+ if isinstance(v, str) and v:
149
+ return v
150
+ return ""
151
+
152
+
153
+ def _compactions(session: ParsedSession) -> tuple[int, list[int]]:
154
+ """Count >30% context-token drops between consecutive turns that carry context.
155
+ Returns (count, list of boundary message-indexes)."""
156
+ count = 0
157
+ boundaries: list[int] = []
158
+ prev: int | None = None
159
+ for idx, m in enumerate(session.messages):
160
+ ctx = m.context_tokens
161
+ if ctx <= 0:
162
+ continue
163
+ if prev is not None and prev * 0.7 > ctx:
164
+ count += 1
165
+ boundaries.append(idx)
166
+ prev = ctx
167
+ return count, boundaries
168
+
169
+
170
+ def _mid_task_compactions(session: ParsedSession, boundaries: list[int]) -> int:
171
+ """A compaction is mid-task when the agent resumes similar work across it: >=2 tool
172
+ names shared between the 5 messages after the boundary and the 10 before it."""
173
+ msgs = session.messages
174
+ mid = 0
175
+ for b in boundaries:
176
+ before = {
177
+ normalize_tool_category(tu.name) for m in msgs[max(0, b - 10) : b] for tu in m.tool_uses
178
+ }
179
+ after = {normalize_tool_category(tu.name) for m in msgs[b : b + 5] for tu in m.tool_uses}
180
+ if len(before & after) >= 2:
181
+ mid += 1
182
+ return mid
183
+
184
+
185
+ def _classify_outcome(
186
+ *,
187
+ is_automated: bool,
188
+ message_count: int,
189
+ ended_with_role: str,
190
+ final_failure_streak: int,
191
+ last_text: str,
192
+ ) -> tuple[str, str]:
193
+ """Port of outcome.go: heuristic on conversation shape, not content quality."""
194
+ if is_automated:
195
+ return "unknown", "low"
196
+ if message_count == 2 and ended_with_role == "assistant":
197
+ return "completed", "medium"
198
+ if message_count < 3:
199
+ return "unknown", "low"
200
+ if ended_with_role == "user":
201
+ return "abandoned", ("high" if message_count >= 10 else "medium")
202
+ if final_failure_streak >= 3:
203
+ return "errored", "medium"
204
+ if ended_with_role == "assistant":
205
+ gave_up = any(p in last_text.lower() for p in _GIVE_UP)
206
+ return "completed", ("low" if gave_up else "medium")
207
+ return "unknown", "low"
208
+
209
+
210
+ def _cap(raw: int, cap: int) -> int:
211
+ return cap if raw > cap else raw
212
+
213
+
214
+ def compute_health_score(s: SessionSignals) -> tuple[int | None, str, dict[str, int]]:
215
+ """Verbatim port of agentsview signals/score.go ComputeHealthScore.
216
+
217
+ Start 100, floor 0. Unscored (None) iff outcome==unknown AND confidence==low AND no
218
+ tool/context data. Grade: >=90 A, >=75 B, >=60 C, >=40 D, else F.
219
+ """
220
+ basis = ["outcome"]
221
+ if s.has_tool_calls:
222
+ basis.append("tool_health")
223
+ if s.has_context_data:
224
+ basis.append("context_pressure")
225
+ can_score = (s.outcome != "unknown" or s.outcome_confidence != "low") or len(basis) > 1
226
+ if not can_score:
227
+ return None, "", {}
228
+
229
+ pen: dict[str, int] = {}
230
+ if s.outcome == "errored":
231
+ pen["outcome_errored"] = 30
232
+ elif s.outcome == "abandoned":
233
+ pen["outcome_abandoned"] = 15
234
+ if s.has_tool_calls:
235
+ if (p := _cap(s.tool_failure_signal_count * 3, 30)) > 0:
236
+ pen["tool_failure_signals"] = p
237
+ if (p := _cap(s.tool_retry_count * 5, 25)) > 0:
238
+ pen["tool_retries"] = p
239
+ if (p := _cap(s.edit_churn_count * 4, 20)) > 0:
240
+ pen["edit_churn"] = p
241
+ if s.consecutive_failure_max >= 3:
242
+ pen["consecutive_failures"] = 10
243
+ if s.has_context_data:
244
+ if s.compaction_count >= 2 and (p := _cap((s.compaction_count - 1) * 5, 15)) > 0:
245
+ pen["compactions"] = p
246
+ if s.mid_task_compaction_count > 0 and (p := _cap(s.mid_task_compaction_count * 8, 18)) > 0:
247
+ pen["mid_task_compactions"] = p
248
+ if s.context_pressure_max is not None and s.context_pressure_max > 0.9:
249
+ pen["context_pressure_high"] = 10
250
+
251
+ score = max(0, 100 - sum(pen.values()))
252
+ grade = (
253
+ "A"
254
+ if score >= 90
255
+ else "B" if score >= 75 else "C" if score >= 60 else "D" if score >= 40 else "F"
256
+ )
257
+ return score, grade, pen
258
+
259
+
260
+ def extract_signals(session: ParsedSession, *, context_window: int | None = None) -> SessionSignals:
261
+ """Compute all behavioral signals + the health score for one parsed session."""
262
+ msgs = session.messages
263
+ s = SessionSignals()
264
+ s.message_count = len(msgs)
265
+ s.user_message_count = sum(1 for m in msgs if m.role == "user")
266
+ s.total_output_tokens = sum(int(m.output_tokens or 0) for m in msgs)
267
+ s.peak_context_tokens = max((m.context_tokens for m in msgs), default=0)
268
+ s.has_tool_calls = any(m.tool_uses for m in msgs)
269
+ s.has_context_data = any(m.context_tokens > 0 for m in msgs)
270
+ s.ended_with_role = msgs[-1].role if msgs else ""
271
+
272
+ # automation heuristic: a one-shot autonomous run (>=6 turns, <=1 human turn).
273
+ s.is_automated = s.message_count >= 6 and s.user_message_count <= 1 and s.has_tool_calls
274
+
275
+ failures = _tool_failure_sequence(session)
276
+ s.tool_failure_signal_count = sum(1 for f in failures if f)
277
+ s.consecutive_failure_max = _max_run(failures)
278
+ s.final_failure_streak = _trailing_run(failures)
279
+ s.tool_retry_count = _retry_count(session)
280
+ s.edit_churn_count = _edit_churn_count(session)
281
+
282
+ s.compaction_count, boundaries = _compactions(session)
283
+ s.mid_task_compaction_count = _mid_task_compactions(session, boundaries)
284
+ s.compaction_boundaries = [msgs[i].ordinal for i in boundaries if 0 <= i < len(msgs)]
285
+ if context_window and s.peak_context_tokens > 0:
286
+ s.context_pressure_max = round(s.peak_context_tokens / float(context_window), 4)
287
+
288
+ last_text = ""
289
+ for m in reversed(msgs):
290
+ if m.role == "assistant" and m.content:
291
+ last_text = m.content
292
+ break
293
+ s.outcome, s.outcome_confidence = _classify_outcome(
294
+ is_automated=s.is_automated,
295
+ message_count=s.message_count,
296
+ ended_with_role=s.ended_with_role,
297
+ final_failure_streak=s.final_failure_streak,
298
+ last_text=last_text,
299
+ )
300
+ s.health_score, s.health_grade, s.penalties = compute_health_score(s)
301
+ return s
302
+
303
+
304
+ def _max_run(flags: list[bool]) -> int:
305
+ best = run = 0
306
+ for f in flags:
307
+ run = run + 1 if f else 0
308
+ best = max(best, run)
309
+ return best
310
+
311
+
312
+ def _trailing_run(flags: list[bool]) -> int:
313
+ run = 0
314
+ for f in reversed(flags):
315
+ if f:
316
+ run += 1
317
+ else:
318
+ break
319
+ return run
@@ -0,0 +1,71 @@
1
+ """Tool-name -> category normalization for tool-mix analytics.
2
+
3
+ Ported from agentsview ``internal/parser/taxonomy.go`` (MIT, see THIRD_PARTY_LICENSES.md):
4
+ a flat name->bucket map plus a ``"subagent" in name -> Task`` fallback, collapsing the
5
+ long tail of agent tool names into 9 stable buckets. Categorize at ingest so the
6
+ dashboard tool-mix is a plain ``GROUP BY category``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ CATEGORIES = ("Read", "Edit", "Write", "Bash", "Grep", "Glob", "Task", "Tool", "Other")
12
+
13
+ # explicit name -> category (lowercased keys). Covers Claude Code + Codex + Cursor names.
14
+ _MAP: dict[str, str] = {
15
+ # read
16
+ "read": "Read",
17
+ "read_file": "Read",
18
+ "notebookread": "Read",
19
+ "view": "Read",
20
+ # edit
21
+ "edit": "Edit",
22
+ "multiedit": "Edit",
23
+ "str_replace_based_edit_tool": "Edit",
24
+ "search_replace": "Edit",
25
+ "apply_patch": "Edit",
26
+ "notebookedit": "Edit",
27
+ # write
28
+ "write": "Write",
29
+ "create_file": "Write",
30
+ "write_file": "Write",
31
+ # bash / shell
32
+ "bash": "Bash",
33
+ "bashoutput": "Bash",
34
+ "killshell": "Bash",
35
+ "run_terminal_cmd": "Bash",
36
+ "shell": "Bash",
37
+ "local_shell": "Bash",
38
+ # grep / search
39
+ "grep": "Grep",
40
+ "codebase_search": "Grep",
41
+ "search": "Grep",
42
+ # glob
43
+ "glob": "Glob",
44
+ "list_dir": "Glob",
45
+ "file_search": "Glob",
46
+ # task / subagent
47
+ "task": "Task",
48
+ "agent": "Task",
49
+ # generic tool buckets (web, mcp, todo, etc. that are not core file/shell ops)
50
+ "webfetch": "Tool",
51
+ "websearch": "Tool",
52
+ "web_search": "Tool",
53
+ "fetch": "Tool",
54
+ "todowrite": "Tool",
55
+ "update_plan": "Tool",
56
+ }
57
+
58
+
59
+ def normalize_tool_category(name: str | None) -> str:
60
+ """Map a raw tool name to one of the 9 buckets. Unknown -> Other; any name
61
+ containing 'subagent' -> Task (mirrors agentsview's fallback)."""
62
+ if not name:
63
+ return "Other"
64
+ key = name.strip().lower()
65
+ if key in _MAP:
66
+ return _MAP[key]
67
+ if "subagent" in key:
68
+ return "Task"
69
+ if key.startswith("mcp__") or key.startswith("mcp_"):
70
+ return "Tool"
71
+ return "Other"