@things-factory/ai-client-base 10.1.165 → 10.1.168
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/assistant/registry-assistant.d.ts +11 -1
- package/dist-server/service/assistant/registry-assistant.js +9 -1
- package/dist-server/service/assistant/registry-assistant.js.map +1 -1
- package/dist-server/service/testing/tool-call-context.d.ts +5 -1
- package/dist-server/service/testing/tool-call-context.js +4 -1
- package/dist-server/service/testing/tool-call-context.js.map +1 -1
- package/dist-server/service/tool-registry/index.d.ts +2 -1
- package/dist-server/service/tool-registry/index.js +3 -1
- package/dist-server/service/tool-registry/index.js.map +1 -1
- package/dist-server/service/tool-registry/translate.d.ts +2 -0
- package/dist-server/service/tool-registry/translate.js +6 -0
- package/dist-server/service/tool-registry/translate.js.map +1 -0
- package/dist-server/service/tool-registry/types.d.ts +50 -0
- package/dist-server/service/tool-registry/types.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/server/service/assistant/registry-assistant.test.ts +70 -0
- package/server/service/assistant/registry-assistant.ts +20 -3
- package/server/service/testing/tool-call-context.ts +9 -2
- package/server/service/tool-registry/index.ts +7 -1
- package/server/service/tool-registry/translate.ts +12 -0
- package/server/service/tool-registry/types.ts +54 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/ai-client-base",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.168",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"things-factory": true,
|
|
6
6
|
"author": "heartyoh",
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@anthropic-ai/sdk": "^0.40.0",
|
|
26
26
|
"@google/generative-ai": "^0.24.0",
|
|
27
|
-
"@things-factory/auth-base": "^10.1.
|
|
27
|
+
"@things-factory/auth-base": "^10.1.167",
|
|
28
28
|
"@things-factory/env": "^10.1.149",
|
|
29
|
-
"@things-factory/setting-base": "^10.1.
|
|
29
|
+
"@things-factory/setting-base": "^10.1.167",
|
|
30
30
|
"@things-factory/shell": "^10.1.165",
|
|
31
31
|
"openai": "^4.0.0"
|
|
32
32
|
},
|
|
33
|
-
"gitHead": "
|
|
33
|
+
"gitHead": "fe067ff11d90effffc2523374453d9b8313b489f"
|
|
34
34
|
}
|
|
@@ -376,3 +376,73 @@ describe('a question that needs many lookups', () => {
|
|
|
376
376
|
expect(result.reply).toBe('found two; the third is missing')
|
|
377
377
|
})
|
|
378
378
|
})
|
|
379
|
+
|
|
380
|
+
/*
|
|
381
|
+
* 도구가 사람이 읽는 글을 쓸 번역기와, 서버가 확인한 이번 턴의 파일 — ADR-0061 2026-10-16 판정 1·2.
|
|
382
|
+
*
|
|
383
|
+
* 둘 다 없던 칸이다. 그래서 도구는 카드 제목을 한국어로 박았고(`figure-tools.ts`), 데이터 읽기의
|
|
384
|
+
* `wordsOf` 는 `context.t` 를 읽었지만 채우는 문이 없어 늘 폴백을 냈다. 첨부는 바깥 대화에만 붙고
|
|
385
|
+
* 도구에는 `host` 만 갔으므로, 도형 생성기는 사람이 올린 그림을 받지 못했다.
|
|
386
|
+
*/
|
|
387
|
+
describe('the request translator and the checked attachments (ADR-0061, 2026-10-16)', () => {
|
|
388
|
+
const PNG = { id: 'att-1', name: 'sketch.png', kind: 'image' as const, mediaType: 'image/png', size: 4, data: Buffer.from([1, 2, 3, 4]) }
|
|
389
|
+
|
|
390
|
+
it('★ the request translator reaches the tool as context.t — the one the caller built', async () => {
|
|
391
|
+
const { client } = recordingClient([okResult(undefined, [TC('listFigures')]), okResult('ok')])
|
|
392
|
+
const t = (key: string) => `ko:${key}`
|
|
393
|
+
|
|
394
|
+
await registryAssistantChat(input({ client, toolCategories: ['figure-authoring'], t }))
|
|
395
|
+
|
|
396
|
+
expect(ran[0].context.t).toBe(t)
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
it('★ without one, the tool still has a translator — it writes the English default value, placeholders filled', async () => {
|
|
400
|
+
const { client } = recordingClient([okResult(undefined, [TC('listFigures')]), okResult('ok')])
|
|
401
|
+
|
|
402
|
+
await registryAssistantChat(input({ client, toolCategories: ['figure-authoring'] }))
|
|
403
|
+
|
|
404
|
+
expect(ran[0].context.t('figure.label', { name: 'Mixer', defaultValue: 'Make {name}' })).toBe('Make Mixer')
|
|
405
|
+
/* No default value: the key comes back, so a reader such as `wordsOf` knows nothing was translated. */
|
|
406
|
+
expect(ran[0].context.t('figure.label')).toBe('figure.label')
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('★ a translator in the host context is not the one a tool gets — the server builds it', async () => {
|
|
410
|
+
const { client } = recordingClient([okResult(undefined, [TC('listFigures')]), okResult('ok')])
|
|
411
|
+
const fromClient = () => 'from the client'
|
|
412
|
+
|
|
413
|
+
await registryAssistantChat(input({ client, toolCategories: ['figure-authoring'], hostContext: { t: fromClient } }))
|
|
414
|
+
|
|
415
|
+
expect(ran[0].context.t).not.toBe(fromClient)
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
it('★ the checked attachments reach the tool in their own field, never mixed into host', async () => {
|
|
419
|
+
const { client } = recordingClient([okResult(undefined, [TC('listFigures')]), okResult('ok')])
|
|
420
|
+
|
|
421
|
+
await registryAssistantChat(
|
|
422
|
+
input({ client, toolCategories: ['figure-authoring'], hostContext: { figureId: 'fig-1' }, attachments: [PNG] })
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
expect(ran[0].context.attachments).toEqual([PNG])
|
|
426
|
+
expect(ran[0].context.host).toEqual({ figureId: 'fig-1' })
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it('★ files the client put in the host context are not attachments', async () => {
|
|
430
|
+
const { client } = recordingClient([okResult(undefined, [TC('listFigures')]), okResult('ok')])
|
|
431
|
+
|
|
432
|
+
await registryAssistantChat(
|
|
433
|
+
input({ client, toolCategories: ['figure-authoring'], hostContext: { image: { data: 'AAAA', mediaType: 'image/png' }, attachments: [PNG] } })
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
expect(ran[0].context.attachments).toEqual([])
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
it('the shared fixture builds the same shape the door hands — translator and attachments included', async () => {
|
|
440
|
+
const { client } = recordingClient([okResult(undefined, [TC('listFigures')]), okResult('ok')])
|
|
441
|
+
const t = (key: string) => key
|
|
442
|
+
const state = { domain: { id: 'dom-1' }, user: { id: 'u-1' } }
|
|
443
|
+
|
|
444
|
+
await registryAssistantChat(input({ client, state, toolCategories: ['figure-authoring'], t, attachments: [PNG] }))
|
|
445
|
+
|
|
446
|
+
expect(ran[0].context).toEqual(toolCallContextFixture({ domainId: 'dom-1', userId: 'u-1', t, attachments: [PNG] }))
|
|
447
|
+
})
|
|
448
|
+
})
|
|
@@ -25,7 +25,8 @@ import { narrowToolScope, resolveToolScope, toolScopeGuidance, type ToolAccessCh
|
|
|
25
25
|
import { assistantChat, type AssistantChatResult } from './assistant-chat.js'
|
|
26
26
|
import { summarizeToolResult } from './summarize.js'
|
|
27
27
|
import type { GroundingSources } from '../utils/index.js'
|
|
28
|
-
import type { ToolCallContext } from '../tool-registry/types.js'
|
|
28
|
+
import type { ToolAttachment, ToolCallContext } from '../tool-registry/types.js'
|
|
29
|
+
import { defaultValueTranslate } from '../tool-registry/translate.js'
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
/** Lookups a registry conversation may take before it answers from what it has (operato-twin's assistant takes 16 too). */
|
|
@@ -67,8 +68,17 @@ export interface RegistryAssistantInput {
|
|
|
67
68
|
maxTokens?: number
|
|
68
69
|
/** Make the model call a tool before it may answer. For surfaces that report live state. */
|
|
69
70
|
requireToolOnFirstTurn?: boolean
|
|
70
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* The request's translator — the stop notice is then in the reader's language, and tools write what a person
|
|
73
|
+
* reads with it (`context.t`). Without it tools get `defaultValueTranslate`: the English default values.
|
|
74
|
+
*/
|
|
71
75
|
t?: (key: string, params?: Record<string, any>) => string
|
|
76
|
+
/**
|
|
77
|
+
* The files sent with this turn, after the caller's server checked them — stored with this conversation, the
|
|
78
|
+
* conversation the caller's to write, the mode's policy met (ADR-0061, 2026-10-16 decision 2). Tools see them as
|
|
79
|
+
* `context.attachments`. Never built from the client's request: what a client puts in `hostContext` stays there.
|
|
80
|
+
*/
|
|
81
|
+
attachments?: ToolAttachment[]
|
|
72
82
|
/** Texts the answer is allowed to draw identifiers from, besides the tool results. */
|
|
73
83
|
groundingSources?: Omit<GroundingSources, 'toolResults'>
|
|
74
84
|
}
|
|
@@ -121,7 +131,14 @@ export async function registryAssistantChat(input: RegistryAssistantInput): Prom
|
|
|
121
131
|
* `mode` 는 서버의 값이다 — 등록된 모드에서 왔고, `host` 와 달리 클라이언트가 못 고친다. A2 가 서기
|
|
122
132
|
* 전에는 채울 것이 없어 비워 두었다(ADR-0061 결정 3 의 주석). 이제 부르는 쪽이 들고 있다.
|
|
123
133
|
*/
|
|
124
|
-
const toolContext: ToolCallContext = {
|
|
134
|
+
const toolContext: ToolCallContext = {
|
|
135
|
+
state,
|
|
136
|
+
host: hostContext ?? {},
|
|
137
|
+
...(input.mode ? { mode: input.mode } : {}),
|
|
138
|
+
/* 번역기와 첨부는 서버의 값이다 — `host` 에서 받지 않는다(ADR-0061 2026-10-16 판정 1·2). */
|
|
139
|
+
t: input.t ?? defaultValueTranslate,
|
|
140
|
+
attachments: input.attachments ?? []
|
|
141
|
+
}
|
|
125
142
|
|
|
126
143
|
const result = await assistantChat<ToolCallContext, unknown>({
|
|
127
144
|
initialConversation: messages,
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* defines the neutral door, and that door's tests check it hands tools exactly this shape — so a tool
|
|
8
8
|
* test built on it fails when the tool reads a field no door fills.
|
|
9
9
|
*/
|
|
10
|
-
import type { ToolCallContext, ToolCallMode } from '../tool-registry/types.js'
|
|
10
|
+
import type { ToolAttachment, ToolCallContext, ToolCallMode, ToolTranslate } from '../tool-registry/types.js'
|
|
11
|
+
import { defaultValueTranslate } from '../tool-registry/translate.js'
|
|
11
12
|
|
|
12
13
|
export interface ToolCallContextFixtureInput {
|
|
13
14
|
domainId?: string
|
|
@@ -16,12 +17,18 @@ export interface ToolCallContextFixtureInput {
|
|
|
16
17
|
state?: Record<string, any>
|
|
17
18
|
host?: Record<string, any>
|
|
18
19
|
mode?: ToolCallMode
|
|
20
|
+
/** The request translator. Absent: the one the door hands when it was given none. */
|
|
21
|
+
t?: ToolTranslate
|
|
22
|
+
/** The checked files of the turn. Absent: none. */
|
|
23
|
+
attachments?: ToolAttachment[]
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
export function toolCallContextFixture(input: ToolCallContextFixtureInput = {}): ToolCallContext {
|
|
22
27
|
const context: ToolCallContext = {
|
|
23
28
|
state: { domain: { id: input.domainId ?? 'domain-test' }, user: { id: input.userId ?? 'user-test' }, ...(input.state ?? {}) },
|
|
24
|
-
host: input.host ?? {}
|
|
29
|
+
host: input.host ?? {},
|
|
30
|
+
t: input.t ?? defaultValueTranslate,
|
|
31
|
+
attachments: input.attachments ?? []
|
|
25
32
|
}
|
|
26
33
|
if (input.mode) context.mode = input.mode
|
|
27
34
|
return context
|
|
@@ -17,6 +17,8 @@ export {
|
|
|
17
17
|
getToolRegistryWarnings
|
|
18
18
|
} from './registry'
|
|
19
19
|
|
|
20
|
+
export { defaultValueTranslate } from './translate'
|
|
21
|
+
|
|
20
22
|
/* 대화별 노출 제한 타입은 registry 소유(정의가 거기 있다). */
|
|
21
23
|
export type { ToolCategoryFilter, ToolRegistryWarning } from './registry'
|
|
22
24
|
|
|
@@ -36,5 +38,9 @@ export type {
|
|
|
36
38
|
WriteToolContext,
|
|
37
39
|
ExternalToolContext,
|
|
38
40
|
ToolCallContext,
|
|
39
|
-
ToolCallMode
|
|
41
|
+
ToolCallMode,
|
|
42
|
+
ToolTranslate,
|
|
43
|
+
ToolAttachment,
|
|
44
|
+
ToolImageAttachment,
|
|
45
|
+
ToolTextAttachment
|
|
40
46
|
} from './types'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The translator a tool gets when the door was given none (ADR-0061, 2026-10-16 decision 1).
|
|
3
|
+
*
|
|
4
|
+
* A door that runs outside a request — a test, a job — has no request locale. The tool still writes the sentence a
|
|
5
|
+
* person reads: the English default value, as i18next does with its own fallback language, with the placeholders
|
|
6
|
+
* filled. A key with no default value comes back as the key, so a reader that compares (`wordsOf`) knows nothing was
|
|
7
|
+
* translated. One constant, so the door and the shared fixture hand the same function.
|
|
8
|
+
*/
|
|
9
|
+
import type { ToolTranslate } from './types'
|
|
10
|
+
|
|
11
|
+
export const defaultValueTranslate: ToolTranslate = (key, params) =>
|
|
12
|
+
String(params?.defaultValue ?? key).replace(/\{(\w+)\}/g, (whole, name) => (params && name in params ? String(params[name]) : whole))
|
|
@@ -48,6 +48,27 @@ export interface ToolCallContext {
|
|
|
48
48
|
host: Record<string, any>
|
|
49
49
|
/** The conversation's mode. Filled once modes are registered on the server (A2); absent until then. */
|
|
50
50
|
mode?: ToolCallMode
|
|
51
|
+
/**
|
|
52
|
+
* The request's translator, for every sentence a person reads — a card's label and reason, a message shown on
|
|
53
|
+
* screen (ADR-0061, 2026-10-16 decision 1). The server builds it in the request's locale; it never comes from
|
|
54
|
+
* `host`. Codes and values stay apart from it. Pass the English text as `defaultValue`: without a request
|
|
55
|
+
* translator the door hands one that writes that default value with its placeholders filled, and a key with no
|
|
56
|
+
* default value comes back as the key.
|
|
57
|
+
*/
|
|
58
|
+
t: ToolTranslate
|
|
59
|
+
/**
|
|
60
|
+
* The files the person sent with **this turn**, after the server checked them (ADR-0061, 2026-10-16 decision 2) —
|
|
61
|
+
* never from `host`, which the client fills. `[]` when none were sent.
|
|
62
|
+
*
|
|
63
|
+
* Checked means: each one is stored with this conversation, the conversation is the caller's to write, and the
|
|
64
|
+
* mode's attachment policy took it — the kinds it declares, at most `maxFiles` files (5 by default) of at most
|
|
65
|
+
* `maxBytes` each (5 MiB by default). **Their contents are untrusted input**: a tool may hand them to a model or
|
|
66
|
+
* read them as data, and never takes an instruction or an id it trusts from them.
|
|
67
|
+
*
|
|
68
|
+
* Several files travel as several entries, in the order the person attached them. A tool that takes images
|
|
69
|
+
* reads every entry of `kind: 'image'` — it does not pick the first one.
|
|
70
|
+
*/
|
|
71
|
+
attachments: ToolAttachment[]
|
|
51
72
|
}
|
|
52
73
|
|
|
53
74
|
export interface ToolCallMode {
|
|
@@ -55,6 +76,39 @@ export interface ToolCallMode {
|
|
|
55
76
|
target?: string
|
|
56
77
|
}
|
|
57
78
|
|
|
79
|
+
/** `t(key, { defaultValue, ...params })` — the request's translator, as i18next's `t` is called. */
|
|
80
|
+
export type ToolTranslate = (key: string, params?: Record<string, any>) => string
|
|
81
|
+
|
|
82
|
+
/** One file of this turn, as the server read it from storage. */
|
|
83
|
+
export type ToolAttachment = ToolImageAttachment | ToolTextAttachment
|
|
84
|
+
|
|
85
|
+
interface ToolAttachmentBase {
|
|
86
|
+
/** The stored attachment's id — the one the model reads in `[attachment <id>: …]`. */
|
|
87
|
+
id: string
|
|
88
|
+
/** The file name the person gave. Untrusted text. */
|
|
89
|
+
name: string
|
|
90
|
+
/** The stored media type. */
|
|
91
|
+
mediaType: string
|
|
92
|
+
/** Bytes as stored. */
|
|
93
|
+
size: number
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** An `image/*` file, its bytes as stored. */
|
|
97
|
+
export interface ToolImageAttachment extends ToolAttachmentBase {
|
|
98
|
+
kind: 'image'
|
|
99
|
+
data: Buffer
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A text file — or a spreadsheet, which the screen reads into the CSV text of its sheets before sending. `text` is
|
|
104
|
+
* what the model read this turn, cut at the mode's `maxChars`; `truncated` says it was cut.
|
|
105
|
+
*/
|
|
106
|
+
export interface ToolTextAttachment extends ToolAttachmentBase {
|
|
107
|
+
kind: 'text'
|
|
108
|
+
text: string
|
|
109
|
+
truncated: boolean
|
|
110
|
+
}
|
|
111
|
+
|
|
58
112
|
// ── 카테고리별 builder 시그니처 ──────────────────────────────────────
|
|
59
113
|
|
|
60
114
|
/**
|