@things-factory/ai-client-base 10.1.213 → 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.213",
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.213",
27
+ "@things-factory/auth-base": "^10.1.215",
28
28
  "@things-factory/env": "^10.1.149",
29
- "@things-factory/setting-base": "^10.1.213",
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": "81f23aa18cb584c7d8185e81cca0d688f509cf8c"
33
+ "gitHead": "7de5e80eb1f4e2e268746aee39f820e7563f454d"
34
34
  }
@@ -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