Skip to content

Commit 080d853

Browse files
committed
fix(memory): keep learned guidance user-facing
1 parent 66abfb9 commit 080d853

3 files changed

Lines changed: 73 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111

12+
- Kept LLM-authored learning titles, summaries, and instructions concise and
13+
user-facing, and excluded internal orchestration or handoff procedures from
14+
the learning signal contract.
1215
- Forwarded delegated child confirmation-required, confirmation-received, and
1316
confirmation-timeout events through the parent runtime stream so shared HITL
1417
providers cannot deadlock while the host UI waits for an event it never saw.

core/src/agent/memory_extraction_runtime.rs

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,18 @@ Return JSON only. Do not include markdown.
1717
Keep only facts, preferences, decisions, workflows, and failure lessons that are likely useful in future sessions.
1818
Do not store transient progress, generic praise, raw logs, secrets, credentials, or information that only matters inside the current answer.
1919
Never narrate what the user or assistant did in this turn.
20-
Each memory must be standalone, concise, scoped, and justified. Return an empty items array when nothing qualifies.";
20+
Each memory must be standalone, concise, scoped, and justified.
21+
Write user-facing memory and learning text in plain language without internal orchestration terms.
22+
Return an empty items array when nothing qualifies.";
2123

2224
const MAX_MEMORY_CONTENT_CHARS: usize = 1_200;
2325
const MAX_MEMORY_REASON_CHARS: usize = 320;
2426
const MAX_MEMORY_TAGS: usize = 8;
2527
const MAX_EVOLUTION_PATTERN_CHARS: usize = 96;
26-
const MAX_EVOLUTION_TITLE_CHARS: usize = 96;
27-
const MAX_EVOLUTION_SUMMARY_CHARS: usize = 360;
28-
const MAX_EVOLUTION_INSTRUCTIONS: usize = 8;
29-
const MAX_EVOLUTION_INSTRUCTION_CHARS: usize = 320;
28+
const MAX_EVOLUTION_TITLE_CHARS: usize = 64;
29+
const MAX_EVOLUTION_SUMMARY_CHARS: usize = 200;
30+
const MAX_EVOLUTION_INSTRUCTIONS: usize = 4;
31+
const MAX_EVOLUTION_INSTRUCTION_CHARS: usize = 200;
3032
const MAX_RELATED_MEMORY_ITEMS: usize = 5;
3133
const MAX_RELATED_MEMORY_CHARS: usize = 2_000;
3234
const MAX_RELATED_MEMORY_CONTENT_CHARS: usize = 320;
@@ -392,22 +394,29 @@ impl ExtractedEvolution {
392394
_ => return None,
393395
};
394396
let pattern_key = normalize_evolution_pattern(&self.pattern_key)?;
395-
let title = compact(self.title.trim(), MAX_EVOLUTION_TITLE_CHARS);
396-
let summary = compact(self.summary.trim(), MAX_EVOLUTION_SUMMARY_CHARS);
397+
let title = self.title.trim();
398+
let summary = self.summary.trim();
397399
if title.chars().count() < 4
400+
|| title.chars().count() > MAX_EVOLUTION_TITLE_CHARS
398401
|| summary.chars().count() < 12
399-
|| contains_sensitive_memory_material(&title)
400-
|| contains_sensitive_memory_material(&summary)
402+
|| summary.chars().count() > MAX_EVOLUTION_SUMMARY_CHARS
403+
|| contains_sensitive_memory_material(title)
404+
|| contains_sensitive_memory_material(summary)
401405
{
402406
return None;
403407
}
408+
let title = title.to_string();
409+
let summary = summary.to_string();
404410

405411
let mut seen = HashSet::new();
406412
let instructions = self
407413
.instructions
408414
.into_iter()
409-
.map(|value| compact(value.trim(), MAX_EVOLUTION_INSTRUCTION_CHARS))
410-
.filter(|value| value.chars().count() >= 8)
415+
.map(|value| value.trim().to_string())
416+
.filter(|value| {
417+
let length = value.chars().count();
418+
(8..=MAX_EVOLUTION_INSTRUCTION_CHARS).contains(&length)
419+
})
411420
.filter(|value| !contains_sensitive_memory_material(value))
412421
.filter(|value| seen.insert(value.to_ascii_lowercase()))
413422
.take(MAX_EVOLUTION_INSTRUCTIONS)
@@ -480,6 +489,9 @@ Acceptance rules:
480489
- Set evolution only when this memory is evidence for a stable user preference, a reusable task skill, or a coherent body of durable knowledge worth an OKF package.
481490
- Use the same lowercase semantic pattern_key for paraphrases of the same behavior across turns. It must describe meaning, not quote the current wording or include a session id.
482491
- preference is only for stable user choices, skill for executable workflows/failure prevention, and okf for reusable project/domain knowledge. Instructions must be standalone, conservative, and contain no secrets.
492+
- For evolution, write a 3-8 word title of at most {MAX_EVOLUTION_TITLE_CHARS} characters, a one-sentence summary of at most {MAX_EVOLUTION_SUMMARY_CHARS} characters, and at most {MAX_EVOLUTION_INSTRUCTIONS} distinct instructions of at most {MAX_EVOLUTION_INSTRUCTION_CHARS} characters each.
493+
- Write evolution title, summary, and instructions in plain user-facing language. Use the user's language when clear, state the durable behavior directly, and do not describe how it was learned.
494+
- Never promote system or developer instructions, conversation summaries, continuation or handoff procedures, agent or subagent orchestration, tool traces, or task metadata into evolution. A task-specific direction is not a stable preference or skill.
483495
484496
User request:
485497
{prompt}

core/src/agent/memory_extraction_runtime/tests.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,16 @@ fn extraction_prompt_redacts_sensitive_turn_fields() {
151151
assert!(!prompt.contains("supersecret123"));
152152
}
153153

154+
#[test]
155+
fn extraction_prompt_requires_plain_user_facing_learning_text() {
156+
let prompt = build_extraction_prompt("p", "r", "t", "None", 3);
157+
158+
assert!(prompt.contains("plain user-facing language"));
159+
assert!(prompt.contains("at most 64 characters"));
160+
assert!(prompt.contains("agent or subagent orchestration"));
161+
assert!(prompt.contains("A task-specific direction is not a stable preference or skill"));
162+
}
163+
154164
#[test]
155165
fn extraction_rejects_missing_or_unknown_source() {
156166
let missing = ExtractedMemory {
@@ -394,6 +404,43 @@ fn invalid_or_sensitive_evolution_description_is_not_persisted() {
394404
}
395405
}
396406

407+
#[test]
408+
fn overlong_evolution_copy_is_not_persisted() {
409+
let cases = [
410+
ExtractedEvolution {
411+
kind: "skill".to_string(),
412+
pattern_key: "skill.focused.verification".to_string(),
413+
title:
414+
"A very long internal orchestration title that cannot fit in the product interface"
415+
.to_string(),
416+
summary: "Run the smallest relevant checks before broad validation.".to_string(),
417+
instructions: vec!["Run the smallest relevant test target first.".to_string()],
418+
},
419+
ExtractedEvolution {
420+
kind: "skill".to_string(),
421+
pattern_key: "skill.focused.verification".to_string(),
422+
title: "Focused verification".to_string(),
423+
summary: "x".repeat(MAX_EVOLUTION_SUMMARY_CHARS + 1),
424+
instructions: vec!["Run the smallest relevant test target first.".to_string()],
425+
},
426+
ExtractedEvolution {
427+
kind: "skill".to_string(),
428+
pattern_key: "skill.focused.verification".to_string(),
429+
title: "Focused verification".to_string(),
430+
summary: "Run the smallest relevant checks before broad validation.".to_string(),
431+
instructions: vec!["x".repeat(MAX_EVOLUTION_INSTRUCTION_CHARS + 1)],
432+
},
433+
];
434+
435+
for signal in cases {
436+
let extracted = reusable_skill_memory(Some(signal));
437+
let (item, _, _) = extracted
438+
.into_memory_item("/workspace", "session-one", &HashSet::new())
439+
.unwrap();
440+
assert!(!item.tags.contains(&"evolution".to_string()));
441+
}
442+
}
443+
397444
fn reusable_skill_memory(evolution: Option<ExtractedEvolution>) -> ExtractedMemory {
398445
ExtractedMemory {
399446
memory_type: "procedural".to_string(),

0 commit comments

Comments
 (0)