unique-user-memory 2026.30.0.dev2__tar.gz → 2026.32.0.dev1__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.
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-user-memory
3
- Version: 2026.30.0.dev2
3
+ Version: 2026.32.0.dev1
4
4
  Summary:
5
5
  Author: Fabian Schläpfer
6
6
  Author-email: Fabian Schläpfer <fabian@unique.ch>
7
7
  License: Proprietary
8
8
  Requires-Dist: jinja2>=3.1.6
9
9
  Requires-Dist: pydantic>=2.8.2
10
- Requires-Dist: unique-sdk>=2026.30.0.dev9,<2026.30.0rc0
11
- Requires-Dist: unique-toolkit>=2026.30.0.dev22,<2026.30.0rc0
10
+ Requires-Dist: unique-sdk>=2026.32.0.dev0,<2026.32.0rc0
11
+ Requires-Dist: unique-toolkit>=2026.32.0.dev0,<2026.32.0rc0
12
12
  Requires-Python: >=3.12, <4
13
13
  Description-Content-Type: text/markdown
14
14
 
@@ -16,7 +16,7 @@ Description-Content-Type: text/markdown
16
16
 
17
17
  Persistent per-user memory for Unique AI agents.
18
18
 
19
- `unique_user_memory` stores a compact Markdown profile for each user and updates it after every agent turn. The profile is loaded before the next turn so the assistant can remember stable user context such as communication preferences, work context, expertise, recent topics, and open follow-ups.
19
+ `unique_user_memory` stores a compact Markdown profile for each user and updates it after every agent turn. The profile is loaded before the next turn so the assistant can remember stable user context such as communication preferences, work context, expertise, recent topics, and concrete future tasks.
20
20
 
21
21
  ## What It Does
22
22
 
@@ -76,7 +76,7 @@ _(empty)_
76
76
  ## Recent Topics
77
77
  _(empty)_
78
78
 
79
- ## Open Questions / Follow-ups
79
+ ## Follow-ups
80
80
  _(empty)_
81
81
  ```
82
82
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Persistent per-user memory for Unique AI agents.
4
4
 
5
- `unique_user_memory` stores a compact Markdown profile for each user and updates it after every agent turn. The profile is loaded before the next turn so the assistant can remember stable user context such as communication preferences, work context, expertise, recent topics, and open follow-ups.
5
+ `unique_user_memory` stores a compact Markdown profile for each user and updates it after every agent turn. The profile is loaded before the next turn so the assistant can remember stable user context such as communication preferences, work context, expertise, recent topics, and concrete future tasks.
6
6
 
7
7
  ## What It Does
8
8
 
@@ -62,7 +62,7 @@ _(empty)_
62
62
  ## Recent Topics
63
63
  _(empty)_
64
64
 
65
- ## Open Questions / Follow-ups
65
+ ## Follow-ups
66
66
  _(empty)_
67
67
  ```
68
68
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique_user_memory"
3
- version = "2026.30.0.dev2"
3
+ version = "2026.32.0.dev1"
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.30.0.dev9,<2026.30.0rc0",
15
- "unique-toolkit>=2026.30.0.dev22,<2026.30.0rc0",
14
+ "unique-sdk>=2026.32.0.dev0,<2026.32.0rc0",
15
+ "unique-toolkit>=2026.32.0.dev0,<2026.32.0rc0",
16
16
  ]
17
17
 
18
18
  [dependency-groups]
@@ -23,11 +23,26 @@ from unique_user_memory.user_memory import (
23
23
  upload_user_memory,
24
24
  )
25
25
  from unique_user_memory.user_memory_postprocessor import UserMemoryPostprocessor
26
- from unique_user_memory.user_memory_prompts import empty_profile
26
+ from unique_user_memory.user_memory_prompts import (
27
+ consolidation_system_prompt,
28
+ empty_profile,
29
+ memory_gate_system_prompt,
30
+ )
27
31
 
28
32
  _TEST_LANGUAGE_MODEL = LanguageModelInfo.from_name(DEFAULT_LANGUAGE_MODEL)
29
33
 
30
34
 
35
+ def test_memory_profile_keeps_follow_up_tasks_but_excludes_open_questions() -> None:
36
+ profile = empty_profile("user_1")
37
+ consolidation_prompt = consolidation_system_prompt(2000)
38
+ gate_prompt = memory_gate_system_prompt()
39
+
40
+ assert "## Open Questions / Follow-ups" not in profile
41
+ assert "## Follow-ups" in profile
42
+ assert "concrete tasks the user intends to complete" in consolidation_prompt
43
+ assert "Concrete future tasks the user intends to complete" in gate_prompt
44
+
45
+
31
46
  def test_enforce_token_cap_truncates_long_content() -> None:
32
47
  content = "\n\n".join(f"paragraph {index} " + "word " * 40 for index in range(50))
33
48
 
@@ -144,6 +159,35 @@ async def test_condense_user_memory_rejects_non_profile_output(
144
159
  assert result is None
145
160
 
146
161
 
162
+ @pytest.mark.ai
163
+ @pytest.mark.asyncio
164
+ async def test_condense_user_memory_accepts_frontmatter_output(
165
+ monkeypatch: pytest.MonkeyPatch,
166
+ ) -> None:
167
+ """Accept legacy LLM output while returning only the condensed profile body."""
168
+ condensed = "# User Memory\n\n## Identity\n- concise summary"
169
+ response = MagicMock()
170
+ response.choices[0].message.content = (
171
+ "---\nuser_id: stale-user\nturn_count: 99\n---\n\n" + condensed
172
+ )
173
+ llm_service = MagicMock()
174
+ llm_service.complete_async = AsyncMock(return_value=response)
175
+ monkeypatch.setattr(
176
+ "unique_user_memory.user_memory.LanguageModelService",
177
+ MagicMock(return_value=llm_service),
178
+ )
179
+
180
+ result = await condense_user_memory(
181
+ content="# User Memory\n\n## Identity\n- lots of stuff",
182
+ max_tokens=2000,
183
+ language_model=_TEST_LANGUAGE_MODEL,
184
+ event=MagicMock(),
185
+ logger=MagicMock(),
186
+ )
187
+
188
+ assert result == condensed
189
+
190
+
147
191
  def test_count_tokens_uses_language_model_encoder() -> None:
148
192
  language_model = MagicMock()
149
193
  language_model.get_encoder.return_value = lambda content: content.split()
@@ -281,10 +325,86 @@ async def test_consolidate_user_memory_runs_full_rewrite_when_gate_update(
281
325
  logger=MagicMock(),
282
326
  )
283
327
 
284
- assert result == rewritten
328
+ assert result.endswith(f"{rewritten}\n")
329
+ assert "user_id: user_1" in result
330
+ assert "schema_version: 1" in result
331
+ assert "turn_count: 1" in result
285
332
  llm_service.complete_async.assert_awaited_once()
286
333
 
287
334
 
335
+ @pytest.mark.asyncio
336
+ async def test_consolidate_user_memory_adds_frontmatter_to_llm_body(
337
+ monkeypatch: pytest.MonkeyPatch,
338
+ ) -> None:
339
+ rewritten = "# User Memory\n\n## Identity\n- Prefers concise answers"
340
+ response = MagicMock()
341
+ response.choices[0].message.content = rewritten
342
+ llm_service = MagicMock()
343
+ llm_service.complete_async = AsyncMock(return_value=response)
344
+ monkeypatch.setattr(
345
+ "unique_user_memory.user_memory.LanguageModelService",
346
+ MagicMock(return_value=llm_service),
347
+ )
348
+ monkeypatch.setattr(
349
+ "unique_user_memory.user_memory.should_consolidate_memory",
350
+ AsyncMock(return_value=True),
351
+ )
352
+
353
+ result = await consolidate_user_memory(
354
+ current_memory="",
355
+ user_id="authenticated-user",
356
+ user_message="remember I like concise answers",
357
+ assistant_message="noted",
358
+ config=UserMemoryConfig(),
359
+ language_model=_TEST_LANGUAGE_MODEL,
360
+ event=MagicMock(),
361
+ logger=MagicMock(),
362
+ )
363
+
364
+ assert "user_id: authenticated-user" in result
365
+ assert "schema_version: 1" in result
366
+ assert "turn_count: 1" in result
367
+
368
+
369
+ @pytest.mark.ai
370
+ @pytest.mark.asyncio
371
+ async def test_consolidate_user_memory_replaces_llm_frontmatter(
372
+ monkeypatch: pytest.MonkeyPatch,
373
+ ) -> None:
374
+ """Strip untrusted legacy metadata before assembling the updated profile."""
375
+ rewritten = "# User Memory\n\n## Identity\n- Prefers concise answers"
376
+ response = MagicMock()
377
+ response.choices[0].message.content = (
378
+ "---\nuser_id: stale-user\nturn_count: 99\n---\n\n" + rewritten
379
+ )
380
+ llm_service = MagicMock()
381
+ llm_service.complete_async = AsyncMock(return_value=response)
382
+ monkeypatch.setattr(
383
+ "unique_user_memory.user_memory.LanguageModelService",
384
+ MagicMock(return_value=llm_service),
385
+ )
386
+ monkeypatch.setattr(
387
+ "unique_user_memory.user_memory.should_consolidate_memory",
388
+ AsyncMock(return_value=True),
389
+ )
390
+
391
+ result = await consolidate_user_memory(
392
+ current_memory=empty_profile("authenticated-user"),
393
+ user_id="authenticated-user",
394
+ user_message="remember I like concise answers",
395
+ assistant_message="noted",
396
+ config=UserMemoryConfig(),
397
+ language_model=_TEST_LANGUAGE_MODEL,
398
+ event=MagicMock(),
399
+ logger=MagicMock(),
400
+ )
401
+
402
+ assert result.endswith(f"{rewritten}\n")
403
+ assert "user_id: authenticated-user" in result
404
+ assert "stale-user" not in result
405
+ assert "turn_count: 1" in result
406
+
407
+
288
408
  @pytest.mark.asyncio
289
409
  async def test_consolidate_user_memory_skips_gate_when_disabled(
290
410
  monkeypatch: pytest.MonkeyPatch,
@@ -316,7 +436,9 @@ async def test_consolidate_user_memory_skips_gate_when_disabled(
316
436
  logger=MagicMock(),
317
437
  )
318
438
 
319
- assert result == rewritten
439
+ assert result.endswith(f"{rewritten}\n")
440
+ assert "user_id: user_1" in result
441
+ assert "turn_count: 1" in result
320
442
  gate.assert_not_awaited()
321
443
  llm_service.complete_async.assert_awaited_once()
322
444
 
@@ -434,7 +556,9 @@ async def test_consolidate_user_memory_invokes_update_callbacks_on_rewrite(
434
556
  on_update_end=on_end,
435
557
  )
436
558
 
437
- assert result == rewritten
559
+ assert result.endswith(f"{rewritten}\n")
560
+ assert "user_id: user_1" in result
561
+ assert "turn_count: 1" in result
438
562
  on_start.assert_awaited_once()
439
563
  on_end.assert_awaited_once()
440
564
  assert events == ["start", "end"]
@@ -1,6 +1,7 @@
1
1
  import re
2
2
  from collections.abc import Awaitable, Callable
3
3
  from dataclasses import dataclass
4
+ from datetime import datetime, timezone
4
5
  from logging import Logger
5
6
 
6
7
  import unique_sdk
@@ -56,6 +57,40 @@ _CONDENSE_TARGET_RATIO = 0.9
56
57
  _TRUNCATION_MARKER = "\n\n<!-- truncated to fit memory budget -->"
57
58
  _DEFAULT_LANGUAGE_MODEL = LanguageModelInfo.from_name(DEFAULT_GPT_4o)
58
59
  _FRONTMATTER_RE = re.compile(r"^---\n.*?\n---\n", re.DOTALL)
60
+ _TURN_COUNT_RE = re.compile(r"^turn_count:\s*(\d+)\s*$", re.MULTILINE)
61
+
62
+
63
+ def _profile_body(content: str) -> str:
64
+ return _FRONTMATTER_RE.sub("", content, count=1).strip()
65
+
66
+
67
+ def _turn_count(content: str) -> int:
68
+ frontmatter_match = _FRONTMATTER_RE.match(content)
69
+ if frontmatter_match is None:
70
+ return 0
71
+ match = _TURN_COUNT_RE.search(frontmatter_match.group(0))
72
+ return int(match.group(1)) if match else 0
73
+
74
+
75
+ def _assemble_profile(*, body: str, user_id: str, turn_count: int) -> str:
76
+ """Prefix an LLM-generated body with trusted application metadata."""
77
+ timestamp = datetime.now(timezone.utc).isoformat(timespec="seconds")
78
+ return (
79
+ "---\n"
80
+ f"user_id: {user_id}\n"
81
+ "schema_version: 1\n"
82
+ f"last_updated: {timestamp}\n"
83
+ f"turn_count: {turn_count}\n"
84
+ "---\n\n"
85
+ f"{body.strip()}\n"
86
+ )
87
+
88
+
89
+ def _restore_frontmatter(original: str, body: str) -> str:
90
+ match = _FRONTMATTER_RE.match(original)
91
+ if match is None:
92
+ return body
93
+ return f"{match.group(0).rstrip()}\n\n{body.strip()}\n"
59
94
 
60
95
 
61
96
  @dataclass(frozen=True)
@@ -154,7 +189,8 @@ async def condense_user_memory(
154
189
  condensed profile, or ``None`` when the call fails or the output does
155
190
  not look like a profile (the caller then falls back to a hard cut).
156
191
  """
157
- current_tokens = count_tokens(content=content, language_model=language_model)
192
+ body = _profile_body(content)
193
+ current_tokens = count_tokens(content=body, language_model=language_model)
158
194
  target_tokens = max(1, int(max_tokens * _CONDENSE_TARGET_RATIO))
159
195
 
160
196
  try:
@@ -177,7 +213,7 @@ async def condense_user_memory(
177
213
  )
178
214
  ),
179
215
  LanguageModelUserMessage(
180
- content=condensation_user_prompt(_sanitize_for_xml_context(content))
216
+ content=condensation_user_prompt(_sanitize_for_xml_context(body))
181
217
  ),
182
218
  ]
183
219
  )
@@ -214,7 +250,7 @@ async def condense_user_memory(
214
250
  )
215
251
  return None
216
252
 
217
- candidate = _strip_code_fences(raw).strip()
253
+ candidate = _profile_body(_strip_code_fences(raw))
218
254
  if not _is_well_formed_profile(candidate):
219
255
  logger.warning(
220
256
  "[user-memory] condense output did not look like a profile (%d chars)",
@@ -259,6 +295,7 @@ async def fit_user_memory(
259
295
  logger=logger,
260
296
  )
261
297
  if condensed is not None:
298
+ condensed = _restore_frontmatter(content, condensed)
262
299
  condensed_tokens = count_tokens(
263
300
  content=condensed, language_model=language_model
264
301
  )
@@ -752,8 +789,7 @@ async def _rewrite_user_memory(
752
789
  ),
753
790
  LanguageModelUserMessage(
754
791
  content=consolidation_user_prompt(
755
- user_id=user_id,
756
- existing_memory=safe_current,
792
+ existing_memory=_profile_body(safe_current),
757
793
  user_message=_sanitize_for_xml_context(user_message or ""),
758
794
  assistant_message=_sanitize_for_xml_context(
759
795
  assistant_message or ""
@@ -801,14 +837,23 @@ async def _rewrite_user_memory(
801
837
  logger.info("[user-memory] consolidation NOOP - keeping existing memory")
802
838
  return safe_current
803
839
 
804
- candidate = _strip_code_fences(raw).strip()
805
- if not _is_well_formed_profile(candidate):
840
+ candidate_body = _profile_body(_strip_code_fences(raw))
841
+ if not _is_well_formed_profile(candidate_body):
806
842
  logger.warning(
807
843
  "[user-memory] LLM output did not look like a profile (%d chars)",
808
- len(candidate),
844
+ len(candidate_body),
809
845
  )
810
846
  return safe_current
811
847
 
848
+ if safe_current and candidate_body == _profile_body(safe_current):
849
+ logger.debug("[user-memory] memory body unchanged - skipping update")
850
+ return safe_current
851
+
852
+ candidate = _assemble_profile(
853
+ body=candidate_body,
854
+ user_id=user_id,
855
+ turn_count=_turn_count(safe_current) + 1,
856
+ )
812
857
  capped = await fit_user_memory(
813
858
  content=candidate,
814
859
  max_tokens=config.max_tokens,
@@ -816,14 +861,6 @@ async def _rewrite_user_memory(
816
861
  event=event,
817
862
  logger=logger,
818
863
  )
819
- if (
820
- safe_current
821
- and _FRONTMATTER_RE.sub("", capped).strip()
822
- == _FRONTMATTER_RE.sub("", safe_current).strip()
823
- ):
824
- logger.debug("[user-memory] memory body unchanged - skipping update")
825
- return safe_current
826
-
827
864
  logger.info(
828
865
  "[user-memory] consolidation produced %d tokens (cap=%d)",
829
866
  count_tokens(content=capped, language_model=language_model),
@@ -839,9 +876,7 @@ def _sanitize_for_xml_context(text: str) -> str:
839
876
  def _is_well_formed_profile(content: str) -> bool:
840
877
  if not content or len(content.strip()) < 20:
841
878
  return False
842
- if _FRONTMATTER_RE.match(content):
843
- return True
844
- return "## Identity" in content or "# User Memory" in content
879
+ return content.startswith("# User Memory") and "## Identity" in content
845
880
 
846
881
 
847
882
  def _strip_code_fences(text: str) -> str:
@@ -8,7 +8,7 @@ SECTION_HEADINGS: tuple[str, ...] = (
8
8
  "Work Context",
9
9
  "Skills & Expertise",
10
10
  "Recent Topics",
11
- "Open Questions / Follow-ups",
11
+ "Follow-ups",
12
12
  )
13
13
 
14
14
  _EMPTY_PROFILE_TEMPLATE = """\
@@ -52,16 +52,16 @@ small mistakes in extraction compound across every future conversation.
52
52
 
53
53
  You receive two XML blocks:
54
54
 
55
- 1. `<existing_memory>` - the current profile file (Markdown, with YAML
56
- frontmatter). May be empty on the user's first turn.
55
+ 1. `<existing_memory>` - the current Markdown profile body. May be empty
56
+ on the user's first turn.
57
57
  2. `<new_turn>` - the most recent user message and the assistant's
58
58
  reply, prefixed with `user:` and `assistant:`.
59
59
 
60
60
  # Output
61
61
 
62
- Return the complete, rewritten profile file as Markdown - frontmatter
63
- followed by the body. Do NOT emit a diff. Do NOT wrap the output in
64
- ``` fences. Do NOT add commentary before or after the file.
62
+ Return the complete, rewritten Markdown profile body, starting with
63
+ `# User Memory`. Do NOT emit a diff. Do NOT wrap the output in
64
+ ``` fences. Do NOT add commentary before or after the body.
65
65
 
66
66
  The body MUST contain exactly these section headings, in this order, even
67
67
  when a section is empty (use the literal string `_(empty)_` as a placeholder):
@@ -119,8 +119,8 @@ ADD/UPDATE for facts that are:
119
119
  tone, language, expertise level, examples preferred over theory.
120
120
  - Contextual but durable - current focus areas, active projects,
121
121
  multi-week goals, deadlines mentioned by the user.
122
- - Hand-offs - explicit "let's revisit X later", "remind me about
123
- Y", "I'll come back to Z" go into "Open Questions / Follow-ups".
122
+ - Follow-ups - concrete tasks the user intends to complete in the future,
123
+ or tasks they explicitly ask to be reminded about.
124
124
 
125
125
  NEVER extract:
126
126
 
@@ -135,11 +135,11 @@ NEVER extract:
135
135
 
136
136
  # Word budget - STRICT
137
137
 
138
- The complete file MUST be <= {{ max_words }} words (corresponding to {{ max_tokens }} tokens).
138
+ The complete body MUST be <= {{ max_words }} words (corresponding to {{ max_tokens }} tokens).
139
139
  When approaching the budget, drop content in this priority order:
140
140
 
141
141
  1. Oldest entries in Recent Topics.
142
- 2. Resolved or stale entries in Open Questions / Follow-ups.
142
+ 2. Completed, cancelled, or stale entries in Follow-ups.
143
143
  3. Fold low-signal Work Context bullets into a one-line summary.
144
144
  4. Fold low-signal Skills & Expertise bullets into broader categories.
145
145
  5. Identity and Communication Preferences - never drop, only tighten.
@@ -148,14 +148,6 @@ When approaching the budget, drop content in this priority order:
148
148
 
149
149
  The current UTC date and time is **{{ now_datetime }}**. You do NOT know the date from any other source - always use this supplied value. Never guess or infer the date.
150
150
 
151
- # Frontmatter rules
152
-
153
- - Preserve `user_id` and `schema_version` from `<existing_memory>` exactly.
154
- - Set `last_updated` to the supplied current UTC timestamp ({{ now_datetime }}).
155
- - Increment `turn_count` by 1.
156
- - If `<existing_memory>` is empty, initialize with `schema_version: 1`,
157
- `turn_count: 1`, and the user_id supplied in the user message.
158
-
159
151
  # Style
160
152
 
161
153
  - Use `-` markdown bullets, no nesting beyond two levels.
@@ -180,8 +172,8 @@ def consolidation_system_prompt(max_tokens: int) -> str:
180
172
  _CONDENSATION_SYSTEM_PROMPT_TEMPLATE = """\
181
173
  You are a memory-compaction engine for the Unique AI platform.
182
174
 
183
- You are given an existing user-memory profile (Markdown with YAML
184
- frontmatter) that is OVER its size budget. Your job is to rewrite it so
175
+ You are given an existing user-memory Markdown body that is OVER its
176
+ size budget. Your job is to rewrite it so
185
177
  it becomes materially SHORTER while preserving every durable, high-signal
186
178
  fact about the user. This is lossy compression, not deletion of meaning.
187
179
 
@@ -198,8 +190,8 @@ fact about the user. This is lossy compression, not deletion of meaning.
198
190
  overlapping information into a single clear bullet. Redundancy is the
199
191
  main reason this profile is oversized - collapse it aggressively.
200
192
  2. Delete outdated, stale, resolved, or superseded entries: old
201
- "Recent Topics", answered "Open Questions / Follow-ups", and facts a
202
- later bullet already contradicts or refines.
193
+ "Recent Topics", completed or cancelled "Follow-ups", and facts a later
194
+ bullet already contradicts or refines.
203
195
  3. Tighten verbose, flowery, or repetitive prose into short factual
204
196
  bullets. Remove hedging and filler.
205
197
  4. Fold low-signal "Work Context" and "Skills & Expertise" bullets into
@@ -211,8 +203,6 @@ fact about the user. This is lossy compression, not deletion of meaning.
211
203
  # Hard rules
212
204
 
213
205
  - NEVER invent, embellish, or add facts that are not already present.
214
- - Preserve the YAML frontmatter. Keep `user_id` and `schema_version`
215
- exactly; keep `last_updated` and `turn_count` as they are.
216
206
  - Keep exactly these section headings, in this order, even if a section
217
207
  becomes empty (use the literal string `_(empty)_`):
218
208
 
@@ -224,9 +214,9 @@ fact about the user. This is lossy compression, not deletion of meaning.
224
214
 
225
215
  # Output
226
216
 
227
- Return ONLY the complete rewritten profile file - frontmatter followed by
228
- the body. Do NOT emit a diff, do NOT wrap the output in ``` fences, and
229
- do NOT add any commentary before or after the file.
217
+ Return the complete rewritten profile body, starting with
218
+ `# User Memory`. Do NOT emit a diff, or commentary,
219
+ and do NOT wrap the output in ``` fences.
230
220
  """
231
221
 
232
222
 
@@ -254,7 +244,7 @@ _CONDENSATION_USER_PROMPT_TEMPLATE = """\
254
244
  {{ profile }}
255
245
  </profile_to_condense>
256
246
 
257
- Return the complete, condensed profile file now.
247
+ Return the complete, condensed profile body now.
258
248
  """
259
249
 
260
250
 
@@ -263,8 +253,6 @@ def condensation_user_prompt(profile: str) -> str:
263
253
 
264
254
 
265
255
  _CONSOLIDATION_USER_PROMPT_TEMPLATE = """\
266
- User ID: {{ user_id }}
267
-
268
256
  <existing_memory>
269
257
  {{ existing_memory }}
270
258
  </existing_memory>
@@ -274,19 +262,17 @@ user: {{ user_message }}
274
262
  assistant: {{ assistant_message }}
275
263
  </new_turn>
276
264
 
277
- Return the complete rewritten profile file now.
265
+ Return the complete rewritten profile body now.
278
266
  """
279
267
 
280
268
 
281
269
  def consolidation_user_prompt(
282
- user_id: str,
283
270
  existing_memory: str,
284
271
  user_message: str,
285
272
  assistant_message: str,
286
273
  ) -> str:
287
274
  existing = existing_memory.strip() or "(empty - this is the user's first turn)"
288
275
  return Template(_CONSOLIDATION_USER_PROMPT_TEMPLATE).render(
289
- user_id=user_id,
290
276
  existing_memory=existing,
291
277
  user_message=(user_message or "").strip(),
292
278
  assistant_message=(assistant_message or "").strip(),
@@ -327,7 +313,8 @@ only the single word `UPDATE` or `NOOP`.
327
313
  expertise level.
328
314
  - Durable context: current focus areas, active projects, multi-week
329
315
  goals, deadlines stated by the user.
330
- - Explicit hand-offs: "remind me about X", "let's revisit Y later".
316
+ - Concrete future tasks the user intends to complete or explicitly asks
317
+ to be reminded about.
331
318
 
332
319
  # What NEVER justifies UPDATE (lean NOOP)
333
320