@things-factory/board-ai 10.1.159 → 10.1.161
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/dist-client/server/service/assistant.js +32 -29
- package/dist-client/server/service/assistant.js.map +1 -1
- package/dist-client/server/service/types.d.ts +6 -0
- package/dist-client/server/service/types.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/assistant.js +32 -29
- package/dist-server/service/assistant.js.map +1 -1
- package/dist-server/service/board-ai-resolver.d.ts +1 -0
- package/dist-server/service/board-ai-resolver.js +25 -2
- package/dist-server/service/board-ai-resolver.js.map +1 -1
- package/dist-server/service/types.d.ts +6 -0
- package/dist-server/service/types.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/server/service/assistant.test.ts +28 -14
- package/server/service/assistant.ts +35 -31
- package/server/service/board-ai-resolver.ts +28 -4
- package/server/service/board-context-path.test.ts +32 -0
- package/server/service/types.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/board-ai",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.161",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"browser": "dist-client/client/index.js",
|
|
6
6
|
"things-factory": true,
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@operato/i18n": "^10.0.0",
|
|
33
33
|
"@operato/scene-ops": "^10.14.1",
|
|
34
34
|
"@operato/styles": "^10.0.0",
|
|
35
|
-
"@things-factory/ai-assistant": "^10.1.
|
|
35
|
+
"@things-factory/ai-assistant": "^10.1.161",
|
|
36
36
|
"@things-factory/ai-client-base": "^10.1.159",
|
|
37
37
|
"@things-factory/auth-base": "^10.1.159",
|
|
38
38
|
"@things-factory/board-import": "^10.1.159",
|
|
@@ -47,5 +47,5 @@
|
|
|
47
47
|
"copyfiles": "^2.4.1",
|
|
48
48
|
"rimraf": "^5.0.0"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "d32fe0614ea40572787acd94e307cc18a681ab38"
|
|
51
51
|
}
|
|
@@ -834,7 +834,7 @@ describe('Tool 분류 (read/write)', () => {
|
|
|
834
834
|
})
|
|
835
835
|
|
|
836
836
|
describe('buildBoardEditTools — refid 기반 tool 이름 (식별자 정책 회귀 방지)', () => {
|
|
837
|
-
test('
|
|
837
|
+
test('42개 tool 모두 정의 (25 write + 9 read + 4 ephemeral action + 4 import)', () => {
|
|
838
838
|
const tools = buildBoardEditTools(['rect', 'circle'])
|
|
839
839
|
const names = tools.map(t => t.name).sort()
|
|
840
840
|
expect(names).toEqual([
|
|
@@ -853,6 +853,7 @@ describe('buildBoardEditTools — refid 기반 tool 이름 (식별자 정책 회
|
|
|
853
853
|
'fitToView',
|
|
854
854
|
'getAtmosphereGuide',
|
|
855
855
|
'getAvailablePrefabs',
|
|
856
|
+
'getBoardOutline',
|
|
856
857
|
'getColorPalettes',
|
|
857
858
|
'getComponentByRefid',
|
|
858
859
|
'getComponentStyle',
|
|
@@ -1617,8 +1618,8 @@ describe('agentic loop (chatViaTools) — mock provider', () => {
|
|
|
1617
1618
|
id: 'mock:test',
|
|
1618
1619
|
chat: async () => '',
|
|
1619
1620
|
generateJSON: async () => ({}),
|
|
1620
|
-
chatWithTools: async (messages: any[], _tools: any[],
|
|
1621
|
-
callLog.push({ messages: JSON.parse(JSON.stringify(messages)) })
|
|
1621
|
+
chatWithTools: async (messages: any[], _tools: any[], options: any) => {
|
|
1622
|
+
callLog.push({ messages: JSON.parse(JSON.stringify(messages)), systemPrompt: options?.systemPrompt ?? '' })
|
|
1622
1623
|
const turn = turns[turnIdx++] ?? { toolCalls: [], stopReason: 'end_turn' }
|
|
1623
1624
|
return {
|
|
1624
1625
|
text: turn.text,
|
|
@@ -1736,23 +1737,36 @@ describe('agentic loop (chatViaTools) — mock provider', () => {
|
|
|
1736
1737
|
expect(r).toBeDefined()
|
|
1737
1738
|
})
|
|
1738
1739
|
|
|
1739
|
-
|
|
1740
|
+
/*
|
|
1741
|
+
* The board and the selection are not pasted into the request (operato-application design/ai/screen-pattern.md
|
|
1742
|
+
* §2-2, ruled 2026-10-13): the open board is named by the server's line, its contents are read with tools.
|
|
1743
|
+
*/
|
|
1744
|
+
test('★ the request carries the person\'s words only - no board JSON, no selection list', async () => {
|
|
1740
1745
|
const { client, callLog } = makeMockClient([{ text: 'ok', toolCalls: [] }])
|
|
1741
1746
|
const a = new DefaultBoardAIAssistant(client)
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
expect(
|
|
1745
|
-
expect(
|
|
1747
|
+
const board: any = { width: 800, components: [{ type: 'rect', refid: 7, left: 10 }] }
|
|
1748
|
+
await a.chat([{ role: 'user', content: 'q' }], board, { selectedRefids: [7] })
|
|
1749
|
+
expect(callLog[0].messages).toEqual([{ role: 'user', content: 'q' }])
|
|
1750
|
+
expect(JSON.stringify(callLog[0])).not.toMatch(/Current board state|User-selected component refids/)
|
|
1746
1751
|
})
|
|
1747
1752
|
|
|
1748
|
-
test('
|
|
1753
|
+
test('★ the system prompt names the open board through the server\'s line, and says to read the board and the selection', async () => {
|
|
1749
1754
|
const { client, callLog } = makeMockClient([{ text: 'ok', toolCalls: [] }])
|
|
1750
1755
|
const a = new DefaultBoardAIAssistant(client)
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
expect(
|
|
1754
|
-
expect(
|
|
1755
|
-
expect(
|
|
1756
|
+
const line = 'On this screen the person has the board named "Line 1" open (id b1).'
|
|
1757
|
+
await a.chat([{ role: 'user', content: 'q' }], undefined, { openSubject: line })
|
|
1758
|
+
expect(callLog[0].systemPrompt).toContain(`==== OPEN SUBJECT ====\n${line}`)
|
|
1759
|
+
expect(callLog[0].systemPrompt).toMatch(/Read the board with `getBoardOutline`/)
|
|
1760
|
+
expect(callLog[0].systemPrompt).toMatch(/the selection with `getSelection`/)
|
|
1761
|
+
})
|
|
1762
|
+
|
|
1763
|
+
test('★ getBoardOutline is the summary the request used to carry', () => {
|
|
1764
|
+
const board: any = { width: 800, components: [{ type: 'rect', refid: 7, left: 10 }] }
|
|
1765
|
+
expect(executeReadTool({ id: '1', name: 'getBoardOutline', arguments: {} } as any, board, [])).toEqual(summarizeBoard(board))
|
|
1766
|
+
expect(executeReadTool({ id: '1', name: 'getBoardOutline', arguments: {} } as any, undefined, [])).toEqual({
|
|
1767
|
+
empty: true,
|
|
1768
|
+
message: 'No board is open in this conversation.'
|
|
1769
|
+
})
|
|
1756
1770
|
})
|
|
1757
1771
|
|
|
1758
1772
|
test('system prompt 가 id vs refid 정책을 AI 한테 명시', async () => {
|
|
@@ -192,38 +192,28 @@ export class DefaultBoardAIAssistant implements BoardAIAssistant {
|
|
|
192
192
|
* 모아 붙이기만 한다(도메인 지식이 board-ai 프롬프트로 새지 않는다). */
|
|
193
193
|
const guidance = getCategoryGuidance(options.toolCategories)
|
|
194
194
|
const surface = (options.surfacePrompt ?? '').trim()
|
|
195
|
+
const openSubject = (options.openSubject ?? '').trim()
|
|
195
196
|
const systemPrompt =
|
|
196
197
|
this.buildSystemPromptForTools(knownTypes, categories, schemas) +
|
|
197
198
|
(surface ? `\n\n==== THIS SURFACE (declared by the app) ====\n${surface}` : '') +
|
|
199
|
+
/* Which board is open - the server's line from the stored anchor, the one way the model learns it (screen-pattern §2-1). */
|
|
200
|
+
(openSubject ? `\n\n==== OPEN SUBJECT ====\n${openSubject}` : '') +
|
|
201
|
+
(gate.coreOn ? `\n\n${READ_THE_BOARD}` : '') +
|
|
198
202
|
(guidance.length ? `\n\n==== TOOL USE RULES (from the tools exposed to this conversation) ====\n${guidance.join('\n')}` : '') +
|
|
199
203
|
(!gate.coreOn && options.boardToolsWithheld ? `\n\n${BOARD_EDIT_WITHHELD}` : '')
|
|
200
204
|
|
|
201
205
|
const selectedRefids = (options.selectedRefids ?? []).filter(
|
|
202
206
|
(n): n is number => typeof n === 'number' && Number.isFinite(n)
|
|
203
207
|
)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
`clarify or call \`getSelection\` to confirm.`
|
|
214
|
-
|
|
215
|
-
const boardContext = currentBoard
|
|
216
|
-
? `Current board state (JSON, summarized; full details via \`getComponent\` / \`findComponents\`):\n` +
|
|
217
|
-
`${JSON.stringify(summarizeBoard(currentBoard), null, 2)}` + selectionContext
|
|
218
|
-
: 'No current board (empty start).' + selectionContext
|
|
219
|
-
|
|
220
|
-
let conversation: AIMessage[] = [
|
|
221
|
-
{ role: 'user', content: boardContext },
|
|
222
|
-
...messages.map<AIMessage>(m => ({
|
|
223
|
-
role: m.role === 'assistant' ? 'assistant' : 'user',
|
|
224
|
-
content: m.content
|
|
225
|
-
}))
|
|
226
|
-
]
|
|
208
|
+
/*
|
|
209
|
+
* The board and the selection are not pasted into the request any more (screen-pattern §2-2, 2026-10-13): the
|
|
210
|
+
* whole board went into every turn as a first user message, a second way of knowing the open board beside the
|
|
211
|
+
* server's line. The model reads what it needs with getBoardOutline · getSelection · findComponents.
|
|
212
|
+
*/
|
|
213
|
+
let conversation: AIMessage[] = messages.map<AIMessage>(m => ({
|
|
214
|
+
role: m.role === 'assistant' ? 'assistant' : 'user',
|
|
215
|
+
content: m.content
|
|
216
|
+
}))
|
|
227
217
|
|
|
228
218
|
// multi-turn agentic loop — LLM 이 read 호출 → 서버 실행 → 결과 회신 → LLM 추론 반복.
|
|
229
219
|
// write tool 은 누적해 client 에 patch 로 전달. 본체는 ./agentic-loop 에 분리.
|
|
@@ -317,8 +307,7 @@ export class DefaultBoardAIAssistant implements BoardAIAssistant {
|
|
|
317
307
|
* 근거집합의 정의와 사정거리는 ./grounding 머리말 참조(식별자 없는 창작 서술은 잡지 못한다). */
|
|
318
308
|
const warnings = checkReplyGrounding(baseReply, {
|
|
319
309
|
systemPrompt,
|
|
320
|
-
/*
|
|
321
|
-
hostContext: boardContext,
|
|
310
|
+
/* 보드는 이제 도구로만 모델에게 닿는다 — 읽은 보드는 toolResults 안에 있다(screen-pattern §2-2). */
|
|
322
311
|
messages: messages.map(m => (typeof m.content === 'string' ? m.content : '')),
|
|
323
312
|
toolResults: loopResult.groundedTexts
|
|
324
313
|
})
|
|
@@ -331,7 +320,7 @@ export class DefaultBoardAIAssistant implements BoardAIAssistant {
|
|
|
331
320
|
confidence: { level: warnings.length ? 'low' as const : 'medium' as const, reasons: warnings.length ? ['grounding-warning'] : ['tool-or-board-context'] },
|
|
332
321
|
limitations: warnings.map(code => ({ code })),
|
|
333
322
|
claims: [{ code: patch ? 'board.patch-proposed' : 'board.context-reviewed', severity: 'info' as const, evidence: { toolCalls: toolUsages.length, componentCount: components.length }, method: 'board-agentic-loop', assumptions: ['Board context is current for this turn.'] }],
|
|
334
|
-
facts: { board:
|
|
323
|
+
facts: { board: summarizeBoard(currentBoard), toolUsages },
|
|
335
324
|
...(patch ? { proposal: patch } : accumulatedActions.length ? { proposal: accumulatedActions } : {}),
|
|
336
325
|
approvalRequired: true as const,
|
|
337
326
|
approval: { required: true as const, mode: 'confirm' as const, reason: patch || accumulatedActions.length ? 'Board or scene change requires user confirmation.' : 'Analysis is advisory.' },
|
|
@@ -449,8 +438,8 @@ Rules:
|
|
|
449
438
|
배열에 자식을 가질 수 있다. 자식이 또 group 일 수 있어 임의 깊이로 중첩 가능.
|
|
450
439
|
|
|
451
440
|
가시성:
|
|
452
|
-
- \`
|
|
453
|
-
|
|
441
|
+
- \`getBoardOutline\` 은 *평면 list* 로 모든 컴포넌트를 돌려준다 — top-level 뿐 아니라
|
|
442
|
+
group/container 안 자식도 모두 포함.
|
|
454
443
|
- 자식은 \`_parentRefid\` (부모 group refid) 와 \`_depth\` (중첩 깊이, 0=root) 메타
|
|
455
444
|
가 첨부되어 어느 group 안에 있는지 식별 가능.
|
|
456
445
|
- \`findComponents\` 는 전체 트리를 search — group 안 자식까지 매칭. 특정 group
|
|
@@ -640,7 +629,7 @@ Behavioral rules:
|
|
|
640
629
|
- If the user is asking a question rather than commanding, REPLY in TEXT WITHOUT calling any tool.
|
|
641
630
|
- If the user's request is ambiguous, REPLY in TEXT (asking for clarification) WITHOUT calling any tool.
|
|
642
631
|
- For multiple components, call addComponent multiple times in parallel.
|
|
643
|
-
- The
|
|
632
|
+
- The board is not in the conversation: read it with \`getBoardOutline\` (all components, flat) and the other read tools before you describe or change it.
|
|
644
633
|
|
|
645
634
|
Reply formatting rules (chat surface):
|
|
646
635
|
- Reply ONCE with ONE form only. NEVER restate the same answer in a second form (e.g. do NOT add a prose summary after a bullet list, or vice versa).
|
|
@@ -713,7 +702,7 @@ export function buildBoardEditTools(knownTypes: string[], exposure: ToolExposure
|
|
|
713
702
|
{
|
|
714
703
|
name: 'removeComponentByRefid',
|
|
715
704
|
description:
|
|
716
|
-
'Remove an existing component by its `refid` — the universal numeric handle that things-scene auto-assigns to every component (
|
|
705
|
+
'Remove an existing component by its `refid` — the universal numeric handle that things-scene auto-assigns to every component (each component in `getBoardOutline` carries it as `refid`).',
|
|
717
706
|
parameters: {
|
|
718
707
|
type: 'object',
|
|
719
708
|
properties: {
|
|
@@ -809,6 +798,12 @@ export function buildBoardEditTools(knownTypes: string[], exposure: ToolExposure
|
|
|
809
798
|
}
|
|
810
799
|
},
|
|
811
800
|
// ── Read tools (서버측 즉시 실행, 결과를 LLM 에 회신) ─────────
|
|
801
|
+
{
|
|
802
|
+
name: 'getBoardOutline',
|
|
803
|
+
description:
|
|
804
|
+
'The open board as it stands now, saved or not: its own attributes (size, background, ...) and every component in a flat list - group and container children included, each with `refid`, `type`, its key attributes and, for a child, `_parentRefid` / `_depth`. Call this before describing or changing the board. Full details of one component: getComponentByRefid.',
|
|
805
|
+
parameters: { type: 'object', properties: {} }
|
|
806
|
+
},
|
|
812
807
|
{
|
|
813
808
|
name: 'getSelection',
|
|
814
809
|
description:
|
|
@@ -818,7 +813,7 @@ export function buildBoardEditTools(knownTypes: string[], exposure: ToolExposure
|
|
|
818
813
|
{
|
|
819
814
|
name: 'getComponentByRefid',
|
|
820
815
|
description:
|
|
821
|
-
'Get full details of a single component by its `refid` (the things-scene-assigned universal numeric handle). Use this to inspect properties
|
|
816
|
+
'Get full details of a single component by its `refid` (the things-scene-assigned universal numeric handle). Use this to inspect properties `getBoardOutline` leaves out.',
|
|
822
817
|
parameters: {
|
|
823
818
|
type: 'object',
|
|
824
819
|
properties: {
|
|
@@ -1202,7 +1197,13 @@ export function buildBoardEditTools(knownTypes: string[], exposure: ToolExposure
|
|
|
1202
1197
|
|
|
1203
1198
|
// ── Read tool 분류 + 실행기 ────────────────────────────────────────
|
|
1204
1199
|
|
|
1200
|
+
/** How the model learns the board and the selection - by reading, every time it needs to (screen-pattern §2-2). */
|
|
1201
|
+
const READ_THE_BOARD =
|
|
1202
|
+
'The board and the selection are not in the conversation. Read the board with `getBoardOutline` before you describe or change it, ' +
|
|
1203
|
+
'and the selection with `getSelection` when the person says "selected", "this" or "these" without naming components; then work only on those.'
|
|
1204
|
+
|
|
1205
1205
|
const READ_TOOL_NAMES = new Set([
|
|
1206
|
+
'getBoardOutline',
|
|
1206
1207
|
'getSelection',
|
|
1207
1208
|
'getComponentByRefid',
|
|
1208
1209
|
'getComponentStyle',
|
|
@@ -1275,6 +1276,9 @@ export function executeReadTool(
|
|
|
1275
1276
|
const flatComponents = collectAllComponents(board).map(f => f.component)
|
|
1276
1277
|
|
|
1277
1278
|
switch (call.name) {
|
|
1279
|
+
case 'getBoardOutline':
|
|
1280
|
+
/* The summary that used to be pasted into every request - now read when the question needs it. */
|
|
1281
|
+
return board ? summarizeBoard(board) : { empty: true, message: 'No board is open in this conversation.' }
|
|
1278
1282
|
case 'getSelection': {
|
|
1279
1283
|
if (selectedRefids.length === 0) {
|
|
1280
1284
|
return { selected: [], message: 'No component is currently selected.' }
|
|
@@ -10,14 +10,14 @@ import { MoreThan } from 'typeorm'
|
|
|
10
10
|
import '@things-factory/auth-base'
|
|
11
11
|
|
|
12
12
|
import { getRepository, rethrowSpoken } from '@things-factory/shell'
|
|
13
|
-
import { aiClientFor, type AgentClient, type AIClient } from '@things-factory/ai-client-base'
|
|
13
|
+
import { aiClientFor, openSubjectLine, type AgentClient, type AIClient } from '@things-factory/ai-client-base'
|
|
14
14
|
import type { ComponentCategory } from '@things-factory/board-import'
|
|
15
15
|
|
|
16
16
|
import { DefaultBoardAIAssistant } from './assistant.js'
|
|
17
17
|
import { effectiveBoardTools } from './board-tool-rights.js'
|
|
18
18
|
import type { LLMMessage } from './types.js'
|
|
19
19
|
import { ChatSession } from '@things-factory/ai-assistant'
|
|
20
|
-
import { resolveModeSettings, requestTranslator, type ModeSettings } from '@things-factory/ai-assistant'
|
|
20
|
+
import { resolveModeSettings, requestTranslator, splitNextRequest, type ModeSettings } from '@things-factory/ai-assistant'
|
|
21
21
|
import { ChatMessage } from '@things-factory/ai-assistant'
|
|
22
22
|
import { buildLlmHistory } from '@things-factory/ai-assistant'
|
|
23
23
|
import { splitByCap, summaryInstruction } from '@things-factory/ai-assistant'
|
|
@@ -207,6 +207,12 @@ class BoardAIChatOutput {
|
|
|
207
207
|
@Field({ description: 'Conversational reply.' })
|
|
208
208
|
reply: string
|
|
209
209
|
|
|
210
|
+
@Field(() => String, {
|
|
211
|
+
nullable: true,
|
|
212
|
+
description: 'The request the person is likely to make next, as the reply named it. The input shows it and Tab writes it in. Not stored.'
|
|
213
|
+
})
|
|
214
|
+
suggestion?: string | null
|
|
215
|
+
|
|
210
216
|
@Field(() => GraphQLJSON, { nullable: true, description: 'SceneEditPatch.' })
|
|
211
217
|
patch?: any
|
|
212
218
|
|
|
@@ -452,7 +458,21 @@ export class BoardAIChatResolver {
|
|
|
452
458
|
const boardTools = await effectiveBoardTools(input.boardTools, context)
|
|
453
459
|
const boardToolsWithheld = input.boardTools !== false && boardTools === false
|
|
454
460
|
|
|
461
|
+
/*
|
|
462
|
+
* The open board - the server's line from the stored anchor, the same door every app's conversation has
|
|
463
|
+
* (operato-application design/ai/screen-pattern.md §2-1). The board's contents are read with tools.
|
|
464
|
+
*/
|
|
465
|
+
const openSubject = session
|
|
466
|
+
? await openSubjectLine({
|
|
467
|
+
anchorType: (session as any).anchorType,
|
|
468
|
+
anchorId: (session as any).anchorId,
|
|
469
|
+
domain,
|
|
470
|
+
state: context.state
|
|
471
|
+
})
|
|
472
|
+
: undefined
|
|
473
|
+
|
|
455
474
|
const r = await assistant.chat(llmMessages, input.currentBoard ?? undefined, {
|
|
475
|
+
openSubject,
|
|
456
476
|
scopes: input.scopes,
|
|
457
477
|
hostContext: input.hostContext ?? undefined,
|
|
458
478
|
surfacePrompt: modeSettings?.systemPrompt,
|
|
@@ -474,6 +494,9 @@ export class BoardAIChatResolver {
|
|
|
474
494
|
toolCallContext: context.state
|
|
475
495
|
})
|
|
476
496
|
|
|
497
|
+
/* A reply may end with the next request; the input offers it and the stored answer does not keep it (screen-pattern §4-1). */
|
|
498
|
+
const { reply, suggestion } = splitNextRequest(r.reply)
|
|
499
|
+
|
|
477
500
|
// ── Phase C: LLM 호출 후 짧은 DB 쓰기 ─────────────────────────
|
|
478
501
|
let assistantMessageId: string | undefined
|
|
479
502
|
let patchId: string | undefined
|
|
@@ -496,7 +519,7 @@ export class BoardAIChatResolver {
|
|
|
496
519
|
// AI 응답의 #token 들을 currentBoard 에 대해 resolve 한 뒤 inline marker 로 박음.
|
|
497
520
|
// patch 로 새로 만든 컴포넌트는 input.currentBoard 에 없으므로 marker 없이 저장 →
|
|
498
521
|
// 클릭 시 runtime fallback 으로 해석 (그때는 patch 적용 후라 scene 에 존재).
|
|
499
|
-
const enrichedReply = injectAiMentionRefids(
|
|
522
|
+
const enrichedReply = injectAiMentionRefids(reply, input.currentBoard ?? null)
|
|
500
523
|
|
|
501
524
|
const aiMsg = await messageRepo.save({
|
|
502
525
|
session: { id: session.id } as any,
|
|
@@ -549,7 +572,8 @@ export class BoardAIChatResolver {
|
|
|
549
572
|
}
|
|
550
573
|
|
|
551
574
|
return {
|
|
552
|
-
reply
|
|
575
|
+
reply,
|
|
576
|
+
suggestion: suggestion ?? null,
|
|
553
577
|
patch: r.patch ?? null,
|
|
554
578
|
followUp: r.followUp ?? null,
|
|
555
579
|
clientId: base.id,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The board conversation knows the open board the way every app's conversation does (operato-application
|
|
3
|
+
* design/ai/screen-pattern.md, ADR-0061 decision 7 exception, ruled 2026-10-13): one server line from the stored
|
|
4
|
+
* anchor, the contents read with tools, and no second way.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from 'fs'
|
|
7
|
+
import { join } from 'path'
|
|
8
|
+
|
|
9
|
+
const resolver = readFileSync(join(__dirname, 'board-ai-resolver.ts'), 'utf8')
|
|
10
|
+
const assistant = readFileSync(join(__dirname, 'assistant.ts'), 'utf8')
|
|
11
|
+
|
|
12
|
+
describe('the board conversation context path', () => {
|
|
13
|
+
it('★ the open board line is built from the stored session anchor, never from what the screen sent', () => {
|
|
14
|
+
const block = resolver.slice(resolver.indexOf('const openSubject = session'), resolver.indexOf('const r = await assistant.chat('))
|
|
15
|
+
expect(block).toMatch(/openSubjectLine\(\{\s+anchorType: \(session as any\)\.anchorType,\s+anchorId: \(session as any\)\.anchorId/)
|
|
16
|
+
expect(block).not.toMatch(/hostContext|currentBoard/)
|
|
17
|
+
expect(resolver).toMatch(/assistant\.chat\(llmMessages, input\.currentBoard \?\? undefined, \{\s+openSubject,/)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('★ no whole-board paste is left - the summary is a read tool', () => {
|
|
21
|
+
expect(assistant).not.toMatch(/Current board state/)
|
|
22
|
+
expect(assistant).not.toMatch(/role: 'user', content: boardContext/)
|
|
23
|
+
expect(assistant).toMatch(/case 'getBoardOutline':/)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('★ the stored answer, the list preview and the reply leave out the Next: line', () => {
|
|
27
|
+
expect(resolver).toMatch(/const \{ reply, suggestion \} = splitNextRequest\(r\.reply\)/)
|
|
28
|
+
expect(resolver).toMatch(/injectAiMentionRefids\(reply, /)
|
|
29
|
+
expect(resolver).toMatch(/suggestion: suggestion \?\? null/)
|
|
30
|
+
expect(resolver).not.toMatch(/injectAiMentionRefids\(r\.reply/)
|
|
31
|
+
})
|
|
32
|
+
})
|
package/server/service/types.ts
CHANGED
|
@@ -25,6 +25,12 @@ export interface ChatOptions {
|
|
|
25
25
|
* and its tools, and the mode describes the product and the person's work.
|
|
26
26
|
*/
|
|
27
27
|
surfacePrompt?: string
|
|
28
|
+
/**
|
|
29
|
+
* What the person has open, said by the server each turn from the conversation's stored anchor
|
|
30
|
+
* (operato-application design/ai/screen-pattern.md §2-1 · ADR-0061 decision 7 exception). The board's
|
|
31
|
+
* contents are read with the read tools (`getBoardOutline` and the rest), never pasted into the request.
|
|
32
|
+
*/
|
|
33
|
+
openSubject?: string
|
|
28
34
|
/**
|
|
29
35
|
* 이 대화에 노출할 **외부 도구 카테고리** 화이트리스트(registerToolCategory 이름).
|
|
30
36
|
* 미지정 = 등록된 전부(현행 동작). 성격이 다른 대화면(보드 저작 도크 · 운영 도크)이
|