unique-user-memory 2026.38.0.dev1__tar.gz → 2026.38.0.dev3__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.
- unique_user_memory-2026.38.0.dev1/README.md → unique_user_memory-2026.38.0.dev3/PKG-INFO +67 -1
- unique_user_memory-2026.38.0.dev1/PKG-INFO → unique_user_memory-2026.38.0.dev3/README.md +53 -15
- {unique_user_memory-2026.38.0.dev1 → unique_user_memory-2026.38.0.dev3}/pyproject.toml +3 -3
- unique_user_memory-2026.38.0.dev3/unique_user_memory/metrics.py +229 -0
- unique_user_memory-2026.38.0.dev3/unique_user_memory/tests/test_metrics.py +501 -0
- {unique_user_memory-2026.38.0.dev1 → unique_user_memory-2026.38.0.dev3}/unique_user_memory/tests/test_user_memory.py +259 -3
- {unique_user_memory-2026.38.0.dev1 → unique_user_memory-2026.38.0.dev3}/unique_user_memory/user_memory.py +351 -118
- {unique_user_memory-2026.38.0.dev1 → unique_user_memory-2026.38.0.dev3}/unique_user_memory/user_memory_postprocessor.py +19 -0
- {unique_user_memory-2026.38.0.dev1 → unique_user_memory-2026.38.0.dev3}/unique_user_memory/user_memory_prompts.py +131 -8
- {unique_user_memory-2026.38.0.dev1 → unique_user_memory-2026.38.0.dev3}/unique_user_memory/__init__.py +0 -0
- {unique_user_memory-2026.38.0.dev1 → unique_user_memory-2026.38.0.dev3}/unique_user_memory/config.py +0 -0
- {unique_user_memory-2026.38.0.dev1 → unique_user_memory-2026.38.0.dev3}/unique_user_memory/user_memory_message_log.py +0 -0
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: unique-user-memory
|
|
3
|
+
Version: 2026.38.0.dev3
|
|
4
|
+
Summary:
|
|
5
|
+
Author: Fabian Schläpfer
|
|
6
|
+
Author-email: Fabian Schläpfer <fabian@unique.ch>
|
|
7
|
+
License: Proprietary
|
|
8
|
+
Requires-Dist: jinja2>=3.1.6
|
|
9
|
+
Requires-Dist: pydantic>=2.8.2
|
|
10
|
+
Requires-Dist: unique-sdk>=2026.38.0.dev4,<2026.38.0rc0
|
|
11
|
+
Requires-Dist: unique-toolkit[monitoring]>=2026.38.0.dev14,<2026.38.0rc0
|
|
12
|
+
Requires-Python: >=3.12, <4
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
1
15
|
# Unique User Memory
|
|
2
16
|
|
|
3
17
|
Persistent per-user memory for Unique AI agents.
|
|
@@ -25,7 +39,38 @@ The memory file is intentionally small and structured. It is rewritten as a full
|
|
|
25
39
|
5. `UserMemoryPostprocessor` runs after the assistant response.
|
|
26
40
|
6. The package asks the configured language model to either return `NOOP` or a complete rewritten profile.
|
|
27
41
|
7. If a rewrite runs, an **Updating your memory** Step is shown while consolidating (no settings entry yet).
|
|
28
|
-
8.
|
|
42
|
+
8. Every rewrite passes through a mandatory CID/PII scrub call (see Content Policy below) before it is assembled for upload. When the scrub cannot vouch for the rewrite, the existing memory is kept unchanged.
|
|
43
|
+
9. If the profile changed and `memory.md` uploads successfully (ingestion skipped, content hidden from chat), that Step is completed with a **Review your context memory** detail entry (same settings badge). On NOOP or failed upload the Step completes without the entry.
|
|
44
|
+
|
|
45
|
+
## Content Policy — the current user's CID/PII only (UN-24886)
|
|
46
|
+
|
|
47
|
+
The profile belongs to exactly one person: the signed-in user. It may contain
|
|
48
|
+
personal data (PII) of that user only. It must never contain
|
|
49
|
+
client-identifying data (CID) or PII of any other person or private entity —
|
|
50
|
+
not other users, not clients, not prospects, not counterparties. Facts about
|
|
51
|
+
the user's own work that involve a client are stored only in a fully
|
|
52
|
+
de-identified form ("runs quarterly portfolio reviews", never "reviews the
|
|
53
|
+
portfolio of J. Muster"). Credentials, payment data, health records, and
|
|
54
|
+
government IDs are never stored for anyone, including the user.
|
|
55
|
+
|
|
56
|
+
The policy text lives once, in `_CID_POLICY` in `user_memory_prompts.py`, and
|
|
57
|
+
is embedded in every write-path stage so the rules cannot drift:
|
|
58
|
+
|
|
59
|
+
| Stage | Role of the policy |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| Gate (`memory_gate_system_prompt`) | Turns whose only new facts are third-party CID/PII lean `NOOP` and never reach the rewrite. |
|
|
62
|
+
| Consolidation (`consolidation_system_prompt`) | The rewrite itself must not extract forbidden content. |
|
|
63
|
+
| Condensation (`condensation_system_prompt`) | Oversized (including legacy) profiles must drop violating bullets on every shrink, progressively cleaning old data. |
|
|
64
|
+
| Scrub (`scrub_system_prompt` / `scrub_user_memory`) | Mandatory final gate on every consolidation rewrite. Answers `CLEAN`, returns a cleaned body, or vetoes the write. Fails closed: on any error the existing memory is kept and the unvalidated candidate is discarded. |
|
|
65
|
+
|
|
66
|
+
Storage-level isolation (per-user home folder with exclusive owner ACL, see
|
|
67
|
+
Storage Model) already prevents cross-user *access*; this policy governs
|
|
68
|
+
cross-user and cross-client *content*.
|
|
69
|
+
|
|
70
|
+
Note for follow-up client-memory work: this package is user memory only. A
|
|
71
|
+
future client memory is a separate artefact with its own scoping and must not
|
|
72
|
+
be built by loosening this policy — client-related durable knowledge is
|
|
73
|
+
intentionally rejected here rather than stored under the user's profile.
|
|
29
74
|
|
|
30
75
|
## Storage Model
|
|
31
76
|
|
|
@@ -99,6 +144,27 @@ config = UserMemoryConfig(
|
|
|
99
144
|
| `max_tokens` | `2000` | Maximum profile size. Must be between 500 and 8000 tokens. |
|
|
100
145
|
| `root_folder` | `user-memory` | Subfolder name under each user's home folder (`/home-<user_id>/<root_folder>`) that holds the memory profile; also read as a legacy fallback at `/<root_folder>/<user_id>`. |
|
|
101
146
|
|
|
147
|
+
## Observability
|
|
148
|
+
|
|
149
|
+
The package registers Prometheus metrics on the shared `unique_toolkit.monitoring` registry (namespace `unique_user_memory`). They appear on whatever host already scrapes `GET /metrics` (assistants-core). Labels are closed enums — no company or user ids. LLM series include the model name so latency can be split per deployed model.
|
|
150
|
+
|
|
151
|
+
QA dashboard: [User Memory](https://qa-grafana.alpine-bowfin.ts.net/d/unique-user-memory) (`unique-user-memory`). The dashboard is provisioned by the [assistants-core Helm chart](https://github.com/Unique-AG/monorepo/tree/master/python/assistants/bundles/core/deploy/helm-chart/files/grafana/dashboards).
|
|
152
|
+
|
|
153
|
+
| Metric | Labels | What it answers |
|
|
154
|
+
| --- | --- | --- |
|
|
155
|
+
| `load_duration_seconds` / `load_total` | `outcome`: `success`, `empty`, `folder_failed`, `skipped_no_ids` | Is load slow? How often do we run without memory? |
|
|
156
|
+
| `postprocessor_duration_seconds` / `postprocessor_total` | `outcome`: `updated`, `noop`, `upload_failed`, `skipped_no_ids`, `error` | Did this turn persist a new profile? |
|
|
157
|
+
| `errors_total` | `stage`, `error_type` | Infra failures by stage (`folder_lookup`, `folder_create`, `download`, `upload`, `gate`, `consolidation`, `scrub`, `condense`, `postprocessor`). |
|
|
158
|
+
| `llm_duration_seconds` / `llm_errors_total` | `purpose`: `gate`, `consolidation`, `scrub`, `condense`; `model`: the configured model name | Which LLM call is the latency or error hog, and how does that differ by model? |
|
|
159
|
+
| `gate_decisions_total` | `decision`: `update`, `noop`, `fail_open` | Is the gate saving consolidations? Is fail-open spiking? |
|
|
160
|
+
| `scrub_decisions_total` | `decision`: `clean`, `cleaned`, `veto` | CID/PII pass health. |
|
|
161
|
+
| `consolidation_results_total` | `result`: `rewritten`, `noop`, `malformed`, `llm_error`, `scrub_veto`, `unchanged_after_scrub` | Why a rewrite did not persist. |
|
|
162
|
+
| `storage_duration_seconds` / `storage_total` | `op`: `folder_lookup`, `folder_create`, `download`, `upload`; `outcome`: `success`, `error`, `not_found`, `refused_empty` | Is the content store the bottleneck? |
|
|
163
|
+
| `memory_length_tokens` / `memory_length_chars` | `phase`: `load`, `write` | How large are profiles in tokens and characters versus `max_tokens`? |
|
|
164
|
+
| `condense_total` | `trigger`: `load`, `post_consolidation`, `condense`; `result`: `llm_ok`, `hard_cut`, `llm_failed_then_hard_cut` | How often the budget shrink path fires. |
|
|
165
|
+
|
|
166
|
+
Token usage for billing still flows through `LanguageModelInvocationStats`, not these series.
|
|
167
|
+
|
|
102
168
|
## Integration
|
|
103
169
|
|
|
104
170
|
Typical orchestration code loads memory before the agent loop and registers the postprocessor for the same turn.
|
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: unique-user-memory
|
|
3
|
-
Version: 2026.38.0.dev1
|
|
4
|
-
Summary:
|
|
5
|
-
Author: Fabian Schläpfer
|
|
6
|
-
Author-email: Fabian Schläpfer <fabian@unique.ch>
|
|
7
|
-
License: Proprietary
|
|
8
|
-
Requires-Dist: jinja2>=3.1.6
|
|
9
|
-
Requires-Dist: pydantic>=2.8.2
|
|
10
|
-
Requires-Dist: unique-sdk>=2026.38.0.dev1,<2026.38.0rc0
|
|
11
|
-
Requires-Dist: unique-toolkit>=2026.38.0.dev5,<2026.38.0rc0
|
|
12
|
-
Requires-Python: >=3.12, <4
|
|
13
|
-
Description-Content-Type: text/markdown
|
|
14
|
-
|
|
15
1
|
# Unique User Memory
|
|
16
2
|
|
|
17
3
|
Persistent per-user memory for Unique AI agents.
|
|
@@ -39,7 +25,38 @@ The memory file is intentionally small and structured. It is rewritten as a full
|
|
|
39
25
|
5. `UserMemoryPostprocessor` runs after the assistant response.
|
|
40
26
|
6. The package asks the configured language model to either return `NOOP` or a complete rewritten profile.
|
|
41
27
|
7. If a rewrite runs, an **Updating your memory** Step is shown while consolidating (no settings entry yet).
|
|
42
|
-
8.
|
|
28
|
+
8. Every rewrite passes through a mandatory CID/PII scrub call (see Content Policy below) before it is assembled for upload. When the scrub cannot vouch for the rewrite, the existing memory is kept unchanged.
|
|
29
|
+
9. If the profile changed and `memory.md` uploads successfully (ingestion skipped, content hidden from chat), that Step is completed with a **Review your context memory** detail entry (same settings badge). On NOOP or failed upload the Step completes without the entry.
|
|
30
|
+
|
|
31
|
+
## Content Policy — the current user's CID/PII only (UN-24886)
|
|
32
|
+
|
|
33
|
+
The profile belongs to exactly one person: the signed-in user. It may contain
|
|
34
|
+
personal data (PII) of that user only. It must never contain
|
|
35
|
+
client-identifying data (CID) or PII of any other person or private entity —
|
|
36
|
+
not other users, not clients, not prospects, not counterparties. Facts about
|
|
37
|
+
the user's own work that involve a client are stored only in a fully
|
|
38
|
+
de-identified form ("runs quarterly portfolio reviews", never "reviews the
|
|
39
|
+
portfolio of J. Muster"). Credentials, payment data, health records, and
|
|
40
|
+
government IDs are never stored for anyone, including the user.
|
|
41
|
+
|
|
42
|
+
The policy text lives once, in `_CID_POLICY` in `user_memory_prompts.py`, and
|
|
43
|
+
is embedded in every write-path stage so the rules cannot drift:
|
|
44
|
+
|
|
45
|
+
| Stage | Role of the policy |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| Gate (`memory_gate_system_prompt`) | Turns whose only new facts are third-party CID/PII lean `NOOP` and never reach the rewrite. |
|
|
48
|
+
| Consolidation (`consolidation_system_prompt`) | The rewrite itself must not extract forbidden content. |
|
|
49
|
+
| Condensation (`condensation_system_prompt`) | Oversized (including legacy) profiles must drop violating bullets on every shrink, progressively cleaning old data. |
|
|
50
|
+
| Scrub (`scrub_system_prompt` / `scrub_user_memory`) | Mandatory final gate on every consolidation rewrite. Answers `CLEAN`, returns a cleaned body, or vetoes the write. Fails closed: on any error the existing memory is kept and the unvalidated candidate is discarded. |
|
|
51
|
+
|
|
52
|
+
Storage-level isolation (per-user home folder with exclusive owner ACL, see
|
|
53
|
+
Storage Model) already prevents cross-user *access*; this policy governs
|
|
54
|
+
cross-user and cross-client *content*.
|
|
55
|
+
|
|
56
|
+
Note for follow-up client-memory work: this package is user memory only. A
|
|
57
|
+
future client memory is a separate artefact with its own scoping and must not
|
|
58
|
+
be built by loosening this policy — client-related durable knowledge is
|
|
59
|
+
intentionally rejected here rather than stored under the user's profile.
|
|
43
60
|
|
|
44
61
|
## Storage Model
|
|
45
62
|
|
|
@@ -113,6 +130,27 @@ config = UserMemoryConfig(
|
|
|
113
130
|
| `max_tokens` | `2000` | Maximum profile size. Must be between 500 and 8000 tokens. |
|
|
114
131
|
| `root_folder` | `user-memory` | Subfolder name under each user's home folder (`/home-<user_id>/<root_folder>`) that holds the memory profile; also read as a legacy fallback at `/<root_folder>/<user_id>`. |
|
|
115
132
|
|
|
133
|
+
## Observability
|
|
134
|
+
|
|
135
|
+
The package registers Prometheus metrics on the shared `unique_toolkit.monitoring` registry (namespace `unique_user_memory`). They appear on whatever host already scrapes `GET /metrics` (assistants-core). Labels are closed enums — no company or user ids. LLM series include the model name so latency can be split per deployed model.
|
|
136
|
+
|
|
137
|
+
QA dashboard: [User Memory](https://qa-grafana.alpine-bowfin.ts.net/d/unique-user-memory) (`unique-user-memory`). The dashboard is provisioned by the [assistants-core Helm chart](https://github.com/Unique-AG/monorepo/tree/master/python/assistants/bundles/core/deploy/helm-chart/files/grafana/dashboards).
|
|
138
|
+
|
|
139
|
+
| Metric | Labels | What it answers |
|
|
140
|
+
| --- | --- | --- |
|
|
141
|
+
| `load_duration_seconds` / `load_total` | `outcome`: `success`, `empty`, `folder_failed`, `skipped_no_ids` | Is load slow? How often do we run without memory? |
|
|
142
|
+
| `postprocessor_duration_seconds` / `postprocessor_total` | `outcome`: `updated`, `noop`, `upload_failed`, `skipped_no_ids`, `error` | Did this turn persist a new profile? |
|
|
143
|
+
| `errors_total` | `stage`, `error_type` | Infra failures by stage (`folder_lookup`, `folder_create`, `download`, `upload`, `gate`, `consolidation`, `scrub`, `condense`, `postprocessor`). |
|
|
144
|
+
| `llm_duration_seconds` / `llm_errors_total` | `purpose`: `gate`, `consolidation`, `scrub`, `condense`; `model`: the configured model name | Which LLM call is the latency or error hog, and how does that differ by model? |
|
|
145
|
+
| `gate_decisions_total` | `decision`: `update`, `noop`, `fail_open` | Is the gate saving consolidations? Is fail-open spiking? |
|
|
146
|
+
| `scrub_decisions_total` | `decision`: `clean`, `cleaned`, `veto` | CID/PII pass health. |
|
|
147
|
+
| `consolidation_results_total` | `result`: `rewritten`, `noop`, `malformed`, `llm_error`, `scrub_veto`, `unchanged_after_scrub` | Why a rewrite did not persist. |
|
|
148
|
+
| `storage_duration_seconds` / `storage_total` | `op`: `folder_lookup`, `folder_create`, `download`, `upload`; `outcome`: `success`, `error`, `not_found`, `refused_empty` | Is the content store the bottleneck? |
|
|
149
|
+
| `memory_length_tokens` / `memory_length_chars` | `phase`: `load`, `write` | How large are profiles in tokens and characters versus `max_tokens`? |
|
|
150
|
+
| `condense_total` | `trigger`: `load`, `post_consolidation`, `condense`; `result`: `llm_ok`, `hard_cut`, `llm_failed_then_hard_cut` | How often the budget shrink path fires. |
|
|
151
|
+
|
|
152
|
+
Token usage for billing still flows through `LanguageModelInvocationStats`, not these series.
|
|
153
|
+
|
|
116
154
|
## Integration
|
|
117
155
|
|
|
118
156
|
Typical orchestration code loads memory before the agent loop and registers the postprocessor for the same turn.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "unique_user_memory"
|
|
3
|
-
version = "2026.38.0.
|
|
3
|
+
version = "2026.38.0.dev3"
|
|
4
4
|
description = ""
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Fabian Schläpfer", email = "fabian@unique.ch" },
|
|
@@ -11,8 +11,8 @@ requires-python = ">=3.12,<4"
|
|
|
11
11
|
dependencies = [
|
|
12
12
|
"jinja2>=3.1.6",
|
|
13
13
|
"pydantic>=2.8.2",
|
|
14
|
-
"unique-sdk>=2026.38.0.
|
|
15
|
-
"unique-toolkit>=2026.38.0.
|
|
14
|
+
"unique-sdk>=2026.38.0.dev4,<2026.38.0rc0",
|
|
15
|
+
"unique-toolkit[monitoring]>=2026.38.0.dev14,<2026.38.0rc0",
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
[dependency-groups]
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
from unique_toolkit.monitoring import MetricNamespace
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _ObservesSeconds(Protocol):
|
|
12
|
+
def observe(self, amount: float) -> None: ...
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
m = MetricNamespace("unique_user_memory")
|
|
16
|
+
|
|
17
|
+
_DURATION_BUCKETS = (0.1, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 30.0, 60.0)
|
|
18
|
+
_TOKEN_BUCKETS = (
|
|
19
|
+
0.0,
|
|
20
|
+
100.0,
|
|
21
|
+
250.0,
|
|
22
|
+
500.0,
|
|
23
|
+
1000.0,
|
|
24
|
+
1500.0,
|
|
25
|
+
2000.0,
|
|
26
|
+
3000.0,
|
|
27
|
+
5000.0,
|
|
28
|
+
8000.0,
|
|
29
|
+
)
|
|
30
|
+
_CHAR_BUCKETS = (
|
|
31
|
+
0.0,
|
|
32
|
+
200.0,
|
|
33
|
+
500.0,
|
|
34
|
+
1000.0,
|
|
35
|
+
2000.0,
|
|
36
|
+
4000.0,
|
|
37
|
+
8000.0,
|
|
38
|
+
16000.0,
|
|
39
|
+
32000.0,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# End-to-end
|
|
43
|
+
load_duration = m.histogram(
|
|
44
|
+
"load_duration_seconds",
|
|
45
|
+
"load_user_memory() duration",
|
|
46
|
+
[],
|
|
47
|
+
buckets=_DURATION_BUCKETS,
|
|
48
|
+
)
|
|
49
|
+
load_total = m.counter(
|
|
50
|
+
"load_total",
|
|
51
|
+
"Memory load attempts by outcome",
|
|
52
|
+
["outcome"],
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
postprocessor_duration = m.histogram(
|
|
56
|
+
"postprocessor_duration_seconds",
|
|
57
|
+
"UserMemoryPostprocessor.run() duration",
|
|
58
|
+
[],
|
|
59
|
+
buckets=_DURATION_BUCKETS,
|
|
60
|
+
)
|
|
61
|
+
postprocessor_total = m.counter(
|
|
62
|
+
"postprocessor_total",
|
|
63
|
+
"Postprocessor runs by outcome",
|
|
64
|
+
["outcome"],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
errors = m.counter(
|
|
68
|
+
"errors_total",
|
|
69
|
+
"User memory infrastructure failures",
|
|
70
|
+
["stage", "error_type"],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# LLM
|
|
74
|
+
llm_duration = m.histogram(
|
|
75
|
+
"llm_duration_seconds",
|
|
76
|
+
"User memory LLM call duration",
|
|
77
|
+
["purpose", "model"],
|
|
78
|
+
buckets=_DURATION_BUCKETS,
|
|
79
|
+
)
|
|
80
|
+
llm_errors = m.counter(
|
|
81
|
+
"llm_errors_total",
|
|
82
|
+
"User memory LLM call failures",
|
|
83
|
+
["purpose", "model", "error_type"],
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Decisions
|
|
87
|
+
gate_decisions = m.counter(
|
|
88
|
+
"gate_decisions_total",
|
|
89
|
+
"Consolidation gate decisions",
|
|
90
|
+
["decision"],
|
|
91
|
+
)
|
|
92
|
+
scrub_decisions = m.counter(
|
|
93
|
+
"scrub_decisions_total",
|
|
94
|
+
"CID/PII scrub decisions",
|
|
95
|
+
["decision"],
|
|
96
|
+
)
|
|
97
|
+
consolidation_results = m.counter(
|
|
98
|
+
"consolidation_results_total",
|
|
99
|
+
"Full-profile rewrite results",
|
|
100
|
+
["result"],
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Storage
|
|
104
|
+
storage_duration = m.histogram(
|
|
105
|
+
"storage_duration_seconds",
|
|
106
|
+
"Folder and content store latency",
|
|
107
|
+
["op"],
|
|
108
|
+
buckets=_DURATION_BUCKETS,
|
|
109
|
+
)
|
|
110
|
+
storage_total = m.counter(
|
|
111
|
+
"storage_total",
|
|
112
|
+
"Folder and content store calls",
|
|
113
|
+
["op", "outcome"],
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Size / budget
|
|
117
|
+
memory_length_tokens = m.histogram(
|
|
118
|
+
"memory_length_tokens",
|
|
119
|
+
"Memory profile size in tokens",
|
|
120
|
+
["phase"],
|
|
121
|
+
buckets=_TOKEN_BUCKETS,
|
|
122
|
+
)
|
|
123
|
+
memory_length_chars = m.histogram(
|
|
124
|
+
"memory_length_chars",
|
|
125
|
+
"Memory profile size in characters",
|
|
126
|
+
["phase"],
|
|
127
|
+
buckets=_CHAR_BUCKETS,
|
|
128
|
+
)
|
|
129
|
+
condense_total = m.counter(
|
|
130
|
+
"condense_total",
|
|
131
|
+
"Over-budget condensation attempts",
|
|
132
|
+
["trigger", "result"],
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def record_error(stage: str, exc: BaseException) -> None:
|
|
137
|
+
errors.labels(stage=stage, error_type=type(exc).__name__).inc()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def record_load(outcome: str) -> None:
|
|
141
|
+
load_total.labels(outcome=outcome).inc()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def record_postprocessor(outcome: str) -> None:
|
|
145
|
+
postprocessor_total.labels(outcome=outcome).inc()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def record_gate(decision: str) -> None:
|
|
149
|
+
gate_decisions.labels(decision=decision).inc()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def record_scrub(decision: str) -> None:
|
|
153
|
+
scrub_decisions.labels(decision=decision).inc()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def record_consolidation(result: str) -> None:
|
|
157
|
+
consolidation_results.labels(result=result).inc()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def record_storage(op: str, outcome: str) -> None:
|
|
161
|
+
storage_total.labels(op=op, outcome=outcome).inc()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def record_condense(trigger: str, result: str) -> None:
|
|
165
|
+
condense_total.labels(trigger=trigger, result=result).inc()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def model_label(model_name: object) -> str:
|
|
169
|
+
"""Stable Prometheus label for a language-model name (enum or string)."""
|
|
170
|
+
value = getattr(model_name, "value", model_name)
|
|
171
|
+
return str(value)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def observe_memory_length(*, phase: str, content: str, tokens: float) -> None:
|
|
175
|
+
memory_length_tokens.labels(phase=phase).observe(tokens)
|
|
176
|
+
memory_length_chars.labels(phase=phase).observe(len(content))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def condense_trigger(invocation_source: str) -> str:
|
|
180
|
+
if invocation_source == "user_memory_load_condense":
|
|
181
|
+
return "load"
|
|
182
|
+
if invocation_source == "user_memory_post_consolidation_condense":
|
|
183
|
+
return "post_consolidation"
|
|
184
|
+
return "condense"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@contextmanager
|
|
188
|
+
def observe_unlabeled(histogram: _ObservesSeconds) -> Iterator[None]:
|
|
189
|
+
"""Time a block on a histogram that has no labels.
|
|
190
|
+
|
|
191
|
+
``metric_scope`` always calls ``labels()``, which prometheus_client
|
|
192
|
+
rejects when the instrument was declared with an empty label set.
|
|
193
|
+
"""
|
|
194
|
+
start = time.perf_counter()
|
|
195
|
+
try:
|
|
196
|
+
yield
|
|
197
|
+
finally:
|
|
198
|
+
histogram.observe(time.perf_counter() - start)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
__all__ = [
|
|
202
|
+
"condense_total",
|
|
203
|
+
"condense_trigger",
|
|
204
|
+
"consolidation_results",
|
|
205
|
+
"errors",
|
|
206
|
+
"gate_decisions",
|
|
207
|
+
"llm_duration",
|
|
208
|
+
"llm_errors",
|
|
209
|
+
"load_duration",
|
|
210
|
+
"load_total",
|
|
211
|
+
"memory_length_chars",
|
|
212
|
+
"memory_length_tokens",
|
|
213
|
+
"model_label",
|
|
214
|
+
"observe_memory_length",
|
|
215
|
+
"observe_unlabeled",
|
|
216
|
+
"postprocessor_duration",
|
|
217
|
+
"postprocessor_total",
|
|
218
|
+
"record_condense",
|
|
219
|
+
"record_consolidation",
|
|
220
|
+
"record_error",
|
|
221
|
+
"record_gate",
|
|
222
|
+
"record_load",
|
|
223
|
+
"record_postprocessor",
|
|
224
|
+
"record_scrub",
|
|
225
|
+
"record_storage",
|
|
226
|
+
"scrub_decisions",
|
|
227
|
+
"storage_duration",
|
|
228
|
+
"storage_total",
|
|
229
|
+
]
|