@things-factory/board-ai 10.0.0 → 10.0.2

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 (67) hide show
  1. package/client/components/board-ai-chat.ts +107 -2
  2. package/client/components/chat-echo-dedup.ts +3 -0
  3. package/client/components/chat-input-builder.ts +6 -0
  4. package/dist-client/client/components/board-ai-chat.d.ts +17 -0
  5. package/dist-client/client/components/board-ai-chat.js +102 -2
  6. package/dist-client/client/components/board-ai-chat.js.map +1 -1
  7. package/dist-client/client/components/chat-echo-dedup.d.ts +2 -0
  8. package/dist-client/client/components/chat-echo-dedup.js +1 -0
  9. package/dist-client/client/components/chat-echo-dedup.js.map +1 -1
  10. package/dist-client/client/components/chat-input-builder.d.ts +5 -0
  11. package/dist-client/client/components/chat-input-builder.js +1 -0
  12. package/dist-client/client/components/chat-input-builder.js.map +1 -1
  13. package/dist-client/server/service/agentic-loop.d.ts +18 -0
  14. package/dist-client/server/service/agentic-loop.js +10 -1
  15. package/dist-client/server/service/agentic-loop.js.map +1 -1
  16. package/dist-client/server/service/assistant.js +14 -2
  17. package/dist-client/server/service/assistant.js.map +1 -1
  18. package/dist-client/server/service/grounding.d.ts +17 -0
  19. package/dist-client/server/service/grounding.js +42 -0
  20. package/dist-client/server/service/grounding.js.map +1 -0
  21. package/dist-client/server/service/types.d.ts +16 -0
  22. package/dist-client/server/service/types.js.map +1 -1
  23. package/dist-client/tsconfig.tsbuildinfo +1 -1
  24. package/dist-server/service/agentic-loop.d.ts +18 -0
  25. package/dist-server/service/agentic-loop.js +10 -1
  26. package/dist-server/service/agentic-loop.js.map +1 -1
  27. package/dist-server/service/assistant.js +14 -2
  28. package/dist-server/service/assistant.js.map +1 -1
  29. package/dist-server/service/board-ai-resolver.d.ts +2 -0
  30. package/dist-server/service/board-ai-resolver.js +22 -1
  31. package/dist-server/service/board-ai-resolver.js.map +1 -1
  32. package/dist-server/service/chat-message/chat-message.d.ts +12 -0
  33. package/dist-server/service/chat-message/chat-message.js +23 -0
  34. package/dist-server/service/chat-message/chat-message.js.map +1 -1
  35. package/dist-server/service/chat-session/session-inbox.d.ts +26 -0
  36. package/dist-server/service/chat-session/session-inbox.js +41 -0
  37. package/dist-server/service/chat-session/session-inbox.js.map +1 -1
  38. package/dist-server/service/chat-session-participant/chat-session-participant.d.ts +11 -0
  39. package/dist-server/service/chat-session-participant/chat-session-participant.js +17 -1
  40. package/dist-server/service/chat-session-participant/chat-session-participant.js.map +1 -1
  41. package/dist-server/service/chat-session-resolver.d.ts +44 -1
  42. package/dist-server/service/chat-session-resolver.js +306 -6
  43. package/dist-server/service/chat-session-resolver.js.map +1 -1
  44. package/dist-server/service/grounding.d.ts +17 -0
  45. package/dist-server/service/grounding.js +46 -0
  46. package/dist-server/service/grounding.js.map +1 -0
  47. package/dist-server/service/types.d.ts +16 -0
  48. package/dist-server/service/types.js.map +1 -1
  49. package/dist-server/tsconfig.tsbuildinfo +1 -1
  50. package/package.json +5 -5
  51. package/server/service/agentic-loop.test.ts +63 -0
  52. package/server/service/agentic-loop.ts +28 -1
  53. package/server/service/assistant.ts +15 -2
  54. package/server/service/board-ai-resolver.ts +23 -1
  55. package/server/service/chat-message/chat-message.ts +26 -0
  56. package/server/service/chat-session/session-inbox.test.ts +69 -1
  57. package/server/service/chat-session/session-inbox.ts +45 -0
  58. package/server/service/chat-session-participant/chat-session-participant.ts +14 -0
  59. package/server/service/chat-session-resolver.ts +297 -5
  60. package/server/service/grounding.test.ts +55 -0
  61. package/server/service/grounding.ts +53 -0
  62. package/server/service/types.ts +16 -0
  63. package/translations/en.json +3 -1
  64. package/translations/ja.json +3 -1
  65. package/translations/ko.json +2 -0
  66. package/translations/ms.json +3 -1
  67. package/translations/zh.json +3 -1
@@ -15,6 +15,8 @@ export interface RemoteMessage {
15
15
  content: string;
16
16
  relatedPatchId?: string;
17
17
  toolUsagesJson?: any;
18
+ /** 접지 경고(영속) — 답이 언급했으나 근거에 없던 식별자. 나중에 읽는 사람도 같은 주의를 봐야 한다. */
19
+ groundingWarningsJson?: any;
18
20
  /** 발신자 — 표시용 name + **자기 여부 판정용 email**(내부 관리키 id 는 쓰지 않는다). */
19
21
  creator?: {
20
22
  name?: string;
@@ -16,6 +16,7 @@ export function remoteMessageToLine(m) {
16
16
  content: m.content,
17
17
  patchId: m.relatedPatchId ?? undefined,
18
18
  toolUsages: Array.isArray(m.toolUsagesJson) ? m.toolUsagesJson : undefined,
19
+ groundingWarnings: Array.isArray(m.groundingWarningsJson) ? m.groundingWarningsJson : undefined,
19
20
  senderEmail: m.creator?.email ?? undefined,
20
21
  senderName: m.creator?.name ?? undefined
21
22
  };
@@ -1 +1 @@
1
- {"version":3,"file":"chat-echo-dedup.js","sourceRoot":"","sources":["../../../client/components/chat-echo-dedup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAwBH,mBAAmB;AACnB,MAAM,UAAU,mBAAmB,CAAC,CAAgB;IAClD,OAAO;QACL,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,OAAO,EAAE,CAAC,CAAC,cAAc,IAAI,SAAS;QACtC,UAAU,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,SAAS;QAC1E,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE,KAAK,IAAI,SAAS;QAC1C,UAAU,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI,IAAI,SAAS;KACzC,CAAA;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB,CAClC,KAAkB,EAClB,IAAiB,EACjB,CAAmC;IAEnC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;QAAE,OAAO,IAAI,CAAA;IAC5B,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QAAE,OAAO,IAAI,CAAA;IAC/B,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAA;IAC9B,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAElB;;sEAEkE;IAClE,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACtB,MAAM,SAAS,GAAG,CAAC,CAAC,OAAO,EAAE,KAAK,CAAA;QAClC,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,CACzB,CAAC,CAAC,EAAE,CACF,CAAC,CAAC,IAAI,KAAK,MAAM;YACjB,CAAC,CAAC,CAAC,EAAE;YACL,CAAC,CAAC,CAAC,CAAC,QAAQ;YACZ,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,WAAW,IAAI,IAAI,IAAI,CAAC,CAAC,WAAW,KAAK,SAAS,CAAC,CACvE,CAAA;QACD,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;YACb,MAAM,SAAS,GAAG,CAAC,GAAG,KAAK,CAAC,CAAA;YAC5B,SAAS,CAAC,GAAG,CAAC,GAAG;gBACf,GAAG,SAAS,CAAC,GAAG,CAAC;gBACjB,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,WAAW,EAAE,SAAS,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,WAAW;gBACpD,UAAU,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,UAAU;aACzD,CAAA;YACD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAA;QAC7C,CAAC;IACH,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,EAAE,mBAAmB,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAA;AACtE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAC/B,KAAkB,EAClB,IAAiB,EACjB,OAAe,EACf,QAA4B,EAC5B,KAAyB;IAEzB,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAA;IACxD,IAAI,GAAG,GAAG,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;IAEnC,MAAM,SAAS,GAAG,CAAC,GAAG,KAAK,CAAC,CAAA;IAC5B;6CACyC;IACzC,IAAI,QAAQ,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC,EAAE,CAAC;QACzE,SAAS,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;QACxB,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,CAAA;IACnC,CAAC;IACD,MAAM,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAC9D,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,GAAG,CAAC,EAAE,GAAG,KAAK,EAAE,EAAE,EAAE,QAAQ,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAA;IACxG,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAA;AAC7C,CAAC","sourcesContent":["/*\n * 그룹챗 메시지 에코 dedup — 순수 로직.\n *\n * 실시간 방송은 자기 발신 메시지도 되돌려준다(에코). 또 pubsub publish 가 mutation 응답보다\n * 먼저 도착할 수 있어(레이스) optimistic 라인과 방송 라인이 겹칠 수 있다. 이 모듈은 그 조정을\n * **id 기반**으로 결정한다 (서버가 멘션 마커로 content 를 enrich 하므로 content 매칭은 불가).\n *\n * 컴포넌트 상태(this.lines / this._seenMessageIds)에서 분리 → 단위테스트 가능.\n * (C2 의 patch-apply-gate.ts 와 동일한 \"순수 게이트\" 컨벤션.)\n */\n\n/** dedup 에 필요한 최소 라인 형태 (board-ai-chat 의 ChatLine 부분집합). */\nexport interface DedupLine {\n id?: string\n _localId?: string\n role: string\n content: string\n senderEmail?: string\n senderName?: string\n [k: string]: any\n}\n\n/** 서버 방송 메시지(필요 필드만). */\nexport interface RemoteMessage {\n id?: string\n role: string\n content: string\n relatedPatchId?: string\n toolUsagesJson?: any\n /** 발신자 — 표시용 name + **자기 여부 판정용 email**(내부 관리키 id 는 쓰지 않는다). */\n creator?: { name?: string; email?: string } | null\n}\n\n/** 방송 메시지 → 라인. */\nexport function remoteMessageToLine(m: RemoteMessage): DedupLine {\n return {\n id: m.id,\n role: m.role,\n content: m.content,\n patchId: m.relatedPatchId ?? undefined,\n toolUsages: Array.isArray(m.toolUsagesJson) ? m.toolUsagesJson : undefined,\n senderEmail: m.creator?.email ?? undefined,\n senderName: m.creator?.name ?? undefined\n }\n}\n\n/**\n * 원격(방송) 메시지 수신 결과를 계산.\n * - id 없음 / 이미 seen → 변화 없음(null).\n * - **내 발신 에코**이고 아직 서버 id 를 못 받은 optimistic 라인이 있으면 → 그 라인에 id 를 입힌다.\n * - 그 밖 → seen 에 추가 + 라인 append.\n *\n * 왜 '입히기'인가: 방송은 mutation 응답보다 먼저 도착할 수 있다. 예전에는 에코를 새 줄로 붙이고\n * 중복 제거를 mutation 응답 시점(reconcileSentLine)에 했는데, 그 응답은 **LLM 이 끝난 뒤** 오므로\n * 사용자는 대기 시간 내내 자기 질문을 두 줄로 본다. 그래서 붙이지 않고 대기 중인 줄에 흡수시킨다.\n *\n * `_localId` 는 유지한다 — 이후 mutation 응답이 patch(요약·patchId 등)를 그 줄에 병합할 수 있어야 한다.\n */\nexport function receiveRemoteMessage(\n lines: DedupLine[],\n seen: Set<string>,\n m: RemoteMessage | undefined | null\n): { lines: DedupLine[]; seen: Set<string> } | null {\n if (!m || !m.id) return null\n if (seen.has(m.id)) return null\n const nextSeen = new Set(seen)\n nextSeen.add(m.id)\n\n /* 내 발신 에코 흡수 — `_localId` 가 있는 줄은 이 클라이언트가 만든 것뿐이다.\n * 발신자 판정: 양쪽 email 이 있으면 같아야 하고, 방송에 발신자 email 이 없으면(단일 사용자·구형)\n * 내 것으로 본다. 다른 사용자의 메시지가 내 대기 줄을 삼키지 않도록 email 이 다르면 흡수하지 않는다. */\n if (m.role === 'user') {\n const echoEmail = m.creator?.email\n const idx = lines.findIndex(\n l =>\n l.role === 'user' &&\n !l.id &&\n !!l._localId &&\n (!echoEmail || l.senderEmail == null || l.senderEmail === echoEmail)\n )\n if (idx >= 0) {\n const nextLines = [...lines]\n nextLines[idx] = {\n ...nextLines[idx],\n id: m.id,\n senderEmail: echoEmail ?? nextLines[idx].senderEmail,\n senderName: m.creator?.name ?? nextLines[idx].senderName\n }\n return { lines: nextLines, seen: nextSeen }\n }\n }\n\n return { lines: [...lines, remoteMessageToLine(m)], seen: nextSeen }\n}\n\n/**\n * 전송한 optimistic 라인(_localId)을 서버 id 로 reconcile.\n * - **다른 줄**이 이미 그 serverId 를 갖고 있으면(옛 append 경로) optimistic 중복 라인 제거.\n * - 아니면 serverId 부여(+ seen 등록) + patch 병합 + _localId 해제.\n * - 해당 _localId 라인이 없으면 변화 없음.\n * 반환: 새 { lines, seen } (불변).\n */\nexport function reconcileSentLine(\n lines: DedupLine[],\n seen: Set<string>,\n localId: string,\n serverId: string | undefined,\n patch: Partial<DedupLine>\n): { lines: DedupLine[]; seen: Set<string> } {\n const idx = lines.findIndex(l => l._localId === localId)\n if (idx < 0) return { lines, seen }\n\n const nextLines = [...lines]\n /* seen 만 보고 지우면, 에코를 이 줄에 **흡수**한 경우(id 가 이미 이 줄에 있다) 자기 줄을 지운다.\n * 실제 중복은 \"다른 줄이 같은 id 를 가진\" 상태일 때뿐이다. */\n if (serverId && nextLines.some((l, i) => i !== idx && l.id === serverId)) {\n nextLines.splice(idx, 1)\n return { lines: nextLines, seen }\n }\n const nextSeen = serverId ? new Set(seen).add(serverId) : seen\n nextLines[idx] = { ...nextLines[idx], ...patch, id: serverId ?? nextLines[idx].id, _localId: undefined }\n return { lines: nextLines, seen: nextSeen }\n}\n"]}
1
+ {"version":3,"file":"chat-echo-dedup.js","sourceRoot":"","sources":["../../../client/components/chat-echo-dedup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA0BH,mBAAmB;AACnB,MAAM,UAAU,mBAAmB,CAAC,CAAgB;IAClD,OAAO;QACL,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,OAAO,EAAE,CAAC,CAAC,cAAc,IAAI,SAAS;QACtC,UAAU,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,SAAS;QAC1E,iBAAiB,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,SAAS;QAC/F,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE,KAAK,IAAI,SAAS;QAC1C,UAAU,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI,IAAI,SAAS;KACzC,CAAA;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB,CAClC,KAAkB,EAClB,IAAiB,EACjB,CAAmC;IAEnC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;QAAE,OAAO,IAAI,CAAA;IAC5B,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QAAE,OAAO,IAAI,CAAA;IAC/B,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAA;IAC9B,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAElB;;sEAEkE;IAClE,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACtB,MAAM,SAAS,GAAG,CAAC,CAAC,OAAO,EAAE,KAAK,CAAA;QAClC,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,CACzB,CAAC,CAAC,EAAE,CACF,CAAC,CAAC,IAAI,KAAK,MAAM;YACjB,CAAC,CAAC,CAAC,EAAE;YACL,CAAC,CAAC,CAAC,CAAC,QAAQ;YACZ,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,WAAW,IAAI,IAAI,IAAI,CAAC,CAAC,WAAW,KAAK,SAAS,CAAC,CACvE,CAAA;QACD,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;YACb,MAAM,SAAS,GAAG,CAAC,GAAG,KAAK,CAAC,CAAA;YAC5B,SAAS,CAAC,GAAG,CAAC,GAAG;gBACf,GAAG,SAAS,CAAC,GAAG,CAAC;gBACjB,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,WAAW,EAAE,SAAS,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,WAAW;gBACpD,UAAU,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,UAAU;aACzD,CAAA;YACD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAA;QAC7C,CAAC;IACH,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,EAAE,mBAAmB,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAA;AACtE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAC/B,KAAkB,EAClB,IAAiB,EACjB,OAAe,EACf,QAA4B,EAC5B,KAAyB;IAEzB,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAA;IACxD,IAAI,GAAG,GAAG,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;IAEnC,MAAM,SAAS,GAAG,CAAC,GAAG,KAAK,CAAC,CAAA;IAC5B;6CACyC;IACzC,IAAI,QAAQ,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC,EAAE,CAAC;QACzE,SAAS,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;QACxB,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,CAAA;IACnC,CAAC;IACD,MAAM,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAC9D,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,GAAG,CAAC,EAAE,GAAG,KAAK,EAAE,EAAE,EAAE,QAAQ,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAA;IACxG,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAA;AAC7C,CAAC","sourcesContent":["/*\n * 그룹챗 메시지 에코 dedup — 순수 로직.\n *\n * 실시간 방송은 자기 발신 메시지도 되돌려준다(에코). 또 pubsub publish 가 mutation 응답보다\n * 먼저 도착할 수 있어(레이스) optimistic 라인과 방송 라인이 겹칠 수 있다. 이 모듈은 그 조정을\n * **id 기반**으로 결정한다 (서버가 멘션 마커로 content 를 enrich 하므로 content 매칭은 불가).\n *\n * 컴포넌트 상태(this.lines / this._seenMessageIds)에서 분리 → 단위테스트 가능.\n * (C2 의 patch-apply-gate.ts 와 동일한 \"순수 게이트\" 컨벤션.)\n */\n\n/** dedup 에 필요한 최소 라인 형태 (board-ai-chat 의 ChatLine 부분집합). */\nexport interface DedupLine {\n id?: string\n _localId?: string\n role: string\n content: string\n senderEmail?: string\n senderName?: string\n [k: string]: any\n}\n\n/** 서버 방송 메시지(필요 필드만). */\nexport interface RemoteMessage {\n id?: string\n role: string\n content: string\n relatedPatchId?: string\n toolUsagesJson?: any\n /** 접지 경고(영속) — 답이 언급했으나 근거에 없던 식별자. 나중에 읽는 사람도 같은 주의를 봐야 한다. */\n groundingWarningsJson?: any\n /** 발신자 — 표시용 name + **자기 여부 판정용 email**(내부 관리키 id 는 쓰지 않는다). */\n creator?: { name?: string; email?: string } | null\n}\n\n/** 방송 메시지 → 라인. */\nexport function remoteMessageToLine(m: RemoteMessage): DedupLine {\n return {\n id: m.id,\n role: m.role,\n content: m.content,\n patchId: m.relatedPatchId ?? undefined,\n toolUsages: Array.isArray(m.toolUsagesJson) ? m.toolUsagesJson : undefined,\n groundingWarnings: Array.isArray(m.groundingWarningsJson) ? m.groundingWarningsJson : undefined,\n senderEmail: m.creator?.email ?? undefined,\n senderName: m.creator?.name ?? undefined\n }\n}\n\n/**\n * 원격(방송) 메시지 수신 결과를 계산.\n * - id 없음 / 이미 seen → 변화 없음(null).\n * - **내 발신 에코**이고 아직 서버 id 를 못 받은 optimistic 라인이 있으면 → 그 라인에 id 를 입힌다.\n * - 그 밖 → seen 에 추가 + 라인 append.\n *\n * 왜 '입히기'인가: 방송은 mutation 응답보다 먼저 도착할 수 있다. 예전에는 에코를 새 줄로 붙이고\n * 중복 제거를 mutation 응답 시점(reconcileSentLine)에 했는데, 그 응답은 **LLM 이 끝난 뒤** 오므로\n * 사용자는 대기 시간 내내 자기 질문을 두 줄로 본다. 그래서 붙이지 않고 대기 중인 줄에 흡수시킨다.\n *\n * `_localId` 는 유지한다 — 이후 mutation 응답이 patch(요약·patchId 등)를 그 줄에 병합할 수 있어야 한다.\n */\nexport function receiveRemoteMessage(\n lines: DedupLine[],\n seen: Set<string>,\n m: RemoteMessage | undefined | null\n): { lines: DedupLine[]; seen: Set<string> } | null {\n if (!m || !m.id) return null\n if (seen.has(m.id)) return null\n const nextSeen = new Set(seen)\n nextSeen.add(m.id)\n\n /* 내 발신 에코 흡수 — `_localId` 가 있는 줄은 이 클라이언트가 만든 것뿐이다.\n * 발신자 판정: 양쪽 email 이 있으면 같아야 하고, 방송에 발신자 email 이 없으면(단일 사용자·구형)\n * 내 것으로 본다. 다른 사용자의 메시지가 내 대기 줄을 삼키지 않도록 email 이 다르면 흡수하지 않는다. */\n if (m.role === 'user') {\n const echoEmail = m.creator?.email\n const idx = lines.findIndex(\n l =>\n l.role === 'user' &&\n !l.id &&\n !!l._localId &&\n (!echoEmail || l.senderEmail == null || l.senderEmail === echoEmail)\n )\n if (idx >= 0) {\n const nextLines = [...lines]\n nextLines[idx] = {\n ...nextLines[idx],\n id: m.id,\n senderEmail: echoEmail ?? nextLines[idx].senderEmail,\n senderName: m.creator?.name ?? nextLines[idx].senderName\n }\n return { lines: nextLines, seen: nextSeen }\n }\n }\n\n return { lines: [...lines, remoteMessageToLine(m)], seen: nextSeen }\n}\n\n/**\n * 전송한 optimistic 라인(_localId)을 서버 id 로 reconcile.\n * - **다른 줄**이 이미 그 serverId 를 갖고 있으면(옛 append 경로) optimistic 중복 라인 제거.\n * - 아니면 serverId 부여(+ seen 등록) + patch 병합 + _localId 해제.\n * - 해당 _localId 라인이 없으면 변화 없음.\n * 반환: 새 { lines, seen } (불변).\n */\nexport function reconcileSentLine(\n lines: DedupLine[],\n seen: Set<string>,\n localId: string,\n serverId: string | undefined,\n patch: Partial<DedupLine>\n): { lines: DedupLine[]; seen: Set<string> } {\n const idx = lines.findIndex(l => l._localId === localId)\n if (idx < 0) return { lines, seen }\n\n const nextLines = [...lines]\n /* seen 만 보고 지우면, 에코를 이 줄에 **흡수**한 경우(id 가 이미 이 줄에 있다) 자기 줄을 지운다.\n * 실제 중복은 \"다른 줄이 같은 id 를 가진\" 상태일 때뿐이다. */\n if (serverId && nextLines.some((l, i) => i !== idx && l.id === serverId)) {\n nextLines.splice(idx, 1)\n return { lines: nextLines, seen }\n }\n const nextSeen = serverId ? new Set(seen).add(serverId) : seen\n nextLines[idx] = { ...nextLines[idx], ...patch, id: serverId ?? nextLines[idx].id, _localId: undefined }\n return { lines: nextLines, seen: nextSeen }\n}\n"]}
@@ -23,6 +23,11 @@ export interface ChatMutationInputArgs {
23
23
  toolCategories?: string[];
24
24
  /** 코어 보드 편집 도구(addComponent·스타일링·씬 조작) 사용 여부. 기본 true. */
25
25
  boardTools?: boolean;
26
+ /**
27
+ * 첫 턴에 도구 호출을 강제할지 — 라이브 상태를 다루는 대화면에서 켠다.
28
+ * 근거 없이 상태를 단언하는 답(도구를 아예 부르지 않은 답)을 원천에서 막는다.
29
+ */
30
+ requireGroundingTools?: boolean;
26
31
  knownTypes?: string[];
27
32
  categories?: string[];
28
33
  componentSchemas?: any;
@@ -21,6 +21,7 @@ export function buildChatMutationInput(args) {
21
21
  /* 미지정은 서버 기본(전부 허용)에 맡긴다 — null 을 보내 굳이 덮어쓰지 않는다. */
22
22
  ...(args.toolCategories ? { toolCategories: args.toolCategories } : {}),
23
23
  ...(args.boardTools === false ? { boardTools: false } : {}),
24
+ ...(args.requireGroundingTools ? { requireGroundingTools: true } : {}),
24
25
  knownTypes: args.knownTypes,
25
26
  categories: args.categories,
26
27
  componentSchemas: args.componentSchemas,
@@ -1 +1 @@
1
- {"version":3,"file":"chat-input-builder.js","sourceRoot":"","sources":["../../../client/components/chat-input-builder.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAkCH;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAA2B;IAChE,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;QACjC,QAAQ,EAAE,IAAI,CAAC,OAAO;QACtB,YAAY,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;QACpC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,GAAG,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC,CAAC,EAAE,sBAAsB,EAAE,IAAI,CAAC,sBAAsB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/F,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9D,qDAAqD;QACrD,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvE,GAAG,CAAC,IAAI,CAAC,UAAU,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3D,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;QACvC,cAAc,EAAE,IAAI,CAAC,cAAc;QACnC,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;KAC9E,CAAA;AACH,CAAC","sourcesContent":["/**\n * boardAIChat GraphQL mutation 의 input 객체 빌더.\n *\n * board-ai-chat.ts (Lit / Material Web 의존) 와 분리 — pure 함수만 두면 jest 의 node\n * 환경에서도 import 가능. 회귀 테스트 (selectedRefids 누락 등) 가 단위 테스트로 잠긴다.\n */\n\nexport interface ChatMutationInputArgs {\n sessionId?: string\n history: Array<{ role: string; content: string }>\n liveBoard: any\n scopes?: string[]\n /** 편집으로 접은 지점(마지막으로 남긴 서버 메시지 id) — 서버가 그 뒤 이력을 에이전트 문맥에서 뺀다. */\n truncateAfterMessageId?: string\n /** 이 대화면이 무엇을 보고 있는지(예: { spaceId }) — 도구가 대상을 알 수 있게. 없으면 모델이 식별자를 되묻는다. */\n hostContext?: any\n /**\n * 이 대화면이 쓸 수 있는 외부 도구 카테고리(registerToolCategory 이름).\n * 미지정 = 등록된 전부(현행 동작). 지정하면 정의와 **실행** 모두 그 범위로 제한된다.\n */\n toolCategories?: string[]\n /** 코어 보드 편집 도구(addComponent·스타일링·씬 조작) 사용 여부. 기본 true. */\n boardTools?: boolean\n knownTypes?: string[]\n categories?: string[]\n componentSchemas?: any\n /**\n * 선택된 컴포넌트의 `refid` 배열 — things-scene universal numeric handle.\n * id (데이터 바인딩 이름) 와는 다른 개념.\n */\n selectedRefids: number[]\n /**\n * popup 에서 명시 선택한 #mention 매핑 — {token, refid}.\n * 사용자 textarea 에는 #token 만 보이고, 정확한 refid 는 이 별도 채널로 server 에 전달.\n * server 가 mention map 으로 LLM 에 정확한 refid 정보 첨부 (사용자 노출 0).\n */\n mentions?: Array<{ token: string; refid: number }>\n}\n\n/**\n * GraphQL mutation 의 input 객체를 빌드.\n *\n * 핵심 보호 — `selectedRefids` 가 항상 포함되어야 한다. 누락이 사용자에게 보고된\n * \"AI 가 선택된 컴포넌트를 모른다\" 의 root cause 였다.\n */\nexport function buildChatMutationInput(args: ChatMutationInputArgs): Record<string, any> {\n return {\n sessionId: args.sessionId ?? null,\n messages: args.history,\n currentBoard: args.liveBoard ?? null,\n scopes: args.scopes,\n ...(args.truncateAfterMessageId ? { truncateAfterMessageId: args.truncateAfterMessageId } : {}),\n ...(args.hostContext ? { hostContext: args.hostContext } : {}),\n /* 미지정은 서버 기본(전부 허용)에 맡긴다 — null 을 보내 굳이 덮어쓰지 않는다. */\n ...(args.toolCategories ? { toolCategories: args.toolCategories } : {}),\n ...(args.boardTools === false ? { boardTools: false } : {}),\n knownTypes: args.knownTypes,\n categories: args.categories,\n componentSchemas: args.componentSchemas,\n selectedRefids: args.selectedRefids,\n ...(args.mentions && args.mentions.length > 0 && { mentions: args.mentions })\n }\n}\n"]}
1
+ {"version":3,"file":"chat-input-builder.js","sourceRoot":"","sources":["../../../client/components/chat-input-builder.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAuCH;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAA2B;IAChE,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;QACjC,QAAQ,EAAE,IAAI,CAAC,OAAO;QACtB,YAAY,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;QACpC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,GAAG,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC,CAAC,EAAE,sBAAsB,EAAE,IAAI,CAAC,sBAAsB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/F,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9D,qDAAqD;QACrD,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvE,GAAG,CAAC,IAAI,CAAC,UAAU,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3D,GAAG,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC,CAAC,EAAE,qBAAqB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtE,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;QACvC,cAAc,EAAE,IAAI,CAAC,cAAc;QACnC,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;KAC9E,CAAA;AACH,CAAC","sourcesContent":["/**\n * boardAIChat GraphQL mutation 의 input 객체 빌더.\n *\n * board-ai-chat.ts (Lit / Material Web 의존) 와 분리 — pure 함수만 두면 jest 의 node\n * 환경에서도 import 가능. 회귀 테스트 (selectedRefids 누락 등) 가 단위 테스트로 잠긴다.\n */\n\nexport interface ChatMutationInputArgs {\n sessionId?: string\n history: Array<{ role: string; content: string }>\n liveBoard: any\n scopes?: string[]\n /** 편집으로 접은 지점(마지막으로 남긴 서버 메시지 id) — 서버가 그 뒤 이력을 에이전트 문맥에서 뺀다. */\n truncateAfterMessageId?: string\n /** 이 대화면이 무엇을 보고 있는지(예: { spaceId }) — 도구가 대상을 알 수 있게. 없으면 모델이 식별자를 되묻는다. */\n hostContext?: any\n /**\n * 이 대화면이 쓸 수 있는 외부 도구 카테고리(registerToolCategory 이름).\n * 미지정 = 등록된 전부(현행 동작). 지정하면 정의와 **실행** 모두 그 범위로 제한된다.\n */\n toolCategories?: string[]\n /** 코어 보드 편집 도구(addComponent·스타일링·씬 조작) 사용 여부. 기본 true. */\n boardTools?: boolean\n /**\n * 첫 턴에 도구 호출을 강제할지 — 라이브 상태를 다루는 대화면에서 켠다.\n * 근거 없이 상태를 단언하는 답(도구를 아예 부르지 않은 답)을 원천에서 막는다.\n */\n requireGroundingTools?: boolean\n knownTypes?: string[]\n categories?: string[]\n componentSchemas?: any\n /**\n * 선택된 컴포넌트의 `refid` 배열 — things-scene universal numeric handle.\n * id (데이터 바인딩 이름) 와는 다른 개념.\n */\n selectedRefids: number[]\n /**\n * popup 에서 명시 선택한 #mention 매핑 — {token, refid}.\n * 사용자 textarea 에는 #token 만 보이고, 정확한 refid 는 이 별도 채널로 server 에 전달.\n * server 가 mention map 으로 LLM 에 정확한 refid 정보 첨부 (사용자 노출 0).\n */\n mentions?: Array<{ token: string; refid: number }>\n}\n\n/**\n * GraphQL mutation 의 input 객체를 빌드.\n *\n * 핵심 보호 — `selectedRefids` 가 항상 포함되어야 한다. 누락이 사용자에게 보고된\n * \"AI 가 선택된 컴포넌트를 모른다\" 의 root cause 였다.\n */\nexport function buildChatMutationInput(args: ChatMutationInputArgs): Record<string, any> {\n return {\n sessionId: args.sessionId ?? null,\n messages: args.history,\n currentBoard: args.liveBoard ?? null,\n scopes: args.scopes,\n ...(args.truncateAfterMessageId ? { truncateAfterMessageId: args.truncateAfterMessageId } : {}),\n ...(args.hostContext ? { hostContext: args.hostContext } : {}),\n /* 미지정은 서버 기본(전부 허용)에 맡긴다 — null 을 보내 굳이 덮어쓰지 않는다. */\n ...(args.toolCategories ? { toolCategories: args.toolCategories } : {}),\n ...(args.boardTools === false ? { boardTools: false } : {}),\n ...(args.requireGroundingTools ? { requireGroundingTools: true } : {}),\n knownTypes: args.knownTypes,\n categories: args.categories,\n componentSchemas: args.componentSchemas,\n selectedRefids: args.selectedRefids,\n ...(args.mentions && args.mentions.length > 0 && { mentions: args.mentions })\n }\n}\n"]}
@@ -38,6 +38,17 @@ export interface AgenticLoopOptions {
38
38
  * LLM 이 같은 잘못을 무한 반복하는 경우 방어.
39
39
  */
40
40
  repeatedFailureLimit?: number;
41
+ /**
42
+ * **첫 턴에 도구 호출을 강제**한다(toolChoice: 'required'). 두 번째 턴부터는 자유(auto).
43
+ *
44
+ * 왜: 접지 가드는 답이 지어낸 **식별자**를 잡지만, 식별자 없이 지어낸 **상황 서술**은 잡지 못한다
45
+ * ("지게차들이 바쁘게 움직이고 있다" — 실제로 이 답이 나왔다). 그건 도구를 아예 부르지 않고 답한
46
+ * 경우이고, 사후 검사로는 막을 수 없다. 첫 턴에 무엇이든 조회하게 만들면 근거 없이 상태를 단언할
47
+ * 길이 없어진다 — 개념·사용법 질문도 문서 조회 도구로 답하므로 막히지 않는다.
48
+ *
49
+ * 대화면이 정한다(라이브 상태를 다루는 면에서만 켠다) — 여기서 질문 내용을 눈치로 분류하지 않는다.
50
+ */
51
+ requireToolOnFirstTurn?: boolean;
41
52
  }
42
53
  export interface AgenticLoopInput {
43
54
  initialConversation: AIMessage[];
@@ -54,6 +65,13 @@ export interface AgenticLoopResult {
54
65
  accumulatedWriteCalls: AIToolCall[];
55
66
  accumulatedActions: BoardActionOp[];
56
67
  toolUsages: ToolUsage[];
68
+ /**
69
+ * LLM 에게 실제로 회신된 도구 결과 문자열(시간순) — **접지 판정의 근거집합**.
70
+ *
71
+ * `toolUsages[].result` 를 쓰면 안 된다: 그건 UI 표시용으로 압축된(잘린) 값이라 근거로 삼으면
72
+ * 잘려 나간 식별자를 "근거 없음" 으로 오판한다. 여기 담기는 것은 tool_result 로 보낸 원문 그대로다.
73
+ */
74
+ groundedTexts: string[];
57
75
  /** LLM 마지막 응답의 stopReason. provider-specific 문자열. */
58
76
  stopReason: string;
59
77
  /**
@@ -31,6 +31,8 @@ export async function runAgenticLoop(input) {
31
31
  const accumulatedWriteCalls = [];
32
32
  const accumulatedActions = [];
33
33
  const toolUsages = [];
34
+ /* 접지 근거 — tool_result 로 LLM 에 보낸 원문(압축 전). 호출부가 답의 접지를 검사한다. */
35
+ const groundedTexts = [];
34
36
  let lastText;
35
37
  let stopReason = 'end_turn';
36
38
  let abortReason;
@@ -54,7 +56,8 @@ export async function runAgenticLoop(input) {
54
56
  model: options.model,
55
57
  temperature: options.temperature,
56
58
  maxTokens: options.maxTokens,
57
- toolChoice: 'auto',
59
+ /* 첫 턴만 강제 — 이후는 자유. 계속 강제하면 조회 결과를 받아 놓고도 답을 못 쓰고 도구를 또 부른다. */
60
+ toolChoice: iter === 0 && options.requireToolOnFirstTurn ? 'required' : 'auto',
58
61
  allowParallelToolCalls: true,
59
62
  signal
60
63
  });
@@ -85,6 +88,11 @@ export async function runAgenticLoop(input) {
85
88
  // tool 결과 빌드 — read 는 즉시 실행, write 는 누적, action 은 별도 채널
86
89
  const beforeUsages = toolUsages.length;
87
90
  const toolResultParts = await processToolCalls(result.toolCalls, currentBoard, selectedRefids, dispatch, accumulatedWriteCalls, accumulatedActions, toolUsages);
91
+ /* 접지 근거 누적 — 모델이 tool_result 로 받은 원문 그대로. 압축 전 값이어야 한다(위 필드 주석). */
92
+ for (const part of toolResultParts) {
93
+ if (typeof part?.content === 'string')
94
+ groundedTexts.push(part.content);
95
+ }
88
96
  // (4) 같은 tool 의 validation 실패 연속 감지 — LLM 이 같은 잘못 반복 시 abort
89
97
  const newUsages = toolUsages.slice(beforeUsages);
90
98
  const repeatedFailureAbort = detectRepeatedValidationFailure(newUsages, lastFailedToolName, consecutiveFailures, failureLimit);
@@ -130,6 +138,7 @@ export async function runAgenticLoop(input) {
130
138
  accumulatedWriteCalls,
131
139
  accumulatedActions,
132
140
  toolUsages,
141
+ groundedTexts,
133
142
  stopReason,
134
143
  abortReason
135
144
  };
@@ -1 +1 @@
1
- {"version":3,"file":"agentic-loop.js","sourceRoot":"","sources":["../../../server/service/agentic-loop.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * agentic-loop — multi-turn LLM ↔ tool 루프.\n *\n * assistant.ts 에서 추출 — loop 자체를 *순수 함수* 로 만들어:\n * 1. 단위 테스트 가능 (assistant.ts 의 board-import → @aws-sdk transitive 경로 회피)\n * 2. 관측성 (error boundary / iter log / AbortSignal / repeated-failure 조기 종료)\n * 을 한 곳에 응집\n * 3. 향후 split (Phase 3) 의 자연 경계\n *\n * 입력 — `chat` 함수와 dispatch helper 들을 의존성 주입. board-ai 본체는 본 모듈에\n * 의존하지만 본 모듈은 board-ai 의 어떤 무거운 import 도 모름.\n */\nimport debug from 'debug'\nimport type {\n AIMessage,\n AIToolCall,\n AIToolChatResult\n} from '@things-factory/ai-client-base'\nimport type { BoardModel } from '@things-factory/board-import'\nimport type { BoardActionOp, ToolUsage } from './types'\n\nconst log = debug('things-factory:board-ai:loop')\n\n// ── Public types ─────────────────────────────────────────────────\n\n/**\n * Loop 가 호출하는 dispatch 함수들. 모두 의존성 주입.\n *\n * assistant.ts 가 자기 자신의 함수 (isReadTool, executeReadTool 등) 를 그대로\n * 넘겨주는 구조. 본 모듈은 이 함수들의 *시그니처* 만 알면 됨.\n */\nexport interface ToolDispatchHelpers {\n isReadTool: (name: string) => boolean\n isWriteTool: (name: string) => boolean\n isActionTool: (name: string) => boolean\n /** read/external 도구 실행. **Promise 를 반환할 수 있다** — 호출부는 반드시 await 한다. */\n executeReadTool: (\n tc: AIToolCall,\n board: BoardModel | undefined,\n selectedRefids: number[]\n ) => any | Promise<any>\n validateWriteToolCall: (\n tc: AIToolCall,\n components: any[]\n ) => { valid: boolean; errors?: any[]; suggestion?: string }\n toolCallToBoardActionOp: (tc: AIToolCall) => BoardActionOp | null\n summarizeToolResult: (value: any) => any\n}\n\nexport interface AgenticLoopOptions {\n systemPrompt: string\n model?: string\n temperature?: number\n maxTokens?: number\n /** 최대 iteration. 기본 8. */\n maxIterations?: number\n /**\n * 사용자 abort 신호. iteration 시작 직전 / chat() 호출 후 체크.\n * 이미 abort 된 신호로 들어오면 첫 iteration 도 안 돔.\n */\n signal?: AbortSignal\n /**\n * 같은 tool 의 validation 실패가 N회 연속이면 조기 종료. 기본 3.\n * LLM 이 같은 잘못을 무한 반복하는 경우 방어.\n */\n repeatedFailureLimit?: number\n}\n\nexport interface AgenticLoopInput {\n initialConversation: AIMessage[]\n tools: any[]\n options: AgenticLoopOptions\n /** LLM provider 의 chatWithTools 위임. provider-specific 구현. */\n chat: (\n messages: AIMessage[],\n tools: any[],\n options: any\n ) => Promise<AIToolChatResult>\n dispatch: ToolDispatchHelpers\n currentBoard: BoardModel | undefined\n selectedRefids: number[]\n}\n\nexport interface AgenticLoopResult {\n lastText: string | undefined\n accumulatedWriteCalls: AIToolCall[]\n accumulatedActions: BoardActionOp[]\n toolUsages: ToolUsage[]\n /** LLM 마지막 응답의 stopReason. provider-specific 문자열. */\n stopReason: string\n /**\n * 비정상 종료 사유 — 정상 종료 시 undefined.\n * 호출자 (assistant.ts) 가 사용자 메시지에 반영 (예: '시도 중 오류가 발생했어요').\n */\n abortReason?:\n | { type: 'provider_error'; message: string; iter: number }\n | { type: 'aborted'; iter: number }\n | { type: 'max_iterations'; iter: number }\n | { type: 'repeated_validation_failure'; toolName: string; consecutive: number; iter: number }\n}\n\n// ── Loop ─────────────────────────────────────────────────────────\n\nconst DEFAULT_MAX_ITERATIONS = 8\n\n/**\n * multi-turn agentic loop.\n *\n * LLM 이 read tool 을 호출 → 서버에서 즉시 실행 → 결과 회신 → LLM 추론 반복.\n * write tool 은 누적해 client 에 patch 로 전달. 무한 루프 방지를 위해 iteration cap.\n *\n * 본 함수는 *순수* — input 만 보고 결정. 외부 mutable state 없음.\n */\nexport async function runAgenticLoop(\n input: AgenticLoopInput\n): Promise<AgenticLoopResult> {\n const {\n initialConversation,\n tools,\n options,\n chat,\n dispatch,\n currentBoard,\n selectedRefids\n } = input\n\n const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS\n const failureLimit = options.repeatedFailureLimit ?? 3\n const signal = options.signal\n\n let conversation = initialConversation\n const accumulatedWriteCalls: AIToolCall[] = []\n const accumulatedActions: BoardActionOp[] = []\n const toolUsages: ToolUsage[] = []\n let lastText: string | undefined\n let stopReason: string = 'end_turn'\n let abortReason: AgenticLoopResult['abortReason']\n\n // 같은 tool 의 validation 실패 연속 카운터 — 다른 tool 호출 시 reset\n let lastFailedToolName: string | undefined\n let consecutiveFailures = 0\n\n let iter = 0\n for (; iter < maxIterations; iter++) {\n // (1) AbortSignal — iteration 시작 시 체크\n if (signal?.aborted) {\n log('iter %d: aborted by signal', iter)\n abortReason = { type: 'aborted', iter }\n break\n }\n\n log('iter %d: chat() 호출', iter)\n let result: AIToolChatResult\n try {\n // (2) Error boundary — provider 예외 시 graceful 종료\n result = await chat(conversation, tools, {\n systemPrompt: options.systemPrompt,\n model: options.model,\n temperature: options.temperature,\n maxTokens: options.maxTokens,\n toolChoice: 'auto',\n allowParallelToolCalls: true,\n signal\n })\n } catch (e: any) {\n // AbortError — 별도 분류\n if (e?.name === 'AbortError' || signal?.aborted) {\n log('iter %d: provider AbortError', iter)\n abortReason = { type: 'aborted', iter }\n } else {\n log('iter %d: provider error: %s', iter, e?.message)\n abortReason = {\n type: 'provider_error',\n message: e?.message ?? String(e),\n iter\n }\n }\n break\n }\n\n lastText = result.text || lastText\n stopReason = result.stopReason\n\n log(\n 'iter %d: text=%s, toolCalls=%d, stopReason=%s',\n iter,\n result.text ? `\"${result.text.slice(0, 60)}...\"` : '(none)',\n result.toolCalls.length,\n result.stopReason\n )\n\n if (result.toolCalls.length === 0) break\n\n // (1b) 삭제: chat() 직후 abort 는 다음 iter 시작 시 (1) 에서 감지.\n // 현재 iter tool 실행을 중단하면 read tool 결과도 버려져 테스트/동작 불일치.\n\n // tool 결과 빌드 — read 는 즉시 실행, write 는 누적, action 은 별도 채널\n const beforeUsages = toolUsages.length\n const toolResultParts = await processToolCalls(\n result.toolCalls,\n currentBoard,\n selectedRefids,\n dispatch,\n accumulatedWriteCalls,\n accumulatedActions,\n toolUsages\n )\n\n // (4) 같은 tool 의 validation 실패 연속 감지 — LLM 이 같은 잘못 반복 시 abort\n const newUsages = toolUsages.slice(beforeUsages)\n const repeatedFailureAbort = detectRepeatedValidationFailure(\n newUsages,\n lastFailedToolName,\n consecutiveFailures,\n failureLimit\n )\n if (repeatedFailureAbort.shouldAbort) {\n log(\n 'iter %d: repeated validation failure on %s (%d consecutive) — abort',\n iter,\n repeatedFailureAbort.toolName,\n repeatedFailureAbort.consecutive\n )\n abortReason = {\n type: 'repeated_validation_failure',\n toolName: repeatedFailureAbort.toolName,\n consecutive: repeatedFailureAbort.consecutive,\n iter\n }\n break\n }\n lastFailedToolName = repeatedFailureAbort.lastFailedToolName\n consecutiveFailures = repeatedFailureAbort.consecutive\n\n // assistant 의 tool_use turn + user 의 tool_result turn 을 conversation 에 누적\n const assistantContent: any[] = []\n if (result.text) assistantContent.push({ type: 'text', text: result.text })\n for (const tc of result.toolCalls) {\n // providerMeta (Gemini thoughtSignature 등) 를 그대로 round-trip — 안 보내면 400.\n assistantContent.push({\n type: 'tool_use',\n id: tc.id,\n name: tc.name,\n arguments: tc.arguments,\n ...(tc.providerMeta && { providerMeta: tc.providerMeta })\n })\n }\n\n conversation = [\n ...conversation,\n { role: 'assistant', content: assistantContent },\n { role: 'user', content: toolResultParts }\n ]\n }\n\n // (3) max iterations 도달 시 사용자에게 표면화\n if (iter >= maxIterations && !abortReason) {\n log('iter %d: max iterations reached', iter)\n abortReason = { type: 'max_iterations', iter }\n }\n\n return {\n lastText,\n accumulatedWriteCalls,\n accumulatedActions,\n toolUsages,\n stopReason,\n abortReason\n }\n}\n\n/**\n * 같은 tool 의 validation 실패 연속 감지 — pure helper.\n *\n * 같은 tool 이 반복 fail 하면 LLM 이 self-correct 못 하는 상태.\n * 다른 tool 이 fail 하면 시퀀스 새로 시작 (전혀 다른 패턴이라).\n * tool 이 succeed 하면 카운터 reset.\n */\nfunction detectRepeatedValidationFailure(\n newUsages: ToolUsage[],\n lastFailedToolName: string | undefined,\n consecutiveFailures: number,\n limit: number\n): {\n shouldAbort: boolean\n toolName: string\n consecutive: number\n lastFailedToolName: string | undefined\n} {\n // 이번 iteration 에서 실패한 write tool 들 (validation 거절)\n const failed = newUsages.filter(\n u => u.kind === 'write' && (u.result as any)?.error === 'Tool args failed validation'\n )\n\n if (failed.length === 0) {\n // 실패 없음 — reset\n return {\n shouldAbort: false,\n toolName: '',\n consecutive: 0,\n lastFailedToolName: undefined\n }\n }\n\n // 모든 실패가 같은 tool 인지 확인\n const firstName = failed[0].name\n const allSame = failed.every(u => u.name === firstName)\n\n if (allSame && firstName === lastFailedToolName) {\n const consecutive = consecutiveFailures + 1\n return {\n shouldAbort: consecutive >= limit,\n toolName: firstName,\n consecutive,\n lastFailedToolName: firstName\n }\n }\n\n // 다른 tool 또는 첫 실패\n return {\n shouldAbort: false,\n toolName: firstName,\n consecutive: 1,\n lastFailedToolName: firstName\n }\n}\n\n/**\n * 한 iteration 의 tool 결과를 분류·실행·누적. (loop 에서 분리해 읽기 쉽게.)\n */\nasync function processToolCalls(\n toolCalls: AIToolCall[],\n currentBoard: BoardModel | undefined,\n selectedRefids: number[],\n dispatch: ToolDispatchHelpers,\n accumulatedWriteCalls: AIToolCall[],\n accumulatedActions: BoardActionOp[],\n toolUsages: ToolUsage[]\n): Promise<any[]> {\n const toolResultParts: any[] = []\n\n for (const tc of toolCalls) {\n if (dispatch.isReadTool(tc.name)) {\n /* await 필수 — registry 에 등록된 read/external builder 는 async 일 수 있다(DB·외부 호출).\n * await 없이 JSON.stringify 하면 Promise 가 '{}' 로 직렬화되어 LLM 이 **빈 결과**를 받는다\n * (board-import 의 getImportSession/importBoardAsync, operato-twin 의 getTwinStructure 등). */\n const value = await dispatch.executeReadTool(tc, currentBoard, selectedRefids)\n toolResultParts.push({\n type: 'tool_result',\n toolUseId: tc.id,\n content: JSON.stringify(value)\n })\n toolUsages.push({\n name: tc.name,\n arguments: tc.arguments ?? {},\n result: dispatch.summarizeToolResult(value),\n kind: 'read'\n })\n } else if (dispatch.isWriteTool(tc.name)) {\n // 사전 검증 — args 가 schema / build 함수 검증 통과해야 누적.\n // 실패 시 LLM 한테 detailed error 반환 → 다음 turn 에서 자기 회복 (retry).\n const components = (currentBoard?.components ?? []) as any[]\n const validation = dispatch.validateWriteToolCall(tc, components)\n if (!validation.valid) {\n const errorResult = {\n error: 'Tool args failed validation',\n issues: validation.errors,\n suggestion: validation.suggestion,\n note: 'Fix the args and call the tool again.'\n }\n toolResultParts.push({\n type: 'tool_result',\n toolUseId: tc.id,\n content: JSON.stringify(errorResult)\n })\n toolUsages.push({\n name: tc.name,\n arguments: tc.arguments ?? {},\n result: errorResult,\n kind: 'write'\n })\n // continue — 누적 안 함. LLM 이 다음 turn 에서 정정 호출.\n } else {\n accumulatedWriteCalls.push(tc)\n const queuedResult = { queued: true, note: 'Will be applied on the client.' }\n toolResultParts.push({\n type: 'tool_result',\n toolUseId: tc.id,\n content: JSON.stringify(queuedResult)\n })\n toolUsages.push({\n name: tc.name,\n arguments: tc.arguments ?? {},\n result: queuedResult,\n kind: 'write'\n })\n }\n } else if (dispatch.isActionTool(tc.name)) {\n // ephemeral scene 조작 — accumulatedActions 로 누적, ChatResponse.actions 로 client 전달\n const action = dispatch.toolCallToBoardActionOp(tc)\n if (action) accumulatedActions.push(action)\n const queuedResult = {\n queued: true,\n note: 'Will be executed on the modeller (ephemeral).'\n }\n toolResultParts.push({\n type: 'tool_result',\n toolUseId: tc.id,\n content: JSON.stringify(queuedResult)\n })\n toolUsages.push({\n name: tc.name,\n arguments: tc.arguments ?? {},\n result: queuedResult,\n kind: 'write'\n })\n } else {\n const errResult = { error: `Unknown tool: ${tc.name}` }\n toolResultParts.push({\n type: 'tool_result',\n toolUseId: tc.id,\n content: JSON.stringify(errResult),\n isError: true\n })\n toolUsages.push({\n name: tc.name,\n arguments: tc.arguments ?? {},\n result: errResult,\n kind: 'unknown'\n })\n }\n }\n\n return toolResultParts\n}\n"]}
1
+ {"version":3,"file":"agentic-loop.js","sourceRoot":"","sources":["../../../server/service/agentic-loop.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * agentic-loop — multi-turn LLM ↔ tool 루프.\n *\n * assistant.ts 에서 추출 — loop 자체를 *순수 함수* 로 만들어:\n * 1. 단위 테스트 가능 (assistant.ts 의 board-import → @aws-sdk transitive 경로 회피)\n * 2. 관측성 (error boundary / iter log / AbortSignal / repeated-failure 조기 종료)\n * 을 한 곳에 응집\n * 3. 향후 split (Phase 3) 의 자연 경계\n *\n * 입력 — `chat` 함수와 dispatch helper 들을 의존성 주입. board-ai 본체는 본 모듈에\n * 의존하지만 본 모듈은 board-ai 의 어떤 무거운 import 도 모름.\n */\nimport debug from 'debug'\nimport type {\n AIMessage,\n AIToolCall,\n AIToolChatResult\n} from '@things-factory/ai-client-base'\nimport type { BoardModel } from '@things-factory/board-import'\nimport type { BoardActionOp, ToolUsage } from './types'\n\nconst log = debug('things-factory:board-ai:loop')\n\n// ── Public types ─────────────────────────────────────────────────\n\n/**\n * Loop 가 호출하는 dispatch 함수들. 모두 의존성 주입.\n *\n * assistant.ts 가 자기 자신의 함수 (isReadTool, executeReadTool 등) 를 그대로\n * 넘겨주는 구조. 본 모듈은 이 함수들의 *시그니처* 만 알면 됨.\n */\nexport interface ToolDispatchHelpers {\n isReadTool: (name: string) => boolean\n isWriteTool: (name: string) => boolean\n isActionTool: (name: string) => boolean\n /** read/external 도구 실행. **Promise 를 반환할 수 있다** — 호출부는 반드시 await 한다. */\n executeReadTool: (\n tc: AIToolCall,\n board: BoardModel | undefined,\n selectedRefids: number[]\n ) => any | Promise<any>\n validateWriteToolCall: (\n tc: AIToolCall,\n components: any[]\n ) => { valid: boolean; errors?: any[]; suggestion?: string }\n toolCallToBoardActionOp: (tc: AIToolCall) => BoardActionOp | null\n summarizeToolResult: (value: any) => any\n}\n\nexport interface AgenticLoopOptions {\n systemPrompt: string\n model?: string\n temperature?: number\n maxTokens?: number\n /** 최대 iteration. 기본 8. */\n maxIterations?: number\n /**\n * 사용자 abort 신호. iteration 시작 직전 / chat() 호출 후 체크.\n * 이미 abort 된 신호로 들어오면 첫 iteration 도 안 돔.\n */\n signal?: AbortSignal\n /**\n * 같은 tool 의 validation 실패가 N회 연속이면 조기 종료. 기본 3.\n * LLM 이 같은 잘못을 무한 반복하는 경우 방어.\n */\n repeatedFailureLimit?: number\n /**\n * **첫 턴에 도구 호출을 강제**한다(toolChoice: 'required'). 두 번째 턴부터는 자유(auto).\n *\n * 왜: 접지 가드는 답이 지어낸 **식별자**를 잡지만, 식별자 없이 지어낸 **상황 서술**은 잡지 못한다\n * (\"지게차들이 바쁘게 움직이고 있다\" — 실제로 이 답이 나왔다). 그건 도구를 아예 부르지 않고 답한\n * 경우이고, 사후 검사로는 막을 수 없다. 첫 턴에 무엇이든 조회하게 만들면 근거 없이 상태를 단언할\n * 길이 없어진다 — 개념·사용법 질문도 문서 조회 도구로 답하므로 막히지 않는다.\n *\n * 대화면이 정한다(라이브 상태를 다루는 면에서만 켠다) — 여기서 질문 내용을 눈치로 분류하지 않는다.\n */\n requireToolOnFirstTurn?: boolean\n}\n\nexport interface AgenticLoopInput {\n initialConversation: AIMessage[]\n tools: any[]\n options: AgenticLoopOptions\n /** LLM provider 의 chatWithTools 위임. provider-specific 구현. */\n chat: (\n messages: AIMessage[],\n tools: any[],\n options: any\n ) => Promise<AIToolChatResult>\n dispatch: ToolDispatchHelpers\n currentBoard: BoardModel | undefined\n selectedRefids: number[]\n}\n\nexport interface AgenticLoopResult {\n lastText: string | undefined\n accumulatedWriteCalls: AIToolCall[]\n accumulatedActions: BoardActionOp[]\n toolUsages: ToolUsage[]\n /**\n * LLM 에게 실제로 회신된 도구 결과 문자열(시간순) — **접지 판정의 근거집합**.\n *\n * `toolUsages[].result` 를 쓰면 안 된다: 그건 UI 표시용으로 압축된(잘린) 값이라 근거로 삼으면\n * 잘려 나간 식별자를 \"근거 없음\" 으로 오판한다. 여기 담기는 것은 tool_result 로 보낸 원문 그대로다.\n */\n groundedTexts: string[]\n /** LLM 마지막 응답의 stopReason. provider-specific 문자열. */\n stopReason: string\n /**\n * 비정상 종료 사유 — 정상 종료 시 undefined.\n * 호출자 (assistant.ts) 가 사용자 메시지에 반영 (예: '시도 중 오류가 발생했어요').\n */\n abortReason?:\n | { type: 'provider_error'; message: string; iter: number }\n | { type: 'aborted'; iter: number }\n | { type: 'max_iterations'; iter: number }\n | { type: 'repeated_validation_failure'; toolName: string; consecutive: number; iter: number }\n}\n\n// ── Loop ─────────────────────────────────────────────────────────\n\nconst DEFAULT_MAX_ITERATIONS = 8\n\n/**\n * multi-turn agentic loop.\n *\n * LLM 이 read tool 을 호출 → 서버에서 즉시 실행 → 결과 회신 → LLM 추론 반복.\n * write tool 은 누적해 client 에 patch 로 전달. 무한 루프 방지를 위해 iteration cap.\n *\n * 본 함수는 *순수* — input 만 보고 결정. 외부 mutable state 없음.\n */\nexport async function runAgenticLoop(\n input: AgenticLoopInput\n): Promise<AgenticLoopResult> {\n const {\n initialConversation,\n tools,\n options,\n chat,\n dispatch,\n currentBoard,\n selectedRefids\n } = input\n\n const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS\n const failureLimit = options.repeatedFailureLimit ?? 3\n const signal = options.signal\n\n let conversation = initialConversation\n const accumulatedWriteCalls: AIToolCall[] = []\n const accumulatedActions: BoardActionOp[] = []\n const toolUsages: ToolUsage[] = []\n /* 접지 근거 — tool_result 로 LLM 에 보낸 원문(압축 전). 호출부가 답의 접지를 검사한다. */\n const groundedTexts: string[] = []\n let lastText: string | undefined\n let stopReason: string = 'end_turn'\n let abortReason: AgenticLoopResult['abortReason']\n\n // 같은 tool 의 validation 실패 연속 카운터 — 다른 tool 호출 시 reset\n let lastFailedToolName: string | undefined\n let consecutiveFailures = 0\n\n let iter = 0\n for (; iter < maxIterations; iter++) {\n // (1) AbortSignal — iteration 시작 시 체크\n if (signal?.aborted) {\n log('iter %d: aborted by signal', iter)\n abortReason = { type: 'aborted', iter }\n break\n }\n\n log('iter %d: chat() 호출', iter)\n let result: AIToolChatResult\n try {\n // (2) Error boundary — provider 예외 시 graceful 종료\n result = await chat(conversation, tools, {\n systemPrompt: options.systemPrompt,\n model: options.model,\n temperature: options.temperature,\n maxTokens: options.maxTokens,\n /* 첫 턴만 강제 — 이후는 자유. 계속 강제하면 조회 결과를 받아 놓고도 답을 못 쓰고 도구를 또 부른다. */\n toolChoice: iter === 0 && options.requireToolOnFirstTurn ? 'required' : 'auto',\n allowParallelToolCalls: true,\n signal\n })\n } catch (e: any) {\n // AbortError — 별도 분류\n if (e?.name === 'AbortError' || signal?.aborted) {\n log('iter %d: provider AbortError', iter)\n abortReason = { type: 'aborted', iter }\n } else {\n log('iter %d: provider error: %s', iter, e?.message)\n abortReason = {\n type: 'provider_error',\n message: e?.message ?? String(e),\n iter\n }\n }\n break\n }\n\n lastText = result.text || lastText\n stopReason = result.stopReason\n\n log(\n 'iter %d: text=%s, toolCalls=%d, stopReason=%s',\n iter,\n result.text ? `\"${result.text.slice(0, 60)}...\"` : '(none)',\n result.toolCalls.length,\n result.stopReason\n )\n\n if (result.toolCalls.length === 0) break\n\n // (1b) 삭제: chat() 직후 abort 는 다음 iter 시작 시 (1) 에서 감지.\n // 현재 iter tool 실행을 중단하면 read tool 결과도 버려져 테스트/동작 불일치.\n\n // tool 결과 빌드 — read 는 즉시 실행, write 는 누적, action 은 별도 채널\n const beforeUsages = toolUsages.length\n const toolResultParts = await processToolCalls(\n result.toolCalls,\n currentBoard,\n selectedRefids,\n dispatch,\n accumulatedWriteCalls,\n accumulatedActions,\n toolUsages\n )\n\n /* 접지 근거 누적 — 모델이 tool_result 로 받은 원문 그대로. 압축 전 값이어야 한다(위 필드 주석). */\n for (const part of toolResultParts) {\n if (typeof part?.content === 'string') groundedTexts.push(part.content)\n }\n\n // (4) 같은 tool 의 validation 실패 연속 감지 — LLM 이 같은 잘못 반복 시 abort\n const newUsages = toolUsages.slice(beforeUsages)\n const repeatedFailureAbort = detectRepeatedValidationFailure(\n newUsages,\n lastFailedToolName,\n consecutiveFailures,\n failureLimit\n )\n if (repeatedFailureAbort.shouldAbort) {\n log(\n 'iter %d: repeated validation failure on %s (%d consecutive) — abort',\n iter,\n repeatedFailureAbort.toolName,\n repeatedFailureAbort.consecutive\n )\n abortReason = {\n type: 'repeated_validation_failure',\n toolName: repeatedFailureAbort.toolName,\n consecutive: repeatedFailureAbort.consecutive,\n iter\n }\n break\n }\n lastFailedToolName = repeatedFailureAbort.lastFailedToolName\n consecutiveFailures = repeatedFailureAbort.consecutive\n\n // assistant 의 tool_use turn + user 의 tool_result turn 을 conversation 에 누적\n const assistantContent: any[] = []\n if (result.text) assistantContent.push({ type: 'text', text: result.text })\n for (const tc of result.toolCalls) {\n // providerMeta (Gemini thoughtSignature 등) 를 그대로 round-trip — 안 보내면 400.\n assistantContent.push({\n type: 'tool_use',\n id: tc.id,\n name: tc.name,\n arguments: tc.arguments,\n ...(tc.providerMeta && { providerMeta: tc.providerMeta })\n })\n }\n\n conversation = [\n ...conversation,\n { role: 'assistant', content: assistantContent },\n { role: 'user', content: toolResultParts }\n ]\n }\n\n // (3) max iterations 도달 시 사용자에게 표면화\n if (iter >= maxIterations && !abortReason) {\n log('iter %d: max iterations reached', iter)\n abortReason = { type: 'max_iterations', iter }\n }\n\n return {\n lastText,\n accumulatedWriteCalls,\n accumulatedActions,\n toolUsages,\n groundedTexts,\n stopReason,\n abortReason\n }\n}\n\n/**\n * 같은 tool 의 validation 실패 연속 감지 — pure helper.\n *\n * 같은 tool 이 반복 fail 하면 LLM 이 self-correct 못 하는 상태.\n * 다른 tool 이 fail 하면 시퀀스 새로 시작 (전혀 다른 패턴이라).\n * tool 이 succeed 하면 카운터 reset.\n */\nfunction detectRepeatedValidationFailure(\n newUsages: ToolUsage[],\n lastFailedToolName: string | undefined,\n consecutiveFailures: number,\n limit: number\n): {\n shouldAbort: boolean\n toolName: string\n consecutive: number\n lastFailedToolName: string | undefined\n} {\n // 이번 iteration 에서 실패한 write tool 들 (validation 거절)\n const failed = newUsages.filter(\n u => u.kind === 'write' && (u.result as any)?.error === 'Tool args failed validation'\n )\n\n if (failed.length === 0) {\n // 실패 없음 — reset\n return {\n shouldAbort: false,\n toolName: '',\n consecutive: 0,\n lastFailedToolName: undefined\n }\n }\n\n // 모든 실패가 같은 tool 인지 확인\n const firstName = failed[0].name\n const allSame = failed.every(u => u.name === firstName)\n\n if (allSame && firstName === lastFailedToolName) {\n const consecutive = consecutiveFailures + 1\n return {\n shouldAbort: consecutive >= limit,\n toolName: firstName,\n consecutive,\n lastFailedToolName: firstName\n }\n }\n\n // 다른 tool 또는 첫 실패\n return {\n shouldAbort: false,\n toolName: firstName,\n consecutive: 1,\n lastFailedToolName: firstName\n }\n}\n\n/**\n * 한 iteration 의 tool 결과를 분류·실행·누적. (loop 에서 분리해 읽기 쉽게.)\n */\nasync function processToolCalls(\n toolCalls: AIToolCall[],\n currentBoard: BoardModel | undefined,\n selectedRefids: number[],\n dispatch: ToolDispatchHelpers,\n accumulatedWriteCalls: AIToolCall[],\n accumulatedActions: BoardActionOp[],\n toolUsages: ToolUsage[]\n): Promise<any[]> {\n const toolResultParts: any[] = []\n\n for (const tc of toolCalls) {\n if (dispatch.isReadTool(tc.name)) {\n /* await 필수 — registry 에 등록된 read/external builder 는 async 일 수 있다(DB·외부 호출).\n * await 없이 JSON.stringify 하면 Promise 가 '{}' 로 직렬화되어 LLM 이 **빈 결과**를 받는다\n * (board-import 의 getImportSession/importBoardAsync, operato-twin 의 getTwinStructure 등). */\n const value = await dispatch.executeReadTool(tc, currentBoard, selectedRefids)\n toolResultParts.push({\n type: 'tool_result',\n toolUseId: tc.id,\n content: JSON.stringify(value)\n })\n toolUsages.push({\n name: tc.name,\n arguments: tc.arguments ?? {},\n result: dispatch.summarizeToolResult(value),\n kind: 'read'\n })\n } else if (dispatch.isWriteTool(tc.name)) {\n // 사전 검증 — args 가 schema / build 함수 검증 통과해야 누적.\n // 실패 시 LLM 한테 detailed error 반환 → 다음 turn 에서 자기 회복 (retry).\n const components = (currentBoard?.components ?? []) as any[]\n const validation = dispatch.validateWriteToolCall(tc, components)\n if (!validation.valid) {\n const errorResult = {\n error: 'Tool args failed validation',\n issues: validation.errors,\n suggestion: validation.suggestion,\n note: 'Fix the args and call the tool again.'\n }\n toolResultParts.push({\n type: 'tool_result',\n toolUseId: tc.id,\n content: JSON.stringify(errorResult)\n })\n toolUsages.push({\n name: tc.name,\n arguments: tc.arguments ?? {},\n result: errorResult,\n kind: 'write'\n })\n // continue — 누적 안 함. LLM 이 다음 turn 에서 정정 호출.\n } else {\n accumulatedWriteCalls.push(tc)\n const queuedResult = { queued: true, note: 'Will be applied on the client.' }\n toolResultParts.push({\n type: 'tool_result',\n toolUseId: tc.id,\n content: JSON.stringify(queuedResult)\n })\n toolUsages.push({\n name: tc.name,\n arguments: tc.arguments ?? {},\n result: queuedResult,\n kind: 'write'\n })\n }\n } else if (dispatch.isActionTool(tc.name)) {\n // ephemeral scene 조작 — accumulatedActions 로 누적, ChatResponse.actions 로 client 전달\n const action = dispatch.toolCallToBoardActionOp(tc)\n if (action) accumulatedActions.push(action)\n const queuedResult = {\n queued: true,\n note: 'Will be executed on the modeller (ephemeral).'\n }\n toolResultParts.push({\n type: 'tool_result',\n toolUseId: tc.id,\n content: JSON.stringify(queuedResult)\n })\n toolUsages.push({\n name: tc.name,\n arguments: tc.arguments ?? {},\n result: queuedResult,\n kind: 'write'\n })\n } else {\n const errResult = { error: `Unknown tool: ${tc.name}` }\n toolResultParts.push({\n type: 'tool_result',\n toolUseId: tc.id,\n content: JSON.stringify(errResult),\n isError: true\n })\n toolUsages.push({\n name: tc.name,\n arguments: tc.arguments ?? {},\n result: errResult,\n kind: 'unknown'\n })\n }\n }\n\n return toolResultParts\n}\n"]}
@@ -6,6 +6,7 @@ import { resolveCatalogMentions } from './catalog-resolver';
6
6
  // 직접 import 안 함 — 신규 styling tool 추가 시 registry 에만 spec 추가하면 됨.
7
7
  import { buildComponentStyleSummary } from './styling/read-tools';
8
8
  import { runAgenticLoop } from './agentic-loop';
9
+ import { checkReplyGrounding } from './grounding';
9
10
  import { collectAllComponents, findComponentByRefid } from './component-tree';
10
11
  import { isStylingTool, dispatchStylingTool, buildStylingToolDefinitions } from './styling/registry.js';
11
12
  import { getToolSpecsFor, getToolKind, findToolSpec } from '@things-factory/ai-client-base';
@@ -137,7 +138,9 @@ export class DefaultBoardAIAssistant {
137
138
  systemPrompt,
138
139
  model: options.model,
139
140
  temperature: options.temperature,
140
- maxTokens: options.maxTokens
141
+ maxTokens: options.maxTokens,
142
+ /* 라이브 상태를 다루는 대화면은 첫 턴에 조회를 강제한다 — 근거 없이 상태를 단언할 길을 막는다. */
143
+ requireToolOnFirstTurn: options.requireGroundingTools
141
144
  },
142
145
  chat: (msgs, tools, opts) => this.base.chatWithTools(msgs, tools, opts),
143
146
  dispatch: {
@@ -179,12 +182,21 @@ export class DefaultBoardAIAssistant {
179
182
  confidence: 0.9
180
183
  };
181
184
  }
185
+ /* 접지 검사 — 답이 **모델이 받은 것** 밖의 식별자를 지목했는지. 답은 고치지 않고 경고만 낸다.
186
+ * 근거집합의 정의와 사정거리는 ./grounding 머리말 참조(식별자 없는 창작 서술은 잡지 못한다). */
187
+ const warnings = checkReplyGrounding(baseReply, {
188
+ systemPrompt,
189
+ boardContext,
190
+ messages: messages.map(m => (typeof m.content === 'string' ? m.content : '')),
191
+ toolResults: loopResult.groundedTexts
192
+ });
182
193
  return {
183
194
  reply,
184
195
  patch,
185
196
  followUp: undefined,
186
197
  toolUsages: toolUsages.length > 0 ? toolUsages : undefined,
187
- actions: accumulatedActions.length > 0 ? accumulatedActions : undefined
198
+ actions: accumulatedActions.length > 0 ? accumulatedActions : undefined,
199
+ groundingWarnings: warnings.length > 0 ? warnings : undefined
188
200
  };
189
201
  }
190
202
  resolveTypesAndCategories(options) {