@open-mercato/ai-assistant 0.6.1-develop.3291.1.6fad645fd0 → 0.6.1
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +30 -4
- package/dist/frontend/components/AiChatButton.js +3 -2
- package/dist/frontend/components/AiChatButton.js.map +2 -2
- package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.js +364 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js +7 -7
- package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js.map +2 -2
- package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js +182 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js +316 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js +8 -7
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/chat/route.js +43 -20
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/settings/route.js +4 -3
- package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/usage/daily/route.js +111 -0
- package/dist/modules/ai_assistant/api/usage/daily/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js +108 -0
- package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/usage/sessions/route.js +153 -0
- package/dist/modules/ai_assistant/api/usage/sessions/route.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +335 -38
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js +2 -7
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +44 -35
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.js +282 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.js +25 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/cli.js +12 -0
- package/dist/modules/ai_assistant/cli.js.map +2 -2
- package/dist/modules/ai_assistant/components/AiAssistantSettingsPageClient.js.map +1 -1
- package/dist/modules/ai_assistant/data/entities.js +177 -1
- package/dist/modules/ai_assistant/data/entities.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +104 -2
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiTokenUsageRepository.js +168 -0
- package/dist/modules/ai_assistant/data/repositories/AiTokenUsageRepository.js.map +7 -0
- package/dist/modules/ai_assistant/events.js +8 -0
- package/dist/modules/ai_assistant/events.js.map +2 -2
- package/dist/modules/ai_assistant/i18n/de.json +74 -1
- package/dist/modules/ai_assistant/i18n/en.json +74 -1
- package/dist/modules/ai_assistant/i18n/es.json +75 -2
- package/dist/modules/ai_assistant/i18n/pl.json +74 -1
- package/dist/modules/ai_assistant/lib/agent-policy.js.map +2 -2
- package/dist/modules/ai_assistant/lib/agent-runtime.js +588 -23
- package/dist/modules/ai_assistant/lib/agent-runtime.js.map +3 -3
- package/dist/modules/ai_assistant/lib/agent-tools.js +6 -1
- package/dist/modules/ai_assistant/lib/agent-tools.js.map +2 -2
- package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
- package/dist/modules/ai_assistant/lib/model-factory.js +63 -22
- package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
- package/dist/modules/ai_assistant/lib/token-usage-recorder.js +78 -0
- package/dist/modules/ai_assistant/lib/token-usage-recorder.js.map +7 -0
- package/dist/modules/ai_assistant/lib/usage-serialization.js +33 -0
- package/dist/modules/ai_assistant/lib/usage-serialization.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.js +25 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.js +88 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.js.map +7 -0
- package/dist/modules/ai_assistant/setup.js +34 -0
- package/dist/modules/ai_assistant/setup.js.map +2 -2
- package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js +114 -0
- package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js.map +7 -0
- package/generated/entities/ai_agent_runtime_override/index.ts +7 -0
- package/generated/entities/ai_token_usage_daily/index.ts +16 -0
- package/generated/entities/ai_token_usage_event/index.ts +19 -0
- package/generated/entities.ids.generated.ts +2 -0
- package/generated/entity-fields-registry.ts +47 -1
- package/package.json +15 -7
- package/src/frontend/components/AiChatButton.tsx +3 -2
- package/src/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.ts +521 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.ts +8 -8
- package/src/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.ts +231 -0
- package/src/modules/ai_assistant/__tests__/events.test.ts +4 -3
- package/src/modules/ai_assistant/__tests__/settings-page-logic.test.ts +5 -5
- package/src/modules/ai_assistant/__tests__/token-usage-recorder.test.ts +109 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.ts +388 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/__tests__/route.test.ts +5 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/route.ts +8 -7
- package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +102 -5
- package/src/modules/ai_assistant/api/ai/chat/route.ts +55 -18
- package/src/modules/ai_assistant/api/settings/route.ts +5 -3
- package/src/modules/ai_assistant/api/usage/daily/__tests__/route.test.ts +159 -0
- package/src/modules/ai_assistant/api/usage/daily/route.ts +126 -0
- package/src/modules/ai_assistant/api/usage/sessions/[sessionId]/__tests__/route.test.ts +143 -0
- package/src/modules/ai_assistant/api/usage/sessions/[sessionId]/route.ts +130 -0
- package/src/modules/ai_assistant/api/usage/sessions/__tests__/route.test.ts +123 -0
- package/src/modules/ai_assistant/api/usage/sessions/route.ts +184 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +372 -16
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.tsx +1 -4
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +26 -9
- package/src/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.tsx +469 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.ts +23 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/usage/page.tsx +12 -0
- package/src/modules/ai_assistant/cli.ts +18 -0
- package/src/modules/ai_assistant/components/AiAssistantSettingsPageClient.tsx +1 -1
- package/src/modules/ai_assistant/data/entities.ts +237 -0
- package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +135 -3
- package/src/modules/ai_assistant/data/repositories/AiTokenUsageRepository.ts +213 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentRuntimeOverrideRepository.test.ts +223 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiTokenUsageRepository.test.ts +58 -0
- package/src/modules/ai_assistant/events.ts +8 -0
- package/src/modules/ai_assistant/i18n/de.json +74 -1
- package/src/modules/ai_assistant/i18n/en.json +74 -1
- package/src/modules/ai_assistant/i18n/es.json +75 -2
- package/src/modules/ai_assistant/i18n/pl.json +74 -1
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase0.test.ts +439 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase1.test.ts +243 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase2.test.ts +388 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase3.test.ts +359 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-phase4a.test.ts +2 -2
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime.test.ts +2 -1
- package/src/modules/ai_assistant/lib/__tests__/max-steps-budget.integration.test.ts +12 -13
- package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +77 -14
- package/src/modules/ai_assistant/lib/agent-policy.ts +9 -0
- package/src/modules/ai_assistant/lib/agent-runtime.ts +1148 -43
- package/src/modules/ai_assistant/lib/agent-tools.ts +5 -1
- package/src/modules/ai_assistant/lib/ai-agent-definition.ts +289 -2
- package/src/modules/ai_assistant/lib/model-factory.ts +128 -43
- package/src/modules/ai_assistant/lib/token-usage-recorder.ts +122 -0
- package/src/modules/ai_assistant/lib/usage-serialization.ts +29 -0
- package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +791 -0
- package/src/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.ts +25 -0
- package/src/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.ts +89 -0
- package/src/modules/ai_assistant/setup.ts +49 -0
- package/src/modules/ai_assistant/workers/__tests__/ai-token-usage-prune.test.ts +144 -0
- package/src/modules/ai_assistant/workers/ai-token-usage-prune.ts +188 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 2 unit tests for the native SDK callback contract on runAiAgentText /
|
|
3
|
+
* runAiAgentObject.
|
|
4
|
+
*
|
|
5
|
+
* Covers:
|
|
6
|
+
* - generateText callback receives a PreparedAiSdkOptions bag with all loop
|
|
7
|
+
* primitives: stopWhen (array), prepareStep, onStepFinish, onStepStart,
|
|
8
|
+
* onToolCallStart, onToolCallFinish, experimental_repairToolCall, activeTools,
|
|
9
|
+
* toolChoice, abortSignal.
|
|
10
|
+
* - When the callback is absent, streamText is called directly with the same
|
|
11
|
+
* prepared options.
|
|
12
|
+
* - generateObject callback receives PreparedAiSdkObjectOptions (object-mode
|
|
13
|
+
* subset) and its result is forwarded correctly.
|
|
14
|
+
* - abortSignal is always an AbortSignal instance in the prepared bag.
|
|
15
|
+
* - Per-call loop fields (maxSteps, onStepFinish, stopWhen, etc.) are forwarded
|
|
16
|
+
* to the prepared-options bag.
|
|
17
|
+
*
|
|
18
|
+
* Phase 2 of spec 2026-04-28-ai-agents-agentic-loop-controls.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const streamTextMock = jest.fn()
|
|
22
|
+
const generateObjectMock = jest.fn()
|
|
23
|
+
const stepCountIsMock = jest.fn((count: number) => ({ __kind: 'stepCount', count }))
|
|
24
|
+
const hasToolCallMock = jest.fn((name: string) => ({ __kind: 'hasToolCall', name }))
|
|
25
|
+
const convertToModelMessagesMock = jest.fn((messages: unknown) => messages)
|
|
26
|
+
|
|
27
|
+
jest.mock('ai', () => {
|
|
28
|
+
const actual = jest.requireActual('ai')
|
|
29
|
+
return {
|
|
30
|
+
...actual,
|
|
31
|
+
streamText: (...args: unknown[]) => streamTextMock(...args),
|
|
32
|
+
generateObject: (...args: unknown[]) => generateObjectMock(...args),
|
|
33
|
+
stepCountIs: (count: number) => stepCountIsMock(count),
|
|
34
|
+
hasToolCall: (name: string) => hasToolCallMock(name),
|
|
35
|
+
convertToModelMessages: (...args: unknown[]) => convertToModelMessagesMock(...args),
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const createModelMock = jest.fn(
|
|
40
|
+
(options: { modelId: string; apiKey: string }) => ({ id: options.modelId, apiKey: options.apiKey }),
|
|
41
|
+
)
|
|
42
|
+
const resolveApiKeyMock = jest.fn(() => 'test-api-key')
|
|
43
|
+
|
|
44
|
+
jest.mock('@open-mercato/shared/lib/ai/llm-provider-registry', () => ({
|
|
45
|
+
llmProviderRegistry: {
|
|
46
|
+
resolveFirstConfigured: () => ({
|
|
47
|
+
id: 'test-provider',
|
|
48
|
+
defaultModel: 'provider-default-model',
|
|
49
|
+
resolveApiKey: resolveApiKeyMock,
|
|
50
|
+
createModel: createModelMock,
|
|
51
|
+
isConfigured: () => true,
|
|
52
|
+
}),
|
|
53
|
+
get: () => null,
|
|
54
|
+
},
|
|
55
|
+
}))
|
|
56
|
+
|
|
57
|
+
import type { AiAgentDefinition, AiAgentLoopConfig } from '../ai-agent-definition'
|
|
58
|
+
import type { PreparedAiSdkOptions, PreparedAiSdkObjectOptions } from '../agent-runtime'
|
|
59
|
+
import {
|
|
60
|
+
resetAgentRegistryForTests,
|
|
61
|
+
seedAgentRegistryForTests,
|
|
62
|
+
} from '../agent-registry'
|
|
63
|
+
import { toolRegistry } from '../tool-registry'
|
|
64
|
+
import { runAiAgentText, runAiAgentObject } from '../agent-runtime'
|
|
65
|
+
|
|
66
|
+
function makeAgent(
|
|
67
|
+
overrides: Partial<AiAgentDefinition> & Pick<AiAgentDefinition, 'id' | 'moduleId'>,
|
|
68
|
+
): AiAgentDefinition {
|
|
69
|
+
return {
|
|
70
|
+
label: `${overrides.id} label`,
|
|
71
|
+
description: `${overrides.id} description`,
|
|
72
|
+
systemPrompt: 'System prompt.',
|
|
73
|
+
allowedTools: [],
|
|
74
|
+
...overrides,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const baseAuth = {
|
|
79
|
+
tenantId: 'tenant-1',
|
|
80
|
+
organizationId: 'org-1',
|
|
81
|
+
userId: 'user-1',
|
|
82
|
+
features: ['*'],
|
|
83
|
+
isSuperAdmin: true,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const baseMessages = [
|
|
87
|
+
{ role: 'user' as const, id: 'm1', parts: [{ type: 'text' as const, text: 'hi' }] },
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
function fakeStreamResult() {
|
|
91
|
+
return {
|
|
92
|
+
toUIMessageStreamResponse: jest.fn(
|
|
93
|
+
() =>
|
|
94
|
+
new Response('streamed', {
|
|
95
|
+
status: 200,
|
|
96
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
97
|
+
}),
|
|
98
|
+
),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
describe('Phase 2: generateText callback on runAiAgentText', () => {
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
jest.clearAllMocks()
|
|
105
|
+
resetAgentRegistryForTests()
|
|
106
|
+
toolRegistry.clear()
|
|
107
|
+
streamTextMock.mockImplementation(() => fakeStreamResult())
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
afterAll(() => {
|
|
111
|
+
resetAgentRegistryForTests()
|
|
112
|
+
toolRegistry.clear()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('invokes the generateText callback with the full prepared-options bag', async () => {
|
|
116
|
+
seedAgentRegistryForTests([
|
|
117
|
+
makeAgent({
|
|
118
|
+
id: 'mod.agent',
|
|
119
|
+
moduleId: 'mod',
|
|
120
|
+
loop: { maxSteps: 6 },
|
|
121
|
+
}),
|
|
122
|
+
])
|
|
123
|
+
|
|
124
|
+
const capturedOptions: PreparedAiSdkOptions[] = []
|
|
125
|
+
const fakeStream = fakeStreamResult()
|
|
126
|
+
|
|
127
|
+
await runAiAgentText({
|
|
128
|
+
agentId: 'mod.agent',
|
|
129
|
+
messages: baseMessages as never,
|
|
130
|
+
authContext: baseAuth,
|
|
131
|
+
generateText: async (options) => {
|
|
132
|
+
capturedOptions.push(options)
|
|
133
|
+
return fakeStream as never
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
expect(capturedOptions).toHaveLength(1)
|
|
138
|
+
const opts = capturedOptions[0]
|
|
139
|
+
expect(opts.model).toBeDefined()
|
|
140
|
+
expect(opts.tools).toBeDefined()
|
|
141
|
+
expect(opts.system).toBe('System prompt.')
|
|
142
|
+
expect(opts.messages).toBeDefined()
|
|
143
|
+
expect(opts.maxSteps).toBe(6)
|
|
144
|
+
expect(Array.isArray(opts.stopWhen)).toBe(true)
|
|
145
|
+
expect(opts.stopWhen.length).toBeGreaterThanOrEqual(1)
|
|
146
|
+
expect(typeof opts.prepareStep).toBe('function')
|
|
147
|
+
expect(opts.abortSignal).toBeInstanceOf(AbortSignal)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('does NOT call streamText when generateText callback is supplied', async () => {
|
|
151
|
+
seedAgentRegistryForTests([
|
|
152
|
+
makeAgent({ id: 'mod.agent', moduleId: 'mod' }),
|
|
153
|
+
])
|
|
154
|
+
const fakeStream = fakeStreamResult()
|
|
155
|
+
|
|
156
|
+
await runAiAgentText({
|
|
157
|
+
agentId: 'mod.agent',
|
|
158
|
+
messages: baseMessages as never,
|
|
159
|
+
authContext: baseAuth,
|
|
160
|
+
generateText: async () => fakeStream as never,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
expect(streamTextMock).not.toHaveBeenCalled()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('calls streamText with prepared loop options when no callback is supplied', async () => {
|
|
167
|
+
seedAgentRegistryForTests([
|
|
168
|
+
makeAgent({
|
|
169
|
+
id: 'mod.agent',
|
|
170
|
+
moduleId: 'mod',
|
|
171
|
+
loop: { maxSteps: 4 },
|
|
172
|
+
}),
|
|
173
|
+
])
|
|
174
|
+
|
|
175
|
+
await runAiAgentText({
|
|
176
|
+
agentId: 'mod.agent',
|
|
177
|
+
messages: baseMessages as never,
|
|
178
|
+
authContext: baseAuth,
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
expect(streamTextMock).toHaveBeenCalledTimes(1)
|
|
182
|
+
const callArg = streamTextMock.mock.calls[0][0] as {
|
|
183
|
+
stopWhen: unknown[]
|
|
184
|
+
prepareStep: unknown
|
|
185
|
+
abortSignal: unknown
|
|
186
|
+
}
|
|
187
|
+
expect(Array.isArray(callArg.stopWhen)).toBe(true)
|
|
188
|
+
expect(callArg.stopWhen.length).toBeGreaterThanOrEqual(1)
|
|
189
|
+
expect(typeof callArg.prepareStep).toBe('function')
|
|
190
|
+
expect(callArg.abortSignal).toBeInstanceOf(AbortSignal)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('forwards per-call loop overrides into the prepared-options bag', async () => {
|
|
194
|
+
seedAgentRegistryForTests([
|
|
195
|
+
makeAgent({
|
|
196
|
+
id: 'mod.agent',
|
|
197
|
+
moduleId: 'mod',
|
|
198
|
+
loop: { maxSteps: 8 },
|
|
199
|
+
}),
|
|
200
|
+
])
|
|
201
|
+
|
|
202
|
+
const capturedOptions: PreparedAiSdkOptions[] = []
|
|
203
|
+
const fakeStream = fakeStreamResult()
|
|
204
|
+
const callerOnStepFinish = jest.fn()
|
|
205
|
+
|
|
206
|
+
await runAiAgentText({
|
|
207
|
+
agentId: 'mod.agent',
|
|
208
|
+
messages: baseMessages as never,
|
|
209
|
+
authContext: baseAuth,
|
|
210
|
+
loop: { maxSteps: 3, onStepFinish: callerOnStepFinish },
|
|
211
|
+
generateText: async (options) => {
|
|
212
|
+
capturedOptions.push(options)
|
|
213
|
+
return fakeStream as never
|
|
214
|
+
},
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
expect(capturedOptions[0].maxSteps).toBe(3)
|
|
218
|
+
// Phase 4: the trace collector wraps onStepFinish; verify it chains down to
|
|
219
|
+
// the caller's hook by invoking the wired function.
|
|
220
|
+
expect(typeof capturedOptions[0].onStepFinish).toBe('function')
|
|
221
|
+
const fakeEvent = { usage: { inputTokens: 1, outputTokens: 1 }, toolCalls: [] }
|
|
222
|
+
await capturedOptions[0].onStepFinish!(fakeEvent as never)
|
|
223
|
+
expect(callerOnStepFinish).toHaveBeenCalledWith(fakeEvent)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('stopWhen array always ends with stepCountIs(maxSteps)', async () => {
|
|
227
|
+
seedAgentRegistryForTests([
|
|
228
|
+
makeAgent({
|
|
229
|
+
id: 'mod.agent',
|
|
230
|
+
moduleId: 'mod',
|
|
231
|
+
loop: {
|
|
232
|
+
maxSteps: 5,
|
|
233
|
+
stopWhen: { kind: 'hasToolCall', toolName: 'mod.update' },
|
|
234
|
+
},
|
|
235
|
+
}),
|
|
236
|
+
])
|
|
237
|
+
|
|
238
|
+
const capturedOptions: PreparedAiSdkOptions[] = []
|
|
239
|
+
const fakeStream = fakeStreamResult()
|
|
240
|
+
|
|
241
|
+
await runAiAgentText({
|
|
242
|
+
agentId: 'mod.agent',
|
|
243
|
+
messages: baseMessages as never,
|
|
244
|
+
authContext: baseAuth,
|
|
245
|
+
generateText: async (options) => {
|
|
246
|
+
capturedOptions.push(options)
|
|
247
|
+
return fakeStream as never
|
|
248
|
+
},
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
const stopWhen = capturedOptions[0].stopWhen
|
|
252
|
+
expect(stopWhen).toHaveLength(2)
|
|
253
|
+
expect(stopWhen[0]).toEqual({ __kind: 'hasToolCall', name: 'mod__update' })
|
|
254
|
+
expect(stopWhen[1]).toEqual({ __kind: 'stepCount', count: 5 })
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('abortSignal is an AbortSignal in the prepared bag (Phases 0-2 pre-wire)', async () => {
|
|
258
|
+
seedAgentRegistryForTests([
|
|
259
|
+
makeAgent({ id: 'mod.agent', moduleId: 'mod' }),
|
|
260
|
+
])
|
|
261
|
+
|
|
262
|
+
const capturedOptions: PreparedAiSdkOptions[] = []
|
|
263
|
+
const fakeStream = fakeStreamResult()
|
|
264
|
+
|
|
265
|
+
await runAiAgentText({
|
|
266
|
+
agentId: 'mod.agent',
|
|
267
|
+
messages: baseMessages as never,
|
|
268
|
+
authContext: baseAuth,
|
|
269
|
+
generateText: async (options) => {
|
|
270
|
+
capturedOptions.push(options)
|
|
271
|
+
return fakeStream as never
|
|
272
|
+
},
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
expect(capturedOptions[0].abortSignal).toBeInstanceOf(AbortSignal)
|
|
276
|
+
expect(capturedOptions[0].abortSignal?.aborted).toBe(false)
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
describe('Phase 2: generateObject callback on runAiAgentObject', () => {
|
|
281
|
+
let z: typeof import('zod').z
|
|
282
|
+
const objSchema = { schemaName: 'Out', schema: null as unknown }
|
|
283
|
+
|
|
284
|
+
beforeEach(async () => {
|
|
285
|
+
jest.clearAllMocks()
|
|
286
|
+
resetAgentRegistryForTests()
|
|
287
|
+
toolRegistry.clear()
|
|
288
|
+
generateObjectMock.mockResolvedValue({ object: { title: 'ok' } })
|
|
289
|
+
const zod = await import('zod')
|
|
290
|
+
z = zod.z
|
|
291
|
+
objSchema.schema = z.object({ title: z.string() })
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
afterAll(() => {
|
|
295
|
+
resetAgentRegistryForTests()
|
|
296
|
+
toolRegistry.clear()
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
function makeObjectAgent(loop?: AiAgentDefinition['loop']): AiAgentDefinition {
|
|
300
|
+
const { z: zInner } = require('zod')
|
|
301
|
+
return makeAgent({
|
|
302
|
+
id: 'mod.obj_agent',
|
|
303
|
+
moduleId: 'mod',
|
|
304
|
+
executionMode: 'object',
|
|
305
|
+
output: { schemaName: 'Out', schema: zInner.object({ title: zInner.string() }) },
|
|
306
|
+
...(loop !== undefined ? { loop } : {}),
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
it('invokes the generateObject callback with PreparedAiSdkObjectOptions', async () => {
|
|
311
|
+
seedAgentRegistryForTests([makeObjectAgent({ maxSteps: 4 })])
|
|
312
|
+
|
|
313
|
+
const capturedOptions: PreparedAiSdkObjectOptions[] = []
|
|
314
|
+
|
|
315
|
+
await runAiAgentObject({
|
|
316
|
+
agentId: 'mod.obj_agent',
|
|
317
|
+
input: 'Generate something',
|
|
318
|
+
authContext: baseAuth,
|
|
319
|
+
output: {
|
|
320
|
+
schemaName: 'TestOutput',
|
|
321
|
+
schema: objSchema.schema,
|
|
322
|
+
mode: 'generate',
|
|
323
|
+
},
|
|
324
|
+
generateObject: async (options) => {
|
|
325
|
+
capturedOptions.push(options)
|
|
326
|
+
return { object: { title: 'result' } } as never
|
|
327
|
+
},
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
expect(capturedOptions).toHaveLength(1)
|
|
331
|
+
const opts = capturedOptions[0]
|
|
332
|
+
expect(opts.model).toBeDefined()
|
|
333
|
+
expect(opts.system).toBe('System prompt.')
|
|
334
|
+
expect(opts.messages).toBeDefined()
|
|
335
|
+
expect(opts.schemaName).toBe('TestOutput')
|
|
336
|
+
expect(opts.schema).toBeDefined()
|
|
337
|
+
expect(opts.maxSteps).toBe(4)
|
|
338
|
+
expect(opts.abortSignal).toBeInstanceOf(AbortSignal)
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('does NOT call generateObject SDK function when callback is supplied', async () => {
|
|
342
|
+
seedAgentRegistryForTests([makeObjectAgent()])
|
|
343
|
+
|
|
344
|
+
await runAiAgentObject({
|
|
345
|
+
agentId: 'mod.obj_agent',
|
|
346
|
+
input: 'Generate',
|
|
347
|
+
authContext: baseAuth,
|
|
348
|
+
output: { schemaName: 'Out', schema: objSchema.schema },
|
|
349
|
+
generateObject: async () => ({ object: { title: 'ok' } } as never),
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
expect(generateObjectMock).not.toHaveBeenCalled()
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('returns generate result from the generateObject callback', async () => {
|
|
356
|
+
seedAgentRegistryForTests([makeObjectAgent()])
|
|
357
|
+
|
|
358
|
+
const result = await runAiAgentObject({
|
|
359
|
+
agentId: 'mod.obj_agent',
|
|
360
|
+
input: 'Generate',
|
|
361
|
+
authContext: baseAuth,
|
|
362
|
+
output: { schemaName: 'Out', schema: objSchema.schema },
|
|
363
|
+
generateObject: async () =>
|
|
364
|
+
({ object: { title: 'found' }, finishReason: 'stop' } as never),
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
expect(result.mode).toBe('generate')
|
|
368
|
+
expect((result as { object: unknown }).object).toEqual({ title: 'found' })
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it('calls generateObject SDK function when no callback is supplied', async () => {
|
|
372
|
+
seedAgentRegistryForTests([makeObjectAgent()])
|
|
373
|
+
generateObjectMock.mockResolvedValue({ object: { title: 'sdk' } })
|
|
374
|
+
|
|
375
|
+
await runAiAgentObject({
|
|
376
|
+
agentId: 'mod.obj_agent',
|
|
377
|
+
input: 'Generate',
|
|
378
|
+
authContext: baseAuth,
|
|
379
|
+
output: { schemaName: 'Out', schema: objSchema.schema },
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
expect(generateObjectMock).toHaveBeenCalledTimes(1)
|
|
383
|
+
const callArg = generateObjectMock.mock.calls[0][0] as {
|
|
384
|
+
abortSignal: unknown
|
|
385
|
+
}
|
|
386
|
+
expect(callArg.abortSignal).toBeInstanceOf(AbortSignal)
|
|
387
|
+
})
|
|
388
|
+
})
|