@ema.co/mcp-toolkit 2026.3.25-4 → 2026.4.9

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.
Files changed (62) hide show
  1. package/dist/auth/login.js +1 -1
  2. package/dist/config/profile.js +1 -1
  3. package/dist/config/tool-guidance.js +7 -3
  4. package/dist/knowledge/extractors/openapi-endpoints.js +160 -2
  5. package/dist/knowledge/guidance-cache.js +4 -3
  6. package/dist/knowledge/search-client.js +57 -47
  7. package/dist/knowledge/search-config.js +2 -1
  8. package/dist/mcp/handlers/config/index.js +124 -8
  9. package/dist/mcp/handlers/feedback/index.js +32 -0
  10. package/dist/mcp/handlers/feedback/store.js +4 -0
  11. package/dist/mcp/handlers/knowledge/confidence-loop.js +10 -5
  12. package/dist/mcp/handlers/knowledge/index.js +25 -7
  13. package/dist/mcp/handlers/knowledge/outcome-feedback.js +205 -0
  14. package/dist/mcp/handlers/knowledge/session-state.js +110 -0
  15. package/dist/mcp/handlers/workflow/deploy.js +33 -0
  16. package/dist/mcp/knowledge-guidance-topics.js +25 -1
  17. package/dist/mcp/knowledge.js +1 -1
  18. package/dist/mcp/resources-dynamic.js +3 -3
  19. package/dist/mcp/tools.js +15 -3
  20. package/dist/sdk/generated/agent-catalog.js +8 -4
  21. package/dist/sdk/generated/deprecated-actions.js +19 -19
  22. package/dist/sdk/generated/proto-fields.js +1 -1
  23. package/dist/sdk/generated/protos/service/agent_qa/v1/agent_qa_pb.js +223 -15
  24. package/dist/sdk/generated/protos/service/common/v1/common_pb.js +51 -1
  25. package/dist/sdk/generated/protos/service/conversation_review/v1/conversation_review_pb.js +63 -16
  26. package/dist/sdk/generated/protos/service/dataingest/v1/dataingest_pb.js +65 -9
  27. package/dist/sdk/generated/protos/service/eval-tool/v1/evaluation_pb.js +114 -1
  28. package/dist/sdk/generated/protos/service/eval-tool/v1/testbed_pb.js +1 -1
  29. package/dist/sdk/generated/protos/service/external_access_service/v1/external_bot_pb.js +5 -1
  30. package/dist/sdk/generated/protos/service/external_tool_connection/v1/connection_manager_pb.js +114 -35
  31. package/dist/sdk/generated/protos/service/external_tool_connection/v1/ema_connector_pb.js +120 -0
  32. package/dist/sdk/generated/protos/service/identity/v1/identity_pb.js +188 -0
  33. package/dist/sdk/generated/protos/service/llmservice/v1/llmservice_pb.js +41 -2
  34. package/dist/sdk/generated/protos/service/permissions/permissions_pb.js +327 -24
  35. package/dist/sdk/generated/protos/service/persona/v1/chatbot_pb.js +41 -3
  36. package/dist/sdk/generated/protos/service/persona/v1/persona_config_pb.js +141 -89
  37. package/dist/sdk/generated/protos/service/persona/v1/persona_pb.js +283 -133
  38. package/dist/sdk/generated/protos/service/persona/v1/shared_widgets/widget_types_pb.js +24 -3
  39. package/dist/sdk/generated/protos/service/persona/v1/voicebot_widgets/widget_types_pb.js +114 -14
  40. package/dist/sdk/generated/protos/service/proposal/v1/proposal_pb.js +57 -1
  41. package/dist/sdk/generated/protos/service/script_executor/v1/script_executor_pb.js +96 -0
  42. package/dist/sdk/generated/protos/service/transform/v1/transform_pb.js +19 -1
  43. package/dist/sdk/generated/protos/service/user/v1/user_pb.js +1 -1
  44. package/dist/sdk/generated/protos/service/voice/v1/external_voice_pb.js +132 -0
  45. package/dist/sdk/generated/protos/service/voice/v1/voice_pb.js +1 -1
  46. package/dist/sdk/generated/protos/service/webcrawl/v1/webcrawl_pb.js +7 -2
  47. package/dist/sdk/generated/protos/service/workflows/v1/action_registry_pb.js +86 -49
  48. package/dist/sdk/generated/protos/service/workflows/v1/action_runner_pb.js +12 -7
  49. package/dist/sdk/generated/protos/service/workflows/v1/action_type_pb.js +27 -17
  50. package/dist/sdk/generated/protos/service/workflows/v1/agentic_search_pb.js +27 -0
  51. package/dist/sdk/generated/protos/service/workflows/v1/chatbot_pb.js +30 -21
  52. package/dist/sdk/generated/protos/service/workflows/v1/dashboards_pb.js +35 -13
  53. package/dist/sdk/generated/protos/service/workflows/v1/embedded_persona_runner_pb.js +6 -1
  54. package/dist/sdk/generated/protos/service/workflows/v1/external_actions_pb.js +19 -1
  55. package/dist/sdk/generated/protos/service/workflows/v1/log_type_pb.js +6 -1
  56. package/dist/sdk/generated/protos/service/workflows/v1/rpc/workflow_rpc_pb.js +71 -14
  57. package/dist/sdk/generated/protos/service/workflows/v1/values_pb.js +8 -2
  58. package/dist/sdk/generated/protos/service/workflows/v1/well_known_pb.js +2 -1
  59. package/dist/sdk/generated/protos/service/workflows/v1/workflow_pb.js +31 -16
  60. package/dist/sdk/generated/template-fallbacks.js +1 -1
  61. package/dist/sdk/generated/well-known-types.js +4 -1
  62. package/package.json +1 -1
@@ -38,7 +38,7 @@ const WELL_KNOWN_APP_URLS = {
38
38
  function resolveApiUrl(envName, explicit) {
39
39
  if (explicit)
40
40
  return explicit.replace(/\/$/, "");
41
- return WELL_KNOWN_API_URLS[envName] ?? WELL_KNOWN_API_URLS.prod;
41
+ return WELL_KNOWN_API_URLS[envName] ?? `https://api.${envName}.ema.co`;
42
42
  }
43
43
  function resolveAppUrl(envName, explicit) {
44
44
  if (explicit)
@@ -23,7 +23,7 @@ export function slugify(name) {
23
23
  .replace(/^-|-$/g, "");
24
24
  }
25
25
  export function profileName(tenantSlug, env) {
26
- return `${tenantSlug}-${env}`;
26
+ return `${tenantSlug}-${slugify(env)}`;
27
27
  }
28
28
  export function displayName(tenantName, env, store) {
29
29
  const sameTenantCount = Object.values(store.profiles).filter((p) => p.tenant.name === tenantName).length;
@@ -21,6 +21,10 @@ export const TOOL_GUIDANCE = {
21
21
  { name: "Switch profile", description: "Switch active profile", example: 'config(method="use", profile="acme-corp-prod")' },
22
22
  { name: "List profiles", description: "List all profiles with auth status", example: 'config(method="profiles")' },
23
23
  { name: "Set default env", description: "Set default login environment", example: 'config(method="set", key="default_env", value="prod")' },
24
+ { name: "Login (custom URL)", description: "Login to custom tenant environment (e.g. sub-tenant staging)", example: 'config(method="login", app_url="https://staging.acme.ema.co")' },
25
+ { name: "Login (custom + API URL)", description: "Login with explicit API URL override", example: 'config(method="login", app_url="https://staging.acme.ema.co", api_url="https://api.staging.acme.ema.co")' },
26
+ { name: "Set API URL", description: "Update current profile's API endpoint", example: 'config(method="set", key="api_url", value="https://api.custom.ema.co")' },
27
+ { name: "Set app URL", description: "Update current profile's frontend URL", example: 'config(method="set", key="app_url", value="https://custom.ema.co")' },
24
28
  ],
25
29
  nextSteps: {
26
30
  status: "If auth is missing or expired, run login. If GCP auth is missing, run gcp_login. Both are required.",
@@ -115,7 +119,7 @@ export const TOOL_GUIDANCE = {
115
119
  },
116
120
  {
117
121
  name: "Get AI answer",
118
- description: "AI-generated summary with citations",
122
+ description: "AI-generated summary with citations. Response includes _warning about fabrication risk and _next_step for verification.",
119
123
  example: 'knowledge("how to add HITL", detail="answer")',
120
124
  },
121
125
  {
@@ -176,13 +180,13 @@ export const TOOL_GUIDANCE = {
176
180
  operations: [
177
181
  { name: "Get", description: "Fetch current workflow_def + generation schema", example: 'workflow(mode="get", persona_id="...")' },
178
182
  { name: "Get (slim)", description: "Fetch slimmed workflow_def for large workflows (strips displaySettings, truncates long values, ~60-70% smaller)", example: 'workflow(mode="get", persona_id="...", slim=true)' },
179
- { name: "Deploy", description: "Deploy LLM-generated workflow_def", example: 'workflow(mode="deploy", persona_id="...", workflow_def={...})' },
183
+ { name: "Deploy", description: "Deploy LLM-generated workflow_def. Deploy outcomes automatically feed knowledge quality — failures demote consulted docs. Test with conversation() after deploy to validate intent alignment.", example: 'workflow(mode="deploy", persona_id="...", workflow_def={...})' },
180
184
  { name: "Validate", description: "Static validation with path enumeration", example: 'workflow(mode="validate", persona_id="...")' },
181
185
  { name: "Optimize", description: "Structural graph optimization", example: 'workflow(mode="optimize", persona_id="...")' },
182
186
  ],
183
187
  nextSteps: {
184
188
  get: "Build a workflow_def based on the generation_schema and deploy it.",
185
- deploy: "Verify deployment: workflow(mode='get', persona_id='...')",
189
+ deploy: "Test with conversation() to validate intent alignment. Deploy success only means the API accepted it — conversation testing validates the persona actually works.",
186
190
  validate: "Fix any reported issues, then deploy.",
187
191
  optimize: "Review optimized workflow_def, then deploy if acceptable.",
188
192
  },
@@ -1,3 +1,139 @@
1
+ const MAX_RESOLVE_DEPTH = 6;
2
+ /** Resolve a $ref string like "#/components/schemas/Foo" against the spec */
3
+ function resolveRef(ref, spec) {
4
+ const parts = ref.replace(/^#\//, "").split("/");
5
+ let current = spec;
6
+ for (const part of parts) {
7
+ if (typeof current !== "object" || !current)
8
+ return undefined;
9
+ current = current[part];
10
+ }
11
+ return current;
12
+ }
13
+ /** Get a human-readable type string for a schema node */
14
+ function schemaTypeString(node, spec, visited, depth) {
15
+ if (node.$ref) {
16
+ const refName = node.$ref.split("/").pop() ?? "object";
17
+ if (visited.has(node.$ref) || depth >= MAX_RESOLVE_DEPTH)
18
+ return refName;
19
+ const resolved = resolveRef(node.$ref, spec);
20
+ if (!resolved)
21
+ return refName;
22
+ return schemaTypeString(resolved, spec, new Set([...visited, node.$ref]), depth + 1);
23
+ }
24
+ if (node.enum)
25
+ return `enum(${node.enum.join("|")})`;
26
+ if (node.anyOf) {
27
+ const types = node.anyOf
28
+ .filter((s) => s.type !== "null")
29
+ .map((s) => schemaTypeString(s, spec, visited, depth + 1));
30
+ const hasNull = node.anyOf.some((s) => s.type === "null");
31
+ const base = types.length === 1 ? types[0] : `(${types.join(" | ")})`;
32
+ return hasNull ? `${base} | null` : base;
33
+ }
34
+ if (node.allOf) {
35
+ return node.allOf.map((s) => schemaTypeString(s, spec, visited, depth + 1)).join(" & ");
36
+ }
37
+ if (node.type === "array" && node.items) {
38
+ return `${schemaTypeString(node.items, spec, visited, depth + 1)}[]`;
39
+ }
40
+ if (node.format)
41
+ return `${node.type ?? "string"}(${node.format})`;
42
+ return node.type ?? "object";
43
+ }
44
+ /** Resolve a schema to a flat list of fields (one level deep, with type strings) */
45
+ function resolveSchemaFields(schema, spec, visited = new Set(), depth = 0) {
46
+ if (depth >= MAX_RESOLVE_DEPTH)
47
+ return [];
48
+ // Follow $ref
49
+ if (schema.$ref) {
50
+ if (visited.has(schema.$ref))
51
+ return [{ name: "(circular)", type: schema.$ref.split("/").pop() ?? "object", required: false }];
52
+ const resolved = resolveRef(schema.$ref, spec);
53
+ if (!resolved)
54
+ return [];
55
+ return resolveSchemaFields(resolved, spec, new Set([...visited, schema.$ref]), depth + 1);
56
+ }
57
+ // Merge allOf — deduplicate by field name (last-wins, matching JSON Schema merge semantics)
58
+ if (schema.allOf) {
59
+ const merged = schema.allOf.flatMap((s) => resolveSchemaFields(s, spec, visited, depth + 1));
60
+ const seen = new Map();
61
+ for (const f of merged)
62
+ seen.set(f.name, f);
63
+ return [...seen.values()];
64
+ }
65
+ // Handle top-level anyOf (e.g., discriminator unions)
66
+ if (schema.anyOf) {
67
+ const nonNull = schema.anyOf.filter((s) => s.type !== "null");
68
+ return nonNull.flatMap((s) => resolveSchemaFields(s, spec, visited, depth + 1));
69
+ }
70
+ // Object with properties
71
+ if (schema.properties) {
72
+ const required = new Set(schema.required ?? []);
73
+ return Object.entries(schema.properties).map(([name, prop]) => ({
74
+ name,
75
+ type: schemaTypeString(prop, spec, visited, depth + 1),
76
+ required: required.has(name),
77
+ description: prop.description,
78
+ }));
79
+ }
80
+ return [];
81
+ }
82
+ /** Format resolved fields as a readable markdown-style block */
83
+ function formatFields(fields) {
84
+ if (fields.length === 0)
85
+ return "";
86
+ return fields
87
+ .map((f) => {
88
+ const req = f.required ? " (required)" : "";
89
+ const desc = f.description ? ` — ${f.description}` : "";
90
+ return ` - ${f.name}: ${f.type}${req}${desc}`;
91
+ })
92
+ .join("\n");
93
+ }
94
+ function buildRequestBodySection(op, spec) {
95
+ if (!op.requestBody?.content)
96
+ return "";
97
+ const lines = ["", "Request Body:"];
98
+ if (op.requestBody.description)
99
+ lines.push(` ${op.requestBody.description}`);
100
+ if (op.requestBody.required)
101
+ lines.push(" (required)");
102
+ for (const [contentType, media] of Object.entries(op.requestBody.content)) {
103
+ lines.push(` Content-Type: ${contentType}`);
104
+ if (media.schema) {
105
+ const fields = resolveSchemaFields(media.schema, spec);
106
+ if (fields.length > 0) {
107
+ lines.push(" Fields:");
108
+ lines.push(formatFields(fields));
109
+ }
110
+ }
111
+ }
112
+ return lines.join("\n");
113
+ }
114
+ function buildResponsesSection(op, spec) {
115
+ if (!op.responses)
116
+ return "";
117
+ const lines = ["", "Responses:"];
118
+ for (const [statusCode, response] of Object.entries(op.responses)) {
119
+ lines.push(` ${statusCode}: ${response.description ?? ""}`);
120
+ if (response.content) {
121
+ for (const [contentType, media] of Object.entries(response.content)) {
122
+ if (media.schema) {
123
+ const fields = resolveSchemaFields(media.schema, spec);
124
+ if (fields.length > 0) {
125
+ lines.push(` ${contentType}:`);
126
+ lines.push(formatFields(fields));
127
+ }
128
+ }
129
+ }
130
+ }
131
+ }
132
+ return lines.join("\n");
133
+ }
134
+ // ─────────────────────────────────────────────────────────────────────────────
135
+ // Main extractor
136
+ // ─────────────────────────────────────────────────────────────────────────────
1
137
  export async function extractOpenAPIEndpoints(config) {
2
138
  const specPath = config.from ?? "src/sdk/generated/openapi.json";
3
139
  const { createRequire } = await import("node:module");
@@ -7,22 +143,39 @@ export async function extractOpenAPIEndpoints(config) {
7
143
  const fullPath = resolve(process.cwd(), specPath);
8
144
  const spec = require(fullPath);
9
145
  const docs = [];
10
- const paths = spec.paths ?? {};
146
+ const paths = (spec.paths ?? {});
147
+ let schemasResolved = 0;
148
+ const HTTP_METHODS = new Set(["get", "put", "post", "delete", "options", "head", "patch", "trace"]);
11
149
  for (const [path, methods] of Object.entries(paths)) {
12
150
  for (const [method, op] of Object.entries(methods)) {
151
+ if (!HTTP_METHODS.has(method))
152
+ continue;
13
153
  if (typeof op !== "object" || !op)
14
154
  continue;
15
155
  const operation = op;
16
156
  const opId = operation.operationId ?? `${method}_${path.replace(/\//g, "_")}`;
157
+ // Build rich content with resolved schemas
17
158
  const content = [
18
159
  `${method.toUpperCase()} ${path}`,
19
160
  operation.summary ?? "",
20
161
  operation.description ?? "",
21
162
  operation.tags?.length ? `Tags: ${operation.tags.join(", ")}` : "",
163
+ // Parameters with descriptions
22
164
  operation.parameters?.length
23
- ? `Parameters: ${operation.parameters.map((p) => `${p.name} (${p.in}${p.required ? ", required" : ""})`).join(", ")}`
165
+ ? `Parameters:\n${operation.parameters.map((p) => {
166
+ const typeStr = p.schema ? schemaTypeString(p.schema, spec, new Set(), 0) : "";
167
+ const desc = p.description ? ` — ${p.description}` : "";
168
+ return ` - ${p.name} (${p.in}${p.required ? ", required" : ""}${typeStr ? `: ${typeStr}` : ""})${desc}`;
169
+ }).join("\n")}`
24
170
  : "",
171
+ // Request body with resolved schema fields
172
+ buildRequestBodySection(operation, spec),
173
+ // Response bodies with resolved schema fields
174
+ buildResponsesSection(operation, spec),
25
175
  ].filter(Boolean).join("\n");
176
+ // Track schema resolution metrics
177
+ if (operation.requestBody?.content || operation.responses)
178
+ schemasResolved++;
26
179
  docs.push({
27
180
  id: `endpoint:${opId}`,
28
181
  structData: {
@@ -49,10 +202,15 @@ export async function extractOpenAPIEndpoints(config) {
49
202
  source_path: specPath,
50
203
  http_method: method.toUpperCase(),
51
204
  http_path: path,
205
+ protocol: "rest",
206
+ visibility: "external",
207
+ has_request_schema: !!operation.requestBody?.content,
208
+ has_response_schema: !!operation.responses,
52
209
  },
53
210
  });
54
211
  }
55
212
  }
213
+ process.stderr.write(` OpenAPI: ${docs.length} endpoints, ${schemasResolved} with resolved schemas\n`);
56
214
  return { documents: docs, edges: [] };
57
215
  }
58
216
  catch (err) {
@@ -212,7 +212,8 @@ export class GuidanceCache {
212
212
  case "structural-invariant":
213
213
  this.invariants.push(data);
214
214
  break;
215
- case "contextual-guidance": {
215
+ case "contextual-guidance":
216
+ case "guide": {
216
217
  const atom = data;
217
218
  const key = atom.key
218
219
  ?? `${atom.tool ?? "*"}.${atom.method ?? "*"}.${atom.result_shape ?? "*"}`;
@@ -244,10 +245,10 @@ export class GuidanceCache {
244
245
  return "qualifying-question";
245
246
  if (tags.includes("structural-invariant"))
246
247
  return "structural-invariant";
247
- if (tags.includes("structural-invariant"))
248
- return "structural-invariant";
249
248
  if (tags.includes("contextual-guidance"))
250
249
  return "contextual-guidance";
250
+ if (tags.includes("guide"))
251
+ return "guide";
251
252
  if (name.includes("tool-guidance"))
252
253
  return "tool-guidance";
253
254
  if (name.startsWith("invariant-"))
@@ -29,9 +29,11 @@ import { getActiveScopes } from "./scopes.js";
29
29
  */
30
30
  const PROMOTED_FIELDS = new Set([
31
31
  // Promoted to top-level fields in search results
32
- "id", "name", "summary", "description", "type", "doc_type", "scope",
33
- "source", "status", "confidence", "confidence_score",
34
- "provenance", "tags", "tier", "severity", "category", "when_to_use",
32
+ "id", "name", "description", "summary", "type", "doc_type", "scope", "scopes",
33
+ "source", "sources", "status",
34
+ // Legacy v2 fields (still on ~829 MCP-extracted docs)
35
+ "confidence", "confidence_score", "provenance", "tags",
36
+ "tier", "severity", "category", "when_to_use",
35
37
  "domain", "audience", "source_repo",
36
38
  // Heavy text — already in content.rawBytes, not needed in results
37
39
  "content",
@@ -121,26 +123,29 @@ export async function ensureGcpAuth() {
121
123
  // ─────────────────────────────────────────────────────────────────────────────
122
124
  // Filter building — dynamic scope filters (DE static controls can't vary per-query)
123
125
  // ─────────────────────────────────────────────────────────────────────────────
124
- const FILTERABLE_KEYS = new Set(["type", "source", "tier", "severity", "category", "tags", "scope", "envs", "status", "doc_type", "provenance", "domain"]);
126
+ // v3 structData: name, description, scopes (array), source, sources, status, signals.
127
+ // scopes for access filtering, source for provenance, status for lifecycle.
128
+ const FILTERABLE_KEYS = new Set(["scopes", "source", "status"]);
125
129
  function buildFilterExpression(filters, elevate) {
126
130
  const parts = [];
127
- // Scope filter: use active scopes from profile/env (never hardcode "internal").
128
- // elevate=true removes the scope filter entirely (e.g., for admin access).
129
- // Callers can pass explicit scope filters to override.
131
+ // Scope filter on scopes (array field). DE ANY() matches array elements.
132
+ // elevate=true removes scope filter (admin access).
130
133
  const hasExplicitScope = filters?.scope && filters.scope.length > 0;
131
134
  if (!elevate && !hasExplicitScope) {
132
135
  const scopes = getActiveScopes();
133
136
  const quoted = scopes.map((s) => `"${s}"`).join(",");
134
- parts.push(`scope: ANY(${quoted})`);
137
+ parts.push(`scopes: ANY(${quoted})`);
135
138
  }
136
139
  if (filters) {
137
140
  for (const [key, values] of Object.entries(filters)) {
138
141
  if (!values || values.length === 0)
139
142
  continue;
140
- if (!FILTERABLE_KEYS.has(key))
143
+ // Map caller's "scope" to v3's "scopes" field name
144
+ const deField = key === "scope" ? "scopes" : key;
145
+ if (!FILTERABLE_KEYS.has(deField))
141
146
  continue;
142
147
  const quoted = values.map((v) => `"${v}"`).join(",");
143
- parts.push(`${key}: ANY(${quoted})`);
148
+ parts.push(`${deField}: ANY(${quoted})`);
144
149
  }
145
150
  }
146
151
  return parts.join(" AND ");
@@ -168,13 +173,27 @@ const QUERY_SIGNALS = [
168
173
  // ─────────────────────────────────────────────────────────────────────────────
169
174
  // Dynamic Preamble — context-aware system instructions for :answer endpoint
170
175
  // ─────────────────────────────────────────────────────────────────────────────
171
- /** Base preamble applied to all answer queries. */
176
+ /**
177
+ * Base preamble applied to all answer queries.
178
+ *
179
+ * The Ema-specific context line applies to the primary use case (Ema platform knowledge).
180
+ * The code-accuracy rules are domain-agnostic and apply to ANY knowledge corpus that
181
+ * contains code examples, API formats, or structured data — they prevent the generative
182
+ * model from fabricating structures that look plausible but are wrong.
183
+ */
172
184
  const BASE_PREAMBLE = [
185
+ // Domain context (Ema-specific — other corpora would swap this line)
173
186
  "You are answering questions about the Ema AI Employee platform — a workflow orchestration system.",
174
- "When referencing actions or nodes, use their exact actionType names (e.g., chat_categorizer, respond_with_sources).",
175
- "When showing fixes, include concrete workflow_def JSON snippets when available in the source documents.",
187
+ "When referencing actions or nodes, use their exact names (e.g., chat_categorizer, respond_with_sources).",
188
+ // Code accuracy (domain-agnostic applies to any technical corpus)
189
+ "CRITICAL — code accuracy rules:",
190
+ "1. NEVER assemble, fabricate, or synthesize complete code examples (JSON, YAML, config). The risk of structural errors is too high.",
191
+ "2. For code formats: REFERENCE the source document by name/ID and tell the user to fetch it directly. Example: 'See schema/workflow-def for the complete deployable example.'",
192
+ "3. You MAY quote SHORT code fragments (under 5 lines) verbatim from source documents with attribution.",
193
+ "4. You MAY describe the structure in words (e.g., 'actions is an array of objects, each with name, action, and inputs').",
194
+ "5. For canonical formats, direct users to the authoritative source rather than reproducing it.",
176
195
  "Cite specific document IDs when available.",
177
- ].join(" ");
196
+ ].join("\n");
178
197
  /** Query-pattern-specific preamble additions. */
179
198
  const PREAMBLE_SIGNALS = [
180
199
  {
@@ -183,7 +202,7 @@ const PREAMBLE_SIGNALS = [
183
202
  },
184
203
  {
185
204
  pattern: /\b(how to|how do|pattern|example|wire|connect|build)\b/i,
186
- addition: "Provide step-by-step instructions with workflow_def JSON examples where available.",
205
+ addition: "Provide step-by-step instructions. For complete code examples, reference the source document (e.g., 'see schema/workflow-def for the deployable example') rather than reproducing it. The agent will fetch the document directly.",
187
206
  },
188
207
  {
189
208
  pattern: /\b(rule|constraint|invariant|must|require|validate)\b/i,
@@ -246,25 +265,15 @@ function transformDeResponse(deResponse, mode) {
246
265
  document: {
247
266
  id: (structData.id ?? doc.id ?? ""),
248
267
  name: (structData.name ?? ""),
268
+ // v3: description is the summary. Fall back to v2 fields for legacy docs.
249
269
  type: (structData.type ?? structData.doc_type ?? ""),
250
270
  scope: (structData.scope ?? "public"),
251
271
  source: (structData.source ?? ""),
252
- summary: (structData.summary ?? ""),
253
- confidence_score: (structData.confidence_score ?? 0),
272
+ summary: (structData.description ?? structData.summary ?? ""),
254
273
  status: (structData.status ?? "active"),
255
- // Optional fields — omit when empty to save tokens
256
- ...(structData.description ? { description: structData.description } : {}),
257
- ...(structData.confidence ? { confidence: structData.confidence } : {}),
258
- ...(structData.category ? { category: structData.category } : {}),
259
- ...(structData.when_to_use ? { when_to_use: structData.when_to_use } : {}),
260
- ...(structData.tier ? { tier: structData.tier } : {}),
261
- ...(structData.severity ? { severity: structData.severity } : {}),
262
- ...(structData.provenance ? { provenance: structData.provenance } : {}),
263
- ...(Array.isArray(structData.tags) ? { tags: structData.tags } : {}),
264
- // Domain isolation + audience targeting
265
- ...(structData.domain ? { domain: structData.domain } : {}),
266
- ...(Array.isArray(structData.audience) ? { audience: structData.audience } : {}),
267
- ...(structData.source_repo ? { source_repo: structData.source_repo } : {}),
274
+ // v3 fields
275
+ ...(Array.isArray(structData.scopes) ? { scopes: structData.scopes } : {}),
276
+ ...(Array.isArray(structData.sources) ? { sources: structData.sources } : {}),
268
277
  // Non-promoted metadata only (no duplication)
269
278
  structData: stripPromotedFields(structData),
270
279
  },
@@ -478,22 +487,12 @@ async function searchDirect(query, options) {
478
487
  // Dynamic domain boost — if query signals a specific platform, boost its domain
479
488
  // and demote the other. DE serves both platforms; this keeps results focused.
480
489
  const queryBoost = buildQueryBoostSpec(query, filters);
481
- // Confidence boost always applied. Verified docs rank higher, low-confidence lower.
482
- // This makes the feedback loop visible at search time: downgraded docs get demoted
483
- // regardless of relevance. DE boost values are additive to relevance score.
484
- // Values calibrated against signal viewer: semantic relevance spreads 0.07-0.99,
485
- // so boosts must be large enough to move docs across that range.
486
- const confidenceBoosts = [
487
- { condition: 'confidence: ANY("verified")', boost: 0.5 },
488
- { condition: 'confidence: ANY("inferred")', boost: -0.2 },
489
- { condition: 'confidence: ANY("low-confidence")', boost: -0.8 },
490
- ];
490
+ // v3: confidence removed from structData. DE text matching handles relevance.
491
+ // Query-specific boosts (domain targeting) still apply.
491
492
  const querySpecs = (queryBoost?.conditionBoostSpecs ?? []);
492
- const allBoosts = [
493
- ...querySpecs,
494
- ...confidenceBoosts,
495
- ];
496
- body.boostSpec = { conditionBoostSpecs: allBoosts };
493
+ if (querySpecs.length > 0) {
494
+ body.boostSpec = { conditionBoostSpecs: querySpecs };
495
+ }
497
496
  // Always request snippets — works with chunked datastores.
498
497
  // (Extractive answers do NOT work with chunking, only snippets.)
499
498
  // For answer mode, also request summary with citations.
@@ -653,6 +652,8 @@ export async function browseDocuments(options = {}) {
653
652
  // ─────────────────────────────────────────────────────────────────────────────
654
653
  // User Event Tracking
655
654
  // ─────────────────────────────────────────────────────────────────────────────
655
+ /** Counters for UserEvent pipeline health — exposed via feedback(method="analyze"). */
656
+ export const userEventCounters = { sent: 0, failed: 0 };
656
657
  export async function writeUserEvent(event) {
657
658
  if (!isVertexEventsEnabled())
658
659
  return;
@@ -661,15 +662,24 @@ export async function writeUserEvent(event) {
661
662
  if (!headers)
662
663
  return;
663
664
  try {
664
- await fetch(`${de.baseUrl}/${de.datastorePath}/userEvents:write`, {
665
+ const resp = await fetch(`${de.baseUrl}/${de.datastorePath}/userEvents:write`, {
665
666
  method: "POST",
666
667
  headers,
667
668
  body: JSON.stringify(event),
668
669
  signal: AbortSignal.timeout(5_000),
669
670
  });
671
+ if (!resp.ok) {
672
+ userEventCounters.failed++;
673
+ const detail = await resp.text().catch(() => "");
674
+ console.error(`[SEARCH-CLIENT] UserEvent write failed: ${resp.status} — ${detail.slice(0, 200)}`);
675
+ }
676
+ else {
677
+ userEventCounters.sent++;
678
+ }
670
679
  }
671
680
  catch {
672
- // Fire-and-forget
681
+ userEventCounters.failed++;
682
+ // Fire-and-forget — timeout or network error
673
683
  }
674
684
  }
675
685
  /**
@@ -38,8 +38,9 @@ export function getSearchBackend() {
38
38
  export function isDiscoveryEngineEnabled() {
39
39
  return getSearchBackend() === "discovery-engine";
40
40
  }
41
+ /** UserEvent tracking is ON by default. Set EMA_VERTEX_EVENTS=false to opt out. */
41
42
  export function isVertexEventsEnabled() {
42
- return process.env.EMA_VERTEX_EVENTS?.trim().toLowerCase() === "true";
43
+ return process.env.EMA_VERTEX_EVENTS?.trim().toLowerCase() !== "false";
43
44
  }
44
45
  export function getDeConfig() {
45
46
  const project = process.env.EMA_GCP_PROJECT?.trim() || DEFAULT_PROJECT;