vfs-example-fixture.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. /** Deterministic source for the filesystem tree bundled into the WebWorker preview. */
  2. import { fileURLToPath } from 'node:url'
  3. import {
  4. SESSION_FORMAT_VERSION,
  5. SessionId,
  6. SessionLogOffset,
  7. SessionSeq,
  8. type SessionEvent,
  9. type SessionHeader,
  10. type SessionLogOffset as SessionLogOffsetType,
  11. type SessionSeq as SessionSeqType,
  12. } from '@deepseek-ai/dsh-session'
  13. import {
  14. eventLines, generationLogFilename, projectKey, toHeaderLine,
  15. } from '@deepseek-ai/dsh-session-persistence-jsonl/src/format.ts'
  16. import { projectionCacheDomainSpec } from '@deepseek-ai/dsh-session-projection-cache'
  17. import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
  18. /** Root copied by the preview image's repository adapter. */
  19. export const VFS_EXAMPLE_ROOT = fileURLToPath(new URL('./fixtures/vfs-example', import.meta.url))
  20. /** Durable ids used by browser assertions and subagent parent links. */
  21. export const VFS_EXAMPLE_SESSION_IDS = {
  22. main: SessionId('preview-showcase'),
  23. oneShot: SessionId('preview-architecture-review'),
  24. continuable: SessionId('preview-follow-up-builder'),
  25. } as const
  26. /** Stable title rendered in the root Session list. */
  27. export const VFS_EXAMPLE_TITLE = 'WebWorker Preview Showcase'
  28. /** Oldest prompt, intentionally outside the first 50-message history page. */
  29. export const VFS_EXAMPLE_OLDEST_MESSAGE = 'History checkpoint 01: verify deterministic preview state.'
  30. /** Settled tail marker used by browser acceptance and the demonstration GIF. */
  31. export const VFS_EXAMPLE_TAIL_MESSAGE = 'Preview tour complete'
  32. const WORKSPACE = '/dsh/workspace'
  33. const CREATED_AT = 1_787_472_000_000
  34. const HISTORICAL_TURNS = 28
  35. const PREVIEW_GUIDE = `# Preview Workspace
  36. This deterministic workspace is bundled with the browser-only preview.
  37. - \`src/preview.ts\` is the file changed by the example write result.
  38. - \`data/tasks.json\` mirrors the completed preview checklist.
  39. - \`.agents/skills/preview-tour/SKILL.md\` proves dot directories survive image packing.
  40. Refresh the preview to restore these image bytes.
  41. `
  42. const PREVIEW_SOURCE_BEFORE = 'export const previewStatus = \'draft\'\n'
  43. const PREVIEW_SOURCE = `export const previewStatus = 'ready'
  44. export const previewFeatures = ['tools', 'subagents', 'pagination'] as const
  45. `
  46. const TASKS = `${JSON.stringify({
  47. title: 'Preview verification',
  48. tasks: [
  49. { name: 'Inspect tool cards', status: 'completed' },
  50. { name: 'Open both subagents', status: 'completed' },
  51. { name: 'Load earlier history', status: 'completed' },
  52. ],
  53. }, null, 2)}\n`
  54. const SKILL = `---
  55. name: preview-tour
  56. description: Inspect the bundled Preview workspace and its deterministic Session examples.
  57. ---
  58. # Preview tour
  59. Read the workspace files, inspect the tool gallery, open both subagent histories, and load the earlier conversation page.
  60. `
  61. interface EventDraft {
  62. readonly type: string
  63. readonly data: unknown
  64. readonly surfaceOp?: 'append'
  65. readonly ignorable?: true
  66. }
  67. class EventLog {
  68. readonly events: SessionEvent[]
  69. private nextTime: number
  70. constructor(time: number, seed: readonly SessionEvent[] = []) {
  71. this.events = seed.map(event => structuredClone(event))
  72. this.nextTime = Math.max(time, (this.events.at(-1)?.time ?? time - 1) + 1)
  73. }
  74. add(draft: EventDraft): SessionSeqType {
  75. const seq = SessionSeq(this.events.length)
  76. this.events.push({ ...draft, seq, time: this.nextTime++ } as unknown as SessionEvent)
  77. return seq
  78. }
  79. }
  80. function userMessage(id: string, text: string): EventDraft {
  81. return {
  82. type: 'user/message',
  83. data: {
  84. id,
  85. role: 'user',
  86. content: [{ type: 'text', text }],
  87. source: { kind: 'user' },
  88. },
  89. surfaceOp: 'append',
  90. }
  91. }
  92. function assistantStream(content: readonly unknown[]): unknown[] {
  93. const stream: unknown[] = []
  94. let toolCalls = false
  95. for (const [index, value] of content.entries()) {
  96. const block = value as Record<string, unknown>
  97. stream.push({ type: 'chunk', time: 0, chunk: { type: 'block-start', index, blockType: block.type } })
  98. if (block.type === 'text') {
  99. stream.push({ type: 'text-chunks', time0: 0, index, dt: [], texts: [block.text] })
  100. } else if (block.type === 'reasoning') {
  101. stream.push({ type: 'reasoning-chunks', time0: 0, index, dt: [], texts: [block.text] })
  102. } else if (block.type === 'tool-call') {
  103. toolCalls = true
  104. stream.push({
  105. type: 'tool-call-chunks',
  106. time0: 0,
  107. index,
  108. dt: [],
  109. id: block.id,
  110. name: block.name,
  111. args: [block.arguments],
  112. })
  113. }
  114. stream.push({ type: 'chunk', time: 0, chunk: { type: 'block-end', index, block } })
  115. }
  116. stream.push({
  117. type: 'chunk',
  118. time: 0,
  119. chunk: { type: 'finish', reason: { kind: toolCalls ? 'tool-calls' : 'stop' } },
  120. })
  121. return stream
  122. }
  123. function assistantMessage(id: string, turn: number, step: number, content: unknown[]): EventDraft {
  124. return {
  125. type: 'assistant/message',
  126. data: {
  127. turn,
  128. step,
  129. message: {
  130. id,
  131. role: 'assistant',
  132. content,
  133. source: { kind: 'model', provider: 'preview-fixture', model: 'deterministic' },
  134. },
  135. stream: assistantStream(content),
  136. },
  137. surfaceOp: 'append',
  138. }
  139. }
  140. interface GalleryCall {
  141. readonly id: string
  142. readonly name: string
  143. readonly args: Record<string, unknown>
  144. readonly result: string
  145. readonly meta?: unknown
  146. readonly error?: { readonly name: string; readonly code: string }
  147. readonly todos?: Array<{ readonly content: string; readonly status: 'pending' | 'in_progress' | 'completed' }>
  148. }
  149. function readResult(): { text: string; meta: unknown } {
  150. const lines = PREVIEW_GUIDE.trimEnd().split('\n').map((text, index) => ({ number: index + 1, text }))
  151. return {
  152. text: `<path>PREVIEW.md</path>\n<type>file</type>\n<content>\n${lines.map(line => `${String(line.number)}: ${line.text}`).join('\n')}\n\n(End of file - total ${String(lines.length)} lines)\n</content>`,
  153. meta: { path: 'PREVIEW.md', offset: 1, lines, totalLines: lines.length, lang: 'md' },
  154. }
  155. }
  156. function galleryCalls(): GalleryCall[] {
  157. const read = readResult()
  158. return [
  159. {
  160. id: 'preview-read',
  161. name: 'read',
  162. args: { file_path: 'PREVIEW.md' },
  163. result: read.text,
  164. meta: read.meta,
  165. },
  166. {
  167. id: 'preview-write',
  168. name: 'write',
  169. args: { file_path: 'src/preview.ts', content: PREVIEW_SOURCE },
  170. result: '<path>src/preview.ts</path>\n<type>file</type>\n<content>\nUpdated file\n</content>',
  171. meta: { diffs: [{ path: 'src/preview.ts', oldText: PREVIEW_SOURCE_BEFORE, newText: PREVIEW_SOURCE }] },
  172. },
  173. {
  174. id: 'preview-bash',
  175. name: 'bash',
  176. args: { command: "printf 'preview ready\\n'", description: 'Print the preview readiness marker' },
  177. result: 'preview ready\n',
  178. },
  179. {
  180. id: 'preview-glob',
  181. name: 'glob',
  182. args: { pattern: '**/*', path: '.' },
  183. result: 'PREVIEW.md\ndata/tasks.json\nsrc/preview.ts',
  184. meta: {
  185. shape: 'paths',
  186. paths: ['PREVIEW.md', 'data/tasks.json', 'src/preview.ts'],
  187. truncated: false,
  188. total: 3,
  189. },
  190. },
  191. {
  192. id: 'preview-grep',
  193. name: 'grep',
  194. args: { pattern: 'preview', path: '.', include: '*.{md,ts,json}' },
  195. result: 'PREVIEW.md:3:This deterministic workspace is bundled with the browser-only preview.\nsrc/preview.ts:1:export const previewStatus = \'ready\'',
  196. meta: {
  197. shape: 'matches',
  198. files: [
  199. { path: 'PREVIEW.md', matches: [{ lineNumber: 3, line: 'This deterministic workspace is bundled with the browser-only preview.' }] },
  200. { path: 'src/preview.ts', matches: [{ lineNumber: 1, line: "export const previewStatus = 'ready'" }] },
  201. ],
  202. truncated: false,
  203. total: 2,
  204. },
  205. },
  206. {
  207. id: 'preview-web-search',
  208. name: 'web_search',
  209. args: { queries: ['Web Worker filesystem compatibility'] },
  210. result: 'Browser workers can host deterministic in-memory filesystems.\n\nSources:\n1. MDN Web Workers API — https://developer.mozilla.org/docs/Web/API/Web_Workers_API',
  211. meta: {
  212. sources: [{
  213. url: 'https://developer.mozilla.org/docs/Web/API/Web_Workers_API',
  214. title: 'Web Workers API',
  215. snippet: 'Web Workers run scripts in background threads.',
  216. }],
  217. truncated: false,
  218. answer: 'Browser workers can host deterministic in-memory filesystems.',
  219. },
  220. },
  221. {
  222. id: 'preview-todo',
  223. name: 'todo_write',
  224. args: {
  225. todos: [
  226. { content: 'Inspect tool cards', status: 'completed' },
  227. { content: 'Open both subagents', status: 'completed' },
  228. { content: 'Load earlier history', status: 'in_progress' },
  229. ],
  230. },
  231. result: 'Updated todo list: 0 pending, 1 in progress, 2 completed.',
  232. todos: [
  233. { content: 'Inspect tool cards', status: 'completed' },
  234. { content: 'Open both subagents', status: 'completed' },
  235. { content: 'Load earlier history', status: 'in_progress' },
  236. ],
  237. },
  238. {
  239. id: 'preview-subagent',
  240. name: 'subagent',
  241. args: { description: 'Continue preview verification', prompt: 'Check the remaining preview cases.', run_in_background: true },
  242. result: `started subagent ${VFS_EXAMPLE_SESSION_IDS.continuable}`,
  243. },
  244. {
  245. id: 'preview-subagent-fork',
  246. name: 'subagent_fork',
  247. args: { description: 'Review preview architecture', prompt: 'Review the fixture architecture.', run_in_background: false },
  248. result: 'The preview fixture remains separate from user-owned WebFS data.',
  249. },
  250. {
  251. id: 'preview-failure',
  252. name: 'read',
  253. args: { file_path: 'missing.txt' },
  254. result: 'Error: ENOENT: no such file, open missing.txt',
  255. error: { name: 'FsError', code: 'ENOENT' },
  256. },
  257. ]
  258. }
  259. function addClosedTextTurn(log: EventLog, turn: number): void {
  260. const checkpoint = String(turn).padStart(2, '0')
  261. log.add({ type: 'turn/start', data: { turn } })
  262. log.add(userMessage(`preview-user-${checkpoint}`, `History checkpoint ${checkpoint}: verify deterministic preview state.`))
  263. if (turn === 1) {
  264. log.add({
  265. type: 'session/title',
  266. data: { title: VFS_EXAMPLE_TITLE, messageSeqs: [], source: { kind: 'user' } },
  267. })
  268. }
  269. log.add({ type: 'step/start', data: { turn, step: 1 } })
  270. log.add(assistantMessage(
  271. `preview-assistant-${checkpoint}`,
  272. turn,
  273. 1,
  274. [{ type: 'text', text: `Checkpoint ${checkpoint} is recorded.` }],
  275. ))
  276. log.add({ type: 'step/end', data: { turn, step: 1 } })
  277. log.add({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } })
  278. }
  279. function mainLog(): {
  280. readonly events: SessionEvent[]
  281. readonly forkSeedLength: SessionLogOffsetType
  282. } {
  283. const log = new EventLog(CREATED_AT)
  284. for (let turn = 1; turn <= HISTORICAL_TURNS; turn++) addClosedTextTurn(log, turn)
  285. const forkSeedLength = SessionLogOffset(log.events.length)
  286. const turn = HISTORICAL_TURNS + 1
  287. const calls = galleryCalls()
  288. log.add({ type: 'turn/start', data: { turn } })
  289. log.add(userMessage('preview-gallery-user', 'Show the seeded workspace, tool cards, subagents, and pagination in one tour.'))
  290. log.add({ type: 'step/start', data: { turn, step: 1 } })
  291. log.add(assistantMessage('preview-gallery-tools', turn, 1, [
  292. { type: 'reasoning', text: 'I will inspect the deterministic workspace and collect each preview surface.' },
  293. ...calls.map(call => ({ type: 'tool-call', id: call.id, name: call.name, arguments: JSON.stringify(call.args) })),
  294. ]))
  295. for (const call of calls) {
  296. log.add({
  297. type: 'tool/call',
  298. data: { turn, step: 1, callId: call.id, name: call.name, arguments: JSON.stringify(call.args) },
  299. })
  300. if (call.todos !== undefined) log.add({ type: 'todo/write', data: { todos: call.todos } })
  301. log.add({
  302. type: 'tool/result',
  303. data: {
  304. turn,
  305. step: 1,
  306. message: {
  307. id: `${call.id}-result`,
  308. role: 'user',
  309. content: [{
  310. type: 'tool-result',
  311. toolCallId: call.id,
  312. content: [{ type: 'text', text: call.result }],
  313. isError: call.error !== undefined,
  314. }],
  315. source: { kind: 'tool', callId: call.id },
  316. },
  317. ...call.meta === undefined ? {} : { meta: call.meta },
  318. ...call.error === undefined ? {} : { error: call.error },
  319. },
  320. surfaceOp: 'append',
  321. })
  322. }
  323. log.add({ type: 'step/end', data: { turn, step: 1 } })
  324. log.add({ type: 'step/start', data: { turn, step: 2 } })
  325. log.add(assistantMessage('preview-gallery-final', turn, 2, [{
  326. type: 'text',
  327. text: `## ${VFS_EXAMPLE_TAIL_MESSAGE}\n\nThe workspace, specialized tool cards, two subagent histories, and an earlier history page are ready to inspect.`,
  328. }]))
  329. log.add({ type: 'step/end', data: { turn, step: 2 } })
  330. log.add({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } })
  331. return { events: log.events, forkSeedLength }
  332. }
  333. function oneShotLog(seed: readonly SessionEvent[]): SessionEvent[] {
  334. const log = new EventLog(CREATED_AT + 100_000, seed)
  335. log.add({ type: 'session/end-seed', data: { inherited: true } })
  336. const turn = HISTORICAL_TURNS + 1
  337. log.add({ type: 'turn/start', data: { turn } })
  338. log.add(userMessage('preview-review-user', 'Review whether the preview fixture is isolated from future WebFS data.'))
  339. log.add({
  340. type: 'subagent/descriptor',
  341. data: snapshotSubagentDescriptor({
  342. mode: 'one-shot', provider: 'fork', label: 'Review preview architecture',
  343. }),
  344. })
  345. log.add({ type: 'step/start', data: { turn, step: 1 } })
  346. log.add(assistantMessage('preview-review-assistant', turn, 1, [{
  347. type: 'text',
  348. text: 'The bundled fixture is static image content; future WebFS state remains user-owned.',
  349. }]))
  350. log.add({ type: 'step/end', data: { turn, step: 1 } })
  351. log.add({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } })
  352. return log.events
  353. }
  354. function continuableLog(): SessionEvent[] {
  355. const log = new EventLog(CREATED_AT + 200_000)
  356. log.add({ type: 'turn/start', data: { turn: 1 } })
  357. log.add(userMessage('preview-builder-user', 'Check that the Preview workspace can support follow-up tasks.'))
  358. log.add({
  359. type: 'subagent/descriptor',
  360. data: snapshotSubagentDescriptor({
  361. mode: 'continuable', provider: 'spawn', label: 'Continue preview verification',
  362. }),
  363. })
  364. log.add({ type: 'step/start', data: { turn: 1, step: 1 } })
  365. log.add(assistantMessage('preview-builder-assistant', 1, 1, [{
  366. type: 'text',
  367. text: 'This child is continuable and ready for another verification turn.',
  368. }]))
  369. log.add({ type: 'step/end', data: { turn: 1, step: 1 } })
  370. log.add({ type: 'turn/end', data: { turn: 1, reason: { kind: 'completed' } } })
  371. return log.events
  372. }
  373. function header(
  374. id: SessionHeader['id'],
  375. createdAt: number,
  376. child?: {
  377. readonly parentSession: SessionHeader['id']
  378. readonly mode: 'one-shot' | 'continuable'
  379. readonly seedLength?: SessionLogOffsetType
  380. },
  381. ): { readonly meta: SessionHeader; readonly inheritedEventCount: SessionLogOffsetType } {
  382. const inheritedEventCount = child?.seedLength ?? SessionLogOffset(0)
  383. return {
  384. meta: {
  385. version: SESSION_FORMAT_VERSION,
  386. id,
  387. createdAt,
  388. cwd: WORKSPACE,
  389. isSeeded: child?.seedLength !== undefined,
  390. delegationDepth: child === undefined ? 0 : 1,
  391. agentPreset: 'standard',
  392. ...child === undefined ? {} : {
  393. parentSession: child.parentSession,
  394. origin: 'subagent' as const,
  395. },
  396. },
  397. inheritedEventCount,
  398. }
  399. }
  400. function renderLog(
  401. storage: { readonly meta: SessionHeader; readonly inheritedEventCount: SessionLogOffsetType },
  402. events: readonly SessionEvent[],
  403. ): string {
  404. return `${JSON.stringify(toHeaderLine(storage.meta, storage.inheritedEventCount))}\n${eventLines(events)}\n`
  405. }
  406. /** Build current-generation fixture files as fixture-relative UTF-8 text; committed predecessors remain untouched. */
  407. export function buildVfsExampleFiles(): ReadonlyMap<string, string> {
  408. const main = mainLog()
  409. const project = projectKey(WORKSPACE)
  410. const sessionPath = (id: string): string =>
  411. `home/sessions/${project}/${id}/${generationLogFilename(SESSION_FORMAT_VERSION, 'none')}`
  412. const projectionCache = `${JSON.stringify({
  413. unit: { name: 'session_projcache', version: projectionCacheDomainSpec.version },
  414. global: null,
  415. tables: {
  416. sessions: {
  417. [VFS_EXAMPLE_SESSION_IDS.main]: {
  418. identity: {
  419. formatVersion: SESSION_FORMAT_VERSION,
  420. createdAt: CREATED_AT,
  421. cwd: WORKSPACE,
  422. isSeeded: false,
  423. inheritedEventCount: 0,
  424. },
  425. rows: {
  426. title: { ver: 1, seq: main.events.at(-1)?.seq ?? -1, val: VFS_EXAMPLE_TITLE },
  427. },
  428. },
  429. },
  430. },
  431. }, null, 2)}\n`
  432. return new Map([
  433. ['workspace/PREVIEW.md', PREVIEW_GUIDE],
  434. ['workspace/src/preview.ts', PREVIEW_SOURCE],
  435. ['workspace/data/tasks.json', TASKS],
  436. ['workspace/.agents/skills/preview-tour/SKILL.md', SKILL],
  437. ['home/storages/session_projcache.json', projectionCache],
  438. [sessionPath(VFS_EXAMPLE_SESSION_IDS.main), renderLog(
  439. header(VFS_EXAMPLE_SESSION_IDS.main, CREATED_AT),
  440. main.events,
  441. )],
  442. [sessionPath(VFS_EXAMPLE_SESSION_IDS.oneShot), renderLog(
  443. header(VFS_EXAMPLE_SESSION_IDS.oneShot, CREATED_AT + 100_000, {
  444. parentSession: VFS_EXAMPLE_SESSION_IDS.main,
  445. mode: 'one-shot',
  446. seedLength: main.forkSeedLength,
  447. }),
  448. oneShotLog(main.events.slice(0, main.forkSeedLength)),
  449. )],
  450. [sessionPath(VFS_EXAMPLE_SESSION_IDS.continuable), renderLog(
  451. header(VFS_EXAMPLE_SESSION_IDS.continuable, CREATED_AT + 200_000, {
  452. parentSession: VFS_EXAMPLE_SESSION_IDS.main,
  453. mode: 'continuable',
  454. }),
  455. continuableLog(),
  456. )],
  457. ])
  458. }