@pentatonic-ai/ai-agent-sdk 0.10.37 → 0.10.38
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.
|
|
881
|
+
var VERSION = "0.10.38";
|
|
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.
|
|
850
|
+
var VERSION = "0.10.38";
|
|
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.
|
|
3
|
+
"version": "0.10.38",
|
|
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",
|
|
@@ -2646,7 +2646,7 @@ def claim_next_batch(conn: psycopg.Connection) -> list[dict[str, Any]]:
|
|
|
2646
2646
|
) AND attempts < %s
|
|
2647
2647
|
FOR UPDATE SKIP LOCKED
|
|
2648
2648
|
)
|
|
2649
|
-
RETURNING id, event_id, attempts
|
|
2649
|
+
RETURNING id, event_id, attempts, transient_attempts
|
|
2650
2650
|
""",
|
|
2651
2651
|
(WORKER_ID, CLAIM_TTL_SEC, MAX_ATTEMPTS, MAX_ATTEMPTS,
|
|
2652
2652
|
BATCH_SIZE, BATCH_SIZE, MAX_ATTEMPTS),
|
|
@@ -2713,6 +2713,68 @@ def _attempts_exhausted(attempts: int) -> bool:
|
|
|
2713
2713
|
return attempts + 1 >= MAX_ATTEMPTS
|
|
2714
2714
|
|
|
2715
2715
|
|
|
2716
|
+
# ── transient vs genuine failures ─────────────────────────────────────
|
|
2717
|
+
# Transient = infra hiccups that resolve on their own: the teacher timing out
|
|
2718
|
+
# under load (ReadTimeout) or the teacher-lb returning 502/503/504 while the
|
|
2719
|
+
# fleet is cold-starting from zero. These must NOT burn the genuine `attempts`
|
|
2720
|
+
# budget (3) — 3 quick ReadTimeouts used to march a row to `failed`, and failed
|
|
2721
|
+
# rows are never requeued, so the extraction was silently lost. Instead they get
|
|
2722
|
+
# their own larger budget (MAX_TRANSIENT_ATTEMPTS) on the `transient_attempts`
|
|
2723
|
+
# counter (migration 013), leaving `attempts` untouched so the row stays
|
|
2724
|
+
# claimable (`attempts < MAX_ATTEMPTS` in claim_next_batch). ReadTimeouts are
|
|
2725
|
+
# self-spaced by LLM_TIMEOUT_SEC and the fair-share round-robin spreads re-claims
|
|
2726
|
+
# across the backlog, so this does not hot-loop; MAX_TRANSIENT_ATTEMPTS bounds a
|
|
2727
|
+
# genuine poison chunk (one that times out even on a healthy multi-box fleet).
|
|
2728
|
+
MAX_TRANSIENT_ATTEMPTS = int(os.environ.get("MAX_TRANSIENT_ATTEMPTS", "12"))
|
|
2729
|
+
_TRANSIENT_MARKERS = (
|
|
2730
|
+
"ReadTimeout",
|
|
2731
|
+
"TimeoutException",
|
|
2732
|
+
"ConnectTimeout",
|
|
2733
|
+
"ConnectError",
|
|
2734
|
+
"PoolTimeout",
|
|
2735
|
+
"502",
|
|
2736
|
+
"503",
|
|
2737
|
+
"504",
|
|
2738
|
+
"Bad Gateway",
|
|
2739
|
+
"Gateway Time-out",
|
|
2740
|
+
"Service Unavailable",
|
|
2741
|
+
"Temporarily Unavailable",
|
|
2742
|
+
)
|
|
2743
|
+
|
|
2744
|
+
|
|
2745
|
+
def _is_transient(err: str) -> bool:
|
|
2746
|
+
"""True if `err` looks like a recoverable infra hiccup (teacher timeout /
|
|
2747
|
+
LB gateway error) rather than a genuine extraction/DB failure."""
|
|
2748
|
+
return any(m in err for m in _TRANSIENT_MARKERS)
|
|
2749
|
+
|
|
2750
|
+
|
|
2751
|
+
def requeue_transient(
|
|
2752
|
+
conn: psycopg.Connection, queue_id: int, transient_attempts: int, error: str
|
|
2753
|
+
) -> None:
|
|
2754
|
+
"""Recoverable INFRA failure. Release the claim back to pending WITHOUT
|
|
2755
|
+
touching `attempts` (preserves the genuine-failure budget), counting instead
|
|
2756
|
+
against `transient_attempts`. Once that budget is exhausted the row IS marked
|
|
2757
|
+
failed — a chunk that times out this many times is genuinely unprocessable,
|
|
2758
|
+
not a passing hiccup."""
|
|
2759
|
+
if transient_attempts + 1 >= MAX_TRANSIENT_ATTEMPTS:
|
|
2760
|
+
mark_failed(conn, queue_id, f"transient-exhausted: {error}")
|
|
2761
|
+
return
|
|
2762
|
+
with conn.cursor() as cur:
|
|
2763
|
+
cur.execute(
|
|
2764
|
+
"""
|
|
2765
|
+
UPDATE distillation_queue SET
|
|
2766
|
+
status = 'pending',
|
|
2767
|
+
claimed_by = NULL,
|
|
2768
|
+
claimed_at = NULL,
|
|
2769
|
+
claim_expires_at = NULL,
|
|
2770
|
+
transient_attempts = transient_attempts + 1,
|
|
2771
|
+
last_error = %s
|
|
2772
|
+
WHERE id = %s
|
|
2773
|
+
""",
|
|
2774
|
+
(error[:1024], queue_id),
|
|
2775
|
+
)
|
|
2776
|
+
|
|
2777
|
+
|
|
2716
2778
|
# --------------------------------------------------------------------
|
|
2717
2779
|
# Main loop
|
|
2718
2780
|
# --------------------------------------------------------------------
|
|
@@ -2924,7 +2986,11 @@ async def _run_teacher(
|
|
|
2924
2986
|
log.warning(
|
|
2925
2987
|
f"extraction failed queue_id={queue_id} attempts={attempts}: {err}"
|
|
2926
2988
|
)
|
|
2927
|
-
if
|
|
2989
|
+
if _is_transient(err):
|
|
2990
|
+
requeue_transient(
|
|
2991
|
+
conn, queue_id, item.get("transient_attempts", 0), err
|
|
2992
|
+
)
|
|
2993
|
+
elif _attempts_exhausted(attempts):
|
|
2928
2994
|
mark_failed(conn, queue_id, err)
|
|
2929
2995
|
else:
|
|
2930
2996
|
release_claim(conn, queue_id, err)
|
|
@@ -3022,8 +3088,13 @@ def _apply_extraction(
|
|
|
3022
3088
|
f"db upsert failed queue_id={queue_id} attempts={attempts}: {err}"
|
|
3023
3089
|
)
|
|
3024
3090
|
# The transaction rolled back; the claim bookkeeping runs on the
|
|
3025
|
-
# now-autocommit connection so it persists independently.
|
|
3026
|
-
|
|
3091
|
+
# now-autocommit connection so it persists independently. A transient DB
|
|
3092
|
+
# blip (connection/pool timeout) requeues without burning the genuine
|
|
3093
|
+
# budget; real errors (UniqueViolation, schema mismatch) don't match
|
|
3094
|
+
# _is_transient and take the normal attempts path.
|
|
3095
|
+
if _is_transient(err):
|
|
3096
|
+
requeue_transient(conn, queue_id, item.get("transient_attempts", 0), err)
|
|
3097
|
+
elif _attempts_exhausted(attempts):
|
|
3027
3098
|
mark_failed(conn, queue_id, err)
|
|
3028
3099
|
else:
|
|
3029
3100
|
release_claim(conn, queue_id, err)
|
package/packages/memory-engine-v2/org-model/migrations/013_distillation_transient_attempts.sql
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-- 013 — separate the TRANSIENT retry budget from the genuine-failure budget.
|
|
2
|
+
--
|
|
3
|
+
-- The distiller shares one `attempts` counter for every failure, capped at
|
|
4
|
+
-- MAX_ATTEMPTS (3) and enforced by the claim query's `attempts < MAX_ATTEMPTS`
|
|
5
|
+
-- filter. That conflates two very different failures:
|
|
6
|
+
-- * genuine — parse error, NULL-subject fact, schema mismatch: real, should
|
|
7
|
+
-- give up after a few tries.
|
|
8
|
+
-- * transient — teacher ReadTimeout under load, teacher-lb 502/503/504 during
|
|
9
|
+
-- a scale-from-zero window: infra hiccups that resolve on their
|
|
10
|
+
-- own once the fleet catches up / a box finishes booting.
|
|
11
|
+
--
|
|
12
|
+
-- Because they shared a counter, a transient hiccup burned the genuine budget:
|
|
13
|
+
-- 3 quick ReadTimeouts → `failed`, and since failed rows are never requeued the
|
|
14
|
+
-- extraction was silently LOST (measured 154 ReadTimeout losses in a single day
|
|
15
|
+
-- during a bulk-import whale, 2026-07-14).
|
|
16
|
+
--
|
|
17
|
+
-- This adds a dedicated `transient_attempts` counter. worker.py routes transient
|
|
18
|
+
-- errors here (incrementing THIS, leaving `attempts` untouched so the row stays
|
|
19
|
+
-- claimable — `attempts` remains < MAX_ATTEMPTS) up to MAX_TRANSIENT_ATTEMPTS,
|
|
20
|
+
-- after which the row is marked failed like any genuinely stuck work. Genuine
|
|
21
|
+
-- errors keep using `attempts` exactly as before.
|
|
22
|
+
--
|
|
23
|
+
-- Metadata-only on PostgreSQL (constant DEFAULT, no table rewrite) — safe to run
|
|
24
|
+
-- online against the ~250k-row prod table. Idempotent.
|
|
25
|
+
|
|
26
|
+
ALTER TABLE distillation_queue
|
|
27
|
+
ADD COLUMN IF NOT EXISTS transient_attempts integer NOT NULL DEFAULT 0;
|