@things-factory/board-ai 10.1.176 → 10.1.178

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/board-ai",
3
- "version": "10.1.176",
3
+ "version": "10.1.178",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/client/index.js",
6
6
  "things-factory": true,
@@ -32,12 +32,12 @@
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.176",
36
- "@things-factory/ai-client-base": "^10.1.176",
37
- "@things-factory/auth-base": "^10.1.176",
38
- "@things-factory/board-import": "^10.1.176",
35
+ "@things-factory/ai-assistant": "^10.1.178",
36
+ "@things-factory/ai-client-base": "^10.1.178",
37
+ "@things-factory/auth-base": "^10.1.178",
38
+ "@things-factory/board-import": "^10.1.178",
39
39
  "@things-factory/env": "^10.1.149",
40
- "@things-factory/shell": "^10.1.165",
40
+ "@things-factory/shell": "^10.1.178",
41
41
  "dompurify": "^3.0.0",
42
42
  "graphql-tag": "^2.12.6",
43
43
  "lit": "^3.1.2",
@@ -47,5 +47,5 @@
47
47
  "copyfiles": "^2.4.1",
48
48
  "rimraf": "^5.0.0"
49
49
  },
50
- "gitHead": "24928f127d73de0e9946423bd4c3295f54cce03d"
50
+ "gitHead": "d57448d3f3045f72a9f24b877a3e306eb34cbaf1"
51
51
  }
@@ -15,7 +15,7 @@ import {
15
15
  isActionTool,
16
16
  executeReadTool,
17
17
  summarizeBoard,
18
- summarizeToolResult,
18
+ resultShapeOf,
19
19
  buildBoardEditTools,
20
20
  DefaultBoardAIAssistant
21
21
  } from './assistant'
@@ -1857,9 +1857,9 @@ describe('agentic loop (chatViaTools) — mock provider', () => {
1857
1857
  expect(u.name).toBe('getSelection')
1858
1858
  expect(u.kind).toBe('read')
1859
1859
  expect(u.arguments).toEqual({})
1860
- // result 는 summarized 객체
1861
- expect(u.result.count).toBe(1)
1862
- expect(u.result.selected[0].name).toBe('AGV-1')
1860
+ // result 는 값이 아니라 모양 — 건수 · 키 이름 (ADR-0061 결정 8)
1861
+ expect(u.result).toMatchObject({ count: 1, keys: expect.arrayContaining(['selected']) })
1862
+ expect(JSON.stringify(u.result)).not.toMatch(/AGV-1/)
1863
1863
  expect(isGroundedAnalysisContract(r.analysis)).toBe(true)
1864
1864
  expect(r.analysis?.kind).toBe('board.analysis.v1')
1865
1865
  expect(r.analysis?.approvalRequired).toBe(true)
@@ -1885,7 +1885,7 @@ describe('agentic loop (chatViaTools) — mock provider', () => {
1885
1885
  const u = r.toolUsages![0]
1886
1886
  expect(u.name).toBe('addComponent')
1887
1887
  expect(u.kind).toBe('write')
1888
- expect(u.result.queued).toBe(true)
1888
+ expect(u.result.keys).toContain('queued')
1889
1889
  })
1890
1890
 
1891
1891
  test('toolUsages 수집 — read + write 혼합 시간순 보존', async () => {
@@ -1929,7 +1929,7 @@ describe('agentic loop (chatViaTools) — mock provider', () => {
1929
1929
  const r = await a.chat([{ role: 'user', content: 'x' }], undefined)
1930
1930
  expect(r.toolUsages).toHaveLength(1)
1931
1931
  expect(r.toolUsages![0].kind).toBe('unknown')
1932
- expect(r.toolUsages![0].result.error).toMatch(/unknown/i)
1932
+ expect(r.toolUsages![0].result.errorCode).toMatch(/unknown/i)
1933
1933
  })
1934
1934
 
1935
1935
  test('actions 수집 — selectComponents 호출 시 ChatResponse.actions 누적', async () => {
@@ -2011,43 +2011,28 @@ describe('agentic loop (chatViaTools) — mock provider', () => {
2011
2011
  })
2012
2012
  })
2013
2013
 
2014
- describe('summarizeToolResult — UI 영속용 결과 압축', () => {
2015
- test('짧은 string / number / boolean 그대로', () => {
2016
- expect(summarizeToolResult('hello')).toBe('hello')
2017
- expect(summarizeToolResult(42)).toBe(42)
2018
- expect(summarizeToolResult(true)).toBe(true)
2019
- expect(summarizeToolResult(null)).toBeNull()
2014
+ describe('resultShapeOf — 대화 행에 남는 결과의 모양 (ADR-0061 결정 8)', () => {
2015
+ test('값이 아니라 크기 · 건수 · 키 이름만 남는다', () => {
2016
+ const pay = { read: 2, rows: [{ name: '김급여', net: 3100000 }, { name: '이급여', net: 2900000 }] }
2017
+ const shape = resultShapeOf(pay)
2018
+ expect(shape).toEqual({ size: JSON.stringify(pay).length, count: 2, keys: ['read', 'rows'] })
2019
+ expect(JSON.stringify(shape)).not.toMatch(/김급여|3100000/)
2020
2020
  })
2021
2021
 
2022
- test('200자 초과 string truncate + 마커', () => {
2023
- const long = 'x'.repeat(500)
2024
- const r = summarizeToolResult(long)
2025
- expect(r).toMatch(/…\[\+\d+ chars\]$/)
2026
- expect(r.length).toBeLessThan(long.length)
2022
+ test('목록은 건수, 값 하나는 크기만', () => {
2023
+ expect(resultShapeOf([1, 2, 3])).toEqual({ size: 7, count: 3 })
2024
+ expect(resultShapeOf('hello')).toEqual({ size: 7 })
2025
+ expect(resultShapeOf(null)).toEqual({ size: 4 })
2027
2026
  })
2028
2027
 
2029
- test('5개 초과 array truncate + 마커', () => {
2030
- const big = Array.from({ length: 12 }, (_, i) => i)
2031
- const r = summarizeToolResult(big)
2032
- expect(r.length).toBe(6) // 5 + marker
2033
- expect(r[5]).toMatch(/items\]/)
2028
+ test('거절과 실패는 코드로', () => {
2029
+ expect(resultShapeOf({ rejected: true, error: 'query-failed', message: '김급여 unauthorized' })).toMatchObject({ errorCode: 'query-failed' })
2030
+ expect(JSON.stringify(resultShapeOf({ error: 'tool-failed', message: 'the ledger of 김급여' }))).not.toMatch(/김급여/)
2034
2031
  })
2035
2032
 
2036
- test('object 의 nested 값 재귀 요약', () => {
2037
- const o = { a: 1, b: { c: 'x'.repeat(500) } }
2038
- const r = summarizeToolResult(o)
2039
- expect(r.a).toBe(1)
2040
- expect(r.b.c).toMatch(/…/)
2041
- })
2042
-
2043
- test('depth 3 초과 object 는 키 목록 string 으로 압축', () => {
2044
- const deep = { l1: { l2: { l3: { l4: { x: 1, y: 2, z: 3 } } } } }
2045
- const r = summarizeToolResult(deep)
2046
- // l1 (d0) → l2 (d1) → l3 (d2) 까지 object 유지, l3 의 값은 d3 진입 → string
2047
- expect(typeof r.l1).toBe('object')
2048
- expect(typeof r.l1.l2).toBe('object')
2049
- // l3 자체가 d3 에서 평가되어 키 목록 문자열로 압축됨
2050
- expect(typeof r.l1.l2.l3).toBe('string')
2051
- expect(r.l1.l2.l3).toMatch(/^\{l4/)
2033
+ test('키가 많으면 앞의 것만 이름으로, 나머지는 수로', () => {
2034
+ const wide = Object.fromEntries(Array.from({ length: 25 }, (_, i) => [`k${i}`, i]))
2035
+ expect(resultShapeOf(wide)).toMatchObject({ moreKeys: 5 })
2036
+ expect(resultShapeOf(wide).keys).toHaveLength(20)
2052
2037
  })
2053
2038
  })
@@ -41,7 +41,7 @@ import { buildComponentStyleSummary } from './styling/read-tools'
41
41
  import {
42
42
  runAgenticLoop,
43
43
  formatAbortNotice,
44
- summarizeToolResult,
44
+ resultShapeOf,
45
45
  registryToolGate,
46
46
  extendToolGate,
47
47
  type ToolGate as NeutralToolGate,
@@ -270,7 +270,8 @@ export class DefaultBoardAIAssistant implements BoardAIAssistant {
270
270
  /* 문서에서 부품을 꺼내는 일은 **여기서** 한다 — 루프는 board 의 모양을 모른다. */
271
271
  validateWriteToolCall: (tc, ctx) => validateWriteToolCall(tc, (ctx.board?.components ?? []) as any[]),
272
272
  toolCallToOp: toolCallToSceneActionOp,
273
- summarizeToolResult
273
+ /* The conversation row keeps each answer's shape, never its values (ADR-0061 decision 8). */
274
+ summarizeToolResult: resultShapeOf
274
275
  },
275
276
  context: { board: currentBoard, selectedRefids }
276
277
  })
@@ -2034,7 +2035,7 @@ export function listLayoutTemplateNames(): string[] {
2034
2035
  }
2035
2036
 
2036
2037
  /*
2037
- * 중단 통지 · 도구 결과 요약 — **공용 패키지로 옮겼다** (2026-09-10).
2038
+ * 중단 통지 · 도구 결과의 모양 — **공용 패키지에 있다** (2026-09-10, 결과는 값 대신 모양 2026-10-17).
2038
2039
  *
2039
2040
  * 둘 다 보드를 모르는 함수였다. 옮긴 이유는 중복이 아니라 **한쪽만 갖고 있었던 것**이다:
2040
2041
  * `twin-ai` 는 `summarizeToolResult: v => v` 로 요약을 아예 안 하고 있었다. 한 제품은 압축해서
@@ -2045,7 +2046,7 @@ export function listLayoutTemplateNames(): string[] {
2045
2046
  *
2046
2047
  * 여기 남은 것은 재수출이다. 이 패키지를 지나는 소비처가 이름을 그대로 쓰게 한다.
2047
2048
  */
2048
- export { formatAbortNotice, summarizeToolResult }
2049
+ export { formatAbortNotice, resultShapeOf }
2049
2050
 
2050
2051
 
2051
2052
  export function isWriteTool(name: string): boolean {