@pentatonic-ai/ai-agent-sdk 0.10.36 → 0.10.37

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/dist/index.cjs CHANGED
@@ -878,7 +878,7 @@ function fireAndForgetEmit(clientConfig, sessionOpts, messages, result, model) {
878
878
  }
879
879
 
880
880
  // src/telemetry.js
881
- var VERSION = "0.10.36";
881
+ var VERSION = "0.10.37";
882
882
  var TELEMETRY_URL = "https://sdk-telemetry.philip-134.workers.dev";
883
883
  function machineId() {
884
884
  const raw = typeof process !== "undefined" ? `${process.env?.USER || process.env?.USERNAME || "u"}:${process.platform || "x"}:${process.arch || "x"}` : "browser";
package/dist/index.js CHANGED
@@ -847,7 +847,7 @@ function fireAndForgetEmit(clientConfig, sessionOpts, messages, result, model) {
847
847
  }
848
848
 
849
849
  // src/telemetry.js
850
- var VERSION = "0.10.36";
850
+ var VERSION = "0.10.37";
851
851
  var TELEMETRY_URL = "https://sdk-telemetry.philip-134.workers.dev";
852
852
  function machineId() {
853
853
  const raw = typeof process !== "undefined" ? `${process.env?.USER || process.env?.USERNAME || "u"}:${process.platform || "x"}:${process.arch || "x"}` : "browser";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pentatonic-ai/ai-agent-sdk",
3
- "version": "0.10.36",
3
+ "version": "0.10.37",
4
4
  "description": "TES SDK — LLM observability and lifecycle tracking via Pentatonic Thing Event System. Track token usage, tool calls, and conversations. Manage things through event-sourced lifecycle stages with AI enrichment and vector search.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -37,6 +37,7 @@ import uuid
37
37
  from contextlib import asynccontextmanager
38
38
  from datetime import datetime
39
39
  from typing import Any
40
+ from urllib.parse import urlsplit
40
41
 
41
42
  import httpx
42
43
  import numpy as np
@@ -59,6 +60,14 @@ NV_EMBED_PROVIDER = os.environ.get("NV_EMBED_PROVIDER", "openai") # 'openai' |
59
60
  EMBED_DIM = int(os.environ.get("EMBED_DIM", "4096"))
60
61
  # Window (minutes) over which /health/deep computes the distillation drain rate.
61
62
  HEALTH_DRAIN_WINDOW_MIN = int(os.environ.get("HEALTH_DRAIN_WINDOW_MIN", "5"))
63
+ # GPU/compute-tier endpoints probed by the /health/deep stack-online gauge. The
64
+ # teacher is probed THROUGH its load-balancer (a stable service name that is up
65
+ # even when the fleet is scaled to zero) — see health_deep for why the probe
66
+ # treats a no-upstream 502 as offline. STUDENT_ENDPOINT is the vLLM student box;
67
+ # when unset (not wired into compat's env) the student tier is simply omitted
68
+ # from the gauge rather than counted as down.
69
+ STUDENT_ENDPOINT = os.environ.get("STUDENT_ENDPOINT", "")
70
+ TEACHER_HEALTH_URL = os.environ.get("TEACHER_HEALTH_URL", "http://teacher-lb:8500/v1/models")
62
71
 
63
72
  COLLECTION_NAME = "evidence"
64
73
 
@@ -651,10 +660,36 @@ async def health():
651
660
  return {"status": "healthy", "service": "pme2-compat", "version": "0.1.0"}
652
661
 
653
662
 
663
+ def _models_url(endpoint: str) -> str:
664
+ """Rewrite any endpoint URL to its OpenAI-style /v1/models path — a cheap
665
+ GET the vLLM student/teacher boxes and the embed gateways all answer. Keeps
666
+ only scheme+host+port so it works whether the source URL was /v1/embeddings,
667
+ /v1/chat/completions, etc."""
668
+ p = urlsplit(endpoint)
669
+ return f"{p.scheme}://{p.netloc}/v1/models"
670
+
671
+
672
+ async def _tier_online(url: str, *, healthy_below: int = 600) -> bool:
673
+ """Liveness probe for one compute tier. Online iff the endpoint answers with
674
+ an HTTP status < healthy_below within the timeout. Connection refused /
675
+ timeout / DNS failure → offline.
676
+
677
+ healthy_below defaults to 600 (any HTTP answer = the box is powered on and
678
+ serving). Pass 500 for a tier fronted by an always-up load balancer (the
679
+ teacher LB): with the fleet scaled to zero the LB itself still answers, but
680
+ with a 502 (no healthy upstream) — which we must read as OFFLINE, not "the
681
+ LB replied, so it's up"."""
682
+ try:
683
+ r = await _http.get(url, timeout=3.0)
684
+ return r.status_code < healthy_below
685
+ except Exception:
686
+ return False
687
+
688
+
654
689
  @app.get("/health/deep")
655
690
  async def health_deep():
656
- """Round-trips all three stores + the embed gateway. Slow; do not
657
- use as a docker healthcheck."""
691
+ """Round-trips all three stores + the embed gateway, then computes a
692
+ stack-online gauge. Slow; do not use as a docker healthcheck."""
658
693
  result = {"compat": "ok", "stores": {}}
659
694
 
660
695
  # org-model
@@ -710,6 +745,44 @@ async def health_deep():
710
745
  except Exception as e:
711
746
  result["stores"]["embed_gateway"] = {"status": "error", "error": str(e)}
712
747
 
748
+ # Stack-online gauge: what fraction of the compute stack is powered on RIGHT
749
+ # NOW. Deliberately counts every tier — including the scale-to-zero teacher
750
+ # fleet — so a parked teacher legitimately lowers the %. This is a "what's
751
+ # running" dial, not a pure health signal: the distillation drain/backlog
752
+ # block above says whether a parked teacher is actually a problem. Tiers the
753
+ # store round-trips already assessed are reused (no re-probe); the GPU tiers
754
+ # are probed concurrently for liveness.
755
+ components: dict[str, bool] = {
756
+ "compat": True, # we are the one answering
757
+ "org_model": result["stores"].get("org_model", {}).get("status") == "ok",
758
+ "vector_index": result["stores"].get("vector_index", {}).get("status") == "ok",
759
+ "embed_interactive": result["stores"].get("embed_gateway", {}).get("status") == "ok",
760
+ }
761
+ # GPU/compute tiers not covered by the store probes. (name, url, healthy_below)
762
+ gpu_probes = [("teacher", TEACHER_HEALTH_URL, 500)]
763
+ # Bulk embedder only counts as a distinct tier when it's a separate box.
764
+ if NV_EMBED_URL_BULK and NV_EMBED_URL_BULK != NV_EMBED_URL:
765
+ gpu_probes.append(("embed_bulk", _models_url(NV_EMBED_URL_BULK), 600))
766
+ if STUDENT_ENDPOINT:
767
+ gpu_probes.append(("student", _models_url(STUDENT_ENDPOINT), 600))
768
+ try:
769
+ probe_results = await asyncio.gather(
770
+ *(_tier_online(url, healthy_below=hb) for _, url, hb in gpu_probes)
771
+ )
772
+ for (name, _, _), up in zip(gpu_probes, probe_results):
773
+ components[name] = up
774
+ except Exception as e: # pragma: no cover — probing must never 500 the endpoint
775
+ result["stack_probe_error"] = str(e)
776
+
777
+ online = sum(1 for up in components.values() if up)
778
+ total = len(components)
779
+ result["stack"] = {
780
+ "online_pct": round(100 * online / total, 1) if total else 0.0,
781
+ "online": online,
782
+ "total": total,
783
+ "components": {k: ("up" if up else "down") for k, up in components.items()},
784
+ }
785
+
713
786
  return result
714
787
 
715
788