@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/ai-assistant",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.28",
|
|
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.
|
|
33
|
-
"@things-factory/auth-base": "^10.1.
|
|
32
|
+
"@things-factory/ai-client-base": "^10.1.28",
|
|
33
|
+
"@things-factory/auth-base": "^10.1.28",
|
|
34
34
|
"@things-factory/env": "^10.1.20",
|
|
35
|
-
"@things-factory/shell": "^10.1.
|
|
35
|
+
"@things-factory/shell": "^10.1.28",
|
|
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": "
|
|
47
|
+
"gitHead": "3a260a0f79bc6b41a692a103955bfcc541aff60e"
|
|
48
48
|
}
|
|
@@ -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,32 @@ 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 {
|
|
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 { readerGate } from './chat-message/reader-gate.js'
|
|
53
|
+
import { inviteMentionedUsers } from './chat-session-participant/mention-invite.js'
|
|
54
|
+
import { buildInviteNotification } from './chat-session-participant/invite-notification.js'
|
|
55
|
+
import { resolveModeSettings } from './assistant-mode/resolve.js'
|
|
56
|
+
import { requestTranslator } from './request-translator.js'
|
|
41
57
|
|
|
42
58
|
/**
|
|
43
59
|
* How many stored messages reach the model.
|
|
@@ -57,6 +73,19 @@ class AssistantMessageInput {
|
|
|
57
73
|
content: string
|
|
58
74
|
}
|
|
59
75
|
|
|
76
|
+
@InputType({ description: 'Which conversation this is, and what it is about.' })
|
|
77
|
+
class AssistantModeInput {
|
|
78
|
+
@Field({ description: 'A mode kind the app server registered — `<app>.<surface>`, e.g. "twin.space".' })
|
|
79
|
+
kind: string
|
|
80
|
+
|
|
81
|
+
@Field({
|
|
82
|
+
nullable: true,
|
|
83
|
+
description:
|
|
84
|
+
'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.'
|
|
85
|
+
})
|
|
86
|
+
target?: string
|
|
87
|
+
}
|
|
88
|
+
|
|
60
89
|
@InputType({ description: 'One turn of a conversation that has no document of its own.' })
|
|
61
90
|
class AssistantChatInput {
|
|
62
91
|
@Field({
|
|
@@ -71,18 +100,11 @@ class AssistantChatInput {
|
|
|
71
100
|
})
|
|
72
101
|
messages: AssistantMessageInput[]
|
|
73
102
|
|
|
74
|
-
@Field({
|
|
103
|
+
@Field(() => AssistantModeInput, {
|
|
75
104
|
description:
|
|
76
|
-
'
|
|
105
|
+
'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.'
|
|
77
106
|
})
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
@Field(() => [String], {
|
|
81
|
-
nullable: true,
|
|
82
|
-
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.'
|
|
84
|
-
})
|
|
85
|
-
toolCategories?: string[]
|
|
107
|
+
mode: AssistantModeInput
|
|
86
108
|
|
|
87
109
|
@Field(() => GraphQLJSON, {
|
|
88
110
|
nullable: true,
|
|
@@ -98,21 +120,6 @@ class AssistantChatInput {
|
|
|
98
120
|
})
|
|
99
121
|
truncateAfterMessageId?: string
|
|
100
122
|
|
|
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
123
|
}
|
|
117
124
|
|
|
118
125
|
@ObjectType()
|
|
@@ -132,6 +139,13 @@ class AssistantChatOutput {
|
|
|
132
139
|
@Field({ nullable: true, description: 'Stored ChatMessage id of the reply.' })
|
|
133
140
|
assistantMessageId?: string
|
|
134
141
|
|
|
142
|
+
@Field(() => GraphQLJSON, {
|
|
143
|
+
nullable: true,
|
|
144
|
+
description:
|
|
145
|
+
'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.'
|
|
146
|
+
})
|
|
147
|
+
historyFolded?: any
|
|
148
|
+
|
|
135
149
|
@Field(() => GraphQLJSON, {
|
|
136
150
|
nullable: true,
|
|
137
151
|
description: 'Tool usages during the turn — {name, arguments, result, kind}. The surface shows them foldably.'
|
|
@@ -194,6 +208,14 @@ export class AssistantChatResolver {
|
|
|
194
208
|
)
|
|
195
209
|
}
|
|
196
210
|
|
|
211
|
+
/*
|
|
212
|
+
* Read the declaration before anything is stored. A kind this server does not have is a
|
|
213
|
+
* mismatched deployment, and the person should see that instead of a conversation with no
|
|
214
|
+
* persona and no tools.
|
|
215
|
+
*/
|
|
216
|
+
const t = requestTranslator(context)
|
|
217
|
+
const settings = resolveModeSettings(input.mode, t)
|
|
218
|
+
|
|
197
219
|
const { domain, user } = context.state
|
|
198
220
|
const sessionRepo = getRepository(ChatSession)
|
|
199
221
|
const messageRepo = getRepository(ChatMessage)
|
|
@@ -203,15 +225,23 @@ export class AssistantChatResolver {
|
|
|
203
225
|
const found = await sessionRepo.findOneBy({ id: input.sessionId, domain: { id: domain.id } as any })
|
|
204
226
|
if (!found) throw new Error(`ChatSession ${input.sessionId} not found`)
|
|
205
227
|
if (found.anchorType === 'figure' && input.hostContext?.figureId !== found.anchorId) {
|
|
206
|
-
throw new Error(
|
|
228
|
+
throw new Error(
|
|
229
|
+
context.t('ai-assistant.text.other-figure-history', {
|
|
230
|
+
defaultValue: 'this conversation belongs to another figure. open the conversation of the model you have open.'
|
|
231
|
+
})
|
|
232
|
+
)
|
|
207
233
|
}
|
|
208
234
|
if (found.anchorType === 'application' && (input.hostContext?.app !== found.anchorId || input.hostContext?.figureId)) {
|
|
209
|
-
throw new Error(
|
|
235
|
+
throw new Error(
|
|
236
|
+
context.t('ai-assistant.text.application-vs-model-history', {
|
|
237
|
+
defaultValue: 'this conversation is about the application, not about a model. open the one you want.'
|
|
238
|
+
})
|
|
239
|
+
)
|
|
210
240
|
}
|
|
211
241
|
session = found
|
|
212
242
|
}
|
|
213
243
|
|
|
214
|
-
assertStationSession(session, input.hostContext)
|
|
244
|
+
assertStationSession(session, input.hostContext, t)
|
|
215
245
|
|
|
216
246
|
const lastUserMessage = input.messages[input.messages.length - 1]
|
|
217
247
|
let userMessageId: string | undefined
|
|
@@ -225,6 +255,12 @@ export class AssistantChatResolver {
|
|
|
225
255
|
session: { id: session.id } as any,
|
|
226
256
|
role: 'user',
|
|
227
257
|
content: lastUserMessage.content,
|
|
258
|
+
/*
|
|
259
|
+
* This door is the one people call to ask the assistant, so a turn stored here was for the
|
|
260
|
+
* assistant (ADR-0070 A15 decision ①). The value comes from which door was called, never
|
|
261
|
+
* from the request — two sources for one fact can disagree.
|
|
262
|
+
*/
|
|
263
|
+
addressee: 'assistant',
|
|
228
264
|
creator: user,
|
|
229
265
|
updater: user,
|
|
230
266
|
parentMessage: previousLast ? ({ id: previousLast.id } as any) : undefined
|
|
@@ -244,6 +280,27 @@ export class AssistantChatResolver {
|
|
|
244
280
|
/* noop */
|
|
245
281
|
}
|
|
246
282
|
|
|
283
|
+
/*
|
|
284
|
+
* 사람이 `@이름` 으로 부른 사람은 이 대화에 들어온다(ADR-0071 결정 1). 이 문은 **사람의 턴**을
|
|
285
|
+
* 저장하는 문이라 그 일을 한다 — 모델의 답이나 도구 결과는 아무도 초대하지 않는다.
|
|
286
|
+
*/
|
|
287
|
+
const invite = await inviteMentionedUsers({
|
|
288
|
+
content: lastUserMessage.content,
|
|
289
|
+
sessionId: session.id!,
|
|
290
|
+
domainId: domain.id,
|
|
291
|
+
inviter: user,
|
|
292
|
+
participants: getRepository(ChatSessionParticipant),
|
|
293
|
+
visibility: (session as any).visibility
|
|
294
|
+
})
|
|
295
|
+
if (invite.invited.length > 0) {
|
|
296
|
+
const payload = buildInviteNotification(invite.invited, undefined, user)
|
|
297
|
+
try {
|
|
298
|
+
if (payload) await (context.state as any)?.notify?.(payload)
|
|
299
|
+
} catch {
|
|
300
|
+
/* 편지가 못 갔다고 방에 들어온 사실이 취소되지는 않는다. */
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
247
304
|
publishChatMessage({ domainId: domain.id, sessionId: session.id, message: saved } as any)
|
|
248
305
|
publishSessionActivity({
|
|
249
306
|
domainId: domain.id,
|
|
@@ -256,13 +313,52 @@ export class AssistantChatResolver {
|
|
|
256
313
|
})
|
|
257
314
|
}
|
|
258
315
|
|
|
316
|
+
/*
|
|
317
|
+
* 이번 턴에 상한 밖으로 밀려난 것이 있는가 — 화면이 「대화가 길어졌다」를 알릴 근거다.
|
|
318
|
+
*
|
|
319
|
+
* 이 문은 그 사실을 내지 않았다. 대화창에는 그 안내를 그리는 자리가 있고(`historyFolded`), 보드
|
|
320
|
+
* 문만 그 값을 채웠다. 그래서 앱의 대화는 앞부분이 잘려 나가도 **사람에게는 아무 말도 하지 않았다** —
|
|
321
|
+
* 모델에게는 「N 개 생략」을 적어 보내면서.
|
|
322
|
+
*/
|
|
323
|
+
const readRows = session
|
|
324
|
+
? await messageRepo.find({
|
|
325
|
+
where: { session: { id: session.id } as any },
|
|
326
|
+
order: { createdAt: 'ASC' },
|
|
327
|
+
relations: ['creator']
|
|
328
|
+
})
|
|
329
|
+
: []
|
|
330
|
+
/*
|
|
331
|
+
* 모델이 읽는 이력도 **부른 사람** 기준으로 가린다 — ADR-0073 판정 1. 가리지 않으면, 문을 한 번도
|
|
332
|
+
* 안 지난 사람이 "아까 그 수 다시 말해 줘" 한 마디로 값을 가져간다. 모델이 옮겨 적는 순간 그 값은
|
|
333
|
+
* 문 없는 새 답이 된다. 가린 자리를 지우지 않고 한 줄로 남기는 이유도 같다 — 지우면 모델이 그런
|
|
334
|
+
* 말이 없었다고 답하고, 그것은 거짓말이다.
|
|
335
|
+
*/
|
|
336
|
+
const storedRows = await readerGate(context)(readRows)
|
|
337
|
+
const capped = splitByCap(
|
|
338
|
+
storedRows.map(row => ({ id: row.id, role: row.role, content: row.content ?? '' })),
|
|
339
|
+
LLM_HISTORY_MAX_TURNS
|
|
340
|
+
)
|
|
341
|
+
const historyFolded =
|
|
342
|
+
capped.dropped.length > 0
|
|
343
|
+
? { omitted: capped.dropped.length, summarized: Boolean((session as any)?.lastSummary) }
|
|
344
|
+
: undefined
|
|
345
|
+
|
|
259
346
|
const conversation = session
|
|
260
347
|
? buildLlmHistory(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
348
|
+
/*
|
|
349
|
+
* The model reads who spoke and who they spoke to. The entity carries the speaker as a
|
|
350
|
+
* relation and the addressee as a column; the history wants them as plain fields, and
|
|
351
|
+
* without this mapping every line arrived unattributed — a room of people read as one
|
|
352
|
+
* voice (ADR-0070 A15 decision ②).
|
|
353
|
+
*/
|
|
354
|
+
storedRows.map(message => ({
|
|
355
|
+
id: message.id,
|
|
356
|
+
role: message.role,
|
|
357
|
+
content: message.content,
|
|
358
|
+
senderId: (message as any).creator?.id,
|
|
359
|
+
senderName: (message as any).creator?.name,
|
|
360
|
+
addressee: (message as any).addressee
|
|
361
|
+
})),
|
|
266
362
|
{
|
|
267
363
|
maxTurns: LLM_HISTORY_MAX_TURNS,
|
|
268
364
|
truncateAfterMessageId: input.truncateAfterMessageId,
|
|
@@ -291,23 +387,51 @@ export class AssistantChatResolver {
|
|
|
291
387
|
const result = await registryAssistantChat({
|
|
292
388
|
client,
|
|
293
389
|
messages: conversation as any,
|
|
294
|
-
systemPrompt:
|
|
295
|
-
toolCategories:
|
|
390
|
+
systemPrompt: settings.systemPrompt,
|
|
391
|
+
toolCategories: settings.toolCategories,
|
|
296
392
|
toolAccess: toolAccessFor(context),
|
|
393
|
+
/* The stop notice is a sentence a person reads — the server writes it in their language (ADR-0054). */
|
|
394
|
+
t,
|
|
297
395
|
hostContext: input.hostContext,
|
|
396
|
+
/*
|
|
397
|
+
* 근거는 도구 결과만이 아니다 — 시스템 프롬프트 · 이 대화의 말 · 화면이 보낸 스냅숏도 근거다
|
|
398
|
+
* (`ai-client-base/utils/grounding`). 이 문은 그 셋을 넘기지 않아, **사람이 방금 타이핑한
|
|
399
|
+
* 식별자**를 되받아 적은 답이 「근거 없음」으로 표시됐다. 그런 경고가 몇 번 나오면 사람이 경고
|
|
400
|
+
* 자체를 안 보게 된다.
|
|
401
|
+
*/
|
|
402
|
+
groundingSources: groundingSourcesFor({
|
|
403
|
+
systemPrompt: settings.systemPrompt,
|
|
404
|
+
messages: conversation as any,
|
|
405
|
+
hostContext: input.hostContext
|
|
406
|
+
}),
|
|
298
407
|
state: context.state,
|
|
299
|
-
model:
|
|
300
|
-
temperature:
|
|
301
|
-
maxTokens:
|
|
302
|
-
requireToolOnFirstTurn:
|
|
408
|
+
model: settings.model,
|
|
409
|
+
temperature: settings.temperature,
|
|
410
|
+
maxTokens: settings.maxTokens,
|
|
411
|
+
requireToolOnFirstTurn: settings.requireToolOnFirstTurn
|
|
303
412
|
})
|
|
304
413
|
|
|
414
|
+
/*
|
|
415
|
+
* 이 답이 지나온 문 — ADR-0073 판정 1. 행에도, 목록 미리보기에도 같은 값이 간다. 한쪽만 적으면
|
|
416
|
+
* 가려진 답이 목록에는 그대로 뜬다.
|
|
417
|
+
*/
|
|
418
|
+
const answerDoors = doorsForAnswer(
|
|
419
|
+
(result.loop.toolUsages ?? []).map((usage: any) => usage?.name).filter(Boolean),
|
|
420
|
+
name => (findToolSpec(name, settings.toolCategories) as any)?.doors
|
|
421
|
+
)
|
|
422
|
+
|
|
305
423
|
let assistantMessageId: string | undefined
|
|
306
424
|
if (session) {
|
|
307
425
|
const stored = await messageRepo.save({
|
|
308
426
|
session: { id: session.id } as any,
|
|
309
427
|
role: 'assistant',
|
|
310
428
|
content: result.reply,
|
|
429
|
+
/*
|
|
430
|
+
* 이 답이 지나온 문을 함께 적는다 — ADR-0073 판정 1. 방은 도메인 안에서 공개이므로, 이 문장이
|
|
431
|
+
* 실어 나르는 값을 나중에 읽는 사람이 열 수 있는지 판정할 근거가 행에 있어야 한다. 적지 않으면
|
|
432
|
+
* 빈 목록이 되고, 빈 목록은 누구나 읽는다는 뜻이다.
|
|
433
|
+
*/
|
|
434
|
+
doors: answerDoors,
|
|
311
435
|
toolUsages: result.loop.toolUsages?.length ? JSON.stringify(result.loop.toolUsages) : undefined,
|
|
312
436
|
groundingWarnings: result.groundingWarnings?.length ? JSON.stringify(result.groundingWarnings) : undefined,
|
|
313
437
|
creator: user,
|
|
@@ -331,8 +455,25 @@ export class AssistantChatResolver {
|
|
|
331
455
|
await sessionRepo.update(session.id!, {
|
|
332
456
|
aiClientId: client.id,
|
|
333
457
|
updater: user,
|
|
334
|
-
...activityPatch(result.reply)
|
|
458
|
+
...activityPatch(result.reply, new Date(), answerDoors)
|
|
335
459
|
} as any)
|
|
460
|
+
|
|
461
|
+
/*
|
|
462
|
+
* 상한 밖으로 밀려난 앞부분을 요약으로 접는다 — **답을 돌려준 뒤**에 한다(사용자 대기 시간에
|
|
463
|
+
* 요약 호출을 얹지 않는다). 기다리지 않으므로 실패해도 대화에는 영향이 없다: 요약이 없으면 다음
|
|
464
|
+
* 턴이 「N 개 생략」으로 정직하게 알린다.
|
|
465
|
+
*
|
|
466
|
+
* 이 문에는 이 호출이 없었다. 조각은 다 있었는데(상한 · 생략 알림 · `lastSummary` · 순수 규칙)
|
|
467
|
+
* 부르는 쪽이 보드 제품 하나뿐이어서, 앱의 대화는 며칠이 지나도 앞부분을 요약하지 않고 매 턴
|
|
468
|
+
* 「생략했다」고만 말했다 — 협의가 길어질수록 처음에 합의한 것이 먼저 사라진다.
|
|
469
|
+
*/
|
|
470
|
+
void foldSessionHistory({
|
|
471
|
+
sessionId: session.id!,
|
|
472
|
+
domainId: domain.id,
|
|
473
|
+
client,
|
|
474
|
+
maxTurns: LLM_HISTORY_MAX_TURNS,
|
|
475
|
+
locale: context.state?.locale
|
|
476
|
+
})
|
|
336
477
|
}
|
|
337
478
|
|
|
338
479
|
return {
|
|
@@ -344,7 +485,8 @@ export class AssistantChatResolver {
|
|
|
344
485
|
toolUsages: result.loop.toolUsages?.length ? result.loop.toolUsages : null,
|
|
345
486
|
offeredTools: result.offeredTools,
|
|
346
487
|
proposals: result.loop.proposals?.length ? result.loop.proposals : null,
|
|
347
|
-
groundingWarnings: result.groundingWarnings?.length ? result.groundingWarnings : null
|
|
488
|
+
groundingWarnings: result.groundingWarnings?.length ? result.groundingWarnings : null,
|
|
489
|
+
historyFolded: historyFolded ?? null
|
|
348
490
|
}
|
|
349
491
|
}
|
|
350
492
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The screen asks what its conversation offers.
|
|
3
|
+
*
|
|
4
|
+
* The `/` sentences, the `@` catalog and the two notices used to travel from the browser into the
|
|
5
|
+
* request, which meant the screen and the door could describe the same conversation differently.
|
|
6
|
+
* They are declared once, in the mode, as i18n keys — and this query hands them over already
|
|
7
|
+
* written in the request's locale (ADR-0054, and the same rule A11 applied to the stop notice).
|
|
8
|
+
* The persona, the tool categories and the limits stay on the server: nothing here returns them.
|
|
9
|
+
*/
|
|
10
|
+
import { Arg, Ctx, Directive, Field, ObjectType, Query, Resolver } from 'type-graphql'
|
|
11
|
+
import { GraphQLJSON } from 'graphql-scalars'
|
|
12
|
+
import '@things-factory/auth-base'
|
|
13
|
+
|
|
14
|
+
import { assistantModeView } from './resolve.js'
|
|
15
|
+
import { assistantModesForSubject } from './registry.js'
|
|
16
|
+
import { requestTranslator } from '../request-translator.js'
|
|
17
|
+
|
|
18
|
+
@ObjectType({ description: 'One `/` sentence, in the words of this request.' })
|
|
19
|
+
class AssistantModeSlashView {
|
|
20
|
+
@Field({ description: 'Short name shown in the list.' })
|
|
21
|
+
name: string
|
|
22
|
+
|
|
23
|
+
@Field({ description: 'One line about what it does.' })
|
|
24
|
+
description: string
|
|
25
|
+
|
|
26
|
+
@Field({ description: 'The sentence that lands in the input.' })
|
|
27
|
+
template: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@ObjectType({ description: 'What a conversation offers on screen.' })
|
|
31
|
+
class AssistantModeView {
|
|
32
|
+
@Field({ description: 'The mode kind this describes.' })
|
|
33
|
+
kind: string
|
|
34
|
+
|
|
35
|
+
@Field(() => [AssistantModeSlashView], { description: 'What `/` offers.' })
|
|
36
|
+
slashTemplates: AssistantModeSlashView[]
|
|
37
|
+
|
|
38
|
+
@Field(() => GraphQLJSON, { description: 'What `@` offers.' })
|
|
39
|
+
catalogEntries: any[]
|
|
40
|
+
|
|
41
|
+
@Field(() => GraphQLJSON, { description: 'Follow-up chips for a proposal that arrived without its own.' })
|
|
42
|
+
proposalChoices: any[]
|
|
43
|
+
|
|
44
|
+
@Field({ nullable: true, description: 'What the empty input says — what this conversation is for.' })
|
|
45
|
+
placeholder?: string
|
|
46
|
+
|
|
47
|
+
@Field({ nullable: true, description: 'The line under an empty conversation.' })
|
|
48
|
+
footerNotice?: string
|
|
49
|
+
|
|
50
|
+
@Field({ nullable: true, description: 'The line shown once the host reports a proposal staged.' })
|
|
51
|
+
stagedNotice?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@ObjectType({ description: 'A conversation a screen may open for what it is showing.' })
|
|
55
|
+
class AssistantModeChoice {
|
|
56
|
+
@Field({ description: 'The mode kind to open.' })
|
|
57
|
+
kind: string
|
|
58
|
+
|
|
59
|
+
@Field({ description: 'What to call it where a person chooses — already in the words of this request.' })
|
|
60
|
+
label: string
|
|
61
|
+
|
|
62
|
+
@Field({ description: 'Whether this is the one to open when the person has not chosen.' })
|
|
63
|
+
isDefault: boolean
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Resolver()
|
|
67
|
+
export class AssistantModeResolver {
|
|
68
|
+
/*
|
|
69
|
+
* 이 화면이 보여 주는 것으로 열 수 있는 대화들 — ADR-0072 A17 ①.
|
|
70
|
+
*
|
|
71
|
+
* 화면은 자기가 무엇을 보여 주는지(`subject`)만 안다. 그 위에 어느 대화가 서는지는 앱 서버가
|
|
72
|
+
* 선언한다. 화면이 종류 목록을 손으로 들고 있으면, 대화를 하나 더 세운 날 그 화면만 모른 채로 남는다.
|
|
73
|
+
*
|
|
74
|
+
* 하나만 오면 고르개를 내지 않는다 — 고를 것이 없는 고르개는 자리만 차지한다.
|
|
75
|
+
*/
|
|
76
|
+
@Query(() => [AssistantModeChoice], {
|
|
77
|
+
description:
|
|
78
|
+
'Conversations that stand for this kind of subject, the default first. A screen showing one subject opens the head of this list and offers the rest; a single entry means there is nothing to choose.'
|
|
79
|
+
})
|
|
80
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "query")')
|
|
81
|
+
async assistantModes(@Arg('subject') subject: string, @Ctx() context: any): Promise<AssistantModeChoice[]> {
|
|
82
|
+
const t = requestTranslator(context)
|
|
83
|
+
|
|
84
|
+
return assistantModesForSubject(subject).map(mode => ({
|
|
85
|
+
kind: mode.kind,
|
|
86
|
+
/*
|
|
87
|
+
* 이름이 없는 대화는 종류를 그대로 쓴다. 부팅 보고가 이미 그것을 경고로 말하므로 여기서 숨기지
|
|
88
|
+
* 않는다 — 화면에 `twin.space` 가 뜨면 그것이 빠진 선언을 가리킨다.
|
|
89
|
+
*/
|
|
90
|
+
label: mode.labelKey ? t(mode.labelKey) : mode.kind,
|
|
91
|
+
isDefault: (mode.defaultFor ?? []).includes(subject)
|
|
92
|
+
}))
|
|
93
|
+
}
|
|
94
|
+
@Query(() => AssistantModeView, {
|
|
95
|
+
description: 'What this conversation offers on screen — `/` sentences, the `@` catalog and the notices, translated.'
|
|
96
|
+
})
|
|
97
|
+
/* Reading what a conversation offers is reading, and the same door the session list uses. */
|
|
98
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "query")')
|
|
99
|
+
async assistantMode(@Arg('kind') kind: string, @Ctx() context: any): Promise<AssistantModeView> {
|
|
100
|
+
return assistantModeView(kind, requestTranslator(context)) as AssistantModeView
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modes — what each conversation is, declared by the app server that owns the screen.
|
|
3
|
+
*
|
|
4
|
+
* Design: ADR-0061 decision 7 (`operato-twin/design/04-decisions.md`) and the AI standard's A2.
|
|
5
|
+
*/
|
|
6
|
+
export {
|
|
7
|
+
registerAssistantMode,
|
|
8
|
+
getAssistantMode,
|
|
9
|
+
listAssistantModes,
|
|
10
|
+
assistantModesForSubject,
|
|
11
|
+
getAssistantModeWarnings,
|
|
12
|
+
clearAssistantModes
|
|
13
|
+
} from './registry.js'
|
|
14
|
+
export { resolveModeSettings, assistantModeView } from './resolve.js'
|
|
15
|
+
export type { AssistantModeSpec, AssistantModeSlash, AssistantModeWarning } from './types.js'
|
|
16
|
+
export type { AssistantModeRef, ModeSettings, AssistantModeView, AssistantModeSlashView, Translate } from './resolve.js'
|