@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.
- package/.context/public/guides/dashboard-operations.md +63 -0
- package/.context/public/guides/workflow-builder-patterns.md +708 -0
- package/LICENSE +29 -21
- package/README.md +58 -35
- package/dist/mcp/domain/proto-constraints.js +284 -0
- package/dist/mcp/domain/structural-rules.js +8 -0
- package/dist/mcp/domain/validation-rules.js +102 -15
- package/dist/mcp/domain/workflow-graph-optimizer.js +235 -0
- package/dist/mcp/domain/workflow-graph-transforms.js +808 -0
- package/dist/mcp/domain/workflow-graph.js +376 -0
- package/dist/mcp/domain/workflow-optimizer.js +10 -4
- package/dist/mcp/guidance.js +45 -2
- package/dist/mcp/handlers/feedback/index.js +139 -0
- package/dist/mcp/handlers/feedback/store.js +262 -0
- package/dist/mcp/handlers/workflow/index.js +12 -11
- package/dist/mcp/handlers/workflow/optimize.js +73 -33
- package/dist/mcp/knowledge.js +87 -36
- package/dist/mcp/resources.js +393 -17
- package/dist/mcp/server.js +38 -4
- package/dist/mcp/tools.js +89 -2
- package/dist/sdk/generated/deprecated-actions.js +182 -96
- package/dist/sdk/generated/proto-fields.js +2 -1
- package/dist/sdk/generated/protos/service/agent_qa/v1/agent_qa_pb.js +460 -21
- package/dist/sdk/generated/protos/service/auth/v1/auth_pb.js +11 -1
- package/dist/sdk/generated/protos/service/dataingest/v1/dataingest_pb.js +173 -66
- package/dist/sdk/generated/protos/service/feedback/v1/feedback_pb.js +43 -1
- package/dist/sdk/generated/protos/service/llmservice/v1/llmservice_pb.js +26 -21
- package/dist/sdk/generated/protos/service/persona/v1/persona_config_pb.js +100 -89
- package/dist/sdk/generated/protos/service/persona/v1/persona_pb.js +126 -116
- package/dist/sdk/generated/protos/service/persona/v1/shared_widgets/widget_types_pb.js +33 -1
- package/dist/sdk/generated/protos/service/persona/v1/voicebot_widgets/widget_types_pb.js +60 -11
- package/dist/sdk/generated/protos/service/tenant/v1/tenant_pb.js +1 -1
- package/dist/sdk/generated/protos/service/user/v1/user_pb.js +1 -1
- package/dist/sdk/generated/protos/service/utils/v1/agent_qa_pb.js +35 -0
- package/dist/sdk/generated/protos/service/workflows/v1/action_registry_pb.js +1 -1
- package/dist/sdk/generated/protos/service/workflows/v1/action_type_pb.js +6 -1
- package/dist/sdk/generated/protos/service/workflows/v1/chatbot_pb.js +106 -11
- package/dist/sdk/generated/protos/service/workflows/v1/common_forms_pb.js +1 -1
- package/dist/sdk/generated/protos/service/workflows/v1/coordinator_pb.js +1 -1
- package/dist/sdk/generated/protos/service/workflows/v1/external_actions_pb.js +31 -1
- package/dist/sdk/generated/protos/service/workflows/v1/well_known_pb.js +5 -1
- package/dist/sdk/generated/protos/service/workflows/v1/workflow_pb.js +1 -1
- package/dist/sdk/generated/protos/util/tracking_metadata_pb.js +1 -1
- 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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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
|
|
10
|
+
* Handle workflow(mode="optimize") - structural graph optimization
|
|
15
11
|
*
|
|
16
|
-
*
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
if (!existingWorkflow) {
|
|
46
|
+
else {
|
|
29
47
|
return {
|
|
30
|
-
error:
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
workflow_def:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
}
|