invariant.ts 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
  1. /**
  2. * Package-owned request-reconstruction invariant for loop-built LLM calls.
  3. * @module @deepseek-ai/dsh-agent-loop/invariant
  4. */
  5. import type { Context } from 'cordis'
  6. import { isAgentLoopRequest, type GenerateOptions } from '@deepseek-ai/dsh-llm'
  7. import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants'
  8. import { foldRequestHeader } from '@deepseek-ai/dsh-session'
  9. const PACKAGE_NAME = '@deepseek-ai/dsh-agent-loop'
  10. /** Cordis companion plugin name. */
  11. export const name = 'agent-loop-invariant'
  12. /** Service required before the companion can reserve package ownership. */
  13. export const inject = ['invariants']
  14. /** Install the request-reconstruction contribution into its child registration fiber. */
  15. const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => {
  16. // Prepend prevents a short-circuiting replay listener from silencing the check.
  17. ctx.on('llm/stream', (options: GenerateOptions, next) => {
  18. if (!isAgentLoopRequest(options)) return next()
  19. if (!Object.isFrozen(options)) fail('a loop-built request must be frozen')
  20. if (options.sessionId === undefined) fail('a loop-built request must carry a session id')
  21. const session = ctx.sessions.get(options.sessionId)
  22. if (!session) fail(`a loop-built request must carry a live session id, got "${String(options.sessionId)}"`)
  23. if (!Object.isFrozen(options.messages)) {
  24. fail('a loop-built request must carry a frozen messages array')
  25. }
  26. const events = session.events
  27. if (!events.some(event => event.type === 'step/start')) {
  28. return fail('a loop-built request with no step/start in its session log')
  29. }
  30. const header = foldRequestHeader(events)
  31. if (header === undefined) {
  32. return fail('a loop-built request with no request/header event in its session log')
  33. }
  34. const expected = session.deriveMessages()
  35. if (JSON.stringify(options.messages) !== JSON.stringify(expected)) {
  36. fail(`llm request for session "${String(session.id)}" diverges from the dispatch-time durable derivation (log-reconstruction desync)`)
  37. }
  38. const headerMatches = options.model === header.config.model
  39. && options.system === header.system
  40. && options.temperature === header.config.temperature
  41. && options.maxTokens === header.config.maxTokens
  42. && JSON.stringify(options.stop) === JSON.stringify(header.config.stop)
  43. && JSON.stringify(options.tools ?? []) === JSON.stringify(header.tools ?? [])
  44. if (!headerMatches) {
  45. fail(`llm request for session "${String(session.id)}" diverges from the folded request header`)
  46. }
  47. return next()
  48. }, { global: true, prepend: true })
  49. }, { inject: ['sessions'] })
  50. /**
  51. * Register the agent-loop invariant companion.
  52. * @param ctx - Cordis context carrying the invariant service.
  53. * @returns the installed registration's disposer after setup succeeds.
  54. */
  55. export const apply = (ctx: Context): Promise<() => void> =>
  56. Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))