@inferencesh/sdk 0.6.10 → 0.6.13
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 +3 -3
- package/dist/agent/actions.test.js +231 -1
- package/dist/agent/api.test.js +18 -1
- package/dist/api/agents.d.ts +10 -0
- package/dist/api/agents.js +17 -3
- package/dist/api/agents.test.js +500 -0
- package/dist/api/api-keys.d.ts +24 -0
- package/dist/api/api-keys.js +26 -0
- package/dist/api/api-keys.test.d.ts +1 -0
- package/dist/api/api-keys.test.js +44 -0
- package/dist/api/apps.js +1 -1
- package/dist/api/apps.test.js +71 -3
- package/dist/api/chats.d.ts +14 -0
- package/dist/api/chats.js +18 -0
- package/dist/api/chats.test.js +52 -0
- package/dist/api/engines.d.ts +12 -0
- package/dist/api/engines.js +18 -0
- package/dist/api/engines.test.js +78 -0
- package/dist/api/files.test.js +183 -0
- package/dist/api/flow-runs.test.js +42 -0
- package/dist/api/flows.d.ts +4 -0
- package/dist/api/flows.js +6 -0
- package/dist/api/flows.test.js +75 -0
- package/dist/api/integrations.d.ts +41 -0
- package/dist/api/integrations.js +56 -0
- package/dist/api/integrations.test.d.ts +1 -0
- package/dist/api/integrations.test.js +109 -0
- package/dist/api/knowledge.d.ts +112 -0
- package/dist/api/knowledge.js +163 -0
- package/dist/api/knowledge.test.d.ts +1 -0
- package/dist/api/knowledge.test.js +238 -0
- package/dist/api/mcp-servers.d.ts +45 -0
- package/dist/api/mcp-servers.js +62 -0
- package/dist/api/mcp-servers.test.d.ts +1 -0
- package/dist/api/mcp-servers.test.js +98 -0
- package/dist/api/projects.d.ts +29 -0
- package/dist/api/projects.js +38 -0
- package/dist/api/projects.test.d.ts +1 -0
- package/dist/api/projects.test.js +61 -0
- package/dist/api/search.d.ts +21 -0
- package/dist/api/search.js +20 -0
- package/dist/api/search.test.d.ts +1 -0
- package/dist/api/search.test.js +39 -0
- package/dist/api/secrets.d.ts +29 -0
- package/dist/api/secrets.js +38 -0
- package/dist/api/secrets.test.d.ts +1 -0
- package/dist/api/secrets.test.js +61 -0
- package/dist/api/sessions.test.js +12 -0
- package/dist/api/tasks.d.ts +13 -1
- package/dist/api/tasks.js +18 -0
- package/dist/api/tasks.test.js +171 -0
- package/dist/api/teams.d.ts +71 -0
- package/dist/api/teams.js +92 -0
- package/dist/api/teams.test.d.ts +1 -0
- package/dist/api/teams.test.js +139 -0
- package/dist/client.test.js +112 -1
- package/dist/http/client.d.ts +5 -0
- package/dist/http/client.js +42 -23
- package/dist/http/client.test.js +245 -0
- package/dist/http/errors.test.js +13 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +25 -0
- package/dist/proxy/hono.test.d.ts +1 -0
- package/dist/proxy/hono.test.js +90 -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/tool-builder.test.js +11 -1
- package/dist/types.d.ts +110 -5
- package/dist/types.js +58 -2
- package/package.json +6 -6
package/dist/api/agents.test.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { HttpClient } from '../http/client';
|
|
2
|
+
import { StreamableManager } from '../http/streamable';
|
|
3
|
+
import { PollManager } from '../http/poll';
|
|
2
4
|
import { ChatStatusBusy, ChatStatusIdle, ToolInvocationStatusAwaitingInput, ToolTypeClient, } from '../types';
|
|
3
5
|
import { FilesAPI } from './files';
|
|
4
6
|
import { AgentsAPI } from './agents';
|
|
@@ -69,6 +71,32 @@ describe('Agent.sendMessage (polling mode)', () => {
|
|
|
69
71
|
expect(result.assistantMessage).toEqual(assistantMessage);
|
|
70
72
|
expect(onChat).toHaveBeenCalledWith(expect.objectContaining({ id: 'chat-1', status: ChatStatusIdle }));
|
|
71
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
|
+
});
|
|
72
100
|
it('should dispatch onToolCall once per client tool invocation', async () => {
|
|
73
101
|
const toolInvocation = {
|
|
74
102
|
id: 'tool-inv-1',
|
|
@@ -103,6 +131,30 @@ describe('Agent.sendMessage (polling mode)', () => {
|
|
|
103
131
|
args: { x: 1 },
|
|
104
132
|
});
|
|
105
133
|
});
|
|
134
|
+
it('should call onMessage for each chat message during polling', async () => {
|
|
135
|
+
const msg1 = makeMessage({ id: 'msg-1', content: 'first' });
|
|
136
|
+
const msg2 = makeMessage({ id: 'msg-2', content: 'second' });
|
|
137
|
+
mockJsonResponse({
|
|
138
|
+
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
139
|
+
assistant_message: makeMessage(),
|
|
140
|
+
});
|
|
141
|
+
mockJsonResponse({ status: ChatStatusBusy });
|
|
142
|
+
mockJsonResponse({
|
|
143
|
+
id: 'chat-1',
|
|
144
|
+
status: ChatStatusBusy,
|
|
145
|
+
chat_messages: [msg1, msg2],
|
|
146
|
+
});
|
|
147
|
+
mockJsonResponse({ status: ChatStatusIdle });
|
|
148
|
+
mockJsonResponse({
|
|
149
|
+
id: 'chat-1',
|
|
150
|
+
status: ChatStatusIdle,
|
|
151
|
+
chat_messages: [msg1, msg2],
|
|
152
|
+
});
|
|
153
|
+
const onMessage = jest.fn();
|
|
154
|
+
await agent().sendMessage('hello', { stream: false, onMessage });
|
|
155
|
+
expect(onMessage).toHaveBeenCalledWith(expect.objectContaining({ id: 'msg-1', content: 'first' }));
|
|
156
|
+
expect(onMessage).toHaveBeenCalledWith(expect.objectContaining({ id: 'msg-2', content: 'second' }));
|
|
157
|
+
});
|
|
106
158
|
it('should return chat output from run() after polling completes', async () => {
|
|
107
159
|
const userMessage = makeMessage({ id: 'user-1', role: 'user' });
|
|
108
160
|
const assistantMessage = makeMessage();
|
|
@@ -123,6 +175,19 @@ describe('Agent.sendMessage (polling mode)', () => {
|
|
|
123
175
|
const output = await agent().run('compute');
|
|
124
176
|
expect(output).toEqual({ answer: 42 });
|
|
125
177
|
});
|
|
178
|
+
it('should return null from run() when the chat has no finish output', async () => {
|
|
179
|
+
mockJsonResponse({
|
|
180
|
+
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
181
|
+
assistant_message: makeMessage(),
|
|
182
|
+
});
|
|
183
|
+
mockJsonResponse({ status: ChatStatusBusy });
|
|
184
|
+
mockJsonResponse({ id: 'chat-1', status: ChatStatusBusy });
|
|
185
|
+
mockJsonResponse({ status: ChatStatusIdle });
|
|
186
|
+
mockJsonResponse({ id: 'chat-1', status: ChatStatusIdle });
|
|
187
|
+
mockJsonResponse({ id: 'chat-1', status: ChatStatusIdle, output: null });
|
|
188
|
+
const output = await agent().run('no finish tool');
|
|
189
|
+
expect(output).toBeNull();
|
|
190
|
+
});
|
|
126
191
|
});
|
|
127
192
|
describe('Agent.sendMessage (streaming mode)', () => {
|
|
128
193
|
beforeEach(() => {
|
|
@@ -188,6 +253,19 @@ describe('Agent.sendMessage (streaming mode)', () => {
|
|
|
188
253
|
args: { x: 1 },
|
|
189
254
|
});
|
|
190
255
|
});
|
|
256
|
+
it('should return immediately without waiting when stream is true and no callbacks', async () => {
|
|
257
|
+
const userMessage = makeMessage({ id: 'user-1', role: 'user' });
|
|
258
|
+
const assistantMessage = makeMessage({ id: 'asst-1' });
|
|
259
|
+
mockJsonResponse({
|
|
260
|
+
user_message: userMessage,
|
|
261
|
+
assistant_message: assistantMessage,
|
|
262
|
+
});
|
|
263
|
+
const result = await streamingAgent().sendMessage('hello');
|
|
264
|
+
expect(result.userMessage).toEqual(userMessage);
|
|
265
|
+
expect(result.assistantMessage).toEqual(assistantMessage);
|
|
266
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
267
|
+
expect(mockFetch.mock.calls[0][0]).toContain('/agents/run');
|
|
268
|
+
});
|
|
191
269
|
it('should open the stream before POST when continuing an existing chat', async () => {
|
|
192
270
|
const http = new HttpClient({
|
|
193
271
|
apiKey: 'test-key',
|
|
@@ -280,6 +358,142 @@ describe('Agent.sendMessage (file attachments)', () => {
|
|
|
280
358
|
expect(body.input.files).toEqual(['inf://files/doc']);
|
|
281
359
|
expect(mockFetch.mock.calls.filter(([url]) => String(url).includes('/files')).length).toBe(0);
|
|
282
360
|
});
|
|
361
|
+
it('should upload Blob attachments before POST /agents/run', async () => {
|
|
362
|
+
const fileRecord = {
|
|
363
|
+
id: 'file-blob',
|
|
364
|
+
uri: 'inf://files/blob-direct',
|
|
365
|
+
upload_url: 'https://upload.example.com/put',
|
|
366
|
+
content_type: 'image/png',
|
|
367
|
+
};
|
|
368
|
+
mockJsonResponse([fileRecord]);
|
|
369
|
+
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
370
|
+
mockJsonResponse({
|
|
371
|
+
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
372
|
+
assistant_message: makeMessage(),
|
|
373
|
+
});
|
|
374
|
+
mockJsonResponse({ status: ChatStatusBusy });
|
|
375
|
+
mockJsonResponse({ id: 'chat-1', status: ChatStatusBusy, chat_messages: [] });
|
|
376
|
+
mockJsonResponse({ status: ChatStatusIdle });
|
|
377
|
+
mockJsonResponse({ id: 'chat-1', status: ChatStatusIdle, chat_messages: [] });
|
|
378
|
+
const blob = new Blob(['png-bytes'], { type: 'image/png' });
|
|
379
|
+
await agent().sendMessage('see image', { stream: false, files: [blob] });
|
|
380
|
+
const fileCreateCall = mockFetch.mock.calls.find(([url]) => String(url).includes('/files') && !String(url).includes('upload.example.com'));
|
|
381
|
+
expect(fileCreateCall).toBeDefined();
|
|
382
|
+
const runCall = mockFetch.mock.calls.find(([url]) => String(url).includes('/agents/run'));
|
|
383
|
+
const body = JSON.parse(String(runCall[1].body));
|
|
384
|
+
expect(body.input.images).toEqual(['inf://files/blob-direct']);
|
|
385
|
+
expect(body.input.files).toBeUndefined();
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
describe('Agent.sendMessage (template ref)', () => {
|
|
389
|
+
beforeEach(() => {
|
|
390
|
+
jest.clearAllMocks();
|
|
391
|
+
});
|
|
392
|
+
const templateAgent = (context) => {
|
|
393
|
+
const http = new HttpClient({
|
|
394
|
+
apiKey: 'test-key',
|
|
395
|
+
stream: false,
|
|
396
|
+
pollIntervalMs: 20,
|
|
397
|
+
});
|
|
398
|
+
return new AgentsAPI(http, new FilesAPI(http)).create('inference/my-agent', { context });
|
|
399
|
+
};
|
|
400
|
+
function mockRunAndPoll() {
|
|
401
|
+
mockJsonResponse({
|
|
402
|
+
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
403
|
+
assistant_message: makeMessage(),
|
|
404
|
+
});
|
|
405
|
+
mockJsonResponse({ status: ChatStatusBusy });
|
|
406
|
+
mockJsonResponse({ id: 'chat-1', status: ChatStatusBusy, chat_messages: [] });
|
|
407
|
+
mockJsonResponse({ status: ChatStatusIdle });
|
|
408
|
+
mockJsonResponse({ id: 'chat-1', status: ChatStatusIdle, chat_messages: [] });
|
|
409
|
+
}
|
|
410
|
+
it('should POST agent ref and context to /agents/run', async () => {
|
|
411
|
+
mockRunAndPoll();
|
|
412
|
+
await templateAgent({ tenant: 'acme' }).sendMessage('hello', { stream: false });
|
|
413
|
+
const runCall = mockFetch.mock.calls.find(([url]) => String(url).includes('/agents/run'));
|
|
414
|
+
const body = JSON.parse(String(runCall[1].body));
|
|
415
|
+
expect(body.agent).toBe('inference/my-agent');
|
|
416
|
+
expect(body.agent_config).toBeUndefined();
|
|
417
|
+
expect(body.context).toEqual({ tenant: 'acme' });
|
|
418
|
+
expect(body.chat_id).toBeNull();
|
|
419
|
+
expect(body.input.text).toBe('hello');
|
|
420
|
+
});
|
|
421
|
+
it('should include chat_id on follow-up messages', async () => {
|
|
422
|
+
const agentInstance = templateAgent();
|
|
423
|
+
mockRunAndPoll();
|
|
424
|
+
await agentInstance.sendMessage('first', { stream: false });
|
|
425
|
+
jest.clearAllMocks();
|
|
426
|
+
mockRunAndPoll();
|
|
427
|
+
await agentInstance.sendMessage('second', { stream: false });
|
|
428
|
+
const runCall = mockFetch.mock.calls.find(([url]) => String(url).includes('/agents/run'));
|
|
429
|
+
const body = JSON.parse(String(runCall[1].body));
|
|
430
|
+
expect(body.agent).toBe('inference/my-agent');
|
|
431
|
+
expect(body.chat_id).toBe('chat-1');
|
|
432
|
+
expect(body.input.text).toBe('second');
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
describe('Agent.sendMessage (ad-hoc config)', () => {
|
|
436
|
+
beforeEach(() => {
|
|
437
|
+
jest.clearAllMocks();
|
|
438
|
+
});
|
|
439
|
+
const adHocAgent = () => {
|
|
440
|
+
const http = new HttpClient({
|
|
441
|
+
apiKey: 'test-key',
|
|
442
|
+
stream: false,
|
|
443
|
+
pollIntervalMs: 20,
|
|
444
|
+
});
|
|
445
|
+
return new AgentsAPI(http, new FilesAPI(http)).create({
|
|
446
|
+
core_app: { ref: 'openrouter/claude@latest' },
|
|
447
|
+
system_prompt: 'You are helpful',
|
|
448
|
+
name: 'adhoc-bot',
|
|
449
|
+
});
|
|
450
|
+
};
|
|
451
|
+
it('should POST agent_config and agent_name instead of agent template ref', async () => {
|
|
452
|
+
mockJsonResponse({
|
|
453
|
+
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
454
|
+
assistant_message: makeMessage(),
|
|
455
|
+
});
|
|
456
|
+
mockJsonResponse({ status: ChatStatusBusy });
|
|
457
|
+
mockJsonResponse({ id: 'chat-1', status: ChatStatusBusy, chat_messages: [] });
|
|
458
|
+
mockJsonResponse({ status: ChatStatusIdle });
|
|
459
|
+
mockJsonResponse({ id: 'chat-1', status: ChatStatusIdle, chat_messages: [] });
|
|
460
|
+
await adHocAgent().sendMessage('hello', { stream: false });
|
|
461
|
+
const runCall = mockFetch.mock.calls.find(([url]) => String(url).includes('/agents/run'));
|
|
462
|
+
const body = JSON.parse(String(runCall[1].body));
|
|
463
|
+
expect(body.agent).toBeUndefined();
|
|
464
|
+
expect(body.agent_config).toEqual({
|
|
465
|
+
core_app: { ref: 'openrouter/claude@latest' },
|
|
466
|
+
system_prompt: 'You are helpful',
|
|
467
|
+
name: 'adhoc-bot',
|
|
468
|
+
});
|
|
469
|
+
expect(body.agent_name).toBe('adhoc-bot');
|
|
470
|
+
expect(body.input.text).toBe('hello');
|
|
471
|
+
});
|
|
472
|
+
it('should prefer AgentOptions.name over config.name for agent_name', async () => {
|
|
473
|
+
const http = new HttpClient({
|
|
474
|
+
apiKey: 'test-key',
|
|
475
|
+
stream: false,
|
|
476
|
+
pollIntervalMs: 20,
|
|
477
|
+
});
|
|
478
|
+
const namedAgent = new AgentsAPI(http, new FilesAPI(http)).create({
|
|
479
|
+
core_app: { ref: 'openrouter/claude@latest' },
|
|
480
|
+
system_prompt: 'You are helpful',
|
|
481
|
+
name: 'config-name',
|
|
482
|
+
}, { name: 'override-name' });
|
|
483
|
+
mockJsonResponse({
|
|
484
|
+
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
485
|
+
assistant_message: makeMessage(),
|
|
486
|
+
});
|
|
487
|
+
mockJsonResponse({ status: ChatStatusBusy });
|
|
488
|
+
mockJsonResponse({ id: 'chat-1', status: ChatStatusBusy, chat_messages: [] });
|
|
489
|
+
mockJsonResponse({ status: ChatStatusIdle });
|
|
490
|
+
mockJsonResponse({ id: 'chat-1', status: ChatStatusIdle, chat_messages: [] });
|
|
491
|
+
await namedAgent.sendMessage('hello', { stream: false });
|
|
492
|
+
const runCall = mockFetch.mock.calls.find(([url]) => String(url).includes('/agents/run'));
|
|
493
|
+
const body = JSON.parse(String(runCall[1].body));
|
|
494
|
+
expect(body.agent_name).toBe('override-name');
|
|
495
|
+
expect(body.agent_config.name).toBe('config-name');
|
|
496
|
+
});
|
|
283
497
|
});
|
|
284
498
|
describe('Agent lifecycle', () => {
|
|
285
499
|
beforeEach(() => {
|
|
@@ -317,6 +531,88 @@ describe('Agent lifecycle', () => {
|
|
|
317
531
|
await agentInstance.stopChat();
|
|
318
532
|
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/chats/chat-1/stop'), expect.anything());
|
|
319
533
|
});
|
|
534
|
+
it('disconnect should stop active poll managers', async () => {
|
|
535
|
+
jest.useFakeTimers();
|
|
536
|
+
const stopSpy = jest.spyOn(PollManager.prototype, 'stop');
|
|
537
|
+
const agentInstance = agent();
|
|
538
|
+
mockJsonResponse({
|
|
539
|
+
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
540
|
+
assistant_message: makeMessage(),
|
|
541
|
+
});
|
|
542
|
+
mockJsonResponse({ status: ChatStatusBusy });
|
|
543
|
+
mockJsonResponse({ id: 'chat-1', status: ChatStatusBusy, chat_messages: [] });
|
|
544
|
+
const sendPromise = agentInstance.sendMessage('hello', {
|
|
545
|
+
stream: false,
|
|
546
|
+
pollIntervalMs: 5000,
|
|
547
|
+
});
|
|
548
|
+
await Promise.resolve();
|
|
549
|
+
await jest.advanceTimersByTimeAsync(0);
|
|
550
|
+
agentInstance.disconnect();
|
|
551
|
+
expect(stopSpy).toHaveBeenCalled();
|
|
552
|
+
stopSpy.mockRestore();
|
|
553
|
+
jest.useRealTimers();
|
|
554
|
+
sendPromise.catch(() => undefined);
|
|
555
|
+
});
|
|
556
|
+
it('disconnect should stop active stream managers', async () => {
|
|
557
|
+
const http = new HttpClient({
|
|
558
|
+
apiKey: 'test-key',
|
|
559
|
+
stream: true,
|
|
560
|
+
pollIntervalMs: 20,
|
|
561
|
+
});
|
|
562
|
+
const agentInstance = new AgentsAPI(http, new FilesAPI(http)).create('my-agent');
|
|
563
|
+
const stopSpy = jest.spyOn(StreamableManager.prototype, 'stop');
|
|
564
|
+
mockFetch.mockImplementation((url) => {
|
|
565
|
+
if (url.includes('/agents/run')) {
|
|
566
|
+
return Promise.resolve({
|
|
567
|
+
ok: true,
|
|
568
|
+
status: 200,
|
|
569
|
+
text: () => Promise.resolve(JSON.stringify({
|
|
570
|
+
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
571
|
+
assistant_message: makeMessage(),
|
|
572
|
+
})),
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
return Promise.resolve(mockNdjsonStream([
|
|
576
|
+
`${JSON.stringify({ event: 'chats', data: { id: 'chat-1', status: ChatStatusIdle } })}\n`,
|
|
577
|
+
]));
|
|
578
|
+
});
|
|
579
|
+
await agentInstance.sendMessage('hello', { onChat: jest.fn() });
|
|
580
|
+
agentInstance.disconnect();
|
|
581
|
+
expect(stopSpy).toHaveBeenCalled();
|
|
582
|
+
stopSpy.mockRestore();
|
|
583
|
+
});
|
|
584
|
+
it('startStreaming should no-op when there is no active chat', () => {
|
|
585
|
+
const agentInstance = agent();
|
|
586
|
+
agentInstance.startStreaming();
|
|
587
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
588
|
+
});
|
|
589
|
+
it('startStreaming should open the chat stream when chatId exists', async () => {
|
|
590
|
+
const http = new HttpClient({
|
|
591
|
+
apiKey: 'test-key',
|
|
592
|
+
stream: true,
|
|
593
|
+
pollIntervalMs: 20,
|
|
594
|
+
});
|
|
595
|
+
const agentInstance = new AgentsAPI(http, new FilesAPI(http)).create('my-agent');
|
|
596
|
+
mockJsonResponse({
|
|
597
|
+
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
598
|
+
assistant_message: makeMessage(),
|
|
599
|
+
});
|
|
600
|
+
mockJsonResponse({ status: ChatStatusBusy });
|
|
601
|
+
mockJsonResponse({
|
|
602
|
+
id: 'chat-1', status: ChatStatusBusy, chat_messages: [],
|
|
603
|
+
});
|
|
604
|
+
mockJsonResponse({ status: ChatStatusIdle });
|
|
605
|
+
mockJsonResponse({
|
|
606
|
+
id: 'chat-1', status: ChatStatusIdle, chat_messages: [],
|
|
607
|
+
});
|
|
608
|
+
await agentInstance.sendMessage('hello', { stream: false });
|
|
609
|
+
jest.clearAllMocks();
|
|
610
|
+
mockFetch.mockResolvedValue(mockNdjsonStream([
|
|
611
|
+
`${JSON.stringify({ event: 'chats', data: { id: 'chat-1', status: ChatStatusIdle } })}\n`,
|
|
612
|
+
]));
|
|
613
|
+
agentInstance.startStreaming({ onChat: jest.fn() });
|
|
614
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/chats/chat-1/stream'), expect.anything());
|
|
615
|
+
});
|
|
320
616
|
it('reset should clear chat state so stopChat is a no-op', async () => {
|
|
321
617
|
const agentInstance = agent();
|
|
322
618
|
mockJsonResponse({
|
|
@@ -337,11 +633,122 @@ describe('Agent lifecycle', () => {
|
|
|
337
633
|
await agentInstance.stopChat();
|
|
338
634
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
339
635
|
});
|
|
636
|
+
it('reset should allow onToolCall to fire again for the same invocation id', async () => {
|
|
637
|
+
const toolInvocation = {
|
|
638
|
+
id: 'tool-inv-reset',
|
|
639
|
+
type: ToolTypeClient,
|
|
640
|
+
status: ToolInvocationStatusAwaitingInput,
|
|
641
|
+
function: { name: 'my_tool', arguments: { x: 1 } },
|
|
642
|
+
};
|
|
643
|
+
const messageWithTool = makeMessage({ tool_invocations: [toolInvocation] });
|
|
644
|
+
const agentInstance = agent();
|
|
645
|
+
const onMessage = jest.fn();
|
|
646
|
+
const onToolCall = jest.fn();
|
|
647
|
+
mockJsonResponse({
|
|
648
|
+
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
649
|
+
assistant_message: makeMessage(),
|
|
650
|
+
});
|
|
651
|
+
mockJsonResponse({ status: ChatStatusBusy });
|
|
652
|
+
mockJsonResponse({
|
|
653
|
+
id: 'chat-1',
|
|
654
|
+
status: ChatStatusBusy,
|
|
655
|
+
chat_messages: [messageWithTool],
|
|
656
|
+
});
|
|
657
|
+
mockJsonResponse({ status: ChatStatusBusy });
|
|
658
|
+
mockJsonResponse({ status: ChatStatusIdle });
|
|
659
|
+
mockJsonResponse({
|
|
660
|
+
id: 'chat-1',
|
|
661
|
+
status: ChatStatusIdle,
|
|
662
|
+
chat_messages: [messageWithTool],
|
|
663
|
+
});
|
|
664
|
+
await agentInstance.sendMessage('run tool', { stream: false, onMessage, onToolCall });
|
|
665
|
+
expect(onToolCall).toHaveBeenCalledTimes(1);
|
|
666
|
+
agentInstance.reset();
|
|
667
|
+
jest.clearAllMocks();
|
|
668
|
+
onToolCall.mockClear();
|
|
669
|
+
mockJsonResponse({
|
|
670
|
+
user_message: makeMessage({ id: 'user-2', role: 'user' }),
|
|
671
|
+
assistant_message: makeMessage({ id: 'asst-2' }),
|
|
672
|
+
});
|
|
673
|
+
mockJsonResponse({ status: ChatStatusBusy });
|
|
674
|
+
mockJsonResponse({
|
|
675
|
+
id: 'chat-1',
|
|
676
|
+
status: ChatStatusBusy,
|
|
677
|
+
chat_messages: [messageWithTool],
|
|
678
|
+
});
|
|
679
|
+
mockJsonResponse({ status: ChatStatusBusy });
|
|
680
|
+
mockJsonResponse({ status: ChatStatusIdle });
|
|
681
|
+
mockJsonResponse({
|
|
682
|
+
id: 'chat-1',
|
|
683
|
+
status: ChatStatusIdle,
|
|
684
|
+
chat_messages: [messageWithTool],
|
|
685
|
+
});
|
|
686
|
+
await agentInstance.sendMessage('run tool again', { stream: false, onMessage, onToolCall });
|
|
687
|
+
expect(onToolCall).toHaveBeenCalledTimes(1);
|
|
688
|
+
expect(onToolCall).toHaveBeenCalledWith({
|
|
689
|
+
id: 'tool-inv-reset',
|
|
690
|
+
name: 'my_tool',
|
|
691
|
+
args: { x: 1 },
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
describe('Agent.getChat', () => {
|
|
696
|
+
beforeEach(() => {
|
|
697
|
+
jest.clearAllMocks();
|
|
698
|
+
});
|
|
699
|
+
const agent = () => {
|
|
700
|
+
const http = new HttpClient({ apiKey: 'test-key' });
|
|
701
|
+
return new AgentsAPI(http, new FilesAPI(http)).create('my-agent');
|
|
702
|
+
};
|
|
703
|
+
it('should return null without a chat id and no active chat', async () => {
|
|
704
|
+
const result = await agent().getChat();
|
|
705
|
+
expect(result).toBeNull();
|
|
706
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
707
|
+
});
|
|
708
|
+
it('should GET /chats/{id} when chatId is provided', async () => {
|
|
709
|
+
const chat = { id: 'chat-42', status: 'idle', chat_messages: [] };
|
|
710
|
+
mockJsonResponse(chat);
|
|
711
|
+
const result = await agent().getChat('chat-42');
|
|
712
|
+
expect(result).toEqual(chat);
|
|
713
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
714
|
+
expect(url).toContain('/chats/chat-42');
|
|
715
|
+
expect(init.method).toBe('GET');
|
|
716
|
+
});
|
|
717
|
+
it('should GET /chats/{id} with established chat id when chatId is omitted', async () => {
|
|
718
|
+
const agentInstance = agent();
|
|
719
|
+
mockJsonResponse({
|
|
720
|
+
user_message: makeMessage({ id: 'user-1', role: 'user' }),
|
|
721
|
+
assistant_message: makeMessage(),
|
|
722
|
+
});
|
|
723
|
+
mockJsonResponse({ status: ChatStatusBusy });
|
|
724
|
+
mockJsonResponse({ id: 'chat-1', status: ChatStatusBusy, chat_messages: [] });
|
|
725
|
+
mockJsonResponse({ status: ChatStatusIdle });
|
|
726
|
+
mockJsonResponse({ id: 'chat-1', status: ChatStatusIdle, chat_messages: [] });
|
|
727
|
+
await agentInstance.sendMessage('hello', { stream: false });
|
|
728
|
+
jest.clearAllMocks();
|
|
729
|
+
const chat = { id: 'chat-1', status: 'idle', chat_messages: [] };
|
|
730
|
+
mockJsonResponse(chat);
|
|
731
|
+
const result = await agentInstance.getChat();
|
|
732
|
+
expect(result).toEqual(chat);
|
|
733
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
734
|
+
expect(url).toContain('/chats/chat-1');
|
|
735
|
+
expect(init.method).toBe('GET');
|
|
736
|
+
});
|
|
340
737
|
});
|
|
341
738
|
describe('Agent.submitToolResult', () => {
|
|
342
739
|
beforeEach(() => {
|
|
343
740
|
jest.clearAllMocks();
|
|
344
741
|
});
|
|
742
|
+
it('should POST plain string results to /tools/{invocationId}', async () => {
|
|
743
|
+
const http = new HttpClient({ apiKey: 'test-key' });
|
|
744
|
+
const agentInstance = new AgentsAPI(http, new FilesAPI(http)).create('my-agent');
|
|
745
|
+
mockJsonResponse(null);
|
|
746
|
+
await agentInstance.submitToolResult('inv-plain', 'done');
|
|
747
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
748
|
+
expect(url).toContain('/tools/inv-plain');
|
|
749
|
+
expect(init.method).toBe('POST');
|
|
750
|
+
expect(JSON.parse(String(init.body))).toEqual({ result: 'done' });
|
|
751
|
+
});
|
|
345
752
|
it('should JSON-stringify structured action results', async () => {
|
|
346
753
|
const http = new HttpClient({ apiKey: 'test-key' });
|
|
347
754
|
const agentInstance = new AgentsAPI(http, new FilesAPI(http)).create('my-agent');
|
|
@@ -356,6 +763,23 @@ describe('Agent.submitToolResult', () => {
|
|
|
356
763
|
expect(body.result).toBe(JSON.stringify(payload));
|
|
357
764
|
});
|
|
358
765
|
});
|
|
766
|
+
describe('AgentsAPI.submitToolResult', () => {
|
|
767
|
+
beforeEach(() => {
|
|
768
|
+
jest.clearAllMocks();
|
|
769
|
+
});
|
|
770
|
+
const api = () => {
|
|
771
|
+
const http = new HttpClient({ apiKey: 'test-key' });
|
|
772
|
+
return new AgentsAPI(http, new FilesAPI(http));
|
|
773
|
+
};
|
|
774
|
+
it('should POST plain string results to /tools/{invocationId}', async () => {
|
|
775
|
+
mockJsonResponse(null);
|
|
776
|
+
await api().submitToolResult('inv-plain', 'done');
|
|
777
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
778
|
+
expect(url).toContain('/tools/inv-plain');
|
|
779
|
+
expect(init.method).toBe('POST');
|
|
780
|
+
expect(JSON.parse(String(init.body))).toEqual({ result: 'done' });
|
|
781
|
+
});
|
|
782
|
+
});
|
|
359
783
|
describe('AgentsAPI (template CRUD)', () => {
|
|
360
784
|
beforeEach(() => {
|
|
361
785
|
jest.clearAllMocks();
|
|
@@ -392,4 +816,80 @@ describe('AgentsAPI (template CRUD)', () => {
|
|
|
392
816
|
expect(init.method).toBe('POST');
|
|
393
817
|
expect(JSON.parse(init.body)).toEqual(payload);
|
|
394
818
|
});
|
|
819
|
+
it('should GET /agents/{namespace}/{name} for getByName()', async () => {
|
|
820
|
+
const agent = { id: 'agent-1', name: 'my-agent' };
|
|
821
|
+
mockJsonResponse(agent);
|
|
822
|
+
const result = await api().getByName('inference', 'my-agent');
|
|
823
|
+
expect(result).toEqual(agent);
|
|
824
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
825
|
+
expect(url).toContain('/agents/inference/my-agent');
|
|
826
|
+
expect(init.method).toBe('GET');
|
|
827
|
+
});
|
|
828
|
+
it('should POST /agents/list for list()', async () => {
|
|
829
|
+
const page = { items: [{ id: 'agent-1' }], next_cursor: null };
|
|
830
|
+
mockJsonResponse(page);
|
|
831
|
+
const result = await api().list({ limit: 10 });
|
|
832
|
+
expect(result).toEqual(page);
|
|
833
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
834
|
+
expect(url).toContain('/agents/list');
|
|
835
|
+
expect(init.method).toBe('POST');
|
|
836
|
+
});
|
|
837
|
+
it('should GET /agents/{id} for get()', async () => {
|
|
838
|
+
const agent = { id: 'agent-1', name: 'support-bot' };
|
|
839
|
+
mockJsonResponse(agent);
|
|
840
|
+
const result = await api().get('agent-1');
|
|
841
|
+
expect(result).toEqual(agent);
|
|
842
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
843
|
+
expect(url).toContain('/agents/agent-1');
|
|
844
|
+
expect(init.method).toBe('GET');
|
|
845
|
+
});
|
|
846
|
+
it('should POST /agents/{id} for update()', async () => {
|
|
847
|
+
const agent = { id: 'agent-1', name: 'updated' };
|
|
848
|
+
mockJsonResponse(agent);
|
|
849
|
+
const result = await api().update('agent-1', { name: 'updated' });
|
|
850
|
+
expect(result).toEqual(agent);
|
|
851
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
852
|
+
expect(JSON.parse(init.body)).toEqual({ name: 'updated' });
|
|
853
|
+
});
|
|
854
|
+
it('should DELETE /agents/{id} for delete()', async () => {
|
|
855
|
+
mockJsonResponse(null);
|
|
856
|
+
await api().delete('agent-1');
|
|
857
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
858
|
+
expect(url).toContain('/agents/agent-1');
|
|
859
|
+
expect(init.method).toBe('DELETE');
|
|
860
|
+
});
|
|
861
|
+
it('should POST /agents/{id}/duplicate for duplicate()', async () => {
|
|
862
|
+
const agent = { id: 'agent-copy' };
|
|
863
|
+
mockJsonResponse(agent);
|
|
864
|
+
const result = await api().duplicate('agent-1');
|
|
865
|
+
expect(result).toEqual(agent);
|
|
866
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
867
|
+
expect(url).toContain('/agents/agent-1/duplicate');
|
|
868
|
+
expect(init.method).toBe('POST');
|
|
869
|
+
});
|
|
870
|
+
it('should POST /agents/{id}/versions/list for listVersions()', async () => {
|
|
871
|
+
const page = { items: [{ id: 'ver-1' }], next_cursor: null };
|
|
872
|
+
mockJsonResponse(page);
|
|
873
|
+
const result = await api().listVersions('agent-1', { limit: 5 });
|
|
874
|
+
expect(result).toEqual(page);
|
|
875
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
876
|
+
expect(url).toContain('/agents/agent-1/versions/list');
|
|
877
|
+
expect(JSON.parse(init.body)).toEqual({ limit: 5 });
|
|
878
|
+
});
|
|
879
|
+
it('should GET /agents/{id}/versions/{versionId} for getVersion()', async () => {
|
|
880
|
+
const version = { id: 'ver-1', agent_id: 'agent-1' };
|
|
881
|
+
mockJsonResponse(version);
|
|
882
|
+
const result = await api().getVersion('agent-1', 'ver-1');
|
|
883
|
+
expect(result).toEqual(version);
|
|
884
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
885
|
+
expect(url).toContain('/agents/agent-1/versions/ver-1');
|
|
886
|
+
expect(init.method).toBe('GET');
|
|
887
|
+
});
|
|
888
|
+
it('should POST visibility for updateVisibility()', async () => {
|
|
889
|
+
const agent = { id: 'agent-1', visibility: 'team' };
|
|
890
|
+
mockJsonResponse(agent);
|
|
891
|
+
await api().updateVisibility('agent-1', 'team');
|
|
892
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
893
|
+
expect(JSON.parse(init.body)).toEqual({ visibility: 'team' });
|
|
894
|
+
});
|
|
395
895
|
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { HttpClient } from '../http/client';
|
|
2
|
+
import { ApiKeyDTO, CursorListRequest, CursorListResponse } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* API Keys API
|
|
5
|
+
*/
|
|
6
|
+
export declare class ApiKeysAPI {
|
|
7
|
+
private readonly http;
|
|
8
|
+
constructor(http: HttpClient);
|
|
9
|
+
/**
|
|
10
|
+
* List API keys with cursor-based pagination
|
|
11
|
+
*/
|
|
12
|
+
list(params?: Partial<CursorListRequest>): Promise<CursorListResponse<ApiKeyDTO>>;
|
|
13
|
+
/**
|
|
14
|
+
* Create an API key
|
|
15
|
+
*/
|
|
16
|
+
create(data: {
|
|
17
|
+
name: string;
|
|
18
|
+
scopes?: string[];
|
|
19
|
+
}): Promise<ApiKeyDTO>;
|
|
20
|
+
/**
|
|
21
|
+
* Delete an API key
|
|
22
|
+
*/
|
|
23
|
+
delete(id: string): Promise<void>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Keys API
|
|
3
|
+
*/
|
|
4
|
+
export class ApiKeysAPI {
|
|
5
|
+
constructor(http) {
|
|
6
|
+
this.http = http;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* List API keys with cursor-based pagination
|
|
10
|
+
*/
|
|
11
|
+
async list(params) {
|
|
12
|
+
return this.http.request('post', '/apikeys/list', { data: params });
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Create an API key
|
|
16
|
+
*/
|
|
17
|
+
async create(data) {
|
|
18
|
+
return this.http.request('post', '/apikeys', { data });
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Delete an API key
|
|
22
|
+
*/
|
|
23
|
+
async delete(id) {
|
|
24
|
+
return this.http.request('delete', `/apikeys/${id}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { HttpClient } from '../http/client';
|
|
2
|
+
import { ApiKeysAPI } from './api-keys';
|
|
3
|
+
const mockFetch = jest.fn();
|
|
4
|
+
global.fetch = mockFetch;
|
|
5
|
+
function mockJsonResponse(body) {
|
|
6
|
+
mockFetch.mockResolvedValueOnce({
|
|
7
|
+
ok: true,
|
|
8
|
+
status: 200,
|
|
9
|
+
text: () => Promise.resolve(JSON.stringify(body)),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
describe('ApiKeysAPI', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
const api = () => new ApiKeysAPI(new HttpClient({ apiKey: 'test-key' }));
|
|
17
|
+
it('should POST /apikeys/list for list()', async () => {
|
|
18
|
+
const page = { items: [{ id: 'key-1', name: 'CI' }], next_cursor: null };
|
|
19
|
+
mockJsonResponse(page);
|
|
20
|
+
const result = await api().list();
|
|
21
|
+
expect(result).toEqual(page);
|
|
22
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
23
|
+
expect(url).toContain('/apikeys/list');
|
|
24
|
+
expect(init.method).toBe('POST');
|
|
25
|
+
});
|
|
26
|
+
it('should POST /apikeys for create()', async () => {
|
|
27
|
+
const payload = { name: 'deploy-bot', scopes: ['tasks:read'] };
|
|
28
|
+
const key = { id: 'key-new', ...payload, key: 'inf_sk_abc' };
|
|
29
|
+
mockJsonResponse(key);
|
|
30
|
+
const result = await api().create(payload);
|
|
31
|
+
expect(result).toEqual(key);
|
|
32
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
33
|
+
expect(url).toContain('/apikeys');
|
|
34
|
+
expect(init.method).toBe('POST');
|
|
35
|
+
expect(JSON.parse(init.body)).toEqual(payload);
|
|
36
|
+
});
|
|
37
|
+
it('should DELETE /apikeys/{id} for delete()', async () => {
|
|
38
|
+
mockJsonResponse(null);
|
|
39
|
+
await api().delete('key-9');
|
|
40
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
41
|
+
expect(url).toContain('/apikeys/key-9');
|
|
42
|
+
expect(init.method).toBe('DELETE');
|
|
43
|
+
});
|
|
44
|
+
});
|
package/dist/api/apps.js
CHANGED
|
@@ -87,7 +87,7 @@ export class AppsAPI {
|
|
|
87
87
|
* Set the current (active) version of an app
|
|
88
88
|
*/
|
|
89
89
|
async setCurrentVersion(appId, versionId) {
|
|
90
|
-
return this.http.request('
|
|
90
|
+
return this.http.request('put', `/apps/${appId}/versions/${versionId}/current`);
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
export function createAppsAPI(http) {
|