principles-disciple 1.153.0 → 1.154.0

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.
@@ -89,3 +89,17 @@ export declare function formatEvolutionPrinciples(active: EvolutionPrincipleEntr
89
89
  * @returns Assembled appendSystemContext (empty string if no parts)
90
90
  */
91
91
  export declare function assembleAppendSystemContext(parts: AppendSystemContextParts): string;
92
+ /**
93
+ * Extracts short, distinct keyword phrases from an EmpathyObserver reason string.
94
+ *
95
+ * Used to feed newly detected expressions back into the keyword store, so the
96
+ * keyword-based fast path can catch them on future turns without re-running
97
+ * the more expensive observer.
98
+ *
99
+ * Pure function — no I/O, no side effects. Fully unit-testable.
100
+ *
101
+ * @param reason - observer's reason string (e.g., "用户表达了强烈的挫败感,提到反复尝试失败")
102
+ * @param lang - UI locale for minimum length threshold (zh=2 chars, en=3 chars)
103
+ * @returns deduplicated array of up to 3 candidate phrases
104
+ */
105
+ export declare function extractPhrasesFromReason(reason: string, lang: 'zh' | 'en'): string[];
@@ -260,3 +260,29 @@ ${executionRules.join('\n')}
260
260
  `;
261
261
  return result;
262
262
  }
263
+ // ---------------------------------------------------------------------------
264
+ // Block H: Observer feedback → keyword phrase extraction
265
+ // ---------------------------------------------------------------------------
266
+ /**
267
+ * Extracts short, distinct keyword phrases from an EmpathyObserver reason string.
268
+ *
269
+ * Used to feed newly detected expressions back into the keyword store, so the
270
+ * keyword-based fast path can catch them on future turns without re-running
271
+ * the more expensive observer.
272
+ *
273
+ * Pure function — no I/O, no side effects. Fully unit-testable.
274
+ *
275
+ * @param reason - observer's reason string (e.g., "用户表达了强烈的挫败感,提到反复尝试失败")
276
+ * @param lang - UI locale for minimum length threshold (zh=2 chars, en=3 chars)
277
+ * @returns deduplicated array of up to 3 candidate phrases
278
+ */
279
+ export function extractPhrasesFromReason(reason, lang) {
280
+ const MAX_PHRASES = 3;
281
+ const MIN_LENGTH = lang === 'zh' ? 2 : 3;
282
+ const MAX_LENGTH = 20;
283
+ const segments = reason
284
+ .split(/[,,。.!!??、\n;;]/)
285
+ .map(s => s.trim())
286
+ .filter(s => s.length >= MIN_LENGTH && s.length <= MAX_LENGTH);
287
+ return [...new Set(segments)].slice(0, MAX_PHRASES);
288
+ }
@@ -11,7 +11,7 @@ import { selectPrinciplesForInjection, DEFAULT_PRINCIPLE_BUDGET } from '../core/
11
11
  import { getCachedMaskedPrincipleSet, WorkflowFunnelLoader, PiAiRuntimeAdapter, EmpathyObserver, AgentScheduler, RUNTIME_V2_PRINCIPLE_BUDGET, trimToBudget, renderPrinciplesToDirectives } from '@principles/core/runtime-v2';
12
12
  import { truncateInjectionToBudget } from '@principles/core/prompt-builder';
13
13
  import { PromptActivationReader } from '../core/runtime-v2-prompt-activation-reader.js';
14
- import { matchEmpathyKeywords, loadKeywordStore, saveKeywordStore, getKeywordStoreSummary, } from '../core/empathy-keyword-matcher.js';
14
+ import { matchEmpathyKeywords, loadKeywordStore, saveKeywordStore, applyKeywordUpdates, getKeywordStoreSummary, } from '../core/empathy-keyword-matcher.js';
15
15
  import { severityToPenalty, DEFAULT_EMPATHY_KEYWORD_CONFIG } from '../core/empathy-types.js';
16
16
  import { evaluatePainDiagnosticGate } from '../core/pain-diagnostic-gate.js';
17
17
  import { emitPainDetectedEvent, buildTrajectoryEvidence } from './pain.js';
@@ -25,7 +25,7 @@ import { buildIntentFrictionBlock } from '@principles/core/runtime-v2';
25
25
  import { CorrectionCueLearner } from '../core/correction-cue-learner.js';
26
26
  import { detectCorrectionCue as coreDetectCorrectionCue, escapeXml, extractMessageContent, isMinimalTrigger, } from '@principles/core/prompt-builder';
27
27
  import { sanitizeForEvidence } from './message-sanitize.js';
28
- import { buildAgentIdentity, buildEmpathySilenceConstraint, extractUserMessageFromPrompt, assembleHeartbeatChecklist, formatCorePrinciples, formatEvolutionPrinciples, assembleAppendSystemContext, } from './prompt-helpers.js';
28
+ import { buildAgentIdentity, buildEmpathySilenceConstraint, extractUserMessageFromPrompt, assembleHeartbeatChecklist, formatCorePrinciples, formatEvolutionPrinciples, assembleAppendSystemContext, extractPhrasesFromReason, } from './prompt-helpers.js';
29
29
  // ---------------------------------------------------------------------------
30
30
  // Static file cache — avoids re-reading rarely-changing files every message
31
31
  // ---------------------------------------------------------------------------
@@ -305,6 +305,8 @@ export async function handleBeforePromptBuild(event, ctx) {
305
305
  // Increment turn counter (Finding #3: session.turnCount doesn't exist)
306
306
  empathyState.turnCounter++;
307
307
  const turnCount = empathyState.turnCounter;
308
+ const painTrigger = wctx.config.get('thresholds.pain_trigger') || 40;
309
+ const highGfiThreshold = Math.max(wctx.config.get('severity_thresholds.high') || 70, painTrigger + 30);
308
310
  if (matchResult.matched) {
309
311
  const penalty = severityToPenalty(matchResult.severity, DEFAULT_EMPATHY_KEYWORD_CONFIG);
310
312
  // trackFriction signature: (sessionId, deltaF: number, hash: string, workspaceDir?, options?)
@@ -314,8 +316,6 @@ export async function handleBeforePromptBuild(event, ctx) {
314
316
  logger?.info?.(`[PD:Empathy] MATCH: "${matchResult.matchedTerms.join(', ')}" → severity=${matchResult.severity}, score=${matchResult.score.toFixed(2)}, penalty=${penalty}`);
315
317
  const currentSession = getSession(sessionId);
316
318
  const currentGfi = currentSession?.currentGfi ?? 0;
317
- const painTrigger = wctx.config.get('thresholds.pain_trigger') || 40;
318
- const highGfiThreshold = Math.max(wctx.config.get('severity_thresholds.high') || 70, painTrigger + 30);
319
319
  if (currentGfi >= highGfiThreshold) {
320
320
  const gfiPainScore = Math.min(Math.round(currentGfi), 60);
321
321
  logger?.info?.(`[PD:Empathy] GFI-TRIGGERED: currentGfi=${currentGfi.toFixed(1)} >= highGfi=${highGfiThreshold}, emitting pain signal (score=${gfiPainScore})`);
@@ -451,7 +451,9 @@ export async function handleBeforePromptBuild(event, ctx) {
451
451
  }
452
452
  }
453
453
  }
454
- // Trigger asynchronous background Empathy Observer deep analysis (Zero Latency)
454
+ }
455
+ else {
456
+ // ── Pipeline: Observer only runs when keyword matcher misses ──
455
457
  const observer = resolveEmpathyObserver(wctx, logger);
456
458
  if (observer) {
457
459
  const scheduler = new AgentScheduler();
@@ -460,14 +462,40 @@ export async function handleBeforePromptBuild(event, ctx) {
460
462
  mode: 'realtime',
461
463
  runner: observer,
462
464
  });
463
- logger?.info?.(`[PD:Empathy] Triggering background Empathy Observer deep analysis for message: "${msgPreview}"`);
465
+ logger?.info?.(`[PD:Empathy] Pipeline: Triggering background Empathy Observer for unmatched message: "${msgPreview}"`);
464
466
  void scheduler.dispatch('empathy-observer', { userMessage: latestUserMessage })
465
467
  .then(async (result) => {
466
468
  if (result.damageDetected) {
467
- logger?.info?.(`[PD:Empathy] Background Empathy Observer detected damage. Severity: ${result.severity}, Reason: ${result.reason}`);
469
+ logger?.info?.(`[PD:Empathy] Pipeline: Background Empathy Observer detected damage. Severity: ${result.severity}, Reason: ${result.reason}`);
468
470
  // ── Persistence Contract ──
469
471
  const painScore = scoreFromSeverityForSpec(result.severity, wctx);
470
472
  trackFriction(sessionId, painScore, `observer_empathy_${result.severity}`, workspaceDir, { source: 'user_empathy' });
473
+ // ── Pipeline: feed back newly detected expressions into keyword store ──
474
+ if (result.reason) {
475
+ const rawLang = wctx.config.get('language');
476
+ const lang = rawLang === 'en' ? 'en' : 'zh';
477
+ const phrases = extractPhrasesFromReason(result.reason, lang);
478
+ if (phrases.length > 0) {
479
+ const updates = {};
480
+ for (const phrase of phrases) {
481
+ if (!Object.hasOwn(keywordStore.terms, phrase)) {
482
+ updates[phrase] = {
483
+ action: 'add',
484
+ weight: 0.4,
485
+ falsePositiveRate: 0.3,
486
+ reasoning: `Discovered by empathy observer: ${result.reason.substring(0, 100)}`,
487
+ };
488
+ }
489
+ }
490
+ if (Object.keys(updates).length > 0) {
491
+ const { added } = applyKeywordUpdates(keywordStore, updates);
492
+ if (added > 0) {
493
+ saveKeywordStore(wctx.stateDir, keywordStore);
494
+ logger?.info?.(`[PD:Empathy] Pipeline: added ${added} new terms from observer feedback`);
495
+ }
496
+ }
497
+ }
498
+ }
471
499
  const eventId = `emp_obs_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
472
500
  wctx.eventLog.recordPainSignal(sessionId, {
473
501
  score: painScore,
@@ -524,7 +552,7 @@ export async function handleBeforePromptBuild(event, ctx) {
524
552
  const sourceKind = resolveSourceKind(rawObs);
525
553
  const triage = evaluateEvidenceTriage(sourceKind, freshGfiPainScore);
526
554
  if (triage.decision !== 'admit') {
527
- logger?.info?.(`[PD:Empathy] Observer triage ${triage.decision}: ${triage.reason}`);
555
+ logger?.info?.(`[PD:Empathy] Pipeline observer triage ${triage.decision}: ${triage.reason}`);
528
556
  }
529
557
  else {
530
558
  const cooldownActive = isSharedCooldownActive(sourceKind, sessionId, 'empathy_gfi_observer');
@@ -538,7 +566,7 @@ export async function handleBeforePromptBuild(event, ctx) {
538
566
  });
539
567
  if (triggerDecision.shouldCreateDiagnosticTask) {
540
568
  markSharedEpisodeAsDiagnosed(sourceKind, sessionId, 'empathy_gfi_observer');
541
- logger?.info?.(`[PD:Empathy] Gate B approved (observer), calling emitPainDetectedEvent...`);
569
+ logger?.info?.(`[PD:Empathy] Pipeline Gate B approved (observer), calling emitPainDetectedEvent...`);
542
570
  try {
543
571
  const evidence = buildTrajectoryEvidence(wctx, sessionId);
544
572
  // PRI-453: Reuse observerPainId for lineage consistency —
@@ -562,11 +590,11 @@ export async function handleBeforePromptBuild(event, ctx) {
562
590
  }, { recordObservability: false });
563
591
  }
564
592
  catch (emitErr) {
565
- logger?.error?.(`[PD:Empathy] FAILED to emit observer-triggered pain event: ${String(emitErr)}`);
593
+ logger?.error?.(`[PD:Empathy] Pipeline FAILED to emit observer-triggered pain event: ${String(emitErr)}`);
566
594
  }
567
595
  }
568
596
  else {
569
- logger?.info?.(`[PD:Empathy] Gate B skipped (observer): ${triggerDecision.reason}`);
597
+ logger?.info?.(`[PD:Empathy] Pipeline Gate B skipped (observer): ${triggerDecision.reason}`);
570
598
  }
571
599
  }
572
600
  }
@@ -586,7 +614,7 @@ export async function handleBeforePromptBuild(event, ctx) {
586
614
  },
587
615
  });
588
616
  if (gate.shouldDiagnose) {
589
- logger?.info?.(`[PD:Empathy] GFI threshold crossed after background observer. Emitting pain signal...`);
617
+ logger?.info?.(`[PD:Empathy] Pipeline GFI threshold crossed after background observer. Emitting pain signal...`);
590
618
  try {
591
619
  const evidence = buildTrajectoryEvidence(wctx, sessionId);
592
620
  // PRI-453: Reuse observerPainId for lineage consistency —
@@ -610,22 +638,20 @@ export async function handleBeforePromptBuild(event, ctx) {
610
638
  }, { recordObservability: false });
611
639
  }
612
640
  catch (emitErr) {
613
- logger?.error?.(`[PD:Empathy] FAILED to emit observer-triggered pain event: ${String(emitErr)}`);
641
+ logger?.error?.(`[PD:Empathy] Pipeline FAILED to emit observer-triggered pain event: ${String(emitErr)}`);
614
642
  }
615
643
  }
616
644
  }
617
645
  }
618
646
  }
619
647
  else {
620
- logger?.info?.(`[PD:Empathy] Background Empathy Observer did not detect any damage.`);
648
+ logger?.info?.(`[PD:Empathy] Pipeline: Background Empathy Observer did not detect any damage.`);
621
649
  }
622
650
  })
623
651
  .catch((err) => {
624
- logger?.warn?.(`[PD:Empathy] Background analysis failed or rejected: ${String(err)}`);
652
+ logger?.warn?.(`[PD:Empathy] Pipeline: Background analysis failed or rejected: ${String(err)}`);
625
653
  });
626
654
  }
627
- }
628
- else {
629
655
  // Log unmatched messages periodically for coverage analysis
630
656
  if (turnCount > 0 && turnCount % 50 === 0) {
631
657
  const sampleMsg = latestUserMessage.substring(0, 80).replace(/\n/g, ' ');
@@ -2,7 +2,7 @@
2
2
  "id": "principles-disciple",
3
3
  "name": "Principles Disciple",
4
4
  "description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
5
- "version": "1.153.0",
5
+ "version": "1.154.0",
6
6
  "activation": {
7
7
  "onCapabilities": [
8
8
  "hook"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.153.0",
3
+ "version": "1.154.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",