@ictechgy/context-guard 0.5.1 → 0.7.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.
@@ -91,6 +91,37 @@ ROUTE_STRUCTURED_TASK_KINDS = {
91
91
  "batch_eval",
92
92
  "eval",
93
93
  }
94
+ ADVISORY_WORKLOAD_SCHEMA_VERSION = "contextguard.advisory-workload.v1"
95
+ ADVISORY_DECISION_SCHEMA_VERSION = "contextguard.advisory-decision.v1"
96
+ ADVISORY_MAX_INTEGER = 1_000_000_000
97
+ ADVISORY_TOP_LEVEL_KEYS = {"invocation", "limits", "schema_version", "signals", "vendor"}
98
+ ADVISORY_INVOCATION_KEYS = {
99
+ "explicit_wrappers_available",
100
+ "hooks_available",
101
+ "host_tool_surface_equal_to_control",
102
+ "rules_loaded",
103
+ "safe_mode",
104
+ "skills_loaded",
105
+ }
106
+ ADVISORY_SIGNAL_INTEGER_KEYS = {
107
+ "candidate_context_bytes",
108
+ "estimated_local_overhead_ms",
109
+ "graph_candidate_bytes",
110
+ "graph_candidate_count",
111
+ "graph_replacement_bytes",
112
+ "largest_file_bytes",
113
+ "log_bytes",
114
+ "selected_file_count",
115
+ "task_prompt_bytes",
116
+ }
117
+ ADVISORY_SIGNAL_KEYS = ADVISORY_SIGNAL_INTEGER_KEYS | {"repo_map_cached"}
118
+ ADVISORY_LIMIT_KEYS = {
119
+ "inline_log_bytes",
120
+ "max_local_overhead_ms",
121
+ "minimum_gross_context_savings_bytes",
122
+ "pack_bytes",
123
+ "symbol_slice_bytes",
124
+ }
94
125
  ALLOWED_FIRST_COMPONENT_SYMLINKS = {
95
126
  "tmp": Path("/private/tmp"),
96
127
  "var": Path("/private/var"),
@@ -272,6 +303,33 @@ def load_json_input(path: str, *, max_bytes: int = DEFAULT_MAX_BYTES) -> tuple[A
272
303
  return data, truncated
273
304
 
274
305
 
306
+ def reject_duplicate_json_pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
307
+ result: dict[str, Any] = {}
308
+ for key, value in pairs:
309
+ if key in result:
310
+ fail("duplicate JSON key is not allowed")
311
+ result[key] = value
312
+ return result
313
+
314
+
315
+ def load_advisory_json_input(path: str, *, max_bytes: int) -> Any:
316
+ text, truncated = read_text_path(path, max_bytes=max_bytes)
317
+ if truncated:
318
+ fail("JSON input exceeded max bytes")
319
+ try:
320
+ return json.loads(
321
+ text,
322
+ parse_constant=reject_json_constant,
323
+ object_pairs_hook=reject_duplicate_json_pairs,
324
+ )
325
+ except json.JSONDecodeError as exc:
326
+ fail(f"invalid JSON input at line {exc.lineno}: {exc.msg}")
327
+ except ValueError as exc:
328
+ if isinstance(exc, CostGuardError):
329
+ raise
330
+ fail(f"invalid JSON input: {exc}")
331
+
332
+
275
333
  def secret_count_in_text(text: str) -> int:
276
334
  return sum(1 for _ in SECRET_RE.finditer(text))
277
335
 
@@ -2532,6 +2590,80 @@ def route_recommendations(
2532
2590
  return recs
2533
2591
 
2534
2592
 
2593
+ def advisory_plain_object(value: Any, label: str, expected_keys: set[str]) -> dict[str, Any]:
2594
+ if type(value) is not dict:
2595
+ fail(f"{label} must be a plain JSON object")
2596
+ if set(value) != expected_keys:
2597
+ fail(f"{label} has missing or unknown fields")
2598
+ return value
2599
+
2600
+
2601
+ def advisory_bool(value: Any, label: str) -> bool:
2602
+ if type(value) is not bool:
2603
+ fail(f"{label} must be boolean")
2604
+ return value
2605
+
2606
+
2607
+ def advisory_integer(value: Any, label: str, *, positive: bool = False) -> int:
2608
+ if type(value) is not int:
2609
+ fail(f"{label} must be an integer")
2610
+ lower_bound = 1 if positive else 0
2611
+ if value < lower_bound or value > ADVISORY_MAX_INTEGER:
2612
+ relation = "> 0" if positive else ">= 0"
2613
+ fail(f"{label} must be {relation} and bounded")
2614
+ return value
2615
+
2616
+
2617
+ def advisory_result(
2618
+ *,
2619
+ activation_status: str,
2620
+ decision: str,
2621
+ reason: str,
2622
+ measurement_eligible: bool,
2623
+ capabilities: dict[str, bool],
2624
+ selected_features: dict[str, bool],
2625
+ actions: list[dict[str, Any]],
2626
+ control_bytes: int,
2627
+ treatment_bytes: int,
2628
+ minimum_savings: int,
2629
+ local_overhead_ms: int,
2630
+ max_local_overhead_ms: int,
2631
+ graph_replacement_delta_bytes: int,
2632
+ ) -> dict[str, Any]:
2633
+ gross_saved = max(0, control_bytes - treatment_bytes)
2634
+ return {
2635
+ "schema_version": ADVISORY_DECISION_SCHEMA_VERSION,
2636
+ "mode": "router_advisory",
2637
+ "activation_status": activation_status,
2638
+ "decision": decision,
2639
+ "reason": reason,
2640
+ "provider_context": "",
2641
+ "provider_context_bytes": 0,
2642
+ "persistent_writes_allowed": False,
2643
+ "receipts_enabled": False,
2644
+ "measurement_eligible": measurement_eligible,
2645
+ "capabilities": capabilities,
2646
+ "selected_features": selected_features,
2647
+ "actions": actions,
2648
+ "accounting": {
2649
+ "control_candidate_context_bytes": control_bytes,
2650
+ "estimated_treatment_context_bytes": treatment_bytes,
2651
+ "estimated_gross_context_saved_bytes": gross_saved,
2652
+ "minimum_gross_context_savings_bytes": minimum_savings,
2653
+ "estimated_local_overhead_ms": local_overhead_ms,
2654
+ "max_local_overhead_ms": max_local_overhead_ms,
2655
+ "graph_replacement_delta_bytes": graph_replacement_delta_bytes,
2656
+ },
2657
+ "claim_boundary": {
2658
+ "provider_token_or_cost_savings_claim_allowed": False,
2659
+ "requires_paired_provider_measurement": True,
2660
+ "requires_non_inferior_quality": True,
2661
+ "task_content_accepted": False,
2662
+ "persistent_context_allowed": False,
2663
+ },
2664
+ }
2665
+
2666
+
2535
2667
  def route_advisor_command(args: argparse.Namespace) -> int:
2536
2668
  workload_raw, _truncated = load_json_input(args.workload, max_bytes=args.max_bytes)
2537
2669
  workload = require_json_object(workload_raw.get("workload") if isinstance(workload_raw, dict) and isinstance(workload_raw.get("workload"), dict) else workload_raw, "workload")
@@ -2599,6 +2731,182 @@ def route_advisor_command(args: argparse.Namespace) -> int:
2599
2731
  return 0
2600
2732
 
2601
2733
 
2734
+ def advisory_decision(raw: Any) -> dict[str, Any]:
2735
+ workload = advisory_plain_object(raw, "advisory workload", ADVISORY_TOP_LEVEL_KEYS)
2736
+ if workload["schema_version"] != ADVISORY_WORKLOAD_SCHEMA_VERSION:
2737
+ fail("advisory workload schema version is unsupported")
2738
+ vendor = workload["vendor"]
2739
+ if type(vendor) is not str or vendor not in {"claude", "codex"}:
2740
+ fail("advisory vendor must be claude or codex")
2741
+ invocation = advisory_plain_object(
2742
+ workload["invocation"], "advisory invocation", ADVISORY_INVOCATION_KEYS
2743
+ )
2744
+ invocation_values = {
2745
+ key: advisory_bool(invocation[key], f"advisory invocation.{key}")
2746
+ for key in sorted(ADVISORY_INVOCATION_KEYS)
2747
+ }
2748
+ if invocation_values["safe_mode"] and vendor != "claude":
2749
+ fail("advisory safe_mode is only valid for claude")
2750
+ signals = advisory_plain_object(
2751
+ workload["signals"], "advisory signals", ADVISORY_SIGNAL_KEYS
2752
+ )
2753
+ signal_values = {
2754
+ key: advisory_integer(signals[key], f"advisory signals.{key}")
2755
+ for key in sorted(ADVISORY_SIGNAL_INTEGER_KEYS)
2756
+ }
2757
+ signal_values["repo_map_cached"] = advisory_bool(
2758
+ signals["repo_map_cached"], "advisory signals.repo_map_cached"
2759
+ )
2760
+ limits = advisory_plain_object(
2761
+ workload["limits"], "advisory limits", ADVISORY_LIMIT_KEYS
2762
+ )
2763
+ limit_values = {
2764
+ key: advisory_integer(
2765
+ limits[key],
2766
+ f"advisory limits.{key}",
2767
+ positive=key in {"inline_log_bytes", "pack_bytes", "symbol_slice_bytes"},
2768
+ )
2769
+ for key in sorted(ADVISORY_LIMIT_KEYS)
2770
+ }
2771
+
2772
+ control_bytes = signal_values["candidate_context_bytes"]
2773
+ if signal_values["largest_file_bytes"] > control_bytes:
2774
+ fail("largest_file_bytes exceeds candidate_context_bytes")
2775
+ if signal_values["log_bytes"] > control_bytes:
2776
+ fail("log_bytes exceeds candidate_context_bytes")
2777
+ if signal_values["graph_replacement_bytes"] > control_bytes:
2778
+ fail("graph_replacement_bytes exceeds candidate_context_bytes")
2779
+ graph_count = signal_values["graph_candidate_count"]
2780
+ graph_candidate_bytes = signal_values["graph_candidate_bytes"]
2781
+ graph_replacement_bytes = signal_values["graph_replacement_bytes"]
2782
+ if graph_count == 0 and (graph_candidate_bytes or graph_replacement_bytes):
2783
+ fail("graph bytes require at least one graph candidate")
2784
+ if graph_count > 0 and not (graph_candidate_bytes and graph_replacement_bytes):
2785
+ fail("graph candidates require nonzero candidate and replacement bytes")
2786
+
2787
+ hooks_effective = bool(
2788
+ invocation_values["hooks_available"]
2789
+ and not (vendor == "claude" and invocation_values["safe_mode"])
2790
+ )
2791
+ persistent_context_absent = not (
2792
+ invocation_values["rules_loaded"] or invocation_values["skills_loaded"]
2793
+ )
2794
+ capabilities = {
2795
+ "hooks_effective": hooks_effective,
2796
+ "explicit_wrappers_available": invocation_values["explicit_wrappers_available"],
2797
+ "persistent_context_absent": persistent_context_absent,
2798
+ "host_tool_surface_equal_to_control": invocation_values[
2799
+ "host_tool_surface_equal_to_control"
2800
+ ],
2801
+ }
2802
+ no_features = {
2803
+ "adaptive": False,
2804
+ "graph": False,
2805
+ "symbol": False,
2806
+ "trim_output": False,
2807
+ }
2808
+ result_common = {
2809
+ "capabilities": capabilities,
2810
+ "control_bytes": control_bytes,
2811
+ "minimum_savings": limit_values["minimum_gross_context_savings_bytes"],
2812
+ "local_overhead_ms": signal_values["estimated_local_overhead_ms"],
2813
+ "max_local_overhead_ms": limit_values["max_local_overhead_ms"],
2814
+ }
2815
+ if not persistent_context_absent:
2816
+ return advisory_result(
2817
+ activation_status="bypass", decision="bypass",
2818
+ reason="persistent_context_loaded", measurement_eligible=False,
2819
+ selected_features=no_features, actions=[], treatment_bytes=control_bytes,
2820
+ graph_replacement_delta_bytes=0, **result_common,
2821
+ )
2822
+ if not invocation_values["host_tool_surface_equal_to_control"]:
2823
+ return advisory_result(
2824
+ activation_status="bypass", decision="bypass",
2825
+ reason="host_tool_surface_mismatch", measurement_eligible=False,
2826
+ selected_features=no_features, actions=[], treatment_bytes=control_bytes,
2827
+ graph_replacement_delta_bytes=0, **result_common,
2828
+ )
2829
+
2830
+ candidates: list[dict[str, Any]] = []
2831
+ log_bytes = signal_values["log_bytes"]
2832
+ inline_log_bytes = limit_values["inline_log_bytes"]
2833
+ if log_bytes > inline_log_bytes:
2834
+ candidates.append({
2835
+ "decision": "trim_output", "reason": "log_savings",
2836
+ "treatment_bytes": control_bytes - (log_bytes - inline_log_bytes),
2837
+ "features": {**no_features, "trim_output": True},
2838
+ "actions": [{"kind": "trim_output", "max_inline_bytes": inline_log_bytes}],
2839
+ "graph_replacement_delta_bytes": 0,
2840
+ })
2841
+ largest_file_bytes = signal_values["largest_file_bytes"]
2842
+ symbol_slice_bytes = limit_values["symbol_slice_bytes"]
2843
+ if largest_file_bytes > symbol_slice_bytes:
2844
+ candidates.append({
2845
+ "decision": "symbol_slice", "reason": "symbol_slice_savings",
2846
+ "treatment_bytes": control_bytes - (largest_file_bytes - symbol_slice_bytes),
2847
+ "features": {**no_features, "symbol": True},
2848
+ "actions": [{"kind": "symbol_slice", "max_bytes": symbol_slice_bytes}],
2849
+ "graph_replacement_delta_bytes": 0,
2850
+ })
2851
+ if signal_values["selected_file_count"] >= 3 and control_bytes > limit_values["pack_bytes"]:
2852
+ graph_net_saved = 0
2853
+ graph_selected = False
2854
+ if (
2855
+ signal_values["repo_map_cached"] and graph_count > 0
2856
+ and graph_replacement_bytes > graph_candidate_bytes
2857
+ ):
2858
+ graph_net_saved = graph_replacement_bytes - graph_candidate_bytes
2859
+ graph_selected = True
2860
+ pack_bytes = limit_values["pack_bytes"]
2861
+ candidates.append({
2862
+ "decision": "adaptive_pack", "reason": "context_pack_savings",
2863
+ "treatment_bytes": pack_bytes,
2864
+ "features": {**no_features, "adaptive": True, "graph": graph_selected},
2865
+ "actions": [{
2866
+ "kind": "context_pack", "budget_bytes": limit_values["pack_bytes"],
2867
+ "adaptive": True, "symbol": False, "graph": graph_selected,
2868
+ }],
2869
+ "graph_replacement_delta_bytes": graph_net_saved,
2870
+ })
2871
+ candidate = min(candidates, key=lambda item: item["treatment_bytes"]) if candidates else None
2872
+ if candidate is None or (
2873
+ control_bytes - int(candidate["treatment_bytes"])
2874
+ < limit_values["minimum_gross_context_savings_bytes"]
2875
+ ):
2876
+ return advisory_result(
2877
+ activation_status="bypass", decision="bypass", reason="below_break_even",
2878
+ measurement_eligible=True, selected_features=no_features, actions=[],
2879
+ treatment_bytes=control_bytes, graph_replacement_delta_bytes=0, **result_common,
2880
+ )
2881
+ if signal_values["estimated_local_overhead_ms"] > limit_values["max_local_overhead_ms"]:
2882
+ return advisory_result(
2883
+ activation_status="bypass", decision="bypass",
2884
+ reason="local_overhead_budget_exceeded", measurement_eligible=True,
2885
+ selected_features=no_features, actions=[], treatment_bytes=control_bytes,
2886
+ graph_replacement_delta_bytes=0, **result_common,
2887
+ )
2888
+ if not invocation_values["explicit_wrappers_available"]:
2889
+ return advisory_result(
2890
+ activation_status="inactive", decision="bypass",
2891
+ reason="explicit_wrappers_unavailable", measurement_eligible=False,
2892
+ selected_features=no_features, actions=[], treatment_bytes=control_bytes,
2893
+ graph_replacement_delta_bytes=0, **result_common,
2894
+ )
2895
+ return advisory_result(
2896
+ activation_status="active", decision=str(candidate["decision"]),
2897
+ reason=str(candidate["reason"]), measurement_eligible=True,
2898
+ selected_features=dict(candidate["features"]), actions=list(candidate["actions"]),
2899
+ treatment_bytes=int(candidate["treatment_bytes"]),
2900
+ graph_replacement_delta_bytes=int(candidate["graph_replacement_delta_bytes"]), **result_common,
2901
+ )
2902
+
2903
+
2904
+ def advisory_command(args: argparse.Namespace) -> int:
2905
+ workload = load_advisory_json_input(args.workload, max_bytes=args.max_bytes)
2906
+ emit(advisory_decision(workload), json_mode=args.json)
2907
+ return 0
2908
+
2909
+
2602
2910
  def usage_int(data: dict[str, Any], key: str) -> int:
2603
2911
  value = data.get(key, 0)
2604
2912
  try:
@@ -3044,6 +3352,12 @@ def emit(data: dict[str, Any], *, json_mode: bool) -> None:
3044
3352
  f"candidates={routing.get('candidate_count', 0)} conditional={routing.get('conditional_count', 0)} "
3045
3353
  f"total_with_shift=${total.get('total_cost_with_shift_usd', 0)}"
3046
3354
  )
3355
+ elif mode == "router_advisory":
3356
+ accounting = data.get("accounting", {}) if isinstance(data.get("accounting"), dict) else {}
3357
+ print(
3358
+ f"{TOOL_NAME}: advisory {data.get('decision', 'bypass')} "
3359
+ f"gross_context_bytes={accounting.get('estimated_gross_context_saved_bytes', 0)}"
3360
+ )
3047
3361
  else:
3048
3362
  summary = data.get("summary", {}) if isinstance(data.get("summary"), dict) else {}
3049
3363
  print(f"{TOOL_NAME}: ledger entries={summary.get('entries', 0)}")
@@ -3113,6 +3427,19 @@ def build_parser() -> argparse.ArgumentParser:
3113
3427
  add_common_cost_args(route)
3114
3428
  route.set_defaults(func=route_advisor_command)
3115
3429
 
3430
+ advisory = sub.add_parser(
3431
+ "advisory",
3432
+ help="plan zero-persistent-context WeightClass/router advisory actions",
3433
+ description=(
3434
+ "select a zero-provider-instruction bypass or explicit local wrapper from "
3435
+ "closed numeric capability signals; never reads task text or project files"
3436
+ ),
3437
+ )
3438
+ advisory.add_argument("--workload", default="-", help="closed advisory workload JSON path, or '-' for stdin")
3439
+ advisory.add_argument("--max-bytes", type=int, default=DEFAULT_MAX_BYTES, help=f"maximum advisory JSON bytes (default: {DEFAULT_MAX_BYTES})")
3440
+ advisory.add_argument("--json", action="store_true", help="emit machine-readable JSON")
3441
+ advisory.set_defaults(func=advisory_command)
3442
+
3116
3443
  return parser
3117
3444
 
3118
3445