@pentatonic-ai/ai-agent-sdk 0.10.32 → 0.10.34
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 +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/packages/memory-engine-v2/extractor-async/worker.py +71 -9
- package/packages/memory-engine-v2/org-model/migrations/012_queue_priority_fair_share.sql +88 -0
- package/packages/memory-engine-v2/scripts/redistill.py +6 -2
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.34";
|
|
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.34";
|
|
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.34",
|
|
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",
|
|
@@ -193,13 +193,24 @@ _PROV_REL = _cap_prov("relationships.provenance_event_ids", "EXCLUDED.provenance
|
|
|
193
193
|
# lever, this is the monitoring + active-learning sample (drift detection and
|
|
194
194
|
# fresh teacher-gold on live traffic). Default conservative-ish 5%.
|
|
195
195
|
STUDENT_SAMPLE_RATE = float(os.environ.get("STUDENT_SAMPLE_RATE", "0.05"))
|
|
196
|
-
# Fact categories that
|
|
197
|
-
#
|
|
196
|
+
# Fact categories that go to the teacher regardless of the student's output
|
|
197
|
+
# (high-value, cheap to over-escalate). Comma-separated, lowercased.
|
|
198
198
|
HIGH_VALUE_CATEGORIES = {
|
|
199
199
|
c.strip().lower()
|
|
200
200
|
for c in os.environ.get("HIGH_VALUE_CATEGORIES", "decision,commitment").split(",")
|
|
201
201
|
if c.strip()
|
|
202
202
|
}
|
|
203
|
+
# Fraction of high-value-class events that ESCALATE to the teacher. 1.0 (default)
|
|
204
|
+
# = the original always-teacher gate. The gate was set at cascade launch when the
|
|
205
|
+
# student was unproven; since then the clean student has zero quality-fail
|
|
206
|
+
# escalations in prod and shadow-eval parity with the teacher — while the HV gate
|
|
207
|
+
# routed ~57% of a decision/commitment-heavy backlog to the teacher (90% of all
|
|
208
|
+
# escalations, 2026-07-09), pinning drain to teacher capacity with the student
|
|
209
|
+
# idle. Lowering this (e.g. 0.25) makes HV student-primary with an elevated
|
|
210
|
+
# teacher sample as the safety net/monitoring corpus on exactly the high-value
|
|
211
|
+
# class. Instant rollback: set back to 1.0. Escalated-anyway HV events keep the
|
|
212
|
+
# 'high_value_class' ledger reason, so the mix stays queryable.
|
|
213
|
+
HIGH_VALUE_SAMPLE_RATE = float(os.environ.get("HIGH_VALUE_SAMPLE_RATE", "1.0"))
|
|
203
214
|
|
|
204
215
|
|
|
205
216
|
# KV-text output format constants. We dropped JSON output (and the
|
|
@@ -1078,10 +1089,14 @@ def escalation_decision(
|
|
|
1078
1089
|
for em in {e.lower() for e in _EMAIL_RE.findall(blob)}:
|
|
1079
1090
|
if em not in known:
|
|
1080
1091
|
return True, "grounding_violation"
|
|
1081
|
-
# 2. High-value fact class —
|
|
1092
|
+
# 2. High-value fact class — teacher at HIGH_VALUE_SAMPLE_RATE (1.0 = the
|
|
1093
|
+
# original always-teacher gate; lower = student-primary with an elevated
|
|
1094
|
+
# teacher sample on exactly this class — see the config note).
|
|
1082
1095
|
for f in student_result.get("facts", []):
|
|
1083
1096
|
if (f.get("category") or "").lower() in HIGH_VALUE_CATEGORIES:
|
|
1084
|
-
|
|
1097
|
+
if HIGH_VALUE_SAMPLE_RATE >= 1.0 or random.random() < HIGH_VALUE_SAMPLE_RATE:
|
|
1098
|
+
return True, "high_value_class"
|
|
1099
|
+
break # HV but sampled-out → student writes; still eligible for gate 3
|
|
1085
1100
|
# 3. Random monitoring/active-learning sample (not a quality lever).
|
|
1086
1101
|
if STUDENT_SAMPLE_RATE > 0 and random.random() < STUDENT_SAMPLE_RATE:
|
|
1087
1102
|
return True, "random_sample"
|
|
@@ -2536,19 +2551,65 @@ def claim_next_batch(conn: psycopg.Connection) -> list[dict[str, Any]]:
|
|
|
2536
2551
|
-- increment lived here and ~3 deploys could push a row to
|
|
2537
2552
|
-- attempts=MAX, making it forever-ineligible for reclaim AND never
|
|
2538
2553
|
-- marked failed → orphaned in `claimed`. See gotcha #11.)
|
|
2554
|
+
-- FAIR-SHARE CLAIM (migration 012). Was `ORDER BY id` — global
|
|
2555
|
+
-- FIFO, so one tenant's 20k-event backfill head-of-line-blocked
|
|
2556
|
+
-- every other tenant. Now each client is a logical queue ordered
|
|
2557
|
+
-- by (priority, emit_ts) — priority is client-set per event
|
|
2558
|
+
-- (envelope attribute high/medium/low → 10/100/200, enriched onto
|
|
2559
|
+
-- the row by trigger) — and the batch interleaves clients
|
|
2560
|
+
-- round-robin: rank rn=1 of every client, then rn=2, ... so every
|
|
2561
|
+
-- active client gets a proportional slice of each claim.
|
|
2562
|
+
--
|
|
2563
|
+
-- Shape: per-client candidates come from a bounded LATERAL scan on
|
|
2564
|
+
-- idx_distillation_fair_claim (client, priority, emit_ts) — the
|
|
2565
|
+
-- window function never sees more than clients x BATCH_SIZE rows.
|
|
2566
|
+
-- FOR UPDATE cannot sit in the same query level as a window
|
|
2567
|
+
-- function, so the ranked ids are re-locked in an inner
|
|
2568
|
+
-- re-check (status/lease guards repeated) — a concurrently-taken
|
|
2569
|
+
-- row is simply skipped, same semantics as before (a claim may
|
|
2570
|
+
-- return fewer than BATCH_SIZE under contention).
|
|
2539
2571
|
WHERE id IN (
|
|
2540
2572
|
SELECT id FROM distillation_queue
|
|
2541
|
-
WHERE (
|
|
2573
|
+
WHERE id = ANY (ARRAY(
|
|
2574
|
+
SELECT cand.id FROM (
|
|
2575
|
+
SELECT x.id,
|
|
2576
|
+
ROW_NUMBER() OVER (
|
|
2577
|
+
PARTITION BY c.client
|
|
2578
|
+
ORDER BY x.priority, x.emit_ts, x.id
|
|
2579
|
+
) AS rn,
|
|
2580
|
+
x.priority, x.emit_ts
|
|
2581
|
+
FROM (
|
|
2582
|
+
SELECT DISTINCT client FROM distillation_queue
|
|
2583
|
+
WHERE (
|
|
2584
|
+
status = 'pending'
|
|
2585
|
+
OR (status = 'claimed' AND claim_expires_at < NOW())
|
|
2586
|
+
) AND attempts < %s
|
|
2587
|
+
) c
|
|
2588
|
+
CROSS JOIN LATERAL (
|
|
2589
|
+
SELECT id, COALESCE(priority, 100) AS priority, emit_ts
|
|
2590
|
+
FROM distillation_queue
|
|
2591
|
+
WHERE client IS NOT DISTINCT FROM c.client
|
|
2592
|
+
AND (
|
|
2593
|
+
status = 'pending'
|
|
2594
|
+
OR (status = 'claimed' AND claim_expires_at < NOW())
|
|
2595
|
+
) AND attempts < %s
|
|
2596
|
+
ORDER BY COALESCE(priority, 100), emit_ts, id
|
|
2597
|
+
LIMIT %s
|
|
2598
|
+
) x
|
|
2599
|
+
) cand
|
|
2600
|
+
ORDER BY cand.rn, cand.priority, cand.emit_ts, cand.id
|
|
2601
|
+
LIMIT %s
|
|
2602
|
+
))
|
|
2603
|
+
AND (
|
|
2542
2604
|
status = 'pending'
|
|
2543
2605
|
OR (status = 'claimed' AND claim_expires_at < NOW())
|
|
2544
2606
|
) AND attempts < %s
|
|
2545
|
-
ORDER BY id
|
|
2546
2607
|
FOR UPDATE SKIP LOCKED
|
|
2547
|
-
LIMIT %s
|
|
2548
2608
|
)
|
|
2549
2609
|
RETURNING id, event_id, attempts
|
|
2550
2610
|
""",
|
|
2551
|
-
(WORKER_ID, CLAIM_TTL_SEC, MAX_ATTEMPTS,
|
|
2611
|
+
(WORKER_ID, CLAIM_TTL_SEC, MAX_ATTEMPTS, MAX_ATTEMPTS,
|
|
2612
|
+
BATCH_SIZE, BATCH_SIZE, MAX_ATTEMPTS),
|
|
2552
2613
|
)
|
|
2553
2614
|
return cur.fetchall()
|
|
2554
2615
|
|
|
@@ -2964,7 +3025,8 @@ async def amain():
|
|
|
2964
3025
|
f"cascade ENABLED — student-primary "
|
|
2965
3026
|
f"(model={STUDENT_MODEL}, endpoint={STUDENT_ENDPOINT or '(unset!)'}, "
|
|
2966
3027
|
f"sample_rate={STUDENT_SAMPLE_RATE}, "
|
|
2967
|
-
f"high_value={sorted(HIGH_VALUE_CATEGORIES)}
|
|
3028
|
+
f"high_value={sorted(HIGH_VALUE_CATEGORIES)}, "
|
|
3029
|
+
f"hv_sample_rate={HIGH_VALUE_SAMPLE_RATE})"
|
|
2968
3030
|
)
|
|
2969
3031
|
if not STUDENT_ENDPOINT:
|
|
2970
3032
|
log.warning(
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
-- 012_queue_priority_fair_share.sql — client-set priority + per-client fair
|
|
2
|
+
-- usage for the distillation queue.
|
|
3
|
+
--
|
|
4
|
+
-- The claim was global FIFO (ORDER BY id): one tenant's 20k-event Drive dump
|
|
5
|
+
-- head-of-line-blocked every other tenant — at 30-40-user onboarding scale the
|
|
6
|
+
-- last user's memory would go live only after everyone ahead of them drained.
|
|
7
|
+
--
|
|
8
|
+
-- Model (decided 2026-07-09): every client effectively gets their OWN queue,
|
|
9
|
+
-- drained round-robin across clients; within a client, items order by
|
|
10
|
+
-- (priority, emit timestamp). Clients set priority per event at emit via the
|
|
11
|
+
-- envelope attribute `priority: "high" | "medium" | "low"` (default medium).
|
|
12
|
+
-- Implemented logically in the one queue table (PARTITION BY client in the
|
|
13
|
+
-- claim) rather than physical per-client tables — identical semantics, none of
|
|
14
|
+
-- the per-tenant DDL/monitoring sprawl; PG LIST-partitioning by client remains
|
|
15
|
+
-- a drop-in physical upgrade under the same claim SQL if ever needed.
|
|
16
|
+
--
|
|
17
|
+
-- Lower priority value = drained first: high=10, medium=100, low=200 (gaps
|
|
18
|
+
-- leave room for future classes without re-mapping).
|
|
19
|
+
--
|
|
20
|
+
-- The queue row is enriched by a BEFORE INSERT trigger (client = arena prefix,
|
|
21
|
+
-- emit_ts = events.emitted_at, priority = mapped envelope attribute unless the
|
|
22
|
+
-- writer set one explicitly). A trigger — not writer-side code — so EVERY
|
|
23
|
+
-- writer (extractor-sync enqueue, the redistill gardener, backfill scripts)
|
|
24
|
+
-- gets consistent enrichment with no emitter/TES changes.
|
|
25
|
+
--
|
|
26
|
+
-- On an already-initialised prod DB (migrations only auto-run on a fresh
|
|
27
|
+
-- volume): apply the ALTERs + trigger + backfill via psql, and build the index
|
|
28
|
+
-- with CREATE INDEX CONCURRENTLY.
|
|
29
|
+
|
|
30
|
+
ALTER TABLE distillation_queue
|
|
31
|
+
ADD COLUMN IF NOT EXISTS client TEXT,
|
|
32
|
+
ADD COLUMN IF NOT EXISTS priority SMALLINT,
|
|
33
|
+
ADD COLUMN IF NOT EXISTS emit_ts TIMESTAMPTZ;
|
|
34
|
+
|
|
35
|
+
CREATE OR REPLACE FUNCTION distillation_queue_enrich() RETURNS trigger AS $$
|
|
36
|
+
DECLARE
|
|
37
|
+
ev RECORD;
|
|
38
|
+
BEGIN
|
|
39
|
+
IF NEW.client IS NULL OR NEW.emit_ts IS NULL OR NEW.priority IS NULL THEN
|
|
40
|
+
SELECT split_part(e.arena, ':', 1) AS client,
|
|
41
|
+
e.emitted_at AS emitted_at,
|
|
42
|
+
lower(e.attributes->>'priority') AS prio
|
|
43
|
+
INTO ev
|
|
44
|
+
FROM events e WHERE e.id = NEW.event_id;
|
|
45
|
+
IF FOUND THEN
|
|
46
|
+
NEW.client := COALESCE(NEW.client, ev.client);
|
|
47
|
+
NEW.emit_ts := COALESCE(NEW.emit_ts, ev.emitted_at);
|
|
48
|
+
-- An explicit writer-set priority wins (e.g. the redistill gardener
|
|
49
|
+
-- self-demoting to low); otherwise the client's envelope attribute;
|
|
50
|
+
-- otherwise medium.
|
|
51
|
+
NEW.priority := COALESCE(
|
|
52
|
+
NEW.priority,
|
|
53
|
+
CASE ev.prio WHEN 'high' THEN 10 WHEN 'medium' THEN 100 WHEN 'low' THEN 200 ELSE 100 END
|
|
54
|
+
);
|
|
55
|
+
ELSE
|
|
56
|
+
-- Orphan enqueue (no event row): defaults keep the row claimable.
|
|
57
|
+
NEW.client := COALESCE(NEW.client, 'unknown');
|
|
58
|
+
NEW.emit_ts := COALESCE(NEW.emit_ts, NOW());
|
|
59
|
+
NEW.priority := COALESCE(NEW.priority, 100);
|
|
60
|
+
END IF;
|
|
61
|
+
END IF;
|
|
62
|
+
RETURN NEW;
|
|
63
|
+
END;
|
|
64
|
+
$$ LANGUAGE plpgsql;
|
|
65
|
+
|
|
66
|
+
DROP TRIGGER IF EXISTS trg_distillation_queue_enrich ON distillation_queue;
|
|
67
|
+
CREATE TRIGGER trg_distillation_queue_enrich
|
|
68
|
+
BEFORE INSERT ON distillation_queue
|
|
69
|
+
FOR EACH ROW EXECUTE FUNCTION distillation_queue_enrich();
|
|
70
|
+
|
|
71
|
+
-- Backfill live rows (pending/claimed). done/failed rows are history — the
|
|
72
|
+
-- daily prune retires them and the claim never reads them.
|
|
73
|
+
UPDATE distillation_queue dq
|
|
74
|
+
SET client = COALESCE(dq.client, split_part(e.arena, ':', 1)),
|
|
75
|
+
emit_ts = COALESCE(dq.emit_ts, e.emitted_at),
|
|
76
|
+
priority = COALESCE(dq.priority,
|
|
77
|
+
CASE lower(e.attributes->>'priority')
|
|
78
|
+
WHEN 'high' THEN 10 WHEN 'medium' THEN 100 WHEN 'low' THEN 200 ELSE 100 END)
|
|
79
|
+
FROM events e
|
|
80
|
+
WHERE dq.event_id = e.id
|
|
81
|
+
AND dq.status IN ('pending', 'claimed')
|
|
82
|
+
AND (dq.client IS NULL OR dq.emit_ts IS NULL OR dq.priority IS NULL);
|
|
83
|
+
|
|
84
|
+
-- Serves both halves of the fair-share claim: the DISTINCT-client scan and
|
|
85
|
+
-- each client's (priority, emit_ts)-ordered candidate scan.
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_distillation_fair_claim
|
|
87
|
+
ON distillation_queue (client, priority, emit_ts)
|
|
88
|
+
WHERE status = 'pending';
|
|
@@ -212,8 +212,12 @@ def main() -> int:
|
|
|
212
212
|
if args.supersede_facts:
|
|
213
213
|
deleted += dump_and_delete_solo_facts(cur, eid, rb)
|
|
214
214
|
cur.execute(
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
# priority=200 (low): re-distills are maintenance — they
|
|
216
|
+
# must never compete with live client traffic. Explicit
|
|
217
|
+
# value wins over the enrichment trigger's mapping
|
|
218
|
+
# (migration 012).
|
|
219
|
+
"INSERT INTO distillation_queue (event_id, status, attempts, priority) "
|
|
220
|
+
"VALUES (%s, 'pending', 0, 200)", (eid,))
|
|
217
221
|
enq += 1
|
|
218
222
|
led.write(json.dumps({
|
|
219
223
|
"event_id": eid, "arena": args.arena,
|