index.ts 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. /**
  2. * Phase reminder to inject before each user message.
  3. * Keeps workflow instructions in the immediate attention window
  4. * to combat instruction-following degradation over long contexts.
  5. *
  6. * Research: "LLMs Get Lost In Multi-Turn Conversation" (arXiv:2505.06120)
  7. * shows ~40% compliance drop after 2-3 turns without reminders.
  8. *
  9. * Uses experimental.chat.messages.transform so it doesn't show in UI.
  10. */
  11. import { PHASE_REMINDER_TEXT } from '../../config/constants';
  12. import { SLIM_INTERNAL_INITIATOR_MARKER } from '../../utils';
  13. export const PHASE_REMINDER = `<reminder>${PHASE_REMINDER_TEXT}</reminder>`;
  14. interface MessageInfo {
  15. role: string;
  16. agent?: string;
  17. sessionID?: string;
  18. }
  19. interface MessagePart {
  20. type: string;
  21. text?: string;
  22. [key: string]: unknown;
  23. }
  24. interface MessageWithParts {
  25. info: MessageInfo;
  26. parts: MessagePart[];
  27. }
  28. /**
  29. * Creates the experimental.chat.messages.transform hook for phase reminder injection.
  30. * This hook runs right before sending to API, so it doesn't affect UI display.
  31. * Only injects for the orchestrator agent.
  32. */
  33. export function createPhaseReminderHook() {
  34. return {
  35. 'experimental.chat.messages.transform': async (
  36. _input: Record<string, never>,
  37. output: { messages: MessageWithParts[] },
  38. ): Promise<void> => {
  39. const { messages } = output;
  40. if (messages.length === 0) {
  41. return;
  42. }
  43. // Find the last user message
  44. let lastUserMessageIndex = -1;
  45. for (let i = messages.length - 1; i >= 0; i--) {
  46. if (messages[i].info.role === 'user') {
  47. lastUserMessageIndex = i;
  48. break;
  49. }
  50. }
  51. if (lastUserMessageIndex === -1) {
  52. return;
  53. }
  54. const lastUserMessage = messages[lastUserMessageIndex];
  55. // Only inject for orchestrator (or if no agent specified = main session)
  56. const agent = lastUserMessage.info.agent;
  57. if (agent && agent !== 'orchestrator') {
  58. return;
  59. }
  60. // Find the first text part
  61. const textPartIndex = lastUserMessage.parts.findIndex(
  62. (p) => p.type === 'text' && p.text !== undefined,
  63. );
  64. if (textPartIndex === -1) {
  65. return;
  66. }
  67. const originalText = lastUserMessage.parts[textPartIndex].text ?? '';
  68. if (originalText.includes(SLIM_INTERNAL_INITIATOR_MARKER)) {
  69. return;
  70. }
  71. // Prepend the reminder to the existing text
  72. lastUserMessage.parts[textPartIndex].text =
  73. `${PHASE_REMINDER}\n\n---\n\n${originalText}`;
  74. },
  75. };
  76. }