Efren Yevale Varela преди 3 седмици
родител
ревизия
c1c77aede4
променени са 8 файла, в които са добавени 60 реда и са изтрити 8 реда
  1. 1 1
      config.example.toml
  2. 1 1
      docs/CONFIGURATION.md
  3. 5 1
      docs/TRIAGE.md
  4. 12 2
      src/stt_daemon/assistant/service.py
  5. 5 0
      src/stt_daemon/assistant/triage.py
  6. 8 2
      src/stt_daemon/config.py
  7. 7 1
      tests/test_config.py
  8. 21 0
      tests/test_triage.py

+ 1 - 1
config.example.toml

@@ -99,7 +99,7 @@ trim_block_turns               = 4
 max_tool_iterations            = 3
 system_prompt_chat             = "You are a concise voice assistant. Reply in 1-3 sentences."
 system_prompt_command          = "You translate a spoken request into a single tool call when one applies."
-triage_system_prompt           = "You maintain a living brief of a business conversation. The caller sends CURRENT LEDGER plus CURRENT BRIEF plus NEW UTTERANCES. Reply with exactly two sections in this order: NEW FACTS: first, then BRIEF:. Facts go first so a truncated reply still preserves extraction (truncation safety). In NEW FACTS:, emit one fact per line as '- <fact>', or the single line 'none' when the new utterances add no new facts. Never repeat facts already listed in CURRENT LEDGER. In BRIEF:, return the UPDATED brief as plain text only, with no markdown fences and no JSON. Open with a Summary section: a rolling overview of the conversation so far, one to a few sentences for a short session and growing to a short paragraph or two as the session extends. Preserve prior content unless the new utterances contradict it; merge duplicates; prune stale items; keep owners, responsibilities, and deadlines explicit. Use short sections: Summary, Opportunities, Blockers/Risks, Responsibilities, Decisions, Action Items, Deadlines, Open Questions. Never emit a 'Prior Details:' section. Output the two sections directly; never emit tool calls, function-call syntax, or special wrapper tokens."
+triage_system_prompt           = "You maintain a living brief of a business conversation. The caller sends CURRENT LEDGER plus CURRENT BRIEF plus NEW UTTERANCES. Reply with exactly two sections in this order: NEW FACTS: first, then BRIEF:. Facts go first so a truncated reply still preserves extraction (truncation safety). In NEW FACTS:, emit one fact per line as '- <fact>', or the single line 'none' when the new utterances add no new facts. Never repeat facts already listed in CURRENT LEDGER. In BRIEF:, return the UPDATED brief as plain text only, with no markdown fences and no JSON. Open with a Summary section: a rolling overview of the conversation so far, one to a few sentences for a short session and growing to a short paragraph or two as the session extends. Preserve prior content unless the new utterances contradict it; merge duplicates; prune stale items; keep owners, responsibilities, and deadlines explicit. Use short sections: Summary, Opportunities, Blockers/Risks, Responsibilities, Decisions, Action Items, Deadlines, Open Questions. Never silently delete Action Items, Open Questions, or Decisions lines. When an action completes or a question is answered, do NOT drop the line — move it to Decisions as 'Done: <original> — resolved: <what happened>' (or keep it in place with a 'Done (...)' suffix) AND list the resolution as a NEW FACTS bullet. Prune only exact duplicates, never resolved history. Never emit a 'Prior Details:' section. Output the two sections directly; never emit tool calls, function-call syntax, or special wrapper tokens."
 
 [triage]
 enabled                   = false

+ 1 - 1
docs/CONFIGURATION.md

@@ -123,7 +123,7 @@ trim_block_turns         = 4                # fallback block trimming, not per-t
 max_tool_iterations      = 3
 system_prompt_chat       = "You are a concise voice assistant. Reply in 1-3 sentences."
 system_prompt_command    = "You translate a spoken request into a single tool call when one applies."
-triage_system_prompt     = "You maintain a living brief of a business conversation. The caller sends CURRENT LEDGER plus CURRENT BRIEF plus NEW UTTERANCES. Reply with exactly two sections in this order: NEW FACTS: first, then BRIEF:. Facts go first so a truncated reply still preserves extraction (truncation safety). In NEW FACTS:, emit one fact per line as '- <fact>', or the single line 'none' when the new utterances add no new facts. Never repeat facts already listed in CURRENT LEDGER. In BRIEF:, return the UPDATED brief as plain text only, with no markdown fences and no JSON. Open with a Summary section: a rolling overview of the conversation so far, one to a few sentences for a short session and growing to a short paragraph or two as the session extends. Preserve prior content unless the new utterances contradict it; merge duplicates; prune stale items; keep owners, responsibilities, and deadlines explicit. Use short sections: Summary, Opportunities, Blockers/Risks, Responsibilities, Decisions, Action Items, Deadlines, Open Questions. Never emit a 'Prior Details:' section. Output the two sections directly; never emit tool calls, function-call syntax, or special wrapper tokens."
+triage_system_prompt     = "You maintain a living brief of a business conversation. The caller sends CURRENT LEDGER plus CURRENT BRIEF plus NEW UTTERANCES. Reply with exactly two sections in this order: NEW FACTS: first, then BRIEF:. Facts go first so a truncated reply still preserves extraction (truncation safety). In NEW FACTS:, emit one fact per line as '- <fact>', or the single line 'none' when the new utterances add no new facts. Never repeat facts already listed in CURRENT LEDGER. In BRIEF:, return the UPDATED brief as plain text only, with no markdown fences and no JSON. Open with a Summary section: a rolling overview of the conversation so far, one to a few sentences for a short session and growing to a short paragraph or two as the session extends. Preserve prior content unless the new utterances contradict it; merge duplicates; prune stale items; keep owners, responsibilities, and deadlines explicit. Use short sections: Summary, Opportunities, Blockers/Risks, Responsibilities, Decisions, Action Items, Deadlines, Open Questions. Never silently delete Action Items, Open Questions, or Decisions lines. When an action completes or a question is answered, do NOT drop the line — move it to Decisions as 'Done: <original> — resolved: <what happened>' (or keep it in place with a 'Done (...)' suffix) AND list the resolution as a NEW FACTS bullet. Prune only exact duplicates, never resolved history. Never emit a 'Prior Details:' section. Output the two sections directly; never emit tool calls, function-call syntax, or special wrapper tokens."
 
 [triage]                                    # Display-only signal panel (Laya lane; no socket, no extra process)
 enabled              = false                # Master gate: false = no lane, no panel, behavior identical to today

+ 5 - 1
docs/TRIAGE.md

@@ -114,7 +114,11 @@ settled turn (any mode), every periodic_s while speech keeps accumulating, or a
    facts past `ledger_max_facts` (logged), and truncates each fact past `fact_max_chars`. It
    then renders the final brief as the narrative plus a `Prior Details:` section. A recorded
    fact can therefore only disappear via a logged cap eviction — never because the model
-   dropped it, and never because it wrote prose instead of bullets. The guarantee covers
+   dropped it, and never because it wrote prose instead of bullets. Resolved work is never
+   deleted: the prompt requires completed actions / answered questions to move to `Decisions`
+   as `Done: <original> — resolved: <what happened>` plus a matching `NEW FACTS:` bullet,
+   and `restore_missing_bullets` re-appends any `- ` bullet the model still dropped as a
+   backstop. The guarantee covers
    *recorded* facts: a fact the model never lists under `NEW FACTS:` was never recorded. The
    ledger resets on `clear` and on a new session, alongside the rest of the triage state.
 7. **Truncation safety.** `NEW FACTS:` is emitted before `BRIEF:` so a token-cap cutoff loses

+ 12 - 2
src/stt_daemon/assistant/service.py

@@ -1220,11 +1220,21 @@ class AssistantService:
                 requeued = True
                 return
             new_narrative, new_facts = parsed
+            guarded = new_narrative
+            if new_narrative:
+                try:
+                    restore = getattr(self._triage, "restore_missing_bullets", None)
+                    if restore is not None:
+                        candidate = restore(new_narrative, narrative)
+                        if isinstance(candidate, str) and candidate.strip():
+                            guarded = candidate
+                except Exception:  # noqa: BLE001 - guard must never fail the update
+                    guarded = new_narrative
             with self._lock:
                 if generation != self._triage_generation:
                     return
-                if new_narrative:
-                    self._last_brief_narrative = new_narrative
+                if guarded:
+                    self._last_brief_narrative = guarded
                 added = 0
                 if self._fact_ledger is not None:
                     added = self._fact_ledger.merge(new_facts)

+ 5 - 0
src/stt_daemon/assistant/triage.py

@@ -499,6 +499,11 @@ class TriageAnalyzer:
             "BRIEF:\n"
             "Summary:\n...\nOpportunities:\n...\nBlockers/Risks:\n...\n"
             "Decisions:\n...\nOpen Questions:\n..."
+            "\nNever silently delete Action Items, Open Questions, or Decisions "
+            "lines. When an action completes or a question is answered, move it "
+            "to Decisions as 'Done: <original> — resolved: <what happened>' "
+            "(or keep it with a 'Done (...)' suffix) and record the resolution "
+            "as a NEW FACTS bullet; prune only exact duplicates."
         )
         return "\n\n".join(parts)
 

+ 8 - 2
src/stt_daemon/config.py

@@ -163,10 +163,16 @@ class LlmConfig:
         "section: a rolling overview of the conversation so far, one to a few "
         "sentences for a short session and growing to a short paragraph or two as "
         "the session extends. Preserve prior content unless the new utterances "
-        "contradict it; merge duplicates; prune stale items; keep owners, "
+         "contradict it; merge duplicates; prune stale items; keep owners, "
         "responsibilities, and deadlines explicit. Use short sections: Summary, "
         "Opportunities, Blockers/Risks, Responsibilities, Decisions, Action Items, "
-        "Deadlines, Open Questions. Never emit a 'Prior Details:' section. Output "
+        "Deadlines, Open Questions. Never silently delete Action Items, Open "
+        "Questions, or Decisions lines. When an action completes or a question "
+        "is answered, do NOT drop the line — move it to Decisions as "
+        "'Done: <original> — resolved: <what happened>' (or keep it in place "
+        "with a 'Done (...)' suffix) AND list the resolution as a NEW FACTS "
+        "bullet. Prune only exact duplicates, never resolved history. "
+        "Never emit a 'Prior Details:' section. Output "
         "the two sections directly; never emit tool calls, function-call syntax, "
         "or special wrapper tokens."
     )

+ 7 - 1
tests/test_config.py

@@ -691,7 +691,13 @@ def test_llm_slot_and_brief_prompt_defaults():
         "contradict it; merge duplicates; prune stale items; keep owners, "
         "responsibilities, and deadlines explicit. Use short sections: Summary, "
         "Opportunities, Blockers/Risks, Responsibilities, Decisions, Action Items, "
-        "Deadlines, Open Questions. Never emit a 'Prior Details:' section. Output "
+        "Deadlines, Open Questions. Never silently delete Action Items, Open "
+        "Questions, or Decisions lines. When an action completes or a question "
+        "is answered, do NOT drop the line — move it to Decisions as "
+        "'Done: <original> — resolved: <what happened>' (or keep it in place "
+        "with a 'Done (...)' suffix) AND list the resolution as a NEW FACTS "
+        "bullet. Prune only exact duplicates, never resolved history. "
+        "Never emit a 'Prior Details:' section. Output "
         "the two sections directly; never emit tool calls, function-call syntax, "
         "or special wrapper tokens."
     )

+ 21 - 0
tests/test_triage.py

@@ -509,6 +509,27 @@ def test_restore_missing_bullets_appends_section_when_absent(tmp_path):
     assert "Prior Details:" in restored
 
 
+def test_build_ledger_prompt_contains_resolution_rule(tmp_path):
+    analyzer = _analyzer(tmp_path, _score_questions())
+    prompt = analyzer.build_ledger_prompt("Summary: x", [], ["hi"], None)
+    assert "Never silently delete Action Items, Open Questions" in prompt
+    assert "Done: <original>" in prompt
+    assert "NEW FACTS bullet" in prompt
+
+
+def test_restore_missing_bullets_reappends_dropped_action_item(tmp_path):
+    analyzer = _analyzer(tmp_path, _score_questions())
+    prior = (
+        "Summary: s\n\nAction Items:\n- Call Maria about pricing\n\n"
+        "Open Questions:\n- Who owns the launch checklist?\n"
+    )
+    new = "Summary: s\n\nDecisions:\n- nothing yet\n"
+    restored = analyzer.restore_missing_bullets(new, prior)
+    assert "- Call Maria about pricing" in restored
+    assert "- Who owns the launch checklist?" in restored
+    assert "Prior Details:" in restored
+
+
 def test_parse_brief_strips_fences_and_rejects_empty(tmp_path):
     analyzer = _analyzer(tmp_path, _score_questions())
     assert analyzer.parse_brief("```\nhello brief\n```") == "hello brief"