netra-zen 1.0.1__py3-none-any.whl → 1.0.3__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.
@@ -1,15 +1,15 @@
1
- zen_orchestrator.py,sha256=QApF_yEo7C8CUq_p8GGYJT2gPwMCLIMLSNIdFZFuxcI,144907
1
+ zen_orchestrator.py,sha256=eUiA8T08QYMfx5kM84LUloDUxscCUzv_k3O0WN3nFpo,148941
2
2
  agent_interface/__init__.py,sha256=OsbOKzElHsxhVgak87oOx_u46QNgKmz-Reis-plAMwk,525
3
3
  agent_interface/base_agent.py,sha256=GNskG9VaZgno7X24lQTpFdxUoQE0yJHLh0UPFJvOPn4,11098
4
- netra_zen-1.0.1.dist-info/licenses/LICENSE.md,sha256=PZrP0UDn58i4LjV4zijIQTnsQPvWm4zq9Fet9i7qgwI,80
4
+ netra_zen-1.0.3.dist-info/licenses/LICENSE.md,sha256=t6LtOzAE2hgIIv5WbaN0wOcU3QCnGtAkMGNclHrKTOs,79
5
5
  token_budget/__init__.py,sha256=_2tmi72DGNtbYcZ-rGIxVKMytdkHFjzJaWz8bDhYACc,33
6
6
  token_budget/budget_manager.py,sha256=VRWxKcGDtgJfIRh-ztYQ4-wuhBvddVJJnyoGfxCBlv0,9567
7
7
  token_budget/models.py,sha256=14xFTk2-R1Ql0F9WLDof7vADrKC_5Fj7EE7UmZdoG00,2384
8
8
  token_budget/visualization.py,sha256=SaNnZ15edHXtjDCA5Yfu7w3AztCZRYsYCPGBqzapupY,719
9
9
  token_transparency/__init__.py,sha256=go86Rg4_VYAPLw3myVpLe1s1PbMu1llDTw1wfomP1lA,453
10
10
  token_transparency/claude_pricing_engine.py,sha256=9zWQJS3HJEs_lljil-xT1cUvG-Jf3ykNAninJFyfNSM,12630
11
- netra_zen-1.0.1.dist-info/METADATA,sha256=ixxjb8v_zQKUIIoMXOPhfSXdrawdMMPWsOxAUBu_xCE,30220
12
- netra_zen-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
- netra_zen-1.0.1.dist-info/entry_points.txt,sha256=oDehCnPGZezG0m9ZWspxjHLHyQ3eERX87eojR4ljaRo,45
14
- netra_zen-1.0.1.dist-info/top_level.txt,sha256=dHz-hgh_dfiiOUrPf8wW80fA31rfEYDFmA6fpu67Yjk,65
15
- netra_zen-1.0.1.dist-info/RECORD,,
11
+ netra_zen-1.0.3.dist-info/METADATA,sha256=WQb-6CiZG9JqJrkW8fuGc5UmejBT4-ZimVnNVH6ehLw,29442
12
+ netra_zen-1.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
+ netra_zen-1.0.3.dist-info/entry_points.txt,sha256=oDehCnPGZezG0m9ZWspxjHLHyQ3eERX87eojR4ljaRo,45
14
+ netra_zen-1.0.3.dist-info/top_level.txt,sha256=dHz-hgh_dfiiOUrPf8wW80fA31rfEYDFmA6fpu67Yjk,65
15
+ netra_zen-1.0.3.dist-info/RECORD,,
@@ -1 +1 @@
1
- © Netra Inc. All rights reserved. Use is subject to Netra's Terms of Service.
1
+ © Netra Inc. All rights reserved. Use is subject to Netra's Terms of Service.
zen_orchestrator.py CHANGED
@@ -60,6 +60,11 @@ import re
60
60
  from uuid import uuid4, UUID
61
61
  from enum import Enum
62
62
 
63
+ try:
64
+ from zen.telemetry import telemetry_manager
65
+ except Exception: # pragma: no cover - telemetry optional
66
+ telemetry_manager = None
67
+
63
68
  # Add token budget imports with proper path handling
64
69
  sys.path.insert(0, str(Path(__file__).parent))
65
70
  try:
@@ -79,6 +84,7 @@ except ImportError as e:
79
84
  ClaudePricingEngine = None
80
85
  TokenUsageData = None
81
86
 
87
+
82
88
  # Setup logging
83
89
  logging.basicConfig(
84
90
  level=logging.INFO,
@@ -107,7 +113,8 @@ def determine_log_level(args) -> LogLevel:
107
113
  @dataclass
108
114
  class InstanceConfig:
109
115
  """Configuration for a Claude Code instance"""
110
- command: str
116
+ command: Optional[str] = None # For slash commands like /help
117
+ prompt: Optional[str] = None # For raw prompts like "What are the available commands?"
111
118
  name: Optional[str] = None
112
119
  description: Optional[str] = None
113
120
  allowed_tools: List[str] = None
@@ -121,10 +128,35 @@ class InstanceConfig:
121
128
 
122
129
  def __post_init__(self):
123
130
  """Set defaults after initialization"""
131
+ # Validate that either command or prompt is provided
132
+ if not self.command and not self.prompt:
133
+ raise ValueError("Either 'command' or 'prompt' must be provided")
134
+
135
+ # If both are provided, prioritize command over prompt
136
+ if self.command and self.prompt:
137
+ # Use command, but log that prompt is being ignored
138
+ pass # This is valid - command takes precedence
139
+
140
+ # If only prompt is provided, treat it as the command for execution
141
+ elif self.prompt and not self.command:
142
+ self.command = self.prompt
143
+
144
+ # Set default name
124
145
  if self.name is None:
125
- self.name = self.command
146
+ if self.command and self.command.startswith('/'):
147
+ self.name = self.command
148
+ else:
149
+ # For prompts, create a shorter name
150
+ display_text = self.prompt or self.command
151
+ self.name = display_text[:30] + "..." if len(display_text) > 30 else display_text
152
+
153
+ # Set default description
126
154
  if self.description is None:
127
- self.description = f"Execute {self.command}"
155
+ if self.command and self.command.startswith('/'):
156
+ self.description = f"Execute {self.command}"
157
+ else:
158
+ self.description = f"Execute prompt: {(self.prompt or self.command)[:50]}..."
159
+
128
160
  # Set permission mode if not explicitly set
129
161
  if self.permission_mode is None:
130
162
  # Default to bypassPermissions for all platforms to avoid approval prompts
@@ -163,6 +195,7 @@ class InstanceStatus:
163
195
  tool_details: Dict[str, int] = None # Tool name -> usage count
164
196
  tool_tokens: Dict[str, int] = None # Tool name -> token usage
165
197
  tool_id_mapping: Dict[str, str] = field(default_factory=dict) # tool_use_id -> tool name mapping
198
+ telemetry_recorded: bool = False
166
199
 
167
200
  def __post_init__(self):
168
201
  """Initialize fields that need special handling"""
@@ -202,6 +235,7 @@ class ClaudeInstanceOrchestrator:
202
235
  self.quiet = quiet
203
236
  self.log_level = log_level
204
237
  self.batch_id = str(uuid4()) # Generate batch ID for this orchestration run
238
+
205
239
  self.optimizer = None
206
240
 
207
241
  # Initialize budget manager if any budget settings are provided
@@ -257,10 +291,17 @@ class ClaudeInstanceOrchestrator:
257
291
 
258
292
  def add_instance(self, config: InstanceConfig):
259
293
  """Add a new instance configuration"""
260
- # Validate slash command exists
261
- if not self.validate_command(config.command):
262
- logger.warning(f"Command '{config.command}' not found in available commands")
263
- logger.info(f"Available commands: {', '.join(self.discover_available_commands())}")
294
+ # Determine if this is a slash command or raw prompt
295
+ is_slash_command = config.command and config.command.startswith('/')
296
+ is_raw_prompt = config.prompt and not is_slash_command
297
+
298
+ if is_slash_command:
299
+ # Validate slash command exists
300
+ if not self.validate_command(config.command):
301
+ logger.warning(f"Command '{config.command}' not found in available commands")
302
+ logger.info(f"Available commands: {', '.join(self.discover_available_commands())}")
303
+ elif is_raw_prompt:
304
+ logger.info(f"Using raw prompt: {config.prompt[:50]}{'...' if len(config.prompt) > 50 else ''}")
264
305
 
265
306
  self.instances[config.name] = config
266
307
  self.statuses[config.name] = InstanceStatus(name=config.name)
@@ -289,36 +330,36 @@ class ClaudeInstanceOrchestrator:
289
330
  command_string = "; ".join(full_command)
290
331
 
291
332
  # Find the claude executable with Mac-specific paths
292
- claude_cmd = shutil.which("claude")
333
+ # IMPORTANT: Use direct paths to avoid shell functions that may have database dependencies
334
+ possible_paths = [
335
+ "/opt/homebrew/bin/claude", # Mac Homebrew ARM - prefer direct path
336
+ "/usr/local/bin/claude", # Mac Homebrew Intel
337
+ "~/.local/bin/claude", # User local install
338
+ "/usr/bin/claude", # System install
339
+ "claude.cmd", # Windows
340
+ "claude.exe", # Windows
341
+ ]
342
+
343
+ claude_cmd = None
344
+ for path in possible_paths:
345
+ # Expand user path if needed
346
+ expanded_path = Path(path).expanduser()
347
+ if expanded_path.exists() and expanded_path.is_file():
348
+ claude_cmd = str(expanded_path)
349
+ logger.info(f"Found Claude executable at: {claude_cmd}")
350
+ break
351
+
352
+ # Only use shutil.which as fallback if no direct path found
293
353
  if not claude_cmd:
294
- # Try common paths on different platforms
295
- possible_paths = [
296
- "claude.cmd", # Windows
297
- "claude.exe", # Windows
298
- "/opt/homebrew/bin/claude", # Mac Homebrew ARM
299
- "/usr/local/bin/claude", # Mac Homebrew Intel
300
- "~/.local/bin/claude", # User local install
301
- "/usr/bin/claude", # System install
302
- "claude" # Final fallback
303
- ]
304
-
305
- for path in possible_paths:
306
- # Expand user path if needed
307
- expanded_path = Path(path).expanduser()
308
- if expanded_path.exists():
309
- claude_cmd = str(expanded_path)
310
- logger.info(f"Found Claude executable at: {claude_cmd}")
311
- break
312
- elif shutil.which(path):
313
- claude_cmd = path
314
- logger.info(f"Found Claude executable via which: {claude_cmd}")
315
- break
354
+ claude_cmd = shutil.which("claude")
355
+ if claude_cmd:
356
+ logger.info(f"Found Claude executable via which: {claude_cmd}")
316
357
 
317
- if not claude_cmd or claude_cmd == "claude":
318
- logger.warning("Claude command not found in PATH or common locations")
319
- logger.warning("Please ensure Claude Code is installed and in your PATH")
320
- logger.warning("Install with: npm install -g @anthropic/claude-code")
321
- claude_cmd = "claude" # Fallback
358
+ if not claude_cmd:
359
+ logger.warning("Claude command not found in PATH or common locations")
360
+ logger.warning("Please ensure Claude Code is installed and in your PATH")
361
+ logger.warning("Install with: npm install -g @anthropic/claude-code")
362
+ claude_cmd = "/opt/homebrew/bin/claude" # Default fallback to most likely location
322
363
 
323
364
  # New approach: slash commands can be included directly in prompt
324
365
  cmd = [
@@ -445,6 +486,11 @@ class ClaudeInstanceOrchestrator:
445
486
  logger.error(f"🚫 BLOCK MODE: {message}")
446
487
  status.status = "failed"
447
488
  status.error = f"Blocked by budget limit - {reason}"
489
+ timestamp = time.time()
490
+ if status.start_time is None:
491
+ status.start_time = timestamp
492
+ status.end_time = timestamp
493
+ self._emit_instance_telemetry(name, config, status)
448
494
  return False
449
495
  else: # warn mode
450
496
  logger.warning(f"⚠️ WARN MODE: {message}")
@@ -522,18 +568,22 @@ class ClaudeInstanceOrchestrator:
522
568
  if returncode == 0:
523
569
  status.status = "completed"
524
570
  logger.info(f"Instance {name} completed successfully")
571
+ self._emit_instance_telemetry(name, config, status)
525
572
  return True
526
573
  else:
527
574
  status.status = "failed"
528
575
  logger.error(f"Instance {name} failed with return code {returncode}")
529
576
  if status.error:
530
577
  logger.error(f"Error output: {status.error}")
578
+ self._emit_instance_telemetry(name, config, status)
531
579
  return False
532
580
 
533
581
  except Exception as e:
534
582
  status.status = "failed"
535
583
  status.error = str(e)
536
584
  logger.error(f"Exception running instance {name}: {e}")
585
+ status.end_time = status.end_time or time.time()
586
+ self._emit_instance_telemetry(name, config, status)
537
587
  return False
538
588
 
539
589
  async def _save_metrics_to_database(self, name: str, config: InstanceConfig, status: InstanceStatus):
@@ -586,6 +636,38 @@ class ClaudeInstanceOrchestrator:
586
636
 
587
637
  return input_cost + output_cost + cache_read_cost + cache_creation_cost + tool_cost
588
638
 
639
+ def _emit_instance_telemetry(self, name: str, config: InstanceConfig, status: InstanceStatus) -> None:
640
+ """Send telemetry span with token usage and cost metadata."""
641
+
642
+ if telemetry_manager is None or not hasattr(telemetry_manager, "is_enabled"):
643
+ return
644
+
645
+ if getattr(status, "telemetry_recorded", False):
646
+ return
647
+
648
+ if not telemetry_manager.is_enabled():
649
+ return
650
+
651
+ cost_usd: Optional[float]
652
+ try:
653
+ cost_usd = self._calculate_cost(status)
654
+ except Exception as exc: # pragma: no cover - defensive guard
655
+ logger.debug(f"Cost calculation failed for telemetry span ({name}): {exc}")
656
+ cost_usd = None
657
+
658
+ try:
659
+ telemetry_manager.record_instance_span(
660
+ batch_id=self.batch_id,
661
+ instance_name=name,
662
+ status=status,
663
+ config=config,
664
+ cost_usd=cost_usd,
665
+ workspace=str(self.workspace_dir),
666
+ )
667
+ status.telemetry_recorded = True
668
+ except Exception as exc: # pragma: no cover - Network/export errors
669
+ logger.debug(f"Telemetry emission failed for {name}: {exc}")
670
+
589
671
  async def _stream_output(self, name: str, process):
590
672
  """Stream output in real-time for stream-json format (DEPRECATED - use _stream_output_parallel)"""
591
673
  status = self.statuses[name]
@@ -761,13 +843,19 @@ class ClaudeInstanceOrchestrator:
761
843
  for name, result in zip(self.instances.keys(), results):
762
844
  if isinstance(result, asyncio.TimeoutError):
763
845
  logger.error(f"Instance {name} timed out after {timeout}s")
764
- self.statuses[name].status = "failed"
765
- self.statuses[name].error = f"Timeout after {timeout}s"
846
+ status = self.statuses[name]
847
+ status.status = "failed"
848
+ status.error = f"Timeout after {timeout}s"
849
+ status.end_time = time.time()
850
+ self._emit_instance_telemetry(name, self.instances[name], status)
766
851
  final_results[name] = False
767
852
  elif isinstance(result, Exception):
768
853
  logger.error(f"Instance {name} failed with exception: {result}")
769
- self.statuses[name].status = "failed"
770
- self.statuses[name].error = str(result)
854
+ status = self.statuses[name]
855
+ status.status = "failed"
856
+ status.error = str(result)
857
+ status.end_time = time.time()
858
+ self._emit_instance_telemetry(name, self.instances[name], status)
771
859
  final_results[name] = False
772
860
  else:
773
861
  final_results[name] = result
@@ -2322,7 +2410,6 @@ async def main():
2322
2410
  parser.add_argument("--start-at", type=str, default=None,
2323
2411
  help="Schedule orchestration to start at specific time. Examples: '2h' (2 hours from now), '30m' (30 minutes), '14:30' (2:30 PM today), '1am' (1 AM today/tomorrow)")
2324
2412
 
2325
-
2326
2413
  # Direct command options
2327
2414
  parser.add_argument("--instance-name", type=str, help="Instance name for direct command execution")
2328
2415
  parser.add_argument("--instance-description", type=str, help="Instance description for direct command execution")
@@ -2404,6 +2491,7 @@ async def main():
2404
2491
 
2405
2492
  logger.info(f"Using workspace: {workspace}")
2406
2493
 
2494
+
2407
2495
  # Load instance configurations with direct command precedence
2408
2496
  direct_instance = create_direct_instance(args, workspace)
2409
2497
 
@@ -2857,6 +2945,11 @@ async def main():
2857
2945
  print("🌐 Learn more: https://netrasystems.ai/")
2858
2946
  print("="*80)
2859
2947
 
2948
+
2949
+ # Flush telemetry before exit
2950
+ if telemetry_manager is not None and hasattr(telemetry_manager, "shutdown"):
2951
+ telemetry_manager.shutdown()
2952
+
2860
2953
  # Exit with appropriate code
2861
2954
  sys.exit(0 if summary['failed'] == 0 else 1)
2862
2955
 
@@ -2865,4 +2958,4 @@ def run():
2865
2958
  asyncio.run(main())
2866
2959
 
2867
2960
  if __name__ == "__main__":
2868
- run()
2961
+ run()