@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 CHANGED
@@ -11,7 +11,7 @@
11
11
  * config validate - Validate config file
12
12
  */
13
13
  import { loadConfig } from "../sdk/config.js";
14
- import { EmaClient } from "../sdk/client.js";
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 EmaClient(env);
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
- * Extracted from workflow-execution-analyzer.ts — only the parts
5
- * used by live validation code (detectLoops + LoopInfo).
4
+ * Uses the shared WorkflowGraph representation from workflow-graph.ts
5
+ * for cycle and re-entry detection in workflow validation.
6
6
  */
7
- import { parseWorkflowDef } from "../knowledge.js";
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
- const nodes = parseWorkflowDef(workflowDef);
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, nodeMap } = buildGraphMaps(nodes);
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
- const trigger = nodes.find(n => n.action_name === 'trigger' || n.id === 'trigger');
60
- if (trigger)
61
- dfs(trigger.id);
62
- for (const node of nodes) {
63
- if (!visited.has(node.id))
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
- for (const node of nodes) {
68
- if (node.action_name === 'chat_categorizer') {
69
- const downstream = forward.get(node.id) || new Set();
70
- const upstream = new Set();
71
- function findUpstream(nId, depth = 0) {
72
- if (depth > 20)
73
- return;
74
- const nodeInfo = nodeMap.get(nId);
75
- if (!nodeInfo?.incoming_edges)
76
- return;
77
- for (const edge of nodeInfo.incoming_edges) {
78
- upstream.add(edge.source_node_id);
79
- findUpstream(edge.source_node_id, depth + 1);
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
- findUpstream(node.id);
83
- for (const downNode of downstream) {
84
- if (upstream.has(downNode)) {
85
- loops.push({
86
- type: 'reentry_loop',
87
- nodes: [node.id, downNode],
88
- description: `Categorizer "${node.display_name || node.id}" routes to "${downNode}" which is upstream - can cause re-processing`,
89
- severity: 'warning',
90
- fixSuggestion: `Add state tracking or one-time-execution gate to prevent re-processing.`,
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
- * TODO: Generate this from OpenAPI spec or gRPC definitions
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
- * NOTE: `loop-detection.ts` has its own `buildGraphMaps()` for validation
8
- * (cycle/re-entry detection). Future work: consolidate both into this module.
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
- // TODO: Generate dummy outputs for other outputs of branching action
106
- // TODO: Generate dummy outputs for non-branching actions
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
  // ─────────────────────────────────────────────────────────────────────────────
@@ -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
+ }