@xpert-ai/plugin-live-dashboard 0.5.0 → 0.6.0

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.
Files changed (58) hide show
  1. package/.xpertai-plugin/plugin.json +1 -1
  2. package/README.md +4 -2
  3. package/dist/index.js +3 -1
  4. package/dist/lib/agent.schema.d.ts +9 -9
  5. package/dist/lib/agent.schema.js +2 -1
  6. package/dist/lib/archive.service.d.ts +26 -2
  7. package/dist/lib/archive.service.js +17 -3
  8. package/dist/lib/bounded-map.d.ts +2 -0
  9. package/dist/lib/bounded-map.js +25 -0
  10. package/dist/lib/capacity.d.ts +14 -0
  11. package/dist/lib/capacity.js +16 -0
  12. package/dist/lib/dashboard-archive.d.ts +1 -1
  13. package/dist/lib/dashboard-queries.d.ts +1 -1
  14. package/dist/lib/dashboard-queries.js +11 -6
  15. package/dist/lib/dashboard-receipt.js +1 -1
  16. package/dist/lib/dashboard-sources.js +3 -2
  17. package/dist/lib/dashboard-view.schema.d.ts +20 -20
  18. package/dist/lib/dashboard.schema.d.ts +18 -18
  19. package/dist/lib/dashboard.schema.js +2 -1
  20. package/dist/lib/dashboard.service.d.ts +24 -5
  21. package/dist/lib/dashboard.service.js +58 -8
  22. package/dist/lib/definition-tools.d.ts +4 -4
  23. package/dist/lib/document-host.js +1 -1
  24. package/dist/lib/document-package.d.ts +1 -1
  25. package/dist/lib/editing-patch.js +2 -1
  26. package/dist/lib/editing.d.ts +11 -10
  27. package/dist/lib/editing.js +5 -1
  28. package/dist/lib/filter-evidence.d.ts +28 -0
  29. package/dist/lib/filter-evidence.js +3 -2
  30. package/dist/lib/html-guidance.d.ts +1 -0
  31. package/dist/lib/html-guidance.js +2 -1
  32. package/dist/lib/html-validation.js +1 -1
  33. package/dist/lib/middleware.d.ts +3 -1
  34. package/dist/lib/middleware.js +89 -11
  35. package/dist/lib/plugin.js +2 -1
  36. package/dist/lib/presentation.provider.d.ts +1 -1
  37. package/dist/lib/preview-retry.d.ts +4 -3
  38. package/dist/lib/preview-retry.js +13 -7
  39. package/dist/lib/prompt-workflows.js +1 -1
  40. package/dist/lib/query-session.d.ts +9 -0
  41. package/dist/lib/query-session.js +64 -0
  42. package/dist/lib/save-requests.d.ts +118 -0
  43. package/dist/lib/save-requests.js +199 -0
  44. package/dist/lib/source-executions.d.ts +243 -2
  45. package/dist/lib/source-executions.js +29 -5
  46. package/dist/lib/view.provider.d.ts +38 -3
  47. package/dist/lib/view.provider.js +20 -4
  48. package/dist/remote/app.js +114 -65
  49. package/dist/remote/build-manifest.json +37 -32
  50. package/dist/sandbox-actions/action.json +1 -1
  51. package/dist/sandbox-actions/bundle/host.js +63 -14
  52. package/dist/sandbox-actions/bundle/validate.mjs +2 -2
  53. package/dist/validation/host-entry.d.ts +5 -1
  54. package/dist/validation/host-entry.js +21 -4
  55. package/docs/docs.json +2 -0
  56. package/docs/large-dashboards-acceptance.mdx +74 -0
  57. package/docs/large-dashboards.mdx +80 -0
  58. package/package.json +5 -5
@@ -1,11 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.filterTestSchema = void 0;
3
4
  exports.createFilterEvidence = createFilterEvidence;
4
5
  const tools_1 = require("@langchain/core/tools");
5
6
  const v3_1 = require("zod/v3");
6
7
  const plugin_sdk_1 = require("@xpert-ai/plugin-sdk");
7
8
  const scalar = v3_1.z.union([v3_1.z.string().max(4000), v3_1.z.number().finite(), v3_1.z.boolean(), v3_1.z.null()]);
8
- const schema = v3_1.z.object({ previewId: v3_1.z.string().min(1),
9
+ exports.filterTestSchema = v3_1.z.object({ previewId: v3_1.z.string().min(1),
9
10
  controls: (0, plugin_sdk_1.jsonEncodedSchema)(v3_1.z.record(scalar)),
10
11
  expectations: (0, plugin_sdk_1.jsonEncodedSchema)(v3_1.z.array(v3_1.z.object({ fieldId: v3_1.z.string().min(1), oneOf: v3_1.z.array(scalar).min(1).max(100) }).strict()).min(1).max(24))
11
12
  }).strict();
@@ -55,6 +56,6 @@ function createFilterEvidence(service, scope, previews) {
55
56
  checked.set(input.previewId, controls);
56
57
  return JSON.stringify({ previewId: input.previewId, passed: true, checkedControls: [...controls], remainingControls: required(binding, input.previewId).map(parameter => parameter.controlId), rows: dataset.rows.slice(0, 20), completeness: dataset.completeness });
57
58
  }, { name: 'dashboard_test_filter', description: 'Verify a previewed Binding with concrete filter values and expected field values. For a nullable dimension selector, test a real non-null value and expect every row of the matching field to equal it before saving. Does not change defaults. A failed check must be repaired before create/update.',
58
- schema, verboseParsingErrors: true, metadata: { toolName: { en_US: 'Verify filter results', zh_Hans: '验证筛选结果' } } })
59
+ schema: exports.filterTestSchema, verboseParsingErrors: true, metadata: { toolName: { en_US: 'Verify filter results', zh_Hans: '验证筛选结果' } } })
59
60
  };
60
61
  }
@@ -1 +1,2 @@
1
1
  export declare const HTML_DASHBOARD_GUIDANCE = "For free-layout HTML dashboards, read the installed live-dashboard skill's references/html-runtime.md through read_skill_file, relative to its SKILL.md path in the skill catalog. Create/edit the HTML with SandboxFile; then dashboard_create_html or dashboard_update validates and saves it. Do not use generic present_files to save or preview a live dashboard. Its preview belongs to the Live dashboards Workbench. Call dashboard_libraries for packaged ECharts/Three.js. Network access is temporarily unrestricted for functional acceptance. CDN scripts/styles/fonts, CSS imports, external images and fetch/XHR are available under browser CORS and mixed-content rules. Tailwind CDN is supported; do not remove styling to avoid network validation. Platform business data remains declarative Binding queries through window.xpertLiveArtifact; never embed credentials. The page MUST declare const live = window.xpertLiveArtifact in its script (live is not a host global), register live.subscribe(bindingId, render), await live.ready(), and invoke live.query(bindingId, typedControls) for initial data and filter changes. window.__datasets and window.__refresh are not platform APIs and are never populated by the host. Read the runtime reference and its html-lifecycle.md example before writing HTML; preserve that lifecycle while freely changing the layout. Use one complete Dataset for KPI, trend and details when they share the same metric and filters; do not create a redundant unfiltered total Binding. Never let filtered and unfiltered subscriptions both write the same KPI: a refresh would race and overwrite the selected-region total. Distinct global totals need distinct, clearly labeled elements. Native data selects require data-dashboard-control=\"<controlId>\"; purely visual selects use data-dashboard-presentation. A data filter must query, not only filter cached browser rows. bindSelect controls ONE binding only. For a shared dimension filter across multiple bindings, follow the skill custom-select example and query every affected binding with the same typed controls; matching control IDs do not broadcast automatically. bindSelect derives options from Dataset fields and is only for dimension filters. Fixed query enums (days, page size, granularity) need options from parameter.allowedValues and a typed change listener calling live.query; never use date rows as numeric day-count options.";
2
+ export declare const HTML_QUERY_ROUNDS = "For a filter shared by multiple Bindings use await live.queryMany(affectedBindingIds, typedControls); render explicit failures from the returned {datasets, failures}. Successful subscriptions are delivered together after that round settles. Individual live.query calls retain independent delivery for legacy pages.";
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HTML_DASHBOARD_GUIDANCE = void 0;
3
+ exports.HTML_QUERY_ROUNDS = exports.HTML_DASHBOARD_GUIDANCE = void 0;
4
4
  exports.HTML_DASHBOARD_GUIDANCE = `For free-layout HTML dashboards, read the installed live-dashboard skill's references/html-runtime.md through read_skill_file, relative to its SKILL.md path in the skill catalog. Create/edit the HTML with SandboxFile; then dashboard_create_html or dashboard_update validates and saves it. Do not use generic present_files to save or preview a live dashboard. Its preview belongs to the Live dashboards Workbench. Call dashboard_libraries for packaged ECharts/Three.js. Network access is temporarily unrestricted for functional acceptance. CDN scripts/styles/fonts, CSS imports, external images and fetch/XHR are available under browser CORS and mixed-content rules. Tailwind CDN is supported; do not remove styling to avoid network validation. Platform business data remains declarative Binding queries through window.xpertLiveArtifact; never embed credentials. The page MUST declare const live = window.xpertLiveArtifact in its script (live is not a host global), register live.subscribe(bindingId, render), await live.ready(), and invoke live.query(bindingId, typedControls) for initial data and filter changes. window.__datasets and window.__refresh are not platform APIs and are never populated by the host. Read the runtime reference and its html-lifecycle.md example before writing HTML; preserve that lifecycle while freely changing the layout. Use one complete Dataset for KPI, trend and details when they share the same metric and filters; do not create a redundant unfiltered total Binding. Never let filtered and unfiltered subscriptions both write the same KPI: a refresh would race and overwrite the selected-region total. Distinct global totals need distinct, clearly labeled elements. Native data selects require data-dashboard-control="<controlId>"; purely visual selects use data-dashboard-presentation. A data filter must query, not only filter cached browser rows. bindSelect controls ONE binding only. For a shared dimension filter across multiple bindings, follow the skill custom-select example and query every affected binding with the same typed controls; matching control IDs do not broadcast automatically. bindSelect derives options from Dataset fields and is only for dimension filters. Fixed query enums (days, page size, granularity) need options from parameter.allowedValues and a typed change listener calling live.query; never use date rows as numeric day-count options.`;
5
+ exports.HTML_QUERY_ROUNDS = 'For a filter shared by multiple Bindings use await live.queryMany(affectedBindingIds, typedControls); render explicit failures from the returned {datasets, failures}. Successful subscriptions are delivered together after that round settles. Individual live.query calls retain independent delivery for legacy pages.';
@@ -22,7 +22,7 @@ const artifact_document_1 = require("@xpert-ai/plugin-sdk/artifact-document");
22
22
  const html_validation_feedback_1 = require("./html-validation-feedback");
23
23
  const PLUGIN = '@xpert-ai/plugin-live-dashboard', ACTION = 'live_dashboard.validate-html', POOL = 'sandbox-browser';
24
24
  const reportSchema = v3_1.z.object({ ok: v3_1.z.boolean(), errors: v3_1.z.array(v3_1.z.string().max(300)).max(16),
25
- requestedBindings: v3_1.z.array(v3_1.z.string().max(80)).max(60), renderedTextLength: v3_1.z.number().int().nonnegative() }).strict();
25
+ requestedBindings: v3_1.z.array(v3_1.z.string().max(80)).max(64), renderedTextLength: v3_1.z.number().int().nonnegative() }).strict();
26
26
  const recordSchema = v3_1.z.object({ tenantId: v3_1.z.string(), organizationId: v3_1.z.string(), userId: v3_1.z.string(), manifestId: v3_1.z.string(), deadline: v3_1.z.number(),
27
27
  filePath: v3_1.z.string(), workspacePath: v3_1.z.string(), size: v3_1.z.number(), sha256: v3_1.z.string() }).strict();
28
28
  const digest = (buffer) => (0, node_crypto_1.createHash)('sha256').update(buffer).digest('hex');
@@ -1,10 +1,12 @@
1
+ import { DashboardSaveRequests } from './save-requests';
1
2
  import type { TAgentMiddlewareMeta } from '@xpert-ai/contracts';
2
3
  import { type AgentMiddleware, type IAgentMiddlewareStrategy, type IAgentMiddlewareContext } from '@xpert-ai/plugin-sdk';
3
4
  import { DashboardService } from './dashboard.service';
4
5
  export declare const DASHBOARD_GUIDANCE = "For a new independent dashboard, use a new unused HTML file path. An existing filename or matching title is not authorization to edit another Artifact; do not switch from creation to dashboard_open/update because a file already exists. For a new dashboard, call dashboard_list_sources before searching raw files, reading old scripts or creating transformations. Named governed sources may already provide the required query; inspect their provider contracts first. File-backed sources do not imply that a script must be written. For creating or editing live dashboards, first read the installed live-dashboard skill using read_skill_file and follow only the references relevant to the task. Use SandboxFile tools for all source-file authoring; HTML alone is not a saved dashboard. Follow preview.validation.checks immediately before authoring HTML; each suggestedCall must actually succeed. Business definitions, real data previews and nullable-filter tests are required before saving new or changed Bindings. Do not invent business data or substitute a different metric without agreement. Report completion only from a successful dashboard_create/dashboard_create_html/dashboard_update draft receipt. Human publication is separate. Successful create, update and open tools emit a chat resource card automatically; resourceCard.status reports emission. Its Open button selects the exact dashboard in Workbench. Never fabricate a link, call present_files for dashboard HTML, or output card JSON as text. If card delivery is unavailable, report the saved receipt and use the Workbench picker.";
5
6
  export declare class DashboardMiddleware implements IAgentMiddlewareStrategy {
6
7
  private readonly service;
8
+ private readonly saveRequests?;
7
9
  readonly meta: TAgentMiddlewareMeta;
8
- constructor(service: DashboardService);
10
+ constructor(service: DashboardService, saveRequests?: DashboardSaveRequests | undefined);
9
11
  createMiddleware(_options: unknown, context: IAgentMiddlewareContext): AgentMiddleware;
10
12
  }
@@ -8,8 +8,14 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
8
8
  var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
11
14
  Object.defineProperty(exports, "__esModule", { value: true });
12
15
  exports.DashboardMiddleware = exports.DASHBOARD_GUIDANCE = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const save_requests_1 = require("./save-requests");
18
+ const bounded_map_1 = require("./bounded-map");
13
19
  const runtime_context_1 = require("./runtime-context");
14
20
  const definition_tools_1 = require("./definition-tools");
15
21
  const analytics_source_1 = require("./analytics-source");
@@ -46,8 +52,9 @@ const providerOptionSchema = {
46
52
  };
47
53
  exports.DASHBOARD_GUIDANCE = `For a new independent dashboard, use a new unused HTML file path. An existing filename or matching title is not authorization to edit another Artifact; do not switch from creation to dashboard_open/update because a file already exists. For a new dashboard, call dashboard_list_sources before searching raw files, reading old scripts or creating transformations. Named governed sources may already provide the required query; inspect their provider contracts first. File-backed sources do not imply that a script must be written. For creating or editing live dashboards, first read the installed live-dashboard skill using read_skill_file and follow only the references relevant to the task. Use SandboxFile tools for all source-file authoring; HTML alone is not a saved dashboard. Follow preview.validation.checks immediately before authoring HTML; each suggestedCall must actually succeed. Business definitions, real data previews and nullable-filter tests are required before saving new or changed Bindings. Do not invent business data or substitute a different metric without agreement. Report completion only from a successful dashboard_create/dashboard_create_html/dashboard_update draft receipt. Human publication is separate. Successful create, update and open tools emit a chat resource card automatically; resourceCard.status reports emission. Its Open button selects the exact dashboard in Workbench. Never fabricate a link, call present_files for dashboard HTML, or output card JSON as text. If card delivery is unavailable, report the saved receipt and use the Workbench picker.`;
48
54
  let DashboardMiddleware = class DashboardMiddleware {
49
- constructor(service) {
55
+ constructor(service, saveRequests) {
50
56
  this.service = service;
57
+ this.saveRequests = saveRequests;
51
58
  this.meta = {
52
59
  name: 'LiveDashboard', label: { en_US: 'Live dashboards', zh_Hans: '实时看板' },
53
60
  description: { en_US: 'Discover, preview, create and update governed live dashboards.', zh_Hans: '发现业务数据、验证查询并持续修改实时看板。' },
@@ -63,6 +70,7 @@ let DashboardMiddleware = class DashboardMiddleware {
63
70
  };
64
71
  }
65
72
  createMiddleware(_options, context) {
73
+ const runId = (0, node_crypto_1.randomUUID)();
66
74
  const options = middlewareOptionsSchema.parse(_options);
67
75
  if (!context.xpertId)
68
76
  throw new Error('DASHBOARD_ASSISTANT_REQUIRED');
@@ -105,7 +113,7 @@ let DashboardMiddleware = class DashboardMiddleware {
105
113
  const controls = Object.fromEntries(binding.parameters.map((parameter) => [parameter.controlId, parameter.defaultValue ?? null]));
106
114
  const dataset = await this.service.preview(scope, { binding, controls });
107
115
  const previewId = previewKey(binding, controls);
108
- if (previews.size >= 24 && !previews.has(previewId))
116
+ if (previews.size >= 256 && !previews.has(previewId))
109
117
  throw new Error('DASHBOARD_PREVIEW_LIMIT');
110
118
  previews.set(previewId, binding);
111
119
  const rows = dataset.rows.slice(0, 20);
@@ -134,7 +142,7 @@ let DashboardMiddleware = class DashboardMiddleware {
134
142
  }
135
143
  }
136
144
  };
137
- const save = async (input, config, html, dependencies = []) => {
145
+ const save = async (input, config, html, dependencies = [], sourceFile) => {
138
146
  const { previewIds, supersedesPaths, creationIntent, ...definition } = input;
139
147
  const bindings = resolvePreviews(previewIds);
140
148
  const manifest = dashboard_schema_1.dashboardSchema.parse({ ...definition, bindings, schema: 'xpert.live-dashboard', id: (0, node_crypto_1.randomUUID)(),
@@ -154,6 +162,8 @@ let DashboardMiddleware = class DashboardMiddleware {
154
162
  const sameTitle = [...created.values()].find(item => item.manifest.title === manifest.title);
155
163
  if (sameTitle && !creationIntent)
156
164
  throw new Error(`DASHBOARD_ALREADY_CREATED: this run already saved this dashboard. Use dashboard_open with artifactId ${sameTitle.receipt.artifactId}, then dashboard_update with fresh previewIds for data changes; preserve the Artifact instead of creating another one.`);
165
+ if (this.saveRequests && bindings.length > 8)
166
+ return JSON.stringify(await this.saveRequests.submit(scope, manifest, null, 0, `${runId}/${key}`, sourceFile));
157
167
  const result = await this.service.save(scope, manifest, null);
158
168
  created.set(key, { manifest, receipt: result });
159
169
  const resourceCard = await (0, resource_card_1.emitDashboardCard)(scope, { ...result, state: 'draft', revision: result.draftRevision }, config);
@@ -165,6 +175,20 @@ let DashboardMiddleware = class DashboardMiddleware {
165
175
  throw new Error('DASHBOARD_WORKSPACE_REQUIRED');
166
176
  return files;
167
177
  });
178
+ const pendingRequests = new Map();
179
+ const activeRequests = new Set(), delivered = new Set();
180
+ const observeRequest = async (result, config) => {
181
+ if (!result.terminal)
182
+ activeRequests.add(result.requestId);
183
+ else
184
+ activeRequests.delete(result.requestId);
185
+ if (!result.dashboard || delivered.has(result.requestId))
186
+ return JSON.stringify(result);
187
+ delivered.add(result.requestId);
188
+ const resourceCard = await (0, resource_card_1.emitDashboardCard)(scope, { ...result.dashboard, state: 'draft', revision: result.dashboard.draftRevision }, config);
189
+ return JSON.stringify({ ...result, resourceCard });
190
+ };
191
+ let observing = false, waits = 0;
168
192
  const previewRetries = (0, preview_retry_1.createPreviewRetryGuard)();
169
193
  let modelCalls = 0;
170
194
  return { name: 'LiveDashboard', contextSchema: editing_1.dashboardContextSchema, stateSchema: editing_session_1.dashboardStateSchema,
@@ -173,12 +197,28 @@ let DashboardMiddleware = class DashboardMiddleware {
173
197
  wrapToolCall: async (request, handler) => {
174
198
  const name = request.toolCall.name;
175
199
  const args = request.toolCall.args;
200
+ observing = name === 'dashboard_wait_request';
201
+ if (observing && ++waits > 40)
202
+ throw new Error('DASHBOARD_WAIT_BUDGET: resume with dashboard_get_request using the existing requestId; do not resubmit.');
176
203
  const path = (0, plugin_sdk_1.workspaceFileMutationPath)(request.tool && 'metadata' in request.tool ? request.tool.metadata : undefined, args);
177
204
  const creation = name === 'dashboard_create_html' ? agent_schema_1.createHtmlToolSchema.safeParse(args)
178
205
  : name === 'dashboard_create' ? agent_schema_1.createToolSchema.safeParse(args) : undefined;
179
- const save = dashboard_receipt_1.DASHBOARD_MUTATION_TOOLS.includes(name) ? evidence.beginSave(creation?.success ? creation.data.supersedesPaths : undefined, name === 'dashboard_create' ? 'draft' : 'source') : undefined;
206
+ const priorAttempt = (name === 'dashboard_wait_request' || name === 'dashboard_get_request') && typeof args.requestId === 'string' ? pendingRequests.get(args.requestId) : undefined;
207
+ const save = priorAttempt ?? (dashboard_receipt_1.DASHBOARD_MUTATION_TOOLS.includes(name) ? evidence.beginSave(creation?.success ? creation.data.supersedesPaths : undefined, name === 'dashboard_create' ? 'draft' : 'source') : undefined);
180
208
  const result = path && (evidence.has(path) || previews.size > 0 && agent_schema_1.htmlPathSchema.safeParse(path).success)
181
209
  ? await evidence.trackWrite(path, async () => handler(request)) : await previewRetries.run(request, handler);
210
+ if (save && 'content' in result && typeof result.content === 'string') {
211
+ try {
212
+ const pending = save_requests_1.saveRequestSchema.extend({ terminal: v3_1.z.boolean() }).passthrough().parse(JSON.parse(result.content));
213
+ if (pending.terminal)
214
+ activeRequests.delete(pending.requestId);
215
+ else {
216
+ pendingRequests.set(pending.requestId, save);
217
+ activeRequests.add(pending.requestId);
218
+ }
219
+ }
220
+ catch { /* normal synchronous result */ }
221
+ }
182
222
  if (save && 'content' in result && (!('status' in result) || result.status !== 'error'))
183
223
  await evidence.accept(save, result.content);
184
224
  if ('content' in result && (!('status' in result) || result.status !== 'error')) {
@@ -202,18 +242,54 @@ let DashboardMiddleware = class DashboardMiddleware {
202
242
  // A noncompliant model must not restart the failed loop even after tools were removed.
203
243
  return result?.tool_calls?.length ? new messages_1.AIMessage(summary) : result;
204
244
  }
205
- const exhausted = ++modelCalls > options.maxModelCalls;
245
+ const exhausted = (observing ? modelCalls : ++modelCalls) > options.maxModelCalls;
206
246
  if (exhausted && evidence.pending)
207
247
  throw new Error('DASHBOARD_SAVE_NOT_CONFIRMED: no successful draft save receipt; Workspace changes have not been saved to the dashboard.');
208
- const result = await handler({ ...request, ...(evidence.pending ? { toolChoice: 'required' } : {}), ...(exhausted ? { tools: [] } : {}), systemMessage: new messages_1.SystemMessage({ content: [
248
+ const result = await handler({ ...request, ...(evidence.pending || activeRequests.size ? { toolChoice: 'required' } : {}), ...(exhausted ? { tools: [] } : {}), systemMessage: new messages_1.SystemMessage({ content: [
209
249
  ...(typeof request.systemMessage?.content === 'string' ? [{ type: 'text', text: request.systemMessage.content }] : request.systemMessage?.content ?? []),
210
- { type: 'text', text: exports.DASHBOARD_GUIDANCE + (editing.current() ? `\nCurrent editing target: ${JSON.stringify(editing.current())}. Continue this Artifact with dashboard_open/update unless the user explicitly requests a new independent dashboard.` : '') + '\n' + definition_tools_1.DEFINITION_GUIDANCE + '\n' + html_guidance_1.HTML_DASHBOARD_GUIDANCE + '\n' + editing_1.EDITING_GUIDANCE + (evidence.pending ? `\nDashboard changes are pending without a matching draft save receipt. Unsaved files: ${JSON.stringify(evidence.pendingPaths)}. Continue reading/repairing and call dashboard_create_html or dashboard_update with the exact file path; do not finish or claim it was saved. When abandoning earlier unsaved HTML creation candidates, explicitly pass their exact paths as supersedesPaths to dashboard_create or dashboard_create_html, matching the requested presentation mode. If a standard dashboard is already saved, repeat dashboard_create with the same definition/previewIds and supersedesPaths; do not create an unwanted HTML dashboard. This only abandons those candidates after a successful save; it never discards another Artifact editing copy.` : '') + (options.allowedFinalProviders ? `\nThis Assistant requires every saved dashboard binding to use one of these providers: ${options.allowedFinalProviders.join(', ')}. Other providers may be previewed only as upstream inputs. Do not save a direct-source substitute.` : '') + (exhausted ? '\nThe dashboard tool budget is exhausted. Stop calls; report only saved receipts or explain the concrete blocker truthfully.' : '') }
250
+ { type: 'text', text: `Current dashboard authoring limit: ${this.service.maxBindings} Bindings (storage maximum 64). Shared charts may reuse one Binding; independent filter-option queries count too. Preview in batches, preserving each receipt and filter check.\n` + exports.DASHBOARD_GUIDANCE + (editing.current() ? `\nCurrent editing target: ${JSON.stringify(editing.current())}. Continue this Artifact with dashboard_open/update unless the user explicitly requests a new independent dashboard.` : '') + '\n' + definition_tools_1.DEFINITION_GUIDANCE + '\n' + html_guidance_1.HTML_DASHBOARD_GUIDANCE + '\n' + html_guidance_1.HTML_QUERY_ROUNDS + '\n' + editing_1.EDITING_GUIDANCE + (activeRequests.size ? `\nDurable save requests pending: ${JSON.stringify([...activeRequests])}. Keep calling dashboard_wait_request with the returned requestId and cursor until terminal. Do not resubmit or claim saved before a dashboard receipt. On a later conversation use dashboard_get_request to recover.` : '') + (evidence.pending ? `\nDashboard changes are pending without a matching draft save receipt. Unsaved files: ${JSON.stringify(evidence.pendingPaths)}. Continue reading/repairing and call dashboard_create_html or dashboard_update with the exact file path; do not finish or claim it was saved. When abandoning earlier unsaved HTML creation candidates, explicitly pass their exact paths as supersedesPaths to dashboard_create or dashboard_create_html, matching the requested presentation mode. If a standard dashboard is already saved, repeat dashboard_create with the same definition/previewIds and supersedesPaths; do not create an unwanted HTML dashboard. This only abandons those candidates after a successful save; it never discards another Artifact editing copy.` : '') + (options.allowedFinalProviders ? `\nThis Assistant requires every saved dashboard binding to use one of these providers: ${options.allowedFinalProviders.join(', ')}. Other providers may be previewed only as upstream inputs. Do not save a direct-source substitute.` : '') + (exhausted ? '\nThe dashboard tool budget is exhausted. Stop calls; report only saved receipts or explain the concrete blocker truthfully.' : '') }
211
251
  ] }) });
212
- if (evidence.pending && result && !result.tool_calls?.length)
252
+ if ((evidence.pending || activeRequests.size) && result && !result.tool_calls?.length)
213
253
  throw new Error('DASHBOARD_SAVE_NOT_CONFIRMED: the model stopped without a successful draft save receipt.');
214
254
  return result;
215
255
  }, tools: [
256
+ ...(this.saveRequests ? [
257
+ (0, tools_1.tool)(async ({ requestId, cursor }, config) => observeRequest(await this.saveRequests.wait(scope, requestId, cursor, config.signal), config), {
258
+ name: 'dashboard_wait_request', description: 'Wait up to 20 seconds for a durable save to change. Repeat with its returned cursor while nextAction=wait. A pending request is not a saved dashboard.', schema: save_requests_1.waitSaveRequestSchema
259
+ }),
260
+ (0, tools_1.tool)(async ({ requestId }, config) => observeRequest(await this.saveRequests.get(scope, requestId), config), {
261
+ name: 'dashboard_get_request', description: 'Recover a prior durable save by its requestId. Do not submit duplicate work after reconnecting.', schema: save_requests_1.saveRequestSchema
262
+ })
263
+ ] : []),
216
264
  filterEvidence.tool,
265
+ (0, tools_1.tool)(async ({ items }) => JSON.stringify({ results: await (0, bounded_map_1.boundedMap)(items, async (item, index) => {
266
+ try {
267
+ return { index, status: 'succeeded', result: await previewRetries.attempt('dashboard_test_filter', item, () => filterEvidence.tool.invoke(item)) };
268
+ }
269
+ catch (error) {
270
+ return { index, status: 'failed', message: error instanceof Error ? error.message : 'DASHBOARD_FILTER_TEST_FAILED' };
271
+ }
272
+ }) }), { name: 'dashboard_test_filter_batch', description: 'Run up to eight required filter checks with four workers. Preserves the exact per-preview filter evidence and retry limits.',
273
+ schema: v3_1.z.object({ items: v3_1.z.array(filter_evidence_1.filterTestSchema).min(1).max(8) }).strict(), verboseParsingErrors: true }),
274
+ (0, tools_1.tool)(async ({ items }) => {
275
+ const results = await (0, bounded_map_1.boundedMap)(items, async (item, index) => {
276
+ try {
277
+ const value = await previewRetries.attempt('dashboard_preview_binding', item, () => preview(plugin_sdk_1.analyticsBindingSchema.parse((0, json_input_1.parseJsonInput)(item.bindingJson, 'bindingJson'))));
278
+ const result = JSON.parse(value), rows = result.rows.slice(0, 5);
279
+ while (Buffer.byteLength(JSON.stringify(rows)) > 8000)
280
+ rows.pop();
281
+ return { index, status: 'succeeded', preview: { previewId: result.previewId, datasetId: result.datasetId, rows,
282
+ completeness: result.completeness, freshness: result.freshness, validation: result.validation,
283
+ preview: { displayedRows: rows.length, omittedRows: result.completeness.returnedRows - rows.length, note: 'Bounded display sample. Binding definition is unchanged from this batch item.' } } };
284
+ }
285
+ catch (error) {
286
+ return { index, status: 'failed', message: error instanceof Error ? error.message : 'DASHBOARD_PREVIEW_FAILED' };
287
+ }
288
+ });
289
+ return JSON.stringify({ results, next: 'Keep every successful previewId. Complete each preview.validation.checks using dashboard_test_filter or dashboard_test_filter_batch. Repair only failed items; never invent a receipt.' });
290
+ }, { name: 'dashboard_preview_batch', description: 'Preview up to eight complete canonical Bindings with at most four concurrent queries. Returns independent receipts and diagnostics; preserves retry and filter-evidence requirements.',
291
+ schema: v3_1.z.object({ items: v3_1.z.array(v3_1.z.object({ bindingJson: v3_1.z.string().min(2).max(200000) }).strict()).min(1).max(8) }).strict(),
292
+ verboseParsingErrors: true, metadata: { toolName: { en_US: 'Verify a batch of sources', zh_Hans: '批量验证看板来源' } } }),
217
293
  ...(0, definition_tools_1.createDefinitionTools)(context, () => { listedSources = undefined; }),
218
294
  (0, tools_1.tool)(async () => JSON.stringify({ libraries: (await (0, document_libraries_1.bundledLibraries)()).map(({ source, ...lock }) => lock),
219
295
  online: { enabled: true, ...document_network_1.DASHBOARD_NETWORK_POLICY,
@@ -223,6 +299,7 @@ let DashboardMiddleware = class DashboardMiddleware {
223
299
  bindingIds: [...new Set([...previews.values()].map(binding => binding.id))],
224
300
  startup: 'const live = window.xpertLiveArtifact; live.subscribe(bindingId, renderDataset); await live.ready(); await live.query(bindingId, typedControls); Use an async function, with the exact binding IDs from saved previews.',
225
301
  refresh: 'live.refresh(bindingId) requires a Binding ID. live.refresh() without an ID is invalid. For a page refresh button or timer, call Promise.all(bindingIds.map(id => live.refresh(id))). Each subscription redraws its KPI, chart and details.',
302
+ batch: 'await live.queryMany(bindingIds, typedControls) returns {datasets, failures}; it commits successful subscriptions together after the group settles. Use for shared filters and initial multi-source rounds. Display unavailable sources from failures, never retain old values as current.',
226
303
  filters: 'Mark native data selects with data-dashboard-control="<exact controlId>". Mark purely visual selects with data-dashboard-presentation instead. Changing a data select must call live.query for every affected Binding, not only filter cached rows in JS. bindSelect updates one Binding only. Read references/html-lifecycle.md before writing the page.'
227
304
  },
228
305
  usage: { 'trend-workspace': 'window.TrendWorkspace: pale-blue KPI rail and aligned trends. Read themes/trend-workspace/README.md.',
@@ -233,7 +310,7 @@ let DashboardMiddleware = class DashboardMiddleware {
233
310
  three: 'window.THREE: bundled core including WebGLRenderer. Requires WebGL2; catch unavailable GPU contexts and show a clear fallback. External textures/models, loaders/addons, WebGPU and workers are not bundled.' },
234
311
  lifecycle: 'Pass packaged library IDs to dashboard_create_html or dashboard_update. Actual version and bytes are pinned on save; omitted libraries on update preserves saved locks. Online resources must load successfully during preflight and viewing; they are not frozen with the Artifact.' }), { name: 'dashboard_libraries', description: 'Discover packaged libraries, themes, exact versions and the current unrestricted network policy. Tailwind CDN and other online dependencies are supported. Returns metadata, never library source.',
235
312
  schema: v3_1.z.object({}).strict(), verboseParsingErrors: true, metadata: { toolName: { en_US: 'Discover dashboard libraries', zh_Hans: '查看看板前端图库' } } }),
236
- ...(0, editing_1.createEditingTools)(this.service, scope, context, resolvePreviews, validateFinal, (path, artifactId) => evidence.opened(path, artifactId), editing.resolve),
313
+ ...(0, editing_1.createEditingTools)(this.service, scope, context, resolvePreviews, validateFinal, (path, artifactId) => evidence.opened(path, artifactId), editing.resolve, this.saveRequests),
237
314
  (0, tools_1.tool)(async () => {
238
315
  listedSources = await this.service.sources(scope);
239
316
  return JSON.stringify(listedSources);
@@ -271,7 +348,7 @@ let DashboardMiddleware = class DashboardMiddleware {
271
348
  throw new Error('DASHBOARD_HTML_TOO_LARGE');
272
349
  const html = artifact_document_1.artifactDocumentHtml.parse(new TextDecoder('utf-8', { fatal: true }).decode(file.buffer));
273
350
  const { path, libraries, supersedesPaths, ...definition } = input;
274
- const result = JSON.parse(await save({ ...definition, charts: [], tables: [] }, config, html, await (0, document_libraries_1.libraryLocks)(libraries ?? [])));
351
+ const result = JSON.parse(await save({ ...definition, charts: [], tables: [] }, config, html, await (0, document_libraries_1.libraryLocks)(libraries ?? []), { path, sha256: (0, node_crypto_1.createHash)('sha256').update(file.buffer).digest('hex') }));
275
352
  return JSON.stringify({ ...result, sourceFile: { path, sha256: (0, node_crypto_1.createHash)('sha256').update(file.buffer).digest('hex') } });
276
353
  }, { name: 'dashboard_create_html', description: 'Save custom HTML from a Workspace path together with run-local preview receipts and controls in one persistent draft (human publication is separate). Requires real data preview; does not accept query payloads or cached rows.',
277
354
  schema: agent_schema_1.createHtmlToolSchema, verboseParsingErrors: true,
@@ -282,7 +359,8 @@ let DashboardMiddleware = class DashboardMiddleware {
282
359
  exports.DashboardMiddleware = DashboardMiddleware;
283
360
  exports.DashboardMiddleware = DashboardMiddleware = __decorate([
284
361
  (0, plugin_sdk_1.AgentMiddlewareStrategy)('LiveDashboard'),
285
- __metadata("design:paramtypes", [dashboard_service_1.DashboardService])
362
+ __param(1, (0, common_1.Optional)()),
363
+ __metadata("design:paramtypes", [dashboard_service_1.DashboardService, save_requests_1.DashboardSaveRequests])
286
364
  ], DashboardMiddleware);
287
365
  function previewKey(binding, controls) {
288
366
  return hash({ binding, controls: Object.fromEntries(binding.parameters.map((parameter) => [parameter.controlId, Object.hasOwn(controls, parameter.controlId) ? controls[parameter.controlId] : parameter.defaultValue ?? null])) });
@@ -7,6 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  };
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.LiveDashboardPlugin = void 0;
10
+ const save_requests_1 = require("./save-requests");
10
11
  const archive_service_1 = require("./archive.service");
11
12
  const projects_service_1 = require("./projects.service");
12
13
  const content_capability_1 = require("./content-capability");
@@ -25,5 +26,5 @@ let LiveDashboardPlugin = class LiveDashboardPlugin {
25
26
  };
26
27
  exports.LiveDashboardPlugin = LiveDashboardPlugin;
27
28
  exports.LiveDashboardPlugin = LiveDashboardPlugin = __decorate([
28
- (0, plugin_sdk_1.XpertServerPlugin)({ controllers: [provider_options_controller_1.DashboardProviderOptionsController], providers: [archive_service_1.DashboardArchiveService, projects_service_1.DashboardProjectsService, monitor_1.DashboardMonitor, monitor_1.DashboardMonitorMiddleware, artifact_type_provider_1.DashboardArtifactType, content_capability_1.DashboardCapability, content_provider_1.DashboardContentProvider, content_provider_1.DashboardContentEditor, dashboard_service_1.DashboardService, view_provider_1.DashboardViewProvider, middleware_1.DashboardMiddleware, html_validation_1.HtmlDashboardValidation, presentation_provider_1.DashboardPresentationProvider, sharing_service_1.DashboardSharingService] })
29
+ (0, plugin_sdk_1.XpertServerPlugin)({ controllers: [provider_options_controller_1.DashboardProviderOptionsController], providers: [save_requests_1.DashboardSaveRequests, archive_service_1.DashboardArchiveService, projects_service_1.DashboardProjectsService, monitor_1.DashboardMonitor, monitor_1.DashboardMonitorMiddleware, artifact_type_provider_1.DashboardArtifactType, content_capability_1.DashboardCapability, content_provider_1.DashboardContentProvider, content_provider_1.DashboardContentEditor, dashboard_service_1.DashboardService, view_provider_1.DashboardViewProvider, middleware_1.DashboardMiddleware, html_validation_1.HtmlDashboardValidation, presentation_provider_1.DashboardPresentationProvider, sharing_service_1.DashboardSharingService] })
29
30
  ], LiveDashboardPlugin);
@@ -24,8 +24,8 @@ export declare class DashboardPresentationProvider implements IArtifactPresentat
24
24
  allowedValues?: (string | number | boolean | null)[] | undefined;
25
25
  }[];
26
26
  charts: {
27
- title: string;
28
27
  type: "bar" | "line" | "area" | "pie" | "scatter";
28
+ title: string;
29
29
  id: string;
30
30
  datasetId: string;
31
31
  x: string;
@@ -1,7 +1,8 @@
1
1
  import type { AgentBuiltInState, ToolCallHandler, ToolCallRequest } from '@xpert-ai/plugin-sdk';
2
2
  /** Run-local bounds include tool-schema failures, before any discovery or query executes. */
3
3
  export declare function createPreviewRetryGuard(): {
4
- readonly exhausted: boolean;
5
- readonly summary: string;
6
- run(request: ToolCallRequest<AgentBuiltInState>, handler: ToolCallHandler): Promise<import("@langchain/core/messages").ToolMessage | import("@langchain/langgraph").Command<unknown, Record<string, unknown>, string>>;
4
+ exhausted: boolean;
5
+ summary: string;
6
+ run(request: ToolCallRequest<AgentBuiltInState>, handler: ToolCallHandler): import("@xpert-ai/plugin-sdk").PromiseOrValue<import("@langchain/core/messages").ToolMessage | import("@langchain/langgraph").Command<unknown, Record<string, unknown>, string>>;
7
+ attempt<T>(name: string, args: unknown, handler: () => Promise<T>): Promise<T>;
7
8
  };
@@ -12,13 +12,11 @@ function createPreviewRetryGuard() {
12
12
  consecutiveFailures++;
13
13
  lastError = message.slice(0, 2000);
14
14
  };
15
- return {
15
+ const guard = {
16
16
  get exhausted() { return consecutiveFailures >= 6; },
17
17
  get summary() { return `DASHBOARD_PREVIEW_RETRY_LIMIT: preview validation repeatedly failed. This run has stopped making tool calls. Report the last error and the required repair; do not claim an unconfirmed save. Last error: ${lastError}`; },
18
- async run(request, handler) {
19
- if (!PREVIEW_TOOLS.has(request.toolCall.name))
20
- return handler(request);
21
- const key = (0, node_crypto_1.createHash)('sha256').update(JSON.stringify({ name: request.toolCall.name, args: request.toolCall.args }, (_key, value) => value && typeof value === 'object' && !Array.isArray(value) ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))) : value)).digest('hex');
18
+ async attempt(name, args, handler) {
19
+ const key = (0, node_crypto_1.createHash)('sha256').update(JSON.stringify({ name, args }, (_key, value) => value && typeof value === 'object' && !Array.isArray(value) ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))) : value)).digest('hex');
22
20
  if (consecutiveFailures >= 6)
23
21
  throw new Error(this.summary);
24
22
  if ((failures.get(key)?.count ?? 0) >= 2) {
@@ -30,8 +28,8 @@ function createPreviewRetryGuard() {
30
28
  throw new Error('DASHBOARD_PREVIEW_IN_PROGRESS: wait for the identical preview already in progress; do not submit duplicates.');
31
29
  pending.add(key);
32
30
  try {
33
- const result = await handler(request);
34
- if ('status' in result && result.status === 'error' && 'content' in result) {
31
+ const result = await handler();
32
+ if (result && typeof result === 'object' && 'status' in result && result.status === 'error' && 'content' in result) {
35
33
  recordFailure(key, typeof result.content === 'string' ? result.content : 'Preview tool returned an error');
36
34
  }
37
35
  else {
@@ -49,4 +47,12 @@ function createPreviewRetryGuard() {
49
47
  }
50
48
  }
51
49
  };
50
+ return { ...guard,
51
+ get exhausted() { return guard.exhausted; },
52
+ get summary() { return guard.summary; },
53
+ run(request, handler) {
54
+ return PREVIEW_TOOLS.has(request.toolCall.name)
55
+ ? guard.attempt(request.toolCall.name, request.toolCall.args, async () => handler(request)) : handler(request);
56
+ }
57
+ };
52
58
  }
@@ -6,7 +6,7 @@ const context = `Work in the current authorized Assistant/project scope. Use the
6
6
  Resolve source/provider pairs, Artifact IDs, fields and file paths from current tools, not names or previous conversation claims. Use only real query results; no invented business data, targets, thresholds, comparison periods or activity feeds. Keep queries read-only and bounded. Report actual tool failures and diagnosticId when supplied. A missing capability or runtime binding is a blocker, not permission to bypass validation or switch presentation modes.`;
7
7
  const metrics = `Confirm metric definitions, units, time range, grain and join keys from metadata and small real samples. Compute ratios from aggregated numerators and denominators, not sums/averages of percentages. Keep unknown/zero denominators as unavailable, not zero. Check join cardinality and avoid duplicate totals. Use chronological date ordering and comparable complete periods. State freshness and gaps; historical data must not be described as current operations. Avoid exposing unnecessary personal or sensitive row-level fields.`;
8
8
  const discover = `Call dashboard_list_sources first, then dashboard_discover for exact authorized source/provider pairs. Follow returned inspectRequest/nextRequest and query contracts; do not scan unrelated schemas or guess SQL fields. Prefer existing governed definitions. If the requested source is missing, use analytics_definition_connections and analytics_definition_inspect; register a missing definition with analytics_definition_register only when needed for this requested dashboard, then refresh sources. Never duplicate an existing definition or ask the user to supply internal IDs.`;
9
- const preview = `Before saving new or changed Bindings, call dashboard_preview or dashboard_preview_binding with the exact final queries and typed default controls in this run. Execute every preview.validation.checks[].suggestedCall with dashboard_test_filter; use observed non-default values and the all-values/null case when applicable. Follow each tool's schema, serialize JSON-string fields correctly, and copy returned bindingJson rather than nesting a Binding inside SQL. Changed queries/defaults need fresh evidence. Use at most eight final Bindings, sharing a query where its metric, grain and filters permit it. Never reuse historical previewIds or hard-code preview rows into the page.`;
9
+ const preview = `Before saving new or changed Bindings, call dashboard_preview or dashboard_preview_binding with the exact final queries and typed default controls in this run. Execute every preview.validation.checks[].suggestedCall with dashboard_test_filter; use observed non-default values and the all-values/null case when applicable. Follow each tool's schema, serialize JSON-string fields correctly, and copy returned bindingJson rather than nesting a Binding inside SQL. Changed queries/defaults need fresh evidence. Respect the installation authoring limit (32 Bindings by default, at most 64), sharing a query where its metric, grain and filters permit it. Never reuse historical previewIds or hard-code preview rows into the page.`;
10
10
  const receipt = `Save a working draft and require a successful domain receipt with dashboard.artifactId, status "draft" and draftRevision. File writes, an existing resource card or an older published version are not proof of this save. Open the exact saved Artifact to inspect it; do not update an unchanged copy just to verify persistence. Verify real filter changes and refresh where available, distinguishing tool checks from unperformed viewer checks. Return the saved receipt, sources, metric definitions and a short manual acceptance checklist. Formal publication remains a separate human Workbench action.`;
11
11
  const newDashboard = `Create a NEW independent dashboard. Do not modify, rename or replace an existing Artifact, including the currently selected dashboard. Choose a distinct business title. In HTML mode choose an unused workspace-relative named .html path; never reuse an old page just because it has a similar title. Reuse verified source definitions, not an old dashboard's identity. If this run already saved the requested dashboard, keep its Artifact rather than creating a duplicate.`;
12
12
  const automaticTheme = `Choose one useful business theme from the authorized data, prioritizing an operational decision such as delivery delays, inventory backlog, supplier performance or production quality. Compare only a few plausible sources by completeness, freshness, metric clarity and actionable detail; then select the strongest one without repeatedly asking the user to choose. Explain who will use it, which decision it supports and which records require attention. If no source supports a defensible theme, report the missing data instead of making a decorative demo.`;
@@ -0,0 +1,9 @@
1
+ import type { AnalyticalDataset } from '@xpert-ai/contracts';
2
+ import { type ArtifactQueryInput, type ArtifactQueryPriority } from '@xpert-ai/plugin-sdk/artifact-document';
3
+ import type { DashboardManifest } from './dashboard.schema';
4
+ import { type SourceEvent } from './source-executions';
5
+ /** One saved definition and actor. No cache survives a navigation or a version change. */
6
+ export declare function createDashboardQuerySession(manifest: DashboardManifest, execute: (input: ArtifactQueryInput) => Promise<AnalyticalDataset>, changed: (datasets: AnalyticalDataset[], failures: string[]) => void, event: (event: SourceEvent) => void, busy: (value: boolean) => void): {
7
+ query(values: ArtifactQueryInput["controls"], priority?: ArtifactQueryPriority, only?: string[]): Promise<void>;
8
+ dispose(): void;
9
+ };
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDashboardQuerySession = createDashboardQuerySession;
4
+ const artifact_document_1 = require("@xpert-ai/plugin-sdk/artifact-document");
5
+ const source_executions_1 = require("./source-executions");
6
+ /** One saved definition and actor. No cache survives a navigation or a version change. */
7
+ function createDashboardQuerySession(manifest, execute, changed, event, busy) {
8
+ const results = new Map(), failures = new Set();
9
+ const keys = new Map(), requests = new Map(), pending = new Set();
10
+ let sequence = 0, disposed = false;
11
+ const publish = () => { if (!disposed)
12
+ changed([...results.values()], [...failures]); };
13
+ const scheduler = (0, artifact_document_1.createArtifactQueryScheduler)({ execute, onBusy: busy, onState: state => {
14
+ if (disposed || !['queued', 'running'].includes(state.state))
15
+ return;
16
+ event({ type: state.state === 'queued' ? 'queue' : 'start', bindingId: state.bindingId,
17
+ requestId: requests.get(state.bindingId), controls: state.controls, at: new Date(state.state === 'queued' ? state.queuedAt : state.startedAt).toISOString() });
18
+ } });
19
+ return {
20
+ async query(values, priority = 'interactive', only) {
21
+ const affected = manifest.bindings.flatMap(binding => {
22
+ if (only && !only.includes(binding.id))
23
+ return [];
24
+ const controls = Object.fromEntries(binding.parameters.map(parameter => [parameter.controlId,
25
+ Object.hasOwn(values, parameter.controlId) ? values[parameter.controlId] : parameter.defaultValue ?? null]));
26
+ const key = JSON.stringify(controls);
27
+ if (priority === 'interactive' && keys.get(binding.id) === key && !failures.has(binding.output.datasetId))
28
+ return [];
29
+ // A repeated refresh shares both the physical query and its Sources lifecycle.
30
+ const requestId = keys.get(binding.id) === key && pending.has(binding.id) ? requests.get(binding.id) : String(++sequence);
31
+ keys.set(binding.id, key);
32
+ pending.add(binding.id);
33
+ requests.set(binding.id, requestId);
34
+ results.delete(binding.output.datasetId);
35
+ failures.delete(binding.output.datasetId);
36
+ return [{ binding, controls, requestId }];
37
+ });
38
+ // Remove every affected old-parameter dataset before publishing any new result.
39
+ publish();
40
+ await Promise.all(affected.map(async ({ binding, controls, requestId }) => {
41
+ try {
42
+ const result = await scheduler.query({ bindingId: binding.id, controls }, priority);
43
+ if (disposed || requests.get(binding.id) !== requestId)
44
+ return;
45
+ results.set(binding.output.datasetId, result);
46
+ if (new TextEncoder().encode(JSON.stringify([...results.values()])).byteLength > 3_000_000) {
47
+ results.delete(binding.output.datasetId);
48
+ throw new Error('DASHBOARD_RESULT_TOO_LARGE');
49
+ }
50
+ event({ type: 'success', bindingId: binding.id, requestId, result: source_executions_1.sourceDatasetSchema.parse(result), at: new Date().toISOString() });
51
+ }
52
+ catch {
53
+ if (disposed || requests.get(binding.id) !== requestId)
54
+ return;
55
+ failures.add(binding.output.datasetId);
56
+ event({ type: 'failure', bindingId: binding.id, requestId, at: new Date().toISOString() });
57
+ }
58
+ pending.delete(binding.id);
59
+ publish();
60
+ }));
61
+ },
62
+ dispose() { disposed = true; scheduler.dispose(); results.clear(); requests.clear(); keys.clear(); pending.clear(); }
63
+ };
64
+ }
@@ -0,0 +1,118 @@
1
+ import { z } from 'zod/v3';
2
+ import type { XpertResolvedViewHostContext } from '@xpert-ai/contracts';
3
+ import { type ManagedQueueJob, type ManagedQueueJobContext, type ManagedQueueService, type RuntimeCapabilityRegistry } from '@xpert-ai/plugin-sdk';
4
+ import { type ArtifactDocumentDependencyAsset } from '@xpert-ai/plugin-sdk/artifact-document';
5
+ import { DashboardService } from './dashboard.service';
6
+ import { type DashboardManifest } from './dashboard.schema';
7
+ declare const sourceFileSchema: z.ZodObject<{
8
+ path: z.ZodString;
9
+ sha256: z.ZodString;
10
+ }, "strict", z.ZodTypeAny, {
11
+ path: string;
12
+ sha256: string;
13
+ }, {
14
+ path: string;
15
+ sha256: string;
16
+ }>;
17
+ export declare const saveRequestSchema: z.ZodObject<{
18
+ requestId: z.ZodString;
19
+ }, "strict", z.ZodTypeAny, {
20
+ requestId: string;
21
+ }, {
22
+ requestId: string;
23
+ }>;
24
+ export declare const waitSaveRequestSchema: z.ZodObject<{
25
+ requestId: z.ZodString;
26
+ } & {
27
+ cursor: z.ZodString;
28
+ }, "strict", z.ZodTypeAny, {
29
+ requestId: string;
30
+ cursor: string;
31
+ }, {
32
+ requestId: string;
33
+ cursor: string;
34
+ }>;
35
+ /** Durable save orchestration; domain validation and CAS remain in DashboardService. */
36
+ export declare class DashboardSaveRequests {
37
+ private readonly service;
38
+ private readonly runtime;
39
+ private readonly queue;
40
+ constructor(service: DashboardService, runtime: RuntimeCapabilityRegistry, queue: ManagedQueueService);
41
+ submit(context: XpertResolvedViewHostContext, manifest: DashboardManifest, expectedVersionId: string | null, expectedRevision: number, idempotencyKey: string, sourceFile?: z.infer<typeof sourceFileSchema>, dependencies?: ArtifactDocumentDependencyAsset[]): Promise<{
42
+ cursor: string;
43
+ nextAction: string;
44
+ error?: string | undefined;
45
+ sourceFile?: {
46
+ path: string;
47
+ sha256: string;
48
+ } | undefined;
49
+ dashboard?: {
50
+ status: "draft";
51
+ title: string;
52
+ artifactId: string;
53
+ draftRevision: number;
54
+ draftId: string;
55
+ baseVersionId: string | null;
56
+ } | undefined;
57
+ requestId: string;
58
+ state: "failed" | "running" | "succeeded" | "queued";
59
+ stage: string;
60
+ completed: number;
61
+ total: number;
62
+ terminal: boolean;
63
+ }>;
64
+ private reference;
65
+ private enqueue;
66
+ private read;
67
+ private snapshot;
68
+ get(context: XpertResolvedViewHostContext, requestId: string): Promise<{
69
+ cursor: string;
70
+ nextAction: string;
71
+ error?: string | undefined;
72
+ sourceFile?: {
73
+ path: string;
74
+ sha256: string;
75
+ } | undefined;
76
+ dashboard?: {
77
+ status: "draft";
78
+ title: string;
79
+ artifactId: string;
80
+ draftRevision: number;
81
+ draftId: string;
82
+ baseVersionId: string | null;
83
+ } | undefined;
84
+ requestId: string;
85
+ state: "failed" | "running" | "succeeded" | "queued";
86
+ stage: string;
87
+ completed: number;
88
+ total: number;
89
+ terminal: boolean;
90
+ }>;
91
+ wait(context: XpertResolvedViewHostContext, requestId: string, cursor: string, signal?: AbortSignal): Promise<{
92
+ cursor: string;
93
+ nextAction: string;
94
+ error?: string | undefined;
95
+ sourceFile?: {
96
+ path: string;
97
+ sha256: string;
98
+ } | undefined;
99
+ dashboard?: {
100
+ status: "draft";
101
+ title: string;
102
+ artifactId: string;
103
+ draftRevision: number;
104
+ draftId: string;
105
+ baseVersionId: string | null;
106
+ } | undefined;
107
+ requestId: string;
108
+ state: "failed" | "running" | "succeeded" | "queued";
109
+ stage: string;
110
+ completed: number;
111
+ total: number;
112
+ terminal: boolean;
113
+ }>;
114
+ handle(job: ManagedQueueJob<{
115
+ requestId: string;
116
+ }>, identity: ManagedQueueJobContext): Promise<void>;
117
+ }
118
+ export {};