@things-factory/board-ai 10.0.1 → 10.0.3

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 (90) hide show
  1. package/client/components/board-ai-chat.ts +565 -21
  2. package/client/components/chat-echo-dedup.test.ts +59 -3
  3. package/client/components/chat-echo-dedup.ts +32 -0
  4. package/client/components/chat-input-builder.ts +6 -0
  5. package/dist-client/client/components/board-ai-chat.d.ts +80 -0
  6. package/dist-client/client/components/board-ai-chat.js +540 -18
  7. package/dist-client/client/components/board-ai-chat.js.map +1 -1
  8. package/dist-client/client/components/chat-echo-dedup.d.ts +2 -0
  9. package/dist-client/client/components/chat-echo-dedup.js +29 -0
  10. package/dist-client/client/components/chat-echo-dedup.js.map +1 -1
  11. package/dist-client/client/components/chat-echo-dedup.test.js +53 -3
  12. package/dist-client/client/components/chat-echo-dedup.test.js.map +1 -1
  13. package/dist-client/client/components/chat-input-builder.d.ts +5 -0
  14. package/dist-client/client/components/chat-input-builder.js +1 -0
  15. package/dist-client/client/components/chat-input-builder.js.map +1 -1
  16. package/dist-client/server/service/agentic-loop.d.ts +33 -0
  17. package/dist-client/server/service/agentic-loop.js +80 -10
  18. package/dist-client/server/service/agentic-loop.js.map +1 -1
  19. package/dist-client/server/service/assistant.js +32 -5
  20. package/dist-client/server/service/assistant.js.map +1 -1
  21. package/dist-client/server/service/grounding.d.ts +17 -0
  22. package/dist-client/server/service/grounding.js +42 -0
  23. package/dist-client/server/service/grounding.js.map +1 -0
  24. package/dist-client/server/service/types.d.ts +39 -0
  25. package/dist-client/server/service/types.js.map +1 -1
  26. package/dist-client/tsconfig.tsbuildinfo +1 -1
  27. package/dist-server/service/agentic-loop.d.ts +33 -0
  28. package/dist-server/service/agentic-loop.js +81 -10
  29. package/dist-server/service/agentic-loop.js.map +1 -1
  30. package/dist-server/service/assistant.js +31 -4
  31. package/dist-server/service/assistant.js.map +1 -1
  32. package/dist-server/service/board-ai-resolver.d.ts +15 -0
  33. package/dist-server/service/board-ai-resolver.js +121 -2
  34. package/dist-server/service/board-ai-resolver.js.map +1 -1
  35. package/dist-server/service/chat-message/chat-message.d.ts +12 -0
  36. package/dist-server/service/chat-message/chat-message.js +23 -0
  37. package/dist-server/service/chat-message/chat-message.js.map +1 -1
  38. package/dist-server/service/chat-message/fold-history.d.ts +30 -0
  39. package/dist-server/service/chat-message/fold-history.js +29 -0
  40. package/dist-server/service/chat-message/fold-history.js.map +1 -0
  41. package/dist-server/service/chat-message/history-summary.d.ts +43 -0
  42. package/dist-server/service/chat-message/history-summary.js +77 -0
  43. package/dist-server/service/chat-message/history-summary.js.map +1 -0
  44. package/dist-server/service/chat-message/llm-history.d.ts +19 -0
  45. package/dist-server/service/chat-message/llm-history.js +31 -1
  46. package/dist-server/service/chat-message/llm-history.js.map +1 -1
  47. package/dist-server/service/chat-session/chat-session.d.ts +8 -0
  48. package/dist-server/service/chat-session/chat-session.js +5 -0
  49. package/dist-server/service/chat-session/chat-session.js.map +1 -1
  50. package/dist-server/service/chat-session/session-inbox.d.ts +26 -0
  51. package/dist-server/service/chat-session/session-inbox.js +41 -0
  52. package/dist-server/service/chat-session/session-inbox.js.map +1 -1
  53. package/dist-server/service/chat-session-participant/chat-session-participant.d.ts +11 -0
  54. package/dist-server/service/chat-session-participant/chat-session-participant.js +17 -1
  55. package/dist-server/service/chat-session-participant/chat-session-participant.js.map +1 -1
  56. package/dist-server/service/chat-session-resolver.d.ts +44 -1
  57. package/dist-server/service/chat-session-resolver.js +306 -6
  58. package/dist-server/service/chat-session-resolver.js.map +1 -1
  59. package/dist-server/service/grounding.d.ts +17 -0
  60. package/dist-server/service/grounding.js +46 -0
  61. package/dist-server/service/grounding.js.map +1 -0
  62. package/dist-server/service/types.d.ts +39 -0
  63. package/dist-server/service/types.js.map +1 -1
  64. package/dist-server/tsconfig.tsbuildinfo +1 -1
  65. package/package.json +6 -6
  66. package/server/service/agentic-loop.test.ts +154 -0
  67. package/server/service/agentic-loop.ts +108 -10
  68. package/server/service/assistant.ts +36 -5
  69. package/server/service/board-ai-resolver.ts +131 -2
  70. package/server/service/chat-message/chat-message.ts +26 -0
  71. package/server/service/chat-message/fold-history.test.ts +98 -0
  72. package/server/service/chat-message/fold-history.ts +60 -0
  73. package/server/service/chat-message/history-summary.test.ts +127 -0
  74. package/server/service/chat-message/history-summary.ts +100 -0
  75. package/server/service/chat-message/llm-history.test.ts +65 -0
  76. package/server/service/chat-message/llm-history.ts +48 -1
  77. package/server/service/chat-session/chat-session.ts +11 -0
  78. package/server/service/chat-session/session-inbox.test.ts +69 -1
  79. package/server/service/chat-session/session-inbox.ts +45 -0
  80. package/server/service/chat-session-participant/chat-session-participant.ts +14 -0
  81. package/server/service/chat-session-resolver.ts +297 -5
  82. package/server/service/dock-contract.test.ts +305 -0
  83. package/server/service/grounding.test.ts +55 -0
  84. package/server/service/grounding.ts +53 -0
  85. package/server/service/types.ts +39 -0
  86. package/translations/en.json +16 -1
  87. package/translations/ja.json +16 -1
  88. package/translations/ko.json +15 -0
  89. package/translations/ms.json +16 -1
  90. package/translations/zh.json +16 -1
@@ -154,9 +154,24 @@ describe('내 발신 에코 흡수 (대기 중 중복 렌더 방지)', () => {
154
154
  expect(r.lines[2]).toMatchObject({ id: 'S9', senderEmail: 'other@x.com' })
155
155
  })
156
156
 
157
- it('assistant 방송은 흡수 대상이 아니다(append)', () => {
158
- const r = receiveRemoteMessage(pending(), new Set(), { id: 'S2', role: 'assistant', content: '답' })!
159
- expect(r.lines).toHaveLength(3)
157
+ /* 이 테스트는 예전에 "assistant 방송은 흡수하지 않고 붙인다" 규칙으로 굳혀 두고 있었다.
158
+ * 그것이 실제 결함이었다 붙이면 뒤이은 mutation 응답이 대기 줄을 지우고, 줄에만 있던
159
+ * **영속되지 않는 응답 데이터**(조치 제안 카드·접지 경고)가 함께 사라진다. 규칙을 바로잡는다. */
160
+ it('assistant 방송도 대기 중인 AI 줄에 흡수한다 — 붙이면 제안 카드가 사라진다', () => {
161
+ const lines = [
162
+ { _localId: 'L1', role: 'user', content: '질문', senderEmail: 'me@x.com' },
163
+ { _localId: 'L2', role: 'assistant', content: '', pending: true }
164
+ ]
165
+ const r = receiveRemoteMessage(lines as any, new Set(), { id: 'S2', role: 'assistant', content: '답' })!
166
+ expect(r.lines).toHaveLength(2)
167
+ expect(r.lines[1]).toMatchObject({ id: 'S2', content: '답', pending: false })
168
+ })
169
+
170
+ it('대기 중인 AI 줄이 없으면 붙인다 — 다른 참여자가 부른 답변', () => {
171
+ const settled = [{ id: 'S0', role: 'user', content: '질문' }]
172
+ const r = receiveRemoteMessage(settled as any, new Set(), { id: 'S2', role: 'assistant', content: '답' })!
173
+ expect(r.lines).toHaveLength(2)
174
+ expect(r.lines[1].id).toBe('S2')
160
175
  })
161
176
 
162
177
  it('흡수한 뒤 mutation 응답 reconcile 은 그 줄을 지우지 않고 patch 를 병합한다', () => {
@@ -172,3 +187,44 @@ describe('내 발신 에코 흡수 (대기 중 중복 렌더 방지)', () => {
172
187
  expect(after.lines[0]._localId).toBeUndefined()
173
188
  })
174
189
  })
190
+
191
+ describe('AI 답변 에코 흡수 — 영속되지 않는 응답 데이터를 잃지 않기', () => {
192
+ /* 실제 사고: 방송이 mutation 응답보다 먼저 와서 assistant 줄이 새로 붙고, 뒤이어 응답이 대기 줄을
193
+ * 지웠다. 대기 줄에만 있던 **조치 제안 카드**가 그때 사라졌다(도구는 분명히 호출됐는데도). */
194
+ const pendingAi = () => [
195
+ { id: 'u1', role: 'user', content: '지게차 한 대 추가해줘' },
196
+ { _localId: 'L2', role: 'assistant', content: '', pending: true, proposals: [{ command: 'resource.add' }] }
197
+ ]
198
+
199
+ it('대기 중인 AI 줄에 흡수한다 — 새 줄로 붙이지 않는다', () => {
200
+ const r = receiveRemoteMessage(pendingAi() as any, new Set(), {
201
+ id: 'a1',
202
+ role: 'assistant',
203
+ content: '제안해 두었습니다.'
204
+ })!
205
+ expect(r.lines).toHaveLength(2)
206
+ expect(r.lines[1].id).toBe('a1')
207
+ expect(r.lines[1].content).toBe('제안해 두었습니다.')
208
+ })
209
+
210
+ it('제안 카드를 유지한다 — 영속되지 않는 값이라 잃으면 화면에서 버튼이 사라진다', () => {
211
+ const r = receiveRemoteMessage(pendingAi() as any, new Set(), {
212
+ id: 'a1',
213
+ role: 'assistant',
214
+ content: '제안해 두었습니다.'
215
+ })!
216
+ expect(r.lines[1].proposals).toEqual([{ command: 'resource.add' }])
217
+ })
218
+
219
+ it('대기 표시를 푼다 — 방송이 먼저 오면 답이 더 빨리 보인다', () => {
220
+ const r = receiveRemoteMessage(pendingAi() as any, new Set(), { id: 'a1', role: 'assistant', content: '답' })!
221
+ expect(r.lines[1].pending).toBe(false)
222
+ })
223
+
224
+ it('대기 줄이 없으면(다른 참여자가 부른 AI 답변) 새 줄로 붙인다', () => {
225
+ const lines = [{ id: 'u1', role: 'user', content: '질문' }]
226
+ const r = receiveRemoteMessage(lines as any, new Set(), { id: 'a9', role: 'assistant', content: '답' })!
227
+ expect(r.lines).toHaveLength(2)
228
+ expect(r.lines[1].id).toBe('a9')
229
+ })
230
+ })
@@ -27,6 +27,8 @@ export interface RemoteMessage {
27
27
  content: string
28
28
  relatedPatchId?: string
29
29
  toolUsagesJson?: any
30
+ /** 접지 경고(영속) — 답이 언급했으나 근거에 없던 식별자. 나중에 읽는 사람도 같은 주의를 봐야 한다. */
31
+ groundingWarningsJson?: any
30
32
  /** 발신자 — 표시용 name + **자기 여부 판정용 email**(내부 관리키 id 는 쓰지 않는다). */
31
33
  creator?: { name?: string; email?: string } | null
32
34
  }
@@ -39,6 +41,7 @@ export function remoteMessageToLine(m: RemoteMessage): DedupLine {
39
41
  content: m.content,
40
42
  patchId: m.relatedPatchId ?? undefined,
41
43
  toolUsages: Array.isArray(m.toolUsagesJson) ? m.toolUsagesJson : undefined,
44
+ groundingWarnings: Array.isArray(m.groundingWarningsJson) ? m.groundingWarningsJson : undefined,
42
45
  senderEmail: m.creator?.email ?? undefined,
43
46
  senderName: m.creator?.name ?? undefined
44
47
  }
@@ -69,6 +72,35 @@ export function receiveRemoteMessage(
69
72
  /* 내 발신 에코 흡수 — `_localId` 가 있는 줄은 이 클라이언트가 만든 것뿐이다.
70
73
  * 발신자 판정: 양쪽 email 이 있으면 같아야 하고, 방송에 발신자 email 이 없으면(단일 사용자·구형)
71
74
  * 내 것으로 본다. 다른 사용자의 메시지가 내 대기 줄을 삼키지 않도록 email 이 다르면 흡수하지 않는다. */
75
+ /* AI 답변 에코 흡수 — 사용자 발화와 **같은 이유**로 붙이지 않고 대기 줄에 흡수한다.
76
+ *
77
+ * 왜 필요한가(실제 사고): 방송은 mutation 응답보다 먼저 도착할 수 있다. 예전에는 assistant 방송을
78
+ * 새 줄로 붙였고, 뒤이어 mutation 응답이 "같은 서버 id 를 가진 다른 줄이 있다" 며 **대기 줄을 지웠다**.
79
+ * 그런데 대기 줄에만 있는 것이 있다 — **영속되지 않는 응답 데이터**(조치 제안 카드·접지 경고).
80
+ * 그래서 화면에서 실행 버튼이 사라졌다(제안 도구는 분명히 호출됐는데도).
81
+ *
82
+ * 흡수하면 서버가 보낸 본문(멘션 마커 포함)을 그 줄에 입히면서 대기 표시를 풀 수 있어, 답이 더
83
+ * 빨리 보이는 효과도 있다. */
84
+ if (m.role === 'assistant') {
85
+ const idx = lines.findIndex(l => l.role === 'assistant' && !l.id && !!l._localId)
86
+ if (idx >= 0) {
87
+ const nextLines = [...lines]
88
+ nextLines[idx] = {
89
+ ...nextLines[idx],
90
+ id: m.id,
91
+ /* 본문·영속 데이터는 서버 것으로 갱신하고, 이 줄이 들고 있던 값(제안 등)은 유지한다. */
92
+ content: m.content,
93
+ patchId: m.relatedPatchId ?? nextLines[idx].patchId,
94
+ toolUsages: Array.isArray(m.toolUsagesJson) ? m.toolUsagesJson : nextLines[idx].toolUsages,
95
+ groundingWarnings: Array.isArray(m.groundingWarningsJson)
96
+ ? m.groundingWarningsJson
97
+ : nextLines[idx].groundingWarnings,
98
+ pending: false
99
+ }
100
+ return { lines: nextLines, seen: nextSeen }
101
+ }
102
+ }
103
+
72
104
  if (m.role === 'user') {
73
105
  const echoEmail = m.creator?.email
74
106
  const idx = lines.findIndex(
@@ -21,6 +21,11 @@ export interface ChatMutationInputArgs {
21
21
  toolCategories?: string[]
22
22
  /** 코어 보드 편집 도구(addComponent·스타일링·씬 조작) 사용 여부. 기본 true. */
23
23
  boardTools?: boolean
24
+ /**
25
+ * 첫 턴에 도구 호출을 강제할지 — 라이브 상태를 다루는 대화면에서 켠다.
26
+ * 근거 없이 상태를 단언하는 답(도구를 아예 부르지 않은 답)을 원천에서 막는다.
27
+ */
28
+ requireGroundingTools?: boolean
24
29
  knownTypes?: string[]
25
30
  categories?: string[]
26
31
  componentSchemas?: any
@@ -54,6 +59,7 @@ export function buildChatMutationInput(args: ChatMutationInputArgs): Record<stri
54
59
  /* 미지정은 서버 기본(전부 허용)에 맡긴다 — null 을 보내 굳이 덮어쓰지 않는다. */
55
60
  ...(args.toolCategories ? { toolCategories: args.toolCategories } : {}),
56
61
  ...(args.boardTools === false ? { boardTools: false } : {}),
62
+ ...(args.requireGroundingTools ? { requireGroundingTools: true } : {}),
57
63
  knownTypes: args.knownTypes,
58
64
  categories: args.categories,
59
65
  componentSchemas: args.componentSchemas,
@@ -100,6 +100,13 @@ export declare class OxBoardAIChat extends LitElement {
100
100
  hostContext?: any;
101
101
  /** 코어 보드 편집 도구 사용 여부. 기본 true. 보드를 고칠 이유가 없는 면에서 false. */
102
102
  boardTools: boolean;
103
+ /**
104
+ * 라이브 상태를 다루는 대화면인가 — true 면 **첫 턴에 도구 호출을 강제**한다.
105
+ *
106
+ * 접지 가드는 지어낸 식별자를 잡지만 식별자 없는 상황 서술("바쁘게 움직이고 있다")은 잡지 못한다.
107
+ * 그건 도구를 부르지 않고 답한 경우이므로, 조회를 먼저 하게 만들어 원천을 막는다.
108
+ */
109
+ requireGroundingTools: boolean;
103
110
  /** board-import registry scopes. */
104
111
  scopes?: string[];
105
112
  knownTypes?: string[];
@@ -128,6 +135,18 @@ export declare class OxBoardAIChat extends LitElement {
128
135
  private toastMessage?;
129
136
  /** 복사 confirmation feedback (메시지 idx → 짧은 시간 동안 ✓ 표시) */
130
137
  private copiedIdx?;
138
+ /** 원본 JSON 을 펼친 단계(라인:단계) — 목록은 한 줄 요약으로 훑고 필요한 것만 펼친다. */
139
+ private openToolSteps;
140
+ /** 이미 실행을 요청한 제안 키 — 같은 카드를 두 번 눌러 되돌릴 수 없는 명령이 두 번 나가지 않게. */
141
+ private sentProposals;
142
+ /**
143
+ * 이 대화가 길어져 앞부분이 프롬프트에서 접혔다는 **사실**(서버 보고). 주제 이탈을 판정한 것이
144
+ * 아니다 — 판정하면 오탐이 생기고, 오탐이 생기면 사용자는 안내 자체를 무시한다.
145
+ * 사실만 알리고 "새 대화로 시작할지" 는 사용자가 정한다.
146
+ */
147
+ private historyFolded?;
148
+ /** 안내를 닫았는가 — 한 번 닫으면 이 세션에서는 다시 띄우지 않는다(잔소리 금지). */
149
+ private foldNoticeDismissed;
131
150
  private busy;
132
151
  private errorMessage?;
133
152
  /** C4 presence/roster — 세션 참여자 목록. lastSeenAt 으로 online 판정. */
@@ -261,6 +280,50 @@ export declare class OxBoardAIChat extends LitElement {
261
280
  * system : 없음
262
281
  */
263
282
  private renderMessageActions;
283
+ /**
284
+ * "대화가 길어졌습니다" 안내 — **사실만** 알리고 결정은 사용자에게 맡긴다.
285
+ *
286
+ * 주제가 바뀌었는지 판정하지 않는다: 판정하면 오탐이 생기고, 오탐이 생기면 사용자는 안내 자체를
287
+ * 무시한다(접지 경고에서 배운 것과 같다). 근거는 "앞부분이 프롬프트에서 접혔다" 는 객관적 사실뿐이다.
288
+ *
289
+ * 왜 알려야 하는가: 한 대화에 주제가 여럿 섞이면 (1) 옛 맥락이 새 답을 오염시키고 (2) 요약이
290
+ * 뭉개지고 (3) 목록의 대화 이름이 거짓말이 된다. 그걸 아는 사람은 사용자뿐이다.
291
+ */
292
+ private renderFoldNotice;
293
+ /**
294
+ * 새 대화로 — **맥락을 옮기지 않는다.** 주제를 바꾸려고 새로 시작하는 것이므로, 옮기면 그 이유가
295
+ * 사라진다(승격은 이어가려는 것이고 이건 끊으려는 것이다). 세션 생성은 호스트의 몫이다.
296
+ */
297
+ private startNewFromNotice;
298
+ /**
299
+ * 조치 제안 카드 — AI 는 제안까지, **실행은 사용자**.
300
+ *
301
+ * 왜 카드인가: 되돌릴 수 없는 현장 명령을 문장 속 링크로 흘리면 실수로 눌린다. 무엇을·왜 하는지
302
+ * 한 줄로 보이고 실행이 **분명한 한 번의 행동**이어야 한다.
303
+ * 실행 결과는 호스트가 **대화에 기록**한다 — 누가 무엇을 실행했는지가 협의 이력에 남아야 한다.
304
+ */
305
+ private renderProposals;
306
+ /**
307
+ * 제안 식별 키 — 같은 조치를 두 번 실행하지 않도록 **효과**(명령·대상·인자)로만 만든다.
308
+ *
309
+ * 인자는 **키 순서를 정렬**해 직렬화한다: 같은 인자를 다른 순서로 받았을 뿐인데 키가 달라지면
310
+ * 한 카드를 실행한 뒤에도 다른 카드가 살아 있어 **같은 명령이 두 번 나간다**(실제로 그랬다).
311
+ * 설명 문구(label·reason)는 키에 넣지 않는다 — 문구가 달라도 같은 조치다.
312
+ */
313
+ private proposalKey;
314
+ /**
315
+ * 실행은 호스트가 한다 — 이 컴포넌트는 도메인(트윈 명령 채널)을 모른다.
316
+ * 확인 대화·명령 전송·결과 기록은 호스트의 몫이고, 여기서는 중복 클릭만 막는다.
317
+ */
318
+ private executeProposal;
319
+ /**
320
+ * 접지 경고 — 답이 언급했지만 **근거에 없던** 대상을 알린다.
321
+ *
322
+ * 왜 답을 지우지 않는가: 판정은 식별자 대조라 확정이 아니다(예: 사용자가 화면에서 본 이름을
323
+ * 대화 밖에서 알고 말한 경우). 그래서 답은 그대로 보여주고, 검증되지 않았다는 사실만 붙인다 —
324
+ * 사용자가 그 대상을 실제 작업의 근거로 삼기 전에 알아야 하는 정보다.
325
+ */
326
+ private renderGroundingWarning;
264
327
  /**
265
328
  * "AI 가 이런 도구를 사용했습니다" fold-able 박스.
266
329
  *
@@ -269,6 +332,23 @@ export declare class OxBoardAIChat extends LitElement {
269
332
  * - 디버그 / 신뢰도 향상용 — 사용자가 "왜 이렇게 답했지" 의문 가질 때 즉시 검증.
270
333
  */
271
334
  private renderToolUsages;
335
+ /** 시스템 기록 펼치기/접기 — 곁줄이 길어도 대화가 가려지지 않게. */
336
+ private toggleSystemNote;
337
+ /**
338
+ * 판단 과정의 한 단계 — 턴 구분 · 도구 · 배지 · **한 줄 요약**.
339
+ *
340
+ * 원본 JSON 을 처음부터 펼쳐 두면 눈이 흐려져 정작 중요한 것(무엇이 거절됐는지, 무엇이 접혔는지)이
341
+ * 안 보인다. 한 줄로 줄이고 원본은 항목별로 눌러서 본다.
342
+ */
343
+ private renderToolStep;
344
+ /** 배지 문구 — 코드가 아니라 사람 말로. */
345
+ private outcomeLabel;
346
+ /**
347
+ * 결과 한 줄 — 무엇을 얻었는지/왜 막혔는지만. 목록에서 훑어 읽을 수 있어야 한다.
348
+ * 거절이면 사유를, 조회면 대표적인 개수를, 제안이면 무엇을 제안했는지.
349
+ */
350
+ private stepSummary;
351
+ private toggleToolStep;
272
352
  private toggleToolUsages;
273
353
  private formatJson;
274
354
  private copyMessage;