@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.
Files changed (75) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/actions.js +3 -3
  3. package/dist/agent/actions.test.js +231 -1
  4. package/dist/agent/api.test.js +18 -1
  5. package/dist/api/agents.d.ts +10 -0
  6. package/dist/api/agents.js +17 -3
  7. package/dist/api/agents.test.js +500 -0
  8. package/dist/api/api-keys.d.ts +24 -0
  9. package/dist/api/api-keys.js +26 -0
  10. package/dist/api/api-keys.test.d.ts +1 -0
  11. package/dist/api/api-keys.test.js +44 -0
  12. package/dist/api/apps.js +1 -1
  13. package/dist/api/apps.test.js +71 -3
  14. package/dist/api/chats.d.ts +14 -0
  15. package/dist/api/chats.js +18 -0
  16. package/dist/api/chats.test.js +52 -0
  17. package/dist/api/engines.d.ts +12 -0
  18. package/dist/api/engines.js +18 -0
  19. package/dist/api/engines.test.js +78 -0
  20. package/dist/api/files.test.js +183 -0
  21. package/dist/api/flow-runs.test.js +42 -0
  22. package/dist/api/flows.d.ts +4 -0
  23. package/dist/api/flows.js +6 -0
  24. package/dist/api/flows.test.js +75 -0
  25. package/dist/api/integrations.d.ts +41 -0
  26. package/dist/api/integrations.js +56 -0
  27. package/dist/api/integrations.test.d.ts +1 -0
  28. package/dist/api/integrations.test.js +109 -0
  29. package/dist/api/knowledge.d.ts +112 -0
  30. package/dist/api/knowledge.js +163 -0
  31. package/dist/api/knowledge.test.d.ts +1 -0
  32. package/dist/api/knowledge.test.js +238 -0
  33. package/dist/api/mcp-servers.d.ts +45 -0
  34. package/dist/api/mcp-servers.js +62 -0
  35. package/dist/api/mcp-servers.test.d.ts +1 -0
  36. package/dist/api/mcp-servers.test.js +98 -0
  37. package/dist/api/projects.d.ts +29 -0
  38. package/dist/api/projects.js +38 -0
  39. package/dist/api/projects.test.d.ts +1 -0
  40. package/dist/api/projects.test.js +61 -0
  41. package/dist/api/search.d.ts +21 -0
  42. package/dist/api/search.js +20 -0
  43. package/dist/api/search.test.d.ts +1 -0
  44. package/dist/api/search.test.js +39 -0
  45. package/dist/api/secrets.d.ts +29 -0
  46. package/dist/api/secrets.js +38 -0
  47. package/dist/api/secrets.test.d.ts +1 -0
  48. package/dist/api/secrets.test.js +61 -0
  49. package/dist/api/sessions.test.js +12 -0
  50. package/dist/api/tasks.d.ts +13 -1
  51. package/dist/api/tasks.js +18 -0
  52. package/dist/api/tasks.test.js +171 -0
  53. package/dist/api/teams.d.ts +71 -0
  54. package/dist/api/teams.js +92 -0
  55. package/dist/api/teams.test.d.ts +1 -0
  56. package/dist/api/teams.test.js +139 -0
  57. package/dist/client.test.js +112 -1
  58. package/dist/http/client.d.ts +5 -0
  59. package/dist/http/client.js +42 -23
  60. package/dist/http/client.test.js +245 -0
  61. package/dist/http/errors.test.js +13 -0
  62. package/dist/index.d.ts +25 -0
  63. package/dist/index.js +25 -0
  64. package/dist/proxy/hono.test.d.ts +1 -0
  65. package/dist/proxy/hono.test.js +90 -0
  66. package/dist/proxy/nextjs.test.d.ts +1 -0
  67. package/dist/proxy/nextjs.test.js +125 -0
  68. package/dist/proxy/remix.test.d.ts +1 -0
  69. package/dist/proxy/remix.test.js +66 -0
  70. package/dist/proxy/svelte.test.d.ts +1 -0
  71. package/dist/proxy/svelte.test.js +69 -0
  72. package/dist/tool-builder.test.js +11 -1
  73. package/dist/types.d.ts +110 -5
  74. package/dist/types.js +58 -2
  75. package/package.json +6 -6
@@ -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('post', `/apps/${appId}/current-version`, { data: { version_id: versionId } });
90
+ return this.http.request('put', `/apps/${appId}/versions/${versionId}/current`);
91
91
  }
92
92
  }
93
93
  export function createAppsAPI(http) {