session-message-updater.test.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import { expect, test } from "bun:test"
  2. import { Effect } from "effect"
  3. import * as DateTime from "effect/DateTime"
  4. import { SessionID } from "../../src/session/schema"
  5. import { EventV2 } from "@kirincode-ai/core/event"
  6. import { ModelV2 } from "@kirincode-ai/core/model"
  7. import { ProviderV2 } from "@kirincode-ai/core/provider"
  8. import { SessionEvent } from "@kirincode-ai/core/session/event"
  9. import { SessionMessageUpdater } from "@kirincode-ai/core/session/message-updater"
  10. import { SessionMessage } from "@kirincode-ai/core/session/message"
  11. test.skip("step snapshots carry over to assistant messages", () => {
  12. const state: SessionMessageUpdater.MemoryState = { messages: [] }
  13. const sessionID = SessionID.make("session")
  14. const assistantMessageID = SessionMessage.ID.create()
  15. Effect.runSync(
  16. SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
  17. id: EventV2.ID.create(),
  18. type: "session.next.step.started",
  19. data: {
  20. sessionID,
  21. assistantMessageID,
  22. timestamp: DateTime.makeUnsafe(1),
  23. agent: "build",
  24. model: {
  25. id: ModelV2.ID.make("model"),
  26. providerID: ProviderV2.ID.make("provider"),
  27. variant: ModelV2.VariantID.make("default"),
  28. },
  29. snapshot: "before",
  30. },
  31. } satisfies SessionEvent.Event),
  32. )
  33. expect(state.messages).toEqual([])
  34. Effect.runSync(
  35. SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
  36. id: EventV2.ID.create(),
  37. type: "session.next.step.ended",
  38. data: {
  39. sessionID,
  40. assistantMessageID,
  41. timestamp: DateTime.makeUnsafe(2),
  42. finish: "stop",
  43. cost: 0,
  44. tokens: {
  45. input: 1,
  46. output: 2,
  47. reasoning: 0,
  48. cache: { read: 0, write: 0 },
  49. },
  50. snapshot: "after",
  51. },
  52. } satisfies SessionEvent.Event),
  53. )
  54. expect(state.messages[0]?.type).toBe("assistant")
  55. if (state.messages[0]?.type !== "assistant") return
  56. expect(state.messages[0].snapshot).toEqual({ start: "before", end: "after" })
  57. expect(state.messages[0].finish).toBe("stop")
  58. })
  59. test.skip("text ended populates assistant text content", () => {
  60. const state: SessionMessageUpdater.MemoryState = { messages: [] }
  61. const sessionID = SessionID.make("session")
  62. const assistantMessageID = SessionMessage.ID.create()
  63. Effect.runSync(
  64. SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
  65. id: EventV2.ID.create(),
  66. type: "session.next.step.started",
  67. data: {
  68. sessionID,
  69. assistantMessageID,
  70. timestamp: DateTime.makeUnsafe(1),
  71. agent: "build",
  72. model: {
  73. id: ModelV2.ID.make("model"),
  74. providerID: ProviderV2.ID.make("provider"),
  75. variant: ModelV2.VariantID.make("default"),
  76. },
  77. },
  78. } satisfies SessionEvent.Event),
  79. )
  80. Effect.runSync(
  81. SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
  82. id: EventV2.ID.create(),
  83. type: "session.next.text.started",
  84. data: {
  85. sessionID,
  86. assistantMessageID,
  87. timestamp: DateTime.makeUnsafe(2),
  88. textID: "text-1",
  89. },
  90. } satisfies SessionEvent.Event),
  91. )
  92. Effect.runSync(
  93. SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
  94. id: EventV2.ID.create(),
  95. type: "session.next.text.ended",
  96. data: {
  97. sessionID,
  98. assistantMessageID,
  99. timestamp: DateTime.makeUnsafe(3),
  100. textID: "text-1",
  101. text: "hello assistant",
  102. },
  103. } satisfies SessionEvent.Event),
  104. )
  105. expect(state.messages[0]?.type).toBe("assistant")
  106. if (state.messages[0]?.type !== "assistant") return
  107. expect(state.messages[0].content).toEqual([{ type: "text", id: "text-1", text: "hello assistant" }])
  108. })
  109. test.skip("tool completion stores completed timestamp", () => {
  110. const state: SessionMessageUpdater.MemoryState = { messages: [] }
  111. const sessionID = SessionID.make("session")
  112. const callID = "call"
  113. const assistantMessageID = SessionMessage.ID.create()
  114. Effect.runSync(
  115. SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
  116. id: EventV2.ID.create(),
  117. type: "session.next.step.started",
  118. data: {
  119. sessionID,
  120. assistantMessageID,
  121. timestamp: DateTime.makeUnsafe(1),
  122. agent: "build",
  123. model: {
  124. id: ModelV2.ID.make("model"),
  125. providerID: ProviderV2.ID.make("provider"),
  126. variant: ModelV2.VariantID.make("default"),
  127. },
  128. },
  129. } satisfies SessionEvent.Event),
  130. )
  131. Effect.runSync(
  132. SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
  133. id: EventV2.ID.create(),
  134. type: "session.next.tool.input.started",
  135. data: {
  136. sessionID,
  137. assistantMessageID,
  138. timestamp: DateTime.makeUnsafe(2),
  139. callID,
  140. name: "bash",
  141. },
  142. } satisfies SessionEvent.Event),
  143. )
  144. Effect.runSync(
  145. SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
  146. id: EventV2.ID.create(),
  147. type: "session.next.tool.called",
  148. data: {
  149. sessionID,
  150. assistantMessageID,
  151. timestamp: DateTime.makeUnsafe(3),
  152. callID,
  153. tool: "bash",
  154. input: { command: "pwd" },
  155. provider: { executed: true, metadata: { fake: { source: "provider" } } },
  156. },
  157. } satisfies SessionEvent.Event),
  158. )
  159. Effect.runSync(
  160. SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
  161. id: EventV2.ID.create(),
  162. type: "session.next.tool.success",
  163. data: {
  164. sessionID,
  165. assistantMessageID,
  166. timestamp: DateTime.makeUnsafe(4),
  167. callID,
  168. structured: {},
  169. content: [{ type: "text", text: "/tmp" }],
  170. provider: { executed: true, metadata: { fake: { status: "done" } } },
  171. },
  172. } satisfies SessionEvent.Event),
  173. )
  174. expect(state.messages[0]?.type).toBe("assistant")
  175. if (state.messages[0]?.type !== "assistant") return
  176. expect(state.messages[0].content[0]?.type).toBe("tool")
  177. if (state.messages[0].content[0]?.type !== "tool") return
  178. expect(state.messages[0].content[0].time.completed).toEqual(DateTime.makeUnsafe(4))
  179. expect(state.messages[0].content[0].provider).toEqual({ executed: true, metadata: { fake: { status: "done" } } })
  180. })
  181. test("compaction events reduce to compaction message only when completed", () => {
  182. const state: SessionMessageUpdater.MemoryState = { messages: [] }
  183. const sessionID = SessionID.make("session")
  184. const id = EventV2.ID.create()
  185. const compactionID = SessionMessage.ID.create()
  186. Effect.runSync(
  187. SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
  188. id,
  189. type: "session.next.compaction.started",
  190. data: {
  191. sessionID,
  192. messageID: compactionID,
  193. timestamp: DateTime.makeUnsafe(1),
  194. reason: "auto",
  195. },
  196. } satisfies SessionEvent.Event),
  197. )
  198. expect(state.messages).toEqual([])
  199. Effect.runSync(
  200. SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
  201. id: EventV2.ID.create(),
  202. type: "session.next.compaction.delta",
  203. data: {
  204. sessionID,
  205. messageID: compactionID,
  206. timestamp: DateTime.makeUnsafe(2),
  207. text: "hello ",
  208. },
  209. } satisfies SessionEvent.Event),
  210. )
  211. Effect.runSync(
  212. SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
  213. id: EventV2.ID.create(),
  214. type: "session.next.compaction.delta",
  215. data: {
  216. sessionID,
  217. messageID: compactionID,
  218. timestamp: DateTime.makeUnsafe(3),
  219. text: "summary",
  220. },
  221. } satisfies SessionEvent.Event),
  222. )
  223. Effect.runSync(
  224. SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
  225. id: EventV2.ID.create(),
  226. type: "session.next.compaction.ended",
  227. data: {
  228. sessionID,
  229. messageID: compactionID,
  230. timestamp: DateTime.makeUnsafe(4),
  231. reason: "auto",
  232. text: "final summary",
  233. recent: "recent context",
  234. },
  235. } satisfies SessionEvent.Event),
  236. )
  237. expect(state.messages).toHaveLength(1)
  238. expect(state.messages[0]).toMatchObject({
  239. id: compactionID,
  240. type: "compaction",
  241. reason: "auto",
  242. summary: "final summary",
  243. recent: "recent context",
  244. time: { created: DateTime.makeUnsafe(4) },
  245. })
  246. })