@ema.co/mcp-toolkit 2026.2.5 → 2026.2.13

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.

Files changed (44) hide show
  1. package/.context/public/guides/dashboard-operations.md +63 -0
  2. package/.context/public/guides/workflow-builder-patterns.md +708 -0
  3. package/LICENSE +29 -21
  4. package/README.md +58 -35
  5. package/dist/mcp/domain/proto-constraints.js +284 -0
  6. package/dist/mcp/domain/structural-rules.js +8 -0
  7. package/dist/mcp/domain/validation-rules.js +102 -15
  8. package/dist/mcp/domain/workflow-graph-optimizer.js +235 -0
  9. package/dist/mcp/domain/workflow-graph-transforms.js +808 -0
  10. package/dist/mcp/domain/workflow-graph.js +376 -0
  11. package/dist/mcp/domain/workflow-optimizer.js +10 -4
  12. package/dist/mcp/guidance.js +45 -2
  13. package/dist/mcp/handlers/feedback/index.js +139 -0
  14. package/dist/mcp/handlers/feedback/store.js +262 -0
  15. package/dist/mcp/handlers/workflow/index.js +12 -11
  16. package/dist/mcp/handlers/workflow/optimize.js +73 -33
  17. package/dist/mcp/knowledge.js +87 -36
  18. package/dist/mcp/resources.js +393 -17
  19. package/dist/mcp/server.js +38 -4
  20. package/dist/mcp/tools.js +89 -2
  21. package/dist/sdk/generated/deprecated-actions.js +182 -96
  22. package/dist/sdk/generated/proto-fields.js +2 -1
  23. package/dist/sdk/generated/protos/service/agent_qa/v1/agent_qa_pb.js +460 -21
  24. package/dist/sdk/generated/protos/service/auth/v1/auth_pb.js +11 -1
  25. package/dist/sdk/generated/protos/service/dataingest/v1/dataingest_pb.js +173 -66
  26. package/dist/sdk/generated/protos/service/feedback/v1/feedback_pb.js +43 -1
  27. package/dist/sdk/generated/protos/service/llmservice/v1/llmservice_pb.js +26 -21
  28. package/dist/sdk/generated/protos/service/persona/v1/persona_config_pb.js +100 -89
  29. package/dist/sdk/generated/protos/service/persona/v1/persona_pb.js +126 -116
  30. package/dist/sdk/generated/protos/service/persona/v1/shared_widgets/widget_types_pb.js +33 -1
  31. package/dist/sdk/generated/protos/service/persona/v1/voicebot_widgets/widget_types_pb.js +60 -11
  32. package/dist/sdk/generated/protos/service/tenant/v1/tenant_pb.js +1 -1
  33. package/dist/sdk/generated/protos/service/user/v1/user_pb.js +1 -1
  34. package/dist/sdk/generated/protos/service/utils/v1/agent_qa_pb.js +35 -0
  35. package/dist/sdk/generated/protos/service/workflows/v1/action_registry_pb.js +1 -1
  36. package/dist/sdk/generated/protos/service/workflows/v1/action_type_pb.js +6 -1
  37. package/dist/sdk/generated/protos/service/workflows/v1/chatbot_pb.js +106 -11
  38. package/dist/sdk/generated/protos/service/workflows/v1/common_forms_pb.js +1 -1
  39. package/dist/sdk/generated/protos/service/workflows/v1/coordinator_pb.js +1 -1
  40. package/dist/sdk/generated/protos/service/workflows/v1/external_actions_pb.js +31 -1
  41. package/dist/sdk/generated/protos/service/workflows/v1/well_known_pb.js +5 -1
  42. package/dist/sdk/generated/protos/service/workflows/v1/workflow_pb.js +1 -1
  43. package/dist/sdk/generated/protos/util/tracking_metadata_pb.js +1 -1
  44. package/package.json +2 -2
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Feedback Store - JSONL file-based storage for feedback and telemetry
3
+ *
4
+ * Provides append-only JSONL storage with rotation to prevent unbounded growth.
5
+ * Data is stored in `.feedback/` directory relative to the toolkit root.
6
+ *
7
+ * Two files:
8
+ * - feedback.jsonl - Explicit agent feedback (gaps, confusion, suggestions)
9
+ * - telemetry.jsonl - Passive telemetry (tool calls, resource fetches, errors)
10
+ */
11
+ import { promises as fs } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { randomUUID } from "node:crypto";
14
+ import { getToolkitRoot } from "../../../sdk/paths.js";
15
+ // ─────────────────────────────────────────────────────────────────────────────
16
+ // Constants
17
+ // ─────────────────────────────────────────────────────────────────────────────
18
+ const FEEDBACK_DIR = ".feedback";
19
+ const FEEDBACK_FILE = "feedback.jsonl";
20
+ const TELEMETRY_FILE = "telemetry.jsonl";
21
+ const MAX_TELEMETRY_ENTRIES = 1000;
22
+ const MAX_FEEDBACK_ENTRIES = 500;
23
+ // ─────────────────────────────────────────────────────────────────────────────
24
+ // Store
25
+ // ─────────────────────────────────────────────────────────────────────────────
26
+ /**
27
+ * Get the feedback directory path, creating it if necessary.
28
+ */
29
+ async function ensureFeedbackDir(rootOverride) {
30
+ const root = rootOverride ?? getToolkitRoot();
31
+ const dir = join(root, FEEDBACK_DIR);
32
+ await fs.mkdir(dir, { recursive: true });
33
+ return dir;
34
+ }
35
+ /**
36
+ * Append a single JSON line to a JSONL file.
37
+ */
38
+ async function appendJsonl(filePath, entry) {
39
+ const line = JSON.stringify(entry) + "\n";
40
+ await fs.appendFile(filePath, line, "utf-8");
41
+ }
42
+ /**
43
+ * Read all entries from a JSONL file.
44
+ * Returns empty array if file doesn't exist.
45
+ */
46
+ async function readJsonl(filePath) {
47
+ try {
48
+ const content = await fs.readFile(filePath, "utf-8");
49
+ return content
50
+ .split("\n")
51
+ .filter((line) => line.trim().length > 0)
52
+ .map((line) => {
53
+ try {
54
+ return JSON.parse(line);
55
+ }
56
+ catch {
57
+ // Skip corrupted lines (e.g., from crash mid-write)
58
+ return null;
59
+ }
60
+ })
61
+ .filter((entry) => entry !== null);
62
+ }
63
+ catch (err) {
64
+ if (err.code === "ENOENT") {
65
+ return [];
66
+ }
67
+ throw err;
68
+ }
69
+ }
70
+ /**
71
+ * Rotate a JSONL file by keeping only the last N entries.
72
+ */
73
+ async function rotateJsonl(filePath, maxEntries) {
74
+ const entries = await readJsonl(filePath);
75
+ if (entries.length <= maxEntries)
76
+ return;
77
+ const kept = entries.slice(entries.length - maxEntries);
78
+ const content = kept.map((e) => JSON.stringify(e)).join("\n") + "\n";
79
+ await fs.writeFile(filePath, content, "utf-8");
80
+ }
81
+ // ─────────────────────────────────────────────────────────────────────────────
82
+ // Public API
83
+ // ─────────────────────────────────────────────────────────────────────────────
84
+ /**
85
+ * Submit a feedback entry from an agent.
86
+ */
87
+ export async function submitFeedback(entry, rootOverride) {
88
+ const dir = await ensureFeedbackDir(rootOverride);
89
+ const filePath = join(dir, FEEDBACK_FILE);
90
+ const full = {
91
+ id: randomUUID(),
92
+ ts: new Date().toISOString(),
93
+ ...entry,
94
+ };
95
+ await appendJsonl(filePath, full);
96
+ // Log to stderr for visibility (stdout is the MCP stdio transport - never write there)
97
+ console.error(`[FEEDBACK] ${full.category}: ${full.message}`);
98
+ return full;
99
+ }
100
+ /**
101
+ * Record a telemetry event (passive, fire-and-forget).
102
+ */
103
+ export async function recordTelemetry(entry, rootOverride) {
104
+ try {
105
+ const dir = await ensureFeedbackDir(rootOverride);
106
+ const filePath = join(dir, TELEMETRY_FILE);
107
+ const full = {
108
+ ts: new Date().toISOString(),
109
+ ...entry,
110
+ };
111
+ await appendJsonl(filePath, full);
112
+ // Rotate periodically (check every 100 writes based on simple modulo of time)
113
+ // We use a lightweight check: rotate if file > MAX * 1.5 entries
114
+ const now = Date.now();
115
+ if (now % 100 < 5) {
116
+ await rotateJsonl(filePath, MAX_TELEMETRY_ENTRIES);
117
+ }
118
+ }
119
+ catch {
120
+ // Telemetry is fire-and-forget; never block tool execution
121
+ }
122
+ }
123
+ /**
124
+ * List recent feedback entries.
125
+ */
126
+ export async function listFeedback(options, rootOverride) {
127
+ const dir = await ensureFeedbackDir(rootOverride);
128
+ const filePath = join(dir, FEEDBACK_FILE);
129
+ let entries = await readJsonl(filePath);
130
+ if (options?.category) {
131
+ entries = entries.filter((e) => e.category === options.category);
132
+ }
133
+ const limit = options?.limit ?? 50;
134
+ return entries.slice(-limit);
135
+ }
136
+ /**
137
+ * List recent telemetry entries.
138
+ */
139
+ export async function listTelemetry(options, rootOverride) {
140
+ const dir = await ensureFeedbackDir(rootOverride);
141
+ const filePath = join(dir, TELEMETRY_FILE);
142
+ let entries = await readJsonl(filePath);
143
+ if (options?.type) {
144
+ entries = entries.filter((e) => e.type === options.type);
145
+ }
146
+ const limit = options?.limit ?? 100;
147
+ return entries.slice(-limit);
148
+ }
149
+ /**
150
+ * Analyze feedback and telemetry to produce actionable insights.
151
+ */
152
+ export async function analyzeFeedback(rootOverride) {
153
+ const dir = await ensureFeedbackDir(rootOverride);
154
+ const feedback = await readJsonl(join(dir, FEEDBACK_FILE));
155
+ const telemetry = await readJsonl(join(dir, TELEMETRY_FILE));
156
+ // Category breakdown
157
+ const categoryBreakdown = {};
158
+ for (const entry of feedback) {
159
+ categoryBreakdown[entry.category] = (categoryBreakdown[entry.category] ?? 0) + 1;
160
+ }
161
+ // Hot spots - which tools/operations have the most issues
162
+ const issuesByTool = {};
163
+ const issuesByOperation = {};
164
+ const negativeFeedback = feedback.filter((e) => e.category !== "success");
165
+ for (const entry of negativeFeedback) {
166
+ if (entry.tool) {
167
+ issuesByTool[entry.tool] = (issuesByTool[entry.tool] ?? 0) + 1;
168
+ }
169
+ if (entry.operation) {
170
+ issuesByOperation[entry.operation] = (issuesByOperation[entry.operation] ?? 0) + 1;
171
+ }
172
+ }
173
+ // Tool usage from telemetry
174
+ const toolUsage = {};
175
+ for (const entry of telemetry.filter((t) => t.type === "tool_call")) {
176
+ const key = entry.tool ?? "unknown";
177
+ if (!toolUsage[key]) {
178
+ toolUsage[key] = { total: 0, errors: 0 };
179
+ }
180
+ toolUsage[key].total++;
181
+ if (!entry.ok) {
182
+ toolUsage[key].errors++;
183
+ }
184
+ }
185
+ // Resource usage from telemetry
186
+ const resourceUsage = {};
187
+ for (const entry of telemetry.filter((t) => t.type === "resource_fetch")) {
188
+ const uri = entry.resource_uri ?? "unknown";
189
+ resourceUsage[uri] = (resourceUsage[uri] ?? 0) + 1;
190
+ }
191
+ // Error patterns
192
+ const errorMessages = {};
193
+ for (const entry of telemetry.filter((t) => t.type === "error" && t.error_message)) {
194
+ const msg = entry.error_message;
195
+ // Normalize error messages by truncating at 100 chars
196
+ const normalized = msg.length > 100 ? msg.slice(0, 100) + "..." : msg;
197
+ errorMessages[normalized] = (errorMessages[normalized] ?? 0) + 1;
198
+ }
199
+ // High-severity items
200
+ const highSeverity = feedback.filter((e) => e.severity === "high");
201
+ // Actionable items
202
+ const actionableItems = [];
203
+ // Tools with high error rates
204
+ for (const [tool, usage] of Object.entries(toolUsage)) {
205
+ const errorRate = usage.errors / usage.total;
206
+ if (errorRate > 0.3 && usage.total >= 5) {
207
+ actionableItems.push(`Tool "${tool}" has ${Math.round(errorRate * 100)}% error rate (${usage.errors}/${usage.total}) - investigate error handling and documentation`);
208
+ }
209
+ }
210
+ // Gaps are high-priority feedback
211
+ const gaps = feedback.filter((e) => e.category === "gap");
212
+ for (const gap of gaps) {
213
+ actionableItems.push(`Documentation gap: ${gap.message}${gap.tool ? ` (tool: ${gap.tool})` : ""}`);
214
+ }
215
+ // Confusion items suggest unclear docs
216
+ const confusions = feedback.filter((e) => e.category === "confusion");
217
+ for (const confusion of confusions) {
218
+ actionableItems.push(`Unclear guidance: ${confusion.message}${confusion.tool ? ` (tool: ${confusion.tool})` : ""}`);
219
+ }
220
+ return {
221
+ summary: {
222
+ total_feedback: feedback.length,
223
+ total_telemetry: telemetry.length,
224
+ feedback_period: feedback.length > 0
225
+ ? { from: feedback[0].ts, to: feedback[feedback.length - 1].ts }
226
+ : null,
227
+ telemetry_period: telemetry.length > 0
228
+ ? { from: telemetry[0].ts, to: telemetry[telemetry.length - 1].ts }
229
+ : null,
230
+ },
231
+ category_breakdown: categoryBreakdown,
232
+ hot_spots: {
233
+ by_tool: sortDescending(issuesByTool),
234
+ by_operation: sortDescending(issuesByOperation),
235
+ },
236
+ tool_usage: toolUsage,
237
+ resource_usage: sortDescending(resourceUsage),
238
+ error_patterns: sortDescending(errorMessages),
239
+ high_severity: highSeverity,
240
+ actionable_items: actionableItems,
241
+ };
242
+ }
243
+ /**
244
+ * Rotate both feedback and telemetry files to prevent unbounded growth.
245
+ */
246
+ export async function rotateLogs(rootOverride) {
247
+ const dir = await ensureFeedbackDir(rootOverride);
248
+ await rotateJsonl(join(dir, FEEDBACK_FILE), MAX_FEEDBACK_ENTRIES);
249
+ await rotateJsonl(join(dir, TELEMETRY_FILE), MAX_TELEMETRY_ENTRIES);
250
+ const feedbackEntries = await readJsonl(join(dir, FEEDBACK_FILE));
251
+ const telemetryEntries = await readJsonl(join(dir, TELEMETRY_FILE));
252
+ return {
253
+ feedback_kept: feedbackEntries.length,
254
+ telemetry_kept: telemetryEntries.length,
255
+ };
256
+ }
257
+ // ─────────────────────────────────────────────────────────────────────────────
258
+ // Helpers
259
+ // ─────────────────────────────────────────────────────────────────────────────
260
+ function sortDescending(record) {
261
+ return Object.fromEntries(Object.entries(record).sort((a, b) => b[1] - a[1]));
262
+ }
@@ -5,6 +5,7 @@
5
5
  * - get: Return workflow data + schema for LLM to generate/modify
6
6
  * - deploy: Deploy LLM-generated workflow_def
7
7
  * - validate: Validate a workflow_def before deploying
8
+ * - optimize: Structural graph optimization
8
9
  *
9
10
  * THE LLM DOES ALL THE THINKING. MCP provides data and executes.
10
11
  *
@@ -13,7 +14,7 @@
13
14
  * - generate: Had MCP generating workflow - LLM should generate full workflow_def
14
15
  *
15
16
  * DEPRECATED (kept for backwards compat, but not exposed):
16
- * - analyze, optimize, compare: LLM can do this by fetching rules and comparing
17
+ * - analyze, compare: LLM can do this by fetching rules and comparing
17
18
  */
18
19
  import { generateSchema } from "../../domain/generation-schema.js";
19
20
  import { fingerprintPersona } from "../../../sync.js";
@@ -228,8 +229,8 @@ async function handleWorkflowGet(args, client) {
228
229
  /**
229
230
  * Main workflow handler with mode-based dispatch
230
231
  *
231
- * PUBLIC modes: get, deploy
232
- * INTERNAL modes: modify, generate, analyze, optimize, compare (called from persona tool)
232
+ * PUBLIC modes: get, deploy, validate, optimize
233
+ * INTERNAL modes: modify, generate, analyze, compare (called from persona tool)
233
234
  */
234
235
  export async function handleWorkflow(args, client, _getTemplateId) {
235
236
  const personaId = args.persona_id;
@@ -246,6 +247,9 @@ export async function handleWorkflow(args, client, _getTemplateId) {
246
247
  if (mode === "validate") {
247
248
  return handleWorkflowValidate(args, client);
248
249
  }
250
+ if (mode === "optimize") {
251
+ return handleWorkflowOptimize(args, client);
252
+ }
249
253
  // REMOVED: modify, generate modes violated LLM-driven architecture
250
254
  // MCP was doing LLM work (parsing operations, generating workflows)
251
255
  // Correct flow: LLM generates full workflow_def → workflow(mode="deploy")
@@ -261,14 +265,11 @@ export async function handleWorkflow(args, client, _getTemplateId) {
261
265
  _tip: "The LLM generates the full workflow_def. MCP just deploys it.",
262
266
  };
263
267
  }
264
- // DEPRECATED: analyze, optimize, compare - LLM can do this directly
268
+ // DEPRECATED: analyze, compare - LLM can do this directly
265
269
  // Kept for backwards compat but may be removed
266
270
  if (mode === "analyze") {
267
271
  return handleWorkflowAnalyze(args, client);
268
272
  }
269
- if (mode === "optimize") {
270
- return handleWorkflowOptimize(args, client);
271
- }
272
273
  if (mode === "compare") {
273
274
  return handleWorkflowCompare(args, client);
274
275
  }
@@ -283,16 +284,16 @@ export async function handleWorkflow(args, client, _getTemplateId) {
283
284
  // Invalid mode
284
285
  return {
285
286
  error: `Invalid or missing mode: ${mode}`,
286
- public_modes: ["get", "deploy", "validate"],
287
- hint: "MCP provides data (get), validates (validate), and executes (deploy). LLM does all thinking.",
287
+ public_modes: ["get", "deploy", "validate", "optimize"],
288
+ hint: "MCP provides data (get), validates (validate), optimizes (optimize), and executes (deploy). LLM does all thinking.",
288
289
  };
289
290
  }
290
291
  /**
291
292
  * Check if a workflow mode has been extracted
292
293
  */
293
294
  export function hasExtractedWorkflowHandler(mode) {
294
- // PUBLIC: get, deploy, validate
295
- // DEPRECATED (kept for compat): analyze, optimize, compare
295
+ // PUBLIC: get, deploy, validate, optimize
296
+ // DEPRECATED (kept for compat): analyze, compare
296
297
  // REMOVED: modify, extend, generate (violated LLM-driven architecture)
297
298
  const extractedModes = ["get", "deploy", "validate", "analyze", "optimize", "compare"];
298
299
  return extractedModes.includes(mode);
@@ -1,50 +1,90 @@
1
1
  /**
2
2
  * Workflow Optimize Handler
3
3
  *
4
- * DEPRECATED: This handler returned pre-computed fixes which violates
5
- * the "MCP = data, LLM = logic" principle.
6
- *
7
- * The LLM should:
8
- * 1. Get workflow with workflow(mode="get")
9
- * 2. Fetch rules from ema://rules/anti-patterns
10
- * 3. Apply rules and propose fixes
11
- * 4. Deploy via workflow(mode="deploy")
4
+ * Structural graph optimization using the workflow-graph-optimizer.
5
+ * Accepts persona_id (fetches workflow) OR workflow_def (direct optimization).
6
+ * Returns optimized workflow_def + report. Does NOT auto-deploy.
12
7
  */
8
+ import { optimizeWorkflow } from "../../domain/workflow-graph-optimizer.js";
13
9
  /**
14
- * Handle workflow optimize mode
10
+ * Handle workflow(mode="optimize") - structural graph optimization
15
11
  *
16
- * NOW DEPRECATED - Returns guidance for LLM to do the analysis.
12
+ * Follows the same parameter extraction pattern as handleWorkflowValidate:
13
+ * - persona_id → fetch workflow from persona
14
+ * - workflow_def → optimize directly
15
+ * - auto_apply (default true) → apply safe auto-transforms
16
+ * - max_passes (default 5) → convergence limit
17
17
  */
18
18
  export async function handleWorkflowOptimize(args, client) {
19
19
  const personaId = args.persona_id;
20
- if (!personaId) {
21
- return { error: "persona_id required for optimize mode" };
20
+ const workflowDef = args.workflow_def;
21
+ const autoApply = args.auto_apply ?? true;
22
+ const maxPasses = args.max_passes ?? 5;
23
+ // Get workflow to optimize
24
+ let workflowToOptimize = null;
25
+ let personaName;
26
+ if (workflowDef) {
27
+ // Use provided workflow_def directly
28
+ workflowToOptimize = workflowDef;
22
29
  }
23
- const persona = await client.getPersonaById(personaId);
24
- if (!persona) {
25
- return { error: `Persona not found: ${personaId}` };
30
+ else if (personaId) {
31
+ // Get workflow from persona
32
+ const persona = await client.getPersonaById(personaId);
33
+ if (!persona) {
34
+ return { error: `Persona not found: ${personaId}` };
35
+ }
36
+ personaName = persona.name;
37
+ const personaWorkflowDef = persona.workflow_def;
38
+ if (!personaWorkflowDef) {
39
+ return {
40
+ error: `Persona "${persona.name}" has no workflow to optimize`,
41
+ _tip: "Deploy a workflow first, then optimize it",
42
+ };
43
+ }
44
+ workflowToOptimize = personaWorkflowDef;
26
45
  }
27
- const existingWorkflow = persona.workflow_def;
28
- if (!existingWorkflow) {
46
+ else {
29
47
  return {
30
- error: `Persona "${persona.name}" has no workflow to optimize`,
31
- hint: "Use mode='generate' to create a workflow first",
48
+ error: "Either persona_id or workflow_def required for optimize mode",
32
49
  };
33
50
  }
34
- // Return the workflow and guidance for LLM to do the analysis
35
- return {
51
+ if (!workflowToOptimize) {
52
+ return { error: "Could not extract workflow for optimization" };
53
+ }
54
+ // Run the graph optimizer
55
+ const result = optimizeWorkflow(workflowToOptimize, {
56
+ autoApply,
57
+ maxPasses,
58
+ });
59
+ // Format response
60
+ const response = {
36
61
  mode: "optimize",
37
- status: "DEPRECATED - Use LLM analysis instead",
38
- persona_id: personaId,
39
- persona_name: persona.name,
40
- // Return the workflow for LLM to analyze
41
- workflow_def: existingWorkflow,
42
- _next_steps: [
43
- "1. Use workflow(mode='get') to get workflow data",
44
- "2. Fetch ema://rules/anti-patterns",
45
- "3. Apply rules to find issues (LLM does this, not MCP)",
46
- "4. Modify workflow based on analysis",
47
- "5. Deploy via workflow(mode='deploy', workflow_def={...})",
48
- ],
62
+ ...(personaId && { persona_id: personaId }),
63
+ ...(personaName && { persona_name: personaName }),
64
+ // Core result
65
+ modified: result.modified,
66
+ workflow_def: result.workflowDef,
67
+ // What was done
68
+ applied_transforms: result.appliedTransforms.length > 0
69
+ ? result.appliedTransforms
70
+ : undefined,
71
+ // What the LLM should review
72
+ advisories: result.advisories.length > 0
73
+ ? result.advisories
74
+ : undefined,
75
+ // Before/after metrics
76
+ metrics: result.metrics,
77
+ // Post-optimization validation
78
+ validation: result.validation,
79
+ // Guidance
80
+ _tip: result.modified
81
+ ? "Optimization applied transforms. Review the changes, then deploy with: workflow(mode='deploy', persona_id='...', base_fingerprint='<fingerprint>', workflow_def={...})"
82
+ : result.advisories.length > 0
83
+ ? "No auto-transforms applied, but advisories found. Review them and modify the workflow_def manually if needed."
84
+ : "Workflow is already optimal. No changes needed.",
85
+ _next_step: result.modified
86
+ ? "workflow(mode='get', persona_id='...') to get fresh fingerprint, then workflow(mode='deploy', persona_id='...', base_fingerprint='<fingerprint>', workflow_def={optimized_workflow_def})"
87
+ : undefined,
49
88
  };
89
+ return response;
50
90
  }