@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/cli/index.js +2 -2
- package/dist/mcp/domain/loop-detection.js +46 -54
- package/dist/mcp/domain/sanitizer.js +1 -1
- package/dist/mcp/domain/workflow-graph.js +2 -2
- package/dist/mcp/domain/workflow-path-enumerator.js +7 -4
- package/dist/mcp/guidance.js +53 -0
- package/dist/mcp/handlers/debug/adapter.js +15 -0
- package/dist/mcp/handlers/debug/formatters.js +282 -0
- package/dist/mcp/handlers/debug/index.js +133 -0
- package/dist/mcp/handlers/demo/adapter.js +180 -0
- package/dist/mcp/handlers/env/config.js +2 -2
- package/dist/mcp/handlers/index.js +0 -1
- package/dist/mcp/handlers/persona/adapter.js +135 -0
- package/dist/mcp/handlers/sync/adapter.js +200 -0
- package/dist/mcp/handlers/workflow/adapter.js +174 -0
- package/dist/mcp/handlers/workflow/fix.js +11 -12
- package/dist/mcp/handlers/workflow/index.js +0 -24
- package/dist/mcp/knowledge-guidance-topics.js +615 -0
- package/dist/mcp/knowledge.js +23 -612
- package/dist/mcp/resources-dynamic.js +2395 -0
- package/dist/mcp/resources-validation.js +408 -0
- package/dist/mcp/resources.js +72 -2724
- package/dist/mcp/server.js +33 -832
- package/dist/mcp/tools.js +104 -2
- package/dist/sdk/client-adapter.js +265 -24
- package/dist/sdk/ema-client.js +100 -9
- package/dist/sdk/generated/well-known-types.js +99 -0
- package/dist/sdk/grpc-client.js +115 -1
- package/dist/sync/sdk.js +2 -2
- package/dist/sync.js +4 -3
- package/package.json +3 -2
- package/dist/mcp/handlers/knowledge/index.js +0 -54
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
247
|
+
* Get dashboard rows via gRPC.
|
|
248
|
+
* Proto response is structurally compatible with legacy DashboardRowsResponse.
|
|
235
249
|
*/
|
|
236
|
-
async
|
|
237
|
-
const response = await this.client.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
*/
|
package/dist/sdk/ema-client.js
CHANGED
|
@@ -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}): ${
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
/**
|