illuma-agents 1.0.38 → 1.0.39
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/cjs/agents/AgentContext.cjs +45 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +2 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +98 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/main.cjs +6 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +140 -47
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/schemas/validate.cjs +173 -0
- package/dist/cjs/schemas/validate.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +45 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +2 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +98 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +140 -47
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/schemas/validate.mjs +167 -0
- package/dist/esm/schemas/validate.mjs.map +1 -0
- package/dist/types/agents/AgentContext.d.ts +19 -1
- package/dist/types/common/enum.d.ts +2 -0
- package/dist/types/graphs/Graph.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/messages/cache.d.ts +4 -1
- package/dist/types/schemas/index.d.ts +1 -0
- package/dist/types/schemas/validate.d.ts +36 -0
- package/dist/types/types/graph.d.ts +69 -0
- package/package.json +2 -2
- package/src/agents/AgentContext.test.ts +312 -0
- package/src/agents/AgentContext.ts +56 -0
- package/src/common/enum.ts +2 -0
- package/src/graphs/Graph.ts +150 -0
- package/src/index.ts +3 -0
- package/src/messages/cache.test.ts +51 -6
- package/src/messages/cache.ts +149 -122
- package/src/schemas/index.ts +2 -0
- package/src/schemas/validate.test.ts +358 -0
- package/src/schemas/validate.ts +238 -0
- package/src/specs/cache.simple.test.ts +396 -0
- package/src/types/graph.test.ts +183 -0
- package/src/types/graph.ts +71 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
import { config } from 'dotenv';
|
|
4
|
+
config();
|
|
5
|
+
import { Calculator } from '@/tools/Calculator';
|
|
6
|
+
import {
|
|
7
|
+
AIMessage,
|
|
8
|
+
BaseMessage,
|
|
9
|
+
HumanMessage,
|
|
10
|
+
UsageMetadata,
|
|
11
|
+
} from '@langchain/core/messages';
|
|
12
|
+
import type * as t from '@/types';
|
|
13
|
+
import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
|
|
14
|
+
import { ModelEndHandler, ToolEndHandler } from '@/events';
|
|
15
|
+
import { capitalizeFirstLetter } from './spec.utils';
|
|
16
|
+
import { GraphEvents, Providers } from '@/common';
|
|
17
|
+
import { getLLMConfig } from '@/utils/llmConfig';
|
|
18
|
+
import { getArgs } from '@/scripts/args';
|
|
19
|
+
import { Run } from '@/run';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* These tests verify that prompt caching works correctly across multi-turn
|
|
23
|
+
* conversations and that messages are not mutated in place.
|
|
24
|
+
*/
|
|
25
|
+
describe('Prompt Caching Integration Tests', () => {
|
|
26
|
+
jest.setTimeout(120000);
|
|
27
|
+
|
|
28
|
+
const setupTest = (): {
|
|
29
|
+
collectedUsage: UsageMetadata[];
|
|
30
|
+
contentParts: Array<t.MessageContentComplex | undefined>;
|
|
31
|
+
customHandlers: Record<string | GraphEvents, t.EventHandler>;
|
|
32
|
+
} => {
|
|
33
|
+
const collectedUsage: UsageMetadata[] = [];
|
|
34
|
+
const { contentParts, aggregateContent } = createContentAggregator();
|
|
35
|
+
|
|
36
|
+
const customHandlers: Record<string | GraphEvents, t.EventHandler> = {
|
|
37
|
+
[GraphEvents.TOOL_END]: new ToolEndHandler(),
|
|
38
|
+
[GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(collectedUsage),
|
|
39
|
+
[GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
|
|
40
|
+
[GraphEvents.ON_RUN_STEP_COMPLETED]: {
|
|
41
|
+
handle: (
|
|
42
|
+
event: GraphEvents.ON_RUN_STEP_COMPLETED,
|
|
43
|
+
data: t.StreamEventData
|
|
44
|
+
): void => {
|
|
45
|
+
aggregateContent({
|
|
46
|
+
event,
|
|
47
|
+
data: data as unknown as { result: t.ToolEndEvent },
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
[GraphEvents.ON_RUN_STEP]: {
|
|
52
|
+
handle: (
|
|
53
|
+
event: GraphEvents.ON_RUN_STEP,
|
|
54
|
+
data: t.StreamEventData
|
|
55
|
+
): void => {
|
|
56
|
+
aggregateContent({ event, data: data as t.RunStep });
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
[GraphEvents.ON_RUN_STEP_DELTA]: {
|
|
60
|
+
handle: (
|
|
61
|
+
event: GraphEvents.ON_RUN_STEP_DELTA,
|
|
62
|
+
data: t.StreamEventData
|
|
63
|
+
): void => {
|
|
64
|
+
aggregateContent({ event, data: data as t.RunStepDeltaEvent });
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
[GraphEvents.ON_MESSAGE_DELTA]: {
|
|
68
|
+
handle: (
|
|
69
|
+
event: GraphEvents.ON_MESSAGE_DELTA,
|
|
70
|
+
data: t.StreamEventData
|
|
71
|
+
): void => {
|
|
72
|
+
aggregateContent({ event, data: data as t.MessageDeltaEvent });
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return { collectedUsage, contentParts, customHandlers };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const streamConfig = {
|
|
81
|
+
configurable: { thread_id: 'cache-test-thread' },
|
|
82
|
+
streamMode: 'values',
|
|
83
|
+
version: 'v2' as const,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
describe('Anthropic Prompt Caching', () => {
|
|
87
|
+
const provider = Providers.ANTHROPIC;
|
|
88
|
+
|
|
89
|
+
test(`${capitalizeFirstLetter(provider)}: multi-turn conversation with caching should not corrupt messages`, async () => {
|
|
90
|
+
const { userName, location } = await getArgs();
|
|
91
|
+
const llmConfig = getLLMConfig(provider);
|
|
92
|
+
const { collectedUsage, customHandlers } = setupTest();
|
|
93
|
+
|
|
94
|
+
const run = await Run.create<t.IState>({
|
|
95
|
+
runId: 'cache-test-anthropic',
|
|
96
|
+
graphConfig: {
|
|
97
|
+
type: 'standard',
|
|
98
|
+
llmConfig: { ...llmConfig, promptCache: true } as t.LLMConfig,
|
|
99
|
+
tools: [new Calculator()],
|
|
100
|
+
instructions: 'You are a helpful assistant.',
|
|
101
|
+
additional_instructions: `User: ${userName}, Location: ${location}`,
|
|
102
|
+
},
|
|
103
|
+
returnContent: true,
|
|
104
|
+
customHandlers,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Turn 1
|
|
108
|
+
const turn1Messages: BaseMessage[] = [
|
|
109
|
+
new HumanMessage('Hello, what is 2+2?'),
|
|
110
|
+
];
|
|
111
|
+
const turn1ContentSnapshot = JSON.stringify(turn1Messages[0].content);
|
|
112
|
+
|
|
113
|
+
const turn1Result = await run.processStream(
|
|
114
|
+
{ messages: turn1Messages },
|
|
115
|
+
streamConfig
|
|
116
|
+
);
|
|
117
|
+
expect(turn1Result).toBeDefined();
|
|
118
|
+
|
|
119
|
+
// Verify original message was NOT mutated
|
|
120
|
+
expect(JSON.stringify(turn1Messages[0].content)).toBe(
|
|
121
|
+
turn1ContentSnapshot
|
|
122
|
+
);
|
|
123
|
+
expect((turn1Messages[0] as any).content).not.toContain('cache_control');
|
|
124
|
+
|
|
125
|
+
const turn1RunMessages = run.getRunMessages();
|
|
126
|
+
expect(turn1RunMessages).toBeDefined();
|
|
127
|
+
expect(turn1RunMessages!.length).toBeGreaterThan(0);
|
|
128
|
+
|
|
129
|
+
// Turn 2 - build on conversation
|
|
130
|
+
const turn2Messages: BaseMessage[] = [
|
|
131
|
+
...turn1Messages,
|
|
132
|
+
...turn1RunMessages!,
|
|
133
|
+
new HumanMessage('Now multiply that by 10'),
|
|
134
|
+
];
|
|
135
|
+
const turn2HumanContentSnapshot = JSON.stringify(
|
|
136
|
+
turn2Messages[turn2Messages.length - 1].content
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const run2 = await Run.create<t.IState>({
|
|
140
|
+
runId: 'cache-test-anthropic-2',
|
|
141
|
+
graphConfig: {
|
|
142
|
+
type: 'standard',
|
|
143
|
+
llmConfig: { ...llmConfig, promptCache: true } as t.LLMConfig,
|
|
144
|
+
tools: [new Calculator()],
|
|
145
|
+
instructions: 'You are a helpful assistant.',
|
|
146
|
+
additional_instructions: `User: ${userName}, Location: ${location}`,
|
|
147
|
+
},
|
|
148
|
+
returnContent: true,
|
|
149
|
+
customHandlers,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const turn2Result = await run2.processStream(
|
|
153
|
+
{ messages: turn2Messages },
|
|
154
|
+
streamConfig
|
|
155
|
+
);
|
|
156
|
+
expect(turn2Result).toBeDefined();
|
|
157
|
+
|
|
158
|
+
// Verify messages were NOT mutated
|
|
159
|
+
expect(
|
|
160
|
+
JSON.stringify(turn2Messages[turn2Messages.length - 1].content)
|
|
161
|
+
).toBe(turn2HumanContentSnapshot);
|
|
162
|
+
|
|
163
|
+
// Check that we got cache read tokens (indicating caching worked)
|
|
164
|
+
console.log(`${provider} Usage:`, collectedUsage);
|
|
165
|
+
expect(collectedUsage.length).toBeGreaterThan(0);
|
|
166
|
+
|
|
167
|
+
console.log(
|
|
168
|
+
`${capitalizeFirstLetter(provider)} multi-turn caching test passed - messages not mutated`
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test(`${capitalizeFirstLetter(provider)}: tool calls should work with caching enabled`, async () => {
|
|
173
|
+
const llmConfig = getLLMConfig(provider);
|
|
174
|
+
const { customHandlers } = setupTest();
|
|
175
|
+
|
|
176
|
+
const run = await Run.create<t.IState>({
|
|
177
|
+
runId: 'cache-test-anthropic-tools',
|
|
178
|
+
graphConfig: {
|
|
179
|
+
type: 'standard',
|
|
180
|
+
llmConfig: { ...llmConfig, promptCache: true } as t.LLMConfig,
|
|
181
|
+
tools: [new Calculator()],
|
|
182
|
+
instructions:
|
|
183
|
+
'You are a math assistant. Use the calculator tool for all calculations.',
|
|
184
|
+
},
|
|
185
|
+
returnContent: true,
|
|
186
|
+
customHandlers,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const messages: BaseMessage[] = [
|
|
190
|
+
new HumanMessage('Calculate 123 * 456 using the calculator'),
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
const result = await run.processStream({ messages }, streamConfig);
|
|
194
|
+
expect(result).toBeDefined();
|
|
195
|
+
|
|
196
|
+
const runMessages = run.getRunMessages();
|
|
197
|
+
expect(runMessages).toBeDefined();
|
|
198
|
+
|
|
199
|
+
// Should have used the calculator tool
|
|
200
|
+
const hasToolUse = runMessages?.some(
|
|
201
|
+
(msg) =>
|
|
202
|
+
msg._getType() === 'ai' &&
|
|
203
|
+
((msg as AIMessage).tool_calls?.length ?? 0) > 0
|
|
204
|
+
);
|
|
205
|
+
expect(hasToolUse).toBe(true);
|
|
206
|
+
|
|
207
|
+
console.log(
|
|
208
|
+
`${capitalizeFirstLetter(provider)} tool call with caching test passed`
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('Bedrock Prompt Caching', () => {
|
|
214
|
+
const provider = Providers.BEDROCK;
|
|
215
|
+
|
|
216
|
+
test(`${capitalizeFirstLetter(provider)}: multi-turn conversation with caching should not corrupt messages`, async () => {
|
|
217
|
+
const { userName, location } = await getArgs();
|
|
218
|
+
const llmConfig = getLLMConfig(provider);
|
|
219
|
+
const { collectedUsage, customHandlers } = setupTest();
|
|
220
|
+
|
|
221
|
+
const run = await Run.create<t.IState>({
|
|
222
|
+
runId: 'cache-test-bedrock',
|
|
223
|
+
graphConfig: {
|
|
224
|
+
type: 'standard',
|
|
225
|
+
llmConfig: { ...llmConfig, promptCache: true } as t.LLMConfig,
|
|
226
|
+
tools: [new Calculator()],
|
|
227
|
+
instructions: 'You are a helpful assistant.',
|
|
228
|
+
additional_instructions: `User: ${userName}, Location: ${location}`,
|
|
229
|
+
},
|
|
230
|
+
returnContent: true,
|
|
231
|
+
customHandlers,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Turn 1
|
|
235
|
+
const turn1Messages: BaseMessage[] = [
|
|
236
|
+
new HumanMessage('Hello, what is 5+5?'),
|
|
237
|
+
];
|
|
238
|
+
const turn1ContentSnapshot = JSON.stringify(turn1Messages[0].content);
|
|
239
|
+
|
|
240
|
+
const turn1Result = await run.processStream(
|
|
241
|
+
{ messages: turn1Messages },
|
|
242
|
+
streamConfig
|
|
243
|
+
);
|
|
244
|
+
expect(turn1Result).toBeDefined();
|
|
245
|
+
|
|
246
|
+
// Verify original message was NOT mutated
|
|
247
|
+
expect(JSON.stringify(turn1Messages[0].content)).toBe(
|
|
248
|
+
turn1ContentSnapshot
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const turn1RunMessages = run.getRunMessages();
|
|
252
|
+
expect(turn1RunMessages).toBeDefined();
|
|
253
|
+
expect(turn1RunMessages!.length).toBeGreaterThan(0);
|
|
254
|
+
|
|
255
|
+
// Turn 2
|
|
256
|
+
const turn2Messages: BaseMessage[] = [
|
|
257
|
+
...turn1Messages,
|
|
258
|
+
...turn1RunMessages!,
|
|
259
|
+
new HumanMessage('Multiply that by 3'),
|
|
260
|
+
];
|
|
261
|
+
const turn2HumanContentSnapshot = JSON.stringify(
|
|
262
|
+
turn2Messages[turn2Messages.length - 1].content
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const run2 = await Run.create<t.IState>({
|
|
266
|
+
runId: 'cache-test-bedrock-2',
|
|
267
|
+
graphConfig: {
|
|
268
|
+
type: 'standard',
|
|
269
|
+
llmConfig: { ...llmConfig, promptCache: true } as t.LLMConfig,
|
|
270
|
+
tools: [new Calculator()],
|
|
271
|
+
instructions: 'You are a helpful assistant.',
|
|
272
|
+
additional_instructions: `User: ${userName}, Location: ${location}`,
|
|
273
|
+
},
|
|
274
|
+
returnContent: true,
|
|
275
|
+
customHandlers,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const turn2Result = await run2.processStream(
|
|
279
|
+
{ messages: turn2Messages },
|
|
280
|
+
streamConfig
|
|
281
|
+
);
|
|
282
|
+
expect(turn2Result).toBeDefined();
|
|
283
|
+
|
|
284
|
+
// Verify messages were NOT mutated
|
|
285
|
+
expect(
|
|
286
|
+
JSON.stringify(turn2Messages[turn2Messages.length - 1].content)
|
|
287
|
+
).toBe(turn2HumanContentSnapshot);
|
|
288
|
+
|
|
289
|
+
console.log(`${provider} Usage:`, collectedUsage);
|
|
290
|
+
expect(collectedUsage.length).toBeGreaterThan(0);
|
|
291
|
+
|
|
292
|
+
console.log(
|
|
293
|
+
`${capitalizeFirstLetter(provider)} multi-turn caching test passed - messages not mutated`
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test(`${capitalizeFirstLetter(provider)}: tool calls should work with caching enabled`, async () => {
|
|
298
|
+
const llmConfig = getLLMConfig(provider);
|
|
299
|
+
const { customHandlers } = setupTest();
|
|
300
|
+
|
|
301
|
+
const run = await Run.create<t.IState>({
|
|
302
|
+
runId: 'cache-test-bedrock-tools',
|
|
303
|
+
graphConfig: {
|
|
304
|
+
type: 'standard',
|
|
305
|
+
llmConfig: { ...llmConfig, promptCache: true } as t.LLMConfig,
|
|
306
|
+
tools: [new Calculator()],
|
|
307
|
+
instructions:
|
|
308
|
+
'You are a math assistant. Use the calculator tool for all calculations.',
|
|
309
|
+
},
|
|
310
|
+
returnContent: true,
|
|
311
|
+
customHandlers,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const messages: BaseMessage[] = [
|
|
315
|
+
new HumanMessage('Calculate 789 * 123 using the calculator'),
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
const result = await run.processStream({ messages }, streamConfig);
|
|
319
|
+
expect(result).toBeDefined();
|
|
320
|
+
|
|
321
|
+
const runMessages = run.getRunMessages();
|
|
322
|
+
expect(runMessages).toBeDefined();
|
|
323
|
+
|
|
324
|
+
// Should have used the calculator tool
|
|
325
|
+
const hasToolUse = runMessages?.some(
|
|
326
|
+
(msg) =>
|
|
327
|
+
msg._getType() === 'ai' &&
|
|
328
|
+
((msg as AIMessage).tool_calls?.length ?? 0) > 0
|
|
329
|
+
);
|
|
330
|
+
expect(hasToolUse).toBe(true);
|
|
331
|
+
|
|
332
|
+
console.log(
|
|
333
|
+
`${capitalizeFirstLetter(provider)} tool call with caching test passed`
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('Cross-provider message isolation', () => {
|
|
339
|
+
test('Messages processed by Anthropic should not affect Bedrock processing', async () => {
|
|
340
|
+
const anthropicConfig = getLLMConfig(Providers.ANTHROPIC);
|
|
341
|
+
const bedrockConfig = getLLMConfig(Providers.BEDROCK);
|
|
342
|
+
const { customHandlers: handlers1 } = setupTest();
|
|
343
|
+
const { customHandlers: handlers2 } = setupTest();
|
|
344
|
+
|
|
345
|
+
// Create a shared message array
|
|
346
|
+
const sharedMessages: BaseMessage[] = [
|
|
347
|
+
new HumanMessage('Hello, what is the capital of France?'),
|
|
348
|
+
];
|
|
349
|
+
const originalContent = JSON.stringify(sharedMessages[0].content);
|
|
350
|
+
|
|
351
|
+
// Process with Anthropic first
|
|
352
|
+
const anthropicRun = await Run.create<t.IState>({
|
|
353
|
+
runId: 'cross-provider-anthropic',
|
|
354
|
+
graphConfig: {
|
|
355
|
+
type: 'standard',
|
|
356
|
+
llmConfig: { ...anthropicConfig, promptCache: true } as t.LLMConfig,
|
|
357
|
+
instructions: 'You are a helpful assistant.',
|
|
358
|
+
},
|
|
359
|
+
returnContent: true,
|
|
360
|
+
customHandlers: handlers1,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const anthropicResult = await anthropicRun.processStream(
|
|
364
|
+
{ messages: sharedMessages },
|
|
365
|
+
streamConfig
|
|
366
|
+
);
|
|
367
|
+
expect(anthropicResult).toBeDefined();
|
|
368
|
+
|
|
369
|
+
// Verify message not mutated
|
|
370
|
+
expect(JSON.stringify(sharedMessages[0].content)).toBe(originalContent);
|
|
371
|
+
|
|
372
|
+
// Now process with Bedrock using the SAME messages
|
|
373
|
+
const bedrockRun = await Run.create<t.IState>({
|
|
374
|
+
runId: 'cross-provider-bedrock',
|
|
375
|
+
graphConfig: {
|
|
376
|
+
type: 'standard',
|
|
377
|
+
llmConfig: { ...bedrockConfig, promptCache: true } as t.LLMConfig,
|
|
378
|
+
instructions: 'You are a helpful assistant.',
|
|
379
|
+
},
|
|
380
|
+
returnContent: true,
|
|
381
|
+
customHandlers: handlers2,
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const bedrockResult = await bedrockRun.processStream(
|
|
385
|
+
{ messages: sharedMessages },
|
|
386
|
+
streamConfig
|
|
387
|
+
);
|
|
388
|
+
expect(bedrockResult).toBeDefined();
|
|
389
|
+
|
|
390
|
+
// Verify message STILL not mutated after both providers processed
|
|
391
|
+
expect(JSON.stringify(sharedMessages[0].content)).toBe(originalContent);
|
|
392
|
+
|
|
393
|
+
console.log('Cross-provider message isolation test passed');
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// src/types/graph.test.ts
|
|
2
|
+
import type {
|
|
3
|
+
StructuredOutputConfig,
|
|
4
|
+
StructuredOutputMode,
|
|
5
|
+
AgentInputs,
|
|
6
|
+
BaseGraphState,
|
|
7
|
+
} from './graph';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Type-level tests to ensure StructuredOutputConfig interface is correctly defined.
|
|
11
|
+
* These tests don't run at runtime but will fail at compile time if types are wrong.
|
|
12
|
+
*/
|
|
13
|
+
describe('StructuredOutputConfig type', () => {
|
|
14
|
+
describe('type compatibility', () => {
|
|
15
|
+
it('should accept minimal configuration', () => {
|
|
16
|
+
const config: StructuredOutputConfig = {
|
|
17
|
+
schema: { type: 'object' },
|
|
18
|
+
};
|
|
19
|
+
expect(config.schema).toBeDefined();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should accept full configuration', () => {
|
|
23
|
+
const config: StructuredOutputConfig = {
|
|
24
|
+
schema: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
name: { type: 'string' },
|
|
28
|
+
value: { type: 'number' },
|
|
29
|
+
},
|
|
30
|
+
required: ['name'],
|
|
31
|
+
},
|
|
32
|
+
name: 'TestSchema',
|
|
33
|
+
description: 'A test schema for structured output',
|
|
34
|
+
mode: 'auto',
|
|
35
|
+
strict: true,
|
|
36
|
+
handleErrors: true,
|
|
37
|
+
maxRetries: 3,
|
|
38
|
+
includeRaw: false,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
expect(config.schema).toBeDefined();
|
|
42
|
+
expect(config.name).toBe('TestSchema');
|
|
43
|
+
expect(config.description).toBe('A test schema for structured output');
|
|
44
|
+
expect(config.mode).toBe('auto');
|
|
45
|
+
expect(config.strict).toBe(true);
|
|
46
|
+
expect(config.handleErrors).toBe(true);
|
|
47
|
+
expect(config.maxRetries).toBe(3);
|
|
48
|
+
expect(config.includeRaw).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should accept all valid mode values', () => {
|
|
52
|
+
const modes: StructuredOutputMode[] = ['auto', 'tool', 'provider'];
|
|
53
|
+
|
|
54
|
+
for (const mode of modes) {
|
|
55
|
+
const config: StructuredOutputConfig = {
|
|
56
|
+
schema: { type: 'string' },
|
|
57
|
+
mode,
|
|
58
|
+
};
|
|
59
|
+
expect(config.mode).toBe(mode);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should make all fields except schema optional', () => {
|
|
64
|
+
const minimalConfig: StructuredOutputConfig = {
|
|
65
|
+
schema: { type: 'boolean' },
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// These should all be undefined for minimal config
|
|
69
|
+
expect(minimalConfig.name).toBeUndefined();
|
|
70
|
+
expect(minimalConfig.description).toBeUndefined();
|
|
71
|
+
expect(minimalConfig.mode).toBeUndefined();
|
|
72
|
+
expect(minimalConfig.strict).toBeUndefined();
|
|
73
|
+
expect(minimalConfig.handleErrors).toBeUndefined();
|
|
74
|
+
expect(minimalConfig.maxRetries).toBeUndefined();
|
|
75
|
+
expect(minimalConfig.includeRaw).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('schema property types', () => {
|
|
80
|
+
it('should accept object schema', () => {
|
|
81
|
+
const config: StructuredOutputConfig = {
|
|
82
|
+
schema: {
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: {
|
|
85
|
+
nested: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
value: { type: 'string' },
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
expect(config.schema.type).toBe('object');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should accept array schema', () => {
|
|
98
|
+
const config: StructuredOutputConfig = {
|
|
99
|
+
schema: {
|
|
100
|
+
type: 'array',
|
|
101
|
+
items: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
id: { type: 'number' },
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
expect(config.schema.type).toBe('array');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should accept schema with enum', () => {
|
|
113
|
+
const config: StructuredOutputConfig = {
|
|
114
|
+
schema: {
|
|
115
|
+
type: 'string',
|
|
116
|
+
enum: ['option1', 'option2', 'option3'],
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
expect(config.schema.enum).toEqual(['option1', 'option2', 'option3']);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should accept schema with $ref', () => {
|
|
123
|
+
const config: StructuredOutputConfig = {
|
|
124
|
+
schema: {
|
|
125
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
126
|
+
definitions: {
|
|
127
|
+
Address: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
street: { type: 'string' },
|
|
131
|
+
city: { type: 'string' },
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
type: 'object',
|
|
136
|
+
properties: {
|
|
137
|
+
address: { $ref: '#/definitions/Address' },
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
expect(config.schema.$schema).toBeDefined();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('AgentInputs with structuredOutput', () => {
|
|
147
|
+
it('should accept structuredOutput as optional field', () => {
|
|
148
|
+
const inputsWithout: Partial<AgentInputs> = {
|
|
149
|
+
agentId: 'agent-1',
|
|
150
|
+
provider: 'openai' as AgentInputs['provider'],
|
|
151
|
+
};
|
|
152
|
+
expect(inputsWithout.structuredOutput).toBeUndefined();
|
|
153
|
+
|
|
154
|
+
const inputsWith: Partial<AgentInputs> = {
|
|
155
|
+
agentId: 'agent-2',
|
|
156
|
+
provider: 'anthropic' as AgentInputs['provider'],
|
|
157
|
+
structuredOutput: {
|
|
158
|
+
schema: { type: 'object' },
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
expect(inputsWith.structuredOutput).toBeDefined();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('BaseGraphState with structuredResponse', () => {
|
|
166
|
+
it('should support structuredResponse field', () => {
|
|
167
|
+
const state: Partial<BaseGraphState> = {
|
|
168
|
+
messages: [],
|
|
169
|
+
structuredResponse: { result: 'success', data: [1, 2, 3] },
|
|
170
|
+
};
|
|
171
|
+
expect(state.structuredResponse).toEqual({
|
|
172
|
+
result: 'success',
|
|
173
|
+
data: [1, 2, 3],
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should allow structuredResponse to be undefined', () => {
|
|
178
|
+
const state: Partial<BaseGraphState> = {
|
|
179
|
+
messages: [],
|
|
180
|
+
};
|
|
181
|
+
expect(state.structuredResponse).toBeUndefined();
|
|
182
|
+
});
|
|
183
|
+
});
|
package/src/types/graph.ts
CHANGED
|
@@ -64,6 +64,11 @@ export type SystemCallbacks = {
|
|
|
64
64
|
|
|
65
65
|
export type BaseGraphState = {
|
|
66
66
|
messages: BaseMessage[];
|
|
67
|
+
/**
|
|
68
|
+
* Structured response when using structured output mode.
|
|
69
|
+
* Contains the validated JSON response conforming to the configured schema.
|
|
70
|
+
*/
|
|
71
|
+
structuredResponse?: Record<string, unknown>;
|
|
67
72
|
};
|
|
68
73
|
|
|
69
74
|
export type MultiAgentGraphState = BaseGraphState & {
|
|
@@ -355,6 +360,66 @@ export type MultiAgentGraphInput = StandardGraphInput & {
|
|
|
355
360
|
edges: GraphEdge[];
|
|
356
361
|
};
|
|
357
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Structured output mode determines how the agent returns structured data.
|
|
365
|
+
* - 'tool': Uses tool calling to return structured output (works with all tool-calling models)
|
|
366
|
+
* - 'provider': Uses provider-native structured output (OpenAI, Anthropic, etc.)
|
|
367
|
+
* - 'auto': Automatically selects the best strategy based on model capabilities
|
|
368
|
+
*/
|
|
369
|
+
export type StructuredOutputMode = 'tool' | 'provider' | 'auto';
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Configuration for structured JSON output from agents.
|
|
373
|
+
* When configured, the agent will return a validated JSON response
|
|
374
|
+
* instead of streaming text.
|
|
375
|
+
*/
|
|
376
|
+
export interface StructuredOutputConfig {
|
|
377
|
+
/**
|
|
378
|
+
* JSON Schema defining the output structure.
|
|
379
|
+
* The model will be forced to return data conforming to this schema.
|
|
380
|
+
*/
|
|
381
|
+
schema: Record<string, unknown>;
|
|
382
|
+
/**
|
|
383
|
+
* Name for the structured output format (used in tool mode).
|
|
384
|
+
* @default 'StructuredResponse'
|
|
385
|
+
*/
|
|
386
|
+
name?: string;
|
|
387
|
+
/**
|
|
388
|
+
* Description of what the structured output represents.
|
|
389
|
+
* Helps the model understand the expected format.
|
|
390
|
+
*/
|
|
391
|
+
description?: string;
|
|
392
|
+
/**
|
|
393
|
+
* Output mode strategy.
|
|
394
|
+
* @default 'auto'
|
|
395
|
+
*/
|
|
396
|
+
mode?: StructuredOutputMode;
|
|
397
|
+
/**
|
|
398
|
+
* Enable strict schema validation.
|
|
399
|
+
* When true, the response must exactly match the schema.
|
|
400
|
+
* @default true
|
|
401
|
+
*/
|
|
402
|
+
strict?: boolean;
|
|
403
|
+
/**
|
|
404
|
+
* Error handling configuration.
|
|
405
|
+
* - true: Auto-retry on validation errors (default)
|
|
406
|
+
* - false: Throw error on validation failure
|
|
407
|
+
* - string: Custom error message for retry
|
|
408
|
+
*/
|
|
409
|
+
handleErrors?: boolean | string;
|
|
410
|
+
/**
|
|
411
|
+
* Maximum number of retry attempts on validation failure.
|
|
412
|
+
* @default 2
|
|
413
|
+
*/
|
|
414
|
+
maxRetries?: number;
|
|
415
|
+
/**
|
|
416
|
+
* Include the raw AI message along with structured response.
|
|
417
|
+
* Useful for debugging.
|
|
418
|
+
* @default false
|
|
419
|
+
*/
|
|
420
|
+
includeRaw?: boolean;
|
|
421
|
+
}
|
|
422
|
+
|
|
358
423
|
export interface AgentInputs {
|
|
359
424
|
agentId: string;
|
|
360
425
|
/** Human-readable name for the agent (used in handoff context). Defaults to agentId if not provided. */
|
|
@@ -384,4 +449,10 @@ export interface AgentInputs {
|
|
|
384
449
|
* and can be cached by Bedrock/Anthropic prompt caching.
|
|
385
450
|
*/
|
|
386
451
|
dynamicContext?: string;
|
|
452
|
+
/**
|
|
453
|
+
* Structured output configuration.
|
|
454
|
+
* When set, disables streaming and returns a validated JSON response
|
|
455
|
+
* conforming to the specified schema.
|
|
456
|
+
*/
|
|
457
|
+
structuredOutput?: StructuredOutputConfig;
|
|
387
458
|
}
|