@things-factory/ai-client-base 10.1.212 → 10.1.215

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/ai-client-base",
3
- "version": "10.1.212",
3
+ "version": "10.1.215",
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.212",
27
+ "@things-factory/auth-base": "^10.1.215",
28
28
  "@things-factory/env": "^10.1.149",
29
- "@things-factory/setting-base": "^10.1.212",
29
+ "@things-factory/setting-base": "^10.1.215",
30
30
  "@things-factory/shell": "^10.1.208",
31
31
  "openai": "^4.0.0"
32
32
  },
33
- "gitHead": "1e1cdb79fca2915580429af451b6cf1dae653a93"
33
+ "gitHead": "7de5e80eb1f4e2e268746aee39f820e7563f454d"
34
34
  }
@@ -101,7 +101,8 @@ export interface RegistryAssistantInput {
101
101
  */
102
102
  onToolCall?: (record: ToolCallRecord) => void
103
103
  /** Set for an unattended run; tools and recorders see it as `context.run`. */
104
- run?: UnattendedRunRef
104
+ run?: UnattendedRunRef /** The stored conversation of this turn; tools see it as `context.conversation`. The caller's value, never the client's. */
105
+ conversation?: { id: string }
105
106
  }
106
107
 
107
108
  export interface RegistryAssistantResult extends AssistantChatResult<unknown> {
@@ -165,7 +166,8 @@ export async function registryAssistantChat(input: RegistryAssistantInput): Prom
165
166
  /* 번역기와 첨부는 서버의 값이다 — `host` 에서 받지 않는다(ADR-0061 2026-10-16 판정 1·2). */
166
167
  t: input.t ?? defaultValueTranslate,
167
168
  attachments: input.attachments ?? [],
168
- ...(input.run ? { run: input.run } : {})
169
+ ...(input.run ? { run: input.run } : {}),
170
+ ...(input.conversation?.id ? { conversation: { id: input.conversation.id } } : {})
169
171
  }
170
172
 
171
173
  const recorded = async (run: ToolCallRun, context: ToolCallContext) => {
@@ -154,6 +154,17 @@ describe('a scope larger than one call carries', () => {
154
154
  expect(JSON.parse(seen[1].messages.at(-1).content[0].content)).toMatchObject({ opened: ['readList150'], alreadyOffered: ['readList3'] })
155
155
  })
156
156
 
157
+ it('★ a wrong name among right ones opens the right ones, and says what the wrong one most likely meant', async () => {
158
+ const categories = registerLists(165)
159
+ const { client, seen } = recordingClient([reply(undefined, [TC(OPEN_TOOLS, { names: ['readList150', 'queryList151'] })]), reply(undefined, [TC(OPEN_TOOLS, { names: ['queryList151'] })]), reply('done')])
160
+ await registryAssistantChat(input({ client, toolCategories: categories, front: { categories: ['list-3'] } }))
161
+
162
+ expect(JSON.parse(seen[1].messages.at(-1).content[0].content)).toMatchObject({ opened: ['readList150'], notOffered: ['queryList151'], suggestions: { queryList151: ['readList151'] } })
163
+ expect(names(seen[1].tools)).toContain('readList150')
164
+ /* Only wrong names: refused, still with what each most likely meant. */
165
+ expect(JSON.parse(seen[2].messages.at(-1).content[0].content)).toMatchObject({ rejected: true, error: 'tool-not-offered', suggestions: { queryList151: ['readList151'] } })
166
+ })
167
+
157
168
  it('★ a tool called before it is opened does not run - the model is told to open it', async () => {
158
169
  const categories = registerLists(165)
159
170
  const { client, seen } = recordingClient([reply(undefined, [TC('readList150')]), reply('done')])
@@ -120,11 +120,32 @@ export function toolOfferState(scope: ToolScope, front: ToolFront = {}) {
120
120
  const offered = new Map<string, ToolSpec>([...plan.offered, ...(opening ? [opening] : [])].map(spec => [spec.name, spec]))
121
121
  const categoryOf = new Map<string, ScopedCategory>(scope.categories.flatMap(category => category.specs.map(spec => [spec.name, category] as [string, ScopedCategory])))
122
122
 
123
+ /*
124
+ * The tools of this scope a wrong name most likely meant - the same name after its verb (`readOfficeExpenditureRequest`
125
+ * for `queryOfficeExpenditureRequest`). Only this scope's: a tool the person may not use is never named.
126
+ */
127
+ const meant = (name: string) => {
128
+ const stem = name.replace(/^[a-z]+/, '')
129
+ return stem ? [...openable.keys(), ...offered.keys()].filter(one => one !== name && one.replace(/^[a-z]+/, '') === stem) : []
130
+ }
131
+
132
+ /*
133
+ * A wrong name among right ones does not hold the right ones back: asked for a card, the model opened the card tool with
134
+ * a read it named wrong, got nothing open, and told the person the tools were not there (2026-10-25). The right ones
135
+ * open; each wrong one is said with what it most likely meant.
136
+ */
123
137
  function open(names: string[]) {
124
- const unknown = names.filter(name => !openable.has(name) && !offered.has(name))
125
- if (unknown.length) {
126
- return { rejected: true, error: 'tool-not-offered', names: unknown, message: `these are not in the list of tools you can open: ${unknown.join(', ')}. Use the exact names from the list.` }
138
+ const unknown = [...new Set(names.filter(name => !openable.has(name) && !offered.has(name)))]
139
+ const suggestions = Object.fromEntries(unknown.map(name => [name, meant(name)]).filter(([, near]) => near.length))
140
+ const notInList = unknown.length
141
+ ? `${unknown.join(', ')} ${unknown.length > 1 ? 'are' : 'is'} not in the list of tools you can open` +
142
+ (Object.keys(suggestions).length ? ` - ${Object.entries(suggestions).map(([name, near]) => `for ${name} use ${(near as string[]).join(' or ')}`).join('; ')}` : '') +
143
+ '. Use the exact names from the list.'
144
+ : ''
145
+ if (unknown.length && unknown.length === new Set(names).size) {
146
+ return { rejected: true, error: 'tool-not-offered', names: unknown, ...(Object.keys(suggestions).length ? { suggestions } : {}), message: `${notInList} Call ${OPEN_TOOLS} again now.` }
127
147
  }
148
+ names = names.filter(name => !unknown.includes(name))
128
149
  const fresh = [...new Set(names)].filter(name => !offered.has(name))
129
150
  const already = [...new Set(names)].filter(name => offered.has(name) && name !== OPEN_TOOLS)
130
151
  const room = MAX_OFFERED_TOOLS - offered.size
@@ -138,13 +159,15 @@ export function toolOfferState(scope: ToolScope, front: ToolFront = {}) {
138
159
  }
139
160
  for (const name of fresh) offered.set(name, openable.get(name)!)
140
161
  const guidance = [...new Set(fresh.map(name => categoryOf.get(name)?.guidance).filter(Boolean))]
162
+ const said = already.length
163
+ ? `${already.join(', ')} ${already.length > 1 ? 'are' : 'is'} already open - call ${already.length > 1 ? 'them' : 'it'} now. ${fresh.length ? 'The others are open from your next step on.' : ''}`.trim()
164
+ : 'these tools are open from your next step on. Call them as the question needs.'
141
165
  return {
142
166
  opened: fresh,
143
167
  ...(already.length ? { alreadyOffered: already } : {}),
168
+ ...(unknown.length ? { notOffered: unknown, ...(Object.keys(suggestions).length ? { suggestions } : {}) } : {}),
144
169
  ...(guidance.length ? { guidance: guidance.join('\n') } : {}),
145
- message: already.length
146
- ? `${already.join(', ')} ${already.length > 1 ? 'are' : 'is'} already open - call ${already.length > 1 ? 'them' : 'it'} now. ${fresh.length ? 'The others are open from your next step on.' : ''}`.trim()
147
- : 'these tools are open from your next step on. Call them as the question needs.'
170
+ message: notInList ? `${said} ${notInList} Open it now and go on.` : said
148
171
  }
149
172
  }
150
173
 
@@ -75,6 +75,12 @@ export interface ToolCallContext {
75
75
  * it; a turn has none. The server's value - never from `host`.
76
76
  */
77
77
  run?: UnattendedRunRef
78
+ /**
79
+ * The stored conversation this turn belongs to - the server's value, never from `host`. A tool that acts on the room
80
+ * itself (inviting someone into it) reads it and asks the room's own rules; absent on a surface with no stored
81
+ * conversation and in an unattended run.
82
+ */
83
+ conversation?: { id: string }
78
84
  }
79
85
 
80
86
  export interface ToolCallMode {