@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.
- 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/cli/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* config validate - Validate config file
|
|
12
12
|
*/
|
|
13
13
|
import { loadConfig } from "../sdk/config.js";
|
|
14
|
-
import {
|
|
14
|
+
import { EmaClientAdapter } from "../sdk/client-adapter.js";
|
|
15
15
|
import { SyncSDK } from "../sync/sdk.js";
|
|
16
16
|
function printUsage() {
|
|
17
17
|
console.log(`
|
|
@@ -220,7 +220,7 @@ async function runAgentsCommand(subcommand, args, options) {
|
|
|
220
220
|
baseUrl: master.baseUrl,
|
|
221
221
|
bearerToken: getEnvOrThrow(master.bearerTokenEnv),
|
|
222
222
|
};
|
|
223
|
-
const client = new
|
|
223
|
+
const client = new EmaClientAdapter(env);
|
|
224
224
|
switch (subcommand) {
|
|
225
225
|
case "list": {
|
|
226
226
|
const actions = await client.listAgents();
|
|
@@ -1,31 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Loop Detection for Workflow Graphs
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Uses the shared WorkflowGraph representation from workflow-graph.ts
|
|
5
|
+
* for cycle and re-entry detection in workflow validation.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
function buildGraphMaps(nodes) {
|
|
9
|
-
const forward = new Map();
|
|
10
|
-
const nodeMap = new Map();
|
|
11
|
-
for (const node of nodes) {
|
|
12
|
-
nodeMap.set(node.id, node);
|
|
13
|
-
forward.set(node.id, new Set());
|
|
14
|
-
}
|
|
15
|
-
for (const node of nodes) {
|
|
16
|
-
if (node.incoming_edges) {
|
|
17
|
-
for (const edge of node.incoming_edges) {
|
|
18
|
-
const sourceId = edge.source_node_id;
|
|
19
|
-
forward.get(sourceId)?.add(node.id);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return { forward, nodeMap };
|
|
24
|
-
}
|
|
7
|
+
import { buildWorkflowGraph } from "./workflow-graph.js";
|
|
25
8
|
export function detectLoops(workflowDef) {
|
|
26
|
-
|
|
9
|
+
if (!workflowDef || typeof workflowDef !== "object")
|
|
10
|
+
return [];
|
|
11
|
+
const graph = buildWorkflowGraph(workflowDef);
|
|
12
|
+
if (graph.nodes.size === 0)
|
|
13
|
+
return [];
|
|
27
14
|
const loops = [];
|
|
28
|
-
const { forward
|
|
15
|
+
const { forward } = graph;
|
|
29
16
|
const visited = new Set();
|
|
30
17
|
const recursionStack = new Set();
|
|
31
18
|
const path = [];
|
|
@@ -33,7 +20,7 @@ export function detectLoops(workflowDef) {
|
|
|
33
20
|
visited.add(nodeId);
|
|
34
21
|
recursionStack.add(nodeId);
|
|
35
22
|
path.push(nodeId);
|
|
36
|
-
for (const neighbor of (forward.get(nodeId)
|
|
23
|
+
for (const neighbor of (forward.get(nodeId) ?? [])) {
|
|
37
24
|
if (!visited.has(neighbor)) {
|
|
38
25
|
if (dfs(neighbor))
|
|
39
26
|
return true;
|
|
@@ -56,42 +43,47 @@ export function detectLoops(workflowDef) {
|
|
|
56
43
|
recursionStack.delete(nodeId);
|
|
57
44
|
return false;
|
|
58
45
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
dfs(node.id);
|
|
46
|
+
if (graph.trigger)
|
|
47
|
+
dfs(graph.trigger);
|
|
48
|
+
for (const nodeId of graph.nodes.keys()) {
|
|
49
|
+
if (!visited.has(nodeId))
|
|
50
|
+
dfs(nodeId);
|
|
65
51
|
}
|
|
66
52
|
// Detect categorizer routing back to upstream (re-entry)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
53
|
+
detectReentryLoops(graph, loops);
|
|
54
|
+
return loops;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Detect categorizer nodes routing back to upstream nodes (re-entry risk).
|
|
58
|
+
* Uses the graph's reverse adjacency map instead of manual edge traversal.
|
|
59
|
+
*/
|
|
60
|
+
function detectReentryLoops(graph, loops) {
|
|
61
|
+
for (const [nodeId, graphNode] of graph.nodes) {
|
|
62
|
+
if (graphNode.actionType !== 'chat_categorizer')
|
|
63
|
+
continue;
|
|
64
|
+
const downstream = graph.forward.get(nodeId) ?? new Set();
|
|
65
|
+
const upstream = new Set();
|
|
66
|
+
function collectUpstream(nId, depth = 0) {
|
|
67
|
+
if (depth > 20)
|
|
68
|
+
return;
|
|
69
|
+
for (const parent of (graph.reverse.get(nId) ?? [])) {
|
|
70
|
+
if (!upstream.has(parent)) {
|
|
71
|
+
upstream.add(parent);
|
|
72
|
+
collectUpstream(parent, depth + 1);
|
|
80
73
|
}
|
|
81
74
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
75
|
+
}
|
|
76
|
+
collectUpstream(nodeId);
|
|
77
|
+
for (const downNode of downstream) {
|
|
78
|
+
if (upstream.has(downNode)) {
|
|
79
|
+
loops.push({
|
|
80
|
+
type: 'reentry_loop',
|
|
81
|
+
nodes: [nodeId, downNode],
|
|
82
|
+
description: `Categorizer "${graphNode.displayName || nodeId}" routes to "${downNode}" which is upstream - can cause re-processing`,
|
|
83
|
+
severity: 'warning',
|
|
84
|
+
fixSuggestion: `Add state tracking or one-time-execution gate to prevent re-processing.`,
|
|
85
|
+
});
|
|
93
86
|
}
|
|
94
87
|
}
|
|
95
88
|
}
|
|
96
|
-
return loops;
|
|
97
89
|
}
|
|
@@ -896,7 +896,7 @@ export function applySessionMappings(text, session) {
|
|
|
896
896
|
* Content fields that SHOULD be sanitized (allowlist approach).
|
|
897
897
|
* Only these fields contain user-facing content that may have PII.
|
|
898
898
|
*
|
|
899
|
-
*
|
|
899
|
+
* Manually maintained — candidate for auto-generation from OpenAPI/gRPC definitions.
|
|
900
900
|
*/
|
|
901
901
|
const CONTENT_FIELDS = new Set([
|
|
902
902
|
// Persona-level
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Single source of truth for parsing workflow_def JSON into a typed graph.
|
|
5
5
|
* Used by the workflow optimizer for static analysis.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* `loop-detection.ts` uses `buildWorkflowGraph()` from this module for
|
|
8
|
+
* cycle and re-entry detection (consolidated from a prior local `buildGraphMaps`).
|
|
9
9
|
*/
|
|
10
10
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
11
|
// Constants
|
|
@@ -50,7 +50,7 @@ export function enumeratePaths(workflow, options) {
|
|
|
50
50
|
throw new Error("Branch outcome without branch_point");
|
|
51
51
|
}
|
|
52
52
|
for (const value of result.branch_point.possible_values) {
|
|
53
|
-
const forkedSession = forkSessionForBranching(session, result.branch_point, value);
|
|
53
|
+
const forkedSession = forkSessionForBranching(session, result.branch_point, value, workflow);
|
|
54
54
|
sessionsToProcess.push(forkedSession);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
@@ -80,7 +80,7 @@ function createInitialSession(workflow) {
|
|
|
80
80
|
action_states: new Map(),
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
|
-
function forkSessionForBranching(session, branchInfo, chosenValue) {
|
|
83
|
+
function forkSessionForBranching(session, branchInfo, chosenValue, workflow) {
|
|
84
84
|
// Deep copy session
|
|
85
85
|
const forked = {
|
|
86
86
|
completed_actions: [...session.completed_actions],
|
|
@@ -102,8 +102,11 @@ function forkSessionForBranching(session, branchInfo, chosenValue) {
|
|
|
102
102
|
node_id: branchInfo.action_that_branched,
|
|
103
103
|
output_name: branchInfo.branching_output_name,
|
|
104
104
|
});
|
|
105
|
-
//
|
|
106
|
-
|
|
105
|
+
// Add remaining outputs from the branching action (e.g. categorizer also produces confidence)
|
|
106
|
+
const branchingNode = workflow.nodes?.find(n => n.id === branchInfo.action_that_branched);
|
|
107
|
+
if (branchingNode) {
|
|
108
|
+
addActionOutputs(forked, branchingNode);
|
|
109
|
+
}
|
|
107
110
|
return forked;
|
|
108
111
|
}
|
|
109
112
|
// ─────────────────────────────────────────────────────────────────────────────
|
package/dist/mcp/guidance.js
CHANGED
|
@@ -366,6 +366,51 @@ export const TOOL_GUIDANCE = {
|
|
|
366
366
|
],
|
|
367
367
|
applicableRules: [],
|
|
368
368
|
},
|
|
369
|
+
debug: {
|
|
370
|
+
toolName: "debug",
|
|
371
|
+
quickTip: "Inspect workflow executions and audit conversations. Follow the drill-down: conversations → detail → show_work → action_detail.",
|
|
372
|
+
operations: [
|
|
373
|
+
{
|
|
374
|
+
name: "List conversations",
|
|
375
|
+
description: "List audit conversations for a persona",
|
|
376
|
+
example: 'debug(method="conversations", persona_id="abc")',
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
name: "Conversation detail",
|
|
380
|
+
description: "Get messages with workflow_run_ids",
|
|
381
|
+
example: 'debug(method="conversation_detail", conversation_id="...")',
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
name: "Show work",
|
|
385
|
+
description: "See all actions' execution traces for a workflow run",
|
|
386
|
+
example: 'debug(method="show_work", persona_id="abc", workflow_run_id="...")',
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
name: "Action detail",
|
|
390
|
+
description: "Deep trace: inputs, outputs, LLM calls, steps",
|
|
391
|
+
example: 'debug(method="action_detail", persona_id="abc", workflow_run_id="...", action_name="...")',
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
name: "Search messages",
|
|
395
|
+
description: "Full-text search across conversation messages",
|
|
396
|
+
example: 'debug(method="search", persona_id="abc", query="pricing")',
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
nextSteps: {
|
|
400
|
+
conversations: "Pick a conversation_id → debug(method='conversation_detail')",
|
|
401
|
+
conversation_detail: "Pick a workflow_run_id → debug(method='show_work', persona_id='...', workflow_run_id='...')",
|
|
402
|
+
show_work: "Pick an action_name (especially ERRORED ones) → debug(method='action_detail', persona_id='...', ...)",
|
|
403
|
+
action_detail: "Examine inputs/outputs/llm_calls to find the root cause",
|
|
404
|
+
search: "Pick a conversation_id from results → debug(method='conversation_detail')",
|
|
405
|
+
},
|
|
406
|
+
commonMistakes: [
|
|
407
|
+
"Jumping to action_detail without running show_work first (you need the action_name)",
|
|
408
|
+
"Forgetting persona_id — required for conversations, show_work, action_detail, and search",
|
|
409
|
+
"Not following _next_step hints in responses",
|
|
410
|
+
"The search method depends on DebuggerService which may not be deployed in all environments",
|
|
411
|
+
],
|
|
412
|
+
applicableRules: [],
|
|
413
|
+
},
|
|
369
414
|
};
|
|
370
415
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
371
416
|
// Contextual Tips (for tool responses)
|
|
@@ -434,8 +479,16 @@ All workflow modifications follow a 3-step flow:
|
|
|
434
479
|
|
|
435
480
|
The LLM generates the full workflow_def. MCP provides data and executes.
|
|
436
481
|
|
|
482
|
+
## Debugging Workflow Executions
|
|
483
|
+
Use the \`debug\` tool to inspect workflow runs and troubleshoot issues:
|
|
484
|
+
1. \`debug(method="conversations", persona_id="...")\` → list audit conversations
|
|
485
|
+
2. \`debug(method="show_work", workflow_run_id="...")\` → see action execution traces
|
|
486
|
+
3. \`debug(method="action_detail", ...)\` → deep trace with LLM calls, inputs/outputs
|
|
487
|
+
Also available as persona sub-resource: \`persona(id="abc", debug={method:"conversations"})\`
|
|
488
|
+
|
|
437
489
|
## Resources
|
|
438
490
|
- \`ema://docs/usage-guide\` - Complete guide
|
|
491
|
+
- \`ema://docs/debugging-guide\` - Debugging workflow executions
|
|
439
492
|
- \`ema://catalog/agents-summary\` - Action catalog
|
|
440
493
|
- \`ema://rules/anti-patterns\` - Common mistakes
|
|
441
494
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug Adapter
|
|
3
|
+
*
|
|
4
|
+
* Thin adapter that resolves client from env and delegates to handleDebug.
|
|
5
|
+
* Same pattern as workflow/adapter.ts.
|
|
6
|
+
*
|
|
7
|
+
* Extracted from server.ts to keep the dispatch table thin.
|
|
8
|
+
*/
|
|
9
|
+
import { handleDebug } from "./index.js";
|
|
10
|
+
export async function handleDebugAdapter(args, createClient, getDefaultEnvName) {
|
|
11
|
+
const targetEnv = args.env ?? getDefaultEnvName();
|
|
12
|
+
// createClient() returns EmaClientAdapter at runtime (see env/config.ts)
|
|
13
|
+
const client = createClient(targetEnv);
|
|
14
|
+
return handleDebug(args, client);
|
|
15
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug Handler Formatters
|
|
3
|
+
*
|
|
4
|
+
* Transforms raw proto→JSON debug responses into concise, actionable summaries.
|
|
5
|
+
* Applies value truncation for large strings and adds _next_step hints
|
|
6
|
+
* to guide the agent through the drill-down flow.
|
|
7
|
+
*/
|
|
8
|
+
const MAX_VALUE_LENGTH = 2000;
|
|
9
|
+
/** Action run status enum values from ShowWorkLog.ActionRunStatus */
|
|
10
|
+
const STATUS_LABELS = {
|
|
11
|
+
0: "UNSPECIFIED",
|
|
12
|
+
1: "SUCCESS",
|
|
13
|
+
2: "ERRORED",
|
|
14
|
+
3: "WARNING",
|
|
15
|
+
4: "NOT_RUN",
|
|
16
|
+
5: "PAUSED",
|
|
17
|
+
ACTION_RUN_STATUS_UNSPECIFIED: "UNSPECIFIED",
|
|
18
|
+
ACTION_RUN_STATUS_SUCCESS: "SUCCESS",
|
|
19
|
+
ACTION_RUN_STATUS_ERRORED: "ERRORED",
|
|
20
|
+
ACTION_RUN_STATUS_WARNING: "WARNING",
|
|
21
|
+
ACTION_RUN_STATUS_NOT_RUN: "NOT_RUN",
|
|
22
|
+
ACTION_RUN_STATUS_PAUSED: "PAUSED",
|
|
23
|
+
};
|
|
24
|
+
function truncateValue(value) {
|
|
25
|
+
if (typeof value === "string" && value.length > MAX_VALUE_LENGTH) {
|
|
26
|
+
return value.slice(0, MAX_VALUE_LENGTH) + `... [truncated, ${value.length} total chars]`;
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
return value.map(truncateValue);
|
|
30
|
+
}
|
|
31
|
+
if (value !== null && typeof value === "object") {
|
|
32
|
+
return truncateObject(value);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function truncateObject(obj) {
|
|
37
|
+
const result = {};
|
|
38
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
39
|
+
result[k] = truncateValue(v);
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
function resolveStatus(raw) {
|
|
44
|
+
if (typeof raw === "number" || typeof raw === "string") {
|
|
45
|
+
return STATUS_LABELS[raw] ?? String(raw);
|
|
46
|
+
}
|
|
47
|
+
return "UNKNOWN";
|
|
48
|
+
}
|
|
49
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
// conversations
|
|
51
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
export function formatConversations(data, personaId) {
|
|
53
|
+
// toJson() serializes the response field as "items" (proto field name)
|
|
54
|
+
const reviews = data.items ?? data.conversationReviews ?? data.conversation_reviews;
|
|
55
|
+
const items = Array.isArray(reviews) ? reviews : [];
|
|
56
|
+
return {
|
|
57
|
+
persona_id: personaId,
|
|
58
|
+
total: items.length,
|
|
59
|
+
conversations: items.map((c) => ({
|
|
60
|
+
conversation_id: c.conversationId ?? c.conversation_id,
|
|
61
|
+
channel: c.channel,
|
|
62
|
+
resolution_status: c.resolutionStatus ?? c.resolution_status,
|
|
63
|
+
user_rating: c.userRating ?? c.user_rating,
|
|
64
|
+
message_count: c.messageCount ?? c.message_count,
|
|
65
|
+
created_at: c.createdAt ?? c.created_at,
|
|
66
|
+
last_message_at: c.lastMessageAt ?? c.last_message_at,
|
|
67
|
+
user_identifier: c.userIdentifier ?? c.user_identifier,
|
|
68
|
+
})),
|
|
69
|
+
_tip: "Use conversation_id with debug(method='conversation_detail') to see messages and workflow_run_ids.",
|
|
70
|
+
_next_step: items.length > 0
|
|
71
|
+
? `debug(method="conversation_detail", conversation_id="${items[0].conversationId ?? items[0].conversation_id}")`
|
|
72
|
+
: "No conversations found. Try adjusting filters or date range.",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
76
|
+
// conversation_detail
|
|
77
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
78
|
+
export function formatConversationDetail(data, conversationId, personaId) {
|
|
79
|
+
const messages = data.messages ?? data.messageDetails ?? data.message_details;
|
|
80
|
+
const items = Array.isArray(messages) ? messages : [];
|
|
81
|
+
const formatted = items.map((m) => {
|
|
82
|
+
const workflowRunId = m.workflowRunId ?? m.workflow_run_id;
|
|
83
|
+
const chatMsg = (m.chatbotMessage ?? m.chatbot_message ?? {});
|
|
84
|
+
// Derive role from ChatbotMessage.type + isUserMessage
|
|
85
|
+
const msgType = chatMsg.type ?? chatMsg.message_type;
|
|
86
|
+
const isUser = chatMsg.isUserMessage ?? chatMsg.is_user_message;
|
|
87
|
+
const role = isUser ? "user" : (msgType ? "assistant" : undefined);
|
|
88
|
+
// Extract content from the message oneof (textMessage, buttonsMessage, etc.)
|
|
89
|
+
const textMsg = (chatMsg.textMessage ?? chatMsg.text_message);
|
|
90
|
+
const buttonsMsg = (chatMsg.buttonsMessage ?? chatMsg.buttons_message);
|
|
91
|
+
const content = textMsg?.contents ?? textMsg?.content ?? buttonsMsg?.content ?? chatMsg.content;
|
|
92
|
+
return {
|
|
93
|
+
workflow_run_id: workflowRunId,
|
|
94
|
+
role,
|
|
95
|
+
type: msgType,
|
|
96
|
+
content: truncateValue(content),
|
|
97
|
+
user_feedback: m.userFeedback ?? m.user_feedback,
|
|
98
|
+
work_log_steps_count: Array.isArray(m.workLogSteps ?? m.work_log_steps)
|
|
99
|
+
? (m.workLogSteps ?? m.work_log_steps).length
|
|
100
|
+
: 0,
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
// Find the first message with a workflow_run_id for the next step hint
|
|
104
|
+
const withRunId = formatted.find((m) => m.workflow_run_id);
|
|
105
|
+
return {
|
|
106
|
+
conversation_id: conversationId,
|
|
107
|
+
message_count: formatted.length,
|
|
108
|
+
messages: formatted,
|
|
109
|
+
_tip: "Messages with workflow_run_id can be inspected with debug(method='show_work', persona_id='...', workflow_run_id='...').",
|
|
110
|
+
_next_step: withRunId
|
|
111
|
+
? `debug(method="show_work", persona_id="${personaId ?? "<persona_id>"}", workflow_run_id="${withRunId.workflow_run_id}")`
|
|
112
|
+
: "No workflow_run_ids found in this conversation.",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
116
|
+
// show_work
|
|
117
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
118
|
+
export function formatShowWork(data, workflowRunId, personaId) {
|
|
119
|
+
const showWorkLogs = (data.showWorkLogs ?? data.show_work_logs ?? {});
|
|
120
|
+
const actions = [];
|
|
121
|
+
const erroredActions = [];
|
|
122
|
+
for (const [actionName, logRaw] of Object.entries(showWorkLogs)) {
|
|
123
|
+
const log = logRaw;
|
|
124
|
+
const status = resolveStatus(log.actionRunStatus ?? log.action_run_status);
|
|
125
|
+
const inputs = log.inputs;
|
|
126
|
+
const outputs = log.outputs;
|
|
127
|
+
const inputEntries = Array.isArray(inputs?.entries) ? inputs.entries : [];
|
|
128
|
+
const outputEntries = Array.isArray(outputs?.entries) ? outputs.entries : [];
|
|
129
|
+
const actionSummary = {
|
|
130
|
+
action_name: actionName,
|
|
131
|
+
title: log.actionTitle ?? log.action_title,
|
|
132
|
+
status,
|
|
133
|
+
llm_call_count: log.llmCallCount ?? log.llm_call_count ?? 0,
|
|
134
|
+
llm_cost_usd: log.llmCostUsd ?? log.llm_cost_usd ?? 0,
|
|
135
|
+
llm_latency_ms: log.llmLatencyMs ?? log.llm_latency_ms ?? 0,
|
|
136
|
+
input_names: inputEntries.map((e) => e.name),
|
|
137
|
+
output_names: outputEntries.map((e) => e.name),
|
|
138
|
+
step_count: Array.isArray(log.workLogs ?? log.work_logs) ? (log.workLogs ?? log.work_logs).length : 0,
|
|
139
|
+
hitl_round_count: Array.isArray(log.hitlRounds ?? log.hitl_rounds) ? (log.hitlRounds ?? log.hitl_rounds).length : 0,
|
|
140
|
+
};
|
|
141
|
+
// Include error info if present
|
|
142
|
+
const messages = Array.isArray(log.messages) ? log.messages : [];
|
|
143
|
+
const errorMessages = messages.filter((m) => m.logLevel === "LOG_LEVEL_ERROR" || m.log_level === "LOG_LEVEL_ERROR" || m.logLevel === 1 || m.log_level === 1);
|
|
144
|
+
if (errorMessages.length > 0) {
|
|
145
|
+
actionSummary.errors = errorMessages.map((m) => {
|
|
146
|
+
const errorInfo = (m.errorInfo ?? m.error_info);
|
|
147
|
+
return {
|
|
148
|
+
message: m.messageTemplate ?? m.message_template,
|
|
149
|
+
error_name: errorInfo?.name,
|
|
150
|
+
external_message: errorInfo?.externalMessage ?? errorInfo?.external_message,
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (status === "ERRORED") {
|
|
155
|
+
erroredActions.push(actionName);
|
|
156
|
+
}
|
|
157
|
+
actions.push(actionSummary);
|
|
158
|
+
}
|
|
159
|
+
const result = {
|
|
160
|
+
workflow_run_id: workflowRunId,
|
|
161
|
+
action_count: actions.length,
|
|
162
|
+
actions,
|
|
163
|
+
_tip: "Use action_name with debug(method='action_detail', persona_id='...') to see full inputs, outputs, LLM calls, and steps for any action.",
|
|
164
|
+
};
|
|
165
|
+
// Smart _next_step: auto-suggest drilling into errored actions
|
|
166
|
+
if (erroredActions.length > 0) {
|
|
167
|
+
const pid = personaId ?? "<persona_id>";
|
|
168
|
+
result._next_step = `Action(s) ERRORED: ${erroredActions.join(", ")}. Investigate with: debug(method="action_detail", persona_id="${pid}", workflow_run_id="${workflowRunId}", action_name="${erroredActions[0]}")`;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const pid = personaId ?? "<persona_id>";
|
|
172
|
+
result._next_step = actions.length > 0
|
|
173
|
+
? `debug(method="action_detail", persona_id="${pid}", workflow_run_id="${workflowRunId}", action_name="${actions[0].action_name}") to see detailed execution trace.`
|
|
174
|
+
: "No actions found in this workflow run.";
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
179
|
+
// action_detail
|
|
180
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
181
|
+
export function formatActionDetail(data, workflowRunId, actionName) {
|
|
182
|
+
const log = (data.showWorkLog ?? data.show_work_log ?? {});
|
|
183
|
+
const status = resolveStatus(log.actionRunStatus ?? log.action_run_status);
|
|
184
|
+
// Inputs
|
|
185
|
+
const inputs = log.inputs;
|
|
186
|
+
const inputEntries = Array.isArray(inputs?.entries) ? inputs.entries : [];
|
|
187
|
+
const formattedInputs = inputEntries.map((e) => ({
|
|
188
|
+
name: e.name,
|
|
189
|
+
value: truncateValue(e.value),
|
|
190
|
+
type: e.type,
|
|
191
|
+
source_agent: e.sourceAgentName ?? e.source_agent_name,
|
|
192
|
+
}));
|
|
193
|
+
// Outputs
|
|
194
|
+
const outputs = log.outputs;
|
|
195
|
+
const outputEntries = Array.isArray(outputs?.entries) ? outputs.entries : [];
|
|
196
|
+
const formattedOutputs = outputEntries.map((e) => ({
|
|
197
|
+
name: e.name,
|
|
198
|
+
value: truncateValue(e.value),
|
|
199
|
+
type: e.type,
|
|
200
|
+
published_as_chat_response: e.publishedAsChatResponse ?? e.published_as_chat_response,
|
|
201
|
+
}));
|
|
202
|
+
// Steps (work logs)
|
|
203
|
+
const steps = Array.isArray(log.workLogs ?? log.work_logs) ? (log.workLogs ?? log.work_logs) : [];
|
|
204
|
+
const formattedSteps = steps.map((s) => {
|
|
205
|
+
const msgs = Array.isArray(s.messages) ? s.messages : [];
|
|
206
|
+
const stepLlmCalls = Array.isArray(s.llmCalls ?? s.llm_calls) ? (s.llmCalls ?? s.llm_calls) : [];
|
|
207
|
+
return {
|
|
208
|
+
name: s.name,
|
|
209
|
+
messages: msgs.map((m) => ({
|
|
210
|
+
heading: m.heading,
|
|
211
|
+
template: truncateValue(m.messageTemplate ?? m.message_template),
|
|
212
|
+
log_level: m.logLevel ?? m.log_level,
|
|
213
|
+
error_info: m.errorInfo ?? m.error_info,
|
|
214
|
+
})),
|
|
215
|
+
llm_calls: stepLlmCalls.map((c) => formatLlmCall(c)),
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
// Top-level LLM calls
|
|
219
|
+
const topLlmCalls = Array.isArray(log.llmCalls ?? log.llm_calls) ? (log.llmCalls ?? log.llm_calls) : [];
|
|
220
|
+
const formattedLlmCalls = topLlmCalls.map((c) => formatLlmCall(c));
|
|
221
|
+
// HITL rounds
|
|
222
|
+
const hitlRounds = Array.isArray(log.hitlRounds ?? log.hitl_rounds) ? (log.hitlRounds ?? log.hitl_rounds) : [];
|
|
223
|
+
const formattedHitl = hitlRounds.map((r) => truncateObject(r));
|
|
224
|
+
return {
|
|
225
|
+
workflow_run_id: workflowRunId,
|
|
226
|
+
action_name: actionName,
|
|
227
|
+
action_type: data.actionType ?? data.action_type,
|
|
228
|
+
title: log.actionTitle ?? log.action_title,
|
|
229
|
+
status,
|
|
230
|
+
llm_call_count: log.llmCallCount ?? log.llm_call_count ?? 0,
|
|
231
|
+
llm_cost_usd: log.llmCostUsd ?? log.llm_cost_usd ?? 0,
|
|
232
|
+
llm_latency_ms: log.llmLatencyMs ?? log.llm_latency_ms ?? 0,
|
|
233
|
+
inputs: formattedInputs,
|
|
234
|
+
outputs: formattedOutputs,
|
|
235
|
+
steps: formattedSteps,
|
|
236
|
+
llm_calls: formattedLlmCalls,
|
|
237
|
+
hitl_rounds: formattedHitl.length > 0 ? formattedHitl : undefined,
|
|
238
|
+
_tip: status === "ERRORED"
|
|
239
|
+
? "Check the steps and error_info fields for failure details. LLM calls show the exact prompts and responses."
|
|
240
|
+
: "Review inputs/outputs for data flow and llm_calls for prompt details.",
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function formatLlmCall(call) {
|
|
244
|
+
const prompt = (call.prompt ?? {});
|
|
245
|
+
const metrics = (call.metrics ?? {});
|
|
246
|
+
return {
|
|
247
|
+
heading: call.heading,
|
|
248
|
+
success: call.success,
|
|
249
|
+
error_message: call.errorMessage ?? call.error_message,
|
|
250
|
+
fusion_used: call.fusionUsed ?? call.fusion_used,
|
|
251
|
+
cost_final_choice: call.llmCostFinalChoice ?? call.llm_cost_final_choice,
|
|
252
|
+
timestamp_ms: call.timestampMs ?? call.timestamp_ms,
|
|
253
|
+
prompt: {
|
|
254
|
+
system_prompt: truncateValue(prompt.systemPrompt ?? prompt.system_prompt),
|
|
255
|
+
user_prompt: truncateValue(prompt.userPrompt ?? prompt.user_prompt),
|
|
256
|
+
response: truncateValue(prompt.response),
|
|
257
|
+
},
|
|
258
|
+
metrics: Object.keys(metrics).length > 0 ? truncateObject(metrics) : undefined,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
262
|
+
// search
|
|
263
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
264
|
+
export function formatSearch(data, query, personaId) {
|
|
265
|
+
const results = Array.isArray(data.results) ? data.results : [];
|
|
266
|
+
return {
|
|
267
|
+
persona_id: personaId,
|
|
268
|
+
query,
|
|
269
|
+
total: results.length,
|
|
270
|
+
results: results.map((r) => ({
|
|
271
|
+
run_id: r.runId ?? r.run_id,
|
|
272
|
+
conversation_id: r.conversationId ?? r.conversation_id,
|
|
273
|
+
user_message_created_at: r.userMessageCreatedAt ?? r.user_message_created_at,
|
|
274
|
+
user_message: truncateValue(r.userMessage ?? r.user_message),
|
|
275
|
+
bot_message: truncateValue(r.botMessage ?? r.bot_message),
|
|
276
|
+
})),
|
|
277
|
+
_tip: "Use conversation_id with debug(method='conversation_detail') to see the full conversation, or run_id to identify the workflow execution.",
|
|
278
|
+
_next_step: results.length > 0
|
|
279
|
+
? `debug(method="conversation_detail", conversation_id="${results[0].conversationId ?? results[0].conversation_id}")`
|
|
280
|
+
: "No messages matched the search query.",
|
|
281
|
+
};
|
|
282
|
+
}
|