@@ -3,43 +3,32 @@ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
33import { dirname , join , resolve } from 'node:path' ;
44import { fileURLToPath } from 'node:url' ;
55
6+ import {
7+ type DashboardSpec ,
8+ dashboardSpecSchema ,
9+ } from '../dashboard-dsl/dashboard-spec.js' ;
10+
611// File-system based cache for the dashboard agent. Resolves to
712// `<repo>/ai-server/cache/` relative to this source file so the location is
813// stable regardless of which directory `mastra dev` is launched from.
14+ //
15+ // Since the move to the dashboard DSL we cache only the parsed
16+ // `DashboardSpec`. A2UI structural ops are recompiled deterministically
17+ // from the spec by `compileDashboard` on every refresh, and the data
18+ // ops are reproduced fresh from the live data sources. The .json
19+ // extension is new on purpose so old `.a2ui.txt` files (which used a
20+ // different layout) no longer collide.
921const SOURCE_DIR = dirname ( fileURLToPath ( import . meta. url ) ) ;
1022const CACHE_DIR = resolve ( SOURCE_DIR , '../../../cache' ) ;
11- const FILE_SUFFIX = '.a2ui.txt ' ;
23+ const FILE_SUFFIX = '.dashboard.json ' ;
1224
1325interface RequestMessage {
1426 readonly role : string ;
1527 readonly content ?: unknown ;
1628}
1729
1830export interface DashboardCacheEntry {
19- /**
20- * Surface-shaping operations: `createSurface` + `updateComponents`.
21- * Re-emitted as-is on every cache hit so the renderer rebuilds the
22- * component tree.
23- */
24- structural : unknown [ ] ;
25- /**
26- * Initial `updateDataModel` operations from the original turn. Used as
27- * a template for the delta-refresh agent: it sees these paths and
28- * re-emits them with fresh values.
29- */
30- dataModel : unknown [ ] ;
31- /**
32- * The surface id used by `structural` and `dataModel`. Convenience
33- * cache; could also be derived from the operations list.
34- */
35- surfaceId : string ;
36- }
37-
38- interface MaybeOperation {
39- readonly createSurface ?: { readonly surfaceId ?: unknown } ;
40- readonly updateComponents ?: { readonly surfaceId ?: unknown } ;
41- readonly updateDataModel ?: unknown ;
42- readonly deleteSurface ?: unknown ;
31+ spec : DashboardSpec ;
4332}
4433
4534export function computeDashboardRequestHash (
@@ -82,12 +71,9 @@ export async function readDashboardCache(
8271
8372export async function writeDashboardCache (
8473 hash : string ,
85- operations : readonly unknown [ ] ,
86- ) : Promise < DashboardCacheEntry | null > {
87- const entry = splitA2uiOperations ( operations ) ;
88- if ( ! entry ) {
89- return null ;
90- }
74+ spec : DashboardSpec ,
75+ ) : Promise < DashboardCacheEntry > {
76+ const entry : DashboardCacheEntry = { spec } ;
9177 await mkdir ( CACHE_DIR , { recursive : true } ) ;
9278 await writeFile (
9379 getCacheFilePath ( hash ) ,
@@ -97,66 +83,19 @@ export async function writeDashboardCache(
9783 return entry ;
9884}
9985
100- /**
101- * Splits a fresh A2UI operations array into the structural part
102- * (`createSurface` + `updateComponents`) and the initial data-model
103- * part (`updateDataModel`). Returns `null` when the operations don't
104- * contain a `createSurface` (in which case caching makes no sense).
105- */
106- export function splitA2uiOperations (
107- operations : readonly unknown [ ] ,
108- ) : DashboardCacheEntry | null {
109- const structural : unknown [ ] = [ ] ;
110- const dataModel : unknown [ ] = [ ] ;
111- let surfaceId : string | null = null ;
112-
113- for ( const op of operations ) {
114- if ( ! op || typeof op !== 'object' ) {
115- continue ;
116- }
117- const candidate = op as MaybeOperation ;
118- if ( candidate . createSurface ) {
119- structural . push ( op ) ;
120- const id = candidate . createSurface . surfaceId ;
121- if ( typeof id === 'string' ) {
122- surfaceId = id ;
123- }
124- continue ;
125- }
126- if ( candidate . updateComponents ) {
127- structural . push ( op ) ;
128- continue ;
129- }
130- if ( candidate . updateDataModel ) {
131- dataModel . push ( op ) ;
132- continue ;
133- }
134- }
135-
136- if ( ! surfaceId || structural . length === 0 ) {
137- return null ;
138- }
139-
140- return { structural, dataModel, surfaceId } ;
141- }
142-
14386function toCacheEntry ( value : unknown ) : DashboardCacheEntry | null {
14487 if ( ! value || typeof value !== 'object' ) {
14588 return null ;
14689 }
147- const candidate = value as Partial < DashboardCacheEntry > ;
148- if (
149- ! Array . isArray ( candidate . structural ) ||
150- ! Array . isArray ( candidate . dataModel ) ||
151- typeof candidate . surfaceId !== 'string'
152- ) {
90+ const candidate = value as { spec ?: unknown } ;
91+ if ( ! candidate . spec ) {
92+ return null ;
93+ }
94+ const result = dashboardSpecSchema . safeParse ( candidate . spec ) ;
95+ if ( ! result . success ) {
15396 return null ;
15497 }
155- return {
156- structural : candidate . structural ,
157- dataModel : candidate . dataModel ,
158- surfaceId : candidate . surfaceId ,
159- } ;
98+ return { spec : result . data } ;
16099}
161100
162101function getCacheFilePath ( hash : string ) : string {
0 commit comments