@things-factory/ai-assistant 10.1.157 → 10.1.160

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 (75) hide show
  1. package/client/components/assistant-chat.ts +25 -213
  2. package/client/components/assistant-dock.ts +11 -32
  3. package/client/components/assistant-mode.test.ts +1 -43
  4. package/client/components/assistant-mode.ts +0 -47
  5. package/client/utils/assistant-surface.ts +1 -1
  6. package/client/utils/composer-frame.ts +6 -4
  7. package/dist-client/components/assistant-chat.d.ts +5 -69
  8. package/dist-client/components/assistant-chat.js +25 -205
  9. package/dist-client/components/assistant-chat.js.map +1 -1
  10. package/dist-client/components/assistant-dock.d.ts +0 -3
  11. package/dist-client/components/assistant-dock.js +10 -34
  12. package/dist-client/components/assistant-dock.js.map +1 -1
  13. package/dist-client/components/assistant-mode.d.ts +0 -33
  14. package/dist-client/components/assistant-mode.js +0 -37
  15. package/dist-client/components/assistant-mode.js.map +1 -1
  16. package/dist-client/components/assistant-mode.test.js +1 -35
  17. package/dist-client/components/assistant-mode.test.js.map +1 -1
  18. package/dist-client/tsconfig.tsbuildinfo +1 -1
  19. package/dist-client/utils/assistant-surface.d.ts +1 -1
  20. package/dist-client/utils/assistant-surface.js.map +1 -1
  21. package/dist-client/utils/composer-frame.d.ts +5 -3
  22. package/dist-client/utils/composer-frame.js +4 -4
  23. package/dist-client/utils/composer-frame.js.map +1 -1
  24. package/dist-server/index.d.ts +1 -0
  25. package/dist-server/index.js +2 -0
  26. package/dist-server/index.js.map +1 -1
  27. package/dist-server/service/assistant-chat-resolver.d.ts +1 -0
  28. package/dist-server/service/assistant-chat-resolver.js +30 -6
  29. package/dist-server/service/assistant-chat-resolver.js.map +1 -1
  30. package/dist-server/service/assistant-mode/assistant-mode-resolver.d.ts +1 -2
  31. package/dist-server/service/assistant-mode/assistant-mode-resolver.js +13 -24
  32. package/dist-server/service/assistant-mode/assistant-mode-resolver.js.map +1 -1
  33. package/dist-server/service/assistant-mode/fallback-subjects.d.ts +1 -0
  34. package/dist-server/service/assistant-mode/fallback-subjects.js +37 -0
  35. package/dist-server/service/assistant-mode/fallback-subjects.js.map +1 -0
  36. package/dist-server/service/assistant-mode/index.d.ts +2 -1
  37. package/dist-server/service/assistant-mode/index.js +7 -2
  38. package/dist-server/service/assistant-mode/index.js.map +1 -1
  39. package/dist-server/service/assistant-mode/registry.d.ts +2 -6
  40. package/dist-server/service/assistant-mode/registry.js +16 -56
  41. package/dist-server/service/assistant-mode/registry.js.map +1 -1
  42. package/dist-server/service/assistant-mode/resolve.js +3 -1
  43. package/dist-server/service/assistant-mode/resolve.js.map +1 -1
  44. package/dist-server/service/assistant-mode/screen-rules.d.ts +25 -0
  45. package/dist-server/service/assistant-mode/screen-rules.js +47 -0
  46. package/dist-server/service/assistant-mode/screen-rules.js.map +1 -0
  47. package/dist-server/service/assistant-mode/types.d.ts +8 -25
  48. package/dist-server/service/assistant-mode/types.js.map +1 -1
  49. package/dist-server/service/testing/mode-conformance.d.ts +11 -0
  50. package/dist-server/service/testing/mode-conformance.js +28 -0
  51. package/dist-server/service/testing/mode-conformance.js.map +1 -1
  52. package/dist-server/tool-registry-boot-report.d.ts +1 -1
  53. package/dist-server/tool-registry-boot-report.js +16 -7
  54. package/dist-server/tool-registry-boot-report.js.map +1 -1
  55. package/dist-server/tsconfig.tsbuildinfo +1 -1
  56. package/package.json +6 -6
  57. package/server/index.ts +2 -0
  58. package/server/service/assistant-chat-resolver.ts +33 -7
  59. package/server/service/assistant-mode/assistant-mode-resolver.ts +12 -25
  60. package/server/service/assistant-mode/fallback-subjects.ts +36 -0
  61. package/server/service/assistant-mode/index.ts +2 -1
  62. package/server/service/assistant-mode/one-conversation-per-subject.test.ts +37 -0
  63. package/server/service/assistant-mode/open-subject-door.test.ts +29 -0
  64. package/server/service/assistant-mode/registry.ts +14 -55
  65. package/server/service/assistant-mode/resolve.test.ts +10 -1
  66. package/server/service/assistant-mode/resolve.ts +3 -1
  67. package/server/service/assistant-mode/screen-rules.test.ts +39 -0
  68. package/server/service/assistant-mode/screen-rules.ts +47 -0
  69. package/server/service/assistant-mode/types.ts +8 -31
  70. package/server/service/testing/mode-conformance.ts +27 -1
  71. package/server/tool-registry-boot-report.test.ts +14 -1
  72. package/server/tool-registry-boot-report.ts +16 -7
  73. package/test/assistant-dock.test.ts +27 -4
  74. package/test/composer-frame.test.ts +12 -2
  75. package/server/service/assistant-mode/subject-defaults.test.ts +0 -86
@@ -12,7 +12,7 @@
12
12
  * · 화면에 낼 말이 **키가 아니라 문장**으로 나오는가
13
13
  * · 슬래시가 가리키는 도구가 그 모드 안에 있는가
14
14
  */
15
- import { registryAssistantChat } from '@things-factory/ai-client-base'
15
+ import { openSubjectLine, registryAssistantChat } from '@things-factory/ai-client-base'
16
16
 
17
17
  import { getAssistantMode, getAssistantModeWarnings } from '../assistant-mode/registry.js'
18
18
  import { assistantModeView, resolveModeSettings, type Translate } from '../assistant-mode/resolve.js'
@@ -127,3 +127,29 @@ export function modePromisesItCannotKeep(kind: string): string[] {
127
127
  .filter(warning => warning.params.kind === kind)
128
128
  .map(warning => `${warning.code}:${warning.params.slash}→${warning.params.tool}`)
129
129
  }
130
+
131
+ /**
132
+ * What an app's prompt carries that every conversation already gets (design/ai/screen-pattern.md §6 · §7).
133
+ *
134
+ * The absence rule is added by the door to every conversation. A copy in an app's prompt says it twice, and the
135
+ * copies drift - five apps had five spellings of it.
136
+ */
137
+ export function modeCarriesSharedRules(kind: string): string[] {
138
+ const prompt = String(getAssistantMode(kind)?.systemPrompt ?? '')
139
+ const found: string[] = []
140
+ if (/name our plumbing|do not describe our machinery/i.test(prompt)) found.push('absence-rule-copy')
141
+ if (/^\s*Next:/im.test(prompt) || /end the reply with one last line/i.test(prompt)) found.push('next-request-rule-copy')
142
+ return found
143
+ }
144
+
145
+ /** Subjects that more than one conversation names - a screen has one (design/ai/screen-pattern.md §3). */
146
+ export function subjectsClaimedTwice(): string[] {
147
+ return getAssistantModeWarnings()
148
+ .filter(warning => warning.code === 'assistant-mode-subject-claimed-twice')
149
+ .map(warning => String(warning.params.subject))
150
+ }
151
+
152
+ /** The line the model reads for an open subject - an app's test checks its describer names its own record. */
153
+ export function openSubjectLineFor(anchorType: string, anchorId: string, domainId: string, state?: any): Promise<string | undefined> {
154
+ return openSubjectLine({ anchorType, anchorId, domain: { id: domainId }, state })
155
+ }
@@ -12,7 +12,7 @@ jest.mock('@things-factory/auth-base', () => ({ privilegeOfField: () => ({ known
12
12
  /* ai-client-base keeps the AI usage table since 2026-10-12, so its root brings shell too - the same stand-in. */
13
13
  jest.mock('@things-factory/ai-client-base', () => ({ getToolRegistryWarnings: () => [] }))
14
14
 
15
- import { formatToolRegistryWarnings } from './tool-registry-boot-report'
15
+ import { formatAssistantModeWarnings, formatToolRegistryWarnings } from './tool-registry-boot-report'
16
16
 
17
17
  it('★ groups the recorded omissions by code and names every tool', () => {
18
18
  const lines = formatToolRegistryWarnings([
@@ -26,3 +26,16 @@ it('★ groups the recorded omissions by code and names every tool', () => {
26
26
  it('says nothing when every tool is declared', () => {
27
27
  expect(formatToolRegistryWarnings([])).toEqual([])
28
28
  })
29
+
30
+ describe('a screen with two conversations is reported by its subject (ADR-0072 rule, ruled 2026-10-13)', () => {
31
+ it('★ the boot line names the code, the subject and both conversations', () => {
32
+ const lines = formatAssistantModeWarnings([
33
+ {
34
+ code: 'assistant-mode-subject-claimed-twice',
35
+ params: { kind: 'twin.board', subject: 'board', kinds: ['twin.board', 'twin.space'] },
36
+ message: ''
37
+ }
38
+ ])
39
+ expect(lines).toEqual(['[ai-assistant] assistant-mode-subject-claimed-twice ×1: board:twin.board+twin.space'])
40
+ })
41
+ })
@@ -28,15 +28,24 @@ export function formatToolRegistryWarnings(warnings: ReadonlyArray<ToolRegistryW
28
28
  return [...byCode].map(([code, tools]) => `[ai-assistant] ${code} ×${tools.length}: ${tools.join(', ')}`)
29
29
  }
30
30
 
31
- /** One line per code: the code, how many, and which modes promised what. */
31
+ /** What one warning is about, in the words its code needs - a subject claimed twice names the subject and both. */
32
+ function detailOf(warning: AssistantModeWarning): string {
33
+ const { kind, slash, tool, subject, kinds, target } = warning.params
34
+ switch (warning.code) {
35
+ case 'assistant-mode-subject-claimed-twice':
36
+ return `${subject}:${(kinds ?? []).join('+')}`
37
+ case 'assistant-mode-entry-target-unknown':
38
+ return `${kind}→${target}`
39
+ default:
40
+ return `${kind}:/${slash}→${tool}`
41
+ }
42
+ }
43
+
44
+ /** One line per code: the code, how many, and what each is about. */
32
45
  export function formatAssistantModeWarnings(warnings: ReadonlyArray<AssistantModeWarning>): string[] {
33
46
  const byCode = new Map<string, string[]>()
34
- for (const warning of warnings) {
35
- const offers = byCode.get(warning.code) ?? []
36
- offers.push(`${warning.params.kind}:/${warning.params.slash}→${warning.params.tool}`)
37
- byCode.set(warning.code, offers)
38
- }
39
- return [...byCode].map(([code, offers]) => `[ai-assistant] ${code} ×${offers.length}: ${offers.join(', ')}`)
47
+ for (const warning of warnings) byCode.set(warning.code, [...(byCode.get(warning.code) ?? []), detailOf(warning)])
48
+ return [...byCode].map(([code, details]) => `[ai-assistant] ${code} ×${details.length}: ${details.join(', ')}`)
40
49
  }
41
50
 
42
51
  /*
@@ -38,9 +38,8 @@ describe('the dock opens on every screen', () => {
38
38
  })
39
39
 
40
40
  describe('every request the chat makes is taken', () => {
41
- it('new conversation, mode change and proposal execute', () => {
41
+ it('new conversation and proposal execute', () => {
42
42
  assert.match(dock, /addEventListener\('assistant-start-new-session'/)
43
- assert.match(dock, /@assistant-mode-change=\$\{this\.onModeChange\}/)
44
43
  assert.match(dock, /@assistant-proposal-execute=\$\{this\.onProposal\}/)
45
44
  })
46
45
 
@@ -87,8 +86,32 @@ describe('sessions', () => {
87
86
  assert.match(dock, /scope: JSON\.stringify\(\[this\.app\.contextPath, userId, this\.kind\]\)/)
88
87
  })
89
88
 
90
- it('a picked conversation of another kind is followed, not refused', () => {
91
- assert.match(dock, /followPickedConversation\(pickedKind, this\.kind\)/)
89
+ it('★ a screen opens its one conversation - nothing to pick, nothing to follow (design/ai/screen-pattern.md §3)', () => {
90
+ assert.match(dock, /assistantModeFor\(subject: \$subject\)/)
91
+ assert.match(dock, /this\.kind = mode\?\.kind \?\? ''/)
92
+ assert.doesNotMatch(dock, /chosenKind|onModeChange|assistant-mode-change|followPickedConversation/)
93
+ })
94
+ })
95
+
96
+ describe('what is being written stays with its conversation (ADR-0072 rule, ruled 2026-10-13)', () => {
97
+ const chat = readFileSync(join(__dirname, '../client/components/assistant-chat.ts'), 'utf8')
98
+
99
+ it('★ the unsent text and the attachments both leave the box with the conversation, and come back to it', () => {
100
+ const take = chat.slice(chat.indexOf(' takeDraft()'), chat.indexOf(' restoreDraft('))
101
+ assert.match(take, /const draft = \{ text: this\.input, attachments: this\.attachments \}/)
102
+ assert.match(take, /this\.input = ''\s+this\.attachments = \[\]/)
103
+ const restore = chat.slice(chat.indexOf(' restoreDraft('), chat.indexOf(' restoreDraft(') + 200)
104
+ assert.match(restore, /this\.input = draft\.text\s+this\.attachments = draft\.attachments/)
105
+ })
106
+
107
+ it('★ the dock keeps it per subject when the page moves, except when the person signed in changed', () => {
108
+ assert.match(dock, /if \(transition !== 'reset'\) this\.keepDraft\(\)/)
109
+ assert.match(dock, /if \(draft\) this\.drafts\.set\(this\.openedFor!, draft\)/)
110
+ assert.match(dock, /void this\.restoreDraft\(\)/)
111
+ })
112
+
113
+ it('★ a page change while answering waits for the answer - it lands in the conversation it was asked in', () => {
114
+ assert.match(dock, /if \(!force && transition === 'wait'\) \{[\s\S]{0,160}this\.followWhenIdle\(\)/)
92
115
  })
93
116
  })
94
117
 
@@ -100,14 +100,24 @@ describe('what a Tab writes in', () => {
100
100
  expect(composerSuggestion({ started: false, modeExamples: [{ items: [] }, ...mode] })).toBe('현재 Figure의 재질 그룹을 두 개로 줄여줘')
101
101
  })
102
102
 
103
- it('★ nothing is suggested once the conversation is under way, or when no one gave an example', () => {
103
+ it('★ after an answer the box suggests the next request that answer named - the opening example no longer fits', () => {
104
+ expect(composerSuggestion({ started: true, modeExamples: mode, hostExamples: host, answered: '날개를 5개로 늘려 줘' })).toBe('날개를 5개로 늘려 줘')
104
105
  expect(composerSuggestion({ started: true, modeExamples: mode, hostExamples: host })).toBeUndefined()
106
+ expect(composerSuggestion({ started: true, answered: ' ' })).toBeUndefined()
105
107
  expect(composerSuggestion({ started: false })).toBeUndefined()
106
108
  })
107
109
 
108
110
  it('★ the box shows the suggestion first and falls back to how to write', () => {
109
111
  expect(chat).toMatch(/placeholder=\$\{this\.suggestion \?\? this\.conversation\.placeholder \?\? this\.placeholder \?\? i18next\.t\('ai-assistant\.text\.input-placeholder'\)\}/)
110
- expect(chat).toMatch(/composerSuggestion\(\{ started: this\.lines\.length > 0, modeExamples: this\.modeView\?\.examples, hostExamples: this\.examples \}\)/)
112
+ expect(chat).toMatch(/answered: this\.nextRequest/)
113
+ })
114
+
115
+ it('★ the answer\'s next request comes from the door and is dropped once the person sends or the conversation changes', () => {
116
+ expect(chat).toMatch(/assistantChat\(input: \$input\) \{\s+reply\s+suggestion/)
117
+ expect(chat).toMatch(/this\.nextRequest = typeof out\.suggestion === 'string'/)
118
+ const send = chat.slice(chat.indexOf(' private async send() {'), chat.indexOf(' private async send() {') + 600)
119
+ expect(send).toMatch(/this\.nextRequest = undefined/)
120
+ expect(chat).toMatch(/changed\.has\('sessionId'\)[^\n]*this\.nextRequest = undefined/)
111
121
  })
112
122
  })
113
123
 
@@ -1,86 +0,0 @@
1
- /*
2
- * 화면이 무엇을 여는가 — ADR-0072 A17 ②.
3
- *
4
- * 한 subject 종류에 기본 대화가 없거나 둘이면 화면이 여는 대화는 부팅 순서가 정한다. 그것은 화면을
5
- * 열어봐야 알 수 있고, 모듈 하나가 늘면 소리 없이 바뀐다. 부팅 보고가 말하게 하고, 여기서 셋 다
6
- * 문다.
7
- */
8
- import {
9
- assistantModesForSubject,
10
- clearAssistantModes,
11
- getAssistantModeWarnings,
12
- registerAssistantMode
13
- } from './registry'
14
-
15
- const persona = 'you are here'
16
-
17
- describe('assistant mode — subjects and defaults', () => {
18
- beforeEach(() => clearAssistantModes())
19
- afterAll(() => clearAssistantModes())
20
-
21
- it('★ a subject nobody claims as default is a boot warning, not a silent pick', () => {
22
- registerAssistantMode({ kind: 'twin.space', systemPrompt: persona, subjects: ['space'] })
23
-
24
- const codes = getAssistantModeWarnings().map(w => w.code)
25
- expect(codes).toContain('assistant-mode-subject-without-default')
26
- })
27
-
28
- it('★ two defaults on one subject is a boot warning — boot order must not decide it', () => {
29
- registerAssistantMode({
30
- kind: 'twin.space',
31
- systemPrompt: persona,
32
- subjects: ['board'],
33
- defaultFor: ['board'],
34
- labelKey: 'a'
35
- })
36
- registerAssistantMode({
37
- kind: 'twin.authoring',
38
- systemPrompt: persona,
39
- subjects: ['board'],
40
- defaultFor: ['board'],
41
- labelKey: 'b'
42
- })
43
-
44
- const twice = getAssistantModeWarnings().find(w => w.code === 'assistant-mode-subject-default-twice')
45
- expect(twice?.params.kinds).toEqual(['twin.space', 'twin.authoring'])
46
- })
47
-
48
- it('★ a conversation offered beside another needs something to call it', () => {
49
- registerAssistantMode({
50
- kind: 'twin.space',
51
- systemPrompt: persona,
52
- subjects: ['board'],
53
- defaultFor: ['board'],
54
- labelKey: 'twin.ai.mode.space'
55
- })
56
- registerAssistantMode({ kind: 'twin.authoring', systemPrompt: persona, subjects: ['board'] })
57
-
58
- const unnamed = getAssistantModeWarnings().find(w => w.code === 'assistant-mode-offered-without-label')
59
- expect(unnamed?.params.kind).toBe('twin.authoring')
60
- })
61
-
62
- it('one conversation alone on its subject needs no label — there is nothing to choose between', () => {
63
- registerAssistantMode({
64
- kind: 'plant.station',
65
- systemPrompt: persona,
66
- subjects: ['station'],
67
- defaultFor: ['station']
68
- })
69
-
70
- expect(getAssistantModeWarnings()).toEqual([])
71
- })
72
-
73
- it('the default comes first, so a screen can open the list head without knowing the rule', () => {
74
- registerAssistantMode({ kind: 'twin.authoring', systemPrompt: persona, subjects: ['board'], labelKey: 'b' })
75
- registerAssistantMode({
76
- kind: 'twin.space',
77
- systemPrompt: persona,
78
- subjects: ['board'],
79
- defaultFor: ['board'],
80
- labelKey: 'a'
81
- })
82
-
83
- expect(assistantModesForSubject('board').map(m => m.kind)).toEqual(['twin.space', 'twin.authoring'])
84
- expect(assistantModesForSubject('nothing-shows-this')).toEqual([])
85
- })
86
- })