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

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.27";
881
+ var VERSION = "0.10.29";
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.27";
850
+ var VERSION = "0.10.29";
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.27",
3
+ "version": "0.10.29",
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",
@@ -121,6 +121,16 @@ STUDENT_API_KEY = os.environ.get("STUDENT_API_KEY", "")
121
121
  STUDENT_MODEL = os.environ.get("STUDENT_MODEL", "nuextract-2.0-4b-ft")
122
122
  STUDENT_TIMEOUT_SEC = float(os.environ.get("STUDENT_TIMEOUT_SEC", "60"))
123
123
  STUDENT_MAX_TOKENS = int(os.environ.get("STUDENT_MAX_TOKENS", "768"))
124
+ # Student-path concurrency — SEPARATE from CONCURRENT_LLM_CALLS. That knob is
125
+ # tuned for the teacher (N concurrent 27B *batch* calls of EVENTS_PER_LLM_CALL
126
+ # events each); the cascade reused it to bound student calls, throttling a 4B
127
+ # model — which batches 30–60 single-event calls at <5% KV cache on an L4 — to
128
+ # just 6/worker. Under a bursty backlog that left the student GPU-pinned but
129
+ # starved (Running:1, Waiting:0, KV:0.1%; 25k-backlog incident 2026-07-08). Each
130
+ # student call is ONE event (tiny KV), so it parallelises far higher. Tune per
131
+ # the box's KV-cache headroom. Do NOT raise CONCURRENT_LLM_CALLS to fix student
132
+ # throughput — that also raises teacher 27B concurrency (OOM risk).
133
+ STUDENT_CONCURRENT_CALLS = int(os.environ.get("STUDENT_CONCURRENT_CALLS", "48"))
124
134
  # Fraction of student-passing events ALSO sent to the teacher — NOT a quality
125
135
  # lever, this is the monitoring + active-learning sample (drift detection and
126
136
  # fresh teacher-gold on live traffic). Default conservative-ish 5%.
@@ -2649,7 +2659,10 @@ async def _process_cascade(
2649
2659
  gates: a pass writes the student's extraction (producer='student'); a fail/
2650
2660
  sample escalates to the teacher. Disjoint — the student writes XOR the event
2651
2661
  is escalated, so there is nothing to supersede."""
2652
- sem = asyncio.Semaphore(CONCURRENT_LLM_CALLS)
2662
+ # STUDENT_CONCURRENT_CALLS, not CONCURRENT_LLM_CALLS: the student is a cheap
2663
+ # single-event 4B call that batches far higher than the teacher's 27B
2664
+ # batch-calls (see the config note). Reusing the teacher knob starved it.
2665
+ sem = asyncio.Semaphore(STUDENT_CONCURRENT_CALLS)
2653
2666
 
2654
2667
  async def _student(item):
2655
2668
  async with sem:
@@ -2792,70 +2805,91 @@ def _apply_extraction(
2792
2805
  det_ents, det_rels = structured_graph_elements(arena, event.get("attributes"))
2793
2806
 
2794
2807
  try:
2795
- name_to_id = upsert_entities(
2796
- conn, arena, event_id, participant_set, disclosure, ents + det_ents,
2797
- event_time, event.get("attributes"),
2798
- )
2799
- n_facts = upsert_facts(
2800
- conn, arena, event_id, participant_set, disclosure, facts, name_to_id,
2801
- event_time, due_at, src,
2802
- )
2803
- n_rels = upsert_relationships(
2804
- conn, arena, event_id, participant_set, disclosure, rels + det_rels, name_to_id,
2805
- event_time,
2806
- )
2807
- mark_done(conn, queue_id)
2808
- log.info(
2809
- f"completed queue_id={queue_id} event_id={event_id} producer={producer} "
2810
- f"entities={len(name_to_id)} facts={n_facts} relationships={n_rels}"
2811
- f" det_edges={len(det_rels)}"
2812
- + (f" llm_ms={llm_ms:.0f}" if not stub_mode and llm_ms else "")
2813
- )
2814
- # Trace logging — best-effort. ONLY the teacher produces training gold;
2815
- # logging student output would train the student on itself.
2816
- if DISTILL_TRACE_ENABLED and not stub_mode and producer == "teacher":
2817
- try:
2818
- _insert_trace(
2819
- conn,
2820
- event_id=event_id,
2821
- user_prompt=build_event_block(local_idx, event),
2822
- raw_response=result.get("raw_slice", ""),
2823
- llm_chunk_ms=llm_ms,
2824
- )
2825
- except Exception as trace_exc:
2826
- log.warning(
2827
- f"trace insert failed queue_id={queue_id} "
2828
- f"event_id={event_id}: {trace_exc}"
2829
- )
2830
- # Cascade ledger — best-effort, records which producer wrote this
2831
- # event's rows (only when the cascade is on).
2832
- if CASCADE_ENABLED:
2833
- try:
2834
- _record_distillation(
2835
- conn,
2836
- event_id=event_id,
2837
- producer=producer,
2838
- llm_model=LLM_MODEL if producer == "teacher" else STUDENT_MODEL,
2839
- escalated=escalated,
2840
- escalate_reason=escalate_reason,
2841
- )
2842
- except Exception as ledger_exc:
2843
- log.warning(
2844
- f"ledger insert failed queue_id={queue_id} "
2845
- f"event_id={event_id}: {ledger_exc}"
2846
- )
2847
- return True
2808
+ # ONE transaction per event. The connection is autocommit (amain), so
2809
+ # without this each statement fsync-commits on its own — ~20 commits per
2810
+ # event (entity resolution SELECTs/locks/upserts + facts + rels +
2811
+ # mark_done), which is the round-trip bottleneck under a backlog: the
2812
+ # graph PG sits idle between the worker's serial commits (profiled
2813
+ # 2026-07-08 39 conns idle, 1 active). Wrapping commits once per event.
2814
+ #
2815
+ # NB: this also makes upsert_entities' `pg_advisory_xact_lock` actually
2816
+ # span the event's resolution. Under autocommit each lock released after
2817
+ # its own single-statement txn effectively a NO-OP — so the
2818
+ # per-surface-form anti-fragmentation lock never protected the
2819
+ # SELECT-overlap→INSERT window. Now it holds for the whole event: correct
2820
+ # (the lock finally does its job), but it serialises concurrent workers
2821
+ # that touch the same surface form. Load-test before trusting on prod.
2822
+ with conn.transaction():
2823
+ name_to_id = upsert_entities(
2824
+ conn, arena, event_id, participant_set, disclosure, ents + det_ents,
2825
+ event_time, event.get("attributes"),
2826
+ )
2827
+ n_facts = upsert_facts(
2828
+ conn, arena, event_id, participant_set, disclosure, facts, name_to_id,
2829
+ event_time, due_at, src,
2830
+ )
2831
+ n_rels = upsert_relationships(
2832
+ conn, arena, event_id, participant_set, disclosure, rels + det_rels, name_to_id,
2833
+ event_time,
2834
+ )
2835
+ mark_done(conn, queue_id)
2848
2836
  except Exception as exc:
2849
2837
  err = f"{type(exc).__name__}: {exc}"
2850
2838
  log.warning(
2851
2839
  f"db upsert failed queue_id={queue_id} attempts={attempts}: {err}"
2852
2840
  )
2841
+ # The transaction rolled back; the claim bookkeeping runs on the
2842
+ # now-autocommit connection so it persists independently.
2853
2843
  if _attempts_exhausted(attempts):
2854
2844
  mark_failed(conn, queue_id, err)
2855
2845
  else:
2856
2846
  release_claim(conn, queue_id, err)
2857
2847
  return False
2858
2848
 
2849
+ log.info(
2850
+ f"completed queue_id={queue_id} event_id={event_id} producer={producer} "
2851
+ f"entities={len(name_to_id)} facts={n_facts} relationships={n_rels}"
2852
+ f" det_edges={len(det_rels)}"
2853
+ + (f" llm_ms={llm_ms:.0f}" if not stub_mode and llm_ms else "")
2854
+ )
2855
+ # Best-effort side-writes, AFTER the graph commit and OUTSIDE the transaction:
2856
+ # a failure here must not roll back the committed graph rows, and an error
2857
+ # inside the txn would poison it (a swallowed error leaves the txn aborted).
2858
+ # Trace: ONLY the teacher produces training gold; logging student output would
2859
+ # train the student on itself.
2860
+ if DISTILL_TRACE_ENABLED and not stub_mode and producer == "teacher":
2861
+ try:
2862
+ _insert_trace(
2863
+ conn,
2864
+ event_id=event_id,
2865
+ user_prompt=build_event_block(local_idx, event),
2866
+ raw_response=result.get("raw_slice", ""),
2867
+ llm_chunk_ms=llm_ms,
2868
+ )
2869
+ except Exception as trace_exc:
2870
+ log.warning(
2871
+ f"trace insert failed queue_id={queue_id} "
2872
+ f"event_id={event_id}: {trace_exc}"
2873
+ )
2874
+ # Cascade ledger — best-effort, records which producer wrote this
2875
+ # event's rows (only when the cascade is on).
2876
+ if CASCADE_ENABLED:
2877
+ try:
2878
+ _record_distillation(
2879
+ conn,
2880
+ event_id=event_id,
2881
+ producer=producer,
2882
+ llm_model=LLM_MODEL if producer == "teacher" else STUDENT_MODEL,
2883
+ escalated=escalated,
2884
+ escalate_reason=escalate_reason,
2885
+ )
2886
+ except Exception as ledger_exc:
2887
+ log.warning(
2888
+ f"ledger insert failed queue_id={queue_id} "
2889
+ f"event_id={event_id}: {ledger_exc}"
2890
+ )
2891
+ return True
2892
+
2859
2893
 
2860
2894
  async def amain():
2861
2895
  log.info(