@inferencesh/sdk 0.6.12 → 0.6.14
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/agent/actions.test.js +329 -2
- package/dist/agent/api.test.js +39 -1
- package/dist/api/agents.test.js +405 -1
- package/dist/api/files.test.js +8 -0
- package/dist/api/knowledge.test.js +144 -0
- package/dist/api/mcp-servers.test.js +34 -0
- package/dist/api/projects.test.js +9 -0
- package/dist/api/search.test.js +11 -0
- package/dist/api/sessions.test.js +12 -0
- package/dist/api/tasks.test.js +58 -0
- package/dist/api/teams.test.js +60 -0
- package/dist/client.test.js +112 -1
- package/dist/http/poll.test.js +61 -0
- package/dist/http/streamable.test.js +100 -0
- package/dist/proxy/hono.test.d.ts +1 -0
- package/dist/proxy/hono.test.js +90 -0
- package/dist/proxy/index.test.js +17 -0
- package/dist/proxy/nextjs.test.d.ts +1 -0
- package/dist/proxy/nextjs.test.js +125 -0
- package/dist/proxy/remix.test.d.ts +1 -0
- package/dist/proxy/remix.test.js +66 -0
- package/dist/proxy/svelte.test.d.ts +1 -0
- package/dist/proxy/svelte.test.js +69 -0
- package/dist/stream.test.js +9 -0
- package/dist/types.d.ts +1 -0
- package/package.json +3 -3
package/dist/api/agents.test.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { HttpClient } from '../http/client';
|
|
2
2
|
import { StreamableManager } from '../http/streamable';
|
|
3
|
-
import {
|
|
3
|
+
import { PollManager } from '../http/poll';
|
|
4
|
+
import { ChatStatusBusy, ChatStatusIdle, ToolInvocationStatusAwaitingInput, ToolInvocationStatusInProgress, ToolTypeClient, } from '../types';
|
|
4
5
|
import { FilesAPI } from './files';
|
|
5
6
|
import { AgentsAPI } from './agents';
|
|
6
7
|
const mockFetch = jest.fn();
|
|
@@ -70,6 +71,66 @@ describe('Agent.sendMessage (polling mode)', () => {
|
|
|
70
71
|
expect(result.assistantMessage).toEqual(assistantMessage);
|
|
71
72
|
expect(onChat).toHaveBeenCalledWith(expect.objectContaining({ id: 'chat-1', status: ChatStatusIdle }));
|
|
72
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
|
+
});
|
|
73
134
|
it('should dispatch onToolCall once per client tool invocation', async () => {
|
|
74
135
|
const toolInvocation = {
|
|
75
136
|
id: 'tool-inv-1',
|
|
@@ -104,6 +165,30 @@ describe('Agent.sendMessage (polling mode)', () => {
|
|
|
104
165
|
args: { x: 1 },
|
|
105
166
|
});
|
|
106
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
|
+
});
|
|
107
192
|
it('should return chat output from run() after polling completes', async () => {
|
|
108
193
|
const userMessage = makeMessage({ id: 'user-1', role: 'user' });
|
|
109
194
|
const assistantMessage = makeMessage();
|
|
@@ -124,6 +209,43 @@ describe('Agent.sendMessage (polling mode)', () => {
|
|
|
124
209
|
const output = await agent().run('compute');
|
|
125
210
|
expect(output).toEqual({ answer: 42 });
|
|
126
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
|
+
});
|
|
127
249
|
});
|
|
128
250
|
describe('Agent.sendMessage (streaming mode)', () => {
|
|
129
251
|
beforeEach(() => {
|
|
@@ -156,6 +278,39 @@ describe('Agent.sendMessage (streaming mode)', () => {
|
|
|
156
278
|
expect(result.userMessage).toEqual(userMessage);
|
|
157
279
|
expect(onChat).toHaveBeenCalledWith(expect.objectContaining({ id: 'chat-1', status: ChatStatusIdle }));
|
|
158
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
|
+
});
|
|
159
314
|
it('should dispatch onToolCall from chat_messages stream events', async () => {
|
|
160
315
|
const toolInvocation = {
|
|
161
316
|
id: 'tool-inv-1',
|
|
@@ -189,6 +344,19 @@ describe('Agent.sendMessage (streaming mode)', () => {
|
|
|
189
344
|
args: { x: 1 },
|
|
190
345
|
});
|
|
191
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
|
+
});
|
|
192
360
|
it('should open the stream before POST when continuing an existing chat', async () => {
|
|
193
361
|
const http = new HttpClient({
|
|
194
362
|
apiKey: 'test-key',
|
|
@@ -281,6 +449,79 @@ describe('Agent.sendMessage (file attachments)', () => {
|
|
|
281
449
|
expect(body.input.files).toEqual(['inf://files/doc']);
|
|
282
450
|
expect(mockFetch.mock.calls.filter(([url]) => String(url).includes('/files')).length).toBe(0);
|
|
283
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
|
+
});
|
|
284
525
|
});
|
|
285
526
|
describe('Agent.sendMessage (ad-hoc config)', () => {
|
|
286
527
|
beforeEach(() => {
|
|
@@ -319,6 +560,31 @@ describe('Agent.sendMessage (ad-hoc config)', () => {
|
|
|
319
560
|
expect(body.agent_name).toBe('adhoc-bot');
|
|
320
561
|
expect(body.input.text).toBe('hello');
|
|
321
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
|
+
});
|
|
322
588
|
});
|
|
323
589
|
describe('Agent lifecycle', () => {
|
|
324
590
|
beforeEach(() => {
|
|
@@ -356,6 +622,28 @@ describe('Agent lifecycle', () => {
|
|
|
356
622
|
await agentInstance.stopChat();
|
|
357
623
|
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/chats/chat-1/stop'), expect.anything());
|
|
358
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
|
+
});
|
|
359
647
|
it('disconnect should stop active stream managers', async () => {
|
|
360
648
|
const http = new HttpClient({
|
|
361
649
|
apiKey: 'test-key',
|
|
@@ -436,6 +724,64 @@ describe('Agent lifecycle', () => {
|
|
|
436
724
|
await agentInstance.stopChat();
|
|
437
725
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
438
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
|
+
});
|
|
439
785
|
});
|
|
440
786
|
describe('Agent.getChat', () => {
|
|
441
787
|
beforeEach(() => {
|
|
@@ -459,11 +805,55 @@ describe('Agent.getChat', () => {
|
|
|
459
805
|
expect(url).toContain('/chats/chat-42');
|
|
460
806
|
expect(init.method).toBe('GET');
|
|
461
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
|
+
});
|
|
462
842
|
});
|
|
463
843
|
describe('Agent.submitToolResult', () => {
|
|
464
844
|
beforeEach(() => {
|
|
465
845
|
jest.clearAllMocks();
|
|
466
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
|
+
});
|
|
467
857
|
it('should JSON-stringify structured action results', async () => {
|
|
468
858
|
const http = new HttpClient({ apiKey: 'test-key' });
|
|
469
859
|
const agentInstance = new AgentsAPI(http, new FilesAPI(http)).create('my-agent');
|
|
@@ -512,6 +902,20 @@ describe('AgentsAPI (template CRUD)', () => {
|
|
|
512
902
|
expect(url).toContain('/agents/internal-tools');
|
|
513
903
|
expect(init.method).toBe('GET');
|
|
514
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
|
+
});
|
|
515
919
|
it('should POST team_id for transferOwnership()', async () => {
|
|
516
920
|
const agent = { id: 'agent-1' };
|
|
517
921
|
mockJsonResponse(agent);
|
package/dist/api/files.test.js
CHANGED
|
@@ -41,6 +41,14 @@ describe('FilesAPI', () => {
|
|
|
41
41
|
expect(init.method).toBe('DELETE');
|
|
42
42
|
});
|
|
43
43
|
describe('processInput', () => {
|
|
44
|
+
it('should return falsy inputs unchanged without uploading', async () => {
|
|
45
|
+
await expect(api().processInput(null)).resolves.toBeNull();
|
|
46
|
+
await expect(api().processInput(undefined)).resolves.toBeUndefined();
|
|
47
|
+
await expect(api().processInput(0)).resolves.toBe(0);
|
|
48
|
+
await expect(api().processInput(false)).resolves.toBe(false);
|
|
49
|
+
await expect(api().processInput('')).resolves.toBe('');
|
|
50
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
51
|
+
});
|
|
44
52
|
it('should not treat short plain strings as base64 file uploads', async () => {
|
|
45
53
|
const result = await api().processInput({ key: 'key1', note: 'hello' });
|
|
46
54
|
expect(result).toEqual({ key: 'key1', note: 'hello' });
|
|
@@ -50,6 +50,55 @@ describe('KnowledgeAPI', () => {
|
|
|
50
50
|
expect(url).toContain('/knowledge/know-1/transfer');
|
|
51
51
|
expect(JSON.parse(init.body)).toEqual({ team_id: 'team-5' });
|
|
52
52
|
});
|
|
53
|
+
it('should GET /knowledge/{id} for get()', async () => {
|
|
54
|
+
const entry = { id: 'know-1', name: 'docs' };
|
|
55
|
+
mockJsonResponse(entry);
|
|
56
|
+
const result = await api().get('know-1');
|
|
57
|
+
expect(result).toEqual(entry);
|
|
58
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
59
|
+
expect(url).toContain('/knowledge/know-1');
|
|
60
|
+
expect(init.method).toBe('GET');
|
|
61
|
+
});
|
|
62
|
+
it('should POST /knowledge/{id} for update()', async () => {
|
|
63
|
+
const entry = { id: 'know-1', name: 'updated' };
|
|
64
|
+
mockJsonResponse(entry);
|
|
65
|
+
const result = await api().update('know-1', { name: 'updated' });
|
|
66
|
+
expect(result).toEqual(entry);
|
|
67
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
68
|
+
expect(JSON.parse(init.body)).toEqual({ name: 'updated' });
|
|
69
|
+
});
|
|
70
|
+
it('should DELETE /knowledge/{id} for delete()', async () => {
|
|
71
|
+
mockJsonResponse(null);
|
|
72
|
+
await api().delete('know-1');
|
|
73
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
74
|
+
expect(url).toContain('/knowledge/know-1');
|
|
75
|
+
expect(init.method).toBe('DELETE');
|
|
76
|
+
});
|
|
77
|
+
it('should POST /knowledge/{id}/versions/list for listVersions()', async () => {
|
|
78
|
+
const page = { items: [{ id: 'ver-1' }], next_cursor: null };
|
|
79
|
+
mockJsonResponse(page);
|
|
80
|
+
const result = await api().listVersions('know-1', { limit: 10 });
|
|
81
|
+
expect(result).toEqual(page);
|
|
82
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
83
|
+
expect(url).toContain('/knowledge/know-1/versions/list');
|
|
84
|
+
expect(JSON.parse(init.body)).toEqual({ limit: 10 });
|
|
85
|
+
});
|
|
86
|
+
it('should GET /knowledge/{id}/versions/{versionId} for getVersion()', async () => {
|
|
87
|
+
const version = { id: 'ver-1', knowledge_id: 'know-1' };
|
|
88
|
+
mockJsonResponse(version);
|
|
89
|
+
const result = await api().getVersion('know-1', 'ver-1');
|
|
90
|
+
expect(result).toEqual(version);
|
|
91
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
92
|
+
expect(url).toContain('/knowledge/know-1/versions/ver-1');
|
|
93
|
+
expect(init.method).toBe('GET');
|
|
94
|
+
});
|
|
95
|
+
it('should POST visibility for updateVisibility()', async () => {
|
|
96
|
+
const entry = { id: 'know-1', visibility: 'team' };
|
|
97
|
+
mockJsonResponse(entry);
|
|
98
|
+
await api().updateVisibility('know-1', 'team');
|
|
99
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
100
|
+
expect(JSON.parse(init.body)).toEqual({ visibility: 'team' });
|
|
101
|
+
});
|
|
53
102
|
});
|
|
54
103
|
describe('SkillsAPI', () => {
|
|
55
104
|
beforeEach(() => {
|
|
@@ -91,4 +140,99 @@ describe('SkillsAPI', () => {
|
|
|
91
140
|
expect(url).toContain('/store/skills/list');
|
|
92
141
|
expect(init.method).toBe('POST');
|
|
93
142
|
});
|
|
143
|
+
it('should GET /skills/resolve with ref only when skill is omitted', async () => {
|
|
144
|
+
const resolved = { ref: 'acme/skill@v1' };
|
|
145
|
+
mockJsonResponse(resolved);
|
|
146
|
+
const result = await api().resolve('acme/skill@v1');
|
|
147
|
+
expect(result).toEqual(resolved);
|
|
148
|
+
const [url] = mockFetch.mock.calls[0];
|
|
149
|
+
expect(url).toContain('/skills/resolve');
|
|
150
|
+
expect(url).toContain('ref=acme');
|
|
151
|
+
expect(url).not.toContain('skill=');
|
|
152
|
+
});
|
|
153
|
+
it('should GET /skills/{namespace}/{name}/content for getContent()', async () => {
|
|
154
|
+
mockJsonResponse({ body: '# Skill instructions' });
|
|
155
|
+
const result = await api().getContent('acme', 'research');
|
|
156
|
+
expect(result).toEqual({ body: '# Skill instructions' });
|
|
157
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
158
|
+
expect(url).toContain('/skills/acme/research/content');
|
|
159
|
+
expect(init.method).toBe('GET');
|
|
160
|
+
});
|
|
161
|
+
it('should POST team_id for transferOwnership()', async () => {
|
|
162
|
+
const skill = { id: 'skill-1' };
|
|
163
|
+
mockJsonResponse(skill);
|
|
164
|
+
await api().transferOwnership('skill-1', 'team-8');
|
|
165
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
166
|
+
expect(url).toContain('/skills/skill-1/transfer');
|
|
167
|
+
expect(JSON.parse(init.body)).toEqual({ team_id: 'team-8' });
|
|
168
|
+
});
|
|
169
|
+
it('should POST /skills/list for list()', async () => {
|
|
170
|
+
const page = { items: [{ id: 'skill-1' }], next_cursor: null };
|
|
171
|
+
mockJsonResponse(page);
|
|
172
|
+
const result = await api().list({ limit: 10 });
|
|
173
|
+
expect(result).toEqual(page);
|
|
174
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
175
|
+
expect(url).toContain('/skills/list');
|
|
176
|
+
expect(init.method).toBe('POST');
|
|
177
|
+
});
|
|
178
|
+
it('should GET /skills/{id} for get()', async () => {
|
|
179
|
+
const skill = { id: 'skill-1', name: 'research' };
|
|
180
|
+
mockJsonResponse(skill);
|
|
181
|
+
const result = await api().get('skill-1');
|
|
182
|
+
expect(result).toEqual(skill);
|
|
183
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
184
|
+
expect(url).toContain('/skills/skill-1');
|
|
185
|
+
expect(init.method).toBe('GET');
|
|
186
|
+
});
|
|
187
|
+
it('should POST /skills for create()', async () => {
|
|
188
|
+
const payload = { namespace: 'acme', name: 'research' };
|
|
189
|
+
const skill = { id: 'skill-new', ...payload };
|
|
190
|
+
mockJsonResponse(skill);
|
|
191
|
+
const result = await api().create(payload);
|
|
192
|
+
expect(result).toEqual(skill);
|
|
193
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
194
|
+
expect(url).toContain('/skills');
|
|
195
|
+
expect(init.method).toBe('POST');
|
|
196
|
+
expect(JSON.parse(init.body)).toEqual(payload);
|
|
197
|
+
});
|
|
198
|
+
it('should POST /skills/{id} for update()', async () => {
|
|
199
|
+
const skill = { id: 'skill-1', name: 'updated' };
|
|
200
|
+
mockJsonResponse(skill);
|
|
201
|
+
const result = await api().update('skill-1', { name: 'updated' });
|
|
202
|
+
expect(result).toEqual(skill);
|
|
203
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
204
|
+
expect(JSON.parse(init.body)).toEqual({ name: 'updated' });
|
|
205
|
+
});
|
|
206
|
+
it('should DELETE /skills/{id} for delete()', async () => {
|
|
207
|
+
mockJsonResponse(null);
|
|
208
|
+
await api().delete('skill-1');
|
|
209
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
210
|
+
expect(url).toContain('/skills/skill-1');
|
|
211
|
+
expect(init.method).toBe('DELETE');
|
|
212
|
+
});
|
|
213
|
+
it('should POST /skills/{id}/versions/list for listVersions()', async () => {
|
|
214
|
+
const page = { items: [{ id: 'ver-1' }], next_cursor: null };
|
|
215
|
+
mockJsonResponse(page);
|
|
216
|
+
const result = await api().listVersions('skill-1', { limit: 5 });
|
|
217
|
+
expect(result).toEqual(page);
|
|
218
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
219
|
+
expect(url).toContain('/skills/skill-1/versions/list');
|
|
220
|
+
expect(JSON.parse(init.body)).toEqual({ limit: 5 });
|
|
221
|
+
});
|
|
222
|
+
it('should GET /skills/{id}/versions/{versionId} for getVersion()', async () => {
|
|
223
|
+
const version = { id: 'ver-1', skill_id: 'skill-1' };
|
|
224
|
+
mockJsonResponse(version);
|
|
225
|
+
const result = await api().getVersion('skill-1', 'ver-1');
|
|
226
|
+
expect(result).toEqual(version);
|
|
227
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
228
|
+
expect(url).toContain('/skills/skill-1/versions/ver-1');
|
|
229
|
+
expect(init.method).toBe('GET');
|
|
230
|
+
});
|
|
231
|
+
it('should POST visibility for updateVisibility()', async () => {
|
|
232
|
+
const skill = { id: 'skill-1', visibility: 'team' };
|
|
233
|
+
mockJsonResponse(skill);
|
|
234
|
+
await api().updateVisibility('skill-1', 'team');
|
|
235
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
236
|
+
expect(JSON.parse(init.body)).toEqual({ visibility: 'team' });
|
|
237
|
+
});
|
|
94
238
|
});
|