@things-factory/ai-assistant 10.1.27 โ†’ 10.1.29

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 (210) hide show
  1. package/client/components/assistant-chat.ts +701 -63
  2. package/client/components/assistant-mode.test.ts +105 -55
  3. package/client/components/assistant-mode.ts +144 -82
  4. package/client/components/chat-echo-dedup.test.ts +23 -0
  5. package/client/components/chat-echo-dedup.ts +29 -3
  6. package/client/components/chat-input-builder.test.ts +20 -20
  7. package/client/components/chat-input-builder.ts +16 -14
  8. package/client/components/chat-proposal-survival.test.ts +73 -0
  9. package/client/components/mention-popup.ts +31 -8
  10. package/client/components/panel-aria.test.ts +54 -0
  11. package/client/components/proposal-outcome-line.ts +33 -0
  12. package/client/utils/assistant-session-controller.test.ts +75 -0
  13. package/client/utils/assistant-session-controller.ts +37 -1
  14. package/client/utils/assistant-session-transport.ts +13 -5
  15. package/dist-client/components/assistant-chat.d.ts +140 -13
  16. package/dist-client/components/assistant-chat.js +659 -71
  17. package/dist-client/components/assistant-chat.js.map +1 -1
  18. package/dist-client/components/assistant-mode.d.ts +102 -68
  19. package/dist-client/components/assistant-mode.js +79 -19
  20. package/dist-client/components/assistant-mode.js.map +1 -1
  21. package/dist-client/components/assistant-mode.test.js +90 -53
  22. package/dist-client/components/assistant-mode.test.js.map +1 -1
  23. package/dist-client/components/chat-echo-dedup.d.ts +2 -0
  24. package/dist-client/components/chat-echo-dedup.js +27 -4
  25. package/dist-client/components/chat-echo-dedup.js.map +1 -1
  26. package/dist-client/components/chat-echo-dedup.test.js +20 -0
  27. package/dist-client/components/chat-echo-dedup.test.js.map +1 -1
  28. package/dist-client/components/chat-input-builder.d.ts +12 -7
  29. package/dist-client/components/chat-input-builder.js +8 -10
  30. package/dist-client/components/chat-input-builder.js.map +1 -1
  31. package/dist-client/components/chat-input-builder.test.js +17 -18
  32. package/dist-client/components/chat-input-builder.test.js.map +1 -1
  33. package/dist-client/components/chat-proposal-survival.test.d.ts +1 -0
  34. package/dist-client/components/chat-proposal-survival.test.js +61 -0
  35. package/dist-client/components/chat-proposal-survival.test.js.map +1 -0
  36. package/dist-client/components/mention-popup.js +28 -8
  37. package/dist-client/components/mention-popup.js.map +1 -1
  38. package/dist-client/components/panel-aria.test.d.ts +1 -0
  39. package/dist-client/components/panel-aria.test.js +49 -0
  40. package/dist-client/components/panel-aria.test.js.map +1 -0
  41. package/dist-client/components/proposal-outcome-line.d.ts +9 -0
  42. package/dist-client/components/proposal-outcome-line.js +15 -0
  43. package/dist-client/components/proposal-outcome-line.js.map +1 -0
  44. package/dist-client/tsconfig.tsbuildinfo +1 -1
  45. package/dist-client/utils/assistant-session-controller.d.ts +20 -0
  46. package/dist-client/utils/assistant-session-controller.js +16 -0
  47. package/dist-client/utils/assistant-session-controller.js.map +1 -1
  48. package/dist-client/utils/assistant-session-controller.test.d.ts +1 -0
  49. package/dist-client/utils/assistant-session-controller.test.js +56 -0
  50. package/dist-client/utils/assistant-session-controller.test.js.map +1 -0
  51. package/dist-client/utils/assistant-session-transport.js +13 -5
  52. package/dist-client/utils/assistant-session-transport.js.map +1 -1
  53. package/dist-server/door-declaration-report.d.ts +14 -0
  54. package/dist-server/door-declaration-report.js +54 -0
  55. package/dist-server/door-declaration-report.js.map +1 -0
  56. package/dist-server/service/assistant-capability-resolver.d.ts +17 -0
  57. package/dist-server/service/assistant-capability-resolver.js +88 -0
  58. package/dist-server/service/assistant-capability-resolver.js.map +1 -0
  59. package/dist-server/service/assistant-chat-resolver.d.ts +6 -6
  60. package/dist-server/service/assistant-chat-resolver.js +185 -44
  61. package/dist-server/service/assistant-chat-resolver.js.map +1 -1
  62. package/dist-server/service/assistant-mode/assistant-mode-resolver.d.ts +33 -0
  63. package/dist-server/service/assistant-mode/assistant-mode-resolver.js +148 -0
  64. package/dist-server/service/assistant-mode/assistant-mode-resolver.js.map +1 -0
  65. package/dist-server/service/assistant-mode/index.d.ts +9 -0
  66. package/dist-server/service/assistant-mode/index.js +19 -0
  67. package/dist-server/service/assistant-mode/index.js.map +1 -0
  68. package/dist-server/service/assistant-mode/registry.d.ts +18 -0
  69. package/dist-server/service/assistant-mode/registry.js +136 -0
  70. package/dist-server/service/assistant-mode/registry.js.map +1 -0
  71. package/dist-server/service/assistant-mode/resolve.d.ts +43 -0
  72. package/dist-server/service/assistant-mode/resolve.js +65 -0
  73. package/dist-server/service/assistant-mode/resolve.js.map +1 -0
  74. package/dist-server/service/assistant-mode/types.d.ts +120 -0
  75. package/dist-server/service/assistant-mode/types.js +12 -0
  76. package/dist-server/service/assistant-mode/types.js.map +1 -0
  77. package/dist-server/service/chat-message/carried-turns.d.ts +48 -0
  78. package/dist-server/service/chat-message/carried-turns.js +92 -0
  79. package/dist-server/service/chat-message/carried-turns.js.map +1 -0
  80. package/dist-server/service/chat-message/chat-message-subscription.d.ts +1 -1
  81. package/dist-server/service/chat-message/chat-message-subscription.js +24 -6
  82. package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -1
  83. package/dist-server/service/chat-message/chat-message.d.ts +32 -0
  84. package/dist-server/service/chat-message/chat-message.js +17 -0
  85. package/dist-server/service/chat-message/chat-message.js.map +1 -1
  86. package/dist-server/service/chat-message/door-access.d.ts +27 -0
  87. package/dist-server/service/chat-message/door-access.js +34 -0
  88. package/dist-server/service/chat-message/door-access.js.map +1 -0
  89. package/dist-server/service/chat-message/fold-session-history.d.ts +30 -0
  90. package/dist-server/service/chat-message/fold-session-history.js +133 -0
  91. package/dist-server/service/chat-message/fold-session-history.js.map +1 -0
  92. package/dist-server/service/chat-message/llm-history.d.ts +7 -0
  93. package/dist-server/service/chat-message/llm-history.js +23 -11
  94. package/dist-server/service/chat-message/llm-history.js.map +1 -1
  95. package/dist-server/service/chat-message/message-doors.d.ts +29 -0
  96. package/dist-server/service/chat-message/message-doors.js +107 -0
  97. package/dist-server/service/chat-message/message-doors.js.map +1 -0
  98. package/dist-server/service/chat-message/proposal-outcome.d.ts +18 -0
  99. package/dist-server/service/chat-message/proposal-outcome.js +31 -0
  100. package/dist-server/service/chat-message/proposal-outcome.js.map +1 -0
  101. package/dist-server/service/chat-message/reader-gate.d.ts +3 -0
  102. package/dist-server/service/chat-message/reader-gate.js +39 -0
  103. package/dist-server/service/chat-message/reader-gate.js.map +1 -0
  104. package/dist-server/service/chat-message/room-message.d.ts +4 -0
  105. package/dist-server/service/chat-message/room-message.js +25 -0
  106. package/dist-server/service/chat-message/room-message.js.map +1 -0
  107. package/dist-server/service/chat-session/chat-session.d.ts +36 -0
  108. package/dist-server/service/chat-session/chat-session.js +27 -2
  109. package/dist-server/service/chat-session/chat-session.js.map +1 -1
  110. package/dist-server/service/chat-session/session-anchor.d.ts +10 -1
  111. package/dist-server/service/chat-session/session-anchor.js +12 -4
  112. package/dist-server/service/chat-session/session-anchor.js.map +1 -1
  113. package/dist-server/service/chat-session/session-inbox.d.ts +6 -1
  114. package/dist-server/service/chat-session/session-inbox.js +3 -2
  115. package/dist-server/service/chat-session/session-inbox.js.map +1 -1
  116. package/dist-server/service/chat-session/session-kind.d.ts +14 -0
  117. package/dist-server/service/chat-session/session-kind.js +30 -0
  118. package/dist-server/service/chat-session/session-kind.js.map +1 -0
  119. package/dist-server/service/chat-session/session-visibility.d.ts +15 -0
  120. package/dist-server/service/chat-session/session-visibility.js +35 -0
  121. package/dist-server/service/chat-session/session-visibility.js.map +1 -0
  122. package/dist-server/service/chat-session/station-session-guard.d.ts +1 -5
  123. package/dist-server/service/chat-session/station-session-guard.js +7 -2
  124. package/dist-server/service/chat-session/station-session-guard.js.map +1 -1
  125. package/dist-server/service/chat-session-participant/mention-invite.d.ts +29 -0
  126. package/dist-server/service/chat-session-participant/mention-invite.js +103 -0
  127. package/dist-server/service/chat-session-participant/mention-invite.js.map +1 -0
  128. package/dist-server/service/chat-session-resolver.d.ts +17 -0
  129. package/dist-server/service/chat-session-resolver.js +271 -15
  130. package/dist-server/service/chat-session-resolver.js.map +1 -1
  131. package/dist-server/service/grounding-sources.d.ts +14 -0
  132. package/dist-server/service/grounding-sources.js +26 -0
  133. package/dist-server/service/grounding-sources.js.map +1 -0
  134. package/dist-server/service/index.d.ts +9 -1
  135. package/dist-server/service/index.js +21 -0
  136. package/dist-server/service/index.js.map +1 -1
  137. package/dist-server/service/request-translator.d.ts +2 -0
  138. package/dist-server/service/request-translator.js +20 -0
  139. package/dist-server/service/request-translator.js.map +1 -0
  140. package/dist-server/service/testing/conversation-script.d.ts +123 -0
  141. package/dist-server/service/testing/conversation-script.js +202 -0
  142. package/dist-server/service/testing/conversation-script.js.map +1 -0
  143. package/dist-server/service/testing/mode-conformance.d.ts +50 -0
  144. package/dist-server/service/testing/mode-conformance.js +103 -0
  145. package/dist-server/service/testing/mode-conformance.js.map +1 -0
  146. package/dist-server/tool-registry-boot-report.d.ts +3 -0
  147. package/dist-server/tool-registry-boot-report.js +32 -2
  148. package/dist-server/tool-registry-boot-report.js.map +1 -1
  149. package/dist-server/tsconfig.tsbuildinfo +1 -1
  150. package/package.json +5 -5
  151. package/server/door-declaration-report.test.ts +53 -0
  152. package/server/door-declaration-report.ts +61 -0
  153. package/server/explicit-graphql-types.test.ts +56 -0
  154. package/server/service/assistant-capability-resolver.ts +66 -0
  155. package/server/service/assistant-chat-resolver.ts +205 -42
  156. package/server/service/assistant-mode/assistant-mode-resolver.ts +108 -0
  157. package/server/service/assistant-mode/index.ts +16 -0
  158. package/server/service/assistant-mode/registry.test.ts +105 -0
  159. package/server/service/assistant-mode/registry.ts +147 -0
  160. package/server/service/assistant-mode/resolve.test.ts +120 -0
  161. package/server/service/assistant-mode/resolve.ts +105 -0
  162. package/server/service/assistant-mode/subject-defaults.test.ts +86 -0
  163. package/server/service/assistant-mode/types.ts +121 -0
  164. package/server/service/chat-message/carried-turns.test.ts +80 -0
  165. package/server/service/chat-message/carried-turns.ts +112 -0
  166. package/server/service/chat-message/chat-message-subscription.ts +20 -4
  167. package/server/service/chat-message/chat-message.ts +44 -0
  168. package/server/service/chat-message/door-access.test.ts +72 -0
  169. package/server/service/chat-message/door-access.ts +65 -0
  170. package/server/service/chat-message/fold-history-window.test.ts +184 -0
  171. package/server/service/chat-message/fold-session-history.test.ts +98 -0
  172. package/server/service/chat-message/fold-session-history.ts +115 -0
  173. package/server/service/chat-message/llm-history.test.ts +58 -7
  174. package/server/service/chat-message/llm-history.ts +30 -11
  175. package/server/service/chat-message/message-doors.test.ts +87 -0
  176. package/server/service/chat-message/message-doors.ts +117 -0
  177. package/server/service/chat-message/proposal-outcome.test.ts +48 -0
  178. package/server/service/chat-message/proposal-outcome.ts +42 -0
  179. package/server/service/chat-message/reader-gate.ts +44 -0
  180. package/server/service/chat-message/room-message.test.ts +21 -0
  181. package/server/service/chat-message/room-message.ts +22 -0
  182. package/server/service/chat-session/chat-session.ts +46 -1
  183. package/server/service/chat-session/session-anchor.ts +12 -3
  184. package/server/service/chat-session/session-inbox.test.ts +18 -0
  185. package/server/service/chat-session/session-inbox.ts +11 -2
  186. package/server/service/chat-session/session-kind.test.ts +24 -0
  187. package/server/service/chat-session/session-kind.ts +32 -0
  188. package/server/service/chat-session/session-visibility.test.ts +52 -0
  189. package/server/service/chat-session/session-visibility.ts +34 -0
  190. package/server/service/chat-session/station-session-guard.ts +10 -2
  191. package/server/service/chat-session-participant/mention-invite.test.ts +125 -0
  192. package/server/service/chat-session-participant/mention-invite.ts +121 -0
  193. package/server/service/chat-session-resolver.ts +276 -15
  194. package/server/service/grounding-sources.test.ts +55 -0
  195. package/server/service/grounding-sources.ts +46 -0
  196. package/server/service/index.ts +21 -0
  197. package/server/service/request-translator.test.ts +45 -0
  198. package/server/service/request-translator.ts +33 -0
  199. package/server/service/station-session-guard.test.ts +12 -5
  200. package/server/service/testing/conversation-scenario.test.ts +163 -0
  201. package/server/service/testing/conversation-script.test.ts +131 -0
  202. package/server/service/testing/conversation-script.ts +288 -0
  203. package/server/service/testing/mode-conformance.ts +122 -0
  204. package/server/tool-registry-boot-report.ts +31 -2
  205. package/test/assistant-chat-call-sites.test.ts +82 -0
  206. package/translations/en.json +46 -3
  207. package/translations/ja.json +46 -3
  208. package/translations/ko.json +46 -3
  209. package/translations/ms.json +46 -3
  210. package/translations/zh.json +46 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/ai-assistant",
3
- "version": "10.1.27",
3
+ "version": "10.1.29",
4
4
  "main": "dist-server/index.js",
5
5
  "things-factory": true,
6
6
  "author": "heartyoh",
@@ -29,10 +29,10 @@
29
29
  "@operato/graphql": "^10.0.0",
30
30
  "@operato/i18n": "^10.0.0",
31
31
  "@operato/styles": "^10.0.0",
32
- "@things-factory/ai-client-base": "^10.1.26",
33
- "@things-factory/auth-base": "^10.1.27",
32
+ "@things-factory/ai-client-base": "^10.1.29",
33
+ "@things-factory/auth-base": "^10.1.29",
34
34
  "@things-factory/env": "^10.1.20",
35
- "@things-factory/shell": "^10.1.27",
35
+ "@things-factory/shell": "^10.1.29",
36
36
  "dompurify": "^3.0.0",
37
37
  "graphql-scalars": "^1.22.0",
38
38
  "graphql-tag": "^2.12.6",
@@ -44,5 +44,5 @@
44
44
  "copyfiles": "^2.4.1",
45
45
  "rimraf": "^5.0.0"
46
46
  },
47
- "gitHead": "d04dc762cacf2aac8ba0b0f5d768eca365fc09da"
47
+ "gitHead": "e6bf9dec003fe5e794746aaeb5852b3b50478232"
48
48
  }
@@ -0,0 +1,53 @@
1
+ /*
2
+ * ๋„๊ตฌ๊ฐ€ ์ ์–ด ๋‘” ๋ฌธ์ด ์Šคํ‚ค๋งˆ์— ์žˆ๋Š”๊ฐ€ โ€” ADR-0072 ๊ฒฐ์ • 5 ์˜ ํ‘œ๋กœ ๋ฌป๋Š”๋‹ค.
3
+ *
4
+ * ์˜คํƒ€ ํ•˜๋‚˜๊ฐ€ ์กฐ์šฉํ•˜๋‹ค. ADR-0073 ํŒ์ • 1 ์€ ๋ชจ๋ฅด๋Š” ๋ฌธ์„ ๋‹ซ์œผ๋ฏ€๋กœ, ์ด๋ฆ„์„ ํ‹€๋ฆฌ๊ฒŒ ์ ์€ ๋„๊ตฌ๋กœ ๋งŒ๋“  ๋‹ต์€
5
+ * **์•„๋ฌด๋„ ๋ชป ์ฝ๋Š”๋‹ค** โ€” ์“ด ์‚ฌ๋žŒ๊นŒ์ง€. ๋ถ€ํŒ…์ด ๊ทธ๊ฒƒ์„ ๋งํ•˜์ง€ ์•Š์œผ๋ฉด ํ™”๋ฉด์—์„œ ์ฒ˜์Œ ๋ณธ๋‹ค.
6
+ */
7
+ import { doorDeclarationWarnings, formatDoorDeclarationWarnings } from './door-declaration-report'
8
+
9
+ const table: Record<string, any> = {
10
+ 'Query.jobOrders': { known: true, gate: { category: 'job-order', privilege: 'query', owner: false, super: false } },
11
+ 'Query.twinExternalAttentions': { known: true, gate: null }
12
+ }
13
+ const lookup = (typeName: string, fieldName: string) => table[`${typeName}.${fieldName}`] ?? { known: false }
14
+
15
+ describe('declared doors, measured against the schema', () => {
16
+ it('โ˜… ์Šคํ‚ค๋งˆ์— ์—†๋Š” ๋ฌธ์€ ๋งํ•œ๋‹ค โ€” ์˜คํƒ€ ํ•˜๋‚˜๊ฐ€ ๊ทธ ๋Œ€ํ™”์˜ ๋‹ต์„ ํ†ต์งธ๋กœ ๊ฐ€๋ฆฐ๋‹ค', () => {
17
+ const found = doorDeclarationWarnings([{ name: 'findJobOrders', doors: ['Query.jobOrder'] }], lookup)
18
+
19
+ expect(found).toEqual([{ code: 'tool-door-not-in-schema', tool: 'findJobOrders', door: 'Query.jobOrder' }])
20
+ })
21
+
22
+ it('โ˜… ์„ ์–ธ์ด ์—†๋Š” ๋ฌธ์€ ์‚ฌ์‹ค๋กœ ๋งํ•œ๋‹ค โ€” ๊ทธ ๋ฌธ์ด ์—ด๋ ค ์žˆ์œผ๋‹ˆ ๋‹ต๋„ ์—ด๋ฆฐ๋‹ค', () => {
23
+ const found = doorDeclarationWarnings(
24
+ [{ name: 'listExternalAttentions', doors: ['Query.twinExternalAttentions'] }],
25
+ lookup
26
+ )
27
+
28
+ expect(found.map(w => w.code)).toEqual(['tool-door-has-no-privilege'])
29
+ })
30
+
31
+ it('์ง€ํ‚ค๋Š” ๋ฌธ์—๋Š” ์•„๋ฌด ๋ง๋„ ํ•˜์ง€ ์•Š๋Š”๋‹ค', () => {
32
+ expect(doorDeclarationWarnings([{ name: 'findJobOrders', doors: ['Query.jobOrders'] }], lookup)).toEqual([])
33
+ })
34
+
35
+ it('๋ฌธ์„ ์•ˆ ์ ์€ ๋„๊ตฌ๋Š” ์ด ๊ฒ€์‚ฌ์˜ ๊ฒƒ์ด ์•„๋‹ˆ๋‹ค โ€” ๋“ฑ๋ก๊ธฐ๊ฐ€ ๋”ฐ๋กœ ๋งํ•œ๋‹ค', () => {
36
+ expect(doorDeclarationWarnings([{ name: 'getOpenScene' }, { name: 'quiet', doors: [] }], lookup)).toEqual([])
37
+ })
38
+
39
+ it('โ˜… ๋ฌธ ๋ชจ์–‘์ด ์•„๋‹Œ ๊ธ€์ž๋„ ์Šคํ‚ค๋งˆ์— ์—†๋Š” ๊ฒƒ์œผ๋กœ ์„ผ๋‹ค', () => {
40
+ const found = doorDeclarationWarnings([{ name: 'odd', doors: ['jobOrders'] }], lookup)
41
+
42
+ expect(found.map(w => w.code)).toEqual(['tool-door-not-in-schema'])
43
+ })
44
+
45
+ it('์ฝ”๋“œ๋งˆ๋‹ค ํ•œ ์ค„๋กœ ๋ชจ์€๋‹ค', () => {
46
+ const lines = formatDoorDeclarationWarnings([
47
+ { code: 'tool-door-not-in-schema', tool: 'a', door: 'Query.x' },
48
+ { code: 'tool-door-not-in-schema', tool: 'b', door: 'Query.y' }
49
+ ])
50
+
51
+ expect(lines).toEqual(['[ai-assistant] tool-door-not-in-schema ร—2: a โ†’ Query.x, b โ†’ Query.y'])
52
+ })
53
+ })
@@ -0,0 +1,61 @@
1
+ /*
2
+ * ๋„๊ตฌ๊ฐ€ ์ ์–ด ๋‘” ๋ฌธ์ด **์Šคํ‚ค๋งˆ์— ์žˆ๋Š” ๋ฌธ์ธ๊ฐ€** โ€” ๋ถ€ํŒ…์—์„œ ํ•œ ๋ฒˆ ๋ฌป๋Š”๋‹ค.
3
+ *
4
+ * โ”€โ”€ ์™œ ๋“ฑ๋ก๊ธฐ๊ฐ€ ์•„๋‹ˆ๋ผ ์—ฌ๊ธฐ์ธ๊ฐ€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
5
+ * ์ด ๋ฌผ์Œ์€ AUTH ์˜ ํ•„๋“œโ†’๊ถŒํ•œ ํ‘œ(`privilegeOfField`, ADR-0072 ๊ฒฐ์ • 5)๊ฐ€ ์žˆ์–ด์•ผ ๋‹ตํ•  ์ˆ˜ ์žˆ๊ณ , ๊ทธ ํ‘œ๋Š”
6
+ * **์Šคํ‚ค๋งˆ๋ฅผ ์ง€์€ ๋’ค์—** ์ฐฌ๋‹ค. ๋“ฑ๋ก๊ธฐ๋Š” ๋ชจ๋“ˆ์ด ์˜ฌ๋ผ์˜ค๋Š” ๋™์•ˆ ๋„๋‹ˆ ๊ทธ๋•Œ๋Š” ํ‘œ๊ฐ€ ๋น„์–ด ์žˆ๋‹ค. ๋ถ€ํŒ… ๋ณด๊ณ ๋Š”
7
+ * ๋ชจ๋“  ๋ชจ๋“ˆ์ด ๋“ฑ๋ก์„ ๋งˆ์น˜๊ณ  ์Šคํ‚ค๋งˆ๊ฐ€ ์„  ๋’ค์— ๋„๋Š” ์ž๋ฆฌ๋‹ค.
8
+ *
9
+ * โ”€โ”€ ๋ฌด์—‡์„ ๊ฐ€๋ฅด๋‚˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
10
+ * `tool-door-not-in-schema` ๊ทธ๋Ÿฐ ๋ฃจํŠธ ํ•„๋“œ๊ฐ€ ์—†๋‹ค. ๋„๊ตฌ๊ฐ€ ์ด๋ฆ„์„ ํ‹€๋ฆฌ๊ฒŒ ์ ์€ ๊ฒƒ์ด๋‹ค โ€” ๊ทธ ๋„๊ตฌ๋กœ
11
+ * ๋งŒ๋“  ๋‹ต์€ **์•„๋ฌด๋„ ๋ชป ์ฝ๋Š”๋‹ค**(ADR-0073 ํŒ์ • 1 ์€ ๋ชจ๋ฅด๋Š” ๋ฌธ์„ ๋‹ซ๋Š”๋‹ค).
12
+ * ์˜คํƒ€ ํ•˜๋‚˜๊ฐ€ ๊ทธ ๋Œ€ํ™”์˜ ๋‹ต์„ ํ†ต์งธ๋กœ ๊ฐ€๋ฆฐ๋‹ค.
13
+ * `tool-door-has-no-privilege` ํ•„๋“œ๋Š” ์žˆ๊ณ  ์•„๋ฌด๊ฒƒ๋„ ์„ ์–ธํ•˜์ง€ ์•Š์•˜๋‹ค. ๊ทธ ๋ฌธ์ด ์—ด๋ ค ์žˆ์œผ๋ฏ€๋กœ ๊ทธ ๋‹ต๋„
14
+ * ๋„๋ฉ”์ธ ์•ˆ์—์„œ ์—ด๋ฆฐ๋‹ค. ๊ฒฐํ•จ์ด ์•„๋‹ˆ๋ผ **์‚ฌ์‹ค**์ด๊ณ , ๋ฌธ์˜ ์ฐจ๋ก€๋‹ค.
15
+ *
16
+ * ๋“ฑ๋ก๊ธฐ์˜ ๊ฐ™์€ ์ด๋ฆ„ ๊ฒฝ๊ณ ์™€ ๋ฌป๋Š” ๊ฒƒ์ด ๋‹ค๋ฅด๋‹ค. ๋“ฑ๋ก๊ธฐ๋Š” ใ€Œ๋„๊ตฌ๊ฐ€ ๋ฌธ์˜ ๊ถŒํ•œ์„ ๋ฒ ๊ปด ์ ์—ˆ๋‚˜ใ€๋ฅผ ๋ฌผ์—ˆ๊ณ ,
17
+ * ๊ทธ ๋ฒ ๋ผ๊ธฐ๋Š” ํ‘œ๊ฐ€ ์„œ๋ฉด์„œ ์—†์–ด์ง„๋‹ค(ADR-0072 ๊ฒฐ์ • 5). ์—ฌ๊ธฐ์„œ๋Š” **๋ฌธ์— ์ง์ ‘** ๋ฌป๋Š”๋‹ค.
18
+ */
19
+ import { getAllToolSpecs } from '@things-factory/ai-client-base'
20
+ import { privilegeOfField } from '@things-factory/auth-base'
21
+
22
+ export interface DoorDeclarationWarning {
23
+ code: 'tool-door-not-in-schema' | 'tool-door-has-no-privilege'
24
+ tool: string
25
+ door: string
26
+ }
27
+
28
+ /** ๋“ฑ๋ก๋œ ๋„๊ตฌ๋“ค์ด ์ ์–ด ๋‘” ๋ฌธ์„ ์Šคํ‚ค๋งˆ์— ๋Œ€์–ด ๋ณธ๋‹ค. ์Šคํ‚ค๋งˆ๊ฐ€ ์„  ๋’ค์—๋งŒ ๋œป์ด ์žˆ๋‹ค. */
29
+ export function doorDeclarationWarnings(
30
+ specs: ReadonlyArray<any> = getAllToolSpecs(),
31
+ lookup: (typeName: string, fieldName: string) => { known: false } | { known: true; gate: unknown } = privilegeOfField
32
+ ): DoorDeclarationWarning[] {
33
+ const out: DoorDeclarationWarning[] = []
34
+
35
+ for (const spec of specs) {
36
+ for (const door of spec?.doors ?? []) {
37
+ const match = /^(Query|Mutation|Subscription)\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(String(door))
38
+ if (!match) {
39
+ out.push({ code: 'tool-door-not-in-schema', tool: spec.name, door: String(door) })
40
+ continue
41
+ }
42
+
43
+ const asked = lookup(match[1], match[2])
44
+ if (!asked.known) out.push({ code: 'tool-door-not-in-schema', tool: spec.name, door })
45
+ else if (asked.gate === null) out.push({ code: 'tool-door-has-no-privilege', tool: spec.name, door })
46
+ }
47
+ }
48
+
49
+ return out
50
+ }
51
+
52
+ /** ์ฝ”๋“œ๋งˆ๋‹ค ํ•œ ์ค„ โ€” ๋ช‡์ด๊ณ  ์–ด๋А ๋„๊ตฌ์˜ ์–ด๋А ๋ฌธ์ธ๊ฐ€. */
53
+ export function formatDoorDeclarationWarnings(warnings: ReadonlyArray<DoorDeclarationWarning>): string[] {
54
+ const byCode = new Map<string, string[]>()
55
+ for (const warning of warnings) {
56
+ const found = byCode.get(warning.code) ?? []
57
+ found.push(`${warning.tool} โ†’ ${warning.door}`)
58
+ byCode.set(warning.code, found)
59
+ }
60
+ return [...byCode].map(([code, found]) => `[ai-assistant] ${code} ร—${found.length}: ${found.join(', ')}`)
61
+ }
@@ -0,0 +1,56 @@
1
+ /*
2
+ * ๋ชจ๋“  ๋ฌธ์ด **์ž๊ธฐ ํ˜•์„ ๋ช…์‹œํ•˜๋Š”๊ฐ€** โ€” ๋น ์ง€๋ฉด ๋ถ€ํŒ…์ด ์ฃฝ๋Š”๋‹ค.
3
+ *
4
+ * โ”€โ”€ ์™œ ์ด ์‹œํ—˜์ด ์žˆ๋‚˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
5
+ * `chatMessageActivity` ๋ฅผ async ๋กœ ๋ฐ”๊พธ์ž(๋ฐ›๋Š” ์‚ฌ๋žŒ๋งˆ๋‹ค ๊ฐ€๋ฆฌ๊ธฐ โ€” ADR-0073 ํŒ์ • 1) ๋ฐ˜ํ™˜ํ˜•์ด
6
+ * `Promise<ChatMessage>` ๊ฐ€ ๋˜์—ˆ๊ณ , type-graphql ์˜ reflection ์€ Promise ์•ˆ์„ ๋ชป ๋ณธ๋‹ค. ์Šคํ‚ค๋งˆ๋ฅผ
7
+ * ์„ธ์šธ ๋•Œ NoExplicitTypeError ๊ฐ€ ๋‚˜๊ณ  **ํŒจํ‚ค์ง€ ์ ์žฌ ์ž์ฒด๊ฐ€ ์‹คํŒจํ•œ๋‹ค** โ€” ์‹œํ—˜์ด ์•„๋‹ˆ๋ผ ์„œ๋ฒ„๊ฐ€ ์ฃฝ๋Š”๋‹ค.
8
+ * ์ปดํŒŒ์ผ์€ ํ†ต๊ณผํ•˜๋ฏ€๋กœ ํƒ€์ž…๊ฒ€์‚ฌ๋„ ๋ชป ์žก๋Š”๋‹ค. plant ๋ ˆ์ธ์ด ์„ค์น˜๋ณธ์„ ์˜ฌ๋ฆฌ๋‹ค ์žก์•„ ์•Œ๋ ค ์ฃผ์—ˆ๋‹ค(2026-09-18).
9
+ *
10
+ * โ”€โ”€ ์™œ ์Šคํ‚ค๋งˆ๋ฅผ ์•ˆ ์„ธ์šฐ๊ณ  ๊ธ€์ž๋ฅผ ์ฝ๋‚˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
11
+ * ์ด ๊พธ๋Ÿฌ๋ฏธ์˜ ์‹œํ—˜๊ธฐ๋Š” babel ์ด๋ผ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ metadata ๊ฐ€ ์—†๋‹ค. ์Šคํ‚ค๋งˆ๋ฅผ ์„ธ์šฐ๋ฉด ๋ฆฌํ”Œ๋ ‰์…˜์ด ์—†์–ด ์ „๋ถ€
12
+ * ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ๋‚˜๊ณ , ๊ทธ๋Ÿฌ๋ฉด ์ด ์‹œํ—˜์€ ์•„๋ฌด๊ฒƒ๋„ ๋ชป ๊ฐ€๋ฅธ๋‹ค. ๋Œ€์‹  ์„ ์–ธ์„ ์ฝ๋Š”๋‹ค โ€” ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๊ฐ€ ํ˜•์„
13
+ * ์ธ์ž๋กœ ๋ฐ›๊ณ  ์žˆ๋Š”์ง€๋งŒ ๋ณธ๋‹ค. ๊ทธ๊ฒƒ์ด ๋น ์ง„ ์ž๋ฆฌ๊ฐ€ ์ •ํ™•ํžˆ ํ„ฐ์ง€๋Š” ์ž๋ฆฌ๋‹ค.
14
+ */
15
+ import { readFileSync, readdirSync } from 'node:fs'
16
+ import { join } from 'node:path'
17
+
18
+ const root = join(__dirname, 'service')
19
+
20
+ function sources(dir: string): { path: string; text: string }[] {
21
+ const out: { path: string; text: string }[] = []
22
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
23
+ const path = join(dir, entry.name)
24
+ if (entry.isDirectory()) out.push(...sources(path))
25
+ else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts')) {
26
+ out.push({ path, text: readFileSync(path, 'utf-8') })
27
+ }
28
+ }
29
+ return out
30
+ }
31
+
32
+ /** `@Query(`, `@Mutation(`, `@Subscription(` ๋ฐ”๋กœ ๋‹ค์Œ ๊ธ€์ž๊ฐ€ ํ˜•์„ ์ฃผ๊ณ  ์žˆ๋‚˜. */
33
+ function untyped(): string[] {
34
+ const out: string[] = []
35
+ for (const { path, text } of sources(root)) {
36
+ for (const match of text.matchAll(/@(Query|Mutation|Subscription)\(\s*([^\n]*)/g)) {
37
+ const [, kind, rest] = match
38
+ /* ํ˜•์„ ์ฃผ๋Š” ๋‘ ๋ชจ์–‘: `() => X` ์™€ `type => X`. ๊ทธ ๋ฐ–์€ ์˜ต์…˜ ๊ฐ์ฒด๋ถ€ํ„ฐ ์‹œ์ž‘ํ•œ ๊ฒƒ์ด๋‹ค. */
39
+ if (/^\(\s*\)\s*=>/.test(rest) || /^\w+\s*=>/.test(rest)) continue
40
+ const line = text.slice(0, match.index).split('\n').length
41
+ out.push(`${path.slice(root.length + 1)}:${line} @${kind}`)
42
+ }
43
+ }
44
+ return out
45
+ }
46
+
47
+ describe('every GraphQL door names its type', () => {
48
+ it('โ˜… ํ˜•์„ ์•ˆ ์ค€ Query/Mutation/Subscription ์ด ์—†๋‹ค โ€” ์žˆ์œผ๋ฉด ๋ถ€ํŒ…์—์„œ ํ„ฐ์ง„๋‹ค', () => {
49
+ expect(untyped()).toEqual([])
50
+ })
51
+
52
+ it('์ด ์‹œํ—˜์ด ์‹ค์ œ๋กœ ๋ฌธ๋“ค์„ ๋ณด๊ณ  ์žˆ๋‹ค โ€” ์ •๊ทœ์‹์ด ์†Œ์Šค์™€ ์–ด๊ธ‹๋‚˜๋ฉด 0๊ฑด์œผ๋กœ ์กฐ์šฉํžˆ ํ†ต๊ณผํ•œ๋‹ค', () => {
53
+ const doors = sources(root).flatMap(s => [...s.text.matchAll(/@(Query|Mutation|Subscription)\(/g)])
54
+ expect(doors.length).toBeGreaterThan(20)
55
+ })
56
+ })
@@ -0,0 +1,66 @@
1
+ /*
2
+ * One window for one question: what state is this deployment's AI in.
3
+ *
4
+ * Two things were being answered by failure or by a log line โ€” whether a provider is configured at
5
+ * all (standard A7-a), and which registrations are not yet what the contracts ask for (A1 stage one,
6
+ * and the modes' `/` sentences). They are the same question asked by different people, so they come
7
+ * back together, as codes.
8
+ *
9
+ * A screen reads this **before** offering to ask the assistant. It is a query: reading what a
10
+ * deployment can do is reading.
11
+ */
12
+ import { Ctx, Directive, Field, ObjectType, Query, Resolver } from 'type-graphql'
13
+ import { GraphQLJSON } from 'graphql-scalars'
14
+ import '@things-factory/auth-base'
15
+
16
+ import { aiCapability, getToolRegistryWarnings } from '@things-factory/ai-client-base'
17
+
18
+ import { getAssistantModeWarnings } from './assistant-mode/index.js'
19
+
20
+ @ObjectType({ description: 'A registration that was accepted but is not yet what the contract asks for.' })
21
+ class AiRegistrationWarning {
22
+ @Field({ description: 'What is missing, as a code โ€” never prose a screen has to parse.' })
23
+ code: string
24
+
25
+ @Field(() => GraphQLJSON, { description: 'Which registration it is about (category, tool, mode, slash).' })
26
+ params: any
27
+
28
+ @Field({ description: 'The same thing in a sentence, for whoever reads a boot log.' })
29
+ message: string
30
+ }
31
+
32
+ @ObjectType({ description: "Whether this deployment has an AI, and what its registrations are missing." })
33
+ class AiCapabilityView {
34
+ @Field({ description: "'configured' or 'not-configured'. A screen that has not asked yet knows a third state of its own: it does not know." })
35
+ state: string
36
+
37
+ @Field({ nullable: true, description: 'Which provider answers.' })
38
+ provider?: string
39
+
40
+ @Field({ nullable: true, description: 'The default model this deployment would use.' })
41
+ model?: string
42
+
43
+ @Field({ nullable: true, description: 'Why it is not configured: no-config ยท no-provider ยท unknown-provider ยท no-adapter.' })
44
+ reason?: string
45
+
46
+ @Field(() => [AiRegistrationWarning], { description: 'Tools with no declared privilege, and modes offering work their tools cannot do.' })
47
+ warnings: AiRegistrationWarning[]
48
+ }
49
+
50
+ @Resolver()
51
+ export class AssistantCapabilityResolver {
52
+ @Query(() => AiCapabilityView, {
53
+ description: 'Whether this deployment has an AI configured, and which registrations are incomplete. Read this before offering to ask the assistant โ€” do not find out by failing.'
54
+ })
55
+ @Directive('@privilege(category: "ai-assistant", privilege: "query")')
56
+ async aiCapability(@Ctx() _context: any): Promise<AiCapabilityView> {
57
+ const capability = aiCapability()
58
+ const warnings = [...getToolRegistryWarnings(), ...getAssistantModeWarnings()].map(warning => ({
59
+ code: warning.code,
60
+ params: warning.params,
61
+ message: warning.message
62
+ }))
63
+
64
+ return { ...capability, warnings } as AiCapabilityView
65
+ }
66
+ }
@@ -28,16 +28,33 @@ import '@things-factory/auth-base'
28
28
 
29
29
  import { config } from '@things-factory/env'
30
30
  import { getRepository } from '@things-factory/shell'
31
- import { createAIClient, getDefaultAIClient, registryAssistantChat, type AIClient } from '@things-factory/ai-client-base'
31
+ import {
32
+ createAIClient,
33
+ findToolSpec,
34
+ getDefaultAIClient,
35
+ registryAssistantChat,
36
+ type AIClient
37
+ } from '@things-factory/ai-client-base'
32
38
 
33
39
  import { ChatSession } from './chat-session/chat-session.js'
34
40
  import { assertStationSession } from './chat-session/station-session-guard.js'
35
41
  import { ChatMessage } from './chat-message/chat-message.js'
36
42
  import { buildLlmHistory } from './chat-message/llm-history.js'
43
+ import { splitByCap } from './chat-message/history-summary.js'
44
+ import { foldSessionHistory } from './chat-message/fold-session-history.js'
45
+ import { groundingSourcesFor } from './grounding-sources.js'
37
46
  import { publishChatMessage } from './chat-message/chat-message-publish.js'
38
47
  import { publishSessionActivity } from './chat-session/session-activity-publish.js'
39
48
  import { activityPatch, autoSessionName } from './chat-session/session-inbox.js'
40
49
  import { toolAccessFor } from './tool-access.js'
50
+ import { ChatSessionParticipant } from './chat-session-participant/chat-session-participant.js'
51
+ import { doorsForAnswer } from './chat-message/message-doors.js'
52
+ import { judgeSessionKind } from './chat-session/session-kind.js'
53
+ import { readerGate } from './chat-message/reader-gate.js'
54
+ import { inviteMentionedUsers } from './chat-session-participant/mention-invite.js'
55
+ import { buildInviteNotification } from './chat-session-participant/invite-notification.js'
56
+ import { resolveModeSettings } from './assistant-mode/resolve.js'
57
+ import { requestTranslator } from './request-translator.js'
41
58
 
42
59
  /**
43
60
  * How many stored messages reach the model.
@@ -57,6 +74,19 @@ class AssistantMessageInput {
57
74
  content: string
58
75
  }
59
76
 
77
+ @InputType({ description: 'Which conversation this is, and what it is about.' })
78
+ class AssistantModeInput {
79
+ @Field({ description: 'A mode kind the app server registered โ€” `<app>.<surface>`, e.g. "twin.space".' })
80
+ kind: string
81
+
82
+ @Field({
83
+ nullable: true,
84
+ description:
85
+ 'What this conversation is about โ€” the space, the station, the model being edited. With `kind` it is the conversation\'s lifetime: when either changes the screen closes the conversation and starts another.'
86
+ })
87
+ target?: string
88
+ }
89
+
60
90
  @InputType({ description: 'One turn of a conversation that has no document of its own.' })
61
91
  class AssistantChatInput {
62
92
  @Field({
@@ -71,18 +101,11 @@ class AssistantChatInput {
71
101
  })
72
102
  messages: AssistantMessageInput[]
73
103
 
74
- @Field({
75
- description:
76
- 'Who the assistant is on this surface, in the words of the product that owns the screen. The rules for using each tool come from whoever registered it and are appended by the server.'
77
- })
78
- systemPrompt: string
79
-
80
- @Field(() => [String], {
81
- nullable: true,
104
+ @Field(() => AssistantModeInput, {
82
105
  description:
83
- 'Registered tool categories this conversation may use (registerToolCategory names, e.g. ["figure-authoring"]). Omitted or empty allows none โ€” a plain conversation. A tool that declares a privilege is offered only to a caller who holds it.'
106
+ 'Which conversation this is. The persona, the tool categories, the model and the limits come from the mode the app server registered (ADR-0061 decision 7) โ€” a caller choosing its own tool categories would be a caller choosing its own authority.'
84
107
  })
85
- toolCategories?: string[]
108
+ mode: AssistantModeInput
86
109
 
87
110
  @Field(() => GraphQLJSON, {
88
111
  nullable: true,
@@ -98,21 +121,6 @@ class AssistantChatInput {
98
121
  })
99
122
  truncateAfterMessageId?: string
100
123
 
101
- @Field({
102
- nullable: true,
103
- description:
104
- 'Require a tool call before the model may answer. For surfaces that report live state, where an unchecked assertion reads the same as a checked one.'
105
- })
106
- requireGroundingTools?: boolean
107
-
108
- @Field({ nullable: true, description: 'Model identifier override. Falls back to the configured default.' })
109
- model?: string
110
-
111
- @Field({ nullable: true, description: 'Sampling temperature override.' })
112
- temperature?: number
113
-
114
- @Field({ nullable: true, description: 'Max output tokens per call.' })
115
- maxTokens?: number
116
124
  }
117
125
 
118
126
  @ObjectType()
@@ -132,6 +140,13 @@ class AssistantChatOutput {
132
140
  @Field({ nullable: true, description: 'Stored ChatMessage id of the reply.' })
133
141
  assistantMessageId?: string
134
142
 
143
+ @Field(() => GraphQLJSON, {
144
+ nullable: true,
145
+ description:
146
+ 'Set when this turn did not fit: { omitted, summarized } โ€” how many earlier messages the model did not see, and whether a summary stood in for them. Without it the person watches the conversation lose its beginning in silence, while the model is told.'
147
+ })
148
+ historyFolded?: any
149
+
135
150
  @Field(() => GraphQLJSON, {
136
151
  nullable: true,
137
152
  description: 'Tool usages during the turn โ€” {name, arguments, result, kind}. The surface shows them foldably.'
@@ -194,6 +209,14 @@ export class AssistantChatResolver {
194
209
  )
195
210
  }
196
211
 
212
+ /*
213
+ * Read the declaration before anything is stored. A kind this server does not have is a
214
+ * mismatched deployment, and the person should see that instead of a conversation with no
215
+ * persona and no tools.
216
+ */
217
+ const t = requestTranslator(context)
218
+ const settings = resolveModeSettings(input.mode, t)
219
+
197
220
  const { domain, user } = context.state
198
221
  const sessionRepo = getRepository(ChatSession)
199
222
  const messageRepo = getRepository(ChatMessage)
@@ -203,15 +226,41 @@ export class AssistantChatResolver {
203
226
  const found = await sessionRepo.findOneBy({ id: input.sessionId, domain: { id: domain.id } as any })
204
227
  if (!found) throw new Error(`ChatSession ${input.sessionId} not found`)
205
228
  if (found.anchorType === 'figure' && input.hostContext?.figureId !== found.anchorId) {
206
- throw new Error('์ด ๋Œ€ํ™”๋Š” ๋‹ค๋ฅธ Figure์˜ ์ด๋ ฅ์ž…๋‹ˆ๋‹ค. ํ˜„์žฌ ๋ชจ๋ธ์˜ ๋Œ€ํ™”๋ฅผ ๋‹ค์‹œ ์—ฌ์„ธ์š”.')
229
+ throw new Error(
230
+ context.t('ai-assistant.text.other-figure-history', {
231
+ defaultValue: 'this conversation belongs to another figure. open the conversation of the model you have open.'
232
+ })
233
+ )
207
234
  }
208
235
  if (found.anchorType === 'application' && (input.hostContext?.app !== found.anchorId || input.hostContext?.figureId)) {
209
- throw new Error('์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋Œ€ํ™”์™€ ๋ชจ๋ธ ๋Œ€ํ™”๋ฅผ ๊ตฌ๋ถ„ํ•ด์„œ ๋‹ค์‹œ ์—ฌ์„ธ์š”.')
236
+ throw new Error(
237
+ context.t('ai-assistant.text.application-vs-model-history', {
238
+ defaultValue: 'this conversation is about the application, not about a model. open the one you want.'
239
+ })
240
+ )
241
+ }
242
+ /*
243
+ * ์ด ์„ธ์…˜์ด **์ด ๋Œ€ํ™”์˜** ์„ธ์…˜์ธ๊ฐ€ โ€” ADR-0072 ๊ฒฐ์ • 3. ๋‹ค๋ฅธ ๋Œ€ํ™”์˜ ์„ธ์…˜์— ๋ง์„ ๋ถ™์ด๋ฉด ๊ทธ ์ด๋ ฅ์ด
244
+ * ์ด ๋Œ€ํ™”์˜ ํ”„๋กฌํ”„ํŠธ๋กœ ๋“ค์–ด๊ฐ€๊ณ , ํ™”๋ฉด์€ ์ค„์ด ์‚ฌ๋ผ์ ธ ์ƒˆ ๋Œ€ํ™”์ฒ˜๋Ÿผ ๋ณด์ด๋ฏ€๋กœ ๋ณด๋‚ธ ์‚ฌ๋žŒ์€ ๋ชจ๋ฅธ๋‹ค.
245
+ */
246
+ const verdict = judgeSessionKind((found as any).kind, input.mode.kind)
247
+ if (verdict.action === 'refuse') {
248
+ throw new Error(
249
+ t('ai-assistant.text.session-kind-mismatch', {
250
+ ...verdict.params,
251
+ defaultValue: 'this conversation is {sessionKind}, and you are writing as {requested}. open the conversation you mean.'
252
+ })
253
+ )
254
+ }
255
+ if (verdict.action === 'stamp') {
256
+ /* ์นธ์ด ์„œ๊ธฐ ์ „์— ๋‚œ ์„ธ์…˜ โ€” ์š”์ฒญํ•œ ๋Œ€ํ™”๋กœ ํ•œ ๋ฒˆ ์ฐ๋Š”๋‹ค. ๋‹ค์Œ๋ถ€ํ„ฐ๋Š” ์œ„ ๊ทœ์น™ ํ•˜๋‚˜๋งŒ ๋‚จ๋Š”๋‹ค. */
257
+ await sessionRepo.update(found.id!, { kind: verdict.kind } as any)
258
+ ;(found as any).kind = verdict.kind
210
259
  }
211
260
  session = found
212
261
  }
213
262
 
214
- assertStationSession(session, input.hostContext)
263
+ assertStationSession(session, input.hostContext, t)
215
264
 
216
265
  const lastUserMessage = input.messages[input.messages.length - 1]
217
266
  let userMessageId: string | undefined
@@ -225,6 +274,12 @@ export class AssistantChatResolver {
225
274
  session: { id: session.id } as any,
226
275
  role: 'user',
227
276
  content: lastUserMessage.content,
277
+ /*
278
+ * This door is the one people call to ask the assistant, so a turn stored here was for the
279
+ * assistant (ADR-0070 A15 decision โ‘ ). The value comes from which door was called, never
280
+ * from the request โ€” two sources for one fact can disagree.
281
+ */
282
+ addressee: 'assistant',
228
283
  creator: user,
229
284
  updater: user,
230
285
  parentMessage: previousLast ? ({ id: previousLast.id } as any) : undefined
@@ -244,6 +299,27 @@ export class AssistantChatResolver {
244
299
  /* noop */
245
300
  }
246
301
 
302
+ /*
303
+ * ์‚ฌ๋žŒ์ด `@์ด๋ฆ„` ์œผ๋กœ ๋ถ€๋ฅธ ์‚ฌ๋žŒ์€ ์ด ๋Œ€ํ™”์— ๋“ค์–ด์˜จ๋‹ค(ADR-0071 ๊ฒฐ์ • 1). ์ด ๋ฌธ์€ **์‚ฌ๋žŒ์˜ ํ„ด**์„
304
+ * ์ €์žฅํ•˜๋Š” ๋ฌธ์ด๋ผ ๊ทธ ์ผ์„ ํ•œ๋‹ค โ€” ๋ชจ๋ธ์˜ ๋‹ต์ด๋‚˜ ๋„๊ตฌ ๊ฒฐ๊ณผ๋Š” ์•„๋ฌด๋„ ์ดˆ๋Œ€ํ•˜์ง€ ์•Š๋Š”๋‹ค.
305
+ */
306
+ const invite = await inviteMentionedUsers({
307
+ content: lastUserMessage.content,
308
+ sessionId: session.id!,
309
+ domainId: domain.id,
310
+ inviter: user,
311
+ participants: getRepository(ChatSessionParticipant),
312
+ visibility: (session as any).visibility
313
+ })
314
+ if (invite.invited.length > 0) {
315
+ const payload = buildInviteNotification(invite.invited, undefined, user)
316
+ try {
317
+ if (payload) await (context.state as any)?.notify?.(payload)
318
+ } catch {
319
+ /* ํŽธ์ง€๊ฐ€ ๋ชป ๊ฐ”๋‹ค๊ณ  ๋ฐฉ์— ๋“ค์–ด์˜จ ์‚ฌ์‹ค์ด ์ทจ์†Œ๋˜์ง€๋Š” ์•Š๋Š”๋‹ค. */
320
+ }
321
+ }
322
+
247
323
  publishChatMessage({ domainId: domain.id, sessionId: session.id, message: saved } as any)
248
324
  publishSessionActivity({
249
325
  domainId: domain.id,
@@ -256,13 +332,52 @@ export class AssistantChatResolver {
256
332
  })
257
333
  }
258
334
 
335
+ /*
336
+ * ์ด๋ฒˆ ํ„ด์— ์ƒํ•œ ๋ฐ–์œผ๋กœ ๋ฐ€๋ ค๋‚œ ๊ฒƒ์ด ์žˆ๋Š”๊ฐ€ โ€” ํ™”๋ฉด์ด ใ€Œ๋Œ€ํ™”๊ฐ€ ๊ธธ์–ด์กŒ๋‹คใ€๋ฅผ ์•Œ๋ฆด ๊ทผ๊ฑฐ๋‹ค.
337
+ *
338
+ * ์ด ๋ฌธ์€ ๊ทธ ์‚ฌ์‹ค์„ ๋‚ด์ง€ ์•Š์•˜๋‹ค. ๋Œ€ํ™”์ฐฝ์—๋Š” ๊ทธ ์•ˆ๋‚ด๋ฅผ ๊ทธ๋ฆฌ๋Š” ์ž๋ฆฌ๊ฐ€ ์žˆ๊ณ (`historyFolded`), ๋ณด๋“œ
339
+ * ๋ฌธ๋งŒ ๊ทธ ๊ฐ’์„ ์ฑ„์› ๋‹ค. ๊ทธ๋ž˜์„œ ์•ฑ์˜ ๋Œ€ํ™”๋Š” ์•ž๋ถ€๋ถ„์ด ์ž˜๋ ค ๋‚˜๊ฐ€๋„ **์‚ฌ๋žŒ์—๊ฒŒ๋Š” ์•„๋ฌด ๋ง๋„ ํ•˜์ง€ ์•Š์•˜๋‹ค** โ€”
340
+ * ๋ชจ๋ธ์—๊ฒŒ๋Š” ใ€ŒN ๊ฐœ ์ƒ๋žตใ€์„ ์ ์–ด ๋ณด๋‚ด๋ฉด์„œ.
341
+ */
342
+ const readRows = session
343
+ ? await messageRepo.find({
344
+ where: { session: { id: session.id } as any },
345
+ order: { createdAt: 'ASC' },
346
+ relations: ['creator']
347
+ })
348
+ : []
349
+ /*
350
+ * ๋ชจ๋ธ์ด ์ฝ๋Š” ์ด๋ ฅ๋„ **๋ถ€๋ฅธ ์‚ฌ๋žŒ** ๊ธฐ์ค€์œผ๋กœ ๊ฐ€๋ฆฐ๋‹ค โ€” ADR-0073 ํŒ์ • 1. ๊ฐ€๋ฆฌ์ง€ ์•Š์œผ๋ฉด, ๋ฌธ์„ ํ•œ ๋ฒˆ๋„
351
+ * ์•ˆ ์ง€๋‚œ ์‚ฌ๋žŒ์ด "์•„๊นŒ ๊ทธ ์ˆ˜ ๋‹ค์‹œ ๋งํ•ด ์ค˜" ํ•œ ๋งˆ๋””๋กœ ๊ฐ’์„ ๊ฐ€์ ธ๊ฐ„๋‹ค. ๋ชจ๋ธ์ด ์˜ฎ๊ฒจ ์ ๋Š” ์ˆœ๊ฐ„ ๊ทธ ๊ฐ’์€
352
+ * ๋ฌธ ์—†๋Š” ์ƒˆ ๋‹ต์ด ๋œ๋‹ค. ๊ฐ€๋ฆฐ ์ž๋ฆฌ๋ฅผ ์ง€์šฐ์ง€ ์•Š๊ณ  ํ•œ ์ค„๋กœ ๋‚จ๊ธฐ๋Š” ์ด์œ ๋„ ๊ฐ™๋‹ค โ€” ์ง€์šฐ๋ฉด ๋ชจ๋ธ์ด ๊ทธ๋Ÿฐ
353
+ * ๋ง์ด ์—†์—ˆ๋‹ค๊ณ  ๋‹ตํ•˜๊ณ , ๊ทธ๊ฒƒ์€ ๊ฑฐ์ง“๋ง์ด๋‹ค.
354
+ */
355
+ const storedRows = await readerGate(context)(readRows)
356
+ const capped = splitByCap(
357
+ storedRows.map(row => ({ id: row.id, role: row.role, content: row.content ?? '' })),
358
+ LLM_HISTORY_MAX_TURNS
359
+ )
360
+ const historyFolded =
361
+ capped.dropped.length > 0
362
+ ? { omitted: capped.dropped.length, summarized: Boolean((session as any)?.lastSummary) }
363
+ : undefined
364
+
259
365
  const conversation = session
260
366
  ? buildLlmHistory(
261
- await messageRepo.find({
262
- where: { session: { id: session.id } as any },
263
- order: { createdAt: 'ASC' },
264
- relations: ['creator']
265
- }),
367
+ /*
368
+ * The model reads who spoke and who they spoke to. The entity carries the speaker as a
369
+ * relation and the addressee as a column; the history wants them as plain fields, and
370
+ * without this mapping every line arrived unattributed โ€” a room of people read as one
371
+ * voice (ADR-0070 A15 decision โ‘ก).
372
+ */
373
+ storedRows.map(message => ({
374
+ id: message.id,
375
+ role: message.role,
376
+ content: message.content,
377
+ senderId: (message as any).creator?.id,
378
+ senderName: (message as any).creator?.name,
379
+ addressee: (message as any).addressee
380
+ })),
266
381
  {
267
382
  maxTurns: LLM_HISTORY_MAX_TURNS,
268
383
  truncateAfterMessageId: input.truncateAfterMessageId,
@@ -291,23 +406,53 @@ export class AssistantChatResolver {
291
406
  const result = await registryAssistantChat({
292
407
  client,
293
408
  messages: conversation as any,
294
- systemPrompt: input.systemPrompt,
295
- toolCategories: input.toolCategories,
409
+ systemPrompt: settings.systemPrompt,
410
+ toolCategories: settings.toolCategories,
411
+ /* ๋„๊ตฌ๊ฐ€ ใ€Œ์–ด๋А ๋Œ€ํ™”์—์„œ ๋ถˆ๋ ธ๋‚˜ใ€๋ฅผ ์•ˆ๋‹ค โ€” ์„œ๋ฒ„์˜ ๊ฐ’์ด๋‹ค(ADR-0061 ๊ฒฐ์ • 3). */
412
+ mode: { kind: input.mode.kind, ...(input.mode.target ? { target: input.mode.target } : {}) },
296
413
  toolAccess: toolAccessFor(context),
414
+ /* The stop notice is a sentence a person reads โ€” the server writes it in their language (ADR-0054). */
415
+ t,
297
416
  hostContext: input.hostContext,
417
+ /*
418
+ * ๊ทผ๊ฑฐ๋Š” ๋„๊ตฌ ๊ฒฐ๊ณผ๋งŒ์ด ์•„๋‹ˆ๋‹ค โ€” ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ยท ์ด ๋Œ€ํ™”์˜ ๋ง ยท ํ™”๋ฉด์ด ๋ณด๋‚ธ ์Šค๋ƒ…์ˆ๋„ ๊ทผ๊ฑฐ๋‹ค
419
+ * (`ai-client-base/utils/grounding`). ์ด ๋ฌธ์€ ๊ทธ ์…‹์„ ๋„˜๊ธฐ์ง€ ์•Š์•„, **์‚ฌ๋žŒ์ด ๋ฐฉ๊ธˆ ํƒ€์ดํ•‘ํ•œ
420
+ * ์‹๋ณ„์ž**๋ฅผ ๋˜๋ฐ›์•„ ์ ์€ ๋‹ต์ด ใ€Œ๊ทผ๊ฑฐ ์—†์Œใ€์œผ๋กœ ํ‘œ์‹œ๋๋‹ค. ๊ทธ๋Ÿฐ ๊ฒฝ๊ณ ๊ฐ€ ๋ช‡ ๋ฒˆ ๋‚˜์˜ค๋ฉด ์‚ฌ๋žŒ์ด ๊ฒฝ๊ณ 
421
+ * ์ž์ฒด๋ฅผ ์•ˆ ๋ณด๊ฒŒ ๋œ๋‹ค.
422
+ */
423
+ groundingSources: groundingSourcesFor({
424
+ systemPrompt: settings.systemPrompt,
425
+ messages: conversation as any,
426
+ hostContext: input.hostContext
427
+ }),
298
428
  state: context.state,
299
- model: input.model,
300
- temperature: input.temperature,
301
- maxTokens: input.maxTokens,
302
- requireToolOnFirstTurn: input.requireGroundingTools
429
+ model: settings.model,
430
+ temperature: settings.temperature,
431
+ maxTokens: settings.maxTokens,
432
+ requireToolOnFirstTurn: settings.requireToolOnFirstTurn
303
433
  })
304
434
 
435
+ /*
436
+ * ์ด ๋‹ต์ด ์ง€๋‚˜์˜จ ๋ฌธ โ€” ADR-0073 ํŒ์ • 1. ํ–‰์—๋„, ๋ชฉ๋ก ๋ฏธ๋ฆฌ๋ณด๊ธฐ์—๋„ ๊ฐ™์€ ๊ฐ’์ด ๊ฐ„๋‹ค. ํ•œ์ชฝ๋งŒ ์ ์œผ๋ฉด
437
+ * ๊ฐ€๋ ค์ง„ ๋‹ต์ด ๋ชฉ๋ก์—๋Š” ๊ทธ๋Œ€๋กœ ๋œฌ๋‹ค.
438
+ */
439
+ const answerDoors = doorsForAnswer(
440
+ (result.loop.toolUsages ?? []).map((usage: any) => usage?.name).filter(Boolean),
441
+ name => (findToolSpec(name, settings.toolCategories) as any)?.doors
442
+ )
443
+
305
444
  let assistantMessageId: string | undefined
306
445
  if (session) {
307
446
  const stored = await messageRepo.save({
308
447
  session: { id: session.id } as any,
309
448
  role: 'assistant',
310
449
  content: result.reply,
450
+ /*
451
+ * ์ด ๋‹ต์ด ์ง€๋‚˜์˜จ ๋ฌธ์„ ํ•จ๊ป˜ ์ ๋Š”๋‹ค โ€” ADR-0073 ํŒ์ • 1. ๋ฐฉ์€ ๋„๋ฉ”์ธ ์•ˆ์—์„œ ๊ณต๊ฐœ์ด๋ฏ€๋กœ, ์ด ๋ฌธ์žฅ์ด
452
+ * ์‹ค์–ด ๋‚˜๋ฅด๋Š” ๊ฐ’์„ ๋‚˜์ค‘์— ์ฝ๋Š” ์‚ฌ๋žŒ์ด ์—ด ์ˆ˜ ์žˆ๋Š”์ง€ ํŒ์ •ํ•  ๊ทผ๊ฑฐ๊ฐ€ ํ–‰์— ์žˆ์–ด์•ผ ํ•œ๋‹ค. ์ ์ง€ ์•Š์œผ๋ฉด
453
+ * ๋นˆ ๋ชฉ๋ก์ด ๋˜๊ณ , ๋นˆ ๋ชฉ๋ก์€ ๋ˆ„๊ตฌ๋‚˜ ์ฝ๋Š”๋‹ค๋Š” ๋œป์ด๋‹ค.
454
+ */
455
+ doors: answerDoors,
311
456
  toolUsages: result.loop.toolUsages?.length ? JSON.stringify(result.loop.toolUsages) : undefined,
312
457
  groundingWarnings: result.groundingWarnings?.length ? JSON.stringify(result.groundingWarnings) : undefined,
313
458
  creator: user,
@@ -331,8 +476,25 @@ export class AssistantChatResolver {
331
476
  await sessionRepo.update(session.id!, {
332
477
  aiClientId: client.id,
333
478
  updater: user,
334
- ...activityPatch(result.reply)
479
+ ...activityPatch(result.reply, new Date(), answerDoors)
335
480
  } as any)
481
+
482
+ /*
483
+ * ์ƒํ•œ ๋ฐ–์œผ๋กœ ๋ฐ€๋ ค๋‚œ ์•ž๋ถ€๋ถ„์„ ์š”์•ฝ์œผ๋กœ ์ ‘๋Š”๋‹ค โ€” **๋‹ต์„ ๋Œ๋ ค์ค€ ๋’ค**์— ํ•œ๋‹ค(์‚ฌ์šฉ์ž ๋Œ€๊ธฐ ์‹œ๊ฐ„์—
484
+ * ์š”์•ฝ ํ˜ธ์ถœ์„ ์–น์ง€ ์•Š๋Š”๋‹ค). ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š์œผ๋ฏ€๋กœ ์‹คํŒจํ•ด๋„ ๋Œ€ํ™”์—๋Š” ์˜ํ–ฅ์ด ์—†๋‹ค: ์š”์•ฝ์ด ์—†์œผ๋ฉด ๋‹ค์Œ
485
+ * ํ„ด์ด ใ€ŒN ๊ฐœ ์ƒ๋žตใ€์œผ๋กœ ์ •์งํ•˜๊ฒŒ ์•Œ๋ฆฐ๋‹ค.
486
+ *
487
+ * ์ด ๋ฌธ์—๋Š” ์ด ํ˜ธ์ถœ์ด ์—†์—ˆ๋‹ค. ์กฐ๊ฐ์€ ๋‹ค ์žˆ์—ˆ๋Š”๋ฐ(์ƒํ•œ ยท ์ƒ๋žต ์•Œ๋ฆผ ยท `lastSummary` ยท ์ˆœ์ˆ˜ ๊ทœ์น™)
488
+ * ๋ถ€๋ฅด๋Š” ์ชฝ์ด ๋ณด๋“œ ์ œํ’ˆ ํ•˜๋‚˜๋ฟ์ด์–ด์„œ, ์•ฑ์˜ ๋Œ€ํ™”๋Š” ๋ฉฐ์น ์ด ์ง€๋‚˜๋„ ์•ž๋ถ€๋ถ„์„ ์š”์•ฝํ•˜์ง€ ์•Š๊ณ  ๋งค ํ„ด
489
+ * ใ€Œ์ƒ๋žตํ–ˆ๋‹คใ€๊ณ ๋งŒ ๋งํ–ˆ๋‹ค โ€” ํ˜‘์˜๊ฐ€ ๊ธธ์–ด์งˆ์ˆ˜๋ก ์ฒ˜์Œ์— ํ•ฉ์˜ํ•œ ๊ฒƒ์ด ๋จผ์ € ์‚ฌ๋ผ์ง„๋‹ค.
490
+ */
491
+ void foldSessionHistory({
492
+ sessionId: session.id!,
493
+ domainId: domain.id,
494
+ client,
495
+ maxTurns: LLM_HISTORY_MAX_TURNS,
496
+ locale: context.state?.locale
497
+ })
336
498
  }
337
499
 
338
500
  return {
@@ -344,7 +506,8 @@ export class AssistantChatResolver {
344
506
  toolUsages: result.loop.toolUsages?.length ? result.loop.toolUsages : null,
345
507
  offeredTools: result.offeredTools,
346
508
  proposals: result.loop.proposals?.length ? result.loop.proposals : null,
347
- groundingWarnings: result.groundingWarnings?.length ? result.groundingWarnings : null
509
+ groundingWarnings: result.groundingWarnings?.length ? result.groundingWarnings : null,
510
+ historyFolded: historyFolded ?? null
348
511
  }
349
512
  }
350
513
  }