@ema.co/mcp-toolkit 2026.3.24 → 2026.3.25-2
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/knowledge/guidance-cache.js +60 -3
- package/dist/knowledge/intelligence/staleness.js +4 -0
- package/dist/knowledge/search-client.js +89 -1
- package/dist/mcp/domain/generation-schema.js +244 -31
- package/dist/mcp/domain/workflow-def-validator.js +321 -1
- package/dist/mcp/domain/workflow-static-validator.js +1 -1
- package/dist/mcp/guidance/classify.js +74 -0
- package/dist/mcp/guidance/defaults.js +114 -0
- package/dist/mcp/guidance/middleware.js +193 -0
- package/dist/mcp/guidance/types.js +7 -0
- package/dist/mcp/guidance.js +12 -8
- package/dist/mcp/handlers/data/index.js +1 -1
- package/dist/mcp/handlers/debug/index.js +80 -7
- package/dist/mcp/handlers/env/index.js +4 -4
- package/dist/mcp/handlers/feedback/coalesce.js +4 -1
- package/dist/mcp/handlers/feedback/index.js +15 -3
- package/dist/mcp/handlers/feedback/store.js +32 -12
- package/dist/mcp/handlers/template/crud.js +26 -9
- package/dist/mcp/handlers/workflow/adapter.js +2 -2
- package/dist/mcp/handlers/workflow/deploy.js +15 -15
- package/dist/mcp/handlers/workflow/index.js +23 -11
- package/dist/mcp/handlers/workflow/validate.js +30 -1
- package/dist/mcp/handlers/workflow/validation.js +4 -4
- package/dist/mcp/server.js +57 -5
- package/dist/mcp/tools.js +18 -11
- package/dist/sdk/client.js +7 -7
- package/dist/sdk/ema-client.js +16 -1
- package/package.json +1 -1
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
17
17
|
const DEFAULT_TTL_MS = 3_600_000; // 1 hour
|
|
18
18
|
const SEARCH_LIMIT = 200;
|
|
19
|
-
|
|
19
|
+
// Accept guidance from both MCP toolkit extractors and knowledge-adapters pipeline.
|
|
20
|
+
// MCP owns raw TypeScript definitions; knowledge-adapters handles extraction + publishing.
|
|
21
|
+
const SOURCE_FILTERS = ["mcp-toolkit", "repo-knowledge"];
|
|
20
22
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
23
|
// Implementation
|
|
22
24
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -29,6 +31,8 @@ export class GuidanceCache {
|
|
|
29
31
|
topics = [];
|
|
30
32
|
patterns = [];
|
|
31
33
|
questions = [];
|
|
34
|
+
invariants = [];
|
|
35
|
+
contextualGuidanceMap = new Map();
|
|
32
36
|
warmedAt = null;
|
|
33
37
|
constructor(searchFn, config) {
|
|
34
38
|
this.searchFn = searchFn;
|
|
@@ -42,8 +46,8 @@ export class GuidanceCache {
|
|
|
42
46
|
*/
|
|
43
47
|
async warm() {
|
|
44
48
|
try {
|
|
45
|
-
const results = await this.searchFn(`source:${
|
|
46
|
-
filters: { source:
|
|
49
|
+
const results = await this.searchFn(`source:${SOURCE_FILTERS[0]}`, {
|
|
50
|
+
filters: { source: SOURCE_FILTERS },
|
|
47
51
|
limit: SEARCH_LIMIT,
|
|
48
52
|
});
|
|
49
53
|
this.index(results);
|
|
@@ -114,6 +118,37 @@ export class GuidanceCache {
|
|
|
114
118
|
this.maybeRefresh();
|
|
115
119
|
return this.questions;
|
|
116
120
|
}
|
|
121
|
+
/** All structural invariants (graph, branching, response, type rules). */
|
|
122
|
+
getInvariants() {
|
|
123
|
+
this.maybeRefresh();
|
|
124
|
+
return this.invariants;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Get contextual guidance for a specific key.
|
|
128
|
+
* Keys follow the pattern: "{tool}.{method}.{result_shape}"
|
|
129
|
+
* Also checks wildcards: "*.{method}.{shape}", "{tool}.*.{shape}", "*.*.{shape}"
|
|
130
|
+
*/
|
|
131
|
+
getContextualGuidance(tool, method, shape) {
|
|
132
|
+
this.maybeRefresh();
|
|
133
|
+
// Try exact match first, then progressively broader wildcards
|
|
134
|
+
const candidates = [
|
|
135
|
+
`${tool}.${method}.${shape}`,
|
|
136
|
+
`${tool}.*.${shape}`,
|
|
137
|
+
`*.${method}.${shape}`,
|
|
138
|
+
`*.*.${shape}`,
|
|
139
|
+
];
|
|
140
|
+
for (const key of candidates) {
|
|
141
|
+
const atom = this.contextualGuidanceMap.get(key);
|
|
142
|
+
if (atom)
|
|
143
|
+
return atom;
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
/** All contextual guidance atoms (for debugging/inspection). */
|
|
148
|
+
getAllContextualGuidance() {
|
|
149
|
+
this.maybeRefresh();
|
|
150
|
+
return Array.from(this.contextualGuidanceMap.values());
|
|
151
|
+
}
|
|
117
152
|
// ── Internals ───────────────────────────────────────────────────────────
|
|
118
153
|
clear() {
|
|
119
154
|
this.rules = [];
|
|
@@ -122,6 +157,8 @@ export class GuidanceCache {
|
|
|
122
157
|
this.topics = [];
|
|
123
158
|
this.patterns = [];
|
|
124
159
|
this.questions = [];
|
|
160
|
+
this.invariants = [];
|
|
161
|
+
this.contextualGuidanceMap = new Map();
|
|
125
162
|
}
|
|
126
163
|
/**
|
|
127
164
|
* Index artifacts by reading structured data from structData.data.
|
|
@@ -172,6 +209,16 @@ export class GuidanceCache {
|
|
|
172
209
|
case "qualifying-question":
|
|
173
210
|
this.questions.push(data);
|
|
174
211
|
break;
|
|
212
|
+
case "structural-invariant":
|
|
213
|
+
this.invariants.push(data);
|
|
214
|
+
break;
|
|
215
|
+
case "contextual-guidance": {
|
|
216
|
+
const atom = data;
|
|
217
|
+
const key = atom.key
|
|
218
|
+
?? `${atom.tool ?? "*"}.${atom.method ?? "*"}.${atom.result_shape ?? "*"}`;
|
|
219
|
+
this.contextualGuidanceMap.set(key, { ...atom, key });
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
175
222
|
default:
|
|
176
223
|
break;
|
|
177
224
|
}
|
|
@@ -195,8 +242,18 @@ export class GuidanceCache {
|
|
|
195
242
|
return "workflow-pattern";
|
|
196
243
|
if (tags.includes("qualifying-question"))
|
|
197
244
|
return "qualifying-question";
|
|
245
|
+
if (tags.includes("structural-invariant"))
|
|
246
|
+
return "structural-invariant";
|
|
247
|
+
if (tags.includes("structural-invariant"))
|
|
248
|
+
return "structural-invariant";
|
|
249
|
+
if (tags.includes("contextual-guidance"))
|
|
250
|
+
return "contextual-guidance";
|
|
198
251
|
if (name.includes("tool-guidance"))
|
|
199
252
|
return "tool-guidance";
|
|
253
|
+
if (name.startsWith("invariant-"))
|
|
254
|
+
return "structural-invariant";
|
|
255
|
+
if (name.includes("contextual-guidance"))
|
|
256
|
+
return "contextual-guidance";
|
|
200
257
|
return undefined;
|
|
201
258
|
}
|
|
202
259
|
}
|
|
@@ -15,6 +15,10 @@ export const FILE_TO_SOURCE = {
|
|
|
15
15
|
"src/config/workflow-patterns.ts": ["workflow-patterns"],
|
|
16
16
|
"src/config/guidance-rules.ts": ["guidance-rules"],
|
|
17
17
|
"src/mcp/knowledge-guidance-topics.ts": ["guidance-topics"],
|
|
18
|
+
// Upstream dependency: workflow-engine/pkg/engine/validator.go
|
|
19
|
+
// When the Go validator changes, structural-rules.ts should be updated to match.
|
|
20
|
+
// The knowledge-adapters pipeline (mcp-toolkit adapter) re-extracts and publishes
|
|
21
|
+
// to DE, where GuidanceCache auto-refreshes within 1hr TTL.
|
|
18
22
|
"src/mcp/domain/structural-rules.ts": ["structural-invariants"],
|
|
19
23
|
"src/mcp/domain/validation-rules.ts": ["validation-rules"],
|
|
20
24
|
"src/sdk/generated/deprecated-actions.ts": ["deprecated-actions"],
|
|
@@ -165,6 +165,46 @@ const QUERY_SIGNALS = [
|
|
|
165
165
|
],
|
|
166
166
|
},
|
|
167
167
|
];
|
|
168
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
169
|
+
// Dynamic Preamble — context-aware system instructions for :answer endpoint
|
|
170
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
171
|
+
/** Base preamble applied to all answer queries. */
|
|
172
|
+
const BASE_PREAMBLE = [
|
|
173
|
+
"You are answering questions about the Ema AI Employee platform — a workflow orchestration system.",
|
|
174
|
+
"When referencing actions or nodes, use their exact actionType names (e.g., chat_categorizer, respond_with_sources).",
|
|
175
|
+
"When showing fixes, include concrete workflow_def JSON snippets when available in the source documents.",
|
|
176
|
+
"Cite specific document IDs when available.",
|
|
177
|
+
].join(" ");
|
|
178
|
+
/** Query-pattern-specific preamble additions. */
|
|
179
|
+
const PREAMBLE_SIGNALS = [
|
|
180
|
+
{
|
|
181
|
+
pattern: /\b(fix|error|fail|broken|wrong|issue|bug|debug)\b/i,
|
|
182
|
+
addition: "Focus on the Fix field from structural invariants. Show the violation, then the fix.",
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
pattern: /\b(how to|how do|pattern|example|wire|connect|build)\b/i,
|
|
186
|
+
addition: "Provide step-by-step instructions with workflow_def JSON examples where available.",
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
pattern: /\b(rule|constraint|invariant|must|require|validate)\b/i,
|
|
190
|
+
addition: "List the relevant rules with their severity (critical/warning) and enforcement status.",
|
|
191
|
+
},
|
|
192
|
+
];
|
|
193
|
+
/**
|
|
194
|
+
* Build a context-aware preamble for the :answer endpoint.
|
|
195
|
+
* Returns undefined if preamble should be omitted (keeps DE default).
|
|
196
|
+
*/
|
|
197
|
+
function buildDefaultPreamble(query) {
|
|
198
|
+
// Skip preamble for very short or exact-match queries (action name lookups)
|
|
199
|
+
if (query.length < 10 || !query.includes(" "))
|
|
200
|
+
return undefined;
|
|
201
|
+
const additions = PREAMBLE_SIGNALS
|
|
202
|
+
.filter(s => s.pattern.test(query))
|
|
203
|
+
.map(s => s.addition);
|
|
204
|
+
return additions.length > 0
|
|
205
|
+
? `${BASE_PREAMBLE} ${additions.join(" ")}`
|
|
206
|
+
: BASE_PREAMBLE;
|
|
207
|
+
}
|
|
168
208
|
/**
|
|
169
209
|
* Build per-query boostSpec from signal matching.
|
|
170
210
|
* Returns undefined if no signals match or if the caller already filtered
|
|
@@ -265,12 +305,14 @@ function transformDeResponse(deResponse, mode) {
|
|
|
265
305
|
* Falls back gracefully — returns results-only if answer generation fails.
|
|
266
306
|
*/
|
|
267
307
|
async function answerDirect(query, options) {
|
|
268
|
-
const { filters, topK = 10, elevate = false } = options;
|
|
308
|
+
const { filters, topK = 10, elevate = false, preamble } = options;
|
|
269
309
|
const de = getDeConfig();
|
|
270
310
|
const headers = buildDeHeaders();
|
|
271
311
|
if (!headers)
|
|
272
312
|
return { results: [], mode: "answer" };
|
|
273
313
|
const filter = buildFilterExpression(filters, elevate);
|
|
314
|
+
// Dynamic preamble: caller-provided or auto-generated from query context
|
|
315
|
+
const effectivePreamble = preamble ?? buildDefaultPreamble(query);
|
|
274
316
|
const body = {
|
|
275
317
|
query: { text: query },
|
|
276
318
|
relatedQuestionsSpec: { enable: true },
|
|
@@ -280,6 +322,7 @@ async function answerDirect(query, options) {
|
|
|
280
322
|
ignoreLowRelevantContent: true,
|
|
281
323
|
includeCitations: true,
|
|
282
324
|
modelSpec: { modelVersion: "stable" },
|
|
325
|
+
...(effectivePreamble ? { promptSpec: { preamble: effectivePreamble } } : {}),
|
|
283
326
|
},
|
|
284
327
|
searchSpec: {
|
|
285
328
|
searchParams: {
|
|
@@ -529,6 +572,7 @@ export async function searchDocuments(query, options = {}) {
|
|
|
529
572
|
filters: options.filters,
|
|
530
573
|
topK: options.topK,
|
|
531
574
|
elevate: options.elevate,
|
|
575
|
+
preamble: options.preamble,
|
|
532
576
|
});
|
|
533
577
|
if (answerResult.generativeAnswer || answerResult.results.length > 0)
|
|
534
578
|
return answerResult;
|
|
@@ -550,6 +594,50 @@ export async function searchDocuments(query, options = {}) {
|
|
|
550
594
|
return result;
|
|
551
595
|
}
|
|
552
596
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
597
|
+
// Browse — list/filter documents without a search query
|
|
598
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
599
|
+
/**
|
|
600
|
+
* Browse DE documents without a search query.
|
|
601
|
+
* Uses the :search endpoint with an empty query — same endpoint, filter-only mode.
|
|
602
|
+
* Useful for listing all documents of a type, source, or tag.
|
|
603
|
+
*
|
|
604
|
+
* @see https://docs.cloud.google.com/generative-ai-app-builder/docs/browse-generic-search
|
|
605
|
+
*/
|
|
606
|
+
export async function browseDocuments(options = {}) {
|
|
607
|
+
const { filters, elevate = false, pageSize = 50, orderBy } = options;
|
|
608
|
+
const de = getDeConfig();
|
|
609
|
+
const headers = buildDeHeaders();
|
|
610
|
+
if (!headers)
|
|
611
|
+
return { results: [], mode: "raw" };
|
|
612
|
+
await ensureGcpAuth();
|
|
613
|
+
const filter = buildFilterExpression(filters, elevate);
|
|
614
|
+
const body = {
|
|
615
|
+
query: "",
|
|
616
|
+
pageSize,
|
|
617
|
+
...(filter ? { filter } : {}),
|
|
618
|
+
...(orderBy ? { orderBy } : {}),
|
|
619
|
+
contentSearchSpec: {
|
|
620
|
+
snippetSpec: { returnSnippet: false },
|
|
621
|
+
},
|
|
622
|
+
};
|
|
623
|
+
const url = `${de.baseUrl}/${de.servingConfig}:search`;
|
|
624
|
+
try {
|
|
625
|
+
const resp = await fetch(url, {
|
|
626
|
+
method: "POST",
|
|
627
|
+
headers,
|
|
628
|
+
body: JSON.stringify(body),
|
|
629
|
+
signal: AbortSignal.timeout(DE_TIMEOUT_MS),
|
|
630
|
+
});
|
|
631
|
+
if (!resp.ok)
|
|
632
|
+
return { results: [], mode: "raw" };
|
|
633
|
+
const data = await resp.json();
|
|
634
|
+
return transformDeResponse(data, "raw");
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
return { results: [], mode: "raw" };
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
553
641
|
// User Event Tracking
|
|
554
642
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
555
643
|
export async function writeUserEvent(event) {
|
|
@@ -1,51 +1,227 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generation Schema
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* API-first schema generation: fetches live action definitions from the
|
|
5
|
+
* backend, enriches with catalog metadata, falls back to static catalog
|
|
6
|
+
* when the API is unavailable.
|
|
7
|
+
*
|
|
8
|
+
* This ensures agents always see the FULL set of required inputs/outputs
|
|
9
|
+
* as defined by the backend, not a hand-curated subset from documentation.
|
|
6
10
|
*
|
|
7
11
|
* The auto-builder sends ~70K tokens of documentation per request.
|
|
8
12
|
* This schema reduces that to ~5K tokens of actionable constraints.
|
|
9
13
|
*/
|
|
10
14
|
import { AGENT_CATALOG } from "../knowledge.js";
|
|
11
15
|
import { INPUT_SOURCE_RULES } from "./validation-rules.js";
|
|
16
|
+
import { STRUCTURAL_INVARIANTS } from "./structural-rules.js";
|
|
12
17
|
import { LLM_ACTIONS, LLM_WIDGET_INPUTS, widgetBinding } from "../../config/widget-bindings.js";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
//
|
|
18
|
+
import { searchDocuments } from "../../knowledge/search-client.js";
|
|
19
|
+
export async function buildCompactAgents(client) {
|
|
20
|
+
// Build catalog index for enrichment
|
|
21
|
+
const catalogIndex = new Map();
|
|
22
|
+
for (const agent of AGENT_CATALOG) {
|
|
23
|
+
catalogIndex.set(agent.actionName, agent);
|
|
24
|
+
}
|
|
25
|
+
// Try API-first for full input/output definitions
|
|
26
|
+
if (client) {
|
|
27
|
+
try {
|
|
28
|
+
const apiAgents = await buildFromApi(client, catalogIndex);
|
|
29
|
+
if (Object.keys(apiAgents).length > 0) {
|
|
30
|
+
return { agents: apiAgents, source: "api" };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
console.error(`[generation-schema] API listActions failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// DE middle layer: search for action definitions in Discovery Engine
|
|
38
|
+
try {
|
|
39
|
+
const deAgents = await buildFromDE(catalogIndex);
|
|
40
|
+
if (deAgents && Object.keys(deAgents).length > 0) {
|
|
41
|
+
return { agents: deAgents, source: "api" };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
console.error(`[generation-schema] DE search failed, using static catalog: ${e instanceof Error ? e.message : String(e)}`);
|
|
46
|
+
}
|
|
47
|
+
// Final fallback: static catalog (may have incomplete inputs)
|
|
48
|
+
return { agents: buildFromCatalog(catalogIndex), source: "catalog" };
|
|
49
|
+
}
|
|
50
|
+
/** Synchronous version for backwards compatibility (uses catalog only). */
|
|
51
|
+
export function buildCompactAgentsSync() {
|
|
52
|
+
const catalogIndex = new Map();
|
|
53
|
+
for (const agent of AGENT_CATALOG) {
|
|
54
|
+
catalogIndex.set(agent.actionName, agent);
|
|
55
|
+
}
|
|
56
|
+
return { agents: buildFromCatalog(catalogIndex), source: "catalog" };
|
|
57
|
+
}
|
|
16
58
|
/**
|
|
17
|
-
* Build
|
|
59
|
+
* Build agents from live API — full input/output definitions.
|
|
60
|
+
* Enriches with catalog metadata (description, whenToUse, etc.).
|
|
18
61
|
*/
|
|
19
|
-
|
|
62
|
+
async function buildFromApi(client, catalogIndex) {
|
|
63
|
+
const actionDTOs = await client.listActions();
|
|
20
64
|
const agents = {};
|
|
21
|
-
for (const
|
|
65
|
+
for (const dto of actionDTOs) {
|
|
66
|
+
const typeName = dto.typeName;
|
|
67
|
+
const name = typeName?.name?.name;
|
|
68
|
+
if (!name)
|
|
69
|
+
continue;
|
|
22
70
|
const inputs = {};
|
|
23
|
-
|
|
24
|
-
|
|
71
|
+
const outputs = {};
|
|
72
|
+
// Parse inputs from API response (FULL definition)
|
|
73
|
+
const inputsObj = dto.inputs?.inputs;
|
|
74
|
+
if (inputsObj && typeof inputsObj === "object") {
|
|
75
|
+
for (const [inputName, inputDef] of Object.entries(inputsObj)) {
|
|
76
|
+
const def = inputDef;
|
|
77
|
+
inputs[inputName] = {
|
|
78
|
+
type: (def.type?.wellKnownType ?? "WELL_KNOWN_TYPE_ANY"),
|
|
79
|
+
required: !def.isOptional,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Parse outputs from API response
|
|
84
|
+
const outputsObj = dto.outputs?.outputs;
|
|
85
|
+
if (outputsObj && typeof outputsObj === "object") {
|
|
86
|
+
for (const [outputName, outputDef] of Object.entries(outputsObj)) {
|
|
87
|
+
const def = outputDef;
|
|
88
|
+
outputs[outputName] = (def.type?.wellKnownType ?? "WELL_KNOWN_TYPE_ANY");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Inject widget inputs for LLM actions (may already be in API response)
|
|
92
|
+
if (LLM_ACTIONS.has(name)) {
|
|
93
|
+
for (const binding of LLM_WIDGET_INPUTS) {
|
|
94
|
+
if (!inputs[binding.inputName]) {
|
|
95
|
+
inputs[binding.inputName] = {
|
|
96
|
+
type: "WELL_KNOWN_TYPE_ANY",
|
|
97
|
+
required: binding.required,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
25
101
|
}
|
|
102
|
+
// Enrich with catalog metadata (doesn't override inputs/outputs)
|
|
103
|
+
const catalogEntry = catalogIndex.get(name);
|
|
104
|
+
agents[name] = {
|
|
105
|
+
name: catalogEntry?.displayName ?? name,
|
|
106
|
+
category: catalogEntry?.category ?? "unknown",
|
|
107
|
+
description: catalogEntry?.description,
|
|
108
|
+
whenToUse: catalogEntry?.whenToUse,
|
|
109
|
+
aliases: catalogEntry?.aliases,
|
|
110
|
+
tier: catalogEntry?.tier,
|
|
111
|
+
inputs,
|
|
112
|
+
outputs,
|
|
113
|
+
constraints: catalogEntry?.criticalRules,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return agents;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Build agents from DE search — middle layer between API and static catalog.
|
|
120
|
+
* Queries DE for entity-type action docs, matches to catalog by action name
|
|
121
|
+
* extracted from the document ID (e.g., "entity:search_v2" → "search_v2").
|
|
122
|
+
* Returns null if DE search yields no action entities.
|
|
123
|
+
*/
|
|
124
|
+
async function buildFromDE(catalogIndex) {
|
|
125
|
+
const response = await searchDocuments("action definitions inputs outputs workflow actions", {
|
|
126
|
+
topK: 15,
|
|
127
|
+
filters: { type: ["entity"] },
|
|
128
|
+
});
|
|
129
|
+
if (!response.results || response.results.length === 0)
|
|
130
|
+
return null;
|
|
131
|
+
const deAgents = {};
|
|
132
|
+
const deActionNames = new Set();
|
|
133
|
+
for (const result of response.results) {
|
|
134
|
+
const doc = result.document;
|
|
135
|
+
if (!doc?.id)
|
|
136
|
+
continue;
|
|
137
|
+
// Extract action name from entity ID (e.g., "entity:search_v2" → "search_v2")
|
|
138
|
+
const actionName = doc.id.startsWith("entity:") ? doc.id.slice("entity:".length) : "";
|
|
139
|
+
if (!actionName)
|
|
140
|
+
continue;
|
|
141
|
+
deActionNames.add(actionName);
|
|
142
|
+
const catalogEntry = catalogIndex.get(actionName);
|
|
143
|
+
const inputs = {};
|
|
26
144
|
const outputs = {};
|
|
27
|
-
|
|
28
|
-
|
|
145
|
+
// Use catalog I/O specs (DE entities don't carry structured I/O)
|
|
146
|
+
if (catalogEntry) {
|
|
147
|
+
for (const input of catalogEntry.inputs ?? []) {
|
|
148
|
+
inputs[input.name] = { type: input.type, required: input.required };
|
|
149
|
+
}
|
|
150
|
+
for (const output of catalogEntry.outputs ?? []) {
|
|
151
|
+
outputs[output.name] = output.type;
|
|
152
|
+
}
|
|
29
153
|
}
|
|
30
|
-
if (LLM_ACTIONS.has(
|
|
154
|
+
if (LLM_ACTIONS.has(actionName)) {
|
|
31
155
|
for (const binding of LLM_WIDGET_INPUTS) {
|
|
156
|
+
if (!inputs[binding.inputName]) {
|
|
157
|
+
inputs[binding.inputName] = {
|
|
158
|
+
type: "WELL_KNOWN_TYPE_ANY",
|
|
159
|
+
required: binding.required,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
deAgents[actionName] = {
|
|
165
|
+
name: catalogEntry?.displayName ?? doc.name ?? actionName,
|
|
166
|
+
category: catalogEntry?.category ?? "unknown",
|
|
167
|
+
description: catalogEntry?.description ?? doc.description ?? doc.summary,
|
|
168
|
+
whenToUse: catalogEntry?.whenToUse,
|
|
169
|
+
aliases: catalogEntry?.aliases,
|
|
170
|
+
tier: catalogEntry?.tier,
|
|
171
|
+
inputs,
|
|
172
|
+
outputs,
|
|
173
|
+
constraints: catalogEntry?.criticalRules,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (deActionNames.size === 0)
|
|
177
|
+
return null;
|
|
178
|
+
// Supplement with catalog entries not found in DE
|
|
179
|
+
for (const [actionName, catalogEntry] of catalogIndex) {
|
|
180
|
+
if (deAgents[actionName])
|
|
181
|
+
continue;
|
|
182
|
+
deAgents[actionName] = buildCatalogAgent(actionName, catalogEntry);
|
|
183
|
+
}
|
|
184
|
+
return deAgents;
|
|
185
|
+
}
|
|
186
|
+
/** Build a CompactAgent from a single catalog entry. */
|
|
187
|
+
function buildCatalogAgent(actionName, entry) {
|
|
188
|
+
const inputs = {};
|
|
189
|
+
for (const input of entry.inputs ?? []) {
|
|
190
|
+
inputs[input.name] = { type: input.type, required: input.required };
|
|
191
|
+
}
|
|
192
|
+
const outputs = {};
|
|
193
|
+
for (const output of entry.outputs ?? []) {
|
|
194
|
+
outputs[output.name] = output.type;
|
|
195
|
+
}
|
|
196
|
+
if (LLM_ACTIONS.has(actionName)) {
|
|
197
|
+
for (const binding of LLM_WIDGET_INPUTS) {
|
|
198
|
+
if (!inputs[binding.inputName]) {
|
|
32
199
|
inputs[binding.inputName] = {
|
|
33
200
|
type: "WELL_KNOWN_TYPE_ANY",
|
|
34
201
|
required: binding.required,
|
|
35
202
|
};
|
|
36
203
|
}
|
|
37
204
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
name: entry.displayName,
|
|
208
|
+
category: entry.category,
|
|
209
|
+
description: entry.description,
|
|
210
|
+
whenToUse: entry.whenToUse,
|
|
211
|
+
aliases: entry.aliases,
|
|
212
|
+
tier: entry.tier,
|
|
213
|
+
inputs,
|
|
214
|
+
outputs,
|
|
215
|
+
constraints: entry.criticalRules,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Build agents from static AGENT_CATALOG (fallback when API unavailable).
|
|
220
|
+
*/
|
|
221
|
+
function buildFromCatalog(catalogIndex) {
|
|
222
|
+
const agents = {};
|
|
223
|
+
for (const [actionName, entry] of catalogIndex) {
|
|
224
|
+
agents[actionName] = buildCatalogAgent(actionName, entry);
|
|
49
225
|
}
|
|
50
226
|
return agents;
|
|
51
227
|
}
|
|
@@ -118,11 +294,37 @@ export function buildConstraints() {
|
|
|
118
294
|
};
|
|
119
295
|
}
|
|
120
296
|
/**
|
|
121
|
-
* Generate the complete compact schema for workflow generation
|
|
297
|
+
* Generate the complete compact schema for workflow generation.
|
|
298
|
+
* API-first: fetches live action definitions when client is provided.
|
|
122
299
|
*/
|
|
123
|
-
export function generateSchema() {
|
|
300
|
+
export async function generateSchema(client, cache) {
|
|
301
|
+
const invariants = cache?.getInvariants()?.length
|
|
302
|
+
? cache.getInvariants()
|
|
303
|
+
: STRUCTURAL_INVARIANTS;
|
|
304
|
+
const { agents, source } = await buildCompactAgents(client);
|
|
305
|
+
return {
|
|
306
|
+
agents,
|
|
307
|
+
schema_source: source,
|
|
308
|
+
typeRules: buildTypeRules(),
|
|
309
|
+
inputRules: INPUT_SOURCE_RULES.map(rule => ({
|
|
310
|
+
action: rule.actionPattern,
|
|
311
|
+
recommended: rule.recommended,
|
|
312
|
+
avoid: rule.avoid,
|
|
313
|
+
severity: rule.severity,
|
|
314
|
+
})),
|
|
315
|
+
constraints: buildConstraints(),
|
|
316
|
+
structuralInvariants: invariants.map(({ id, rule, violation, fix, severity }) => ({ id, rule, violation, fix, severity })),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/** Synchronous version (catalog-only, no API). For callers that can't await. */
|
|
320
|
+
export function generateSchemaSync(cache) {
|
|
321
|
+
const invariants = cache?.getInvariants()?.length
|
|
322
|
+
? cache.getInvariants()
|
|
323
|
+
: STRUCTURAL_INVARIANTS;
|
|
324
|
+
const { agents, source } = buildCompactAgentsSync();
|
|
124
325
|
return {
|
|
125
|
-
agents
|
|
326
|
+
agents,
|
|
327
|
+
schema_source: source,
|
|
126
328
|
typeRules: buildTypeRules(),
|
|
127
329
|
inputRules: INPUT_SOURCE_RULES.map(rule => ({
|
|
128
330
|
action: rule.actionPattern,
|
|
@@ -131,14 +333,15 @@ export function generateSchema() {
|
|
|
131
333
|
severity: rule.severity,
|
|
132
334
|
})),
|
|
133
335
|
constraints: buildConstraints(),
|
|
336
|
+
structuralInvariants: invariants.map(({ id, rule, violation, fix, severity }) => ({ id, rule, violation, fix, severity })),
|
|
134
337
|
};
|
|
135
338
|
}
|
|
136
339
|
/**
|
|
137
340
|
* Generate a Markdown summary suitable for LLM prompts
|
|
138
341
|
* This is ~5K tokens vs ~70K for full documentation
|
|
139
342
|
*/
|
|
140
|
-
export function generateSchemaMarkdown() {
|
|
141
|
-
const schema =
|
|
343
|
+
export function generateSchemaMarkdown(cache) {
|
|
344
|
+
const schema = generateSchemaSync(cache);
|
|
142
345
|
let md = `# Workflow Generation Schema\n\n`;
|
|
143
346
|
// Agent summary table with tier and description
|
|
144
347
|
md += `## Available Agents (${Object.keys(schema.agents).length})\n\n`;
|
|
@@ -195,13 +398,23 @@ export function generateSchemaMarkdown() {
|
|
|
195
398
|
for (const c of schema.constraints.inputFormats) {
|
|
196
399
|
md += `- ${c}\n`;
|
|
197
400
|
}
|
|
401
|
+
// Structural self-check — critical invariants the LLM must verify before outputting
|
|
402
|
+
const criticalInvariants = schema.structuralInvariants.filter(i => i.severity === "critical");
|
|
403
|
+
md += `\n## Self-Check (VERIFY BEFORE OUTPUTTING)\n\n`;
|
|
404
|
+
md += `Before finalizing your workflow, verify each rule:\n\n`;
|
|
405
|
+
md += `| # | Rule | Violation Example | Fix |\n`;
|
|
406
|
+
md += `|---|------|-------------------|-----|\n`;
|
|
407
|
+
for (let i = 0; i < criticalInvariants.length; i++) {
|
|
408
|
+
const inv = criticalInvariants[i];
|
|
409
|
+
md += `| ${i + 1} | ${inv.rule} | ${inv.violation} | ${inv.fix} |\n`;
|
|
410
|
+
}
|
|
198
411
|
return md;
|
|
199
412
|
}
|
|
200
413
|
/**
|
|
201
414
|
* Get compact agent info for a specific action
|
|
202
415
|
*/
|
|
203
416
|
export function getAgentSchema(actionName) {
|
|
204
|
-
const schema =
|
|
417
|
+
const schema = generateSchemaSync();
|
|
205
418
|
return schema.agents[actionName];
|
|
206
419
|
}
|
|
207
420
|
/**
|
|
@@ -215,7 +428,7 @@ export function isTypeCompatible(sourceType, targetType) {
|
|
|
215
428
|
if (sourceType === targetType)
|
|
216
429
|
return true;
|
|
217
430
|
// Check explicit rules
|
|
218
|
-
const schema =
|
|
431
|
+
const schema = generateSchemaSync();
|
|
219
432
|
const rule = schema.typeRules.find(r => r.sourceType === sourceType);
|
|
220
433
|
return rule?.targetTypes.includes(targetType) ?? false;
|
|
221
434
|
}
|