@pentatonic-ai/ai-agent-sdk 0.10.29 → 0.10.30

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.29";
881
+ var VERSION = "0.10.30";
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.29";
850
+ var VERSION = "0.10.30";
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.29",
3
+ "version": "0.10.30",
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",
@@ -131,6 +131,28 @@ STUDENT_MAX_TOKENS = int(os.environ.get("STUDENT_MAX_TOKENS", "768"))
131
131
  # the box's KV-cache headroom. Do NOT raise CONCURRENT_LLM_CALLS to fix student
132
132
  # throughput — that also raises teacher 27B concurrency (OOM risk).
133
133
  STUDENT_CONCURRENT_CALLS = int(os.environ.get("STUDENT_CONCURRENT_CALLS", "48"))
134
+ # How many claimed batches the worker processes CONCURRENTLY. The prior loop
135
+ # claimed a batch, awaited it end-to-end (student gather → straggler tail →
136
+ # teacher tail), and only then claimed the next — a hard barrier. Wall-clock
137
+ # profiling of the 25k-backlog incident (2026-07-08) showed the student L40S
138
+ # burst to Running:50 for ~10s then sit at Running:1 for ~3min draining a
139
+ # straggler tail while the (LB'd) teacher fleet idled — the GPUs were starved by
140
+ # the loop, not by capacity. Overlapping N batches, all sharing ONE student pool
141
+ # and ONE teacher pool (the shared semaphores below), keeps those pools
142
+ # continuously full: batch B's fresh student calls take the slots batch A's
143
+ # stragglers free. Total LLM concurrency is still bounded by the shared sems
144
+ # (STUDENT_CONCURRENT_CALLS / CONCURRENT_LLM_CALLS) REGARDLESS of this value —
145
+ # this only controls how many independent claim→apply pipelines feed them (each
146
+ # on its own PG connection; applies within a batch stay sequential, so one conn
147
+ # per batch is never touched concurrently). Raise to feed the pools harder; the
148
+ # sems, not this, cap GPU load.
149
+ MAX_INFLIGHT_BATCHES = int(os.environ.get("MAX_INFLIGHT_BATCHES", "4"))
150
+ # Process-global shared concurrency pools, created once inside amain's event
151
+ # loop (NOT per-batch — per-batch pools are what caused the burst-then-idle).
152
+ # All in-flight batches acquire from these, so the student/teacher GPUs see one
153
+ # continuous stream of calls instead of per-batch bursts. None until amain runs.
154
+ _STUDENT_SEM: "asyncio.Semaphore | None" = None
155
+ _TEACHER_SEM: "asyncio.Semaphore | None" = None
134
156
  # Fraction of student-passing events ALSO sent to the teacher — NOT a quality
135
157
  # lever, this is the monitoring + active-learning sample (drift detection and
136
158
  # fresh teacher-gold on live traffic). Default conservative-ish 5%.
@@ -2662,7 +2684,11 @@ async def _process_cascade(
2662
2684
  # STUDENT_CONCURRENT_CALLS, not CONCURRENT_LLM_CALLS: the student is a cheap
2663
2685
  # single-event 4B call that batches far higher than the teacher's 27B
2664
2686
  # batch-calls (see the config note). Reusing the teacher knob starved it.
2665
- sem = asyncio.Semaphore(STUDENT_CONCURRENT_CALLS)
2687
+ # SHARED across all in-flight batches (see MAX_INFLIGHT_BATCHES) so the
2688
+ # student pool stays continuously full — a per-batch semaphore burst-then-
2689
+ # idled the GPU. amain initialises it before any batch runs.
2690
+ sem = _STUDENT_SEM
2691
+ assert sem is not None, "amain must initialise _STUDENT_SEM before processing"
2666
2692
 
2667
2693
  async def _student(item):
2668
2694
  async with sem:
@@ -2719,8 +2745,12 @@ async def _run_teacher(
2719
2745
  chunks.append((chunk_items, chunk_events))
2720
2746
 
2721
2747
  # Bound parallelism with a semaphore — gather only waits for slots,
2722
- # not for everything to be ready at once.
2723
- sem = asyncio.Semaphore(CONCURRENT_LLM_CALLS)
2748
+ # not for everything to be ready at once. SHARED across all in-flight
2749
+ # batches so the teacher fleet (behind the LB) sees a continuous stream and
2750
+ # its total concurrency stays capped at CONCURRENT_LLM_CALLS regardless of
2751
+ # how many batches are escalating at once. amain initialises it.
2752
+ sem = _TEACHER_SEM
2753
+ assert sem is not None, "amain must initialise _TEACHER_SEM before processing"
2724
2754
 
2725
2755
  async def run_one(chunk_items, chunk_events):
2726
2756
  async with sem:
@@ -2917,20 +2947,65 @@ async def amain():
2917
2947
  if stub_mode:
2918
2948
  log.warning("LLM_ENDPOINT not set — running in stub mode (no extraction).")
2919
2949
 
2950
+ # Shared concurrency pools — created HERE, inside the running event loop, so
2951
+ # ONE student pool and ONE teacher pool are shared by every in-flight batch
2952
+ # (see MAX_INFLIGHT_BATCHES). Per-batch pools burst the GPU to Running:~50
2953
+ # then idled it at Running:1 through each batch's straggler/teacher tail; one
2954
+ # shared pool stays continuously full across overlapping batches.
2955
+ global _STUDENT_SEM, _TEACHER_SEM
2956
+ _STUDENT_SEM = asyncio.Semaphore(STUDENT_CONCURRENT_CALLS)
2957
+ _TEACHER_SEM = asyncio.Semaphore(CONCURRENT_LLM_CALLS)
2958
+ log.info(
2959
+ f"pipelined loop: max_inflight_batches={MAX_INFLIGHT_BATCHES}, "
2960
+ f"student_pool={STUDENT_CONCURRENT_CALLS}, teacher_pool={CONCURRENT_LLM_CALLS}"
2961
+ )
2962
+
2920
2963
  # Single async client across the loop. The httpx default limits
2921
- # (max_connections=100, max_keepalive=20) easily cover BATCH_SIZE
2964
+ # (max_connections=100, max_keepalive=20) easily cover the shared-pool
2922
2965
  # concurrent inflight LLM calls.
2923
2966
  async with httpx.AsyncClient(timeout=LLM_TIMEOUT_SEC) as http:
2967
+
2968
+ async def _run_one_batch() -> int:
2969
+ """Claim + fully process one batch on a DEDICATED connection.
2970
+ Returns the number of items claimed (0 = queue empty). Applies within
2971
+ a batch happen sequentially (after each gather), so a batch's
2972
+ connection is never touched concurrently — one conn per batch is
2973
+ safe. The `with` closes the conn on both success and exception; a
2974
+ crash leaves that batch's items claimed until CLAIM_TTL_SEC expires
2975
+ and they are reclaimed (unchanged from the prior loop)."""
2976
+ with psycopg.connect(PG_DSN, autocommit=True) as conn:
2977
+ items = claim_next_batch(conn)
2978
+ if not items:
2979
+ return 0
2980
+ log.info(f"claimed {len(items)} item(s)")
2981
+ await process_batch(http, conn, items, stub_mode)
2982
+ return len(items)
2983
+
2984
+ # Keep up to MAX_INFLIGHT_BATCHES independent claim→apply pipelines
2985
+ # running at once. Their student/teacher calls all draw from the shared
2986
+ # pools above, so while one batch drains its slow tail the others keep
2987
+ # the GPUs fed. Total GPU concurrency is capped by the sems, NOT by this
2988
+ # count.
2989
+ inflight: set[asyncio.Task] = set()
2924
2990
  while True:
2925
2991
  try:
2926
- with psycopg.connect(PG_DSN, autocommit=True) as conn:
2927
- items = claim_next_batch(conn)
2928
- if not items:
2929
- await asyncio.sleep(POLL_INTERVAL_SEC)
2930
- continue
2931
-
2932
- log.info(f"claimed {len(items)} item(s)")
2933
- await process_batch(http, conn, items, stub_mode)
2992
+ while len(inflight) < MAX_INFLIGHT_BATCHES:
2993
+ inflight.add(asyncio.create_task(_run_one_batch()))
2994
+ done, inflight = await asyncio.wait(
2995
+ inflight, return_when=asyncio.FIRST_COMPLETED
2996
+ )
2997
+ claimed_any = False
2998
+ for t in done:
2999
+ try:
3000
+ if t.result() > 0:
3001
+ claimed_any = True
3002
+ except Exception as exc:
3003
+ log.error(f"batch task error: {exc}")
3004
+ # All finished batches were empty claims ⇒ queue drained; back
3005
+ # off before re-claiming so idle workers don't hot-spin. Under a
3006
+ # real backlog every claim returns items and this never sleeps.
3007
+ if not claimed_any:
3008
+ await asyncio.sleep(POLL_INTERVAL_SEC)
2934
3009
  except Exception as exc:
2935
3010
  log.error(f"worker loop error: {exc}")
2936
3011
  await asyncio.sleep(POLL_INTERVAL_SEC * 2)