invariant.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  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 { Session, SessionId, 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
  17. // check; correctness itself comes from the sequence-bounded reconstruction.
  18. ctx.on('llm/stream', (options: GenerateOptions, next) => {
  19. if (!isAgentLoopRequest(options)) return next()
  20. if (!Object.isFrozen(options)) fail('a loop-built request must be frozen')
  21. if (options.sessionId === undefined) fail('a loop-built request must carry a session id')
  22. const session = ctx.sessions.get(options.sessionId)
  23. if (!session) fail(`a loop-built request must carry a live session id, got "${String(options.sessionId)}"`)
  24. if (!Object.isFrozen(options.messages)) {
  25. fail('a loop-built request must carry a frozen messages array')
  26. }
  27. const events = session.events
  28. let boundary = -1
  29. for (let index = events.length - 1; index >= 0; index -= 1) {
  30. if (events[index]?.type === 'step/start') {
  31. boundary = index
  32. break
  33. }
  34. }
  35. if (boundary === -1) {
  36. return fail('a loop-built request with no step/start in its session log')
  37. }
  38. const header = foldRequestHeader(events)
  39. if (header === undefined) {
  40. return fail('a loop-built request with no request/header event in its session log')
  41. }
  42. const rebuilt = new Session(
  43. SessionId(`${String(session.id)}-invariant-rebuild`),
  44. structuredClone(events.slice(0, boundary)),
  45. )
  46. const expected = rebuilt.deriveMessages()
  47. if (JSON.stringify(options.messages) !== JSON.stringify(expected)) {
  48. fail(`llm request for session "${String(session.id)}" diverges from the boundary derivation (log-reconstruction desync)`)
  49. }
  50. const headerMatches = options.model === header.config.model
  51. && options.system === header.system
  52. && options.temperature === header.config.temperature
  53. && options.maxTokens === header.config.maxTokens
  54. && JSON.stringify(options.stop) === JSON.stringify(header.config.stop)
  55. && JSON.stringify(options.tools ?? []) === JSON.stringify(header.tools ?? [])
  56. if (!headerMatches) {
  57. fail(`llm request for session "${String(session.id)}" diverges from the folded request header`)
  58. }
  59. return next()
  60. }, { global: true, prepend: true })
  61. }, { inject: ['sessions'] })
  62. /**
  63. * Register the agent-loop invariant companion.
  64. * @param ctx - Cordis context carrying the invariant service.
  65. * @returns the installed registration's disposer after setup succeeds.
  66. */
  67. export const apply = (ctx: Context): Promise<() => void> =>
  68. Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))