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.
- {netra_zen-1.0.1.dist-info → netra_zen-1.0.3.dist-info}/METADATA +776 -776
- {netra_zen-1.0.1.dist-info → netra_zen-1.0.3.dist-info}/RECORD +7 -7
- {netra_zen-1.0.1.dist-info → netra_zen-1.0.3.dist-info}/licenses/LICENSE.md +1 -1
- zen_orchestrator.py +134 -41
- {netra_zen-1.0.1.dist-info → netra_zen-1.0.3.dist-info}/WHEEL +0 -0
- {netra_zen-1.0.1.dist-info → netra_zen-1.0.3.dist-info}/entry_points.txt +0 -0
- {netra_zen-1.0.1.dist-info → netra_zen-1.0.3.dist-info}/top_level.txt +0 -0
@@ -1,15 +1,15 @@
|
|
1
|
-
zen_orchestrator.py,sha256=
|
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.
|
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.
|
12
|
-
netra_zen-1.0.
|
13
|
-
netra_zen-1.0.
|
14
|
-
netra_zen-1.0.
|
15
|
-
netra_zen-1.0.
|
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.
|
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.
|
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
|
-
#
|
261
|
-
|
262
|
-
|
263
|
-
|
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
|
-
|
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
|
-
|
295
|
-
|
296
|
-
|
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
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
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]
|
765
|
-
|
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]
|
770
|
-
|
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()
|
File without changes
|
File without changes
|
File without changes
|