@things-factory/board-ai 10.1.4 → 10.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/client/components/default-catalog-entries.ts +1 -1
  2. package/client/components/default-slash-templates.ts +1 -1
  3. package/client/index.ts +27 -6
  4. package/dist-client/client/components/default-catalog-entries.d.ts +1 -1
  5. package/dist-client/client/components/default-catalog-entries.js.map +1 -1
  6. package/dist-client/client/components/default-slash-templates.d.ts +1 -1
  7. package/dist-client/client/components/default-slash-templates.js.map +1 -1
  8. package/dist-client/client/index.d.ts +3 -11
  9. package/dist-client/client/index.js +25 -6
  10. package/dist-client/client/index.js.map +1 -1
  11. package/dist-client/server/service/assistant.d.ts +22 -22
  12. package/dist-client/server/service/assistant.js +32 -70
  13. package/dist-client/server/service/assistant.js.map +1 -1
  14. package/dist-client/server/service/grounding.d.ts +2 -12
  15. package/dist-client/server/service/grounding.js +15 -23
  16. package/dist-client/server/service/grounding.js.map +1 -1
  17. package/dist-client/server/service/types.d.ts +2 -4
  18. package/dist-client/server/service/types.js.map +1 -1
  19. package/dist-client/server/service/validation/board-model-schema.d.ts +270 -270
  20. package/dist-client/tsconfig.tsbuildinfo +1 -1
  21. package/dist-server/service/assistant.d.ts +22 -22
  22. package/dist-server/service/assistant.js +25 -76
  23. package/dist-server/service/assistant.js.map +1 -1
  24. package/dist-server/service/board-ai-resolver.js +41 -30
  25. package/dist-server/service/board-ai-resolver.js.map +1 -1
  26. package/dist-server/service/board-session-resolver.d.ts +12 -0
  27. package/dist-server/service/board-session-resolver.js +203 -0
  28. package/dist-server/service/board-session-resolver.js.map +1 -0
  29. package/dist-server/service/board-tool-rights.d.ts +7 -0
  30. package/dist-server/service/board-tool-rights.js +62 -0
  31. package/dist-server/service/board-tool-rights.js.map +1 -0
  32. package/dist-server/service/grounding.d.ts +2 -12
  33. package/dist-server/service/grounding.js +16 -24
  34. package/dist-server/service/grounding.js.map +1 -1
  35. package/dist-server/service/index.d.ts +4 -10
  36. package/dist-server/service/index.js +16 -23
  37. package/dist-server/service/index.js.map +1 -1
  38. package/dist-server/service/patch-entry/board-patch-subscription.js +2 -2
  39. package/dist-server/service/patch-entry/board-patch-subscription.js.map +1 -1
  40. package/dist-server/service/patch-entry/patch-entry.d.ts +1 -1
  41. package/dist-server/service/patch-entry/patch-entry.js +3 -3
  42. package/dist-server/service/patch-entry/patch-entry.js.map +1 -1
  43. package/dist-server/service/types.d.ts +2 -4
  44. package/dist-server/service/types.js.map +1 -1
  45. package/dist-server/tsconfig.tsbuildinfo +1 -1
  46. package/package.json +6 -5
  47. package/server/service/apply-patch-drift.test.ts +3 -1
  48. package/server/service/assistant.ts +47 -73
  49. package/server/service/board-ai-resolver.ts +22 -12
  50. package/server/service/board-session-resolver.ts +183 -0
  51. package/server/service/board-tool-rights.test.ts +96 -0
  52. package/server/service/board-tool-rights.ts +73 -0
  53. package/server/service/dock-contract.test.ts +3 -0
  54. package/server/service/grounding.test.ts +3 -3
  55. package/server/service/grounding.ts +15 -34
  56. package/server/service/index.ts +16 -22
  57. package/server/service/patch-entry/board-patch-subscription.ts +1 -1
  58. package/server/service/patch-entry/patch-entry.ts +3 -2
  59. package/server/service/registry-tool-loop.test.ts +1 -0
  60. package/server/service/tool-exposure.test.ts +2 -0
  61. package/server/service/types.ts +3 -4
  62. package/client/components/board-ai-chat.test.ts +0 -149
  63. package/client/components/board-ai-chat.ts +0 -3184
  64. package/client/components/chat-echo-dedup.test.ts +0 -230
  65. package/client/components/chat-echo-dedup.ts +0 -155
  66. package/client/components/chat-input-builder.ts +0 -69
  67. package/client/components/markdown.test.ts +0 -518
  68. package/client/components/markdown.ts +0 -508
  69. package/client/components/mention-popup-helpers.ts +0 -433
  70. package/client/components/mention-popup.test.ts +0 -588
  71. package/client/components/mention-popup.ts +0 -238
  72. package/client/utils/board-edit-patch.ts +0 -374
  73. package/dist-client/client/components/board-ai-chat.d.ts +0 -384
  74. package/dist-client/client/components/board-ai-chat.js +0 -3008
  75. package/dist-client/client/components/board-ai-chat.js.map +0 -1
  76. package/dist-client/client/components/board-ai-chat.test.d.ts +0 -1
  77. package/dist-client/client/components/board-ai-chat.test.js +0 -138
  78. package/dist-client/client/components/board-ai-chat.test.js.map +0 -1
  79. package/dist-client/client/components/chat-echo-dedup.d.ts +0 -54
  80. package/dist-client/client/components/chat-echo-dedup.js +0 -115
  81. package/dist-client/client/components/chat-echo-dedup.js.map +0 -1
  82. package/dist-client/client/components/chat-echo-dedup.test.d.ts +0 -1
  83. package/dist-client/client/components/chat-echo-dedup.test.js +0 -203
  84. package/dist-client/client/components/chat-echo-dedup.test.js.map +0 -1
  85. package/dist-client/client/components/chat-input-builder.d.ts +0 -55
  86. package/dist-client/client/components/chat-input-builder.js +0 -32
  87. package/dist-client/client/components/chat-input-builder.js.map +0 -1
  88. package/dist-client/client/components/markdown.d.ts +0 -60
  89. package/dist-client/client/components/markdown.js +0 -422
  90. package/dist-client/client/components/markdown.js.map +0 -1
  91. package/dist-client/client/components/markdown.test.d.ts +0 -1
  92. package/dist-client/client/components/markdown.test.js +0 -418
  93. package/dist-client/client/components/markdown.test.js.map +0 -1
  94. package/dist-client/client/components/mention-popup-helpers.d.ts +0 -165
  95. package/dist-client/client/components/mention-popup-helpers.js +0 -330
  96. package/dist-client/client/components/mention-popup-helpers.js.map +0 -1
  97. package/dist-client/client/components/mention-popup.d.ts +0 -35
  98. package/dist-client/client/components/mention-popup.js +0 -223
  99. package/dist-client/client/components/mention-popup.js.map +0 -1
  100. package/dist-client/client/components/mention-popup.test.d.ts +0 -1
  101. package/dist-client/client/components/mention-popup.test.js +0 -474
  102. package/dist-client/client/components/mention-popup.test.js.map +0 -1
  103. package/dist-client/client/utils/board-edit-patch.d.ts +0 -157
  104. package/dist-client/client/utils/board-edit-patch.js +0 -284
  105. package/dist-client/client/utils/board-edit-patch.js.map +0 -1
  106. package/dist-server/service/chat-message/chat-message-broadcast.d.ts +0 -13
  107. package/dist-server/service/chat-message/chat-message-broadcast.js +0 -13
  108. package/dist-server/service/chat-message/chat-message-broadcast.js.map +0 -1
  109. package/dist-server/service/chat-message/chat-message-publish.d.ts +0 -6
  110. package/dist-server/service/chat-message/chat-message-publish.js +0 -27
  111. package/dist-server/service/chat-message/chat-message-publish.js.map +0 -1
  112. package/dist-server/service/chat-message/chat-message-subscription.d.ts +0 -8
  113. package/dist-server/service/chat-message/chat-message-subscription.js +0 -53
  114. package/dist-server/service/chat-message/chat-message-subscription.js.map +0 -1
  115. package/dist-server/service/chat-message/chat-message.d.ts +0 -68
  116. package/dist-server/service/chat-message/chat-message.js +0 -172
  117. package/dist-server/service/chat-message/chat-message.js.map +0 -1
  118. package/dist-server/service/chat-message/fold-history.d.ts +0 -43
  119. package/dist-server/service/chat-message/fold-history.js +0 -33
  120. package/dist-server/service/chat-message/fold-history.js.map +0 -1
  121. package/dist-server/service/chat-message/history-summary.d.ts +0 -43
  122. package/dist-server/service/chat-message/history-summary.js +0 -77
  123. package/dist-server/service/chat-message/history-summary.js.map +0 -1
  124. package/dist-server/service/chat-message/index.d.ts +0 -5
  125. package/dist-server/service/chat-message/index.js +0 -12
  126. package/dist-server/service/chat-message/index.js.map +0 -1
  127. package/dist-server/service/chat-message/llm-history.d.ts +0 -46
  128. package/dist-server/service/chat-message/llm-history.js +0 -87
  129. package/dist-server/service/chat-message/llm-history.js.map +0 -1
  130. package/dist-server/service/chat-session/chat-session.d.ts +0 -62
  131. package/dist-server/service/chat-session/chat-session.js +0 -164
  132. package/dist-server/service/chat-session/chat-session.js.map +0 -1
  133. package/dist-server/service/chat-session/index.d.ts +0 -3
  134. package/dist-server/service/chat-session/index.js +0 -7
  135. package/dist-server/service/chat-session/index.js.map +0 -1
  136. package/dist-server/service/chat-session/session-activity-broadcast.d.ts +0 -30
  137. package/dist-server/service/chat-session/session-activity-broadcast.js +0 -34
  138. package/dist-server/service/chat-session/session-activity-broadcast.js.map +0 -1
  139. package/dist-server/service/chat-session/session-activity-publish.d.ts +0 -12
  140. package/dist-server/service/chat-session/session-activity-publish.js +0 -37
  141. package/dist-server/service/chat-session/session-activity-publish.js.map +0 -1
  142. package/dist-server/service/chat-session/session-activity-subscription.d.ts +0 -14
  143. package/dist-server/service/chat-session/session-activity-subscription.js +0 -87
  144. package/dist-server/service/chat-session/session-activity-subscription.js.map +0 -1
  145. package/dist-server/service/chat-session/session-anchor.d.ts +0 -12
  146. package/dist-server/service/chat-session/session-anchor.js +0 -20
  147. package/dist-server/service/chat-session/session-anchor.js.map +0 -1
  148. package/dist-server/service/chat-session/session-inbox.d.ts +0 -80
  149. package/dist-server/service/chat-session/session-inbox.js +0 -133
  150. package/dist-server/service/chat-session/session-inbox.js.map +0 -1
  151. package/dist-server/service/chat-session-participant/chat-session-participant.d.ts +0 -49
  152. package/dist-server/service/chat-session-participant/chat-session-participant.js +0 -141
  153. package/dist-server/service/chat-session-participant/chat-session-participant.js.map +0 -1
  154. package/dist-server/service/chat-session-participant/index.d.ts +0 -4
  155. package/dist-server/service/chat-session-participant/index.js +0 -10
  156. package/dist-server/service/chat-session-participant/index.js.map +0 -1
  157. package/dist-server/service/chat-session-participant/invite-notification.d.ts +0 -27
  158. package/dist-server/service/chat-session-participant/invite-notification.js +0 -30
  159. package/dist-server/service/chat-session-participant/invite-notification.js.map +0 -1
  160. package/dist-server/service/chat-session-participant/presence.d.ts +0 -4
  161. package/dist-server/service/chat-session-participant/presence.js +0 -22
  162. package/dist-server/service/chat-session-participant/presence.js.map +0 -1
  163. package/dist-server/service/chat-session-resolver.d.ts +0 -102
  164. package/dist-server/service/chat-session-resolver.js +0 -856
  165. package/dist-server/service/chat-session-resolver.js.map +0 -1
  166. package/dist-server/service/subscribe-auth.d.ts +0 -14
  167. package/dist-server/service/subscribe-auth.js +0 -33
  168. package/dist-server/service/subscribe-auth.js.map +0 -1
  169. package/server/service/chat-message/chat-message-broadcast.integration.test.ts +0 -73
  170. package/server/service/chat-message/chat-message-broadcast.test.ts +0 -40
  171. package/server/service/chat-message/chat-message-broadcast.ts +0 -27
  172. package/server/service/chat-message/chat-message-publish.ts +0 -22
  173. package/server/service/chat-message/chat-message-subscription.ts +0 -41
  174. package/server/service/chat-message/chat-message.ts +0 -195
  175. package/server/service/chat-message/fold-history.test.ts +0 -98
  176. package/server/service/chat-message/fold-history.ts +0 -77
  177. package/server/service/chat-message/history-summary.test.ts +0 -127
  178. package/server/service/chat-message/history-summary.ts +0 -100
  179. package/server/service/chat-message/index.ts +0 -12
  180. package/server/service/chat-message/llm-history.test.ts +0 -194
  181. package/server/service/chat-message/llm-history.ts +0 -163
  182. package/server/service/chat-session/chat-session.ts +0 -182
  183. package/server/service/chat-session/index.ts +0 -5
  184. package/server/service/chat-session/session-activity-broadcast.ts +0 -57
  185. package/server/service/chat-session/session-activity-publish.ts +0 -44
  186. package/server/service/chat-session/session-activity-subscription.ts +0 -67
  187. package/server/service/chat-session/session-anchor.ts +0 -35
  188. package/server/service/chat-session/session-inbox.test.ts +0 -185
  189. package/server/service/chat-session/session-inbox.ts +0 -161
  190. package/server/service/chat-session-participant/chat-session-participant.ts +0 -135
  191. package/server/service/chat-session-participant/index.ts +0 -6
  192. package/server/service/chat-session-participant/invite-notification.test.ts +0 -30
  193. package/server/service/chat-session-participant/invite-notification.ts +0 -48
  194. package/server/service/chat-session-participant/presence.test.ts +0 -39
  195. package/server/service/chat-session-participant/presence.ts +0 -21
  196. package/server/service/chat-session-resolver.ts +0 -779
  197. package/server/service/session-anchor.test.ts +0 -65
  198. package/server/service/subscribe-auth.test.ts +0 -57
  199. package/server/service/subscribe-auth.ts +0 -34
@@ -1,98 +0,0 @@
1
- /*
2
- * 요약 접기 오케스트레이션 검증 — **"배선했지만 한 번도 안 돌아본" 코드를 만들지 않기 위해.**
3
- *
4
- * 이 경로는 51번째 메시지부터 처음 돌기 때문에 실제 대화로 확인하려면 오래 걸린다. 그래서 요약
5
- * 생성기(LLM)만 대역으로 끼우고 **판정·조립·저장 값**을 여기서 고정한다.
6
- *
7
- * 무엇을 지키는가:
8
- * ① 밀려난 것이 없으면 부르지 않는다(비용).
9
- * ② 임계치 미만이면 부르지 않는다(매 턴 호출 방지).
10
- * ③ 이전 요약을 이어 넘긴다(누적 — 전체 재요약 금지).
11
- * ④ 저장 값은 본문 + 어디까지 접었는지, 항상 함께.
12
- * ⑤ 생성 실패·빈 응답은 삼키되 **이유를 남긴다**(조용한 무동작 금지).
13
- */
14
- import { foldHistorySummary } from './fold-history'
15
-
16
- const rows = (n: number) =>
17
- Array.from({ length: n }, (_, i) => ({
18
- id: `m${i}`,
19
- role: i % 2 ? 'assistant' : 'user',
20
- content: `말 ${i}`
21
- }))
22
-
23
- /** 호출 여부·입력을 기록하는 요약 생성기 대역. */
24
- function recorder(reply: string | undefined | null = '요약 결과') {
25
- const calls: string[] = []
26
- return {
27
- calls,
28
- summarize: async (content: string) => {
29
- calls.push(content)
30
- return reply
31
- }
32
- }
33
- }
34
-
35
- describe('foldHistorySummary', () => {
36
- it('상한 안이면 요약기를 부르지 않는다 — 비용을 쓰지 않는다', async () => {
37
- const rec = recorder()
38
- const r = await foldHistorySummary({ rows: rows(10), maxTurns: 40, threshold: 10, summarize: rec.summarize })
39
- expect(r.patch).toBeNull()
40
- expect(r.skipped).toBe('nothing-dropped')
41
- expect(rec.calls).toHaveLength(0)
42
- })
43
-
44
- it('밀려났지만 임계치 미만이면 부르지 않는다 — 상한 넘긴 뒤 매 턴 호출 방지', async () => {
45
- const rec = recorder()
46
- /* 45개 중 40개 유지 → 5개 밀려남(임계치 10 미만) */
47
- const r = await foldHistorySummary({ rows: rows(45), maxTurns: 40, threshold: 10, summarize: rec.summarize })
48
- expect(r.skipped).toBe('below-threshold')
49
- expect(rec.calls).toHaveLength(0)
50
- })
51
-
52
- it('임계치를 넘으면 요약을 만들고 저장 값을 돌려준다', async () => {
53
- const rec = recorder('· dock-1 포화 확인\n· 지게차 1대 추가로 합의')
54
- const r = await foldHistorySummary({ rows: rows(52), maxTurns: 40, threshold: 10, summarize: rec.summarize })
55
- expect(rec.calls).toHaveLength(1)
56
- expect(r.patch).toEqual({
57
- lastSummary: '· dock-1 포화 확인\n· 지게차 1대 추가로 합의',
58
- /* 밀려난 것은 m0..m11 → 마지막으로 접은 것이 m11 */
59
- summaryUpToMessageId: 'm11'
60
- })
61
- })
62
-
63
- it('이전 요약을 이어 넘긴다 — 원본 전체를 다시 읽지 않는다(누적)', async () => {
64
- const rec = recorder()
65
- await foldHistorySummary({
66
- rows: rows(60),
67
- stored: { text: '앞에서 정한 것: 지게차 1대 추가', upToMessageId: 'm5' },
68
- maxTurns: 40,
69
- threshold: 10,
70
- summarize: rec.summarize
71
- })
72
- const sent = rec.calls[0]
73
- expect(sent).toContain('앞에서 정한 것: 지게차 1대 추가')
74
- /* 이미 접은 m0..m5 는 다시 넘기지 않는다 */
75
- expect(sent).not.toContain('말 0')
76
- expect(sent).toContain('말 6')
77
- })
78
-
79
- it('빈 요약은 저장하지 않는다 — 이유를 남긴다', async () => {
80
- const rec = recorder(' ')
81
- const r = await foldHistorySummary({ rows: rows(52), maxTurns: 40, threshold: 10, summarize: rec.summarize })
82
- expect(r.patch).toBeNull()
83
- expect(r.skipped).toBe('empty-summary')
84
- })
85
-
86
- it('요약기가 실패해도 대화를 막지 않는다 — 이유를 남기고 넘어간다', async () => {
87
- const r = await foldHistorySummary({
88
- rows: rows(52),
89
- maxTurns: 40,
90
- threshold: 10,
91
- summarize: async () => {
92
- throw new Error('provider down')
93
- }
94
- })
95
- expect(r.patch).toBeNull()
96
- expect(r.skipped).toBe('failed')
97
- })
98
- })
@@ -1,77 +0,0 @@
1
- /*
2
- * 요약 접기 오케스트레이션 — **판정과 조립은 여기, DB 는 호출부**.
3
- *
4
- * 왜 분리하는가: 요약을 만드는 결정(무엇을·언제·무엇에 이어)은 규칙이고, 규칙은 테스트로 고정할 수
5
- * 있어야 한다. 리졸버 안에 두면 DB 없이 검증할 수 없어 "배선했지만 한 번도 안 돌아본" 코드가 된다.
6
- * 요약 생성기(LLM)와 이력·저장은 호출부가 주입한다.
7
- */
8
- import type { HistoryRow } from './llm-history.js'
9
- import {
10
- splitByCap,
11
- pendingForSummary,
12
- shouldResummarize,
13
- summaryUserContent,
14
- summaryPatch,
15
- type StoredSummary
16
- } from './history-summary.js'
17
-
18
- export interface FoldHistoryInput {
19
- /** 시간 오름차순 이력. 기본은 **전체**이고, `rowsStartAfterSummary` 면 요약 뒤부터의 구간이다. */
20
- rows: HistoryRow[]
21
- /** 세션에 저장된 요약 상태. */
22
- stored?: StoredSummary
23
- /**
24
- * `rows` 가 **이미 요약 뒤부터** 잘려 온 구간인가.
25
- *
26
- * ── 왜 이 플래그가 필요한가 ─────────────────────────────────────────────────
27
- * 호출부가 DB 에서 워터마크(`stored.upToMessageId`) 뒤부터만 읽으면(대화가 길어져도 상수 시간),
28
- * 여기서 `pendingForSummary` 로 **또 자를 것이 없다.** 그런데 그 함수는 「id 를 못 찾으면 전부
29
- * 미반영으로 본다」는 안전 규칙을 갖고 있어서, 창으로 읽은 경우에도 우연히 맞는 답을 낸다.
30
- *
31
- * 우연에 기대지 않는다. 그 규칙은 **행이 지워졌을 때의 안전망**이고, 여기서 필요한 것은 「이미
32
- * 잘랐다」는 사실이다. 둘을 같은 분기로 두면, 나중에 그 안전망을 고치는 사람이 이 경로를 조용히
33
- * 망가뜨린다(요약이 앞부분을 두 번 담거나 빠뜨린다).
34
- */
35
- rowsStartAfterSummary?: boolean
36
- /** 프롬프트 상한(이 밖으로 밀려난 것이 요약 대상). */
37
- maxTurns: number
38
- /** 새로 밀려난 것이 이 수 이상일 때만 요약을 만든다(매 턴 호출 방지). */
39
- threshold: number
40
- /** 요약 생성 — 실패하면 throw 하거나 빈 문자열을 돌려주면 된다(둘 다 "접지 않음" 으로 처리). */
41
- summarize: (content: string) => Promise<string | undefined | null>
42
- }
43
-
44
- export interface FoldHistoryResult {
45
- /** 세션에 저장할 값. null 이면 이번엔 접지 않는다(대상 없음·임계치 미만·생성 실패). */
46
- patch: { lastSummary: string; summaryUpToMessageId?: string } | null
47
- /** 접지 않은 이유 — 관측·테스트용(조용한 무동작을 남기지 않는다). */
48
- skipped?: 'nothing-dropped' | 'below-threshold' | 'empty-summary' | 'failed'
49
- }
50
-
51
- /**
52
- * 접을지 판정하고, 접어야 하면 요약을 만들어 저장 값을 돌려준다.
53
- *
54
- * 실패를 삼키되 **왜 안 접었는지**는 남긴다 — 조용한 무동작은 나중에 "요약이 왜 안 생기지" 를
55
- * 추적 불가능하게 만든다.
56
- */
57
- export async function foldHistorySummary(input: FoldHistoryInput): Promise<FoldHistoryResult> {
58
- const { dropped } = splitByCap(input.rows, input.maxTurns)
59
- if (dropped.length === 0) return { patch: null, skipped: 'nothing-dropped' }
60
-
61
- /*
62
- * 창으로 읽어 왔으면 밀려난 것이 곧 미반영분이다 — 요약이 덮은 앞부분은 애초에 실려 오지 않았다.
63
- * 전체를 읽어 왔을 때만 워터마크로 자른다.
64
- */
65
- const pending = input.rowsStartAfterSummary ? dropped : pendingForSummary(dropped, input.stored)
66
- if (!shouldResummarize(pending, input.threshold)) return { patch: null, skipped: 'below-threshold' }
67
-
68
- let text: string | undefined | null
69
- try {
70
- text = await input.summarize(summaryUserContent(input.stored?.text, pending))
71
- } catch {
72
- return { patch: null, skipped: 'failed' }
73
- }
74
- if (!text || !text.trim()) return { patch: null, skipped: 'empty-summary' }
75
-
76
- return { patch: summaryPatch(text, pending) }
77
- }
@@ -1,127 +0,0 @@
1
- /*
2
- * 이력 요약 접기 규칙 검증.
3
- *
4
- * 무엇을 지키는가:
5
- * ① **누적** — 이전 요약 뒤부터만 새로 접는다(전체 재요약=비용, 중복 접기=왜곡).
6
- * ② **아껴 만든다** — 새로 밀려난 것이 임계치를 넘을 때만. 상한 넘긴 뒤 매 턴 부르면 호출이 두 배.
7
- * ③ **덮은 범위를 함께 저장** — 본문과 upToMessageId 는 항상 같이 간다(따로 저장하면 어긋난다).
8
- * ④ **모르면 다시 요약** — 기록된 id 를 이력에서 못 찾으면 전부 미반영으로 본다(맥락 상실 방지).
9
- * ⑤ **지시는 사실 위주** — 결정·미결·식별자 보존을 요구하고 창작을 금지한다.
10
- */
11
- import {
12
- splitByCap,
13
- pendingForSummary,
14
- shouldResummarize,
15
- summarySourceText,
16
- summaryUserContent,
17
- summaryInstruction,
18
- summaryPatch
19
- } from './history-summary'
20
-
21
- const rows = (n: number, from = 0) =>
22
- Array.from({ length: n }, (_, i) => ({
23
- id: `m${from + i}`,
24
- role: (from + i) % 2 ? 'assistant' : 'user',
25
- content: `말 ${from + i}`
26
- }))
27
-
28
- describe('splitByCap', () => {
29
- it('상한 이하면 아무것도 밀려나지 않는다', () => {
30
- const r = splitByCap(rows(5), 10)
31
- expect(r.dropped).toEqual([])
32
- expect(r.kept).toHaveLength(5)
33
- })
34
-
35
- it('상한을 넘으면 앞부분이 밀려나고 최근이 남는다', () => {
36
- const r = splitByCap(rows(12), 4)
37
- expect(r.dropped).toHaveLength(8)
38
- expect(r.kept.map(x => x.id)).toEqual(['m8', 'm9', 'm10', 'm11'])
39
- })
40
- })
41
-
42
- describe('pendingForSummary — 누적', () => {
43
- const dropped = rows(8)
44
-
45
- it('요약이 없으면 밀려난 전부가 대상', () => {
46
- expect(pendingForSummary(dropped, undefined)).toHaveLength(8)
47
- })
48
-
49
- it('요약이 덮은 지점 뒤부터만 대상 — 이미 접은 것을 또 접지 않는다', () => {
50
- const p = pendingForSummary(dropped, { text: '앞부분 요약', upToMessageId: 'm4' })
51
- expect(p.map(x => x.id)).toEqual(['m5', 'm6', 'm7'])
52
- })
53
-
54
- it('기록된 id 를 못 찾으면 전부 다시 — 빠뜨리는 쪽보다 안전하다', () => {
55
- const p = pendingForSummary(dropped, { text: '요약', upToMessageId: 'deleted' })
56
- expect(p).toHaveLength(8)
57
- })
58
-
59
- it('본문 없는 기록은 요약이 없는 것으로 본다', () => {
60
- expect(pendingForSummary(dropped, { text: '', upToMessageId: 'm4' })).toHaveLength(8)
61
- })
62
- })
63
-
64
- describe('shouldResummarize — 아껴 만들기', () => {
65
- it('임계치를 넘으면 만든다', () => {
66
- expect(shouldResummarize(rows(10), 10)).toBe(true)
67
- })
68
- it('임계치 미만이면 만들지 않는다 — 매 턴 호출 방지', () => {
69
- expect(shouldResummarize(rows(9), 10)).toBe(false)
70
- })
71
- it('밀려난 것이 없으면 만들지 않는다', () => {
72
- expect(shouldResummarize([], 10)).toBe(false)
73
- })
74
- })
75
-
76
- describe('요약 입력', () => {
77
- it('발신자 표기를 유지한다 — 협의에서 누가 말했는지가 핵심 정보', () => {
78
- const text = summarySourceText([
79
- { id: 'a', role: 'user', content: '도크 포화 확인 필요', senderName: '김철수' },
80
- { id: 'b', role: 'assistant', content: 'dock-1 점유 92%' },
81
- { id: 'c', role: 'system', content: '실행됨: o7 보류' }
82
- ])
83
- expect(text).toContain('김철수: 도크 포화 확인 필요')
84
- expect(text).toContain('AI: dock-1 점유 92%')
85
- expect(text).toContain('SYSTEM: 실행됨: o7 보류')
86
- })
87
-
88
- it('이전 요약을 앞에 얹어 누적 입력을 만든다', () => {
89
- const content = summaryUserContent('앞에서 정한 것: 지게차 1대 추가', rows(2))
90
- expect(content).toContain('SUMMARY SO FAR')
91
- expect(content).toContain('지게차 1대 추가')
92
- expect(content).toContain('NEW MESSAGES TO FOLD IN')
93
- })
94
-
95
- it('이전 요약이 없으면 새 대화만 넣는다', () => {
96
- expect(summaryUserContent(null, rows(2))).not.toContain('SUMMARY SO FAR')
97
- })
98
- })
99
-
100
- describe('요약 지시', () => {
101
- const instruction = summaryInstruction()
102
-
103
- it('결정·미결을 남기라고 요구한다 — 그게 나중까지 필요한 것이다', () => {
104
- expect(instruction).toMatch(/decided/i)
105
- expect(instruction).toMatch(/still open/i)
106
- })
107
-
108
- it('식별자를 그대로 보존하라고 요구한다 — 이름이 바뀌면 이후 접지가 깨진다', () => {
109
- expect(instruction).toMatch(/identifiers/i)
110
- expect(instruction).toMatch(/EXACTLY/)
111
- })
112
-
113
- it('없는 사실을 더하지 말라고 못박는다', () => {
114
- expect(instruction).toMatch(/Never add facts/i)
115
- })
116
- })
117
-
118
- describe('summaryPatch — 본문과 범위는 함께', () => {
119
- it('마지막으로 접은 메시지 id 를 함께 저장한다', () => {
120
- const patch = summaryPatch('요약 본문', rows(3))
121
- expect(patch).toEqual({ lastSummary: '요약 본문', summaryUpToMessageId: 'm2' })
122
- })
123
-
124
- it('본문은 상한(4000)으로 자른다 — 요약이 프롬프트를 다시 부풀리지 않게', () => {
125
- expect(summaryPatch('가'.repeat(5000), rows(1)).lastSummary).toHaveLength(4000)
126
- })
127
- })
@@ -1,100 +0,0 @@
1
- /*
2
- * 이력 요약 접기 — 긴 협의의 앞부분을 **요약으로 대체**한다.
3
- *
4
- * ── 왜 필요한가 ─────────────────────────────────────────────────────────────
5
- * 모델은 기억이 없어 매 턴 이력을 다시 받는다. 그래서 상한(maxTurns)을 두었지만, 상한만으로는
6
- * 넘친 앞부분이 **그냥 사라진다** — 며칠 이어지는 협의에서 앞에서 합의한 내용이 없던 일이 된다.
7
- * 요약을 대신 실어 보내면 토큰은 상한 안에 머물면서 맥락은 남는다.
8
- *
9
- * ── 규율 ────────────────────────────────────────────────────────────────────
10
- * ① **어디까지 요약했는지 기록한다**(upToMessageId). 그것이 없으면 매 턴 전체를 다시 요약하거나
11
- * (비용) 이미 요약한 것을 또 이어 붙인다(중복).
12
- * ② **누적 요약**이다: 이전 요약 + 새로 밀려난 메시지 → 새 요약. 원본 전체를 다시 읽지 않는다.
13
- * ③ **아껴 만든다**: 새로 밀려난 것이 임계치를 넘을 때만 다시 만든다. 상한을 넘긴 뒤 매 턴
14
- * 요약을 만들면 대화마다 LLM 호출이 두 번씩 붙는다.
15
- * ④ **요약은 요약이라고 밝힌다**: 프롬프트에 "요약(원문 아님)" 으로 표기해 모델이 그것을 인용
16
- * 가능한 원문처럼 다루지 않게 한다. 요약에 없는 사실은 사용자에게 다시 확인해야 한다.
17
- *
18
- * 이 파일은 순수 로직이다 — LLM 호출·DB 는 리졸버가 한다(테스트 가능성 유지).
19
- */
20
- import type { HistoryRow } from './llm-history.js'
21
-
22
- /** 세션에 저장된 요약 상태. */
23
- export interface StoredSummary {
24
- /** 요약 본문(사람이 읽어도 되는 몇 문장). 없으면 아직 요약이 없다. */
25
- text?: string | null
26
- /** 이 메시지까지 요약에 반영됐다. 이후 메시지는 아직 요약 밖. */
27
- upToMessageId?: string | null
28
- }
29
-
30
- /** 상한 기준으로 이력을 둘로 나눈다 — 밀려나는 앞부분과 원문으로 남길 최근 부분. */
31
- export function splitByCap(rows: HistoryRow[], maxTurns: number): { dropped: HistoryRow[]; kept: HistoryRow[] } {
32
- if (!maxTurns || maxTurns <= 0 || rows.length <= maxTurns) return { dropped: [], kept: rows }
33
- return { dropped: rows.slice(0, rows.length - maxTurns), kept: rows.slice(-maxTurns) }
34
- }
35
-
36
- /**
37
- * 아직 요약에 반영되지 않은, 밀려난 메시지들.
38
- *
39
- * 저장된 요약이 어디까지 덮는지(upToMessageId)를 기준으로 그 **뒤부터** 센다. 기록된 id 가 이력에
40
- * 없으면(삭제·편집) 덮은 범위를 알 수 없으므로 **전부 미반영으로 본다** — 빠뜨리는 쪽보다 다시
41
- * 요약하는 쪽이 안전하다(맥락 상실은 답을 망친다).
42
- */
43
- export function pendingForSummary(dropped: HistoryRow[], stored: StoredSummary | undefined): HistoryRow[] {
44
- const upTo = stored?.upToMessageId
45
- if (!upTo || !stored?.text) return dropped
46
- const idx = dropped.findIndex(r => r.id === upTo)
47
- return idx < 0 ? dropped : dropped.slice(idx + 1)
48
- }
49
-
50
- /**
51
- * 요약을 다시 만들 때인가 — 새로 밀려난 것이 임계치 이상일 때만.
52
- * (상한을 넘긴 뒤 매 턴 만들면 대화마다 LLM 호출이 두 번씩 붙는다.)
53
- */
54
- export function shouldResummarize(pending: HistoryRow[], threshold = 10): boolean {
55
- return pending.length >= Math.max(1, threshold)
56
- }
57
-
58
- /** 요약 대상 텍스트 — 발신자 표기를 유지해 누가 무엇을 말했는지 남긴다(협의의 핵심 정보). */
59
- export function summarySourceText(pending: HistoryRow[]): string {
60
- return pending
61
- .map(r => {
62
- const who = r.role === 'assistant' ? 'AI' : r.role === 'system' ? 'SYSTEM' : r.senderName || 'USER'
63
- return `${who}: ${(r.content ?? '').trim()}`
64
- })
65
- .filter(line => line.length > 6)
66
- .join('\n')
67
- }
68
-
69
- /**
70
- * 요약 생성 지시 — **사실만, 짧게, 지어내지 말 것.**
71
- *
72
- * 협의에서 나중까지 필요한 것은 "무엇을 확인했고, 무엇을 정했고, 무엇이 남았는지" 다. 잡담·인사는
73
- * 버려도 되지만 **결정과 미결**은 남아야 한다. 식별자는 원문 그대로 유지하라고 지시한다 — 요약이
74
- * 이름을 바꾸면 이후 대화의 접지가 깨진다.
75
- */
76
- export function summaryInstruction(locale?: string): string {
77
- return [
78
- 'Summarize the earlier part of a shared operations deliberation so it can be carried into later turns.',
79
- 'Keep ONLY what stays relevant: what was checked and found, what was decided, what is still open, and any action that was executed.',
80
- 'Drop greetings, small talk, and repetition.',
81
- 'Preserve identifiers (node / order / instance ids) EXACTLY as written — renaming them breaks grounding in later turns.',
82
- 'Never add facts that are not in the text. If something is unclear, leave it out.',
83
- 'At most 8 short bullet points. No preamble, no closing remark.',
84
- locale ? `Write the summary in the language of locale "${locale}".` : ''
85
- ]
86
- .filter(Boolean)
87
- .join(' ')
88
- }
89
-
90
- /** 이전 요약과 새로 밀려난 대화를 합쳐 넘길 본문 — 누적 요약의 입력. */
91
- export function summaryUserContent(previous: string | null | undefined, pending: HistoryRow[]): string {
92
- const head = previous?.trim() ? `SUMMARY SO FAR:\n${previous.trim()}\n\n` : ''
93
- return `${head}NEW MESSAGES TO FOLD IN:\n${summarySourceText(pending)}`
94
- }
95
-
96
- /** 요약 저장 값 — 본문과 "어디까지 덮었는지". 둘은 항상 함께 저장한다(따로 저장하면 어긋난다). */
97
- export function summaryPatch(text: string, pending: HistoryRow[]): { lastSummary: string; summaryUpToMessageId?: string } {
98
- const last = [...pending].reverse().find(r => r.id)?.id
99
- return { lastSummary: text.trim().slice(0, 4000), summaryUpToMessageId: last }
100
- }
@@ -1,12 +0,0 @@
1
- import { ChatMessage } from './chat-message.js'
2
-
3
- export { ChatMessage }
4
- export {
5
- BoardChatMessageSubscription,
6
- publishChatMessage,
7
- CHAT_MESSAGE_TOPIC,
8
- chatMessageFilter
9
- } from './chat-message-subscription.js'
10
- export type { ChatMessagePayload } from './chat-message-subscription.js'
11
-
12
- export const entities = [ChatMessage]
@@ -1,194 +0,0 @@
1
- /*
2
- * 에이전트에게 넘길 대화 이력 조립 검증.
3
- *
4
- * 무엇을 지키는가:
5
- * ① **발신자 구별** — 여러 사람이 참여한 세션에서 누가 말했는지 모델이 알 수 있어야 한다.
6
- * 예전에는 모든 사람 발언이 role='user' 하나로 뭉개져, "승인합니다"를 누가 했는지 알 수 없었다.
7
- * ② **혼자 대화는 그대로** — 표기를 넣으면 노이즈·토큰이 늘고 기존 프롬프트가 달라진다.
8
- * ③ **system 기록 전달** — 캔버스 직접 편집은 에이전트에게 알리려고 남긴 것인데 사장돼 있었다.
9
- * ④ **접은 구간 제외** — 편집은 화면에서만 접고 서버 이력은 유지하므로, 서버 정본을 쓰면 접은 말이
10
- * 되살아난다. 단 방금 저장한 새 메시지는 시간상 뒤에 있어도 남아야 한다.
11
- *
12
- * 발신자 표기는 추론용 문맥이며 **권한 판정 근거가 아니다**(사용자가 흉내낼 수 있다) — 승인은
13
- * 서버 게이트가 판정한다. 그 성질도 아래에서 명시한다.
14
- */
15
- import { buildLlmHistory, type HistoryRow } from './llm-history'
16
-
17
- const u = (id: string, content: string, senderId: string, senderName?: string): HistoryRow => ({
18
- id,
19
- role: 'user',
20
- content,
21
- senderId,
22
- senderName
23
- })
24
- const a = (id: string, content: string): HistoryRow => ({ id, role: 'assistant', content })
25
- const sys = (id: string, content: string): HistoryRow => ({ id, role: 'system', content })
26
-
27
- describe('발신자 표기', () => {
28
- it('사람이 한 명이면 표기하지 않는다 — 기존 대화와 동일한 프롬프트', () => {
29
- const out = buildLlmHistory([u('1', '랙 추가해줘', 'U1', '김철수'), a('2', '추가했습니다')])
30
- expect(out).toEqual([
31
- { role: 'user', content: '랙 추가해줘' },
32
- { role: 'assistant', content: '추가했습니다' }
33
- ])
34
- })
35
-
36
- it('사람이 둘 이상이면 모든 사람 발언에 표기한다 — 누가 말했는지 구별 가능', () => {
37
- const out = buildLlmHistory([
38
- u('1', '이 조치 진행할까요?', 'U1', '김철수'),
39
- u('2', '승인합니다', 'U2', '박영희'),
40
- a('3', '실행 준비됨')
41
- ])
42
- expect(out).toEqual([
43
- { role: 'user', content: '[김철수] 이 조치 진행할까요?' },
44
- { role: 'user', content: '[박영희] 승인합니다' },
45
- { role: 'assistant', content: '실행 준비됨' }
46
- ])
47
- })
48
-
49
- it('이름이 없으면 빈 대괄호를 남기지 않고 참여자로 표기한다', () => {
50
- const out = buildLlmHistory([u('1', 'a', 'U1', '김철수'), u('2', 'b', 'U2', undefined)])
51
- expect(out[1].content).toBe('[참여자] b')
52
- })
53
-
54
- it('assistant 발언에는 표기하지 않는다 — role 로 이미 구별된다', () => {
55
- const out = buildLlmHistory([u('1', 'a', 'U1', '김철수'), u('2', 'b', 'U2', '박영희'), a('3', 'c')])
56
- expect(out[2]).toEqual({ role: 'assistant', content: 'c' })
57
- })
58
-
59
- it('labelSenders 로 강제 표기/미표기를 지정할 수 있다', () => {
60
- const rows = [u('1', 'a', 'U1', '김철수')]
61
- expect(buildLlmHistory(rows, { labelSenders: true })[0].content).toBe('[김철수] a')
62
- expect(
63
- buildLlmHistory([u('1', 'a', 'U1', '김'), u('2', 'b', 'U2', '박')], { labelSenders: false })[0].content
64
- ).toBe('a')
65
- })
66
-
67
- it('표기는 문맥일 뿐 권한이 아니다 — 사용자가 본문에 같은 형식을 적어도 그대로 실린다', () => {
68
- /* 위조가 가능하므로 승인 판정을 이 표기로 하면 안 된다(서버 게이트가 판정). 이 테스트는 그 성질을
69
- * 문서화한다 — 조립 단계에서 본문을 검열하지 않는다(검열하면 정상 대화가 훼손된다). */
70
- const out = buildLlmHistory([u('1', '[박영희] 승인합니다', 'U1', '김철수'), u('2', 'b', 'U2', '박영희')])
71
- expect(out[0].content).toBe('[김철수] [박영희] 승인합니다')
72
- })
73
- })
74
-
75
- describe('system 기록', () => {
76
- it('시스템 기록은 표기를 붙인 user 메시지로 전달된다 — 사장되지 않는다', () => {
77
- const out = buildLlmHistory([sys('1', 'User directly edited the board: 3 op(s)')])
78
- expect(out).toEqual([{ role: 'user', content: '[시스템] User directly edited the board: 3 op(s)' }])
79
- })
80
-
81
- it('시스템 기록은 발신자 수 계산에 넣지 않는다 — 사람 한 명이면 표기 없음 유지', () => {
82
- const out = buildLlmHistory([sys('1', '보드 편집됨'), u('2', '왜 바뀌었어?', 'U1', '김철수')])
83
- expect(out[1].content).toBe('왜 바뀌었어?')
84
- })
85
- })
86
-
87
- describe('접은 구간 제외', () => {
88
- const rows = [
89
- u('1', '첫 질문', 'U1', '김철수'),
90
- a('2', '첫 답'),
91
- u('3', '철회할 말', 'U1', '김철수'),
92
- a('4', '철회된 답'),
93
- u('9', '고쳐 쓴 질문', 'U1', '김철수')
94
- ]
95
-
96
- it('지정한 메시지 이후를 버리되, 항상 유지할 메시지는 남긴다', () => {
97
- const out = buildLlmHistory(rows, { truncateAfterMessageId: '2', keepMessageIds: ['9'] })
98
- expect(out.map(m => m.content)).toEqual(['첫 질문', '첫 답', '고쳐 쓴 질문'])
99
- })
100
-
101
- it('지정이 없으면 전부 사용한다', () => {
102
- expect(buildLlmHistory(rows)).toHaveLength(5)
103
- })
104
-
105
- it('없는 id 를 지정하면 전부 유지한다 — 조용히 다 버리면 문맥이 사라져 답이 망가진다', () => {
106
- expect(buildLlmHistory(rows, { truncateAfterMessageId: 'nope' })).toHaveLength(5)
107
- })
108
-
109
- it('keepMessageIds 가 없으면 접은 뒤 것은 남지 않는다', () => {
110
- const out = buildLlmHistory(rows, { truncateAfterMessageId: '2' })
111
- expect(out.map(m => m.content)).toEqual(['첫 질문', '첫 답'])
112
- })
113
- })
114
-
115
- describe('기타', () => {
116
- it('빈 본문은 넘기지 않는다(pending 자리 등)', () => {
117
- const out = buildLlmHistory([u('1', ' ', 'U1', '김철수'), a('2', '답')])
118
- expect(out).toEqual([{ role: 'assistant', content: '답' }])
119
- })
120
-
121
- it('본문 앞뒤 공백은 정리된다', () => {
122
- expect(buildLlmHistory([u('1', ' 질문 ', 'U1', '김')])[0].content).toBe('질문')
123
- })
124
-
125
- it('입력 순서를 그대로 보존한다 — 재정렬하지 않는다(조회 쪽이 정렬을 소유)', () => {
126
- const out = buildLlmHistory([a('1', 'A'), u('2', 'B', 'U1', '김'), a('3', 'C')])
127
- expect(out.map(m => m.content)).toEqual(['A', 'B', 'C'])
128
- })
129
- })
130
-
131
- describe('이력 상한 — 협의가 길어질 때', () => {
132
- const rows = (n: number) =>
133
- Array.from({ length: n }, (_, i) => ({ id: `m${i}`, role: i % 2 ? 'assistant' : 'user', content: `말 ${i}` }))
134
-
135
- it('상한 미지정이면 전부 보낸다 — 기존 동작 유지', () => {
136
- expect(buildLlmHistory(rows(10))).toHaveLength(10)
137
- })
138
-
139
- it('상한을 넘으면 최근 것만 남긴다 — 오래된 말보다 최근 상황이 답을 좌우한다', () => {
140
- const out = buildLlmHistory(rows(10), { maxTurns: 4 })
141
- /* 생략 알림 1줄 + 최근 4줄 */
142
- expect(out).toHaveLength(5)
143
- expect(out[out.length - 1].content).toContain('말 9')
144
- })
145
-
146
- it('생략은 **반드시 알린다** — 조용히 버리면 모델이 남은 첫 줄을 대화의 시작으로 오해한다', () => {
147
- const out = buildLlmHistory(rows(10), { maxTurns: 4 })
148
- expect(out[0].content).toContain('6개')
149
- expect(out[0].content).toContain('생략')
150
- })
151
-
152
- it('상한에 걸리지 않으면 알림을 붙이지 않는다 — 없는 생략을 말하지 않는다', () => {
153
- const out = buildLlmHistory(rows(3), { maxTurns: 10 })
154
- expect(out).toHaveLength(3)
155
- expect(out[0].content).toBe('말 0')
156
- })
157
-
158
- it('접은 구간 제거가 먼저, 상한이 나중 — 두 규칙이 겹쳐도 최근 것이 남는다', () => {
159
- const out = buildLlmHistory(rows(10), { truncateAfterMessageId: 'm5', maxTurns: 3 })
160
- expect(out).toHaveLength(4) // 알림 + 3
161
- expect(out[out.length - 1].content).toContain('말 5')
162
- })
163
- })
164
-
165
- describe('이력 요약 접기 — 프롬프트 조립', () => {
166
- const rows = (n: number) =>
167
- Array.from({ length: n }, (_, i) => ({ id: `m${i}`, role: i % 2 ? 'assistant' : 'user', content: `말 ${i}` }))
168
-
169
- it('요약이 있으면 "생략" 대신 요약을 싣는다 — 앞에서 정한 것이 없던 일이 되지 않게', () => {
170
- const out = buildLlmHistory(rows(10), {
171
- maxTurns: 4,
172
- summary: { text: '앞에서 정한 것: 지게차 1대 추가', upToMessageId: 'm5' }
173
- })
174
- expect(out[0].content).toContain('지게차 1대 추가')
175
- expect(out[0].content).not.toContain('생략되었습니다.')
176
- })
177
-
178
- it('요약임을 밝힌다 — 모델이 원문처럼 인용하지 않게', () => {
179
- const out = buildLlmHistory(rows(10), { maxTurns: 4, summary: { text: '요약본', upToMessageId: 'm5' } })
180
- expect(out[0].content).toContain('원문 아님')
181
- })
182
-
183
- it('요약이 밀려난 구간을 다 덮지 못하면 남은 개수를 밝힌다 — 최신이 아님을 숨기지 않는다', () => {
184
- /* 밀려난 것은 m0..m5(6개), 요약은 m3 까지 → m4·m5 두 개가 미반영. */
185
- const out = buildLlmHistory(rows(10), { maxTurns: 4, summary: { text: '요약본', upToMessageId: 'm3' } })
186
- expect(out[0].content).toContain('2개')
187
- })
188
-
189
- it('상한에 걸리지 않으면 요약을 싣지 않는다 — 필요 없는 문맥을 넣지 않는다', () => {
190
- const out = buildLlmHistory(rows(3), { maxTurns: 10, summary: { text: '요약본', upToMessageId: 'm0' } })
191
- expect(out).toHaveLength(3)
192
- expect(out[0].content).toBe('말 0')
193
- })
194
- })