vfs-example-fixture.ts 16 KB

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