@things-factory/ai-assistant 10.1.26 → 10.1.28
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.
- package/client/components/assistant-chat.ts +653 -59
- package/client/components/assistant-mode.test.ts +86 -55
- package/client/components/assistant-mode.ts +124 -82
- package/client/components/chat-echo-dedup.test.ts +23 -0
- package/client/components/chat-echo-dedup.ts +29 -3
- package/client/components/chat-input-builder.test.ts +20 -20
- package/client/components/chat-input-builder.ts +16 -14
- package/client/components/chat-proposal-survival.test.ts +73 -0
- package/client/components/mention-popup.ts +31 -8
- package/client/components/panel-aria.test.ts +34 -0
- package/client/components/proposal-outcome-line.ts +33 -0
- package/client/utils/assistant-session-controller.test.ts +75 -0
- package/client/utils/assistant-session-controller.ts +23 -0
- package/client/utils/assistant-session-transport.ts +11 -3
- package/dist-client/components/assistant-chat.d.ts +128 -13
- package/dist-client/components/assistant-chat.js +613 -67
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/assistant-mode.d.ts +80 -68
- package/dist-client/components/assistant-mode.js +63 -19
- package/dist-client/components/assistant-mode.js.map +1 -1
- package/dist-client/components/assistant-mode.test.js +74 -53
- package/dist-client/components/assistant-mode.test.js.map +1 -1
- package/dist-client/components/chat-echo-dedup.d.ts +2 -0
- package/dist-client/components/chat-echo-dedup.js +27 -4
- package/dist-client/components/chat-echo-dedup.js.map +1 -1
- package/dist-client/components/chat-echo-dedup.test.js +20 -0
- package/dist-client/components/chat-echo-dedup.test.js.map +1 -1
- package/dist-client/components/chat-input-builder.d.ts +12 -7
- package/dist-client/components/chat-input-builder.js +8 -10
- package/dist-client/components/chat-input-builder.js.map +1 -1
- package/dist-client/components/chat-input-builder.test.js +17 -18
- package/dist-client/components/chat-input-builder.test.js.map +1 -1
- package/dist-client/components/chat-proposal-survival.test.d.ts +1 -0
- package/dist-client/components/chat-proposal-survival.test.js +61 -0
- package/dist-client/components/chat-proposal-survival.test.js.map +1 -0
- package/dist-client/components/mention-popup.js +28 -8
- package/dist-client/components/mention-popup.js.map +1 -1
- package/dist-client/components/panel-aria.test.d.ts +1 -0
- package/dist-client/components/panel-aria.test.js +31 -0
- package/dist-client/components/panel-aria.test.js.map +1 -0
- package/dist-client/components/proposal-outcome-line.d.ts +9 -0
- package/dist-client/components/proposal-outcome-line.js +15 -0
- package/dist-client/components/proposal-outcome-line.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/assistant-session-controller.d.ts +10 -0
- package/dist-client/utils/assistant-session-controller.js +16 -0
- package/dist-client/utils/assistant-session-controller.js.map +1 -1
- package/dist-client/utils/assistant-session-controller.test.d.ts +1 -0
- package/dist-client/utils/assistant-session-controller.test.js +56 -0
- package/dist-client/utils/assistant-session-controller.test.js.map +1 -0
- package/dist-client/utils/assistant-session-transport.js +11 -3
- package/dist-client/utils/assistant-session-transport.js.map +1 -1
- package/dist-server/service/assistant-capability-resolver.d.ts +17 -0
- package/dist-server/service/assistant-capability-resolver.js +88 -0
- package/dist-server/service/assistant-capability-resolver.js.map +1 -0
- package/dist-server/service/assistant-chat-resolver.d.ts +6 -6
- package/dist-server/service/assistant-chat-resolver.js +166 -44
- package/dist-server/service/assistant-chat-resolver.js.map +1 -1
- package/dist-server/service/assistant-mode/assistant-mode-resolver.d.ts +25 -0
- package/dist-server/service/assistant-mode/assistant-mode-resolver.js +140 -0
- package/dist-server/service/assistant-mode/assistant-mode-resolver.js.map +1 -0
- package/dist-server/service/assistant-mode/index.d.ts +9 -0
- package/dist-server/service/assistant-mode/index.js +19 -0
- package/dist-server/service/assistant-mode/index.js.map +1 -0
- package/dist-server/service/assistant-mode/registry.d.ts +18 -0
- package/dist-server/service/assistant-mode/registry.js +136 -0
- package/dist-server/service/assistant-mode/registry.js.map +1 -0
- package/dist-server/service/assistant-mode/resolve.d.ts +33 -0
- package/dist-server/service/assistant-mode/resolve.js +60 -0
- package/dist-server/service/assistant-mode/resolve.js.map +1 -0
- package/dist-server/service/assistant-mode/types.d.ts +99 -0
- package/dist-server/service/assistant-mode/types.js +12 -0
- package/dist-server/service/assistant-mode/types.js.map +1 -0
- package/dist-server/service/chat-message/carried-turns.d.ts +48 -0
- package/dist-server/service/chat-message/carried-turns.js +80 -0
- package/dist-server/service/chat-message/carried-turns.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-subscription.d.ts +1 -1
- package/dist-server/service/chat-message/chat-message-subscription.js +24 -6
- package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -1
- package/dist-server/service/chat-message/chat-message.d.ts +32 -0
- package/dist-server/service/chat-message/chat-message.js +17 -0
- package/dist-server/service/chat-message/chat-message.js.map +1 -1
- package/dist-server/service/chat-message/door-access.d.ts +15 -0
- package/dist-server/service/chat-message/door-access.js +26 -0
- package/dist-server/service/chat-message/door-access.js.map +1 -0
- package/dist-server/service/chat-message/fold-session-history.d.ts +30 -0
- package/dist-server/service/chat-message/fold-session-history.js +133 -0
- package/dist-server/service/chat-message/fold-session-history.js.map +1 -0
- package/dist-server/service/chat-message/llm-history.d.ts +7 -0
- package/dist-server/service/chat-message/llm-history.js +23 -11
- package/dist-server/service/chat-message/llm-history.js.map +1 -1
- package/dist-server/service/chat-message/message-doors.d.ts +29 -0
- package/dist-server/service/chat-message/message-doors.js +107 -0
- package/dist-server/service/chat-message/message-doors.js.map +1 -0
- package/dist-server/service/chat-message/proposal-outcome.d.ts +18 -0
- package/dist-server/service/chat-message/proposal-outcome.js +31 -0
- package/dist-server/service/chat-message/proposal-outcome.js.map +1 -0
- package/dist-server/service/chat-message/reader-gate.d.ts +3 -0
- package/dist-server/service/chat-message/reader-gate.js +37 -0
- package/dist-server/service/chat-message/reader-gate.js.map +1 -0
- package/dist-server/service/chat-message/room-message.d.ts +4 -0
- package/dist-server/service/chat-message/room-message.js +25 -0
- package/dist-server/service/chat-message/room-message.js.map +1 -0
- package/dist-server/service/chat-session/chat-session.d.ts +24 -0
- package/dist-server/service/chat-session/chat-session.js +26 -2
- package/dist-server/service/chat-session/chat-session.js.map +1 -1
- package/dist-server/service/chat-session/session-inbox.d.ts +4 -1
- package/dist-server/service/chat-session/session-inbox.js +3 -2
- package/dist-server/service/chat-session/session-inbox.js.map +1 -1
- package/dist-server/service/chat-session/session-visibility.d.ts +15 -0
- package/dist-server/service/chat-session/session-visibility.js +28 -0
- package/dist-server/service/chat-session/session-visibility.js.map +1 -0
- package/dist-server/service/chat-session/station-session-guard.d.ts +1 -5
- package/dist-server/service/chat-session/station-session-guard.js +7 -2
- package/dist-server/service/chat-session/station-session-guard.js.map +1 -1
- package/dist-server/service/chat-session-participant/mention-invite.d.ts +29 -0
- package/dist-server/service/chat-session-participant/mention-invite.js +103 -0
- package/dist-server/service/chat-session-participant/mention-invite.js.map +1 -0
- package/dist-server/service/chat-session-resolver.d.ts +15 -0
- package/dist-server/service/chat-session-resolver.js +267 -15
- package/dist-server/service/chat-session-resolver.js.map +1 -1
- package/dist-server/service/grounding-sources.d.ts +14 -0
- package/dist-server/service/grounding-sources.js +26 -0
- package/dist-server/service/grounding-sources.js.map +1 -0
- package/dist-server/service/index.d.ts +8 -1
- package/dist-server/service/index.js +19 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/request-translator.d.ts +2 -0
- package/dist-server/service/request-translator.js +20 -0
- package/dist-server/service/request-translator.js.map +1 -0
- package/dist-server/service/testing/conversation-script.d.ts +123 -0
- package/dist-server/service/testing/conversation-script.js +202 -0
- package/dist-server/service/testing/conversation-script.js.map +1 -0
- package/dist-server/service/testing/mode-conformance.d.ts +40 -0
- package/dist-server/service/testing/mode-conformance.js +90 -0
- package/dist-server/service/testing/mode-conformance.js.map +1 -0
- package/dist-server/tool-registry-boot-report.d.ts +3 -0
- package/dist-server/tool-registry-boot-report.js +25 -2
- package/dist-server/tool-registry-boot-report.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/server/explicit-graphql-types.test.ts +56 -0
- package/server/service/assistant-capability-resolver.ts +66 -0
- package/server/service/assistant-chat-resolver.ts +184 -42
- package/server/service/assistant-mode/assistant-mode-resolver.ts +102 -0
- package/server/service/assistant-mode/index.ts +16 -0
- package/server/service/assistant-mode/registry.test.ts +105 -0
- package/server/service/assistant-mode/registry.ts +147 -0
- package/server/service/assistant-mode/resolve.test.ts +78 -0
- package/server/service/assistant-mode/resolve.ts +96 -0
- package/server/service/assistant-mode/subject-defaults.test.ts +86 -0
- package/server/service/assistant-mode/types.ts +103 -0
- package/server/service/chat-message/carried-turns.test.ts +69 -0
- package/server/service/chat-message/carried-turns.ts +99 -0
- package/server/service/chat-message/chat-message-subscription.ts +20 -4
- package/server/service/chat-message/chat-message.ts +44 -0
- package/server/service/chat-message/door-access.ts +45 -0
- package/server/service/chat-message/fold-session-history.test.ts +98 -0
- package/server/service/chat-message/fold-session-history.ts +115 -0
- package/server/service/chat-message/llm-history.test.ts +58 -7
- package/server/service/chat-message/llm-history.ts +30 -11
- package/server/service/chat-message/message-doors.test.ts +87 -0
- package/server/service/chat-message/message-doors.ts +117 -0
- package/server/service/chat-message/proposal-outcome.test.ts +48 -0
- package/server/service/chat-message/proposal-outcome.ts +42 -0
- package/server/service/chat-message/reader-gate.ts +42 -0
- package/server/service/chat-message/room-message.test.ts +21 -0
- package/server/service/chat-message/room-message.ts +22 -0
- package/server/service/chat-session/chat-session.ts +31 -1
- package/server/service/chat-session/session-inbox.test.ts +18 -0
- package/server/service/chat-session/session-inbox.ts +9 -2
- package/server/service/chat-session/session-visibility.test.ts +36 -0
- package/server/service/chat-session/session-visibility.ts +26 -0
- package/server/service/chat-session/station-session-guard.ts +10 -2
- package/server/service/chat-session-participant/mention-invite.test.ts +125 -0
- package/server/service/chat-session-participant/mention-invite.ts +121 -0
- package/server/service/chat-session-resolver.ts +272 -15
- package/server/service/grounding-sources.test.ts +55 -0
- package/server/service/grounding-sources.ts +46 -0
- package/server/service/index.ts +19 -0
- package/server/service/request-translator.test.ts +45 -0
- package/server/service/request-translator.ts +33 -0
- package/server/service/station-session-guard.test.ts +12 -5
- package/server/service/testing/conversation-scenario.test.ts +163 -0
- package/server/service/testing/conversation-script.test.ts +131 -0
- package/server/service/testing/conversation-script.ts +288 -0
- package/server/service/testing/mode-conformance.ts +107 -0
- package/server/tool-registry-boot-report.ts +25 -2
- package/test/assistant-chat-call-sites.test.ts +82 -0
- package/translations/en.json +44 -2
- package/translations/ja.json +44 -2
- package/translations/ko.json +44 -2
- package/translations/ms.json +44 -2
- package/translations/zh.json +44 -2
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* A mode is one value, and the point of it is that the parts of a conversation cannot
|
|
3
|
-
*
|
|
2
|
+
* A mode is one value, and the point of it is that the parts of a conversation cannot disagree
|
|
3
|
+
* with each other. Since ADR-0061 decision 7 the parts live on the server: the browser names the
|
|
4
|
+
* conversation (`kind`) and its subject (`target`), and the persona, the tool categories, the
|
|
5
|
+
* model and the limits are the app server's.
|
|
4
6
|
*
|
|
5
|
-
* The defect this guards against shipped: a screen whose
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* The defect this still guards against shipped: a screen whose prompt said "you do not edit boards
|
|
8
|
+
* here" offered `/add-monitor` in the same panel, because the prompt was an element property and
|
|
9
|
+
* the slash list was a module global. A mode's wording now comes from the same declaration the
|
|
10
|
+
* door reads, so the two cannot describe different conversations.
|
|
9
11
|
*/
|
|
10
|
-
import { isDifferentConversation, resolveConversation, type AssistantMode } from './assistant-mode'
|
|
11
|
-
|
|
12
|
-
const MODE: AssistantMode = {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
slashTemplates: [{ name: 'status', template: '이 공간의 지금 상태를 요약해줘' }]
|
|
12
|
+
import { isDifferentConversation, resolveConversation, type AssistantMode, pickInitialMode } from './assistant-mode'
|
|
13
|
+
|
|
14
|
+
const MODE: AssistantMode = { kind: 'twin.space', target: 'S-12', hostContext: { spaceId: 'S-12' } }
|
|
15
|
+
|
|
16
|
+
/** What the server said this mode offers, already in the request's locale. */
|
|
17
|
+
const MODE_VIEW = {
|
|
18
|
+
kind: 'twin.space',
|
|
19
|
+
slashTemplates: [{ name: 'status', description: 'summarize', template: '이 공간의 지금 상태를 요약해줘' }],
|
|
20
|
+
catalogEntries: [],
|
|
21
|
+
proposalChoices: [],
|
|
22
|
+
footerNotice: undefined,
|
|
23
|
+
stagedNotice: undefined
|
|
18
24
|
}
|
|
19
25
|
|
|
26
|
+
/** The board product, which declares no mode and names its own door. */
|
|
20
27
|
const HOST_PROPERTIES = {
|
|
21
|
-
systemPrompt: 'a prompt the host set the old way',
|
|
22
|
-
toolCategories: ['board-ai'],
|
|
23
28
|
hostContext: { boardId: 'B-1' },
|
|
24
29
|
chatEndpoint: 'boardAIChat' as const
|
|
25
30
|
}
|
|
@@ -27,71 +32,97 @@ const HOST_PROPERTIES = {
|
|
|
27
32
|
const GLOBAL_DEFAULTS = {
|
|
28
33
|
catalogEntries: [{ type: 'rect' }],
|
|
29
34
|
slashTemplates: [{ name: 'add-monitor', template: '모니터를 추가해줘' }],
|
|
30
|
-
|
|
35
|
+
footerNotice: 'previewed on the board'
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
describe('
|
|
34
|
-
test('
|
|
35
|
-
const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS)
|
|
36
|
-
|
|
37
|
-
expect(c.
|
|
38
|
+
describe('a mode names the conversation; the server says what it offers', () => {
|
|
39
|
+
test('★ the subject travels, and the door is the neutral one', () => {
|
|
40
|
+
const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS, MODE_VIEW)
|
|
41
|
+
|
|
42
|
+
expect(c.kind).toBe('twin.space')
|
|
43
|
+
expect(c.target).toBe('S-12')
|
|
38
44
|
expect(c.hostContext).toEqual({ spaceId: 'S-12' })
|
|
45
|
+
expect(c.chatEndpoint).toBe('assistantChat')
|
|
39
46
|
})
|
|
40
47
|
|
|
41
|
-
test('
|
|
42
|
-
const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS)
|
|
43
|
-
expect(c.slashTemplates).toEqual(MODE.slashTemplates)
|
|
44
|
-
/* A mode that says it does not edit boards must not offer a board edit. */
|
|
45
|
-
expect(JSON.stringify(c.slashTemplates)).not.toContain('add-monitor')
|
|
46
|
-
})
|
|
48
|
+
test('★ what the mode offers comes from the server, not from what another package registered', () => {
|
|
49
|
+
const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS, MODE_VIEW)
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
/* The board's `@` catalog and its "previewed on the board" line belong to board authoring. */
|
|
51
|
+
expect(c.slashTemplates).toEqual(MODE_VIEW.slashTemplates)
|
|
52
|
+
expect(JSON.stringify(c.slashTemplates)).not.toContain('add-monitor')
|
|
51
53
|
expect(c.catalogEntries).toEqual([])
|
|
52
|
-
expect(c.
|
|
54
|
+
expect(c.footerNotice).toBeUndefined()
|
|
53
55
|
})
|
|
54
56
|
|
|
55
|
-
test('★
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const c = resolveConversation({ ...withoutSlash, id: 'plant:factory:D-1' }, HOST_PROPERTIES, GLOBAL_DEFAULTS)
|
|
57
|
+
test('★ before the server has answered, a mode offers nothing — the defaults do not stand in', () => {
|
|
58
|
+
const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS, undefined)
|
|
59
|
+
|
|
59
60
|
expect(c.slashTemplates).toEqual([])
|
|
61
|
+
expect(c.catalogEntries).toEqual([])
|
|
60
62
|
})
|
|
61
63
|
|
|
62
|
-
test('
|
|
63
|
-
const c = resolveConversation({
|
|
64
|
-
|
|
65
|
-
expect(c.
|
|
66
|
-
expect(c.hostContext).toBeUndefined()
|
|
64
|
+
test('a view for another kind is ignored — the panel is showing this conversation', () => {
|
|
65
|
+
const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS, { ...MODE_VIEW, kind: 'twin.board' })
|
|
66
|
+
|
|
67
|
+
expect(c.slashTemplates).toEqual([])
|
|
67
68
|
})
|
|
69
|
+
})
|
|
68
70
|
|
|
69
|
-
|
|
71
|
+
describe('a host with no mode', () => {
|
|
72
|
+
test('gets its own properties and the registered defaults, as before modes existed', () => {
|
|
70
73
|
const c = resolveConversation(undefined, HOST_PROPERTIES, GLOBAL_DEFAULTS)
|
|
71
|
-
|
|
72
|
-
expect(c.
|
|
74
|
+
|
|
75
|
+
expect(c.hostContext).toEqual({ boardId: 'B-1' })
|
|
73
76
|
expect(c.slashTemplates).toEqual(GLOBAL_DEFAULTS.slashTemplates)
|
|
77
|
+
expect(c.footerNotice).toBe('previewed on the board')
|
|
78
|
+
expect(c.chatEndpoint).toBe('boardAIChat')
|
|
74
79
|
})
|
|
75
80
|
|
|
76
|
-
test('
|
|
77
|
-
|
|
78
|
-
expect(c.catalogEntries).toEqual([])
|
|
79
|
-
expect(c.slashTemplates).toEqual([])
|
|
80
|
-
expect(c.proposalChoices).toEqual([])
|
|
81
|
-
expect(c.chatEndpoint).toBe('boardAIChat')
|
|
81
|
+
test('★ must name its door — there is no default any more', () => {
|
|
82
|
+
expect(() => resolveConversation(undefined, {}, {})).toThrow(/chatEndpoint/)
|
|
82
83
|
})
|
|
83
84
|
})
|
|
84
85
|
|
|
85
86
|
describe('isDifferentConversation', () => {
|
|
86
|
-
const station3: AssistantMode = {
|
|
87
|
+
const station3: AssistantMode = { kind: 'plant.station', target: '3' }
|
|
87
88
|
|
|
88
|
-
test('a different subject ends the conversation', () => {
|
|
89
|
-
expect(isDifferentConversation(station3, {
|
|
89
|
+
test('★ a different subject ends the conversation, and so does a different kind', () => {
|
|
90
|
+
expect(isDifferentConversation(station3, { kind: 'plant.station', target: '7' })).toBe(true)
|
|
91
|
+
expect(isDifferentConversation(station3, { kind: 'plant.factory', target: '3' })).toBe(true)
|
|
90
92
|
expect(isDifferentConversation(undefined, station3)).toBe(true)
|
|
91
93
|
})
|
|
92
94
|
|
|
93
|
-
test('
|
|
94
|
-
expect(isDifferentConversation(station3, { ...station3,
|
|
95
|
+
test('the same subject, described again, does not', () => {
|
|
96
|
+
expect(isDifferentConversation(station3, { ...station3, hostContext: { reread: true } })).toBe(false)
|
|
95
97
|
expect(isDifferentConversation(undefined, undefined)).toBe(false)
|
|
96
98
|
})
|
|
97
99
|
})
|
|
100
|
+
|
|
101
|
+
/*
|
|
102
|
+
* ADR-0072 A17 ① — 화면을 열 때 어느 대화로 서나.
|
|
103
|
+
*
|
|
104
|
+
* 되살리는 것은 고른 대화이지 세션이 아니다. 그리고 없어진 대화로 되살리지 않는다 — 그러면 대화가
|
|
105
|
+
* 안 열리고 사람은 자기가 무엇을 잘못했는지 알 길이 없다.
|
|
106
|
+
*/
|
|
107
|
+
describe('pickInitialMode', () => {
|
|
108
|
+
const choices = [
|
|
109
|
+
{ kind: 'twin.board', label: '이 그림 고치기', isDefault: true },
|
|
110
|
+
{ kind: 'twin.space', label: '이 현장 지금', isDefault: false }
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
it('★ 지난번에 고른 대화로 돌아간다 — 매번 같은 손질을 다시 하지 않는다', () => {
|
|
114
|
+
expect(pickInitialMode('twin.board', choices, 'twin.space')).toBe('twin.space')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('★ 지금 목록에 없는 기억은 따르지 않는다 — 안 열리는 대화로 들어간다', () => {
|
|
118
|
+
expect(pickInitialMode('twin.board', choices, 'twin.gone')).toBe('twin.board')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('★ 고를 것이 하나뿐이면 기억을 보지 않는다 — 고른 적이 없는 화면이다', () => {
|
|
122
|
+
expect(pickInitialMode('plant.station', [choices[0]], 'twin.space')).toBe('plant.station')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('기억이 없으면 host 가 준 것으로 선다', () => {
|
|
126
|
+
expect(pickInitialMode('twin.board', choices, undefined)).toBe('twin.board')
|
|
127
|
+
})
|
|
128
|
+
})
|
|
@@ -1,130 +1,172 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A mode is what a conversation *is*:
|
|
3
|
-
* about, what it offers to start with, and when it is over.
|
|
2
|
+
* A mode is what a conversation *is*: which conversation this is, and what it is about.
|
|
4
3
|
*
|
|
5
|
-
* Those
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* So a host declares one value and hands it over. What cannot be split cannot fall out of step.
|
|
4
|
+
* Those two used to be nine. The browser declared the persona, the tool categories, the model and
|
|
5
|
+
* the limits and sent them with every turn, which meant a caller holding the door's privilege
|
|
6
|
+
* chose its own authority — and it meant a screen could describe a conversation one way while the
|
|
7
|
+
* server answered it another. Since ADR-0061 decision 7 the app server registers the mode and the
|
|
8
|
+
* browser names it:
|
|
11
9
|
*
|
|
12
10
|
* ```ts
|
|
13
|
-
*
|
|
14
|
-
* id: `twin:space:${spaceId}`,
|
|
15
|
-
* systemPrompt: '...',
|
|
16
|
-
* toolCategories: ['twin'],
|
|
17
|
-
* hostContext: { spaceId },
|
|
18
|
-
* slashTemplates: TWIN_SLASH_TEMPLATES
|
|
19
|
-
* })
|
|
20
|
-
*
|
|
21
|
-
* html`<ox-assistant-chat .mode=${SPACE_MODE(this.spaceId)}></ox-assistant-chat>`
|
|
11
|
+
* html`<ox-assistant-chat .mode=${{ kind: 'twin.space', target: spaceId, hostContext: { spaceId } }}></ox-assistant-chat>`
|
|
22
12
|
* ```
|
|
13
|
+
*
|
|
14
|
+
* What the panel shows — the `/` sentences, the `@` catalog, the two notices — comes from the same
|
|
15
|
+
* declaration, fetched from the server already written in the person's language, so the panel and
|
|
16
|
+
* the door cannot offer different conversations.
|
|
23
17
|
*/
|
|
24
18
|
export interface AssistantMode {
|
|
25
19
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* **The conversation's lifetime hangs on this.** When it changes, the chat closes what is on
|
|
29
|
-
* screen and starts again — because a conversation about station 3 is not a conversation
|
|
30
|
-
* about station 7, and leaving the old one up means the user reads an answer about a screen
|
|
31
|
-
* they are no longer looking at.
|
|
32
|
-
*
|
|
33
|
-
* Hosts built this by hand before, each in their own way: plant kept a module-global registry
|
|
34
|
-
* with subscribers and re-checked the context on every send, twin closed its session when the
|
|
35
|
-
* anchor moved. Both were writing the same guard around the same absence.
|
|
36
|
-
*
|
|
37
|
-
* Include whatever makes the subject specific — `twin:space:S-12`, `plant:station:3`.
|
|
20
|
+
* Which conversation this is — a `<app>.<surface>` kind the app server registered.
|
|
38
21
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
/** Who the assistant is here, and what it must not claim to do. */
|
|
42
|
-
systemPrompt?: string
|
|
43
|
-
|
|
44
|
-
/** Which tool categories the server may expose for this conversation. */
|
|
45
|
-
toolCategories?: string[]
|
|
46
|
-
|
|
47
|
-
/** What the host knows about the subject; travels with each request. */
|
|
48
|
-
hostContext?: any
|
|
49
|
-
|
|
50
|
-
/** Which server-side chat entry point answers. */
|
|
51
|
-
chatEndpoint?: 'boardAIChat' | 'assistantChat'
|
|
52
|
-
|
|
53
|
-
/** What `@` offers. */
|
|
54
|
-
catalogEntries?: any[]
|
|
22
|
+
kind: string
|
|
55
23
|
|
|
56
24
|
/**
|
|
57
|
-
* What
|
|
25
|
+
* What it is about: the space, the station, the model being edited.
|
|
58
26
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
27
|
+
* **With `kind`, this is the conversation's lifetime.** When either changes the chat closes what
|
|
28
|
+
* is on screen and starts again — a conversation about station 3 is not a conversation about
|
|
29
|
+
* station 7, and leaving the old one up means the person reads an answer about a screen they are
|
|
30
|
+
* no longer looking at.
|
|
62
31
|
*/
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
/** The i18n key for the line under an empty conversation. */
|
|
66
|
-
footerNoticeKey?: string
|
|
32
|
+
target?: string
|
|
67
33
|
|
|
68
|
-
/**
|
|
69
|
-
|
|
34
|
+
/**
|
|
35
|
+
* What the host knows about the subject; travels with each request.
|
|
36
|
+
*
|
|
37
|
+
* **The model never sees this** — it reaches the tools as `ctx.host` and nothing else (ADR-0061
|
|
38
|
+
* decision 7). Anything the model must know is answered by a read tool.
|
|
39
|
+
*/
|
|
40
|
+
hostContext?: any
|
|
41
|
+
}
|
|
70
42
|
|
|
71
|
-
|
|
72
|
-
|
|
43
|
+
/** What the server said this mode offers, already in the request's locale. */
|
|
44
|
+
export interface AssistantModeView {
|
|
45
|
+
kind: string
|
|
46
|
+
slashTemplates: any[]
|
|
47
|
+
catalogEntries: any[]
|
|
48
|
+
proposalChoices: any[]
|
|
49
|
+
placeholder?: string
|
|
50
|
+
footerNotice?: string
|
|
51
|
+
stagedNotice?: string
|
|
73
52
|
}
|
|
74
53
|
|
|
75
54
|
/** True when these two describe different conversations, so the old one should be closed. */
|
|
76
55
|
export function isDifferentConversation(a?: AssistantMode, b?: AssistantMode): boolean {
|
|
77
|
-
return
|
|
56
|
+
return `${a?.kind ?? ''}|${a?.target ?? ''}` !== `${b?.kind ?? ''}|${b?.target ?? ''}`
|
|
78
57
|
}
|
|
79
58
|
|
|
80
|
-
/** Everything a conversation needs, after the mode, the host's own properties
|
|
81
|
-
*
|
|
59
|
+
/** Everything a conversation needs, after the mode, the host's own properties, the registered
|
|
60
|
+
* defaults and the server's answer have been reconciled. */
|
|
82
61
|
export interface ConversationSettings {
|
|
83
|
-
|
|
84
|
-
|
|
62
|
+
/** Set when a mode names this conversation; absent for a host that declares none. */
|
|
63
|
+
kind?: string
|
|
64
|
+
target?: string
|
|
85
65
|
hostContext?: any
|
|
86
66
|
chatEndpoint: 'boardAIChat' | 'assistantChat'
|
|
87
67
|
catalogEntries: any[]
|
|
88
68
|
slashTemplates: any[]
|
|
89
|
-
|
|
69
|
+
/** What the empty input says. A mode's comes translated from the server. */
|
|
70
|
+
placeholder?: string
|
|
71
|
+
/** Sentences, not keys — a mode's come translated from the server, a host's from its own i18n. */
|
|
72
|
+
footerNotice?: string
|
|
90
73
|
proposalChoices: any[]
|
|
91
|
-
|
|
74
|
+
stagedNotice?: string
|
|
92
75
|
}
|
|
93
76
|
|
|
94
77
|
/**
|
|
95
78
|
* Reconcile the places a setting can come from.
|
|
96
79
|
*
|
|
97
|
-
* **A host that declares a mode gets the mode and nothing else.**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* wants those declares them.
|
|
80
|
+
* **A host that declares a mode gets the mode and nothing else.** What the mode does not offer is
|
|
81
|
+
* not filled in from the element's own properties or from the defaults another package registered.
|
|
82
|
+
* Filling it back in is how the defect this type removes returned — board-ai registers board
|
|
83
|
+
* authoring's `/add-monitor`, its component `@` catalog and its "previewed on the board" line
|
|
84
|
+
* globally, and plant's factory mode, which declares no slash list, offered them on a factory
|
|
85
|
+
* screen once the board modeller had been loaded.
|
|
104
86
|
*
|
|
105
|
-
* A
|
|
106
|
-
*
|
|
87
|
+
* A mode's offering arrives asynchronously, so until the server has answered a mode offers
|
|
88
|
+
* nothing. Standing the defaults in during that gap would show the board's list for a moment on a
|
|
89
|
+
* screen that does not edit boards.
|
|
107
90
|
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
91
|
+
* A host that declares no mode gets exactly what it got before modes existed — its own properties,
|
|
92
|
+
* falling back to the registered defaults — and it must name its own door: the `boardAIChat`
|
|
93
|
+
* default was removed with decision 7, because a missing door should show when a surface is
|
|
94
|
+
* declared, not when a person says something.
|
|
110
95
|
*/
|
|
111
96
|
export function resolveConversation(
|
|
112
97
|
mode: AssistantMode | undefined,
|
|
113
98
|
own: Partial<ConversationSettings>,
|
|
114
|
-
defaults: Partial<ConversationSettings
|
|
99
|
+
defaults: Partial<ConversationSettings>,
|
|
100
|
+
view?: AssistantModeView
|
|
115
101
|
): ConversationSettings {
|
|
102
|
+
if (mode) {
|
|
103
|
+
/* A view for another kind is the previous conversation's answer arriving late. */
|
|
104
|
+
const offered = view && view.kind === mode.kind ? view : undefined
|
|
105
|
+
return {
|
|
106
|
+
kind: mode.kind,
|
|
107
|
+
target: mode.target,
|
|
108
|
+
hostContext: mode.hostContext,
|
|
109
|
+
chatEndpoint: 'assistantChat',
|
|
110
|
+
catalogEntries: offered?.catalogEntries ?? [],
|
|
111
|
+
slashTemplates: offered?.slashTemplates ?? [],
|
|
112
|
+
proposalChoices: offered?.proposalChoices ?? [],
|
|
113
|
+
placeholder: offered?.placeholder,
|
|
114
|
+
footerNotice: offered?.footerNotice,
|
|
115
|
+
stagedNotice: offered?.stagedNotice
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
116
119
|
const pick = <K extends keyof ConversationSettings>(key: K): ConversationSettings[K] =>
|
|
117
|
-
|
|
120
|
+
(own as any)[key] ?? (defaults as any)[key]
|
|
121
|
+
|
|
122
|
+
const chatEndpoint = pick('chatEndpoint')
|
|
123
|
+
if (!chatEndpoint) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
'[ox-assistant-chat] a surface with no mode must name its chatEndpoint. The `boardAIChat` default was ' +
|
|
126
|
+
'removed (ADR-0061 decision 7) — apps declare a mode and talk to `assistantChat`; the board product ' +
|
|
127
|
+
'names `boardAIChat` on the element.'
|
|
128
|
+
)
|
|
129
|
+
}
|
|
118
130
|
|
|
119
131
|
return {
|
|
120
|
-
systemPrompt: pick('systemPrompt'),
|
|
121
|
-
toolCategories: pick('toolCategories'),
|
|
122
132
|
hostContext: pick('hostContext'),
|
|
123
|
-
|
|
133
|
+
placeholder: pick('placeholder'),
|
|
134
|
+
chatEndpoint,
|
|
124
135
|
catalogEntries: pick('catalogEntries') ?? [],
|
|
125
136
|
slashTemplates: pick('slashTemplates') ?? [],
|
|
126
|
-
|
|
137
|
+
footerNotice: pick('footerNotice'),
|
|
127
138
|
proposalChoices: pick('proposalChoices') ?? [],
|
|
128
|
-
|
|
139
|
+
stagedNotice: pick('stagedNotice')
|
|
129
140
|
}
|
|
130
141
|
}
|
|
142
|
+
|
|
143
|
+
/** 한 subject 위에 설 수 있는 대화 하나 — 서버가 준 목록의 한 줄. */
|
|
144
|
+
export interface AssistantModeChoice {
|
|
145
|
+
kind: string
|
|
146
|
+
label: string
|
|
147
|
+
isDefault: boolean
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 이 화면을 열 때 어느 대화로 설 것인가 — ADR-0072 A17 ①.
|
|
152
|
+
*
|
|
153
|
+
* ── 왜 기억하나 ───────────────────────────────────────────────────────────
|
|
154
|
+
* 보드를 열 때마다 저작 대화로 되돌아가면, 현장을 물으려고 온 사람은 화면을 열 때마다 같은 손질을
|
|
155
|
+
* 다시 한다. 기억은 이 브라우저에만 남는다 — 서버에 칸을 만들지 않는다.
|
|
156
|
+
*
|
|
157
|
+
* ── 기억을 언제 버리나 ────────────────────────────────────────────────────
|
|
158
|
+
* 기억한 종류가 지금 목록에 없으면 따르지 않는다. 대화가 하나 없어진 판으로 바뀌었는데 그 이름으로
|
|
159
|
+
* 열려고 하면 대화가 안 열리고, 사람은 자기가 무엇을 잘못했는지 알 길이 없다. 그때는 host 가 준
|
|
160
|
+
* 대화로 둔다.
|
|
161
|
+
*
|
|
162
|
+
* 고를 것이 하나뿐이면 기억을 보지 않는다 — 고른 적이 없는 화면이다.
|
|
163
|
+
*/
|
|
164
|
+
export function pickInitialMode(
|
|
165
|
+
current: string | undefined,
|
|
166
|
+
choices: ReadonlyArray<AssistantModeChoice>,
|
|
167
|
+
remembered: string | undefined
|
|
168
|
+
): string | undefined {
|
|
169
|
+
if (choices.length < 2) return current
|
|
170
|
+
if (!remembered || remembered === current) return current
|
|
171
|
+
return choices.some(choice => choice.kind === remembered) ? remembered : current
|
|
172
|
+
}
|
|
@@ -228,3 +228,26 @@ describe('AI 답변 에코 흡수 — 영속되지 않는 응답 데이터를
|
|
|
228
228
|
expect(r.lines[1].id).toBe('a9')
|
|
229
229
|
})
|
|
230
230
|
})
|
|
231
|
+
|
|
232
|
+
/*
|
|
233
|
+
* ADR-0073 판정 1 — 가려진 줄이 화면까지 가려진 채로 오는가.
|
|
234
|
+
*
|
|
235
|
+
* 서버가 본문을 한 줄로 바꿔 보내도, 클라이언트가 그 사실을 안 들고 오면 말풍선은 그것을 어시스턴트가
|
|
236
|
+
* 쓴 문장으로 그린다. 그러면 「이 답은 … 권한이 필요합니다」가 어시스턴트의 말이 된다.
|
|
237
|
+
*/
|
|
238
|
+
describe('redacted rows reach the line', () => {
|
|
239
|
+
it('★ 서버가 가렸다고 하면 줄도 가려진 줄이다', () => {
|
|
240
|
+
const line = remoteMessageToLine({
|
|
241
|
+
id: 'm1',
|
|
242
|
+
role: 'assistant',
|
|
243
|
+
content: 'this answer needs Query.jobOrders permission',
|
|
244
|
+
redacted: true
|
|
245
|
+
} as any)
|
|
246
|
+
|
|
247
|
+
expect(line.redacted).toBe(true)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('보통 답은 가려지지 않은 줄이다 — 없는 값을 참으로 읽지 않는다', () => {
|
|
251
|
+
expect(remoteMessageToLine({ id: 'm2', role: 'assistant', content: '오늘 양품 1,204' } as any).redacted).toBe(false)
|
|
252
|
+
})
|
|
253
|
+
})
|
|
@@ -31,6 +31,8 @@ export interface RemoteMessage {
|
|
|
31
31
|
groundingWarningsJson?: any
|
|
32
32
|
/** 발신자 — 표시용 name + **자기 여부 판정용 email**(내부 관리키 id 는 쓰지 않는다). */
|
|
33
33
|
creator?: { name?: string; email?: string } | null
|
|
34
|
+
/** 이 사람이 이 답의 문을 못 지나 본문이 한 줄로 바뀌어 왔나(ADR-0073 판정 1). */
|
|
35
|
+
redacted?: boolean
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
/** 방송 메시지 → 라인. */
|
|
@@ -43,7 +45,13 @@ export function remoteMessageToLine(m: RemoteMessage): DedupLine {
|
|
|
43
45
|
toolUsages: Array.isArray(m.toolUsagesJson) ? m.toolUsagesJson : undefined,
|
|
44
46
|
groundingWarnings: Array.isArray(m.groundingWarningsJson) ? m.groundingWarningsJson : undefined,
|
|
45
47
|
senderEmail: m.creator?.email ?? undefined,
|
|
46
|
-
senderName: m.creator?.name ?? undefined
|
|
48
|
+
senderName: m.creator?.name ?? undefined,
|
|
49
|
+
/*
|
|
50
|
+
* 서버가 이 사람에게 값을 안 내준 줄 — ADR-0073 판정 1. `content` 에 온 것은 어시스턴트가 쓴
|
|
51
|
+
* 문장이 아니라 「권한이 필요합니다」 한 줄이다. 그대로 말풍선에 넣으면 어시스턴트가 그렇게 말한
|
|
52
|
+
* 것처럼 읽힌다.
|
|
53
|
+
*/
|
|
54
|
+
redacted: m.redacted === true
|
|
47
55
|
}
|
|
48
56
|
}
|
|
49
57
|
|
|
@@ -140,12 +148,30 @@ export function reconcileSentLine(
|
|
|
140
148
|
patch: Partial<DedupLine>
|
|
141
149
|
): { lines: DedupLine[]; seen: Set<string> } {
|
|
142
150
|
const idx = lines.findIndex(l => l._localId === localId)
|
|
143
|
-
|
|
151
|
+
/* 같은 서버 id 를 든 **다른** 줄 — 에코가 흡수되지 못하고 새 줄로 붙은 경우다. */
|
|
152
|
+
const echoed = serverId ? lines.findIndex((l, i) => i !== idx && l.id === serverId) : -1
|
|
153
|
+
|
|
154
|
+
/*
|
|
155
|
+
* 대기 줄이 없어졌다 — 보내는 동안 대화가 지워졌거나 이력으로 갈렸다. 그래도 **응답이 어느 메시지의
|
|
156
|
+
* 것인지는 안다**(`assistantMessageId`). 그 줄에 합친다.
|
|
157
|
+
*
|
|
158
|
+
* 여기서 그냥 돌아가면, 저장되는 값(본문 · 도구 사용)은 에코가 들고 있으니 화면은 멀쩡해 보이고
|
|
159
|
+
* **저장되지 않는 것만 사라진다** — 제안 카드다. 모델은 「카드를 눌러 적용하세요」라고 말하고 화면에는
|
|
160
|
+
* 누를 것이 없다(2026-09-18 트윈에서 실제로 그랬다).
|
|
161
|
+
*/
|
|
162
|
+
if (idx < 0) {
|
|
163
|
+
if (echoed < 0) return { lines, seen }
|
|
164
|
+
const merged = [...lines]
|
|
165
|
+
merged[echoed] = { ...merged[echoed], ...patch, id: serverId ?? merged[echoed].id }
|
|
166
|
+
return { lines: merged, seen: serverId ? new Set(seen).add(serverId) : seen }
|
|
167
|
+
}
|
|
144
168
|
|
|
145
169
|
const nextLines = [...lines]
|
|
146
170
|
/* seen 만 보고 지우면, 에코를 이 줄에 **흡수**한 경우(id 가 이미 이 줄에 있다) 자기 줄을 지운다.
|
|
147
171
|
* 실제 중복은 "다른 줄이 같은 id 를 가진" 상태일 때뿐이다. */
|
|
148
|
-
if (
|
|
172
|
+
if (echoed >= 0) {
|
|
173
|
+
/* 남는 것은 에코 줄이다. 응답에만 있는 것(제안)을 그 줄에 옮기고 대기 줄을 지운다. */
|
|
174
|
+
nextLines[echoed] = { ...nextLines[echoed], ...patch, id: serverId ?? nextLines[echoed].id }
|
|
149
175
|
nextLines.splice(idx, 1)
|
|
150
176
|
return { lines: nextLines, seen }
|
|
151
177
|
}
|
|
@@ -14,7 +14,7 @@ import { buildAssistantChatInput, buildChatMutationInput } from './chat-input-bu
|
|
|
14
14
|
|
|
15
15
|
const history = [{ role: 'user', content: 'what figures do we have' }]
|
|
16
16
|
|
|
17
|
-
const args = (over: any = {}) => ({ history,
|
|
17
|
+
const args = (over: any = {}) => ({ history, mode: { kind: 'figure.modeller', target: 'fig-1' }, ...over })
|
|
18
18
|
|
|
19
19
|
describe('the neutral door carries no document', () => {
|
|
20
20
|
it('★ no board field reaches assistantChat', () => {
|
|
@@ -25,40 +25,40 @@ describe('the neutral door carries no document', () => {
|
|
|
25
25
|
}
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
-
it('carries the conversation, the session and
|
|
28
|
+
it('carries the conversation, the session and which conversation this is', () => {
|
|
29
29
|
const input = buildAssistantChatInput(args({ sessionId: 's-1' }))
|
|
30
30
|
|
|
31
31
|
expect(input.sessionId).toBe('s-1')
|
|
32
32
|
expect(input.messages).toBe(history)
|
|
33
|
-
expect(input.
|
|
33
|
+
expect(input.mode).toEqual({ kind: 'figure.modeller', target: 'fig-1' })
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
-
it('
|
|
37
|
-
|
|
36
|
+
it('★ the persona, the tools and the limits are not on the wire — they are the server\'s', () => {
|
|
37
|
+
const input = buildAssistantChatInput(args({ sessionId: 's-1' }))
|
|
38
|
+
|
|
39
|
+
for (const field of ['systemPrompt', 'toolCategories', 'model', 'temperature', 'maxTokens', 'requireGroundingTools']) {
|
|
40
|
+
expect(input).not.toHaveProperty(field)
|
|
41
|
+
}
|
|
38
42
|
})
|
|
39
|
-
})
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
expect(() => buildAssistantChatInput(args({ systemPrompt: '' }))).toThrow(/systemPrompt is required/)
|
|
44
|
-
expect(() => buildAssistantChatInput(args({ systemPrompt: ' ' }))).toThrow(/systemPrompt is required/)
|
|
45
|
-
expect(() => buildAssistantChatInput(args({ systemPrompt: undefined }))).toThrow(/systemPrompt is required/)
|
|
44
|
+
it('a one-off question has no session — null, not absent, so the server reads it as ad-hoc', () => {
|
|
45
|
+
expect(buildAssistantChatInput(args()).sessionId).toBeNull()
|
|
46
46
|
})
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
-
describe('
|
|
50
|
-
it('
|
|
51
|
-
expect(buildAssistantChatInput(args())).
|
|
49
|
+
describe('the mode is required', () => {
|
|
50
|
+
it('★ refuses a turn with no mode instead of sending one', () => {
|
|
51
|
+
expect(() => buildAssistantChatInput(args({ mode: { kind: '' } }))).toThrow(/mode.kind is required/)
|
|
52
|
+
expect(() => buildAssistantChatInput(args({ mode: { kind: ' ' } }))).toThrow(/mode.kind is required/)
|
|
53
|
+
expect(() => buildAssistantChatInput(args({ mode: undefined }))).toThrow(/mode.kind is required/)
|
|
52
54
|
})
|
|
53
55
|
|
|
54
|
-
it('
|
|
55
|
-
expect(buildAssistantChatInput(args({
|
|
56
|
+
it('a conversation about nothing in particular carries no target', () => {
|
|
57
|
+
expect(buildAssistantChatInput(args({ mode: { kind: 'twin.none' } })).mode).toEqual({ kind: 'twin.none' })
|
|
56
58
|
})
|
|
59
|
+
})
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
expect(buildAssistantChatInput(args())).not.toHaveProperty('requireGroundingTools')
|
|
60
|
-
expect(buildAssistantChatInput(args({ requireGroundingTools: true })).requireGroundingTools).toBe(true)
|
|
61
|
-
})
|
|
61
|
+
describe('optional fields are sent only when they mean something', () => {
|
|
62
62
|
|
|
63
63
|
it('host context and truncation travel when given', () => {
|
|
64
64
|
const input = buildAssistantChatInput(args({ hostContext: { figureId: 'fig-1' }, truncateAfterMessageId: 'm-9' }))
|