@things-factory/ai-client-base 10.1.244 → 10.1.246

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.244",
3
+ "version": "10.1.246",
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.244",
27
+ "@things-factory/auth-base": "^10.1.246",
28
28
  "@things-factory/env": "^10.1.149",
29
- "@things-factory/setting-base": "^10.1.244",
30
- "@things-factory/shell": "^10.1.234",
29
+ "@things-factory/setting-base": "^10.1.246",
30
+ "@things-factory/shell": "^10.1.246",
31
31
  "openai": "^4.0.0"
32
32
  },
33
- "gitHead": "f80402ed4e682642b9dfb3ce68d8b00d591d44ee"
33
+ "gitHead": "46c0d3c93edf765cc1f13040b1f9d03fd1d9e60b"
34
34
  }
@@ -4,15 +4,22 @@
4
4
  * design/ai/agent-settings.md §4).
5
5
  */
6
6
  const saved: any[] = []
7
+ /* How long a usage row takes to write: 0, or slower than the answer it records. */
8
+ const mockWrite = { ms: 0 }
7
9
 
8
10
  jest.mock('@things-factory/shell', () => ({
9
11
  ...jest.requireActual('@things-factory/shell'),
10
- getRepository: jest.fn(() => ({ save: async (rows: any[]) => saved.push(...rows) }))
12
+ getRepository: jest.fn(() => ({
13
+ save: async (rows: any[]) => {
14
+ if (mockWrite.ms) await new Promise(resolve => setTimeout(resolve, mockWrite.ms))
15
+ saved.push(...rows)
16
+ }
17
+ }))
11
18
  }))
12
19
  jest.mock('./agent-directory', () => ({ readAiAgentDirectory: jest.fn() }))
13
20
 
14
21
  import { readAiAgentDirectory } from './agent-directory'
15
- import { aiClientFor, hourOf, outcomeOf, pickAgent } from './ai-client-for'
22
+ import { aiClientFor, aiUsageRecorded, hourOf, outcomeOf, pickAgent } from './ai-client-for'
16
23
  import { registerAIProvider } from '../factory'
17
24
 
18
25
  const entry = (id: string, extra: any = {}) => ({
@@ -49,6 +56,7 @@ const flush = () => new Promise(resolve => setImmediate(resolve))
49
56
  beforeEach(() => {
50
57
  saved.length = 0
51
58
  replies = []
59
+ mockWrite.ms = 0
52
60
  })
53
61
 
54
62
  describe('which agent answers', () => {
@@ -115,6 +123,16 @@ describe('usage', () => {
115
123
  expect(saved[0].creator).toBeUndefined()
116
124
  })
117
125
 
126
+ /* A script closing its database after its last call (figure-ai-scenarios-agent.cjs) waits for the rows first. */
127
+ it('the rows still being written are waited for: none is left to a database closed after the last answer', async () => {
128
+ mockWrite.ms = 30
129
+ const client = await aiClientFor({ domain: { id: 'plant' } as any })
130
+ await client.chatWithTools!([], [], {})
131
+ expect(saved).toHaveLength(0)
132
+ await aiUsageRecorded()
133
+ expect(saved).toHaveLength(2)
134
+ })
135
+
118
136
  it('the caller\'s own usage listener still hears every request', async () => {
119
137
  const heard: any[] = []
120
138
  const client = await aiClientFor({ domain: { id: 'plant' } as any })
@@ -32,6 +32,17 @@ export interface AgentClient extends AIClient {
32
32
  readonly agent: { id: string; name: string; provider: string; model: string }
33
33
  }
34
34
 
35
+ /* The usage rows still being written: a request's answer does not wait for its row. */
36
+ const recording = new Set<Promise<unknown>>()
37
+
38
+ /**
39
+ * Once every usage row asked for so far is written (or failed and said so). For a process that closes its database
40
+ * after its last call -- a script running scenarios -- so the last rows are not lost to a closed handle.
41
+ */
42
+ export async function aiUsageRecorded(): Promise<void> {
43
+ while (recording.size) await Promise.allSettled([...recording])
44
+ }
45
+
35
46
  export async function aiClientFor(request: AiClientRequest): Promise<AgentClient> {
36
47
  if (_testing) return _testing(request)
37
48
  if (!request.domain) throw new Error('aiClientFor needs the domain the call is made in')
@@ -133,9 +144,11 @@ function meter(client: AIClient, entry: AiAgentEntry, request: AiClientRequest):
133
144
  }))
134
145
  /* Outside the caller's transaction: a rolled-back write still spent the tokens. By name, so the decorated entity
135
146
  stays out of this module's imports. */
136
- getRepository<AiUsage>('AiUsage')
147
+ const written: Promise<unknown> = getRepository<AiUsage>('AiUsage')
137
148
  .save(rows as any)
138
149
  .catch(error => console.error('[ai-client-base] could not record AI usage:', error?.message ?? error))
150
+ .finally(() => recording.delete(written))
151
+ recording.add(written)
139
152
  }
140
153
 
141
154
  const metered: AgentClient = {