amfs-adapter-postgres 0.2.16__tar.gz → 0.2.17__tar.gz
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.
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/PKG-INFO +2 -2
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/pyproject.toml +2 -2
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/adapter.py +274 -31
- amfs_adapter_postgres-0.2.17/src/amfs_postgres/migrations/009_action_priors.sql +254 -0
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/schema.sql +44 -5
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/.gitignore +0 -0
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/__init__.py +0 -0
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/_fts.py +0 -0
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/async_adapter.py +0 -0
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/migrations/001_initial.sql +0 -0
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/migrations/002_artifact_refs.sql +0 -0
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/migrations/002_search_indexes.sql +0 -0
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/migrations/003_knowledge_graph.sql +0 -0
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/migrations/004_agent_groups.sql +0 -0
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/migrations/005_configurable_embedding_dim.sql +0 -0
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/migrations/006_is_artifact.sql +0 -0
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/migrations/006_outcome_confidence_semantics.sql +0 -0
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/migrations/007_outcome_propagation_row_copy.sql +0 -0
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/migrations/008_outcome_evidence.sql +0 -0
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/retention.py +0 -0
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/tenant_context.py +0 -0
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/tenant_gucs.py +0 -0
- {amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/trace_partitions.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: amfs-adapter-postgres
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.17
|
|
4
4
|
Summary: AMFS Postgres adapter with back-propagation triggers
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.11
|
|
7
|
-
Requires-Dist: amfs-core>=0.3.
|
|
7
|
+
Requires-Dist: amfs-core>=0.3.30
|
|
8
8
|
Requires-Dist: psycopg-pool>=3.1
|
|
9
9
|
Requires-Dist: psycopg[binary]<4,>=3.1
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "amfs-adapter-postgres"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.17"
|
|
4
4
|
description = "AMFS Postgres adapter with back-propagation triggers"
|
|
5
5
|
requires-python = ">=3.11"
|
|
6
6
|
license = "Apache-2.0"
|
|
7
7
|
dependencies = [
|
|
8
8
|
# 0.3.14 adds the capture fields this adapter now persists and selects back,
|
|
9
9
|
# tool_calls among them.
|
|
10
|
-
"amfs-core>=0.3.
|
|
10
|
+
"amfs-core>=0.3.30",
|
|
11
11
|
"psycopg[binary]>=3.1,<4",
|
|
12
12
|
"psycopg-pool>=3.1",
|
|
13
13
|
]
|
|
@@ -146,6 +146,14 @@ _SCHEMA_SQL = (Path(__file__).parent / "schema.sql").read_text(encoding="utf-8")
|
|
|
146
146
|
_OUTCOME_EVIDENCE_SQL = (
|
|
147
147
|
Path(__file__).parent / "migrations" / "008_outcome_evidence.sql"
|
|
148
148
|
).read_text(encoding="utf-8")
|
|
149
|
+
#: Migration 009 — action-level learning (actions_taken / entity_paths /
|
|
150
|
+
#: situation on outcomes, validators on entries, first-strike tolerance in the
|
|
151
|
+
#: step function). Same treatment as 008. The outcomes' ``task_embedding``
|
|
152
|
+
#: column is not in the file: it is a pgvector column whose dimension follows
|
|
153
|
+
#: the entries' embedding, so ``ensure_outcome_embedding_column`` adds it.
|
|
154
|
+
_ACTION_PRIORS_SQL = (
|
|
155
|
+
Path(__file__).parent / "migrations" / "009_action_priors.sql"
|
|
156
|
+
).read_text(encoding="utf-8")
|
|
149
157
|
|
|
150
158
|
# Shared by the sync and async adapters so the two cannot drift apart. account_id
|
|
151
159
|
# is omitted on purpose: the hosted product gives the column a default in a tenant
|
|
@@ -518,9 +526,14 @@ ENTRY_SELECT = ", ".join(_ENTRY_COLUMNS)
|
|
|
518
526
|
ENTRY_SELECT_NO_ARTIFACT_COL = ", ".join(c for c in _ENTRY_COLUMNS if c != "is_artifact")
|
|
519
527
|
|
|
520
528
|
|
|
521
|
-
def entry_select(has_is_artifact_col: bool) -> str:
|
|
522
|
-
"""The SELECT list for queries whose rows become MemoryEntry objects.
|
|
523
|
-
|
|
529
|
+
def entry_select(has_is_artifact_col: bool, has_validators_col: bool = False) -> str:
|
|
530
|
+
"""The SELECT list for queries whose rows become MemoryEntry objects.
|
|
531
|
+
|
|
532
|
+
``validators`` (migration 009) is appended only once the column exists; the
|
|
533
|
+
decoder defaults it to an empty list otherwise.
|
|
534
|
+
"""
|
|
535
|
+
base = ENTRY_SELECT if has_is_artifact_col else ENTRY_SELECT_NO_ARTIFACT_COL
|
|
536
|
+
return base + ", validators" if has_validators_col else base
|
|
524
537
|
|
|
525
538
|
|
|
526
539
|
def _evidence_params(entry: MemoryEntry) -> list[Any]:
|
|
@@ -537,6 +550,18 @@ def _evidence_params(entry: MemoryEntry) -> list[Any]:
|
|
|
537
550
|
]
|
|
538
551
|
|
|
539
552
|
|
|
553
|
+
def _jsonb(raw: Any, default: Any) -> Any:
|
|
554
|
+
"""A JSONB column as Python, whether the driver decoded it or left a string."""
|
|
555
|
+
if raw is None:
|
|
556
|
+
return default
|
|
557
|
+
if isinstance(raw, str):
|
|
558
|
+
try:
|
|
559
|
+
return json.loads(raw)
|
|
560
|
+
except ValueError:
|
|
561
|
+
return default
|
|
562
|
+
return raw
|
|
563
|
+
|
|
564
|
+
|
|
540
565
|
def _inherit_from_row(entry: MemoryEntry, row: Any) -> MemoryEntry:
|
|
541
566
|
"""Apply ``inherit_evidence`` against the live row fetched inside the write
|
|
542
567
|
transaction, so the hot paths that bypass the engine honour the same rule."""
|
|
@@ -556,10 +581,21 @@ def _inherit_from_row(entry: MemoryEntry, row: Any) -> MemoryEntry:
|
|
|
556
581
|
for f in EVIDENCE_FIELDS},
|
|
557
582
|
"evidence_success": float(row.get("evidence_success") or 0.0),
|
|
558
583
|
"evidence_failure": float(row.get("evidence_failure") or 0.0),
|
|
584
|
+
"validators": _validators_of(row),
|
|
559
585
|
})
|
|
560
586
|
return inherit_evidence(entry, current)
|
|
561
587
|
|
|
562
588
|
|
|
589
|
+
def _validators_of(row: Any) -> list[str]:
|
|
590
|
+
raw = row.get("validators") if hasattr(row, "get") else None
|
|
591
|
+
if isinstance(raw, str):
|
|
592
|
+
try:
|
|
593
|
+
raw = json.loads(raw)
|
|
594
|
+
except ValueError:
|
|
595
|
+
raw = None
|
|
596
|
+
return [str(v) for v in raw] if isinstance(raw, list) else []
|
|
597
|
+
|
|
598
|
+
|
|
563
599
|
class PostgresAdapter(AdapterABC):
|
|
564
600
|
"""Store AMFS entries in PostgreSQL.
|
|
565
601
|
|
|
@@ -597,6 +633,9 @@ class PostgresAdapter(AdapterABC):
|
|
|
597
633
|
self._has_search_tsv = False
|
|
598
634
|
self._has_is_artifact_col = False
|
|
599
635
|
self._has_evidence_cols = False
|
|
636
|
+
self._has_validators_col = False
|
|
637
|
+
self._has_action_cols = False
|
|
638
|
+
self._has_outcome_embedding_col = False
|
|
600
639
|
# When an embedder is provided, embeddings are computed at write time and
|
|
601
640
|
# persisted, so ANN retrieval (semantic_search / pgvector HNSW) works
|
|
602
641
|
# without a separate backfill pass. embedding_dim must match the column
|
|
@@ -739,7 +778,7 @@ class PostgresAdapter(AdapterABC):
|
|
|
739
778
|
if migrations is None:
|
|
740
779
|
return None
|
|
741
780
|
return hashlib.sha256(
|
|
742
|
-
(_SCHEMA_SQL + migrations + _OUTCOME_EVIDENCE_SQL).encode("utf-8")
|
|
781
|
+
(_SCHEMA_SQL + migrations + _OUTCOME_EVIDENCE_SQL + _ACTION_PRIORS_SQL).encode("utf-8")
|
|
743
782
|
).hexdigest()
|
|
744
783
|
|
|
745
784
|
def _expected_tables(self) -> frozenset[str]:
|
|
@@ -1086,6 +1125,30 @@ class PostgresAdapter(AdapterABC):
|
|
|
1086
1125
|
# with its helpers. Applied from the migration file rather than a copy
|
|
1087
1126
|
# here — the copy is what drifted twice before (see 006 and 007).
|
|
1088
1127
|
cur.execute(_OUTCOME_EVIDENCE_SQL)
|
|
1128
|
+
# Action-level learning (trigger v4): see migration 009.
|
|
1129
|
+
cur.execute(_ACTION_PRIORS_SQL)
|
|
1130
|
+
# task_embedding on outcomes follows the entries' embedding dimension,
|
|
1131
|
+
# so it is derived here rather than declared in the migration.
|
|
1132
|
+
cur.execute(
|
|
1133
|
+
"""
|
|
1134
|
+
SELECT format_type(a.atttypid, a.atttypmod) AS t
|
|
1135
|
+
FROM pg_attribute a
|
|
1136
|
+
WHERE a.attrelid = 'amfs_memory_entries'::regclass
|
|
1137
|
+
AND a.attname = 'embedding' AND NOT a.attisdropped
|
|
1138
|
+
"""
|
|
1139
|
+
)
|
|
1140
|
+
row = cur.fetchone()
|
|
1141
|
+
m = re.search(r"vector\((\d+)\)", str(row["t"])) if row else None
|
|
1142
|
+
if m:
|
|
1143
|
+
cur.execute(
|
|
1144
|
+
f"ALTER TABLE amfs_outcomes ADD COLUMN IF NOT EXISTS "
|
|
1145
|
+
f"task_embedding vector({int(m.group(1))})"
|
|
1146
|
+
)
|
|
1147
|
+
cur.execute(
|
|
1148
|
+
"CREATE INDEX IF NOT EXISTS idx_outcomes_task_embedding "
|
|
1149
|
+
"ON amfs_outcomes USING hnsw (task_embedding vector_cosine_ops) "
|
|
1150
|
+
"WITH (m = 16, ef_construction = 64)"
|
|
1151
|
+
)
|
|
1089
1152
|
# Soft-delete for team members (user removal flow)
|
|
1090
1153
|
cur.execute("""
|
|
1091
1154
|
ALTER TABLE amfs_team_members
|
|
@@ -1474,7 +1537,7 @@ class PostgresAdapter(AdapterABC):
|
|
|
1474
1537
|
"""
|
|
1475
1538
|
SELECT column_name FROM information_schema.columns
|
|
1476
1539
|
WHERE table_name = 'amfs_memory_entries'
|
|
1477
|
-
AND column_name IN ('embedding', 'search_tsv', 'is_artifact', 'success_count')
|
|
1540
|
+
AND column_name IN ('embedding', 'search_tsv', 'is_artifact', 'success_count', 'validators')
|
|
1478
1541
|
""",
|
|
1479
1542
|
)
|
|
1480
1543
|
found = {row["column_name"] for row in cur.fetchall()}
|
|
@@ -1482,6 +1545,17 @@ class PostgresAdapter(AdapterABC):
|
|
|
1482
1545
|
self._has_search_tsv = "search_tsv" in found
|
|
1483
1546
|
self._has_is_artifact_col = "is_artifact" in found
|
|
1484
1547
|
self._has_evidence_cols = "success_count" in found
|
|
1548
|
+
self._has_validators_col = "validators" in found
|
|
1549
|
+
cur.execute(
|
|
1550
|
+
"""
|
|
1551
|
+
SELECT column_name FROM information_schema.columns
|
|
1552
|
+
WHERE table_name = 'amfs_outcomes'
|
|
1553
|
+
AND column_name IN ('actions_taken', 'task_embedding')
|
|
1554
|
+
""",
|
|
1555
|
+
)
|
|
1556
|
+
found = {row["column_name"] for row in cur.fetchall()}
|
|
1557
|
+
self._has_action_cols = "actions_taken" in found
|
|
1558
|
+
self._has_outcome_embedding_col = "task_embedding" in found
|
|
1485
1559
|
|
|
1486
1560
|
# ------------------------------------------------------------------
|
|
1487
1561
|
# read
|
|
@@ -1574,7 +1648,7 @@ class PostgresAdapter(AdapterABC):
|
|
|
1574
1648
|
with conn.cursor() as cur:
|
|
1575
1649
|
cur.execute(
|
|
1576
1650
|
f"""
|
|
1577
|
-
SELECT version, value, confidence{_EVIDENCE_SELECT if self._has_evidence_cols else ""}
|
|
1651
|
+
SELECT version, value, confidence{_EVIDENCE_SELECT if self._has_evidence_cols else ""}{", validators" if self._has_validators_col else ""}
|
|
1578
1652
|
FROM amfs_memory_entries
|
|
1579
1653
|
WHERE namespace = %s AND branch = %s
|
|
1580
1654
|
AND entity_path = %s AND key = %s
|
|
@@ -1657,6 +1731,9 @@ class PostgresAdapter(AdapterABC):
|
|
|
1657
1731
|
if self._has_evidence_cols:
|
|
1658
1732
|
columns.extend(_EVIDENCE_COLUMNS)
|
|
1659
1733
|
params.extend(_evidence_params(entry))
|
|
1734
|
+
if self._has_validators_col:
|
|
1735
|
+
columns.append("validators")
|
|
1736
|
+
params.append(json.dumps(list(entry.validators or [])))
|
|
1660
1737
|
|
|
1661
1738
|
# Write-time embedding: compute and persist an embedding when
|
|
1662
1739
|
# the caller did not supply one and an embedder is configured.
|
|
@@ -1968,7 +2045,7 @@ class PostgresAdapter(AdapterABC):
|
|
|
1968
2045
|
|
|
1969
2046
|
where = " AND ".join(conditions)
|
|
1970
2047
|
query = (
|
|
1971
|
-
f"SELECT {entry_select(self._has_is_artifact_col)} FROM amfs_memory_entries "
|
|
2048
|
+
f"SELECT {entry_select(self._has_is_artifact_col, self._has_validators_col)} FROM amfs_memory_entries "
|
|
1972
2049
|
f"WHERE {where} ORDER BY entity_path, key, version"
|
|
1973
2050
|
)
|
|
1974
2051
|
|
|
@@ -2073,7 +2150,7 @@ class PostgresAdapter(AdapterABC):
|
|
|
2073
2150
|
|
|
2074
2151
|
where = " AND ".join(conditions)
|
|
2075
2152
|
sql = f"""
|
|
2076
|
-
SELECT {entry_select(col_ready)} FROM amfs_memory_entries
|
|
2153
|
+
SELECT {entry_select(col_ready, self._has_validators_col)} FROM amfs_memory_entries
|
|
2077
2154
|
WHERE {where}
|
|
2078
2155
|
ORDER BY {order}
|
|
2079
2156
|
LIMIT %s
|
|
@@ -3231,29 +3308,44 @@ class PostgresAdapter(AdapterABC):
|
|
|
3231
3308
|
"SELECT set_config('amfs.outcome_model', %s, true)",
|
|
3232
3309
|
(outcome_model(),),
|
|
3233
3310
|
)
|
|
3234
|
-
|
|
3235
|
-
"""
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
record.
|
|
3250
|
-
|
|
3251
|
-
[a.model_dump(mode="json") for a in record.attempts],
|
|
3252
|
-
default=str,
|
|
3253
|
-
),
|
|
3254
|
-
record.final_action_index,
|
|
3255
|
-
json.dumps(dict(record.causal_entry_versions or {})),
|
|
3311
|
+
columns = [
|
|
3312
|
+
"namespace", "outcome_ref", "outcome_type", "causal_confidence",
|
|
3313
|
+
"committed_at", "causal_entry_keys", "agent_id",
|
|
3314
|
+
"attempts", "final_action_index", "causal_entry_versions",
|
|
3315
|
+
]
|
|
3316
|
+
placeholders = ["%s"] * 7 + ["%s::jsonb", "%s", "%s::jsonb"]
|
|
3317
|
+
params: list[Any] = [
|
|
3318
|
+
self._namespace,
|
|
3319
|
+
record.outcome_ref,
|
|
3320
|
+
record.outcome_type.value,
|
|
3321
|
+
record.causal_confidence,
|
|
3322
|
+
record.committed_at,
|
|
3323
|
+
record.causal_entry_keys,
|
|
3324
|
+
record.agent_id,
|
|
3325
|
+
json.dumps(
|
|
3326
|
+
[a.model_dump(mode="json") for a in record.attempts],
|
|
3327
|
+
default=str,
|
|
3256
3328
|
),
|
|
3329
|
+
record.final_action_index,
|
|
3330
|
+
json.dumps(dict(record.causal_entry_versions or {})),
|
|
3331
|
+
]
|
|
3332
|
+
if self._has_action_cols:
|
|
3333
|
+
columns += ["actions_taken", "entity_paths", "situation"]
|
|
3334
|
+
placeholders += ["%s::jsonb", "%s", "%s"]
|
|
3335
|
+
params += [
|
|
3336
|
+
json.dumps(list(record.actions_taken or []), default=str),
|
|
3337
|
+
list(record.entity_paths or []),
|
|
3338
|
+
record.situation,
|
|
3339
|
+
]
|
|
3340
|
+
task_embedding = self._outcome_embedding(record)
|
|
3341
|
+
if task_embedding is not None:
|
|
3342
|
+
columns.append("task_embedding")
|
|
3343
|
+
placeholders.append("%s")
|
|
3344
|
+
params.append(f"[{','.join(str(v) for v in task_embedding)}]")
|
|
3345
|
+
cur.execute(
|
|
3346
|
+
f"INSERT INTO amfs_outcomes ({', '.join(columns)}) "
|
|
3347
|
+
f"VALUES ({', '.join(placeholders)})",
|
|
3348
|
+
params,
|
|
3257
3349
|
)
|
|
3258
3350
|
|
|
3259
3351
|
# Every entry any step cited, including the failed attempts' — those
|
|
@@ -3320,10 +3412,11 @@ class PostgresAdapter(AdapterABC):
|
|
|
3320
3412
|
where, params = self._outcome_conditions(
|
|
3321
3413
|
entity_path=entity_path, since=since, outcome_ref=outcome_ref
|
|
3322
3414
|
)
|
|
3415
|
+
extra = ", actions_taken, entity_paths, situation" if self._has_action_cols else ""
|
|
3323
3416
|
sql = f"""
|
|
3324
3417
|
SELECT outcome_ref, outcome_type, causal_confidence,
|
|
3325
3418
|
committed_at, causal_entry_keys, agent_id,
|
|
3326
|
-
attempts, final_action_index
|
|
3419
|
+
attempts, final_action_index, causal_entry_versions{extra}
|
|
3327
3420
|
FROM amfs_outcomes
|
|
3328
3421
|
WHERE {where}
|
|
3329
3422
|
ORDER BY committed_at DESC
|
|
@@ -3352,10 +3445,159 @@ class PostgresAdapter(AdapterABC):
|
|
|
3352
3445
|
agent_id=row["agent_id"],
|
|
3353
3446
|
attempts=_attempts_from_row(row.get("attempts")),
|
|
3354
3447
|
final_action_index=row.get("final_action_index"),
|
|
3448
|
+
causal_entry_versions=_jsonb(row.get("causal_entry_versions"), {}),
|
|
3449
|
+
actions_taken=_jsonb(row.get("actions_taken"), []),
|
|
3450
|
+
entity_paths=list(row.get("entity_paths") or []),
|
|
3451
|
+
situation=row.get("situation"),
|
|
3355
3452
|
)
|
|
3356
3453
|
)
|
|
3357
3454
|
return results
|
|
3358
3455
|
|
|
3456
|
+
# ------------------------------------------------------------------
|
|
3457
|
+
# action priors (migration 009)
|
|
3458
|
+
# ------------------------------------------------------------------
|
|
3459
|
+
|
|
3460
|
+
def _outcome_embedding(self, record: OutcomeRecord) -> list[float] | None:
|
|
3461
|
+
"""The vector stored with an outcome so later tasks can find it by
|
|
3462
|
+
similarity. Built from the situation label and the task input; ``None``
|
|
3463
|
+
when there is nothing to embed, no embedder, or no column."""
|
|
3464
|
+
if self._embedder is None or not self._has_outcome_embedding_col:
|
|
3465
|
+
return None
|
|
3466
|
+
text = " ".join(t for t in (record.situation, record.task_input) if t).strip()
|
|
3467
|
+
if not text:
|
|
3468
|
+
return None
|
|
3469
|
+
try:
|
|
3470
|
+
return self._embedder.embed(text[:2000])
|
|
3471
|
+
except Exception: # noqa: BLE001
|
|
3472
|
+
logger.warning("outcome embedding failed for %s; storing without vector",
|
|
3473
|
+
record.outcome_ref, exc_info=True)
|
|
3474
|
+
return None
|
|
3475
|
+
|
|
3476
|
+
def ensure_outcome_embedding_column(self) -> bool:
|
|
3477
|
+
"""Add ``amfs_outcomes.task_embedding`` at the entries' embedding dimension.
|
|
3478
|
+
|
|
3479
|
+
Idempotent. Returns ``False`` (and does nothing) when the entries table
|
|
3480
|
+
has no embedding column yet — the dimension is read from it, never
|
|
3481
|
+
assumed, because deployments provision 384 or 1024 (see
|
|
3482
|
+
``ensure_embedding_column``). The HNSW index is built over a column that
|
|
3483
|
+
is NULL on every existing row, so it is cheap on a populated table.
|
|
3484
|
+
"""
|
|
3485
|
+
with self._maintenance_connection() as conn:
|
|
3486
|
+
with conn.cursor() as cur:
|
|
3487
|
+
cur.execute(
|
|
3488
|
+
"""
|
|
3489
|
+
SELECT format_type(a.atttypid, a.atttypmod) AS t
|
|
3490
|
+
FROM pg_attribute a
|
|
3491
|
+
WHERE a.attrelid = 'amfs_memory_entries'::regclass
|
|
3492
|
+
AND a.attname = 'embedding' AND NOT a.attisdropped
|
|
3493
|
+
"""
|
|
3494
|
+
)
|
|
3495
|
+
row = cur.fetchone()
|
|
3496
|
+
if not row:
|
|
3497
|
+
return False
|
|
3498
|
+
m = re.search(r"vector\((\d+)\)", str(row["t"]))
|
|
3499
|
+
if not m:
|
|
3500
|
+
return False
|
|
3501
|
+
dim = int(m.group(1))
|
|
3502
|
+
cur.execute(
|
|
3503
|
+
f"ALTER TABLE amfs_outcomes ADD COLUMN IF NOT EXISTS task_embedding vector({dim})"
|
|
3504
|
+
)
|
|
3505
|
+
cur.execute(
|
|
3506
|
+
"CREATE INDEX IF NOT EXISTS idx_outcomes_task_embedding "
|
|
3507
|
+
"ON amfs_outcomes USING hnsw (task_embedding vector_cosine_ops) "
|
|
3508
|
+
"WITH (m = 16, ef_construction = 64)"
|
|
3509
|
+
)
|
|
3510
|
+
self._detect_optional_columns()
|
|
3511
|
+
return True
|
|
3512
|
+
|
|
3513
|
+
def similar_outcomes(
|
|
3514
|
+
self,
|
|
3515
|
+
entity_path: str,
|
|
3516
|
+
embedding: list[float],
|
|
3517
|
+
*,
|
|
3518
|
+
k: int = 20,
|
|
3519
|
+
min_similarity: float = 0.75,
|
|
3520
|
+
since: datetime | None = None,
|
|
3521
|
+
) -> list[dict[str, Any]]:
|
|
3522
|
+
"""The ``k`` committed outcomes about ``entity_path`` whose task was most
|
|
3523
|
+
like the one embedded, each with its ``actions_taken`` and ``similarity``.
|
|
3524
|
+
|
|
3525
|
+
Empty when the store has no task embeddings (no embedder, or the column
|
|
3526
|
+
is not provisioned), so callers fall back to :meth:`action_stats`.
|
|
3527
|
+
"""
|
|
3528
|
+
if not self._has_outcome_embedding_col or not self._has_action_cols or not embedding:
|
|
3529
|
+
return []
|
|
3530
|
+
vec = f"[{','.join(str(v) for v in embedding)}]"
|
|
3531
|
+
conditions = ["namespace = %s", "task_embedding IS NOT NULL", "%s = ANY(entity_paths)"]
|
|
3532
|
+
params: list[Any] = [self._namespace, entity_path]
|
|
3533
|
+
if since is not None:
|
|
3534
|
+
conditions.append("committed_at >= %s")
|
|
3535
|
+
params.append(since)
|
|
3536
|
+
sql = f"""
|
|
3537
|
+
SELECT outcome_ref, outcome_type, committed_at, agent_id, actions_taken, situation,
|
|
3538
|
+
1 - (task_embedding <=> %s::vector) AS similarity
|
|
3539
|
+
FROM amfs_outcomes
|
|
3540
|
+
WHERE {" AND ".join(conditions)}
|
|
3541
|
+
ORDER BY task_embedding <=> %s::vector
|
|
3542
|
+
LIMIT %s
|
|
3543
|
+
"""
|
|
3544
|
+
with self._pool.connection() as conn:
|
|
3545
|
+
with conn.cursor() as cur:
|
|
3546
|
+
cur.execute(sql, [vec, *params, vec, int(k)])
|
|
3547
|
+
rows = cur.fetchall()
|
|
3548
|
+
out: list[dict[str, Any]] = []
|
|
3549
|
+
for row in rows:
|
|
3550
|
+
sim = float(row["similarity"] or 0.0)
|
|
3551
|
+
if sim < min_similarity:
|
|
3552
|
+
continue
|
|
3553
|
+
out.append({
|
|
3554
|
+
"outcome_ref": row["outcome_ref"],
|
|
3555
|
+
"outcome_type": row["outcome_type"],
|
|
3556
|
+
"committed_at": row["committed_at"],
|
|
3557
|
+
"agent_id": row["agent_id"],
|
|
3558
|
+
"situation": row.get("situation"),
|
|
3559
|
+
"actions_taken": _jsonb(row.get("actions_taken"), []),
|
|
3560
|
+
"similarity": sim,
|
|
3561
|
+
})
|
|
3562
|
+
return out
|
|
3563
|
+
|
|
3564
|
+
def action_stats(
|
|
3565
|
+
self,
|
|
3566
|
+
entity_path: str,
|
|
3567
|
+
*,
|
|
3568
|
+
since: datetime | None = None,
|
|
3569
|
+
limit: int = 500,
|
|
3570
|
+
) -> list[dict[str, Any]]:
|
|
3571
|
+
"""The most recent outcomes about ``entity_path`` with their
|
|
3572
|
+
``actions_taken`` — the similarity-free fallback for priors."""
|
|
3573
|
+
if not self._has_action_cols:
|
|
3574
|
+
return []
|
|
3575
|
+
conditions = ["namespace = %s", "%s = ANY(entity_paths)", "actions_taken <> '[]'::jsonb"]
|
|
3576
|
+
params: list[Any] = [self._namespace, entity_path]
|
|
3577
|
+
if since is not None:
|
|
3578
|
+
conditions.append("committed_at >= %s")
|
|
3579
|
+
params.append(since)
|
|
3580
|
+
sql = f"""
|
|
3581
|
+
SELECT outcome_ref, outcome_type, committed_at, agent_id, actions_taken, situation
|
|
3582
|
+
FROM amfs_outcomes
|
|
3583
|
+
WHERE {" AND ".join(conditions)}
|
|
3584
|
+
ORDER BY committed_at DESC
|
|
3585
|
+
LIMIT %s
|
|
3586
|
+
"""
|
|
3587
|
+
with self._pool.connection() as conn:
|
|
3588
|
+
with conn.cursor() as cur:
|
|
3589
|
+
cur.execute(sql, [*params, int(limit)])
|
|
3590
|
+
rows = cur.fetchall()
|
|
3591
|
+
return [{
|
|
3592
|
+
"outcome_ref": row["outcome_ref"],
|
|
3593
|
+
"outcome_type": row["outcome_type"],
|
|
3594
|
+
"committed_at": row["committed_at"],
|
|
3595
|
+
"agent_id": row["agent_id"],
|
|
3596
|
+
"situation": row.get("situation"),
|
|
3597
|
+
"actions_taken": _jsonb(row.get("actions_taken"), []),
|
|
3598
|
+
"similarity": 1.0,
|
|
3599
|
+
} for row in rows]
|
|
3600
|
+
|
|
3359
3601
|
# ------------------------------------------------------------------
|
|
3360
3602
|
# decision traces
|
|
3361
3603
|
# ------------------------------------------------------------------
|
|
@@ -3931,6 +4173,7 @@ class PostgresAdapter(AdapterABC):
|
|
|
3931
4173
|
last_outcome=row.get("last_outcome"),
|
|
3932
4174
|
last_outcome_at=row.get("last_outcome_at"),
|
|
3933
4175
|
discredited_at=row.get("discredited_at"),
|
|
4176
|
+
validators=_validators_of(row),
|
|
3934
4177
|
recall_count=row.get("recall_count", 0),
|
|
3935
4178
|
priority_score=float(row["priority_score"]) if row.get("priority_score") is not None else None,
|
|
3936
4179
|
tier=row.get("tier", 3),
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
-- Migration 009: action-level learning, first-strike tolerance, validators
|
|
2
|
+
--
|
|
3
|
+
-- Grid v2 of the continual-learning benchmark showed two things the evidence
|
|
4
|
+
-- model of 008 could not do:
|
|
5
|
+
--
|
|
6
|
+
-- * Memory entries record what someone *wrote*; nothing recorded what
|
|
7
|
+
-- happened when an agent *did* something. A support fleet spent 24
|
|
8
|
+
-- attempts per ticket class on the same two failing actions and never
|
|
9
|
+
-- tried the one that worked, because "tried and failed here" was not a
|
|
10
|
+
-- thing memory could say. `actions_taken` on the outcome row is that
|
|
11
|
+
-- record: the action that ended each failed attempt (a loss) and the
|
|
12
|
+
-- terminal action (the outcome). `entity_paths` scopes it to the entities
|
|
13
|
+
-- the outcome was about; `situation` is an optional caller label. The
|
|
14
|
+
-- server adds `task_embedding` (a pgvector column at the same dimension as
|
|
15
|
+
-- the entries' embedding — provisioned by the adapter, not here, because
|
|
16
|
+
-- the dimension is deployment-specific) so a retrieve can ask "on the
|
|
17
|
+
-- tasks most like this one, what worked?".
|
|
18
|
+
-- * A single failure on a rule validated four times discredited it
|
|
19
|
+
-- (0.89 -> 0.49) and pulled it out of retrieval. That is how the surprise
|
|
20
|
+
-- term should treat a fresh claim, not a proven one. The first failure of
|
|
21
|
+
-- an entry with a clean record of at least three successes is now weighed
|
|
22
|
+
-- without surprise: the rule is left contested (~0.62), and a second
|
|
23
|
+
-- failure still discredits it (~0.40), so a regime change is unlearned in
|
|
24
|
+
-- the same two strikes as before.
|
|
25
|
+
--
|
|
26
|
+
-- `validators` on entries lists the distinct agents whose successes credited
|
|
27
|
+
-- the claim; the trigger maintains it. The step function gains the committing
|
|
28
|
+
-- agent as an eighth argument; the seven-argument form is retired.
|
|
29
|
+
--
|
|
30
|
+
-- Arithmetic mirrors amfs_core/evidence.py and must stay identical to
|
|
31
|
+
-- schema.sql, which carries the same function bodies.
|
|
32
|
+
|
|
33
|
+
ALTER TABLE amfs_outcomes ADD COLUMN IF NOT EXISTS actions_taken JSONB NOT NULL DEFAULT '[]';
|
|
34
|
+
ALTER TABLE amfs_outcomes ADD COLUMN IF NOT EXISTS entity_paths TEXT[] NOT NULL DEFAULT '{}';
|
|
35
|
+
ALTER TABLE amfs_outcomes ADD COLUMN IF NOT EXISTS situation TEXT;
|
|
36
|
+
ALTER TABLE amfs_memory_entries ADD COLUMN IF NOT EXISTS validators JSONB NOT NULL DEFAULT '[]';
|
|
37
|
+
|
|
38
|
+
-- Priors are scoped by entity overlap, then narrowed by vector similarity.
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_outcomes_entity_paths
|
|
40
|
+
ON amfs_outcomes USING gin (entity_paths);
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_outcomes_ns_committed
|
|
42
|
+
ON amfs_outcomes (namespace, committed_at DESC);
|
|
43
|
+
|
|
44
|
+
-- The seven-argument step predates validators; the trigger below calls the
|
|
45
|
+
-- eight-argument one, so the old overload is retired here.
|
|
46
|
+
DROP FUNCTION IF EXISTS amfs_apply_outcome_step(TEXT, UUID, TEXT[], TEXT, NUMERIC, TEXT, JSONB);
|
|
47
|
+
|
|
48
|
+
CREATE OR REPLACE FUNCTION amfs_apply_outcome_step(
|
|
49
|
+
p_namespace TEXT,
|
|
50
|
+
p_account UUID,
|
|
51
|
+
p_keys TEXT[],
|
|
52
|
+
p_outcome_type TEXT,
|
|
53
|
+
p_causal_confidence NUMERIC,
|
|
54
|
+
p_model TEXT,
|
|
55
|
+
p_versions JSONB,
|
|
56
|
+
p_agent TEXT
|
|
57
|
+
) RETURNS INTEGER AS $$
|
|
58
|
+
DECLARE
|
|
59
|
+
keys TEXT[];
|
|
60
|
+
entry_key TEXT;
|
|
61
|
+
ep TEXT;
|
|
62
|
+
k TEXT;
|
|
63
|
+
cur RECORD;
|
|
64
|
+
read_version INTEGER;
|
|
65
|
+
read_value JSONB;
|
|
66
|
+
n_keys INTEGER;
|
|
67
|
+
is_ok BOOLEAN;
|
|
68
|
+
target NUMERIC;
|
|
69
|
+
w NUMERIC;
|
|
70
|
+
prior NUMERIC;
|
|
71
|
+
e_s NUMERIC;
|
|
72
|
+
e_f NUMERIC;
|
|
73
|
+
new_conf NUMERIC;
|
|
74
|
+
disc TIMESTAMPTZ;
|
|
75
|
+
surprise NUMERIC;
|
|
76
|
+
vals JSONB;
|
|
77
|
+
touched INTEGER := 0;
|
|
78
|
+
BEGIN
|
|
79
|
+
-- Distinct, well-formed specs only; a key cited twice is one citation.
|
|
80
|
+
SELECT COALESCE(array_agg(DISTINCT x), '{}') INTO keys
|
|
81
|
+
FROM unnest(COALESCE(p_keys, '{}')) AS x
|
|
82
|
+
WHERE position('/' in x) > 0;
|
|
83
|
+
n_keys := cardinality(keys);
|
|
84
|
+
IF n_keys = 0 THEN
|
|
85
|
+
RETURN 0;
|
|
86
|
+
END IF;
|
|
87
|
+
|
|
88
|
+
-- An outcome type neither side of the model knows is not evidence of
|
|
89
|
+
-- anything (the multipliers treated it as x1.0; this treats it as no step).
|
|
90
|
+
IF NOT amfs_outcome_is_known(p_outcome_type) THEN
|
|
91
|
+
RETURN 0;
|
|
92
|
+
END IF;
|
|
93
|
+
is_ok := amfs_outcome_is_success(p_outcome_type);
|
|
94
|
+
target := CASE WHEN is_ok THEN 1.0 ELSE 0.0 END;
|
|
95
|
+
|
|
96
|
+
FOREACH entry_key IN ARRAY keys
|
|
97
|
+
LOOP
|
|
98
|
+
-- Last-slash split, matching Python's rsplit("/", 1).
|
|
99
|
+
k := substring(entry_key from '([^/]+)$');
|
|
100
|
+
ep := left(entry_key, length(entry_key) - length(k) - 1);
|
|
101
|
+
|
|
102
|
+
SELECT * INTO cur FROM amfs_memory_entries
|
|
103
|
+
WHERE namespace = p_namespace
|
|
104
|
+
AND entity_path = ep
|
|
105
|
+
AND key = k
|
|
106
|
+
AND superseded_at IS NULL
|
|
107
|
+
AND account_id IS NOT DISTINCT FROM p_account
|
|
108
|
+
ORDER BY version DESC LIMIT 1;
|
|
109
|
+
|
|
110
|
+
IF NOT FOUND THEN
|
|
111
|
+
CONTINUE;
|
|
112
|
+
END IF;
|
|
113
|
+
|
|
114
|
+
-- Credit the claim that was read. If the key has been rewritten since
|
|
115
|
+
-- and now says something else, this outcome is about the old claim.
|
|
116
|
+
read_version := NULLIF(COALESCE(p_versions, '{}'::jsonb)->>entry_key, '')::INTEGER;
|
|
117
|
+
IF read_version IS NOT NULL AND read_version <> cur.version THEN
|
|
118
|
+
SELECT value INTO read_value FROM amfs_memory_entries
|
|
119
|
+
WHERE namespace = p_namespace
|
|
120
|
+
AND entity_path = ep
|
|
121
|
+
AND key = k
|
|
122
|
+
AND version = read_version
|
|
123
|
+
AND account_id IS NOT DISTINCT FROM p_account
|
|
124
|
+
LIMIT 1;
|
|
125
|
+
IF FOUND AND read_value IS DISTINCT FROM cur.value THEN
|
|
126
|
+
CONTINUE;
|
|
127
|
+
END IF;
|
|
128
|
+
END IF;
|
|
129
|
+
|
|
130
|
+
prior := LEAST(1.0, GREATEST(0.0, COALESCE(cur.prior_confidence, cur.confidence)));
|
|
131
|
+
|
|
132
|
+
IF p_model = 'multiplicative' THEN
|
|
133
|
+
new_conf := LEAST(1.0, GREATEST(0.0,
|
|
134
|
+
cur.confidence * amfs_outcome_multiplier(p_outcome_type) * p_causal_confidence));
|
|
135
|
+
e_s := COALESCE(cur.evidence_success, 0) + CASE WHEN is_ok THEN 1 ELSE 0 END;
|
|
136
|
+
e_f := COALESCE(cur.evidence_failure, 0) + CASE WHEN is_ok THEN 0 ELSE 1 END;
|
|
137
|
+
ELSE
|
|
138
|
+
-- First strike: the first failure of an entry with a clean record
|
|
139
|
+
-- of at least three successes is weighed without the surprise
|
|
140
|
+
-- term (amfs_core.evidence.first_strike). One slip on a validated
|
|
141
|
+
-- rule leaves it contested; the second still discredits it.
|
|
142
|
+
surprise := 1.0 + abs(target - LEAST(1.0, GREATEST(0.0, cur.confidence)));
|
|
143
|
+
IF NOT is_ok AND COALESCE(cur.failure_count, 0) = 0
|
|
144
|
+
AND COALESCE(cur.success_count, 0) >= 3 THEN
|
|
145
|
+
surprise := 1.0;
|
|
146
|
+
END IF;
|
|
147
|
+
w := amfs_outcome_severity(p_outcome_type)
|
|
148
|
+
* GREATEST(0.0, p_causal_confidence)
|
|
149
|
+
/ n_keys
|
|
150
|
+
* surprise;
|
|
151
|
+
e_s := COALESCE(cur.evidence_success, 0) * 0.8 + CASE WHEN is_ok THEN w ELSE 0 END;
|
|
152
|
+
e_f := COALESCE(cur.evidence_failure, 0) * 0.8 + CASE WHEN is_ok THEN 0 ELSE w END;
|
|
153
|
+
new_conf := LEAST(1.0, GREATEST(0.0, (2.0 * prior + e_s) / (2.0 + e_s + e_f)));
|
|
154
|
+
END IF;
|
|
155
|
+
|
|
156
|
+
IF new_conf < 0.5 AND NOT is_ok THEN
|
|
157
|
+
disc := COALESCE(cur.discredited_at, NOW());
|
|
158
|
+
ELSIF new_conf >= 0.5 THEN
|
|
159
|
+
disc := NULL;
|
|
160
|
+
ELSE
|
|
161
|
+
disc := cur.discredited_at;
|
|
162
|
+
END IF;
|
|
163
|
+
|
|
164
|
+
-- Validators: distinct agents whose successes credited this claim,
|
|
165
|
+
-- most recent last, capped at ten (amfs_core.evidence.validators_after).
|
|
166
|
+
vals := COALESCE(cur.validators, '[]'::jsonb);
|
|
167
|
+
IF is_ok AND COALESCE(p_agent, '') <> '' THEN
|
|
168
|
+
vals := vals - p_agent;
|
|
169
|
+
vals := vals || to_jsonb(ARRAY[p_agent]);
|
|
170
|
+
IF jsonb_array_length(vals) > 10 THEN
|
|
171
|
+
SELECT COALESCE(jsonb_agg(v), '[]'::jsonb) INTO vals
|
|
172
|
+
FROM (
|
|
173
|
+
SELECT v FROM jsonb_array_elements(vals) WITH ORDINALITY AS t(v, ord)
|
|
174
|
+
ORDER BY ord OFFSET jsonb_array_length(vals) - 10
|
|
175
|
+
) s;
|
|
176
|
+
END IF;
|
|
177
|
+
END IF;
|
|
178
|
+
|
|
179
|
+
UPDATE amfs_memory_entries
|
|
180
|
+
SET superseded_at = NOW()
|
|
181
|
+
WHERE id = cur.id;
|
|
182
|
+
|
|
183
|
+
-- Copy the row (see 007); override only what this version changes.
|
|
184
|
+
INSERT INTO amfs_memory_entries
|
|
185
|
+
SELECT * FROM jsonb_populate_record(
|
|
186
|
+
NULL::amfs_memory_entries,
|
|
187
|
+
to_jsonb(cur) || jsonb_build_object(
|
|
188
|
+
'id', gen_random_uuid(),
|
|
189
|
+
'version', cur.version + 1,
|
|
190
|
+
'confidence', new_conf,
|
|
191
|
+
'outcome_count', cur.outcome_count + 1,
|
|
192
|
+
'success_count', COALESCE(cur.success_count, 0) + CASE WHEN is_ok THEN 1 ELSE 0 END,
|
|
193
|
+
'failure_count', COALESCE(cur.failure_count, 0) + CASE WHEN is_ok THEN 0 ELSE 1 END,
|
|
194
|
+
'evidence_success', e_s,
|
|
195
|
+
'evidence_failure', e_f,
|
|
196
|
+
'prior_confidence', prior,
|
|
197
|
+
'last_outcome', p_outcome_type,
|
|
198
|
+
'last_outcome_at', NOW(),
|
|
199
|
+
'discredited_at', disc,
|
|
200
|
+
'validators', vals,
|
|
201
|
+
'superseded_at', NULL
|
|
202
|
+
)
|
|
203
|
+
);
|
|
204
|
+
touched := touched + 1;
|
|
205
|
+
END LOOP;
|
|
206
|
+
RETURN touched;
|
|
207
|
+
END;
|
|
208
|
+
$$ LANGUAGE plpgsql;
|
|
209
|
+
|
|
210
|
+
CREATE OR REPLACE FUNCTION amfs_propagate_outcome() RETURNS TRIGGER AS $$
|
|
211
|
+
DECLARE
|
|
212
|
+
model TEXT;
|
|
213
|
+
att JSONB;
|
|
214
|
+
att_keys TEXT[];
|
|
215
|
+
BEGIN
|
|
216
|
+
model := COALESCE(NULLIF(current_setting('amfs.outcome_model', true), ''), 'evidence');
|
|
217
|
+
|
|
218
|
+
-- Failed attempts first, oldest to newest: each hands its own failure to
|
|
219
|
+
-- the entries that attempt relied on.
|
|
220
|
+
FOR att IN
|
|
221
|
+
SELECT value FROM jsonb_array_elements(COALESCE(NEW.attempts, '[]'::jsonb))
|
|
222
|
+
ORDER BY COALESCE((value->>'attempt')::INTEGER, 0)
|
|
223
|
+
LOOP
|
|
224
|
+
SELECT COALESCE(array_agg(x), '{}') INTO att_keys
|
|
225
|
+
FROM jsonb_array_elements_text(COALESCE(att->'causal_entry_keys', '[]'::jsonb)) AS x;
|
|
226
|
+
PERFORM amfs_apply_outcome_step(
|
|
227
|
+
NEW.namespace, NEW.account_id, att_keys,
|
|
228
|
+
COALESCE(att->>'outcome_type', 'minor_failure'),
|
|
229
|
+
NEW.causal_confidence, model,
|
|
230
|
+
COALESCE(att->'causal_entry_versions', '{}'::jsonb),
|
|
231
|
+
NEW.agent_id
|
|
232
|
+
);
|
|
233
|
+
END LOOP;
|
|
234
|
+
|
|
235
|
+
-- Then the terminal outcome to the entries the resolution relied on.
|
|
236
|
+
PERFORM amfs_apply_outcome_step(
|
|
237
|
+
NEW.namespace, NEW.account_id, NEW.causal_entry_keys,
|
|
238
|
+
NEW.outcome_type, NEW.causal_confidence, model,
|
|
239
|
+
COALESCE(NEW.causal_entry_versions, '{}'::jsonb),
|
|
240
|
+
NEW.agent_id
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
PERFORM pg_notify('amfs_outcome', json_build_object(
|
|
244
|
+
'namespace', NEW.namespace,
|
|
245
|
+
'outcome_ref', NEW.outcome_ref,
|
|
246
|
+
'outcome_type', NEW.outcome_type,
|
|
247
|
+
'agent_id', NEW.agent_id,
|
|
248
|
+
'causal_confidence', NEW.causal_confidence,
|
|
249
|
+
'attempts', jsonb_array_length(COALESCE(NEW.attempts, '[]'::jsonb))
|
|
250
|
+
)::TEXT);
|
|
251
|
+
|
|
252
|
+
RETURN NEW;
|
|
253
|
+
END;
|
|
254
|
+
$$ LANGUAGE plpgsql;
|
|
@@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS amfs_memory_entries (
|
|
|
22
22
|
last_outcome TEXT,
|
|
23
23
|
last_outcome_at TIMESTAMPTZ,
|
|
24
24
|
discredited_at TIMESTAMPTZ,
|
|
25
|
+
validators JSONB NOT NULL DEFAULT '[]',
|
|
25
26
|
recall_count INTEGER DEFAULT 0,
|
|
26
27
|
priority_score NUMERIC(10,6),
|
|
27
28
|
tier SMALLINT DEFAULT 3,
|
|
@@ -58,7 +59,10 @@ CREATE TABLE IF NOT EXISTS amfs_outcomes (
|
|
|
58
59
|
account_id UUID,
|
|
59
60
|
attempts JSONB NOT NULL DEFAULT '[]',
|
|
60
61
|
causal_entry_versions JSONB NOT NULL DEFAULT '{}',
|
|
61
|
-
final_action_index INTEGER
|
|
62
|
+
final_action_index INTEGER,
|
|
63
|
+
actions_taken JSONB NOT NULL DEFAULT '[]',
|
|
64
|
+
entity_paths TEXT[] NOT NULL DEFAULT '{}',
|
|
65
|
+
situation TEXT
|
|
62
66
|
);
|
|
63
67
|
|
|
64
68
|
-- Range-partitioned by month on created_at. Traces are append-only and read by
|
|
@@ -427,6 +431,10 @@ CREATE INDEX IF NOT EXISTS idx_pr_reviews_pr
|
|
|
427
431
|
CREATE INDEX IF NOT EXISTS idx_entries_discredited
|
|
428
432
|
ON amfs_memory_entries (namespace, entity_path, discredited_at)
|
|
429
433
|
WHERE discredited_at IS NOT NULL AND superseded_at IS NULL;
|
|
434
|
+
CREATE INDEX IF NOT EXISTS idx_outcomes_entity_paths
|
|
435
|
+
ON amfs_outcomes USING gin (entity_paths);
|
|
436
|
+
CREATE INDEX IF NOT EXISTS idx_outcomes_ns_committed
|
|
437
|
+
ON amfs_outcomes (namespace, committed_at DESC);
|
|
430
438
|
|
|
431
439
|
CREATE OR REPLACE FUNCTION amfs_outcome_is_success(t TEXT) RETURNS BOOLEAN AS $$
|
|
432
440
|
SELECT t IN ('success', 'clean_deploy');
|
|
@@ -470,6 +478,7 @@ $$ LANGUAGE sql IMMUTABLE;
|
|
|
470
478
|
-- credit-split over those keys. The trigger calls this once per failed
|
|
471
479
|
-- attempt and once for the terminal outcome.
|
|
472
480
|
DROP FUNCTION IF EXISTS amfs_apply_outcome_step(TEXT, UUID, TEXT[], TEXT, NUMERIC, TEXT);
|
|
481
|
+
DROP FUNCTION IF EXISTS amfs_apply_outcome_step(TEXT, UUID, TEXT[], TEXT, NUMERIC, TEXT, JSONB);
|
|
473
482
|
|
|
474
483
|
CREATE OR REPLACE FUNCTION amfs_apply_outcome_step(
|
|
475
484
|
p_namespace TEXT,
|
|
@@ -478,7 +487,8 @@ CREATE OR REPLACE FUNCTION amfs_apply_outcome_step(
|
|
|
478
487
|
p_outcome_type TEXT,
|
|
479
488
|
p_causal_confidence NUMERIC,
|
|
480
489
|
p_model TEXT,
|
|
481
|
-
p_versions JSONB
|
|
490
|
+
p_versions JSONB,
|
|
491
|
+
p_agent TEXT
|
|
482
492
|
) RETURNS INTEGER AS $$
|
|
483
493
|
DECLARE
|
|
484
494
|
keys TEXT[];
|
|
@@ -497,6 +507,8 @@ DECLARE
|
|
|
497
507
|
e_f NUMERIC;
|
|
498
508
|
new_conf NUMERIC;
|
|
499
509
|
disc TIMESTAMPTZ;
|
|
510
|
+
surprise NUMERIC;
|
|
511
|
+
vals JSONB;
|
|
500
512
|
touched INTEGER := 0;
|
|
501
513
|
BEGIN
|
|
502
514
|
-- Distinct, well-formed specs only; a key cited twice is one citation.
|
|
@@ -558,10 +570,19 @@ BEGIN
|
|
|
558
570
|
e_s := COALESCE(cur.evidence_success, 0) + CASE WHEN is_ok THEN 1 ELSE 0 END;
|
|
559
571
|
e_f := COALESCE(cur.evidence_failure, 0) + CASE WHEN is_ok THEN 0 ELSE 1 END;
|
|
560
572
|
ELSE
|
|
573
|
+
-- First strike: the first failure of an entry with a clean record
|
|
574
|
+
-- of at least three successes is weighed without the surprise
|
|
575
|
+
-- term (amfs_core.evidence.first_strike). One slip on a validated
|
|
576
|
+
-- rule leaves it contested; the second still discredits it.
|
|
577
|
+
surprise := 1.0 + abs(target - LEAST(1.0, GREATEST(0.0, cur.confidence)));
|
|
578
|
+
IF NOT is_ok AND COALESCE(cur.failure_count, 0) = 0
|
|
579
|
+
AND COALESCE(cur.success_count, 0) >= 3 THEN
|
|
580
|
+
surprise := 1.0;
|
|
581
|
+
END IF;
|
|
561
582
|
w := amfs_outcome_severity(p_outcome_type)
|
|
562
583
|
* GREATEST(0.0, p_causal_confidence)
|
|
563
584
|
/ n_keys
|
|
564
|
-
*
|
|
585
|
+
* surprise;
|
|
565
586
|
e_s := COALESCE(cur.evidence_success, 0) * 0.8 + CASE WHEN is_ok THEN w ELSE 0 END;
|
|
566
587
|
e_f := COALESCE(cur.evidence_failure, 0) * 0.8 + CASE WHEN is_ok THEN 0 ELSE w END;
|
|
567
588
|
new_conf := LEAST(1.0, GREATEST(0.0, (2.0 * prior + e_s) / (2.0 + e_s + e_f)));
|
|
@@ -575,6 +596,21 @@ BEGIN
|
|
|
575
596
|
disc := cur.discredited_at;
|
|
576
597
|
END IF;
|
|
577
598
|
|
|
599
|
+
-- Validators: distinct agents whose successes credited this claim,
|
|
600
|
+
-- most recent last, capped at ten (amfs_core.evidence.validators_after).
|
|
601
|
+
vals := COALESCE(cur.validators, '[]'::jsonb);
|
|
602
|
+
IF is_ok AND COALESCE(p_agent, '') <> '' THEN
|
|
603
|
+
vals := vals - p_agent;
|
|
604
|
+
vals := vals || to_jsonb(ARRAY[p_agent]);
|
|
605
|
+
IF jsonb_array_length(vals) > 10 THEN
|
|
606
|
+
SELECT COALESCE(jsonb_agg(v), '[]'::jsonb) INTO vals
|
|
607
|
+
FROM (
|
|
608
|
+
SELECT v FROM jsonb_array_elements(vals) WITH ORDINALITY AS t(v, ord)
|
|
609
|
+
ORDER BY ord OFFSET jsonb_array_length(vals) - 10
|
|
610
|
+
) s;
|
|
611
|
+
END IF;
|
|
612
|
+
END IF;
|
|
613
|
+
|
|
578
614
|
UPDATE amfs_memory_entries
|
|
579
615
|
SET superseded_at = NOW()
|
|
580
616
|
WHERE id = cur.id;
|
|
@@ -596,6 +632,7 @@ BEGIN
|
|
|
596
632
|
'last_outcome', p_outcome_type,
|
|
597
633
|
'last_outcome_at', NOW(),
|
|
598
634
|
'discredited_at', disc,
|
|
635
|
+
'validators', vals,
|
|
599
636
|
'superseded_at', NULL
|
|
600
637
|
)
|
|
601
638
|
);
|
|
@@ -625,7 +662,8 @@ BEGIN
|
|
|
625
662
|
NEW.namespace, NEW.account_id, att_keys,
|
|
626
663
|
COALESCE(att->>'outcome_type', 'minor_failure'),
|
|
627
664
|
NEW.causal_confidence, model,
|
|
628
|
-
COALESCE(att->'causal_entry_versions', '{}'::jsonb)
|
|
665
|
+
COALESCE(att->'causal_entry_versions', '{}'::jsonb),
|
|
666
|
+
NEW.agent_id
|
|
629
667
|
);
|
|
630
668
|
END LOOP;
|
|
631
669
|
|
|
@@ -633,7 +671,8 @@ BEGIN
|
|
|
633
671
|
PERFORM amfs_apply_outcome_step(
|
|
634
672
|
NEW.namespace, NEW.account_id, NEW.causal_entry_keys,
|
|
635
673
|
NEW.outcome_type, NEW.causal_confidence, model,
|
|
636
|
-
COALESCE(NEW.causal_entry_versions, '{}'::jsonb)
|
|
674
|
+
COALESCE(NEW.causal_entry_versions, '{}'::jsonb),
|
|
675
|
+
NEW.agent_id
|
|
637
676
|
);
|
|
638
677
|
|
|
639
678
|
PERFORM pg_notify('amfs_outcome', json_build_object(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/async_adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/retention.py
RENAMED
|
File without changes
|
{amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/tenant_context.py
RENAMED
|
File without changes
|
{amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/tenant_gucs.py
RENAMED
|
File without changes
|
{amfs_adapter_postgres-0.2.16 → amfs_adapter_postgres-0.2.17}/src/amfs_postgres/trace_partitions.py
RENAMED
|
File without changes
|