@inferencesh/sdk 0.6.14 → 0.6.19
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/README.md +1 -1
- package/dist/agent/actions.js +10 -0
- package/dist/agent/provider.d.ts +2 -1
- package/dist/agent/provider.js +5 -5
- package/dist/agent/types.d.ts +3 -0
- package/dist/api/apps.d.ts +4 -0
- package/dist/api/apps.js +6 -0
- package/dist/api/index.d.ts +3 -3
- package/dist/http/index.d.ts +3 -3
- package/dist/index.d.ts +9 -9
- package/dist/types.d.ts +131 -19
- package/dist/types.js +26 -9
- package/package.json +8 -5
- package/dist/agent/actions.test.d.ts +0 -1
- package/dist/agent/actions.test.js +0 -820
- package/dist/agent/api.test.d.ts +0 -1
- package/dist/agent/api.test.js +0 -246
- package/dist/agent/reducer.test.d.ts +0 -1
- package/dist/agent/reducer.test.js +0 -131
- package/dist/agent/types.test.d.ts +0 -1
- package/dist/agent/types.test.js +0 -75
- package/dist/api/agents.test.d.ts +0 -1
- package/dist/api/agents.test.js +0 -1014
- package/dist/api/api-keys.test.d.ts +0 -1
- package/dist/api/api-keys.test.js +0 -44
- package/dist/api/apps.test.d.ts +0 -1
- package/dist/api/apps.test.js +0 -135
- package/dist/api/chats.test.d.ts +0 -1
- package/dist/api/chats.test.js +0 -85
- package/dist/api/engines.test.d.ts +0 -1
- package/dist/api/engines.test.js +0 -133
- package/dist/api/files.test.d.ts +0 -1
- package/dist/api/files.test.js +0 -256
- package/dist/api/flow-runs.test.d.ts +0 -1
- package/dist/api/flow-runs.test.js +0 -97
- package/dist/api/flows.test.d.ts +0 -1
- package/dist/api/flows.test.js +0 -118
- package/dist/api/integrations.test.d.ts +0 -1
- package/dist/api/integrations.test.js +0 -109
- package/dist/api/knowledge.test.d.ts +0 -1
- package/dist/api/knowledge.test.js +0 -238
- package/dist/api/mcp-servers.test.d.ts +0 -1
- package/dist/api/mcp-servers.test.js +0 -98
- package/dist/api/projects.test.d.ts +0 -1
- package/dist/api/projects.test.js +0 -61
- package/dist/api/search.test.d.ts +0 -1
- package/dist/api/search.test.js +0 -50
- package/dist/api/secrets.test.d.ts +0 -1
- package/dist/api/secrets.test.js +0 -61
- package/dist/api/sessions.test.d.ts +0 -1
- package/dist/api/sessions.test.js +0 -61
- package/dist/api/tasks.test.d.ts +0 -1
- package/dist/api/tasks.test.js +0 -381
- package/dist/api/teams.test.d.ts +0 -1
- package/dist/api/teams.test.js +0 -139
- package/dist/client.test.d.ts +0 -1
- package/dist/client.test.js +0 -446
- package/dist/http/client.test.d.ts +0 -1
- package/dist/http/client.test.js +0 -435
- package/dist/http/errors.test.d.ts +0 -1
- package/dist/http/errors.test.js +0 -66
- package/dist/http/poll.test.d.ts +0 -1
- package/dist/http/poll.test.js +0 -146
- package/dist/http/streamable.test.d.ts +0 -4
- package/dist/http/streamable.test.js +0 -510
- package/dist/integration.test.d.ts +0 -9
- package/dist/integration.test.js +0 -107
- package/dist/proxy/express.test.d.ts +0 -1
- package/dist/proxy/express.test.js +0 -106
- package/dist/proxy/hono.test.d.ts +0 -1
- package/dist/proxy/hono.test.js +0 -90
- package/dist/proxy/index.test.d.ts +0 -1
- package/dist/proxy/index.test.js +0 -173
- package/dist/proxy/nextjs.test.d.ts +0 -1
- package/dist/proxy/nextjs.test.js +0 -125
- package/dist/proxy/remix.test.d.ts +0 -1
- package/dist/proxy/remix.test.js +0 -66
- package/dist/proxy/svelte.test.d.ts +0 -1
- package/dist/proxy/svelte.test.js +0 -69
- package/dist/sessions.integration.test.d.ts +0 -13
- package/dist/sessions.integration.test.js +0 -310
- package/dist/stream.test.d.ts +0 -1
- package/dist/stream.test.js +0 -363
- package/dist/streamable.integration.test.d.ts +0 -11
- package/dist/streamable.integration.test.js +0 -150
- package/dist/tool-builder.test.d.ts +0 -1
- package/dist/tool-builder.test.js +0 -407
- package/dist/utils.test.d.ts +0 -1
- package/dist/utils.test.js +0 -43
package/dist/api/agents.test.js
DELETED
|
@@ -1,1014 +0,0 @@
|
|
|
1
|
-
import { HttpClient } from '../http/client';
|
|
2
|
-
import { StreamableManager } from '../http/streamable';
|
|
3
|
-
import { PollManager } from '../http/poll';
|
|
4
|
-
import { ChatStatusBusy, ChatStatusIdle, ToolInvocationStatusAwaitingInput, ToolInvocationStatusInProgress, ToolTypeClient, } from '../types';
|
|
5
|
-
import { FilesAPI } from './files';
|
|
6
|
-
import { AgentsAPI } from './agents';
|
|
7
|
-
const mockFetch = jest.fn();
|
|
8
|
-
global.fetch = mockFetch;
|
|
9
|
-
function mockJsonResponse(body) {
|
|
10
|
-
mockFetch.mockResolvedValueOnce({
|
|
11
|
-
ok: true,
|
|
12
|
-
status: 200,
|
|
13
|
-
text: () => Promise.resolve(JSON.stringify(body)),
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
function makeMessage(overrides = {}) {
|
|
17
|
-
return {
|
|
18
|
-
id: 'msg-1',
|
|
19
|
-
chat_id: 'chat-1',
|
|
20
|
-
role: 'assistant',
|
|
21
|
-
content: 'hello',
|
|
22
|
-
...overrides,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
function mockNdjsonStream(chunks) {
|
|
26
|
-
let chunkIndex = 0;
|
|
27
|
-
const mockReader = {
|
|
28
|
-
read: jest.fn().mockImplementation(async () => {
|
|
29
|
-
if (chunkIndex >= chunks.length) {
|
|
30
|
-
return { done: true, value: undefined };
|
|
31
|
-
}
|
|
32
|
-
return { done: false, value: new TextEncoder().encode(chunks[chunkIndex++]) };
|
|
33
|
-
}),
|
|
34
|
-
releaseLock: jest.fn(),
|
|
35
|
-
};
|
|
36
|
-
return {
|
|
37
|
-
ok: true,
|
|
38
|
-
status: 200,
|
|
39
|
-
body: { getReader: () => mockReader },
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
describe('Agent.sendMessage (polling mode)', () => {
|
|
43
|
-
beforeEach(() => {
|
|
44
|
-
jest.clearAllMocks();
|
|
45
|
-
});
|
|
46
|
-
const agent = () => {
|
|
47
|
-
const http = new HttpClient({
|
|
48
|
-
apiKey: 'test-key',
|
|
49
|
-
stream: false,
|
|
50
|
-
pollIntervalMs: 20,
|
|
51
|
-
});
|
|
52
|
-
return new AgentsAPI(http, new FilesAPI(http)).create('my-agent');
|
|
53
|
-
};
|
|
54
|
-
it('should wait until chat is idle when stream is false', async () => {
|
|
55
|
-
const userMessage = makeMessage({ id: 'user-1', role: 'user' });
|
|
56
|
-
const assistantMessage = makeMessage({ id: 'asst-1' });
|
|
57
|
-
mockJsonResponse({
|
|
58
|
-
user_message: userMessage, assistant_message: assistantMessage,
|
|
59
|
-
});
|
|
60
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
61
|
-
mockJsonResponse({
|
|
62
|
-
id: 'chat-1', status: ChatStatusBusy, chat_messages: [],
|
|
63
|
-
});
|
|
64
|
-
mockJsonResponse({ status: ChatStatusIdle });
|
|
65
|
-
mockJsonResponse({
|
|
66
|
-
id: 'chat-1', status: ChatStatusIdle, chat_messages: [],
|
|
67
|
-
});
|
|
68
|
-
const onChat = jest.fn();
|
|
69
|
-
const result = await agent().sendMessage('hello', { stream: false, onChat });
|
|
70
|
-
expect(result.userMessage).toEqual(userMessage);
|
|
71
|
-
expect(result.assistantMessage).toEqual(assistantMessage);
|
|
72
|
-
expect(onChat).toHaveBeenCalledWith(expect.objectContaining({ id: 'chat-1', status: ChatStatusIdle }));
|
|
73
|
-
});
|
|
74
|
-
it('should skip full GET /chats when poll status is unchanged', async () => {
|
|
75
|
-
mockJsonResponse({
|
|
76
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
77
|
-
assistant_message: makeMessage(),
|
|
78
|
-
});
|
|
79
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
80
|
-
mockJsonResponse({
|
|
81
|
-
id: 'chat-1',
|
|
82
|
-
status: ChatStatusBusy,
|
|
83
|
-
chat_messages: [],
|
|
84
|
-
});
|
|
85
|
-
// Same status again — pollUntilIdle should return a stub without fetching full chat
|
|
86
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
87
|
-
mockJsonResponse({ status: ChatStatusIdle });
|
|
88
|
-
mockJsonResponse({
|
|
89
|
-
id: 'chat-1',
|
|
90
|
-
status: ChatStatusIdle,
|
|
91
|
-
chat_messages: [],
|
|
92
|
-
});
|
|
93
|
-
await agent().sendMessage('hello', { stream: false });
|
|
94
|
-
const fullChatGets = mockFetch.mock.calls.filter(([url, init]) => String(url).includes('/chats/chat-1') &&
|
|
95
|
-
!String(url).includes('/status') &&
|
|
96
|
-
!String(url).includes('/stop') &&
|
|
97
|
-
init.method === 'GET');
|
|
98
|
-
expect(fullChatGets).toHaveLength(2);
|
|
99
|
-
});
|
|
100
|
-
it('should dispatch onToolCall for in_progress client tool invocations', async () => {
|
|
101
|
-
const toolInvocation = {
|
|
102
|
-
id: 'tool-inv-progress',
|
|
103
|
-
type: ToolTypeClient,
|
|
104
|
-
status: ToolInvocationStatusInProgress,
|
|
105
|
-
function: { name: 'my_tool', arguments: { x: 2 } },
|
|
106
|
-
};
|
|
107
|
-
const messageWithTool = makeMessage({ tool_invocations: [toolInvocation] });
|
|
108
|
-
mockJsonResponse({
|
|
109
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
110
|
-
assistant_message: makeMessage(),
|
|
111
|
-
});
|
|
112
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
113
|
-
mockJsonResponse({
|
|
114
|
-
id: 'chat-1',
|
|
115
|
-
status: ChatStatusBusy,
|
|
116
|
-
chat_messages: [messageWithTool],
|
|
117
|
-
});
|
|
118
|
-
mockJsonResponse({ status: ChatStatusIdle });
|
|
119
|
-
mockJsonResponse({
|
|
120
|
-
id: 'chat-1',
|
|
121
|
-
status: ChatStatusIdle,
|
|
122
|
-
chat_messages: [messageWithTool],
|
|
123
|
-
});
|
|
124
|
-
const onMessage = jest.fn();
|
|
125
|
-
const onToolCall = jest.fn();
|
|
126
|
-
await agent().sendMessage('run tool', { stream: false, onMessage, onToolCall });
|
|
127
|
-
expect(onToolCall).toHaveBeenCalledTimes(1);
|
|
128
|
-
expect(onToolCall).toHaveBeenCalledWith({
|
|
129
|
-
id: 'tool-inv-progress',
|
|
130
|
-
name: 'my_tool',
|
|
131
|
-
args: { x: 2 },
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
it('should dispatch onToolCall once per client tool invocation', async () => {
|
|
135
|
-
const toolInvocation = {
|
|
136
|
-
id: 'tool-inv-1',
|
|
137
|
-
type: ToolTypeClient,
|
|
138
|
-
status: ToolInvocationStatusAwaitingInput,
|
|
139
|
-
function: { name: 'my_tool', arguments: { x: 1 } },
|
|
140
|
-
};
|
|
141
|
-
const messageWithTool = makeMessage({ tool_invocations: [toolInvocation] });
|
|
142
|
-
mockJsonResponse({
|
|
143
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
144
|
-
assistant_message: makeMessage(),
|
|
145
|
-
});
|
|
146
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
147
|
-
mockJsonResponse({
|
|
148
|
-
id: 'chat-1',
|
|
149
|
-
status: ChatStatusBusy,
|
|
150
|
-
chat_messages: [messageWithTool],
|
|
151
|
-
});
|
|
152
|
-
// Same status again — stub poll should not re-dispatch tool
|
|
153
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
154
|
-
mockJsonResponse({ status: ChatStatusIdle });
|
|
155
|
-
mockJsonResponse({
|
|
156
|
-
id: 'chat-1', status: ChatStatusIdle, chat_messages: [messageWithTool],
|
|
157
|
-
});
|
|
158
|
-
const onMessage = jest.fn();
|
|
159
|
-
const onToolCall = jest.fn();
|
|
160
|
-
await agent().sendMessage('run tool', { stream: false, onMessage, onToolCall });
|
|
161
|
-
expect(onToolCall).toHaveBeenCalledTimes(1);
|
|
162
|
-
expect(onToolCall).toHaveBeenCalledWith({
|
|
163
|
-
id: 'tool-inv-1',
|
|
164
|
-
name: 'my_tool',
|
|
165
|
-
args: { x: 1 },
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
it('should call onMessage for each chat message during polling', async () => {
|
|
169
|
-
const msg1 = makeMessage({ id: 'msg-1', content: 'first' });
|
|
170
|
-
const msg2 = makeMessage({ id: 'msg-2', content: 'second' });
|
|
171
|
-
mockJsonResponse({
|
|
172
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
173
|
-
assistant_message: makeMessage(),
|
|
174
|
-
});
|
|
175
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
176
|
-
mockJsonResponse({
|
|
177
|
-
id: 'chat-1',
|
|
178
|
-
status: ChatStatusBusy,
|
|
179
|
-
chat_messages: [msg1, msg2],
|
|
180
|
-
});
|
|
181
|
-
mockJsonResponse({ status: ChatStatusIdle });
|
|
182
|
-
mockJsonResponse({
|
|
183
|
-
id: 'chat-1',
|
|
184
|
-
status: ChatStatusIdle,
|
|
185
|
-
chat_messages: [msg1, msg2],
|
|
186
|
-
});
|
|
187
|
-
const onMessage = jest.fn();
|
|
188
|
-
await agent().sendMessage('hello', { stream: false, onMessage });
|
|
189
|
-
expect(onMessage).toHaveBeenCalledWith(expect.objectContaining({ id: 'msg-1', content: 'first' }));
|
|
190
|
-
expect(onMessage).toHaveBeenCalledWith(expect.objectContaining({ id: 'msg-2', content: 'second' }));
|
|
191
|
-
});
|
|
192
|
-
it('should return chat output from run() after polling completes', async () => {
|
|
193
|
-
const userMessage = makeMessage({ id: 'user-1', role: 'user' });
|
|
194
|
-
const assistantMessage = makeMessage();
|
|
195
|
-
mockJsonResponse({
|
|
196
|
-
user_message: userMessage, assistant_message: assistantMessage,
|
|
197
|
-
});
|
|
198
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
199
|
-
mockJsonResponse({
|
|
200
|
-
id: 'chat-1', status: ChatStatusBusy,
|
|
201
|
-
});
|
|
202
|
-
mockJsonResponse({ status: ChatStatusIdle });
|
|
203
|
-
mockJsonResponse({
|
|
204
|
-
id: 'chat-1', status: ChatStatusIdle,
|
|
205
|
-
});
|
|
206
|
-
mockJsonResponse({
|
|
207
|
-
id: 'chat-1', status: ChatStatusIdle, output: { answer: 42 },
|
|
208
|
-
});
|
|
209
|
-
const output = await agent().run('compute');
|
|
210
|
-
expect(output).toEqual({ answer: 42 });
|
|
211
|
-
});
|
|
212
|
-
it('should return null from run() when the chat has no finish output', async () => {
|
|
213
|
-
mockJsonResponse({
|
|
214
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
215
|
-
assistant_message: makeMessage(),
|
|
216
|
-
});
|
|
217
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
218
|
-
mockJsonResponse({ id: 'chat-1', status: ChatStatusBusy });
|
|
219
|
-
mockJsonResponse({ status: ChatStatusIdle });
|
|
220
|
-
mockJsonResponse({ id: 'chat-1', status: ChatStatusIdle });
|
|
221
|
-
mockJsonResponse({ id: 'chat-1', status: ChatStatusIdle, output: null });
|
|
222
|
-
const output = await agent().run('no finish tool');
|
|
223
|
-
expect(output).toBeNull();
|
|
224
|
-
});
|
|
225
|
-
it('should warn on repeated poll errors without resolving sendMessage', async () => {
|
|
226
|
-
jest.useFakeTimers();
|
|
227
|
-
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
|
|
228
|
-
mockJsonResponse({
|
|
229
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
230
|
-
assistant_message: makeMessage(),
|
|
231
|
-
});
|
|
232
|
-
mockFetch.mockRejectedValue(new Error('status endpoint down'));
|
|
233
|
-
const sendPromise = agent().sendMessage('hello', { stream: false });
|
|
234
|
-
for (let i = 0; i < 6; i++) {
|
|
235
|
-
await Promise.resolve();
|
|
236
|
-
jest.advanceTimersByTime(20);
|
|
237
|
-
await Promise.resolve();
|
|
238
|
-
}
|
|
239
|
-
expect(warnSpy).toHaveBeenCalledWith('[Agent] Poll error:', expect.any(Error));
|
|
240
|
-
let settled = false;
|
|
241
|
-
sendPromise.then(() => {
|
|
242
|
-
settled = true;
|
|
243
|
-
});
|
|
244
|
-
await Promise.resolve();
|
|
245
|
-
expect(settled).toBe(false);
|
|
246
|
-
warnSpy.mockRestore();
|
|
247
|
-
jest.useRealTimers();
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
describe('Agent.sendMessage (streaming mode)', () => {
|
|
251
|
-
beforeEach(() => {
|
|
252
|
-
jest.clearAllMocks();
|
|
253
|
-
});
|
|
254
|
-
const streamingAgent = () => {
|
|
255
|
-
const http = new HttpClient({ apiKey: 'test-key', stream: true });
|
|
256
|
-
return new AgentsAPI(http, new FilesAPI(http)).create('my-agent');
|
|
257
|
-
};
|
|
258
|
-
it('should wait until chat is idle via typed stream events', async () => {
|
|
259
|
-
const userMessage = makeMessage({ id: 'user-1', role: 'user' });
|
|
260
|
-
const assistantMessage = makeMessage({ id: 'asst-1' });
|
|
261
|
-
mockFetch.mockImplementation((url) => {
|
|
262
|
-
if (url.includes('/agents/run')) {
|
|
263
|
-
return Promise.resolve({
|
|
264
|
-
ok: true,
|
|
265
|
-
status: 200,
|
|
266
|
-
text: () => Promise.resolve(JSON.stringify({
|
|
267
|
-
user_message: userMessage, assistant_message: assistantMessage,
|
|
268
|
-
})),
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
return Promise.resolve(mockNdjsonStream([
|
|
272
|
-
`${JSON.stringify({ event: 'chats', data: { id: 'chat-1', status: ChatStatusBusy } })}\n`,
|
|
273
|
-
`${JSON.stringify({ event: 'chats', data: { id: 'chat-1', status: ChatStatusIdle } })}\n`,
|
|
274
|
-
]));
|
|
275
|
-
});
|
|
276
|
-
const onChat = jest.fn();
|
|
277
|
-
const result = await streamingAgent().sendMessage('hello', { onChat });
|
|
278
|
-
expect(result.userMessage).toEqual(userMessage);
|
|
279
|
-
expect(onChat).toHaveBeenCalledWith(expect.objectContaining({ id: 'chat-1', status: ChatStatusIdle }));
|
|
280
|
-
});
|
|
281
|
-
it('should dispatch onToolCall for in_progress tools from chat_messages stream events', async () => {
|
|
282
|
-
const toolInvocation = {
|
|
283
|
-
id: 'tool-inv-progress',
|
|
284
|
-
type: ToolTypeClient,
|
|
285
|
-
status: ToolInvocationStatusInProgress,
|
|
286
|
-
function: { name: 'my_tool', arguments: { x: 2 } },
|
|
287
|
-
};
|
|
288
|
-
const messageWithTool = makeMessage({ tool_invocations: [toolInvocation] });
|
|
289
|
-
mockFetch.mockImplementation((url) => {
|
|
290
|
-
if (url.includes('/agents/run')) {
|
|
291
|
-
return Promise.resolve({
|
|
292
|
-
ok: true,
|
|
293
|
-
status: 200,
|
|
294
|
-
text: () => Promise.resolve(JSON.stringify({
|
|
295
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
296
|
-
assistant_message: makeMessage(),
|
|
297
|
-
})),
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
return Promise.resolve(mockNdjsonStream([
|
|
301
|
-
`${JSON.stringify({ event: 'chat_messages', data: messageWithTool })}\n`,
|
|
302
|
-
`${JSON.stringify({ event: 'chats', data: { id: 'chat-1', status: ChatStatusIdle } })}\n`,
|
|
303
|
-
]));
|
|
304
|
-
});
|
|
305
|
-
const onToolCall = jest.fn();
|
|
306
|
-
await streamingAgent().sendMessage('run tool', { onToolCall });
|
|
307
|
-
expect(onToolCall).toHaveBeenCalledTimes(1);
|
|
308
|
-
expect(onToolCall).toHaveBeenCalledWith({
|
|
309
|
-
id: 'tool-inv-progress',
|
|
310
|
-
name: 'my_tool',
|
|
311
|
-
args: { x: 2 },
|
|
312
|
-
});
|
|
313
|
-
});
|
|
314
|
-
it('should dispatch onToolCall from chat_messages stream events', async () => {
|
|
315
|
-
const toolInvocation = {
|
|
316
|
-
id: 'tool-inv-1',
|
|
317
|
-
type: ToolTypeClient,
|
|
318
|
-
status: ToolInvocationStatusAwaitingInput,
|
|
319
|
-
function: { name: 'my_tool', arguments: { x: 1 } },
|
|
320
|
-
};
|
|
321
|
-
const messageWithTool = makeMessage({ tool_invocations: [toolInvocation] });
|
|
322
|
-
mockFetch.mockImplementation((url) => {
|
|
323
|
-
if (url.includes('/agents/run')) {
|
|
324
|
-
return Promise.resolve({
|
|
325
|
-
ok: true,
|
|
326
|
-
status: 200,
|
|
327
|
-
text: () => Promise.resolve(JSON.stringify({
|
|
328
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
329
|
-
assistant_message: makeMessage(),
|
|
330
|
-
})),
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
return Promise.resolve(mockNdjsonStream([
|
|
334
|
-
`${JSON.stringify({ event: 'chat_messages', data: messageWithTool })}\n`,
|
|
335
|
-
`${JSON.stringify({ event: 'chats', data: { id: 'chat-1', status: ChatStatusIdle } })}\n`,
|
|
336
|
-
]));
|
|
337
|
-
});
|
|
338
|
-
const onToolCall = jest.fn();
|
|
339
|
-
await streamingAgent().sendMessage('run tool', { onToolCall });
|
|
340
|
-
expect(onToolCall).toHaveBeenCalledTimes(1);
|
|
341
|
-
expect(onToolCall).toHaveBeenCalledWith({
|
|
342
|
-
id: 'tool-inv-1',
|
|
343
|
-
name: 'my_tool',
|
|
344
|
-
args: { x: 1 },
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
it('should return immediately without waiting when stream is true and no callbacks', async () => {
|
|
348
|
-
const userMessage = makeMessage({ id: 'user-1', role: 'user' });
|
|
349
|
-
const assistantMessage = makeMessage({ id: 'asst-1' });
|
|
350
|
-
mockJsonResponse({
|
|
351
|
-
user_message: userMessage,
|
|
352
|
-
assistant_message: assistantMessage,
|
|
353
|
-
});
|
|
354
|
-
const result = await streamingAgent().sendMessage('hello');
|
|
355
|
-
expect(result.userMessage).toEqual(userMessage);
|
|
356
|
-
expect(result.assistantMessage).toEqual(assistantMessage);
|
|
357
|
-
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
358
|
-
expect(mockFetch.mock.calls[0][0]).toContain('/agents/run');
|
|
359
|
-
});
|
|
360
|
-
it('should open the stream before POST when continuing an existing chat', async () => {
|
|
361
|
-
const http = new HttpClient({
|
|
362
|
-
apiKey: 'test-key',
|
|
363
|
-
stream: true,
|
|
364
|
-
pollIntervalMs: 20,
|
|
365
|
-
});
|
|
366
|
-
const agentInstance = new AgentsAPI(http, new FilesAPI(http)).create('my-agent');
|
|
367
|
-
mockJsonResponse({
|
|
368
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
369
|
-
assistant_message: makeMessage(),
|
|
370
|
-
});
|
|
371
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
372
|
-
mockJsonResponse({
|
|
373
|
-
id: 'chat-1', status: ChatStatusBusy, chat_messages: [],
|
|
374
|
-
});
|
|
375
|
-
mockJsonResponse({ status: ChatStatusIdle });
|
|
376
|
-
mockJsonResponse({
|
|
377
|
-
id: 'chat-1', status: ChatStatusIdle, chat_messages: [],
|
|
378
|
-
});
|
|
379
|
-
await agentInstance.sendMessage('first', { stream: false });
|
|
380
|
-
const callOrder = [];
|
|
381
|
-
mockFetch.mockImplementation((url) => {
|
|
382
|
-
callOrder.push(url);
|
|
383
|
-
if (url.includes('/agents/run')) {
|
|
384
|
-
return Promise.resolve({
|
|
385
|
-
ok: true,
|
|
386
|
-
status: 200,
|
|
387
|
-
text: () => Promise.resolve(JSON.stringify({
|
|
388
|
-
user_message: makeMessage({ id: 'user-2', role: 'user' }),
|
|
389
|
-
assistant_message: makeMessage({ id: 'asst-2' }),
|
|
390
|
-
})),
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
return Promise.resolve(mockNdjsonStream([
|
|
394
|
-
`${JSON.stringify({ event: 'chats', data: { id: 'chat-1', status: ChatStatusIdle } })}\n`,
|
|
395
|
-
]));
|
|
396
|
-
});
|
|
397
|
-
await agentInstance.sendMessage('second', { onChat: jest.fn() });
|
|
398
|
-
const streamIndex = callOrder.findIndex((u) => u.includes('/stream'));
|
|
399
|
-
const runIndex = callOrder.findIndex((u) => u.includes('/agents/run'));
|
|
400
|
-
expect(streamIndex).toBeGreaterThanOrEqual(0);
|
|
401
|
-
expect(runIndex).toBeGreaterThanOrEqual(0);
|
|
402
|
-
expect(streamIndex).toBeLessThan(runIndex);
|
|
403
|
-
});
|
|
404
|
-
});
|
|
405
|
-
describe('Agent.sendMessage (file attachments)', () => {
|
|
406
|
-
beforeEach(() => {
|
|
407
|
-
jest.clearAllMocks();
|
|
408
|
-
});
|
|
409
|
-
const agent = () => {
|
|
410
|
-
const http = new HttpClient({
|
|
411
|
-
apiKey: 'test-key',
|
|
412
|
-
stream: false,
|
|
413
|
-
pollIntervalMs: 20,
|
|
414
|
-
});
|
|
415
|
-
return new AgentsAPI(http, new FilesAPI(http)).create('my-agent');
|
|
416
|
-
};
|
|
417
|
-
it('should route image and non-image URIs into images vs files on the run request', async () => {
|
|
418
|
-
const imageFile = {
|
|
419
|
-
id: 'file-img',
|
|
420
|
-
uri: 'inf://files/img',
|
|
421
|
-
filename: 'photo.png',
|
|
422
|
-
content_type: 'image/png',
|
|
423
|
-
};
|
|
424
|
-
const docFile = {
|
|
425
|
-
id: 'file-doc',
|
|
426
|
-
uri: 'inf://files/doc',
|
|
427
|
-
filename: 'notes.pdf',
|
|
428
|
-
content_type: 'application/pdf',
|
|
429
|
-
};
|
|
430
|
-
mockJsonResponse({
|
|
431
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
432
|
-
assistant_message: makeMessage(),
|
|
433
|
-
});
|
|
434
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
435
|
-
mockJsonResponse({
|
|
436
|
-
id: 'chat-1', status: ChatStatusBusy, chat_messages: [],
|
|
437
|
-
});
|
|
438
|
-
mockJsonResponse({ status: ChatStatusIdle });
|
|
439
|
-
mockJsonResponse({
|
|
440
|
-
id: 'chat-1', status: ChatStatusIdle, chat_messages: [],
|
|
441
|
-
});
|
|
442
|
-
await agent().sendMessage('see attachments', {
|
|
443
|
-
stream: false,
|
|
444
|
-
files: [imageFile, docFile],
|
|
445
|
-
});
|
|
446
|
-
const runCall = mockFetch.mock.calls.find(([url]) => String(url).includes('/agents/run'));
|
|
447
|
-
const body = JSON.parse(String(runCall[1].body));
|
|
448
|
-
expect(body.input.images).toEqual(['inf://files/img']);
|
|
449
|
-
expect(body.input.files).toEqual(['inf://files/doc']);
|
|
450
|
-
expect(mockFetch.mock.calls.filter(([url]) => String(url).includes('/files')).length).toBe(0);
|
|
451
|
-
});
|
|
452
|
-
it('should upload Blob attachments before POST /agents/run', async () => {
|
|
453
|
-
const fileRecord = {
|
|
454
|
-
id: 'file-blob',
|
|
455
|
-
uri: 'inf://files/blob-direct',
|
|
456
|
-
upload_url: 'https://upload.example.com/put',
|
|
457
|
-
content_type: 'image/png',
|
|
458
|
-
};
|
|
459
|
-
mockJsonResponse([fileRecord]);
|
|
460
|
-
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
461
|
-
mockJsonResponse({
|
|
462
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
463
|
-
assistant_message: makeMessage(),
|
|
464
|
-
});
|
|
465
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
466
|
-
mockJsonResponse({ id: 'chat-1', status: ChatStatusBusy, chat_messages: [] });
|
|
467
|
-
mockJsonResponse({ status: ChatStatusIdle });
|
|
468
|
-
mockJsonResponse({ id: 'chat-1', status: ChatStatusIdle, chat_messages: [] });
|
|
469
|
-
const blob = new Blob(['png-bytes'], { type: 'image/png' });
|
|
470
|
-
await agent().sendMessage('see image', { stream: false, files: [blob] });
|
|
471
|
-
const fileCreateCall = mockFetch.mock.calls.find(([url]) => String(url).includes('/files') && !String(url).includes('upload.example.com'));
|
|
472
|
-
expect(fileCreateCall).toBeDefined();
|
|
473
|
-
const runCall = mockFetch.mock.calls.find(([url]) => String(url).includes('/agents/run'));
|
|
474
|
-
const body = JSON.parse(String(runCall[1].body));
|
|
475
|
-
expect(body.input.images).toEqual(['inf://files/blob-direct']);
|
|
476
|
-
expect(body.input.files).toBeUndefined();
|
|
477
|
-
});
|
|
478
|
-
});
|
|
479
|
-
describe('Agent.sendMessage (template ref)', () => {
|
|
480
|
-
beforeEach(() => {
|
|
481
|
-
jest.clearAllMocks();
|
|
482
|
-
});
|
|
483
|
-
const templateAgent = (context) => {
|
|
484
|
-
const http = new HttpClient({
|
|
485
|
-
apiKey: 'test-key',
|
|
486
|
-
stream: false,
|
|
487
|
-
pollIntervalMs: 20,
|
|
488
|
-
});
|
|
489
|
-
return new AgentsAPI(http, new FilesAPI(http)).create('inference/my-agent', { context });
|
|
490
|
-
};
|
|
491
|
-
function mockRunAndPoll() {
|
|
492
|
-
mockJsonResponse({
|
|
493
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
494
|
-
assistant_message: makeMessage(),
|
|
495
|
-
});
|
|
496
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
497
|
-
mockJsonResponse({ id: 'chat-1', status: ChatStatusBusy, chat_messages: [] });
|
|
498
|
-
mockJsonResponse({ status: ChatStatusIdle });
|
|
499
|
-
mockJsonResponse({ id: 'chat-1', status: ChatStatusIdle, chat_messages: [] });
|
|
500
|
-
}
|
|
501
|
-
it('should POST agent ref and context to /agents/run', async () => {
|
|
502
|
-
mockRunAndPoll();
|
|
503
|
-
await templateAgent({ tenant: 'acme' }).sendMessage('hello', { stream: false });
|
|
504
|
-
const runCall = mockFetch.mock.calls.find(([url]) => String(url).includes('/agents/run'));
|
|
505
|
-
const body = JSON.parse(String(runCall[1].body));
|
|
506
|
-
expect(body.agent).toBe('inference/my-agent');
|
|
507
|
-
expect(body.agent_config).toBeUndefined();
|
|
508
|
-
expect(body.context).toEqual({ tenant: 'acme' });
|
|
509
|
-
expect(body.chat_id).toBeNull();
|
|
510
|
-
expect(body.input.text).toBe('hello');
|
|
511
|
-
});
|
|
512
|
-
it('should include chat_id on follow-up messages', async () => {
|
|
513
|
-
const agentInstance = templateAgent();
|
|
514
|
-
mockRunAndPoll();
|
|
515
|
-
await agentInstance.sendMessage('first', { stream: false });
|
|
516
|
-
jest.clearAllMocks();
|
|
517
|
-
mockRunAndPoll();
|
|
518
|
-
await agentInstance.sendMessage('second', { stream: false });
|
|
519
|
-
const runCall = mockFetch.mock.calls.find(([url]) => String(url).includes('/agents/run'));
|
|
520
|
-
const body = JSON.parse(String(runCall[1].body));
|
|
521
|
-
expect(body.agent).toBe('inference/my-agent');
|
|
522
|
-
expect(body.chat_id).toBe('chat-1');
|
|
523
|
-
expect(body.input.text).toBe('second');
|
|
524
|
-
});
|
|
525
|
-
});
|
|
526
|
-
describe('Agent.sendMessage (ad-hoc config)', () => {
|
|
527
|
-
beforeEach(() => {
|
|
528
|
-
jest.clearAllMocks();
|
|
529
|
-
});
|
|
530
|
-
const adHocAgent = () => {
|
|
531
|
-
const http = new HttpClient({
|
|
532
|
-
apiKey: 'test-key',
|
|
533
|
-
stream: false,
|
|
534
|
-
pollIntervalMs: 20,
|
|
535
|
-
});
|
|
536
|
-
return new AgentsAPI(http, new FilesAPI(http)).create({
|
|
537
|
-
core_app: { ref: 'openrouter/claude@latest' },
|
|
538
|
-
system_prompt: 'You are helpful',
|
|
539
|
-
name: 'adhoc-bot',
|
|
540
|
-
});
|
|
541
|
-
};
|
|
542
|
-
it('should POST agent_config and agent_name instead of agent template ref', async () => {
|
|
543
|
-
mockJsonResponse({
|
|
544
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
545
|
-
assistant_message: makeMessage(),
|
|
546
|
-
});
|
|
547
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
548
|
-
mockJsonResponse({ id: 'chat-1', status: ChatStatusBusy, chat_messages: [] });
|
|
549
|
-
mockJsonResponse({ status: ChatStatusIdle });
|
|
550
|
-
mockJsonResponse({ id: 'chat-1', status: ChatStatusIdle, chat_messages: [] });
|
|
551
|
-
await adHocAgent().sendMessage('hello', { stream: false });
|
|
552
|
-
const runCall = mockFetch.mock.calls.find(([url]) => String(url).includes('/agents/run'));
|
|
553
|
-
const body = JSON.parse(String(runCall[1].body));
|
|
554
|
-
expect(body.agent).toBeUndefined();
|
|
555
|
-
expect(body.agent_config).toEqual({
|
|
556
|
-
core_app: { ref: 'openrouter/claude@latest' },
|
|
557
|
-
system_prompt: 'You are helpful',
|
|
558
|
-
name: 'adhoc-bot',
|
|
559
|
-
});
|
|
560
|
-
expect(body.agent_name).toBe('adhoc-bot');
|
|
561
|
-
expect(body.input.text).toBe('hello');
|
|
562
|
-
});
|
|
563
|
-
it('should prefer AgentOptions.name over config.name for agent_name', async () => {
|
|
564
|
-
const http = new HttpClient({
|
|
565
|
-
apiKey: 'test-key',
|
|
566
|
-
stream: false,
|
|
567
|
-
pollIntervalMs: 20,
|
|
568
|
-
});
|
|
569
|
-
const namedAgent = new AgentsAPI(http, new FilesAPI(http)).create({
|
|
570
|
-
core_app: { ref: 'openrouter/claude@latest' },
|
|
571
|
-
system_prompt: 'You are helpful',
|
|
572
|
-
name: 'config-name',
|
|
573
|
-
}, { name: 'override-name' });
|
|
574
|
-
mockJsonResponse({
|
|
575
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
576
|
-
assistant_message: makeMessage(),
|
|
577
|
-
});
|
|
578
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
579
|
-
mockJsonResponse({ id: 'chat-1', status: ChatStatusBusy, chat_messages: [] });
|
|
580
|
-
mockJsonResponse({ status: ChatStatusIdle });
|
|
581
|
-
mockJsonResponse({ id: 'chat-1', status: ChatStatusIdle, chat_messages: [] });
|
|
582
|
-
await namedAgent.sendMessage('hello', { stream: false });
|
|
583
|
-
const runCall = mockFetch.mock.calls.find(([url]) => String(url).includes('/agents/run'));
|
|
584
|
-
const body = JSON.parse(String(runCall[1].body));
|
|
585
|
-
expect(body.agent_name).toBe('override-name');
|
|
586
|
-
expect(body.agent_config.name).toBe('config-name');
|
|
587
|
-
});
|
|
588
|
-
});
|
|
589
|
-
describe('Agent lifecycle', () => {
|
|
590
|
-
beforeEach(() => {
|
|
591
|
-
jest.clearAllMocks();
|
|
592
|
-
});
|
|
593
|
-
const agent = () => {
|
|
594
|
-
const http = new HttpClient({
|
|
595
|
-
apiKey: 'test-key',
|
|
596
|
-
stream: false,
|
|
597
|
-
pollIntervalMs: 20,
|
|
598
|
-
});
|
|
599
|
-
return new AgentsAPI(http, new FilesAPI(http)).create('my-agent');
|
|
600
|
-
};
|
|
601
|
-
it('stopChat should no-op when there is no active chat', async () => {
|
|
602
|
-
await agent().stopChat();
|
|
603
|
-
expect(mockFetch).not.toHaveBeenCalled();
|
|
604
|
-
});
|
|
605
|
-
it('stopChat should POST to /chats/{id}/stop when a chat exists', async () => {
|
|
606
|
-
const agentInstance = agent();
|
|
607
|
-
mockJsonResponse({
|
|
608
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
609
|
-
assistant_message: makeMessage(),
|
|
610
|
-
});
|
|
611
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
612
|
-
mockJsonResponse({
|
|
613
|
-
id: 'chat-1', status: ChatStatusBusy, chat_messages: [],
|
|
614
|
-
});
|
|
615
|
-
mockJsonResponse({ status: ChatStatusIdle });
|
|
616
|
-
mockJsonResponse({
|
|
617
|
-
id: 'chat-1', status: ChatStatusIdle, chat_messages: [],
|
|
618
|
-
});
|
|
619
|
-
await agentInstance.sendMessage('hello', { stream: false });
|
|
620
|
-
jest.clearAllMocks();
|
|
621
|
-
mockJsonResponse(null);
|
|
622
|
-
await agentInstance.stopChat();
|
|
623
|
-
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/chats/chat-1/stop'), expect.anything());
|
|
624
|
-
});
|
|
625
|
-
it('disconnect should stop active poll managers', async () => {
|
|
626
|
-
jest.useFakeTimers();
|
|
627
|
-
const stopSpy = jest.spyOn(PollManager.prototype, 'stop');
|
|
628
|
-
const agentInstance = agent();
|
|
629
|
-
mockJsonResponse({
|
|
630
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
631
|
-
assistant_message: makeMessage(),
|
|
632
|
-
});
|
|
633
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
634
|
-
mockJsonResponse({ id: 'chat-1', status: ChatStatusBusy, chat_messages: [] });
|
|
635
|
-
const sendPromise = agentInstance.sendMessage('hello', {
|
|
636
|
-
stream: false,
|
|
637
|
-
pollIntervalMs: 5000,
|
|
638
|
-
});
|
|
639
|
-
await Promise.resolve();
|
|
640
|
-
await jest.advanceTimersByTimeAsync(0);
|
|
641
|
-
agentInstance.disconnect();
|
|
642
|
-
expect(stopSpy).toHaveBeenCalled();
|
|
643
|
-
stopSpy.mockRestore();
|
|
644
|
-
jest.useRealTimers();
|
|
645
|
-
sendPromise.catch(() => undefined);
|
|
646
|
-
});
|
|
647
|
-
it('disconnect should stop active stream managers', async () => {
|
|
648
|
-
const http = new HttpClient({
|
|
649
|
-
apiKey: 'test-key',
|
|
650
|
-
stream: true,
|
|
651
|
-
pollIntervalMs: 20,
|
|
652
|
-
});
|
|
653
|
-
const agentInstance = new AgentsAPI(http, new FilesAPI(http)).create('my-agent');
|
|
654
|
-
const stopSpy = jest.spyOn(StreamableManager.prototype, 'stop');
|
|
655
|
-
mockFetch.mockImplementation((url) => {
|
|
656
|
-
if (url.includes('/agents/run')) {
|
|
657
|
-
return Promise.resolve({
|
|
658
|
-
ok: true,
|
|
659
|
-
status: 200,
|
|
660
|
-
text: () => Promise.resolve(JSON.stringify({
|
|
661
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
662
|
-
assistant_message: makeMessage(),
|
|
663
|
-
})),
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
return Promise.resolve(mockNdjsonStream([
|
|
667
|
-
`${JSON.stringify({ event: 'chats', data: { id: 'chat-1', status: ChatStatusIdle } })}\n`,
|
|
668
|
-
]));
|
|
669
|
-
});
|
|
670
|
-
await agentInstance.sendMessage('hello', { onChat: jest.fn() });
|
|
671
|
-
agentInstance.disconnect();
|
|
672
|
-
expect(stopSpy).toHaveBeenCalled();
|
|
673
|
-
stopSpy.mockRestore();
|
|
674
|
-
});
|
|
675
|
-
it('startStreaming should no-op when there is no active chat', () => {
|
|
676
|
-
const agentInstance = agent();
|
|
677
|
-
agentInstance.startStreaming();
|
|
678
|
-
expect(mockFetch).not.toHaveBeenCalled();
|
|
679
|
-
});
|
|
680
|
-
it('startStreaming should open the chat stream when chatId exists', async () => {
|
|
681
|
-
const http = new HttpClient({
|
|
682
|
-
apiKey: 'test-key',
|
|
683
|
-
stream: true,
|
|
684
|
-
pollIntervalMs: 20,
|
|
685
|
-
});
|
|
686
|
-
const agentInstance = new AgentsAPI(http, new FilesAPI(http)).create('my-agent');
|
|
687
|
-
mockJsonResponse({
|
|
688
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
689
|
-
assistant_message: makeMessage(),
|
|
690
|
-
});
|
|
691
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
692
|
-
mockJsonResponse({
|
|
693
|
-
id: 'chat-1', status: ChatStatusBusy, chat_messages: [],
|
|
694
|
-
});
|
|
695
|
-
mockJsonResponse({ status: ChatStatusIdle });
|
|
696
|
-
mockJsonResponse({
|
|
697
|
-
id: 'chat-1', status: ChatStatusIdle, chat_messages: [],
|
|
698
|
-
});
|
|
699
|
-
await agentInstance.sendMessage('hello', { stream: false });
|
|
700
|
-
jest.clearAllMocks();
|
|
701
|
-
mockFetch.mockResolvedValue(mockNdjsonStream([
|
|
702
|
-
`${JSON.stringify({ event: 'chats', data: { id: 'chat-1', status: ChatStatusIdle } })}\n`,
|
|
703
|
-
]));
|
|
704
|
-
agentInstance.startStreaming({ onChat: jest.fn() });
|
|
705
|
-
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/chats/chat-1/stream'), expect.anything());
|
|
706
|
-
});
|
|
707
|
-
it('reset should clear chat state so stopChat is a no-op', async () => {
|
|
708
|
-
const agentInstance = agent();
|
|
709
|
-
mockJsonResponse({
|
|
710
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
711
|
-
assistant_message: makeMessage(),
|
|
712
|
-
});
|
|
713
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
714
|
-
mockJsonResponse({
|
|
715
|
-
id: 'chat-1', status: ChatStatusBusy, chat_messages: [],
|
|
716
|
-
});
|
|
717
|
-
mockJsonResponse({ status: ChatStatusIdle });
|
|
718
|
-
mockJsonResponse({
|
|
719
|
-
id: 'chat-1', status: ChatStatusIdle, chat_messages: [],
|
|
720
|
-
});
|
|
721
|
-
await agentInstance.sendMessage('hello', { stream: false });
|
|
722
|
-
agentInstance.reset();
|
|
723
|
-
jest.clearAllMocks();
|
|
724
|
-
await agentInstance.stopChat();
|
|
725
|
-
expect(mockFetch).not.toHaveBeenCalled();
|
|
726
|
-
});
|
|
727
|
-
it('reset should allow onToolCall to fire again for the same invocation id', async () => {
|
|
728
|
-
const toolInvocation = {
|
|
729
|
-
id: 'tool-inv-reset',
|
|
730
|
-
type: ToolTypeClient,
|
|
731
|
-
status: ToolInvocationStatusAwaitingInput,
|
|
732
|
-
function: { name: 'my_tool', arguments: { x: 1 } },
|
|
733
|
-
};
|
|
734
|
-
const messageWithTool = makeMessage({ tool_invocations: [toolInvocation] });
|
|
735
|
-
const agentInstance = agent();
|
|
736
|
-
const onMessage = jest.fn();
|
|
737
|
-
const onToolCall = jest.fn();
|
|
738
|
-
mockJsonResponse({
|
|
739
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
740
|
-
assistant_message: makeMessage(),
|
|
741
|
-
});
|
|
742
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
743
|
-
mockJsonResponse({
|
|
744
|
-
id: 'chat-1',
|
|
745
|
-
status: ChatStatusBusy,
|
|
746
|
-
chat_messages: [messageWithTool],
|
|
747
|
-
});
|
|
748
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
749
|
-
mockJsonResponse({ status: ChatStatusIdle });
|
|
750
|
-
mockJsonResponse({
|
|
751
|
-
id: 'chat-1',
|
|
752
|
-
status: ChatStatusIdle,
|
|
753
|
-
chat_messages: [messageWithTool],
|
|
754
|
-
});
|
|
755
|
-
await agentInstance.sendMessage('run tool', { stream: false, onMessage, onToolCall });
|
|
756
|
-
expect(onToolCall).toHaveBeenCalledTimes(1);
|
|
757
|
-
agentInstance.reset();
|
|
758
|
-
jest.clearAllMocks();
|
|
759
|
-
onToolCall.mockClear();
|
|
760
|
-
mockJsonResponse({
|
|
761
|
-
user_message: makeMessage({ id: 'user-2', role: 'user' }),
|
|
762
|
-
assistant_message: makeMessage({ id: 'asst-2' }),
|
|
763
|
-
});
|
|
764
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
765
|
-
mockJsonResponse({
|
|
766
|
-
id: 'chat-1',
|
|
767
|
-
status: ChatStatusBusy,
|
|
768
|
-
chat_messages: [messageWithTool],
|
|
769
|
-
});
|
|
770
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
771
|
-
mockJsonResponse({ status: ChatStatusIdle });
|
|
772
|
-
mockJsonResponse({
|
|
773
|
-
id: 'chat-1',
|
|
774
|
-
status: ChatStatusIdle,
|
|
775
|
-
chat_messages: [messageWithTool],
|
|
776
|
-
});
|
|
777
|
-
await agentInstance.sendMessage('run tool again', { stream: false, onMessage, onToolCall });
|
|
778
|
-
expect(onToolCall).toHaveBeenCalledTimes(1);
|
|
779
|
-
expect(onToolCall).toHaveBeenCalledWith({
|
|
780
|
-
id: 'tool-inv-reset',
|
|
781
|
-
name: 'my_tool',
|
|
782
|
-
args: { x: 1 },
|
|
783
|
-
});
|
|
784
|
-
});
|
|
785
|
-
});
|
|
786
|
-
describe('Agent.getChat', () => {
|
|
787
|
-
beforeEach(() => {
|
|
788
|
-
jest.clearAllMocks();
|
|
789
|
-
});
|
|
790
|
-
const agent = () => {
|
|
791
|
-
const http = new HttpClient({ apiKey: 'test-key' });
|
|
792
|
-
return new AgentsAPI(http, new FilesAPI(http)).create('my-agent');
|
|
793
|
-
};
|
|
794
|
-
it('should return null without a chat id and no active chat', async () => {
|
|
795
|
-
const result = await agent().getChat();
|
|
796
|
-
expect(result).toBeNull();
|
|
797
|
-
expect(mockFetch).not.toHaveBeenCalled();
|
|
798
|
-
});
|
|
799
|
-
it('should GET /chats/{id} when chatId is provided', async () => {
|
|
800
|
-
const chat = { id: 'chat-42', status: 'idle', chat_messages: [] };
|
|
801
|
-
mockJsonResponse(chat);
|
|
802
|
-
const result = await agent().getChat('chat-42');
|
|
803
|
-
expect(result).toEqual(chat);
|
|
804
|
-
const [url, init] = mockFetch.mock.calls[0];
|
|
805
|
-
expect(url).toContain('/chats/chat-42');
|
|
806
|
-
expect(init.method).toBe('GET');
|
|
807
|
-
});
|
|
808
|
-
it('should expose currentChatId after sendMessage establishes a chat', async () => {
|
|
809
|
-
const agentInstance = agent();
|
|
810
|
-
mockJsonResponse({
|
|
811
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
812
|
-
assistant_message: makeMessage(),
|
|
813
|
-
});
|
|
814
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
815
|
-
mockJsonResponse({ id: 'chat-1', status: ChatStatusBusy, chat_messages: [] });
|
|
816
|
-
mockJsonResponse({ status: ChatStatusIdle });
|
|
817
|
-
mockJsonResponse({ id: 'chat-1', status: ChatStatusIdle, chat_messages: [] });
|
|
818
|
-
expect(agentInstance.currentChatId).toBeNull();
|
|
819
|
-
await agentInstance.sendMessage('hello', { stream: false });
|
|
820
|
-
expect(agentInstance.currentChatId).toBe('chat-1');
|
|
821
|
-
});
|
|
822
|
-
it('should GET /chats/{id} with established chat id when chatId is omitted', async () => {
|
|
823
|
-
const agentInstance = agent();
|
|
824
|
-
mockJsonResponse({
|
|
825
|
-
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
826
|
-
assistant_message: makeMessage(),
|
|
827
|
-
});
|
|
828
|
-
mockJsonResponse({ status: ChatStatusBusy });
|
|
829
|
-
mockJsonResponse({ id: 'chat-1', status: ChatStatusBusy, chat_messages: [] });
|
|
830
|
-
mockJsonResponse({ status: ChatStatusIdle });
|
|
831
|
-
mockJsonResponse({ id: 'chat-1', status: ChatStatusIdle, chat_messages: [] });
|
|
832
|
-
await agentInstance.sendMessage('hello', { stream: false });
|
|
833
|
-
jest.clearAllMocks();
|
|
834
|
-
const chat = { id: 'chat-1', status: 'idle', chat_messages: [] };
|
|
835
|
-
mockJsonResponse(chat);
|
|
836
|
-
const result = await agentInstance.getChat();
|
|
837
|
-
expect(result).toEqual(chat);
|
|
838
|
-
const [url, init] = mockFetch.mock.calls[0];
|
|
839
|
-
expect(url).toContain('/chats/chat-1');
|
|
840
|
-
expect(init.method).toBe('GET');
|
|
841
|
-
});
|
|
842
|
-
});
|
|
843
|
-
describe('Agent.submitToolResult', () => {
|
|
844
|
-
beforeEach(() => {
|
|
845
|
-
jest.clearAllMocks();
|
|
846
|
-
});
|
|
847
|
-
it('should POST plain string results to /tools/{invocationId}', async () => {
|
|
848
|
-
const http = new HttpClient({ apiKey: 'test-key' });
|
|
849
|
-
const agentInstance = new AgentsAPI(http, new FilesAPI(http)).create('my-agent');
|
|
850
|
-
mockJsonResponse(null);
|
|
851
|
-
await agentInstance.submitToolResult('inv-plain', 'done');
|
|
852
|
-
const [url, init] = mockFetch.mock.calls[0];
|
|
853
|
-
expect(url).toContain('/tools/inv-plain');
|
|
854
|
-
expect(init.method).toBe('POST');
|
|
855
|
-
expect(JSON.parse(String(init.body))).toEqual({ result: 'done' });
|
|
856
|
-
});
|
|
857
|
-
it('should JSON-stringify structured action results', async () => {
|
|
858
|
-
const http = new HttpClient({ apiKey: 'test-key' });
|
|
859
|
-
const agentInstance = new AgentsAPI(http, new FilesAPI(http)).create('my-agent');
|
|
860
|
-
mockJsonResponse(null);
|
|
861
|
-
const payload = {
|
|
862
|
-
action: { type: 'form_submit', payload: { field: 'value' } },
|
|
863
|
-
form_data: { field: 'value' },
|
|
864
|
-
};
|
|
865
|
-
await agentInstance.submitToolResult('inv-99', payload);
|
|
866
|
-
const [, init] = mockFetch.mock.calls[0];
|
|
867
|
-
const body = JSON.parse(String(init.body));
|
|
868
|
-
expect(body.result).toBe(JSON.stringify(payload));
|
|
869
|
-
});
|
|
870
|
-
});
|
|
871
|
-
describe('AgentsAPI.submitToolResult', () => {
|
|
872
|
-
beforeEach(() => {
|
|
873
|
-
jest.clearAllMocks();
|
|
874
|
-
});
|
|
875
|
-
const api = () => {
|
|
876
|
-
const http = new HttpClient({ apiKey: 'test-key' });
|
|
877
|
-
return new AgentsAPI(http, new FilesAPI(http));
|
|
878
|
-
};
|
|
879
|
-
it('should POST plain string results to /tools/{invocationId}', async () => {
|
|
880
|
-
mockJsonResponse(null);
|
|
881
|
-
await api().submitToolResult('inv-plain', 'done');
|
|
882
|
-
const [url, init] = mockFetch.mock.calls[0];
|
|
883
|
-
expect(url).toContain('/tools/inv-plain');
|
|
884
|
-
expect(init.method).toBe('POST');
|
|
885
|
-
expect(JSON.parse(String(init.body))).toEqual({ result: 'done' });
|
|
886
|
-
});
|
|
887
|
-
});
|
|
888
|
-
describe('AgentsAPI (template CRUD)', () => {
|
|
889
|
-
beforeEach(() => {
|
|
890
|
-
jest.clearAllMocks();
|
|
891
|
-
});
|
|
892
|
-
const api = () => {
|
|
893
|
-
const http = new HttpClient({ apiKey: 'test-key' });
|
|
894
|
-
return new AgentsAPI(http, new FilesAPI(http));
|
|
895
|
-
};
|
|
896
|
-
it('should GET /agents/internal-tools for getInternalTools()', async () => {
|
|
897
|
-
const tools = [{ name: 'search', description: 'Search the web' }];
|
|
898
|
-
mockJsonResponse(tools);
|
|
899
|
-
const result = await api().getInternalTools();
|
|
900
|
-
expect(result).toEqual(tools);
|
|
901
|
-
const [url, init] = mockFetch.mock.calls[0];
|
|
902
|
-
expect(url).toContain('/agents/internal-tools');
|
|
903
|
-
expect(init.method).toBe('GET');
|
|
904
|
-
});
|
|
905
|
-
it('should GET /agents/{id}/card for getA2ACard()', async () => {
|
|
906
|
-
const card = {
|
|
907
|
-
name: 'support-bot',
|
|
908
|
-
description: 'Customer support agent',
|
|
909
|
-
url: 'https://api.example.com/agents/support-bot',
|
|
910
|
-
version: '1.0.0',
|
|
911
|
-
};
|
|
912
|
-
mockJsonResponse(card);
|
|
913
|
-
const result = await api().getA2ACard('agent-1');
|
|
914
|
-
expect(result).toEqual(card);
|
|
915
|
-
const [url, init] = mockFetch.mock.calls[0];
|
|
916
|
-
expect(url).toContain('/agents/agent-1/card');
|
|
917
|
-
expect(init.method).toBe('GET');
|
|
918
|
-
});
|
|
919
|
-
it('should POST team_id for transferOwnership()', async () => {
|
|
920
|
-
const agent = { id: 'agent-1' };
|
|
921
|
-
mockJsonResponse(agent);
|
|
922
|
-
await api().transferOwnership('agent-1', 'team-42');
|
|
923
|
-
const [url, init] = mockFetch.mock.calls[0];
|
|
924
|
-
expect(url).toContain('/agents/agent-1/transfer');
|
|
925
|
-
expect(JSON.parse(init.body)).toEqual({ team_id: 'team-42' });
|
|
926
|
-
});
|
|
927
|
-
it('should POST /agents for createAgent()', async () => {
|
|
928
|
-
const payload = { name: 'support-bot', core_app: { ref: 'app/ref' } };
|
|
929
|
-
const created = { id: 'agent-new', ...payload };
|
|
930
|
-
mockJsonResponse(created);
|
|
931
|
-
const result = await api().createAgent(payload);
|
|
932
|
-
expect(result).toEqual(created);
|
|
933
|
-
const [url, init] = mockFetch.mock.calls[0];
|
|
934
|
-
expect(url).toContain('/agents');
|
|
935
|
-
expect(init.method).toBe('POST');
|
|
936
|
-
expect(JSON.parse(init.body)).toEqual(payload);
|
|
937
|
-
});
|
|
938
|
-
it('should GET /agents/{namespace}/{name} for getByName()', async () => {
|
|
939
|
-
const agent = { id: 'agent-1', name: 'my-agent' };
|
|
940
|
-
mockJsonResponse(agent);
|
|
941
|
-
const result = await api().getByName('inference', 'my-agent');
|
|
942
|
-
expect(result).toEqual(agent);
|
|
943
|
-
const [url, init] = mockFetch.mock.calls[0];
|
|
944
|
-
expect(url).toContain('/agents/inference/my-agent');
|
|
945
|
-
expect(init.method).toBe('GET');
|
|
946
|
-
});
|
|
947
|
-
it('should POST /agents/list for list()', async () => {
|
|
948
|
-
const page = { items: [{ id: 'agent-1' }], next_cursor: null };
|
|
949
|
-
mockJsonResponse(page);
|
|
950
|
-
const result = await api().list({ limit: 10 });
|
|
951
|
-
expect(result).toEqual(page);
|
|
952
|
-
const [url, init] = mockFetch.mock.calls[0];
|
|
953
|
-
expect(url).toContain('/agents/list');
|
|
954
|
-
expect(init.method).toBe('POST');
|
|
955
|
-
});
|
|
956
|
-
it('should GET /agents/{id} for get()', async () => {
|
|
957
|
-
const agent = { id: 'agent-1', name: 'support-bot' };
|
|
958
|
-
mockJsonResponse(agent);
|
|
959
|
-
const result = await api().get('agent-1');
|
|
960
|
-
expect(result).toEqual(agent);
|
|
961
|
-
const [url, init] = mockFetch.mock.calls[0];
|
|
962
|
-
expect(url).toContain('/agents/agent-1');
|
|
963
|
-
expect(init.method).toBe('GET');
|
|
964
|
-
});
|
|
965
|
-
it('should POST /agents/{id} for update()', async () => {
|
|
966
|
-
const agent = { id: 'agent-1', name: 'updated' };
|
|
967
|
-
mockJsonResponse(agent);
|
|
968
|
-
const result = await api().update('agent-1', { name: 'updated' });
|
|
969
|
-
expect(result).toEqual(agent);
|
|
970
|
-
const [, init] = mockFetch.mock.calls[0];
|
|
971
|
-
expect(JSON.parse(init.body)).toEqual({ name: 'updated' });
|
|
972
|
-
});
|
|
973
|
-
it('should DELETE /agents/{id} for delete()', async () => {
|
|
974
|
-
mockJsonResponse(null);
|
|
975
|
-
await api().delete('agent-1');
|
|
976
|
-
const [url, init] = mockFetch.mock.calls[0];
|
|
977
|
-
expect(url).toContain('/agents/agent-1');
|
|
978
|
-
expect(init.method).toBe('DELETE');
|
|
979
|
-
});
|
|
980
|
-
it('should POST /agents/{id}/duplicate for duplicate()', async () => {
|
|
981
|
-
const agent = { id: 'agent-copy' };
|
|
982
|
-
mockJsonResponse(agent);
|
|
983
|
-
const result = await api().duplicate('agent-1');
|
|
984
|
-
expect(result).toEqual(agent);
|
|
985
|
-
const [url, init] = mockFetch.mock.calls[0];
|
|
986
|
-
expect(url).toContain('/agents/agent-1/duplicate');
|
|
987
|
-
expect(init.method).toBe('POST');
|
|
988
|
-
});
|
|
989
|
-
it('should POST /agents/{id}/versions/list for listVersions()', async () => {
|
|
990
|
-
const page = { items: [{ id: 'ver-1' }], next_cursor: null };
|
|
991
|
-
mockJsonResponse(page);
|
|
992
|
-
const result = await api().listVersions('agent-1', { limit: 5 });
|
|
993
|
-
expect(result).toEqual(page);
|
|
994
|
-
const [url, init] = mockFetch.mock.calls[0];
|
|
995
|
-
expect(url).toContain('/agents/agent-1/versions/list');
|
|
996
|
-
expect(JSON.parse(init.body)).toEqual({ limit: 5 });
|
|
997
|
-
});
|
|
998
|
-
it('should GET /agents/{id}/versions/{versionId} for getVersion()', async () => {
|
|
999
|
-
const version = { id: 'ver-1', agent_id: 'agent-1' };
|
|
1000
|
-
mockJsonResponse(version);
|
|
1001
|
-
const result = await api().getVersion('agent-1', 'ver-1');
|
|
1002
|
-
expect(result).toEqual(version);
|
|
1003
|
-
const [url, init] = mockFetch.mock.calls[0];
|
|
1004
|
-
expect(url).toContain('/agents/agent-1/versions/ver-1');
|
|
1005
|
-
expect(init.method).toBe('GET');
|
|
1006
|
-
});
|
|
1007
|
-
it('should POST visibility for updateVisibility()', async () => {
|
|
1008
|
-
const agent = { id: 'agent-1', visibility: 'team' };
|
|
1009
|
-
mockJsonResponse(agent);
|
|
1010
|
-
await api().updateVisibility('agent-1', 'team');
|
|
1011
|
-
const [, init] = mockFetch.mock.calls[0];
|
|
1012
|
-
expect(JSON.parse(init.body)).toEqual({ visibility: 'team' });
|
|
1013
|
-
});
|
|
1014
|
-
});
|