@ema.co/mcp-toolkit 2026.2.19 → 2026.2.23

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.

Potentially problematic release.


This version of @ema.co/mcp-toolkit might be problematic. Click here for more details.

package/dist/mcp/tools.js CHANGED
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * MCP Tools v2 - Minimal, LLM-optimized tool set with explicit methods
3
3
  *
4
- * 5 tools:
4
+ * 7 tools:
5
5
  * 1. persona - AI Employee entity + data + incremental modifications
6
6
  * 2. catalog - Reference data (actions, templates, widgets, voices, patterns)
7
7
  * 3. workflow - Complex workflow generation/compilation
8
8
  * 4. sync - Cross-environment operations
9
9
  * 5. env - Environment info
10
+ * 6. toolkit_feedback - Agent feedback collection
11
+ * 7. debug - Audit conversations + workflow execution traces
10
12
  *
11
13
  * Design principles:
12
14
  * - ALL operations require explicit `method` or `mode` parameter
@@ -222,6 +224,23 @@ persona(
222
224
  type: "string",
223
225
  description: "Version to restore, e.g. 'v3' (for method=restore)"
224
226
  },
227
+ // === Debug sub-resource ===
228
+ debug: {
229
+ type: "object",
230
+ description: "Debug operations (requires persona id)",
231
+ properties: {
232
+ method: {
233
+ type: "string",
234
+ enum: ["conversations", "conversation_detail", "show_work", "action_detail", "search"],
235
+ description: "Debug operation to perform (required)"
236
+ },
237
+ conversation_id: { type: "string", description: "Conversation ID (for conversation_detail)" },
238
+ workflow_run_id: { type: "string", description: "Workflow run ID (for show_work, action_detail)" },
239
+ action_name: { type: "string", description: "Action name (for action_detail)" },
240
+ query: { type: "string", description: "Search query (for search)" },
241
+ },
242
+ required: ["method"],
243
+ },
225
244
  // === Data sub-resource ===
226
245
  data: {
227
246
  type: "object",
@@ -625,6 +644,90 @@ persona(
625
644
  required: ["method"],
626
645
  },
627
646
  },
647
+ // ═══════════════════════════════════════════════════════════════════════════
648
+ // 7. DEBUG - Audit conversations + workflow execution traces
649
+ // ═══════════════════════════════════════════════════════════════════════════
650
+ {
651
+ name: "debug",
652
+ description: `Inspect workflow executions and audit conversations. Drill down from conversations → messages → workflow traces → action details.
653
+
654
+ **IMPORTANT**: All operations require explicit \`method\` parameter.
655
+
656
+ ## Typical debugging flow
657
+ 1. \`debug(method="conversations", persona_id="abc")\` - list audit conversations
658
+ 2. \`debug(method="conversation_detail", conversation_id="...")\` - see messages with workflow_run_ids
659
+ 3. \`debug(method="show_work", persona_id="abc", workflow_run_id="...")\` - see all actions' execution traces
660
+ 4. \`debug(method="action_detail", persona_id="abc", workflow_run_id="...", action_name="...")\` - deep trace (inputs, outputs, LLM calls, steps)
661
+
662
+ ## Search
663
+ - \`debug(method="search", persona_id="abc", query="pricing")\` - full-text search across conversation messages
664
+
665
+ ## Also available as persona sub-resource
666
+ - \`persona(id="abc", debug={method:"conversations"})\` - same as debug tool but scoped to persona`,
667
+ inputSchema: {
668
+ type: "object",
669
+ properties: {
670
+ method: {
671
+ type: "string",
672
+ enum: ["conversations", "conversation_detail", "show_work", "action_detail", "search"],
673
+ description: "Operation to perform (required)",
674
+ },
675
+ persona_id: {
676
+ type: "string",
677
+ description: "Persona ID (required for conversations, show_work, action_detail, search)",
678
+ },
679
+ conversation_id: {
680
+ type: "string",
681
+ description: "Conversation ID (for method=conversation_detail, or to scope search)",
682
+ },
683
+ workflow_run_id: {
684
+ type: "string",
685
+ description: "Workflow run ID (for method=show_work, action_detail)",
686
+ },
687
+ action_name: {
688
+ type: "string",
689
+ description: "Action name within the workflow (for method=action_detail). Get from show_work results.",
690
+ },
691
+ query: {
692
+ type: "string",
693
+ description: "Search query string (for method=search)",
694
+ },
695
+ start_time: {
696
+ type: "string",
697
+ description: "Filter start time as ISO string (for method=conversations, search)",
698
+ },
699
+ end_time: {
700
+ type: "string",
701
+ description: "Filter end time as ISO string (for method=conversations, search)",
702
+ },
703
+ limit: {
704
+ type: "number",
705
+ description: "Max results to return (for method=conversations)",
706
+ },
707
+ offset: {
708
+ type: "number",
709
+ description: "Pagination offset (for method=conversations)",
710
+ },
711
+ channel: {
712
+ type: "string",
713
+ description: "Filter by channel (for method=conversations)",
714
+ },
715
+ resolution: {
716
+ type: "string",
717
+ description: "Filter by resolution status (for method=conversations)",
718
+ },
719
+ rating: {
720
+ type: "string",
721
+ description: "Filter by user rating (for method=conversations)",
722
+ },
723
+ env: {
724
+ type: "string",
725
+ description: envDescription,
726
+ },
727
+ },
728
+ required: ["method"],
729
+ },
730
+ },
628
731
  ];
629
732
  }
630
733
  /**
@@ -632,7 +735,6 @@ persona(
632
735
  * These still work for backwards compat but aren't in the tool list
633
736
  */
634
737
  export const INTERNAL_TOOLS = [
635
- "knowledge", // → persona(id=..., data={method:...})
636
738
  "demo", // → persona(id=..., data={method:"upload", content:...})
637
739
  "action", // → catalog(method="list", type="actions")
638
740
  "template", // → catalog(method="list", type="templates")
@@ -20,6 +20,10 @@
20
20
  * ```
21
21
  */
22
22
  import { EmaClientV2 } from './ema-client.js';
23
+ import { toJson } from '@bufbuild/protobuf';
24
+ import { GetConversationReviewsResponseSchema, GetConversationReviewDetailResponseSchema, } from './generated/protos/service/conversation_review/v1/conversation_review_pb.js';
25
+ import { WorkflowLevelDebugLogResponseSchema, ActionLevelShowWorkLogResponseSchema, } from './generated/protos/service/persona/v1/debug_logs_pb.js';
26
+ import { SearchMessagesResponseSchema, } from './generated/protos/service/debugger/service_pb.js';
23
27
  /**
24
28
  * Adapter that wraps EmaClientV2 with the legacy EmaClient interface.
25
29
  *
@@ -30,6 +34,9 @@ export class EmaClientAdapter {
30
34
  env;
31
35
  tokenRefreshConfig;
32
36
  refreshIntervalId;
37
+ static SYNC_TAG_REGEX = /<!-- synced_from:([^/]+)\/([a-f0-9-]+) -->/;
38
+ static LEGACY_JSON_REGEX = /<!-- _ema_sync:(.+?) -->/;
39
+ static SYNC_CLEANUP_REGEX = /<!-- (?:synced_from|sync|_ema_sync):[^\n]*-->/g;
33
40
  constructor(env, opts) {
34
41
  this.env = env;
35
42
  this.client = new EmaClientV2(env);
@@ -65,6 +72,8 @@ export class EmaClientAdapter {
65
72
  }
66
73
  try {
67
74
  const newToken = await this.tokenRefreshConfig.refreshCallback();
75
+ if (!newToken)
76
+ return false;
68
77
  this.updateToken(newToken);
69
78
  return true;
70
79
  }
@@ -136,18 +145,22 @@ export class EmaClientAdapter {
136
145
  }
137
146
  }
138
147
  /**
139
- * Create a new AI Employee
148
+ * Create a new AI Employee.
149
+ * Passes all fields through to the V2 client (template_id, source_persona_id, etc.)
140
150
  */
141
- async createAiEmployee(req) {
142
- // persona_template_id is required by the API
151
+ async createAiEmployee(req, _opts) {
143
152
  const templateId = req.persona_template_id ?? req.template_id;
144
- if (!templateId) {
145
- throw new Error('persona_template_id or template_id is required');
153
+ if (!templateId && !req.source_persona_id) {
154
+ throw new Error('persona_template_id, template_id, or source_persona_id is required');
146
155
  }
147
156
  const result = await this.client.createAiEmployee({
148
157
  name: req.name,
149
- persona_template_id: templateId,
158
+ template_id: templateId,
150
159
  description: req.description,
160
+ source_persona_id: req.source_persona_id,
161
+ clone_data: req.clone_data,
162
+ proto_config: req.proto_config,
163
+ trigger_type: req.trigger_type,
151
164
  });
152
165
  return {
153
166
  persona_id: result.persona_id ?? '',
@@ -157,7 +170,7 @@ export class EmaClientAdapter {
157
170
  /**
158
171
  * Update an existing AI Employee
159
172
  */
160
- async updateAiEmployee(req) {
173
+ async updateAiEmployee(req, _opts) {
161
174
  const result = await this.client.updateAiEmployee({
162
175
  persona_id: req.persona_id,
163
176
  workflow: req.workflow,
@@ -231,15 +244,38 @@ export class EmaClientAdapter {
231
244
  // Dashboard Operations
232
245
  // ═══════════════════════════════════════════════════════════════════════════
233
246
  /**
234
- * Get dashboard schema
247
+ * Get dashboard rows via gRPC.
248
+ * Proto response is structurally compatible with legacy DashboardRowsResponse.
235
249
  */
236
- async getDashboardSchema(personaId, _dashboardId) {
237
- const response = await this.client.getDashboardSchema();
238
- // Extract columns from proto response
239
- const schema = response;
240
- return {
241
- columns: schema.schema?.columns ?? [],
242
- };
250
+ async getDashboardRows(dashboardId, personaId, opts) {
251
+ const response = await this.client.getDashboardRows(dashboardId, personaId, { limit: opts?.limit });
252
+ return response;
253
+ }
254
+ /**
255
+ * Get dashboard row result (for polling completion status and retrieving output).
256
+ */
257
+ async getDashboardRowResult(personaId, rowId, includeFileContents) {
258
+ return this.client.getDashboardRowResult(personaId, rowId, includeFileContents);
259
+ }
260
+ /**
261
+ * Upload inputs and run a new dashboard row (triggers workflow execution).
262
+ */
263
+ async uploadAndRunDashboardRow(personaId, inputs) {
264
+ return this.client.uploadAndRunDashboardRow(personaId, inputs);
265
+ }
266
+ /**
267
+ * Re-run the workflow for an existing dashboard row.
268
+ */
269
+ async rerunDashboardRow(personaId, rowId) {
270
+ return this.client.rerunDashboardRow(personaId, rowId);
271
+ }
272
+ /**
273
+ * Get dashboard schema (columns).
274
+ * Legacy signature takes (dashboardId, personaId).
275
+ */
276
+ async getDashboardSchema(dashboardId, personaId) {
277
+ const rows = await this.getDashboardRows(dashboardId, personaId, { limit: 1 });
278
+ return rows.schema;
243
279
  }
244
280
  // ═══════════════════════════════════════════════════════════════════════════
245
281
  // Data Source Operations
@@ -323,20 +359,66 @@ export class EmaClientAdapter {
323
359
  return this.client.waitForReplication(requestId, personaId, opts);
324
360
  }
325
361
  // ═══════════════════════════════════════════════════════════════════════════
362
+ // Document Generation Operations
363
+ // ═══════════════════════════════════════════════════════════════════════════
364
+ /**
365
+ * Create a new document generation request.
366
+ * Returns immediately with a document_id for polling.
367
+ */
368
+ async createDocument(personaId, instructions) {
369
+ const result = await this.client.createDocument(personaId, instructions);
370
+ return result;
371
+ }
372
+ /**
373
+ * Retrieve document generation status and content.
374
+ * Poll until status is COMPLETED or FAILED.
375
+ */
376
+ async retrieveDocument(personaId, documentId, projectId) {
377
+ const result = await this.client.retrieveDocument(personaId, documentId, projectId);
378
+ return result;
379
+ }
380
+ /**
381
+ * Generate a document end-to-end: create + poll until completion.
382
+ */
383
+ async generateDocument(personaId, instructions, opts) {
384
+ const result = await this.client.generateDocument(personaId, instructions, opts);
385
+ return result;
386
+ }
387
+ /**
388
+ * Regenerate a section of a document based on user instructions.
389
+ */
390
+ async regenerateDocument(personaId, request) {
391
+ const result = await this.client.regenerateDocument(personaId, request);
392
+ return result;
393
+ }
394
+ /**
395
+ * Update (replace) the content of a document.
396
+ */
397
+ async updateDocument(personaId, request) {
398
+ const result = await this.client.updateDocument(personaId, request);
399
+ return result;
400
+ }
401
+ // ═══════════════════════════════════════════════════════════════════════════
326
402
  // Sync Metadata Operations
327
403
  // ═══════════════════════════════════════════════════════════════════════════
328
404
  /**
329
- * Get sync metadata from a persona
405
+ * Get sync metadata from a persona's description.
406
+ * Supports compact format (<!-- synced_from:env/id -->),
407
+ * legacy JSON format (<!-- _ema_sync:{...} -->),
408
+ * and proto_config._ema_sync fallback.
330
409
  */
331
410
  getSyncMetadata(persona) {
332
- // Sync metadata is stored in custom_metadata
333
- const customMeta = persona.custom_metadata;
334
- if (!customMeta)
335
- return null;
336
- const syncData = customMeta['ema_mcp_sync'];
337
- if (!syncData || typeof syncData !== 'object')
338
- return null;
339
- return syncData;
411
+ const desc = persona.description;
412
+ if (desc) {
413
+ const meta = this.extractSyncMetadataFromString(desc);
414
+ if (meta)
415
+ return meta;
416
+ }
417
+ const protoConfig = persona.proto_config;
418
+ if (protoConfig?._ema_sync) {
419
+ return protoConfig._ema_sync;
420
+ }
421
+ return null;
340
422
  }
341
423
  /**
342
424
  * Check if persona was synced from another environment
@@ -344,6 +426,133 @@ export class EmaClientAdapter {
344
426
  isSyncedPersona(persona) {
345
427
  return this.getSyncMetadata(persona) !== null;
346
428
  }
429
+ /**
430
+ * List all personas that have sync metadata.
431
+ */
432
+ async listSyncedPersonas() {
433
+ const personas = await this.getPersonasForTenant();
434
+ const synced = [];
435
+ for (const p of personas) {
436
+ const meta = this.getSyncMetadata(p);
437
+ if (meta) {
438
+ synced.push({ persona: p, syncMetadata: meta });
439
+ }
440
+ }
441
+ return synced;
442
+ }
443
+ /**
444
+ * Find a synced persona by its master environment and master ID.
445
+ */
446
+ async findSyncedPersona(masterEnv, masterId) {
447
+ const synced = await this.listSyncedPersonas();
448
+ return (synced.find((s) => s.syncMetadata.master_env === masterEnv && s.syncMetadata.master_id === masterId) ?? null);
449
+ }
450
+ /**
451
+ * Tag a persona as synced by appending metadata to description.
452
+ */
453
+ async tagAsSynced(personaId, metadata, currentDescription, existingProtoConfig) {
454
+ const cleanDesc = this.getCleanDescription(currentDescription);
455
+ const marker = `<!-- synced_from:${metadata.master_env}/${metadata.master_id} -->`;
456
+ const newDescription = cleanDesc ? `${cleanDesc}\n\n${marker}` : marker;
457
+ await this.updateAiEmployee({
458
+ persona_id: personaId,
459
+ proto_config: existingProtoConfig ?? {},
460
+ description: newDescription,
461
+ });
462
+ }
463
+ /**
464
+ * Remove sync metadata from a persona (unlink from master).
465
+ */
466
+ async removeSyncTag(personaId, currentDescription, existingProtoConfig) {
467
+ const cleanDesc = this.getCleanDescription(currentDescription);
468
+ await this.updateAiEmployee({
469
+ persona_id: personaId,
470
+ proto_config: existingProtoConfig ?? {},
471
+ description: cleanDesc,
472
+ });
473
+ }
474
+ // ═══════════════════════════════════════════════════════════════════════════
475
+ // Utility Operations
476
+ // ═══════════════════════════════════════════════════════════════════════════
477
+ /**
478
+ * Extract file list from persona's status_log.
479
+ */
480
+ extractFilesFromStatusLog(persona, widgetName = "fileUpload") {
481
+ const statusLog = persona.status_log;
482
+ if (!statusLog)
483
+ return [];
484
+ const files = statusLog[widgetName];
485
+ if (!Array.isArray(files))
486
+ return [];
487
+ return files
488
+ .filter((f) => typeof f === "object" && f !== null)
489
+ .map((f) => ({
490
+ id: f.id ?? f.filename ?? "",
491
+ filename: f.filename ?? f.id ?? "",
492
+ status: f.status ?? "unknown",
493
+ tags: Array.isArray(f.tags) ? f.tags : undefined,
494
+ }))
495
+ .filter((f) => f.id !== "");
496
+ }
497
+ /**
498
+ * Get workflow definition by workflow_id.
499
+ * Not used by handlers — falls back gracefully.
500
+ */
501
+ async getWorkflowDef(_workflowId) {
502
+ // V2 client doesn't expose a direct getWorkflow-by-id method.
503
+ // Handlers get workflow_def from getPersonaById() instead.
504
+ return null;
505
+ }
506
+ /**
507
+ * Check/validate a workflow definition.
508
+ */
509
+ async checkWorkflow(workflowDef) {
510
+ const result = await this.client.checkWorkflow(workflowDef);
511
+ return result;
512
+ }
513
+ // ═══════════════════════════════════════════════════════════════════════════
514
+ // Debug / Audit Operations
515
+ // ═══════════════════════════════════════════════════════════════════════════
516
+ /**
517
+ * List audit conversations for a persona with optional filters.
518
+ * Uses toJson() for proper proto→JSON serialization of Timestamp and nested types.
519
+ *
520
+ * Opts shape matches EmaClientV2 / GrpcClient exactly — no translation needed.
521
+ */
522
+ async getConversationReviews(personaId, opts) {
523
+ const response = await this.client.getConversationReviews(personaId, opts);
524
+ return toJson(GetConversationReviewsResponseSchema, response);
525
+ }
526
+ /**
527
+ * Get full conversation detail including messages with workflow_run_ids.
528
+ */
529
+ async getConversationReviewDetail(conversationId) {
530
+ const response = await this.client.getConversationReviewDetail(conversationId);
531
+ return toJson(GetConversationReviewDetailResponseSchema, response);
532
+ }
533
+ /**
534
+ * Get workflow-level debug log (all actions' show work for a run).
535
+ * personaId is required — DebugLogService uses X-Persona-Id header for auth.
536
+ */
537
+ async getWorkflowLevelDebugLog(workflowRunId, personaId) {
538
+ const response = await this.client.getWorkflowLevelDebugLog(workflowRunId, personaId);
539
+ return toJson(WorkflowLevelDebugLogResponseSchema, response);
540
+ }
541
+ /**
542
+ * Get action-level show work log (deep trace for a single action).
543
+ * personaId is required — DebugLogService uses X-Persona-Id header for auth.
544
+ */
545
+ async getActionLevelShowWorkLog(actionName, workflowRunId, personaId) {
546
+ const response = await this.client.getActionLevelShowWorkLog(actionName, workflowRunId, personaId);
547
+ return toJson(ActionLevelShowWorkLogResponseSchema, response);
548
+ }
549
+ /**
550
+ * Full-text search across conversation messages.
551
+ */
552
+ async searchMessages(opts) {
553
+ const response = await this.client.searchMessages(opts);
554
+ return toJson(SearchMessagesResponseSchema, response);
555
+ }
347
556
  // ═══════════════════════════════════════════════════════════════════════════
348
557
  // Private Helpers
349
558
  // ═══════════════════════════════════════════════════════════════════════════
@@ -388,6 +597,38 @@ export class EmaClientAdapter {
388
597
  proto_config: t.proto_config,
389
598
  };
390
599
  }
600
+ /**
601
+ * Extract sync metadata from a string (description field).
602
+ * Supports compact format and legacy JSON format.
603
+ */
604
+ extractSyncMetadataFromString(text) {
605
+ const compactMatch = text.match(EmaClientAdapter.SYNC_TAG_REGEX);
606
+ if (compactMatch) {
607
+ return {
608
+ master_env: compactMatch[1],
609
+ master_id: compactMatch[2],
610
+ synced_at: new Date().toISOString(),
611
+ };
612
+ }
613
+ const legacyMatch = text.match(EmaClientAdapter.LEGACY_JSON_REGEX);
614
+ if (legacyMatch) {
615
+ try {
616
+ return JSON.parse(legacyMatch[1]);
617
+ }
618
+ catch {
619
+ return null;
620
+ }
621
+ }
622
+ return null;
623
+ }
624
+ /**
625
+ * Extract the clean description (without sync marker).
626
+ */
627
+ getCleanDescription(description) {
628
+ if (!description)
629
+ return "";
630
+ return description.replace(EmaClientAdapter.SYNC_CLEANUP_REGEX, "").trim();
631
+ }
391
632
  /**
392
633
  * Convert null to undefined for type compatibility
393
634
  */
@@ -38,14 +38,49 @@ function createRestClient(env) {
38
38
  request.headers.set('Authorization', `Bearer ${env.bearerToken}`);
39
39
  return request;
40
40
  });
41
- // Add error handling interceptor
41
+ // Add error handling interceptor — preserve full response body for debugging
42
42
  client.interceptors.response.use(async (response) => {
43
43
  if (!response.ok) {
44
44
  const body = await response.text().catch(() => '');
45
+ let detail = response.statusText;
46
+ try {
47
+ const parsed = JSON.parse(body);
48
+ const parts = [];
49
+ if (parsed.detail)
50
+ parts.push(parsed.detail);
51
+ if (parsed.message)
52
+ parts.push(parsed.message);
53
+ if (parsed.error)
54
+ parts.push(parsed.error);
55
+ if (parsed.gwe_issues) {
56
+ const gwe = parsed.gwe_issues;
57
+ if (gwe.detail)
58
+ parts.push(`GWE: ${gwe.detail}`);
59
+ if (Array.isArray(gwe.missing_inputs)) {
60
+ parts.push(`Missing inputs: ${gwe.missing_inputs.map((i) => `${i.action_name}.${i.input_name}`).join(', ')}`);
61
+ }
62
+ if (Array.isArray(gwe.mismatched_inputs)) {
63
+ parts.push(`Mismatched inputs: ${gwe.mismatched_inputs.map((i) => `${i.action_name}.${i.input_name}`).join(', ')}`);
64
+ }
65
+ if (Array.isArray(gwe.named_result_errors)) {
66
+ parts.push(`Result errors: ${gwe.named_result_errors.map((e) => `${e.named_result_key}: ${e.error_description}`).join('; ')}`);
67
+ }
68
+ if (gwe.has_cycles)
69
+ parts.push('Workflow has cycles');
70
+ if (gwe.has_no_outputs)
71
+ parts.push('Workflow has no outputs');
72
+ }
73
+ if (parts.length > 0)
74
+ detail = parts.join(' | ');
75
+ }
76
+ catch {
77
+ if (body)
78
+ detail = body.slice(0, 500);
79
+ }
45
80
  throw new EmaApiError({
46
81
  statusCode: response.status,
47
82
  body,
48
- message: `API error (${response.status}): ${response.statusText}`,
83
+ message: `API error (${response.status}): ${detail}`,
49
84
  });
50
85
  }
51
86
  return response;
@@ -163,12 +198,24 @@ export class EmaClientV2 {
163
198
  }
164
199
  }
165
200
  /**
166
- * Create a new persona from a template
201
+ * Create a new persona from a template.
202
+ *
203
+ * Handles field name translation: callers use `persona_template_id` (legacy)
204
+ * but the generated API expects `template_id`.
167
205
  */
168
206
  async createPersona(data) {
207
+ const templateId = data.template_id ?? data.persona_template_id;
169
208
  const result = await api.createPersona({
170
209
  client: this.restClient,
171
- body: data,
210
+ body: {
211
+ name: data.name,
212
+ template_id: templateId,
213
+ description: data.description,
214
+ source_persona_id: data.source_persona_id,
215
+ clone_data: data.clone_data,
216
+ proto_config: data.proto_config,
217
+ trigger_type: data.trigger_type,
218
+ },
172
219
  });
173
220
  return result.data;
174
221
  }
@@ -187,6 +234,7 @@ export class EmaClientV2 {
187
234
  let workflow = data.workflow;
188
235
  // CRITICAL: Handle workflow namespace quirk at SDK level
189
236
  if (workflow) {
237
+ workflow = JSON.parse(JSON.stringify(workflow));
190
238
  const incomingWfName = workflow.workflowName;
191
239
  const incomingIsPersonaScoped = incomingWfName?.name?.namespaces?.[1] === "personas" &&
192
240
  incomingWfName?.name?.namespaces?.[2] === personaId;
@@ -198,20 +246,28 @@ export class EmaClientV2 {
198
246
  const existingWorkflow = existing?.workflow_def;
199
247
  const existingWfName = existingWorkflow?.workflowName;
200
248
  if (existingWfName?.name) {
201
- // Copy namespace from existing workflow
202
- workflow = { ...workflow, workflowName: existingWfName };
249
+ workflow.workflowName = existingWfName;
203
250
  }
204
251
  else {
205
- // Generate a valid namespace for personas without existing workflows
206
- // Format: ["ema", "personas", "<persona_id>"] with name "workflow"
207
252
  const generatedWfName = {
208
253
  name: {
209
254
  namespaces: ["ema", "personas", personaId],
210
255
  name: "workflow",
211
256
  },
212
257
  };
213
- workflow = { ...workflow, workflowName: generatedWfName };
258
+ workflow.workflowName = generatedWfName;
259
+ }
260
+ }
261
+ const results = workflow.results;
262
+ if (results) {
263
+ const fixedResults = {};
264
+ for (const [key, value] of Object.entries(results)) {
265
+ if (value.actionName && value.outputName) {
266
+ const correctKey = `${value.actionName}.${value.outputName}`;
267
+ fixedResults[correctKey] = value;
268
+ }
214
269
  }
270
+ workflow.results = fixedResults;
215
271
  }
216
272
  }
217
273
  const result = await api.updatePersona({
@@ -311,6 +367,41 @@ export class EmaClientV2 {
311
367
  return this.grpcClient.getReplicationStatus(requestId);
312
368
  }
313
369
  // ─────────────────────────────────────────────────────────────────────────────
370
+ // Debug & Audit Operations - gRPC (ConversationReview, DebugLog, Debugger)
371
+ // ─────────────────────────────────────────────────────────────────────────────
372
+ /**
373
+ * List conversation reviews for a persona (audit tab).
374
+ */
375
+ async getConversationReviews(personaId, opts) {
376
+ return this.grpcClient.getConversationReviews(personaId, opts);
377
+ }
378
+ /**
379
+ * Get detailed conversation review with messages and workflow run IDs.
380
+ */
381
+ async getConversationReviewDetail(conversationId) {
382
+ return this.grpcClient.getConversationReviewDetail(conversationId);
383
+ }
384
+ /**
385
+ * Get workflow-level debug log (show work for all actions in a run).
386
+ * personaId is required — DebugLogService uses X-Persona-Id header for auth.
387
+ */
388
+ async getWorkflowLevelDebugLog(workflowRunId, personaId) {
389
+ return this.grpcClient.getWorkflowLevelDebugLog(workflowRunId, personaId);
390
+ }
391
+ /**
392
+ * Get action-level show work log (deep trace for a single action).
393
+ * personaId is required — DebugLogService uses X-Persona-Id header for auth.
394
+ */
395
+ async getActionLevelShowWorkLog(actionName, workflowRunId, personaId) {
396
+ return this.grpcClient.getActionLevelShowWorkLog(actionName, workflowRunId, personaId);
397
+ }
398
+ /**
399
+ * Search messages across conversations.
400
+ */
401
+ async searchMessages(opts) {
402
+ return this.grpcClient.searchMessages(opts);
403
+ }
404
+ // ─────────────────────────────────────────────────────────────────────────────
314
405
  // Dashboard Row Operations - REST API (not in OpenAPI spec)
315
406
  // ─────────────────────────────────────────────────────────────────────────────
316
407
  /**