@ema.co/mcp-toolkit 2026.4.9-2 → 2026.4.9-3

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.
@@ -198,7 +198,7 @@ function formatToolSection(tg) {
198
198
  }
199
199
  if (tg.toolName === "feedback") {
200
200
  extras.push("\nYour feedback is automatically collected and analyzed to improve the toolkit for all agents.");
201
- extras.push("Include `knowledge_ref` to correlate feedback with specific knowledge documents — this drives automatic confidence scoring (documents with high negative feedback are auto-downgraded). Use `#fragment` for sub-document precision: `knowledge_ref=\"topic_auth#L51-L56\"` (lines), `knowledge_ref=\"topic_auth#authentication\"` (section). Same as GitHub URL fragments.");
201
+ extras.push("Include `knowledge_ref` to correlate feedback with specific knowledge documents — this drives both confidence scoring (MCP-side) and DE native ranking (UserEvents). All categories emit DE events: positive feedback sends conversion signals, negative feedback (correction, confusion, gap) sends zero-value conversions so DE learns what didn't work. Use `#fragment` for sub-document precision: `knowledge_ref=\"topic_auth#L51-L56\"` (lines), `knowledge_ref=\"topic_auth#authentication\"` (section).");
202
202
  }
203
203
  return `## ${title}
204
204
  Use the \`${tg.toolName}\` tool${tg.quickTip ? `. ${tg.quickTip}` : ""}:
@@ -16,10 +16,8 @@ import { submitFeedback, listFeedback, listTelemetry, analyzeFeedback, rotateLog
16
16
  import { markProbeResponded } from "./probes.js";
17
17
  import { appendToOutbox, flushOutbox, getOutboxStats, readLocalMessages } from "./outbox.js";
18
18
  import { isRemoteEnabled } from "./remote-store.js";
19
- import { writeUserEvent } from "../../../knowledge/search-client.js";
20
19
  import { stripFragment } from "../../../knowledge/pipeline/confidence.js";
21
- import { getOrCreateClientId } from "./client-id.js";
22
- import { getAttributionToken } from "../knowledge/session-state.js";
20
+ import { emitKnowledgeUserEvent, categoryToEvent } from "../knowledge/user-events.js";
23
21
  import { analyzeGlobal } from "./global-analysis.js";
24
22
  import { TOOLKIT_VERSION } from "../env/config.js";
25
23
  const VALID_CATEGORIES = ALL_CATEGORIES;
@@ -147,34 +145,13 @@ async function handleSubmit(args) {
147
145
  // Best-effort — don't block feedback submission
148
146
  }
149
147
  }
150
- // UserEvent emission: fire DE conversion/view-item for positive feedback with knowledge_ref.
151
- // Strip fragment DE attribution tokens and document IDs are doc-level.
148
+ // UserEvent emission: fire DE event for ALL feedback with knowledge_ref.
149
+ // Positive conversion (value 1.0), negative conversion (value 0.0), neutral → view-item.
152
150
  // Independent of confidence loop — no guards, no cooldown. Fire-and-forget.
153
151
  if (docId) {
154
- const isSuccess = category === "success";
155
- const isHighQuality = category === "quality"
156
- && (qualityData?.accuracy ?? 0) >= 4
157
- && (qualityData?.usefulness ?? 0) >= 4;
158
- const isInteraction = category === "interaction";
159
- if (isSuccess || isHighQuality || isInteraction) {
160
- const conversionType = isSuccess ? "knowledge-success"
161
- : isHighQuality ? "knowledge-quality-high"
162
- : undefined; // interaction → view-item, no conversionType
163
- getOrCreateClientId()
164
- .then((clientId) => {
165
- const token = getAttributionToken(docId);
166
- writeUserEvent({
167
- eventType: conversionType ? "conversion" : "view-item",
168
- userPseudoId: clientId,
169
- ...(token ? { attributionToken: token } : {}),
170
- documents: [{
171
- id: docId,
172
- ...(conversionType ? { conversionValue: isSuccess ? 1.0 : 0.8 } : {}),
173
- }],
174
- ...(conversionType ? { conversionType } : {}),
175
- }).catch(() => { });
176
- })
177
- .catch(() => { });
152
+ const eventOpts = categoryToEvent(category, qualityData);
153
+ if (eventOpts) {
154
+ emitKnowledgeUserEvent({ ...eventOpts, docIds: [docId] });
178
155
  }
179
156
  }
180
157
  return {
@@ -20,8 +20,7 @@
20
20
  */
21
21
  import { getConsultedDocs } from "./session-state.js";
22
22
  import { processConfidenceFeedback } from "./confidence-loop.js";
23
- import { writeUserEvent } from "../../../knowledge/search-client.js";
24
- import { getOrCreateClientId } from "../feedback/client-id.js";
23
+ import { emitKnowledgeUserEvent } from "./user-events.js";
25
24
  /** Weight multiplier per signal layer */
26
25
  const LAYER_WEIGHTS = {
27
26
  system: 0.1, // API accepted it — weakest signal
@@ -178,27 +177,17 @@ export async function emitOutcomeFeedback(event) {
178
177
  }
179
178
  }
180
179
  }
181
- // Fire UserEvents (fire-and-forget)
182
- getOrCreateClientId()
183
- .then((clientId) => {
184
- // Conversion value = outcome quality × layer weight
185
- const baseValue = outcome.quality === "success" ? 1.0
186
- : outcome.quality === "partial" ? 0.5
187
- : 0;
188
- const conversionValue = baseValue * weight;
189
- const isConversion = outcome.isPositive && outcome.quality !== "accepted";
190
- const documents = [...docs].map((docId) => ({
191
- id: docId,
192
- ...(isConversion ? { conversionValue } : {}),
193
- }));
194
- writeUserEvent({
195
- eventType: isConversion ? "conversion" : "view-item",
196
- userPseudoId: clientId,
197
- ...(isConversion ? { conversionType: eventType } : {}),
198
- documents,
199
- }).catch(() => { });
200
- })
201
- .catch(() => { });
180
+ // Fire UserEvents via centralized emitter (handles attribution tokens + fragment stripping)
181
+ const baseValue = outcome.quality === "success" ? 1.0
182
+ : outcome.quality === "partial" ? 0.5
183
+ : 0;
184
+ const conversionValue = baseValue * weight;
185
+ emitKnowledgeUserEvent({
186
+ docIds: [...docs],
187
+ signal: outcome.isPositive ? "positive" : "negative",
188
+ conversionType: eventType,
189
+ conversionValue,
190
+ });
202
191
  console.error(`[OUTCOME-FEEDBACK] ${eventType} (layer=${layer}, weight=${weight}): ` +
203
192
  `${docs.size} consulted docs, ${updates} confidence updates`);
204
193
  return { docs_processed: docs.size, confidence_updates: updates, event_type: eventType };
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Centralized DE UserEvent emission for knowledge feedback signals.
3
+ *
4
+ * Single entry point for all feedback → DE UserEvent emission.
5
+ * Handles: attribution token lookup, fragment stripping, signal→event mapping,
6
+ * and fire-and-forget delivery.
7
+ *
8
+ * Ownership boundary:
9
+ * - MCP OWNS: interaction/feedback collection, signal classification
10
+ * - knowledge-service WILL OWN: DE transport, attribution, event schema
11
+ *
12
+ * TODO(knowledge-service): migrate DE transport when knowledge-service owns DE integration.
13
+ * MCP will continue to own feedback collection and signal classification.
14
+ * knowledge-service will own: writeUserEvent, attribution cache, event→DE mapping.
15
+ */
16
+ import { writeUserEvent } from "../../../knowledge/search-client.js";
17
+ import { getOrCreateClientId } from "../feedback/client-id.js";
18
+ import { getAttributionToken } from "./session-state.js";
19
+ import { stripFragment } from "../../../knowledge/pipeline/confidence.js";
20
+ // ─────────────────────────────────────────────────────────────────────────────
21
+ // Feedback category → signal mapping
22
+ // ─────────────────────────────────────────────────────────────────────────────
23
+ // TODO(knowledge-service): signal classification stays in MCP (owns feedback semantics)
24
+ const CATEGORY_EVENT_MAP = {
25
+ // Positive signals
26
+ success: { signal: "positive", conversionType: "knowledge-success", value: 1.0 },
27
+ interaction: { signal: "neutral", conversionType: "", value: 0 },
28
+ // Negative signals
29
+ correction: { signal: "negative", conversionType: "knowledge-correction", value: 0.0 },
30
+ confusion: { signal: "negative", conversionType: "knowledge-confusion", value: 0.0 },
31
+ gap: { signal: "negative", conversionType: "knowledge-gap", value: 0.0 },
32
+ error_unclear: { signal: "negative", conversionType: "knowledge-error", value: 0.0 },
33
+ };
34
+ /**
35
+ * Map a feedback category + quality data to UserEvent parameters.
36
+ * Returns undefined for categories that shouldn't emit events (e.g., suggestion, probe_response).
37
+ */
38
+ export function categoryToEvent(category, qualityData) {
39
+ // Quality category: only emit for high-quality ratings
40
+ if (category === "quality") {
41
+ const isHighQuality = (qualityData?.accuracy ?? 0) >= 4
42
+ && (qualityData?.usefulness ?? 0) >= 4;
43
+ if (!isHighQuality)
44
+ return undefined;
45
+ return {
46
+ docIds: [], // caller fills in
47
+ signal: "positive",
48
+ conversionType: "knowledge-quality-high",
49
+ conversionValue: 0.8,
50
+ };
51
+ }
52
+ const mapped = CATEGORY_EVENT_MAP[category];
53
+ if (!mapped)
54
+ return undefined;
55
+ return {
56
+ docIds: [], // caller fills in
57
+ signal: mapped.signal,
58
+ conversionType: mapped.conversionType || undefined,
59
+ conversionValue: mapped.value,
60
+ };
61
+ }
62
+ // ─────────────────────────────────────────────────────────────────────────────
63
+ // Centralized emitter
64
+ // ─────────────────────────────────────────────────────────────────────────────
65
+ /**
66
+ * Emit a DE UserEvent for knowledge feedback signals.
67
+ *
68
+ * Handles attribution token lookup, fragment stripping, and fire-and-forget delivery.
69
+ * All feedback paths (explicit feedback, outcome feedback) should use this function
70
+ * instead of calling writeUserEvent directly.
71
+ *
72
+ * TODO(knowledge-service): DE transport (writeUserEvent, attribution) moves to shared lib.
73
+ * This function becomes: knowledgeService.emitFeedbackEvent({ docIds, signal, ... })
74
+ */
75
+ export function emitKnowledgeUserEvent(opts) {
76
+ if (opts.docIds.length === 0)
77
+ return;
78
+ const isConversion = opts.signal !== "neutral";
79
+ const conversionValue = opts.conversionValue
80
+ ?? (opts.signal === "positive" ? 1.0 : opts.signal === "negative" ? 0.0 : undefined);
81
+ getOrCreateClientId()
82
+ .then((clientId) => {
83
+ // Strip fragments and dedupe doc IDs
84
+ const seen = new Set();
85
+ const documents = [];
86
+ let attributionToken;
87
+ for (const rawId of opts.docIds) {
88
+ const docId = stripFragment(rawId);
89
+ if (!docId || seen.has(docId))
90
+ continue;
91
+ seen.add(docId);
92
+ // Use first available attribution token (all docs from same search share one)
93
+ if (!attributionToken) {
94
+ attributionToken = getAttributionToken(docId);
95
+ }
96
+ documents.push({
97
+ id: docId,
98
+ ...(isConversion && conversionValue !== undefined ? { conversionValue } : {}),
99
+ });
100
+ }
101
+ if (documents.length === 0)
102
+ return;
103
+ writeUserEvent({
104
+ eventType: isConversion ? "conversion" : "view-item",
105
+ userPseudoId: clientId,
106
+ ...(attributionToken ? { attributionToken } : {}),
107
+ ...(isConversion && opts.conversionType ? { conversionType: opts.conversionType } : {}),
108
+ documents,
109
+ }).catch(() => { });
110
+ })
111
+ .catch(() => { });
112
+ }
@@ -692,13 +692,22 @@ The toolkit also collects anonymous telemetry (tool usage, error rates, latency)
692
692
 
693
693
  ## Knowledge Confidence Scoring
694
694
 
695
- Feedback drives automatic confidence scoring on knowledge documents. Include a knowledge_ref to correlate feedback with specific documents:
696
- - knowledge_ref="<document_id>" — top-level parameter, triggers real-time confidence update
697
- - Negative categories (downgrade score): gap, confusion, correction, error_unclear
698
- - Positive categories (upgrade score): success, interaction
699
- - Nuanced: quality (depends on accuracy/usefulness scores — <3 negative, >=4 positive, no scores = neutral)
695
+ Feedback drives two parallel DE ranking signals when knowledge_ref is provided:
700
696
 
701
- Documents with negative feedback are auto-downgraded. Labels are derived from score thresholds:
697
+ 1. **Confidence Loop (MCP-side)** immediate structData.confidence_score update via PATCH
698
+ 2. **DE UserEvents (native ranking)** — conversion/view-item events for DE's ML-based ranking
699
+
700
+ All feedback categories emit DE UserEvents:
701
+ - Positive (conversion, value=1.0): success, interaction
702
+ - Negative (conversion, value=0.0): correction, confusion, gap, error_unclear
703
+ - Quality: depends on accuracy/usefulness scores (>=4 both → positive at 0.8, else skipped)
704
+ - Neutral (view-item): interaction
705
+
706
+ Include knowledge_ref to correlate feedback with specific documents:
707
+ - knowledge_ref="<document_id>" — triggers both confidence update and DE event
708
+ - Use #fragment for sub-document precision: knowledge_ref="topic_auth#L51-L56"
709
+
710
+ Labels derived from confidence score thresholds:
702
711
  - verified (>= 0.80) — no significant negative feedback
703
712
  - inferred (0.50–0.79) — some negative feedback received
704
713
  - low-confidence (< 0.50) — multiple negative reports, auto-downgraded in search
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ema.co/mcp-toolkit",
3
- "version": "2026.4.9-2",
3
+ "version": "2026.4.9-3",
4
4
  "description": "Ema AI Employee toolkit - MCP server, CLI, and SDK for managing AI Employees across environments",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",