@things-factory/ai-client-base 10.1.28 → 10.1.29
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-server/service/agentic-loop/agentic-loop.js +13 -1
- package/dist-server/service/agentic-loop/agentic-loop.js.map +1 -1
- package/dist-server/service/assistant/registry-assistant.d.ts +11 -0
- package/dist-server/service/assistant/registry-assistant.js +5 -1
- package/dist-server/service/assistant/registry-assistant.js.map +1 -1
- package/dist-server/service/tool-registry/registry.d.ts +1 -1
- package/dist-server/service/tool-registry/registry.js +27 -4
- package/dist-server/service/tool-registry/registry.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/server/service/agentic-loop/agentic-loop.ts +14 -1
- package/server/service/agentic-loop/proposal-key.test.ts +31 -0
- package/server/service/assistant/registry-assistant.test.ts +43 -0
- package/server/service/assistant/registry-assistant.ts +13 -1
- package/server/service/tool-registry/registry.test.ts +48 -8
- package/server/service/tool-registry/registry.ts +31 -5
|
@@ -23,6 +23,16 @@ afterEach(() => {
|
|
|
23
23
|
})
|
|
24
24
|
|
|
25
25
|
// 헬퍼 — 카테고리 fixture
|
|
26
|
+
/**
|
|
27
|
+
* 권한 축의 경고만 — 문 축(`tool-doors-undeclared`)은 자기 시험이 따로 본다.
|
|
28
|
+
*
|
|
29
|
+
* 한 시험이 두 축을 함께 세면, 한 축에 코드가 하나 늘 때마다 다른 축의 시험이 빨개진다. 그러면 고치는
|
|
30
|
+
* 사람은 자기가 건드리지 않은 시험을 고치게 되고, 그 손질이 시험을 무디게 만든다.
|
|
31
|
+
*/
|
|
32
|
+
function privilegeWarnings() {
|
|
33
|
+
return getToolRegistryWarnings().filter(w => w.code !== 'tool-doors-undeclared')
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
function makeCategory(name: string, specs: Partial<ToolSpec>[]): ToolCategory {
|
|
27
37
|
return {
|
|
28
38
|
name,
|
|
@@ -272,7 +282,7 @@ describe('registry — a tool that changes something names its door (A1, stage o
|
|
|
272
282
|
specs: [{ kind, name: 'changesThings', description: 'd', schema: {}, builder: () => null } as any]
|
|
273
283
|
})
|
|
274
284
|
expect(getToolKind('changesThings')).toBe(kind)
|
|
275
|
-
expect(
|
|
285
|
+
expect(privilegeWarnings()).toEqual([
|
|
276
286
|
expect.objectContaining({
|
|
277
287
|
code: 'tool-privilege-undeclared',
|
|
278
288
|
params: { category: 'no-door', tool: 'changesThings', kind }
|
|
@@ -289,12 +299,12 @@ describe('registry — a tool that changes something names its door (A1, stage o
|
|
|
289
299
|
{ kind: 'external', name: 'halfDoor', description: 'd', schema: {}, builder: () => null, privilege: { category: 'board' } } as any
|
|
290
300
|
]
|
|
291
301
|
})
|
|
292
|
-
expect(
|
|
302
|
+
expect(privilegeWarnings().map(w => w.params.tool)).toEqual(['halfDoor'])
|
|
293
303
|
})
|
|
294
304
|
|
|
295
305
|
test('a declared tool leaves no warning', () => {
|
|
296
306
|
registerToolCategory(makeCategory('declared', [{ name: 'w1', kind: 'write' }]))
|
|
297
|
-
expect(
|
|
307
|
+
expect(privilegeWarnings()).toEqual([])
|
|
298
308
|
})
|
|
299
309
|
|
|
300
310
|
/*
|
|
@@ -304,7 +314,7 @@ describe('registry — a tool that changes something names its door (A1, stage o
|
|
|
304
314
|
test('★ a read tool that says nothing about its door is recorded too', () => {
|
|
305
315
|
registerToolCategory(makeCategory('reads', [{ name: 'r1', kind: 'read' }]))
|
|
306
316
|
|
|
307
|
-
expect(
|
|
317
|
+
expect(privilegeWarnings()).toEqual([
|
|
308
318
|
expect.objectContaining({ code: 'tool-privilege-undeclared', params: { category: 'reads', tool: 'r1', kind: 'read' } })
|
|
309
319
|
])
|
|
310
320
|
})
|
|
@@ -318,7 +328,7 @@ describe('registry — a tool that changes something names its door (A1, stage o
|
|
|
318
328
|
]
|
|
319
329
|
})
|
|
320
330
|
|
|
321
|
-
expect(
|
|
331
|
+
expect(privilegeWarnings()).toEqual([])
|
|
322
332
|
})
|
|
323
333
|
|
|
324
334
|
test('★ a named door with no privilege of its own is its own fact, not an omission', () => {
|
|
@@ -330,7 +340,7 @@ describe('registry — a tool that changes something names its door (A1, stage o
|
|
|
330
340
|
]
|
|
331
341
|
})
|
|
332
342
|
|
|
333
|
-
expect(
|
|
343
|
+
expect(privilegeWarnings()).toEqual([
|
|
334
344
|
expect.objectContaining({
|
|
335
345
|
code: 'tool-door-has-no-privilege',
|
|
336
346
|
params: { category: 'waiting', tool: 'twinNowTool', kind: 'read', doors: ['Query.twinNow'] }
|
|
@@ -338,13 +348,43 @@ describe('registry — a tool that changes something names its door (A1, stage o
|
|
|
338
348
|
])
|
|
339
349
|
})
|
|
340
350
|
|
|
351
|
+
/*
|
|
352
|
+
* 문을 안 적은 도구는 그 답을 **아무도 못 읽게 만든다** — 쓴 사람 본인까지.
|
|
353
|
+
*
|
|
354
|
+
* ADR-0073 뒤로 답 행에 도구가 지난 문이 적히고, 선언이 없는 도구는 `unresolved:<도구>` 로 적혀
|
|
355
|
+
* 아무도 못 지난다. 2026-09-18 트윈 모델러에서 저작 대화의 답이 새로 고친 뒤 작성자에게
|
|
356
|
+
* 「이 답을 볼 권한이 없습니다」로 보였다.
|
|
357
|
+
*
|
|
358
|
+
* 부팅은 그때 아무 말도 안 했다. 권한 검사가 `privilege` 가 있으면 건너뛰는 자리에서 문 검사까지
|
|
359
|
+
* 함께 건너뛰었기 때문이다 — 트윈의 저작 도구 넷이 정확히 그 모양이었다(권한 있음, 문 없음).
|
|
360
|
+
*/
|
|
361
|
+
test('★ a tool that declares no doors is reported even when it declares a privilege', () => {
|
|
362
|
+
registerToolCategory(
|
|
363
|
+
makeCategory('authoring', [
|
|
364
|
+
{ name: 'proposeSceneEdit', kind: 'write', privilege: { category: 'board', privilege: 'mutation' } }
|
|
365
|
+
])
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
const doors = getToolRegistryWarnings().filter(w => w.code === 'tool-doors-undeclared')
|
|
369
|
+
expect(doors.map(w => w.params.tool)).toEqual(['proposeSceneEdit'])
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
test('★ `doors: []` is a declaration — it says this tool reaches none', () => {
|
|
373
|
+
registerToolCategory(makeCategory('surface', [{ name: 'getOpenScene', kind: 'read', doors: [] }]))
|
|
374
|
+
|
|
375
|
+
expect(getToolRegistryWarnings().filter(w => w.code === 'tool-doors-undeclared')).toEqual([])
|
|
376
|
+
})
|
|
377
|
+
|
|
341
378
|
test('re-registering a category replaces its warnings instead of piling them up', () => {
|
|
342
379
|
const undeclared = { kind: 'write', name: 'w9', description: 'd', schema: {}, builder: () => null } as any
|
|
343
380
|
registerToolCategory({ name: 'again', guidance: '- g', specs: [undeclared] })
|
|
381
|
+
const once = getToolRegistryWarnings().map(w => w.code).sort()
|
|
382
|
+
|
|
344
383
|
registerToolCategory({ name: 'again', guidance: '- g', specs: [undeclared] })
|
|
345
|
-
|
|
384
|
+
/* 두 번 등록해도 한 번 등록한 것과 같아야 한다 — 축이 몇이든. */
|
|
385
|
+
expect(getToolRegistryWarnings().map(w => w.code).sort()).toEqual(once)
|
|
346
386
|
|
|
347
387
|
registerToolCategory({ name: 'again', guidance: '- g', specs: [{ ...undeclared, privilege: { category: 'c', privilege: 'mutation' } }] })
|
|
348
|
-
expect(
|
|
388
|
+
expect(privilegeWarnings()).toEqual([])
|
|
349
389
|
})
|
|
350
390
|
})
|
|
@@ -37,7 +37,7 @@ export interface ToolRegistryWarning {
|
|
|
37
37
|
* (twin's queries, waiting on AUTH stage two). The tool cannot be stricter than its door, so
|
|
38
38
|
* this is a different fact from saying nothing, and the report keeps the two apart.
|
|
39
39
|
*/
|
|
40
|
-
code: 'tool-privilege-undeclared' | 'tool-door-has-no-privilege'
|
|
40
|
+
code: 'tool-privilege-undeclared' | 'tool-door-has-no-privilege' | 'tool-doors-undeclared'
|
|
41
41
|
params: { category: string; tool: string; kind: ToolSpec['kind']; doors?: string[] }
|
|
42
42
|
/** For a person reading it; the code and params are what callers act on. */
|
|
43
43
|
message: string
|
|
@@ -95,6 +95,30 @@ export function registerToolCategory(category: ToolCategory): void {
|
|
|
95
95
|
* is empty across the apps, this becomes a refusal.
|
|
96
96
|
*/
|
|
97
97
|
warnings.splice(0, warnings.length, ...warnings.filter(w => w.params.category !== category.name))
|
|
98
|
+
|
|
99
|
+
/*
|
|
100
|
+
* ── 문을 안 적은 도구는 그 답을 **아무도 못 읽게 만든다** ─────────────────
|
|
101
|
+
*
|
|
102
|
+
* ADR-0073 뒤로 어시스턴트의 답 행에는 그 답을 만든 도구들이 지난 문이 적힌다. 선언이 없는 도구는
|
|
103
|
+
* `unresolved:<도구>` 로 적히고 그 이름은 아무도 못 지난다 — **답을 쓴 본인도** 새로 고치면
|
|
104
|
+
* 「이 답을 볼 권한이 없습니다」를 본다. 2026-09-18 트윈 모델러에서 그렇게 나왔다.
|
|
105
|
+
*
|
|
106
|
+
* 이 검사를 권한 검사와 따로 도는 이유가 그것이다. 아래 고리는 `privilege` 가 있으면 건너뛰는데,
|
|
107
|
+
* 문을 안 적은 해는 권한을 적었는지와 상관이 없다. 트윈의 저작 도구 넷이 정확히 그 자리였다 —
|
|
108
|
+
* 권한은 있고 문은 없어서 부팅이 아무 말도 안 했다.
|
|
109
|
+
*/
|
|
110
|
+
for (const spec of category.specs) {
|
|
111
|
+
if (Array.isArray(spec.doors)) continue
|
|
112
|
+
|
|
113
|
+
warnings.push({
|
|
114
|
+
code: 'tool-doors-undeclared',
|
|
115
|
+
params: { category: category.name, tool: spec.name, kind: spec.kind },
|
|
116
|
+
message:
|
|
117
|
+
`tool '${spec.name}' in category '${category.name}' declares no doors, so answers built with it ` +
|
|
118
|
+
'are hidden from every reader including their author — declare `doors`, or `doors: []` when it reaches none'
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
98
122
|
for (const spec of category.specs) {
|
|
99
123
|
const declared = Boolean(spec.privilege?.category && spec.privilege?.privilege)
|
|
100
124
|
if (declared) continue
|
|
@@ -115,8 +139,9 @@ export function registerToolCategory(category: ToolCategory): void {
|
|
|
115
139
|
code: 'tool-door-has-no-privilege',
|
|
116
140
|
params: { category: category.name, tool: spec.name, kind: spec.kind, doors: spec.doors },
|
|
117
141
|
message:
|
|
118
|
-
`tool '${spec.name}' reaches ${spec.doors.join(' + ')}
|
|
119
|
-
'
|
|
142
|
+
`tool '${spec.name}' reaches ${spec.doors.join(' + ')} and declares no privilege of its own — ` +
|
|
143
|
+
'whether those doors require anything is asked at boot, against the schema, not copied here ' +
|
|
144
|
+
'(ADR-0072 decision 5)'
|
|
120
145
|
})
|
|
121
146
|
continue
|
|
122
147
|
}
|
|
@@ -125,8 +150,9 @@ export function registerToolCategory(category: ToolCategory): void {
|
|
|
125
150
|
code: 'tool-privilege-undeclared',
|
|
126
151
|
params: { category: category.name, tool: spec.name, kind: spec.kind },
|
|
127
152
|
message:
|
|
128
|
-
`tool '${spec.name}' in category '${category.name}'
|
|
129
|
-
|
|
153
|
+
`tool '${spec.name}' in category '${category.name}' declares no privilege of its own — ` +
|
|
154
|
+
'a tool that changes something says who may run it. What the doors it reaches require is a ' +
|
|
155
|
+
"separate question, answered at boot against the schema (`tool-door-not-in-schema`)."
|
|
130
156
|
})
|
|
131
157
|
}
|
|
132
158
|
|