semantic-checkpoint.snapshot.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
  2. import { tmpdir } from 'node:os'
  3. import { dirname, join } from 'node:path'
  4. import { fileURLToPath } from 'node:url'
  5. import { Context } from 'cordis'
  6. import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
  7. import {
  8. launchAcpTestAgent,
  9. normalizeSessionLog,
  10. normalizeStdout,
  11. scrubRequestHeaders,
  12. type AgentUnderTest,
  13. type NormalizeContext,
  14. } from '@deepseek-ai/dsh-acp-snapshot'
  15. import { CallId } from '@deepseek-ai/dsh-llm'
  16. import SessionStore, { SESSION_FORMAT_VERSION, SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
  17. import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
  18. import { describe, expect, it } from 'vitest'
  19. const fixtureDir = join(dirname(fileURLToPath(import.meta.url)), 'semantic-checkpoint-snapshots/tool-outcome-unknown')
  20. const replayFixture = join(fixtureDir, 'replay.jsonl')
  21. const replayOverride = join(fixtureDir, 'replay.override.json')
  22. const stdoutExpected = join(fixtureDir, 'stdout.expected.jsonl')
  23. const sessionExpected = join(fixtureDir, 'session.expected.jsonl')
  24. const sessionId = SessionId('semantic-checkpoint-unknown-outcome')
  25. const refreshing = process.env.DSH_SNAPSHOT === 'refresh'
  26. const agent: AgentUnderTest = {
  27. binScript: fileURLToPath(new URL('../../../packages/examples/acp-demo/src/bin.ts', import.meta.url)),
  28. configPath: fileURLToPath(new URL('../cordis.yml', import.meta.url)),
  29. tsconfigPath: fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)),
  30. }
  31. async function seedInterruptedSession(root: string, cwd: string): Promise<string> {
  32. const ctx = new Context()
  33. await ctx.plugin(SessionStore)
  34. await ctx.plugin(SessionPersistenceJsonl, { root, compression: 'none' })
  35. const meta: SessionHeader = {
  36. version: SESSION_FORMAT_VERSION,
  37. id: sessionId,
  38. createdAt: 1,
  39. cwd,
  40. delegationDepth: 0,
  41. }
  42. const events: SessionEvent[] = [
  43. { type: 'turn/start', seq: 0, time: 10, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } },
  44. { type: 'user/message', seq: 1, time: 11, data: { content: [{ type: 'text', text: 'Perform one side-effecting remote mutation.' }], source: { kind: 'user' } }, surfaceOp: 'append' },
  45. { type: 'step/start', seq: 2, time: 12, data: { turn: 1, step: 1 } },
  46. {
  47. type: 'assistant/message',
  48. seq: 3,
  49. time: 13,
  50. data: {
  51. turn: 1,
  52. step: 1,
  53. content: [{ type: 'tool-call', id: CallId('unknown-outcome-call'), name: 'write_remote', arguments: '{"value":1}' }],
  54. provenance: { provider: 'deepseek', model: 'deepseek-v4-flash' },
  55. },
  56. surfaceOp: 'append',
  57. },
  58. {
  59. type: 'tool/call',
  60. seq: 4,
  61. time: 14,
  62. data: {
  63. turn: 1,
  64. step: 1,
  65. callId: CallId('unknown-outcome-call'),
  66. name: 'write_remote',
  67. arguments: '{"value":1}',
  68. },
  69. },
  70. ]
  71. try {
  72. await ctx.sessionPersistence.create(meta)
  73. await ctx.sessionPersistence.append(sessionId, events)
  74. const location = ctx.sessionPersistence.locate(meta)
  75. if (location === undefined) throw new Error('JSONL backend did not locate the seeded session')
  76. return location.path
  77. } finally {
  78. await ctx.fiber.dispose()
  79. }
  80. }
  81. describe('semantic checkpoint recovery snapshot', () => {
  82. it('loads an unknown tool outcome and carries retry-risk guidance into the next model turn', async () => {
  83. const cwd = await mkdtemp(join(tmpdir(), 'dsh-semantic-snapshot-cwd-'))
  84. const sessionsRoot = await mkdtemp(join(tmpdir(), 'dsh-semantic-snapshot-sessions-'))
  85. let launched: ReturnType<typeof launchAcpTestAgent> | undefined
  86. try {
  87. const sessionPath = await seedInterruptedSession(sessionsRoot, cwd)
  88. launched = launchAcpTestAgent({
  89. agent,
  90. cwd,
  91. env: {
  92. DSH_SNAPSHOT: 'replay',
  93. DSH_SNAPSHOT_FILE: replayFixture,
  94. DSH_SNAPSHOT_OVERRIDE: replayOverride,
  95. DSH_SNAPSHOT_SESSIONS_ROOT: sessionsRoot,
  96. },
  97. })
  98. await launched.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
  99. await launched.client.loadSession({ sessionId, cwd, mcpServers: [] })
  100. await launched.client.prompt({
  101. sessionId,
  102. prompt: [{ type: 'text', text: 'Continue safely from the interrupted operation.' }],
  103. })
  104. await launched.close()
  105. const normalization: NormalizeContext = { sessionIds: [sessionId], cwd }
  106. const stdout = normalizeStdout(launched.rawStdout(), normalization)
  107. const session = scrubRequestHeaders(normalizeSessionLog(await readFile(sessionPath, 'utf8'), normalization))
  108. if (refreshing) {
  109. await writeFile(stdoutExpected, stdout)
  110. await writeFile(sessionExpected, session)
  111. }
  112. expect(stdout).toBe(await readFile(stdoutExpected, 'utf8'))
  113. expect(session).toBe(await readFile(sessionExpected, 'utf8'))
  114. expect(session).toContain('TOOL_OUTCOME_UNKNOWN')
  115. expect(session).toContain('Do not retry blindly.')
  116. } finally {
  117. await launched?.close('SIGKILL').catch(() => undefined)
  118. await Promise.all([
  119. rm(cwd, { recursive: true, force: true }),
  120. rm(sessionsRoot, { recursive: true, force: true }),
  121. ])
  122. }
  123. })
  124. })