index.ts 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. /**
  2. * Semantic durability checkpoints for model requests, top-level tool dispatch,
  3. * and completed agent steps.
  4. * @module @deepseek-ai/dsh-session-checkpoint-policy
  5. */
  6. import type { Context } from '@deepseek-ai/cordis'
  7. import type { Session } from '@deepseek-ai/dsh-session'
  8. import type { StreamChunk } from '@deepseek-ai/dsh-llm'
  9. import { TOOL_ABORTED_BEFORE_DISPATCH, type ToolExecutionResult } from '@deepseek-ai/dsh-tools'
  10. import type { PreStepDecision } from '@deepseek-ai/dsh-agent'
  11. import type {} from '@deepseek-ai/dsh-session-persistence'
  12. /** Cordis plugin name used by Loader diagnostics. */
  13. export const name = 'session-checkpoint-policy'
  14. /** Services whose request, tool, session, and persistence boundaries this policy joins. */
  15. export const inject = ['llm', 'sessionPersistence', 'sessions', 'tools']
  16. /**
  17. * Delay construction of the downstream model stream until the complete logged
  18. * request prefix is durable. A checkpoint rejection prevents adapter dispatch.
  19. *
  20. * @param ctx - plugin context that owns the session store.
  21. * @param session - live session named by the model request.
  22. * @param next - downstream `llm/stream` chain.
  23. * @returns a stream that checkpoints before requesting its first chunk.
  24. */
  25. function afterCheckpoint(
  26. ctx: Context,
  27. session: Session,
  28. next: () => AsyncIterable<StreamChunk>,
  29. ): AsyncIterable<StreamChunk> {
  30. return (async function* (): AsyncIterable<StreamChunk> {
  31. await ctx.sessions.flush(session)
  32. yield* next()
  33. })()
  34. }
  35. /** Materialize the canonical result for a call cancelled before tool dispatch. */
  36. function abortedBeforeDispatchResult(): ToolExecutionResult {
  37. return {
  38. content: [{ type: 'text', text: 'Error: tool call aborted before dispatch' }],
  39. isError: true,
  40. error: {
  41. message: 'tool call aborted before dispatch',
  42. info: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH },
  43. },
  44. }
  45. }
  46. /**
  47. * Install semantic checkpoint listeners. Loop-built model calls checkpoint the
  48. * logged request before adapter dispatch; top-level tool calls checkpoint their
  49. * recorded call before the tool body; the next request boundary checkpoints
  50. * the preceding response/result batch. Nested tool dispatches reuse the durable outer call.
  51. *
  52. * Checkpoint failures are fail-closed at the model and tool side-effect
  53. * boundaries: the downstream adapter or tool body is not invoked.
  54. *
  55. * @param ctx - plugin context that owns the listeners.
  56. */
  57. export function apply(ctx: Context): void {
  58. ctx.on('llm/stream', (options, next): AsyncIterable<StreamChunk> => {
  59. if (options.sessionId === undefined) return next()
  60. const session = ctx.sessions.get(options.sessionId)
  61. return session === undefined ? next() : afterCheckpoint(ctx, session, next)
  62. })
  63. ctx.on('tools/execute', async (exec, next): Promise<ToolExecutionResult> => {
  64. if (exec.agent === undefined || exec.parent !== undefined) return next()
  65. await ctx.sessions.flush(exec.agent.session)
  66. if (exec.signal.aborted) return abortedBeforeDispatchResult()
  67. return next()
  68. })
  69. // Before each request, persist everything committed by the preceding step;
  70. // the first step's call is an intentional no-op beyond any prompt intake.
  71. ctx.on('agent/pre-step', async ({ agent }, next): Promise<PreStepDecision> => {
  72. await ctx.sessions.flush(agent.session)
  73. return next()
  74. })
  75. }