@things-factory/board-ai 10.0.0-beta.70 → 10.0.0-beta.72

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.
@@ -7,7 +7,8 @@ import { resolveCatalogMentions } from './catalog-resolver';
7
7
  import { buildComponentStyleSummary } from './styling/read-tools';
8
8
  import { runAgenticLoop } from './agentic-loop';
9
9
  import { collectAllComponents, findComponentByRefid } from './component-tree';
10
- import { isStylingTool, dispatchStylingTool, buildStylingToolDefinitions } from './styling/registry';
10
+ import { isStylingTool, dispatchStylingTool, buildStylingToolDefinitions } from './styling/registry.js';
11
+ import { getAllToolSpecs, getToolKind, findToolSpec } from '@things-factory/ai-client-base';
11
12
  import { validateWriteToolCall } from './validation/tool-validation';
12
13
  const ALL_CATEGORIES = [
13
14
  'container',
@@ -99,6 +100,29 @@ export class DefaultBoardAIAssistant {
99
100
  ];
100
101
  // multi-turn agentic loop — LLM 이 read 호출 → 서버 실행 → 결과 회신 → LLM 추론 반복.
101
102
  // write tool 은 누적해 client 에 patch 로 전달. 본체는 ./agentic-loop 에 분리.
103
+ //
104
+ // executeReadTool 은 closure wrapper — board-ai 자체 read tool (READ_TOOL_NAMES) 은
105
+ // 기존 hardcoded executeReadTool 로 dispatch, registry 에 등록된 read/external tool
106
+ // 은 spec.builder 에 ctx (board, selectedRefids, state=options.toolCallContext) 주입.
107
+ const toolCallContext = options.toolCallContext;
108
+ const wrappedExecuteReadTool = (call, board, selectedRefids) => {
109
+ // 1) board-ai 코어 read tool — 기존 hardcoded 경로
110
+ if (READ_TOOL_NAMES.has(call.name)) {
111
+ return executeReadTool(call, board, selectedRefids);
112
+ }
113
+ // 2) registry — read 와 external 둘 다 동일 dispatch path (LLM 회신 측면 동일).
114
+ // external builder 가 server-side mutation 호출하는 케이스라 ctx.state 주입 필수.
115
+ const spec = findToolSpec(call.name);
116
+ if (spec && (spec.kind === 'read' || spec.kind === 'external')) {
117
+ return spec.builder(call.arguments ?? {}, {
118
+ board,
119
+ selectedRefids,
120
+ state: toolCallContext
121
+ });
122
+ }
123
+ // 3) 미등록 / 잘못된 분기 — 빈 응답. agentic-loop 가 LLM 에 회신 후 다음 turn.
124
+ return undefined;
125
+ };
102
126
  const loopResult = await runAgenticLoop({
103
127
  initialConversation: conversation,
104
128
  tools,
@@ -113,7 +137,7 @@ export class DefaultBoardAIAssistant {
113
137
  isReadTool,
114
138
  isWriteTool,
115
139
  isActionTool,
116
- executeReadTool,
140
+ executeReadTool: wrappedExecuteReadTool,
117
141
  validateWriteToolCall,
118
142
  toolCallToBoardActionOp,
119
143
  summarizeToolResult
@@ -529,6 +553,10 @@ export function buildBoardEditTools(knownTypes) {
529
553
  // styling registry 에서 자동 도출 — setFill 부터 점진 이전 중.
530
554
  // 다음 tool 들은 registry 로 옮겨질 때마다 본 spread 가 자동 흡수.
531
555
  ...buildStylingToolDefinitions(),
556
+ // tool-registry plugin point — 외부 도메인 패키지 (board-import 등) 가
557
+ // registerToolCategory() 로 등록한 모든 tool 의 LLM 정의를 spread.
558
+ // styling 처럼 board-ai 코어 안에 정의되지 않고 외부에서 들어오는 카테고리들.
559
+ ...buildExternalCategoryDefinitions(),
532
560
  {
533
561
  name: 'getComponentStyle',
534
562
  description: "Get the current STYLING state of a component (read-only). Useful before modifying styles to understand what's already applied (avoid blind overwrites). Returns fill / stroke / shadow / material3d / font / opacity / visibility / text. Use `aspects` to limit response size.",
@@ -1698,7 +1726,13 @@ const LAYOUT_TEMPLATES = {
1698
1726
  ]
1699
1727
  };
1700
1728
  export function isReadTool(name) {
1701
- return READ_TOOL_NAMES.has(name);
1729
+ if (READ_TOOL_NAMES.has(name))
1730
+ return true;
1731
+ // tool-registry plugin point — 'read' 와 'external' 는 LLM 측에서 동일하게 다뤄짐
1732
+ // (server 즉시 실행 + 결과 회신). external 은 board model 외부 entity 변경이
1733
+ // 가능한 점에서 의미 차이가 있지만, dispatch path 는 read 와 같다.
1734
+ const kind = getToolKind(name);
1735
+ return kind === 'read' || kind === 'external';
1702
1736
  }
1703
1737
  /**
1704
1738
  * 등록된 색 팔레트 이름 목록 — catalog-resolver / 클라이언트 popup 데이터 source.
@@ -1769,10 +1803,26 @@ export function summarizeToolResult(value, depth = 0) {
1769
1803
  return undefined;
1770
1804
  }
1771
1805
  export function isWriteTool(name) {
1772
- return WRITE_TOOL_NAMES.has(name);
1806
+ if (WRITE_TOOL_NAMES.has(name))
1807
+ return true;
1808
+ return getToolKind(name) === 'write';
1773
1809
  }
1774
1810
  export function isActionTool(name) {
1775
- return ACTION_TOOL_NAMES.has(name);
1811
+ if (ACTION_TOOL_NAMES.has(name))
1812
+ return true;
1813
+ return getToolKind(name) === 'action';
1814
+ }
1815
+ /**
1816
+ * tool-registry plugin point — 등록된 모든 외부 카테고리의 LLM tool 정의를 도출.
1817
+ * styling-tools 가 buildStylingToolDefinitions 로 자기 정의를 내보내듯, 외부 카테고리
1818
+ * (board-import 등) 도 본 함수로 합집합. buildBoardEditTools 의 spread 부분에서 호출.
1819
+ */
1820
+ function buildExternalCategoryDefinitions() {
1821
+ return getAllToolSpecs().map(spec => ({
1822
+ name: spec.name,
1823
+ description: spec.themeNote ? `${spec.description} ${spec.themeNote}` : spec.description,
1824
+ parameters: spec.schema
1825
+ }));
1776
1826
  }
1777
1827
  /**
1778
1828
  * Tool call → BoardActionOp 변환 (C-1 ephemeral scene 조작).
@@ -1846,6 +1896,18 @@ export function toolCallToBoardEditOp(tc, components = []) {
1846
1896
  if (isStylingTool(tc.name)) {
1847
1897
  return dispatchStylingTool(tc.name, args, components);
1848
1898
  }
1899
+ // tool-registry plugin point — 외부 등록 'write' 카테고리 dispatch.
1900
+ // builder 결과는 BoardEditOp[] | { ops, errors? } | null 셋 중 하나.
1901
+ // (read/external 카테고리는 별도 dispatch path — agentic loop 의 executeReadTool 이 직접 처리.)
1902
+ const registrySpec = findToolSpec(tc.name);
1903
+ if (registrySpec && registrySpec.kind === 'write') {
1904
+ const r = registrySpec.builder(args, { components });
1905
+ if (!r)
1906
+ return null;
1907
+ if (Array.isArray(r))
1908
+ return r.length > 0 ? r : null;
1909
+ return r.ops.length > 0 ? r.ops : null;
1910
+ }
1849
1911
  switch (tc.name) {
1850
1912
  case 'addComponent': {
1851
1913
  if (typeof args.type !== 'string')