@ema.co/mcp-toolkit 2026.2.27 → 2026.2.28
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/ema-user-guide.md +7 -6
- package/.context/public/guides/mcp-tools-guide.md +46 -23
- package/dist/config/index.js +11 -0
- package/dist/config/workflow-patterns.js +361 -0
- package/dist/mcp/autobuilder.js +2 -2
- package/dist/mcp/domain/generation-schema.js +15 -9
- package/dist/mcp/domain/structural-rules.js +3 -3
- package/dist/mcp/domain/validation-rules.js +20 -27
- package/dist/mcp/domain/workflow-generator.js +3 -3
- package/dist/mcp/domain/workflow-graph.js +1 -1
- package/dist/mcp/guidance.js +60 -1
- package/dist/mcp/handlers/conversation/adapter.js +13 -0
- package/dist/mcp/handlers/conversation/create.js +19 -0
- package/dist/mcp/handlers/conversation/delete.js +18 -0
- package/dist/mcp/handlers/conversation/formatters.js +62 -0
- package/dist/mcp/handlers/conversation/history.js +15 -0
- package/dist/mcp/handlers/conversation/index.js +43 -0
- package/dist/mcp/handlers/conversation/list.js +40 -0
- package/dist/mcp/handlers/conversation/messages.js +13 -0
- package/dist/mcp/handlers/conversation/rename.js +16 -0
- package/dist/mcp/handlers/conversation/send.js +90 -0
- package/dist/mcp/handlers/data/index.js +169 -3
- package/dist/mcp/handlers/feedback/client-id.js +49 -0
- package/dist/mcp/handlers/feedback/coalesce.js +167 -0
- package/dist/mcp/handlers/feedback/index.js +42 -1
- package/dist/mcp/handlers/feedback/outbox.js +301 -0
- package/dist/mcp/handlers/feedback/probes.js +127 -0
- package/dist/mcp/handlers/feedback/remote-store.js +59 -0
- package/dist/mcp/handlers/feedback/store.js +13 -1
- package/dist/mcp/handlers/persona/delete.js +7 -28
- package/dist/mcp/handlers/persona/update.js +7 -26
- package/dist/mcp/handlers/persona/version.js +30 -15
- package/dist/mcp/handlers/template/adapter.js +23 -0
- package/dist/mcp/handlers/template/crud.js +174 -0
- package/dist/mcp/handlers/template/index.js +6 -7
- package/dist/mcp/handlers/workflow/adapter.js +30 -46
- package/dist/mcp/handlers/workflow/index.js +2 -2
- package/dist/mcp/handlers/workflow/validation.js +2 -2
- package/dist/mcp/knowledge-guidance-topics.js +90 -53
- package/dist/mcp/knowledge.js +7 -357
- package/dist/mcp/prompts.js +5 -5
- package/dist/mcp/resources-dynamic.js +46 -38
- package/dist/mcp/resources-validation.js +5 -5
- package/dist/mcp/server.js +38 -5
- package/dist/mcp/tools.js +340 -8
- package/dist/sdk/client-adapter.js +90 -2
- package/dist/sdk/client.js +7 -0
- package/dist/sdk/ema-client.js +242 -27
- package/dist/sdk/generated/agent-catalog.js +96 -39
- package/dist/sdk/generated/deprecated-actions.js +1 -1
- package/dist/sdk/grpc-client.js +67 -5
- package/dist/sync/central-factory.js +86 -0
- package/dist/sync/central-version-storage.js +387 -0
- package/dist/sync/dis-port.js +75 -0
- package/dist/sync/version-policy.js +29 -31
- package/dist/sync/version-storage-interface.js +11 -0
- package/dist/sync/version-storage.js +22 -22
- package/package.json +2 -1
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Agent Catalog —
|
|
2
|
+
* Agent Catalog — API Fallback + Enrichments
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* SINGLE SOURCE OF TRUTH for action definitions used when API is unavailable.
|
|
5
|
+
* Enriched with `tier` and `aliases` for guidance quality.
|
|
6
|
+
*
|
|
7
|
+
* Tier definitions:
|
|
8
|
+
* - core: Production-proven, actively recommended
|
|
9
|
+
* - standard: Available, use when intent calls for it
|
|
10
|
+
* - specialized: Domain-specific verticals
|
|
11
|
+
* - experimental: Minimal/zero production usage
|
|
12
|
+
*
|
|
13
|
+
* IMPORTANT: respond_for_external_actions is ONLY for external_action_caller results.
|
|
14
|
+
* For KB Q&A after search, use call_llm with named_inputs (89% of production personas).
|
|
6
15
|
*
|
|
7
|
-
* Generated from source repos via scheduled workflows.
|
|
8
16
|
* See: .context/core/guides/source-repos.md for full architecture
|
|
9
17
|
*/
|
|
10
18
|
export const AGENT_CATALOG = [
|
|
@@ -24,6 +32,8 @@ export const AGENT_CATALOG = [
|
|
|
24
32
|
"Each user message triggers a NEW workflow execution",
|
|
25
33
|
"chat_conversation accumulates; user_query is current message only",
|
|
26
34
|
],
|
|
35
|
+
tier: "core",
|
|
36
|
+
aliases: ["Chat Entry Point"],
|
|
27
37
|
},
|
|
28
38
|
{
|
|
29
39
|
actionName: "document_trigger",
|
|
@@ -35,6 +45,8 @@ export const AGENT_CATALOG = [
|
|
|
35
45
|
{ name: "user_query", type: "WELL_KNOWN_TYPE_DOCUMENT", description: "Uploaded document(s)" },
|
|
36
46
|
],
|
|
37
47
|
whenToUse: "When workflow is triggered by document upload for processing/extraction",
|
|
48
|
+
tier: "core",
|
|
49
|
+
aliases: ["Document Entry Point", "Upload Trigger"],
|
|
38
50
|
},
|
|
39
51
|
{
|
|
40
52
|
actionName: "voice_trigger",
|
|
@@ -47,6 +59,8 @@ export const AGENT_CATALOG = [
|
|
|
47
59
|
{ name: "user_query", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "Current utterance" },
|
|
48
60
|
],
|
|
49
61
|
whenToUse: "Voice AI employees handling phone calls",
|
|
62
|
+
tier: "core",
|
|
63
|
+
aliases: ["Voice Entry Point", "Phone Call Trigger"],
|
|
50
64
|
},
|
|
51
65
|
// Routing / Classification
|
|
52
66
|
{
|
|
@@ -71,6 +85,8 @@ export const AGENT_CATALOG = [
|
|
|
71
85
|
whenToUse: "When you need to route chat conversations to different processing paths based on intent",
|
|
72
86
|
whenNotToUse: "Simple single-path workflows that don't need routing",
|
|
73
87
|
example: "Categories: [Market_Impact, Client_Lookup, Compliance_Check, Fallback]",
|
|
88
|
+
tier: "core",
|
|
89
|
+
aliases: ["Intent Router", "Conversation Classifier"],
|
|
74
90
|
},
|
|
75
91
|
{
|
|
76
92
|
actionName: "text_categorizer",
|
|
@@ -87,6 +103,8 @@ export const AGENT_CATALOG = [
|
|
|
87
103
|
"Same rules as chat_categorizer: must have Fallback, edges for each category",
|
|
88
104
|
],
|
|
89
105
|
whenToUse: "When routing based on text content rather than full conversation history",
|
|
106
|
+
tier: "standard",
|
|
107
|
+
aliases: ["Text Router"],
|
|
90
108
|
},
|
|
91
109
|
{
|
|
92
110
|
actionName: "document_categorizer",
|
|
@@ -100,6 +118,8 @@ export const AGENT_CATALOG = [
|
|
|
100
118
|
{ name: "category", type: "WELL_KNOWN_TYPE_ENUM", description: "Document classification" },
|
|
101
119
|
],
|
|
102
120
|
whenToUse: "When routing based on document type or content",
|
|
121
|
+
tier: "standard",
|
|
122
|
+
aliases: ["Document Router"],
|
|
103
123
|
},
|
|
104
124
|
{
|
|
105
125
|
actionName: "conversation_to_search_query",
|
|
@@ -118,6 +138,8 @@ export const AGENT_CATALOG = [
|
|
|
118
138
|
"May still be needed WITH chat_conversation for downstream agents requiring specific format",
|
|
119
139
|
"Use to reduce conversation size due to LLM context window limits",
|
|
120
140
|
],
|
|
141
|
+
tier: "standard",
|
|
142
|
+
aliases: ["Query Refiner", "Conversation to Query"],
|
|
121
143
|
},
|
|
122
144
|
// Search & Retrieval
|
|
123
145
|
{
|
|
@@ -135,6 +157,8 @@ export const AGENT_CATALOG = [
|
|
|
135
157
|
whenToUse: "When searching static uploaded documents or internal knowledge base. Always use search/v2 (not deprecated search/v0).",
|
|
136
158
|
whenNotToUse: "For real-time web content — use live_web_search or ai_web_search instead",
|
|
137
159
|
example: "FAQ lookup, policy search, documentation assistant",
|
|
160
|
+
tier: "core",
|
|
161
|
+
aliases: ["Knowledge Search", "KB Lookup", "File Search"],
|
|
138
162
|
},
|
|
139
163
|
{
|
|
140
164
|
actionName: "live_web_search",
|
|
@@ -149,27 +173,11 @@ export const AGENT_CATALOG = [
|
|
|
149
173
|
],
|
|
150
174
|
whenToUse: "When you need real-time external information not in the knowledge base",
|
|
151
175
|
example: "Current events, live data, external research",
|
|
176
|
+
tier: "standard",
|
|
177
|
+
aliases: ["Web Search", "Real-Time Search"],
|
|
152
178
|
},
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
displayName: "Combine Search Results",
|
|
156
|
-
category: "search",
|
|
157
|
-
description: "Merges results from multiple search sources with deduplication.",
|
|
158
|
-
inputs: [
|
|
159
|
-
{ name: "search_results_1", type: "WELL_KNOWN_TYPE_SEARCH_RESULT", required: true, description: "First result set" },
|
|
160
|
-
{ name: "search_results_2", type: "WELL_KNOWN_TYPE_SEARCH_RESULT", required: true, description: "Second result set" },
|
|
161
|
-
],
|
|
162
|
-
outputs: [
|
|
163
|
-
{ name: "combined_results", type: "WELL_KNOWN_TYPE_SEARCH_RESULT", description: "Merged, deduplicated results" },
|
|
164
|
-
],
|
|
165
|
-
criticalRules: [
|
|
166
|
-
"AVOID using unless necessary - prefer combine_and_rerank_search_results or call_llm with named_inputs",
|
|
167
|
-
"combine_and_rerank_search_results provides intelligent relevance ranking",
|
|
168
|
-
"call_llm with named_inputs allows prompt-based ranking control",
|
|
169
|
-
],
|
|
170
|
-
whenToUse: "When combining local + web search, or multiple knowledge bases",
|
|
171
|
-
whenNotToUse: "When you need intelligent ranking - use combine_and_rerank_search_results instead",
|
|
172
|
-
},
|
|
179
|
+
// combine_search_results REMOVED — does not exist in the live API.
|
|
180
|
+
// Use combine_and_rerank_search_results instead.
|
|
173
181
|
{
|
|
174
182
|
actionName: "combine_and_rerank_search_results",
|
|
175
183
|
displayName: "Rerank Search Results",
|
|
@@ -183,6 +191,8 @@ export const AGENT_CATALOG = [
|
|
|
183
191
|
{ name: "reranked_results", type: "WELL_KNOWN_TYPE_SEARCH_RESULT", description: "Reranked results" },
|
|
184
192
|
],
|
|
185
193
|
whenToUse: "When you need to prioritize results by relevance after combining multiple sources",
|
|
194
|
+
tier: "standard",
|
|
195
|
+
aliases: ["Rerank Results", "Result Prioritizer"],
|
|
186
196
|
},
|
|
187
197
|
{
|
|
188
198
|
actionName: "document_metasearch",
|
|
@@ -197,6 +207,8 @@ export const AGENT_CATALOG = [
|
|
|
197
207
|
{ name: "document_metasearch_results", type: "WELL_KNOWN_TYPE_SEARCH_RESULT", description: "Metadata search results" },
|
|
198
208
|
],
|
|
199
209
|
whenToUse: "When searching by document metadata rather than content",
|
|
210
|
+
tier: "standard",
|
|
211
|
+
aliases: ["Metadata Search"],
|
|
200
212
|
},
|
|
201
213
|
// Generation & Response
|
|
202
214
|
{
|
|
@@ -222,6 +234,8 @@ export const AGENT_CATALOG = [
|
|
|
222
234
|
],
|
|
223
235
|
example: "named_inputs_Market_Context, named_inputs_Client_Data | " +
|
|
224
236
|
"LLM Template prompt: 'Generate structured content with clear ## section headers, organized by themes'",
|
|
237
|
+
tier: "core",
|
|
238
|
+
aliases: ["Respond", "LLM Response", "Generate Text"],
|
|
225
239
|
},
|
|
226
240
|
{
|
|
227
241
|
actionName: "generate_document",
|
|
@@ -243,6 +257,8 @@ export const AGENT_CATALOG = [
|
|
|
243
257
|
"Optional: Add CSS/styling in markdown for professional formatting",
|
|
244
258
|
],
|
|
245
259
|
example: "detailed_content (call_llm) → generate_doc → send_email.named_inputs_Attachment",
|
|
260
|
+
tier: "standard",
|
|
261
|
+
aliases: ["Publish Document", "Create PDF"],
|
|
246
262
|
},
|
|
247
263
|
{
|
|
248
264
|
actionName: "respond_with_sources",
|
|
@@ -261,6 +277,8 @@ export const AGENT_CATALOG = [
|
|
|
261
277
|
"Enable use_citation_based_filtering for trust",
|
|
262
278
|
"Implement confidence thresholds for quality control",
|
|
263
279
|
],
|
|
280
|
+
tier: "standard",
|
|
281
|
+
aliases: ["Answer with Citations", "Grounded Response"],
|
|
264
282
|
},
|
|
265
283
|
{
|
|
266
284
|
actionName: "respond_for_external_actions",
|
|
@@ -275,6 +293,8 @@ export const AGENT_CATALOG = [
|
|
|
275
293
|
{ name: "response", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "Human-friendly explanation" },
|
|
276
294
|
],
|
|
277
295
|
whenToUse: "After external_action_caller to explain results to user",
|
|
296
|
+
tier: "standard",
|
|
297
|
+
aliases: ["Explain Tool Results"],
|
|
278
298
|
},
|
|
279
299
|
{
|
|
280
300
|
actionName: "fixed_response",
|
|
@@ -299,6 +319,8 @@ export const AGENT_CATALOG = [
|
|
|
299
319
|
],
|
|
300
320
|
example: "Template: 'Dear {{Customer_Name}}, your order {{Order_ID}} has been {{Status}}.' " +
|
|
301
321
|
"→ Connect entity_extraction.customer_name to named_inputs_Customer_Name",
|
|
322
|
+
tier: "core",
|
|
323
|
+
aliases: ["Template Response", "Static Response"],
|
|
302
324
|
},
|
|
303
325
|
{
|
|
304
326
|
actionName: "send_email_agent",
|
|
@@ -322,6 +344,8 @@ export const AGENT_CATALOG = [
|
|
|
322
344
|
"Never connect summarized_conversation or search_results directly to email_to",
|
|
323
345
|
],
|
|
324
346
|
example: "entity_extraction → fixed_response('{{email}}') → send_email.email_to (CORRECT pattern)",
|
|
347
|
+
tier: "core",
|
|
348
|
+
aliases: ["Email Sender"],
|
|
325
349
|
},
|
|
326
350
|
// External Actions
|
|
327
351
|
{
|
|
@@ -342,6 +366,8 @@ export const AGENT_CATALOG = [
|
|
|
342
366
|
"external_action_caller does NOT support HITL — only send_email_agent and entity_extraction_with_documents do",
|
|
343
367
|
],
|
|
344
368
|
example: "ServiceNow ticket creation, Salesforce record update, Email sending",
|
|
369
|
+
tier: "core",
|
|
370
|
+
aliases: ["Call API", "Tool Caller", "Intelligent Actions"],
|
|
345
371
|
},
|
|
346
372
|
// Entity & Rule Processing
|
|
347
373
|
{
|
|
@@ -362,6 +388,8 @@ export const AGENT_CATALOG = [
|
|
|
362
388
|
],
|
|
363
389
|
whenToUse: "When you need structured data extraction for cross-document linking, API calls, or audit trails with citations",
|
|
364
390
|
whenNotToUse: "For simple conversational queries—use LLM resolution instead",
|
|
391
|
+
tier: "core",
|
|
392
|
+
aliases: ["Extract Fields", "Data Extraction"],
|
|
365
393
|
},
|
|
366
394
|
{
|
|
367
395
|
actionName: "rule_validation_with_documents",
|
|
@@ -376,31 +404,38 @@ export const AGENT_CATALOG = [
|
|
|
376
404
|
{ name: "ruleset_output", type: "WELL_KNOWN_TYPE_ANY", description: "Pass/Fail results per rule with explanations" },
|
|
377
405
|
],
|
|
378
406
|
whenToUse: "For compliance checks, business rule validation, threshold checking. Rules (e.g., 'Required Fields', 'Data Variance') are defined in the node's UI settings — the LLM only needs to wire inputs/outputs correctly.",
|
|
407
|
+
tier: "standard",
|
|
408
|
+
aliases: ["Business Rules", "Compliance Checker"],
|
|
379
409
|
},
|
|
380
410
|
// Human Collaboration
|
|
381
|
-
// NOTE: general_hitl is NOT a deployable node — it appears in catalogs but cannot be deployed.
|
|
382
|
-
// HITL is implemented as a FLAG on specific agents: entity_extraction_with_documents and send_email_agent.
|
|
383
|
-
// Kept here for reference when analyzing existing workflows that may use it.
|
|
384
411
|
{
|
|
385
412
|
actionName: "general_hitl",
|
|
386
|
-
displayName: "Human Collaboration
|
|
413
|
+
displayName: "Human Collaboration Agent",
|
|
387
414
|
category: "collaboration",
|
|
388
|
-
description: "
|
|
415
|
+
description: "Pauses workflow to collect human input, then resumes. Supports three modes: Conversational (back-and-forth dialogue with success/failure criteria), Standard Form (structured fields: string, number, boolean, multi-select), and Custom Form (external hosted form via URL). Uses intelligent categorization to detect success, failure, or need for continued conversation.",
|
|
389
416
|
inputs: [
|
|
390
|
-
{ name: "
|
|
417
|
+
{ name: "conversation", type: "WELL_KNOWN_TYPE_CHAT_CONVERSATION", required: true, description: "Chat conversation for ongoing dialogue context" },
|
|
418
|
+
{ name: "named_inputs", type: "WELL_KNOWN_TYPE_ANY", required: false, description: "Flexible inputs from other agents (search results, tool outputs, text, structured data)" },
|
|
391
419
|
],
|
|
392
420
|
outputs: [
|
|
393
|
-
{ name: "
|
|
394
|
-
{ name: "
|
|
421
|
+
{ name: "human_collaboration_status", type: "WELL_KNOWN_TYPE_ENUM", description: "HITL Success or HITL Failure — use with runIf for branching" },
|
|
422
|
+
{ name: "summarized_conversation", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "Summary of the full conversation history" },
|
|
395
423
|
],
|
|
396
424
|
criticalRules: [
|
|
397
|
-
"
|
|
398
|
-
"
|
|
399
|
-
"
|
|
400
|
-
"
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
425
|
+
"Three modes: Conversational (dialogue), Standard Form (structured input), Custom Form (external URL)",
|
|
426
|
+
"Conversational mode: define success criteria, failure criteria, and instructions for Ema's behavior",
|
|
427
|
+
"Standard Form mode: define fields (string, number, boolean, multi-select). Output is JSON of submitted values",
|
|
428
|
+
"Custom Form mode: requires form URL, button label, and display dimensions",
|
|
429
|
+
"Max exchanges setting prevents infinite loops — defaults to 5. Exceeding max = HITL Failure",
|
|
430
|
+
"Context window controls how many recent messages the categorizer considers (default: 10)",
|
|
431
|
+
"Use runIf conditions on downstream nodes to branch on HITL Success vs HITL Failure",
|
|
432
|
+
"entity_extraction_with_documents and send_email_agent also support HITL via their own flags (disable_human_interaction: false)",
|
|
433
|
+
],
|
|
434
|
+
whenToUse: "Content approval workflows, quality assurance checkpoints, interactive data collection, approval workflows for business decisions",
|
|
435
|
+
whenNotToUse: "Simple yes/no approval on email sends — use send_email_agent's built-in HITL flag instead",
|
|
436
|
+
example: "Content approval: success='User approves content', instructions='Present content and ask for feedback', max_exchanges=3",
|
|
437
|
+
tier: "core",
|
|
438
|
+
aliases: ["HITL", "Human Review", "Approval Gate", "Human-in-the-Loop"],
|
|
404
439
|
},
|
|
405
440
|
// Validation & Guardrails
|
|
406
441
|
{
|
|
@@ -416,6 +451,8 @@ export const AGENT_CATALOG = [
|
|
|
416
451
|
{ name: "abstain_reason", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "Reason if validation fails" },
|
|
417
452
|
],
|
|
418
453
|
whenToUse: "For guardrails, compliance checking on generated responses, quality control",
|
|
454
|
+
tier: "experimental",
|
|
455
|
+
aliases: ["Guardrail", "Response Checker"],
|
|
419
456
|
},
|
|
420
457
|
{
|
|
421
458
|
actionName: "abstain_action",
|
|
@@ -429,6 +466,7 @@ export const AGENT_CATALOG = [
|
|
|
429
466
|
{ name: "abstain_reason", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "Abstention message" },
|
|
430
467
|
],
|
|
431
468
|
whenToUse: "When the AI should explicitly decline to answer or redirect",
|
|
469
|
+
tier: "experimental",
|
|
432
470
|
},
|
|
433
471
|
// Sentiment & Analysis
|
|
434
472
|
{
|
|
@@ -443,6 +481,7 @@ export const AGENT_CATALOG = [
|
|
|
443
481
|
{ name: "sentiment", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "Sentiment analysis" },
|
|
444
482
|
],
|
|
445
483
|
whenToUse: "When you need to gauge user emotion for routing or response tone adjustment",
|
|
484
|
+
tier: "standard",
|
|
446
485
|
},
|
|
447
486
|
// Domain Agents - Finance
|
|
448
487
|
{
|
|
@@ -453,6 +492,7 @@ export const AGENT_CATALOG = [
|
|
|
453
492
|
inputs: [{ name: "query", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", required: true, description: "Risk query" }],
|
|
454
493
|
outputs: [{ name: "risk_assessment", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "Risk analysis" }],
|
|
455
494
|
whenToUse: "Financial services: portfolio risk, credit assessment, exposure analysis",
|
|
495
|
+
tier: "specialized",
|
|
456
496
|
},
|
|
457
497
|
{
|
|
458
498
|
actionName: "financial_statement_analyzer",
|
|
@@ -462,6 +502,7 @@ export const AGENT_CATALOG = [
|
|
|
462
502
|
inputs: [{ name: "documents", type: "WELL_KNOWN_TYPE_DOCUMENT", required: true, description: "Financial documents" }],
|
|
463
503
|
outputs: [{ name: "analysis", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "Financial analysis" }],
|
|
464
504
|
whenToUse: "Analyzing P&L, balance sheets, annual reports",
|
|
505
|
+
tier: "specialized",
|
|
465
506
|
},
|
|
466
507
|
{
|
|
467
508
|
actionName: "tax_advisor",
|
|
@@ -471,6 +512,7 @@ export const AGENT_CATALOG = [
|
|
|
471
512
|
inputs: [{ name: "query", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", required: true, description: "Tax question" }],
|
|
472
513
|
outputs: [{ name: "tax_advice", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "Tax guidance" }],
|
|
473
514
|
whenToUse: "Tax planning, compliance questions, deduction analysis",
|
|
515
|
+
tier: "specialized",
|
|
474
516
|
},
|
|
475
517
|
{
|
|
476
518
|
actionName: "audit_evidence_verifier",
|
|
@@ -480,6 +522,7 @@ export const AGENT_CATALOG = [
|
|
|
480
522
|
inputs: [{ name: "documents", type: "WELL_KNOWN_TYPE_DOCUMENT", required: true, description: "Audit documents" }],
|
|
481
523
|
outputs: [{ name: "verification", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "Verification results" }],
|
|
482
524
|
whenToUse: "Audit support, evidence verification, compliance documentation",
|
|
525
|
+
tier: "specialized",
|
|
483
526
|
},
|
|
484
527
|
// Domain Agents - Healthcare
|
|
485
528
|
{
|
|
@@ -490,6 +533,7 @@ export const AGENT_CATALOG = [
|
|
|
490
533
|
inputs: [{ name: "documents", type: "WELL_KNOWN_TYPE_DOCUMENT", required: true, description: "Medical records" }],
|
|
491
534
|
outputs: [{ name: "summary", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "Record summary" }],
|
|
492
535
|
whenToUse: "Healthcare: summarizing patient records, medical history, clinical notes",
|
|
536
|
+
tier: "specialized",
|
|
493
537
|
},
|
|
494
538
|
{
|
|
495
539
|
actionName: "medical_research_assistant",
|
|
@@ -499,6 +543,7 @@ export const AGENT_CATALOG = [
|
|
|
499
543
|
inputs: [{ name: "query", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", required: true, description: "Research query" }],
|
|
500
544
|
outputs: [{ name: "research", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "Research findings" }],
|
|
501
545
|
whenToUse: "Medical research, literature review, clinical guidance",
|
|
546
|
+
tier: "specialized",
|
|
502
547
|
},
|
|
503
548
|
{
|
|
504
549
|
actionName: "insurance_claim_summarizer",
|
|
@@ -508,6 +553,7 @@ export const AGENT_CATALOG = [
|
|
|
508
553
|
inputs: [{ name: "documents", type: "WELL_KNOWN_TYPE_DOCUMENT", required: true, description: "Claim documents" }],
|
|
509
554
|
outputs: [{ name: "summary", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "Claim summary" }],
|
|
510
555
|
whenToUse: "Insurance processing, claim analysis",
|
|
556
|
+
tier: "specialized",
|
|
511
557
|
},
|
|
512
558
|
// Domain Agents - Legal
|
|
513
559
|
{
|
|
@@ -518,6 +564,7 @@ export const AGENT_CATALOG = [
|
|
|
518
564
|
inputs: [{ name: "documents", type: "WELL_KNOWN_TYPE_DOCUMENT", required: true, description: "Documents to analyze" }],
|
|
519
565
|
outputs: [{ name: "compliance_findings", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "Compliance analysis" }],
|
|
520
566
|
whenToUse: "Regulatory compliance, contract review, policy compliance",
|
|
567
|
+
tier: "specialized",
|
|
521
568
|
},
|
|
522
569
|
{
|
|
523
570
|
actionName: "legal_expert",
|
|
@@ -527,6 +574,7 @@ export const AGENT_CATALOG = [
|
|
|
527
574
|
inputs: [{ name: "query", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", required: true, description: "Legal question" }],
|
|
528
575
|
outputs: [{ name: "legal_analysis", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "Legal guidance" }],
|
|
529
576
|
whenToUse: "Legal research, contract analysis, regulatory questions",
|
|
577
|
+
tier: "specialized",
|
|
530
578
|
},
|
|
531
579
|
{
|
|
532
580
|
actionName: "information_redacter",
|
|
@@ -536,6 +584,7 @@ export const AGENT_CATALOG = [
|
|
|
536
584
|
inputs: [{ name: "documents", type: "WELL_KNOWN_TYPE_DOCUMENT", required: true, description: "Documents to redact" }],
|
|
537
585
|
outputs: [{ name: "redacted", type: "WELL_KNOWN_TYPE_DOCUMENT", description: "Redacted documents" }],
|
|
538
586
|
whenToUse: "Privacy compliance, document sanitization",
|
|
587
|
+
tier: "specialized",
|
|
539
588
|
},
|
|
540
589
|
// Domain Agents - IT/Security
|
|
541
590
|
{
|
|
@@ -546,6 +595,7 @@ export const AGENT_CATALOG = [
|
|
|
546
595
|
inputs: [{ name: "email_content", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", required: true, description: "Email to analyze" }],
|
|
547
596
|
outputs: [{ name: "detection_result", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "Phishing analysis" }],
|
|
548
597
|
whenToUse: "Email security, security operations",
|
|
598
|
+
tier: "specialized",
|
|
549
599
|
},
|
|
550
600
|
{
|
|
551
601
|
actionName: "cybersecurity_expert",
|
|
@@ -555,6 +605,7 @@ export const AGENT_CATALOG = [
|
|
|
555
605
|
inputs: [{ name: "query", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", required: true, description: "Security query" }],
|
|
556
606
|
outputs: [{ name: "security_analysis", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "Security guidance" }],
|
|
557
607
|
whenToUse: "Security incident analysis, vulnerability assessment",
|
|
608
|
+
tier: "specialized",
|
|
558
609
|
},
|
|
559
610
|
{
|
|
560
611
|
actionName: "technical_support",
|
|
@@ -564,6 +615,7 @@ export const AGENT_CATALOG = [
|
|
|
564
615
|
inputs: [{ name: "query", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", required: true, description: "Support query" }],
|
|
565
616
|
outputs: [{ name: "support_response", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "Technical guidance" }],
|
|
566
617
|
whenToUse: "IT helpdesk, troubleshooting, technical guidance",
|
|
618
|
+
tier: "specialized",
|
|
567
619
|
},
|
|
568
620
|
// Domain Agents - Sales
|
|
569
621
|
{
|
|
@@ -574,6 +626,7 @@ export const AGENT_CATALOG = [
|
|
|
574
626
|
inputs: [{ name: "query", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", required: true, description: "Sales query" }],
|
|
575
627
|
outputs: [{ name: "intelligence", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "Sales insights" }],
|
|
576
628
|
whenToUse: "Account research, competitive analysis, opportunity assessment",
|
|
629
|
+
tier: "specialized",
|
|
577
630
|
},
|
|
578
631
|
{
|
|
579
632
|
actionName: "email_writer",
|
|
@@ -583,6 +636,7 @@ export const AGENT_CATALOG = [
|
|
|
583
636
|
inputs: [{ name: "query", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", required: true, description: "Email context" }],
|
|
584
637
|
outputs: [{ name: "email", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "Generated email" }],
|
|
585
638
|
whenToUse: "Sales outreach, customer communication, follow-ups",
|
|
639
|
+
tier: "specialized",
|
|
586
640
|
},
|
|
587
641
|
// Formatting Agents
|
|
588
642
|
{
|
|
@@ -593,6 +647,7 @@ export const AGENT_CATALOG = [
|
|
|
593
647
|
inputs: [{ name: "content", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", required: true, description: "Content to format" }],
|
|
594
648
|
outputs: [{ name: "json", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "JSON formatted output" }],
|
|
595
649
|
whenToUse: "When structured JSON output is required",
|
|
650
|
+
tier: "standard",
|
|
596
651
|
},
|
|
597
652
|
{
|
|
598
653
|
actionName: "json_extractor",
|
|
@@ -602,6 +657,7 @@ export const AGENT_CATALOG = [
|
|
|
602
657
|
inputs: [{ name: "content", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", required: true, description: "Content containing JSON" }],
|
|
603
658
|
outputs: [{ name: "extracted_json", type: "WELL_KNOWN_TYPE_ANY", description: "Extracted JSON object" }],
|
|
604
659
|
whenToUse: "When you need to parse JSON from LLM responses",
|
|
660
|
+
tier: "standard",
|
|
605
661
|
},
|
|
606
662
|
{
|
|
607
663
|
actionName: "markdown_formatter",
|
|
@@ -611,5 +667,6 @@ export const AGENT_CATALOG = [
|
|
|
611
667
|
inputs: [{ name: "content", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", required: true, description: "Content to format" }],
|
|
612
668
|
outputs: [{ name: "markdown", type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES", description: "Markdown formatted output" }],
|
|
613
669
|
whenToUse: "When rich text formatting is needed",
|
|
670
|
+
tier: "standard",
|
|
614
671
|
},
|
|
615
672
|
];
|
|
@@ -240,7 +240,7 @@ export const DEPRECATED_ACTIONS_MAP = {
|
|
|
240
240
|
"json_mapper/v0": { replacement: "json_mapper/v1" },
|
|
241
241
|
"meta_respond/v0": {},
|
|
242
242
|
"meta_respond/v2": {},
|
|
243
|
-
"respond_with_sources/v0": { replacement: "
|
|
243
|
+
"respond_with_sources/v0": { replacement: "call_llm with named_inputs", notes: "For KB Q&A use call_llm; respond_for_external_actions is ONLY for external_action_caller results" },
|
|
244
244
|
"rule_validation_with_documents/v0": { replacement: "rule_validation_with_documents/v1" },
|
|
245
245
|
"search/v0": { replacement: "search/v2", notes: "v2 requires datastore_configs input" },
|
|
246
246
|
"text_categorizer/v0": { replacement: "text_categorizer/v1" },
|
package/dist/sdk/grpc-client.js
CHANGED
|
@@ -14,9 +14,10 @@ import { createGrpcWebTransport } from '@connectrpc/connect-node';
|
|
|
14
14
|
import { create, fromJson } from '@bufbuild/protobuf';
|
|
15
15
|
// Generated service definitions (v2: services and schemas in same _pb.ts file)
|
|
16
16
|
import { ActionManager, ListActionsRequestSchema, ListActionsFromWorkflowRequestSchema, GetActionRequestSchema, } from './generated/protos/service/workflows/v1/action_registry_pb.js';
|
|
17
|
-
import { DashboardsService, GetDashboardRowsRequestSchema, GetDashboardSchemaRequestSchema, } from './generated/protos/service/workflows/v1/dashboards_pb.js';
|
|
17
|
+
import { DashboardsService, GetDashboardRowsRequestSchema, GetDashboardSchemaRequestSchema, ContinueWorkflowForRowRequestSchema, } from './generated/protos/service/workflows/v1/dashboards_pb.js';
|
|
18
|
+
import { ContinuationMessageSchema, } from './generated/protos/service/workflows/v1/coordinator_pb.js';
|
|
18
19
|
import { WorkflowManager, CheckWorkflowRequestSchema, GetWorkflowRequestSchema, } from './generated/protos/service/workflows/v1/rpc/workflow_rpc_pb.js';
|
|
19
|
-
import { DataIngestService, GetRootContentNodesRequestSchema, GetContentNodeAggregatesRequestSchema, ReplicateDataRequestSchema, GetReplicationStatusRequestSchema, FilePickerGroupType, WidgetReplicationMappingSchema, } from './generated/protos/service/dataingest/v1/dataingest_pb.js';
|
|
20
|
+
import { DataIngestService, GetRootContentNodesRequestSchema, GetContentNodeAggregatesRequestSchema, ReplicateDataRequestSchema, GetReplicationStatusRequestSchema, GetSignedUrlRequestSchema, DeleteFilesForGroupRequestSchema, DeletionGroupType, FilePickerGroupType, WidgetReplicationMappingSchema, } from './generated/protos/service/dataingest/v1/dataingest_pb.js';
|
|
20
21
|
import { ConversationReviewService, GetConversationReviewsRequestSchema, GetConversationReviewDetailRequestSchema, ConversationReviewFiltersSchema, } from './generated/protos/service/conversation_review/v1/conversation_review_pb.js';
|
|
21
22
|
import { DebugLogService, WorkflowLevelDebugLogRequestSchema, ActionLevelShowWorkLogRequestSchema, } from './generated/protos/service/persona/v1/debug_logs_pb.js';
|
|
22
23
|
import { DebuggerService, SearchMessagesRequestSchema as DebuggerSearchMessagesRequestSchema, } from './generated/protos/service/debugger/service_pb.js';
|
|
@@ -163,6 +164,22 @@ export class GrpcClient {
|
|
|
163
164
|
const request = create(GetDashboardSchemaRequestSchema, {});
|
|
164
165
|
return this.dashboardsService.getDashboardSchema(request);
|
|
165
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Continue a paused (HITL) workflow for a dashboard row.
|
|
169
|
+
* Sends a continuation message (text, button selection, or form submission)
|
|
170
|
+
* to resume the workflow.
|
|
171
|
+
*
|
|
172
|
+
* @param rowId - The dashboard row ID
|
|
173
|
+
* @param continuationMessage - The continuation message as JSON (matches ContinuationMessage proto)
|
|
174
|
+
* @returns Empty response on success
|
|
175
|
+
*/
|
|
176
|
+
async continueWorkflowForRow(rowId, continuationMessage) {
|
|
177
|
+
const request = create(ContinueWorkflowForRowRequestSchema, {
|
|
178
|
+
rowId,
|
|
179
|
+
continuationMessage: fromJson(ContinuationMessageSchema, continuationMessage),
|
|
180
|
+
});
|
|
181
|
+
return this.dashboardsService.continueWorkflowForRow(request);
|
|
182
|
+
}
|
|
166
183
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
167
184
|
// WorkflowManager RPCs
|
|
168
185
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -218,7 +235,10 @@ export class GrpcClient {
|
|
|
218
235
|
page: opts?.page ?? 1,
|
|
219
236
|
limit: opts?.limit ?? 100,
|
|
220
237
|
});
|
|
221
|
-
|
|
238
|
+
// DataIngestService has PERSONA_REQUIRED auth — pass persona via gRPC header
|
|
239
|
+
return this.dataIngestService.getRootContentNodes(request, {
|
|
240
|
+
headers: new Headers({ 'x-persona-id': personaId }),
|
|
241
|
+
});
|
|
222
242
|
}
|
|
223
243
|
/**
|
|
224
244
|
* Get content node aggregates (file counts by status).
|
|
@@ -233,7 +253,10 @@ export class GrpcClient {
|
|
|
233
253
|
filePickerGroupId: personaId,
|
|
234
254
|
filePickerGroupType: FilePickerGroupType.PERSONA,
|
|
235
255
|
});
|
|
236
|
-
|
|
256
|
+
// DataIngestService has PERSONA_REQUIRED auth — pass persona via gRPC header
|
|
257
|
+
return this.dataIngestService.getContentNodeAggregates(request, {
|
|
258
|
+
headers: new Headers({ 'x-persona-id': personaId }),
|
|
259
|
+
});
|
|
237
260
|
}
|
|
238
261
|
/**
|
|
239
262
|
* Replicate data from one persona to another.
|
|
@@ -259,7 +282,24 @@ export class GrpcClient {
|
|
|
259
282
|
...(m.targetWidget ? { destinationWidgetName: m.targetWidget } : {}),
|
|
260
283
|
})),
|
|
261
284
|
});
|
|
262
|
-
|
|
285
|
+
// DataIngestService has PERSONA_REQUIRED auth — use target persona for write auth
|
|
286
|
+
return this.dataIngestService.replicateData(request, {
|
|
287
|
+
headers: new Headers({ 'x-persona-id': target.id }),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Get a signed download URL for a file by its UploadedFile.Id.
|
|
292
|
+
*
|
|
293
|
+
* @param fileId - The UploadedFile.Id (from ContentNodeResponse.id, NOT contentNodeId)
|
|
294
|
+
* @returns Signed URL, MIME type, title, and deletion status
|
|
295
|
+
*/
|
|
296
|
+
async getSignedUrl(fileId, personaId) {
|
|
297
|
+
const request = create(GetSignedUrlRequestSchema, { fileId });
|
|
298
|
+
// DataIngestService may require PERSONA_REQUIRED auth
|
|
299
|
+
const callOpts = personaId
|
|
300
|
+
? { headers: new Headers({ 'x-persona-id': personaId }) }
|
|
301
|
+
: undefined;
|
|
302
|
+
return this.dataIngestService.getSignedUrl(request, callOpts);
|
|
263
303
|
}
|
|
264
304
|
/**
|
|
265
305
|
* Get the status of a replication request.
|
|
@@ -273,6 +313,28 @@ export class GrpcClient {
|
|
|
273
313
|
});
|
|
274
314
|
return this.dataIngestService.getReplicationStatus(request);
|
|
275
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Delete files from a group (persona/project/dashboard_row) via gRPC.
|
|
318
|
+
*
|
|
319
|
+
* NOTE: DIS does NOT expose a REST DELETE endpoint for files.
|
|
320
|
+
* This gRPC method is the ONLY way to delete files.
|
|
321
|
+
*
|
|
322
|
+
* @param personaId - The persona ID (group)
|
|
323
|
+
* @param fileIds - Array of UploadedFile.Id values to delete
|
|
324
|
+
* @param widgetName - Optional widget name scope
|
|
325
|
+
*/
|
|
326
|
+
async deleteFilesForGroup(personaId, fileIds, widgetName) {
|
|
327
|
+
const request = create(DeleteFilesForGroupRequestSchema, {
|
|
328
|
+
groupId: personaId,
|
|
329
|
+
groupType: DeletionGroupType.PERSONA,
|
|
330
|
+
fileIds,
|
|
331
|
+
...(widgetName ? { widgetName } : {}),
|
|
332
|
+
});
|
|
333
|
+
// DataIngestService has PERSONA_REQUIRED auth
|
|
334
|
+
return this.dataIngestService.deleteFilesForGroup(request, {
|
|
335
|
+
headers: new Headers({ 'x-persona-id': personaId }),
|
|
336
|
+
});
|
|
337
|
+
}
|
|
276
338
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
277
339
|
// ConversationReviewService RPCs (Audit)
|
|
278
340
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central storage factory — creates DIS-backed version storage + policy engine.
|
|
3
|
+
*
|
|
4
|
+
* Used by all 4 call sites (version.ts, update.ts, delete.ts, workflow/adapter.ts)
|
|
5
|
+
* to avoid duplicating construction logic.
|
|
6
|
+
*
|
|
7
|
+
* Also provides lazy migration from local (.ema-versions/) to central (DIS).
|
|
8
|
+
*/
|
|
9
|
+
import { DISAdapter } from "./dis-port.js";
|
|
10
|
+
import { CentralVersionStorage } from "./central-version-storage.js";
|
|
11
|
+
import { VersionPolicyEngine } from "./version-policy.js";
|
|
12
|
+
/**
|
|
13
|
+
* Extract a DISClientLike from an SDK client.
|
|
14
|
+
*
|
|
15
|
+
* At runtime, `createClient()` returns EmaClientAdapter which wraps EmaClientV2.
|
|
16
|
+
* EmaClientV2 satisfies DISClientLike (has uploadDataSource, downloadFile,
|
|
17
|
+
* deleteDataSource, listDataSourceFiles with raw gRPC response).
|
|
18
|
+
*
|
|
19
|
+
* This function checks for the `.v2Client` accessor (EmaClientAdapter) and
|
|
20
|
+
* falls back to using the client directly (e.g., in tests with a mock).
|
|
21
|
+
*/
|
|
22
|
+
function extractDISClient(client) {
|
|
23
|
+
// If the client has a v2Client accessor (EmaClientAdapter), use the inner V2 client
|
|
24
|
+
const maybeAdapter = client;
|
|
25
|
+
if (maybeAdapter.v2Client) {
|
|
26
|
+
return maybeAdapter.v2Client;
|
|
27
|
+
}
|
|
28
|
+
// Fallback: assume the client itself satisfies DISClientLike (tests, direct V2 usage)
|
|
29
|
+
return client;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Create a central version storage + policy engine pair for a persona.
|
|
33
|
+
*
|
|
34
|
+
* @param client - SDK client. Typically the EmaClient/EmaClientAdapter from createClient().
|
|
35
|
+
* The factory auto-extracts the underlying EmaClientV2 for raw DIS operations.
|
|
36
|
+
* @param personaId - The persona ID to scope storage to
|
|
37
|
+
*/
|
|
38
|
+
export function createCentralStorageEngine(client, personaId) {
|
|
39
|
+
const disClient = extractDISClient(client);
|
|
40
|
+
const port = new DISAdapter(disClient, personaId);
|
|
41
|
+
const storage = new CentralVersionStorage(port, personaId);
|
|
42
|
+
const engine = new VersionPolicyEngine(storage);
|
|
43
|
+
return { storage, engine };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Lazily migrate local versions to central storage on first access.
|
|
47
|
+
*
|
|
48
|
+
* Called by version.ts before creating a new snapshot. If central already has
|
|
49
|
+
* versions, this is a no-op. Otherwise, reads the last `limit` local versions
|
|
50
|
+
* and uploads them to DIS, preserving version numbers and parent chains.
|
|
51
|
+
*
|
|
52
|
+
* @param localStorage - Local git-backed storage (VersionStorage)
|
|
53
|
+
* @param centralStorage - DIS-backed central storage
|
|
54
|
+
* @param personaId - Persona to migrate
|
|
55
|
+
* @param limit - Max versions to migrate (default: 5, most recent)
|
|
56
|
+
* @returns Number of versions migrated (0 if central already populated or no local versions)
|
|
57
|
+
*/
|
|
58
|
+
export async function migrateLocalToCentral(localStorage, centralStorage, personaId, limit = 5) {
|
|
59
|
+
// Skip if central already has versions (idempotent)
|
|
60
|
+
const centralManifest = await centralStorage.getManifest(personaId);
|
|
61
|
+
if (centralManifest && centralManifest.versions.length > 0) {
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
// Read local versions
|
|
65
|
+
const localVersions = await localStorage.listVersions(personaId);
|
|
66
|
+
if (localVersions.length === 0) {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
// Take the most recent `limit` versions, sorted ascending for upload order
|
|
70
|
+
const sorted = [...localVersions].sort((a, b) => b.version_number - a.version_number);
|
|
71
|
+
const toMigrate = sorted.slice(0, limit).reverse();
|
|
72
|
+
let migrated = 0;
|
|
73
|
+
for (const entry of toMigrate) {
|
|
74
|
+
try {
|
|
75
|
+
const snapshot = await localStorage.getVersion(personaId, entry.version_number);
|
|
76
|
+
if (!snapshot)
|
|
77
|
+
continue;
|
|
78
|
+
await centralStorage.saveVersion(snapshot);
|
|
79
|
+
migrated++;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Best-effort: skip failed individual versions
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return migrated;
|
|
86
|
+
}
|