@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.
@@ -104,32 +104,24 @@ export class DefaultBoardAIAssistant {
104
104
  * 모아 붙이기만 한다(도메인 지식이 board-ai 프롬프트로 새지 않는다). */
105
105
  const guidance = getCategoryGuidance(options.toolCategories);
106
106
  const surface = (options.surfacePrompt ?? '').trim();
107
+ const openSubject = (options.openSubject ?? '').trim();
107
108
  const systemPrompt = this.buildSystemPromptForTools(knownTypes, categories, schemas) +
108
109
  (surface ? `\n\n==== THIS SURFACE (declared by the app) ====\n${surface}` : '') +
110
+ /* Which board is open - the server's line from the stored anchor, the one way the model learns it (screen-pattern §2-1). */
111
+ (openSubject ? `\n\n==== OPEN SUBJECT ====\n${openSubject}` : '') +
112
+ (gate.coreOn ? `\n\n${READ_THE_BOARD}` : '') +
109
113
  (guidance.length ? `\n\n==== TOOL USE RULES (from the tools exposed to this conversation) ====\n${guidance.join('\n')}` : '') +
110
114
  (!gate.coreOn && options.boardToolsWithheld ? `\n\n${BOARD_EDIT_WITHHELD}` : '');
111
115
  const selectedRefids = (options.selectedRefids ?? []).filter((n) => typeof n === 'number' && Number.isFinite(n));
112
- // selection 정보는 항상 포함 (비어 있어도) — LLM 이 "선택된 게 뭐냐" 류 질문에
113
- // getSelection 을 호출할지 판단하는 데 필요.
114
- const selectionContext = selectedRefids.length > 0
115
- ? `\n\nUser-selected component refids: ${JSON.stringify(selectedRefids)}\n` +
116
- `When the user says "selected" / "선택한" / "this" / "그것" / "these" etc. ` +
117
- `WITHOUT specifying targets, operate ONLY on these refids. ` +
118
- `Do not modify other components unless explicitly asked. ` +
119
- `For inspecting selected components, prefer calling \`getSelection\` over guessing.`
120
- : `\n\nNo component is currently selected. If the user asks about "selected" / "this", ` +
121
- `clarify or call \`getSelection\` to confirm.`;
122
- const boardContext = currentBoard
123
- ? `Current board state (JSON, summarized; full details via \`getComponent\` / \`findComponents\`):\n` +
124
- `${JSON.stringify(summarizeBoard(currentBoard), null, 2)}` + selectionContext
125
- : 'No current board (empty start).' + selectionContext;
126
- let conversation = [
127
- { role: 'user', content: boardContext },
128
- ...messages.map(m => ({
129
- role: m.role === 'assistant' ? 'assistant' : 'user',
130
- content: m.content
131
- }))
132
- ];
116
+ /*
117
+ * The board and the selection are not pasted into the request any more (screen-pattern §2-2, 2026-10-13): the
118
+ * whole board went into every turn as a first user message, a second way of knowing the open board beside the
119
+ * server's line. The model reads what it needs with getBoardOutline · getSelection · findComponents.
120
+ */
121
+ let conversation = messages.map(m => ({
122
+ role: m.role === 'assistant' ? 'assistant' : 'user',
123
+ content: m.content
124
+ }));
133
125
  // multi-turn agentic loop — LLM 이 read 호출 → 서버 실행 → 결과 회신 → LLM 추론 반복.
134
126
  // write tool 은 누적해 client 에 patch 로 전달. 본체는 ./agentic-loop 에 분리.
135
127
  //
@@ -214,8 +206,7 @@ export class DefaultBoardAIAssistant {
214
206
  * 근거집합의 정의와 사정거리는 ./grounding 머리말 참조(식별자 없는 창작 서술은 잡지 못한다). */
215
207
  const warnings = checkReplyGrounding(baseReply, {
216
208
  systemPrompt,
217
- /* 근거집합의 칸 이름은 공용 패키지의 것이다 — 이 값이 보드 문맥이라는 것은 이 파일만 안다. */
218
- hostContext: boardContext,
209
+ /* 보드는 이제 도구로만 모델에게 닿는다 — 읽은 보드는 toolResults 안에 있다(screen-pattern §2-2). */
219
210
  messages: messages.map(m => (typeof m.content === 'string' ? m.content : '')),
220
211
  toolResults: loopResult.groundedTexts
221
212
  });
@@ -227,7 +218,7 @@ export class DefaultBoardAIAssistant {
227
218
  confidence: { level: warnings.length ? 'low' : 'medium', reasons: warnings.length ? ['grounding-warning'] : ['tool-or-board-context'] },
228
219
  limitations: warnings.map(code => ({ code })),
229
220
  claims: [{ code: patch ? 'board.patch-proposed' : 'board.context-reviewed', severity: 'info', evidence: { toolCalls: toolUsages.length, componentCount: components.length }, method: 'board-agentic-loop', assumptions: ['Board context is current for this turn.'] }],
230
- facts: { board: boardContext, toolUsages },
221
+ facts: { board: summarizeBoard(currentBoard), toolUsages },
231
222
  ...(patch ? { proposal: patch } : accumulatedActions.length ? { proposal: accumulatedActions } : {}),
232
223
  approvalRequired: true,
233
224
  approval: { required: true, mode: 'confirm', reason: patch || accumulatedActions.length ? 'Board or scene change requires user confirmation.' : 'Analysis is advisory.' },
@@ -329,8 +320,8 @@ Rules:
329
320
  배열에 자식을 가질 수 있다. 자식이 또 group 일 수 있어 임의 깊이로 중첩 가능.
330
321
 
331
322
  가시성:
332
- - \`summarizeBoard\` (대화 시작 시 보여지는 보드 상태) 는 *평면 list* 로 모든
333
- 컴포넌트를 노출한다 — top-level 뿐 아니라 group/container 안 자식도 모두 포함.
323
+ - \`getBoardOutline\` 은 *평면 list* 로 모든 컴포넌트를 돌려준다 — top-level 뿐 아니라
324
+ group/container 안 자식도 모두 포함.
334
325
  - 자식은 \`_parentRefid\` (부모 group refid) 와 \`_depth\` (중첩 깊이, 0=root) 메타
335
326
  가 첨부되어 어느 group 안에 있는지 식별 가능.
336
327
  - \`findComponents\` 는 전체 트리를 search — group 안 자식까지 매칭. 특정 group
@@ -520,7 +511,7 @@ Behavioral rules:
520
511
  - If the user is asking a question rather than commanding, REPLY in TEXT WITHOUT calling any tool.
521
512
  - If the user's request is ambiguous, REPLY in TEXT (asking for clarification) WITHOUT calling any tool.
522
513
  - For multiple components, call addComponent multiple times in parallel.
523
- - The current board JSON is provided as the first user message in the conversation.
514
+ - 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.
524
515
 
525
516
  Reply formatting rules (chat surface):
526
517
  - 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).
@@ -587,7 +578,7 @@ export function buildBoardEditTools(knownTypes, exposure = {}) {
587
578
  },
588
579
  {
589
580
  name: 'removeComponentByRefid',
590
- description: 'Remove an existing component by its `refid` — the universal numeric handle that things-scene auto-assigns to every component (visible in board summary as the `refid` field of each component).',
581
+ description: '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`).',
591
582
  parameters: {
592
583
  type: 'object',
593
584
  properties: {
@@ -678,6 +669,11 @@ export function buildBoardEditTools(knownTypes, exposure = {}) {
678
669
  }
679
670
  },
680
671
  // ── Read tools (서버측 즉시 실행, 결과를 LLM 에 회신) ─────────
672
+ {
673
+ name: 'getBoardOutline',
674
+ description: '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.',
675
+ parameters: { type: 'object', properties: {} }
676
+ },
681
677
  {
682
678
  name: 'getSelection',
683
679
  description: 'Get full details of components currently selected by the user in the modeller. Call this when the user asks "what is selected?" / "선택된 게 뭐냐?" / refers to "the selected component" without specifying ids. Returns array of full component objects (no truncation).',
@@ -685,7 +681,7 @@ export function buildBoardEditTools(knownTypes, exposure = {}) {
685
681
  },
686
682
  {
687
683
  name: 'getComponentByRefid',
688
- description: 'Get full details of a single component by its `refid` (the things-scene-assigned universal numeric handle). Use this to inspect properties not visible in the board summary.',
684
+ description: '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.',
689
685
  parameters: {
690
686
  type: 'object',
691
687
  properties: {
@@ -1037,7 +1033,11 @@ export function buildBoardEditTools(knownTypes, exposure = {}) {
1037
1033
  return tools;
1038
1034
  }
1039
1035
  // ── Read tool 분류 + 실행기 ────────────────────────────────────────
1036
+ /** How the model learns the board and the selection - by reading, every time it needs to (screen-pattern §2-2). */
1037
+ const READ_THE_BOARD = 'The board and the selection are not in the conversation. Read the board with `getBoardOutline` before you describe or change it, ' +
1038
+ 'and the selection with `getSelection` when the person says "selected", "this" or "these" without naming components; then work only on those.';
1040
1039
  const READ_TOOL_NAMES = new Set([
1040
+ 'getBoardOutline',
1041
1041
  'getSelection',
1042
1042
  'getComponentByRefid',
1043
1043
  'getComponentStyle',
@@ -1103,6 +1103,9 @@ export function executeReadTool(call, currentBoard, selectedRefids) {
1103
1103
  // 컴포넌트 인지 가능. (top-level 만 보던 옛 동작 → 자식 누락 사고 방지)
1104
1104
  const flatComponents = collectAllComponents(board).map(f => f.component);
1105
1105
  switch (call.name) {
1106
+ case 'getBoardOutline':
1107
+ /* The summary that used to be pasted into every request - now read when the question needs it. */
1108
+ return board ? summarizeBoard(board) : { empty: true, message: 'No board is open in this conversation.' };
1106
1109
  case 'getSelection': {
1107
1110
  if (selectedRefids.length === 0) {
1108
1111
  return { selected: [], message: 'No component is currently selected.' };