@inferencesh/sdk 0.6.8 → 0.6.10

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/CHANGELOG.md CHANGED
@@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.8] - 2026-05-20
11
+
10
12
  ### Added
11
13
 
14
+ - README tool builder section: `httpTool`/`callTool` auth, `mcpTool`, and builder comparison table
15
+ - `examples/tool-builder.ts` demonstrates HTTP and MCP tool schemas
16
+
12
17
  - Typed SDK constants for integrations: `IntegrationProvider*`, `IntegrationAuthType*`, `IntegrationStatus*`
13
18
  - `IntegrationDTO` fields (`provider`, `type`, `auth`, `status`) now use those typed aliases
14
19
  - Additional `InstanceStatus*` constants (`creating`, `pending_provider`, `error`, `deleting`)
@@ -65,7 +70,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
65
70
  - Configurable reconnection behavior
66
71
  - Comprehensive error handling
67
72
 
68
- [Unreleased]: https://github.com/inference-sh/sdk-js/compare/v0.6.7...HEAD
73
+ [Unreleased]: https://github.com/inference-sh/sdk-js/compare/v0.6.8...HEAD
74
+ [0.6.8]: https://github.com/inference-sh/sdk-js/compare/v0.6.7...v0.6.8
69
75
  [0.6.7]: https://github.com/inference-sh/sdk-js/compare/v0.6.6...v0.6.7
70
76
  [0.1.1]: https://github.com/inference-sh/sdk-js/compare/v0.1.0...v0.1.1
71
77
  [0.1.0]: https://github.com/inference-sh/sdk-js/releases/tag/v0.1.0
package/README.md CHANGED
@@ -340,6 +340,75 @@ await agent.sendMessage('What is the weather in Paris?', {
340
340
 
341
341
  For multi-turn chats, the SDK opens the chat stream before sending the next message so updates are not missed. Use `stopChat()` to cancel in-flight generation (`POST /chats/{id}/stop`), and `reset()` to clear the current chat and start fresh.
342
342
 
343
+ ### Tool builder
344
+
345
+ Use the fluent builders to define `AgentTool` schemas. Client tools (`tool`) run in your app via `onToolCall`; server-side tools run on inference.sh.
346
+
347
+ | Builder | Runs on | Description |
348
+ |---------|---------|-------------|
349
+ | `tool(name)` | Client | Local handler; only the schema is sent to the API |
350
+ | `appTool(name, appRef)` | Server | Invoke another inference app |
351
+ | `agentTool(name, agentRef)` | Server | Delegate to a sub-agent |
352
+ | `httpTool(name, url)` / `callTool(name, url)` | Server | HTTP request with credential injection (preferred over `webhookTool`) |
353
+ | `webhookTool(name, url)` | Server | Unsigned webhook (legacy; use `httpTool` for new tools) |
354
+ | `mcpTool(name, integrationId, toolName)` | Server | Call a tool on a connected MCP integration |
355
+ | `internalTools()` | Server | Built-in plan, memory, and widget tools |
356
+
357
+ ```typescript
358
+ import {
359
+ inference,
360
+ tool,
361
+ appTool,
362
+ httpTool,
363
+ mcpTool,
364
+ internalTools,
365
+ string,
366
+ IntegrationProviderGoogle,
367
+ } from '@inferencesh/sdk';
368
+
369
+ const clientTool = tool('get_weather')
370
+ .describe('Get current weather')
371
+ .param('city', string('City name'))
372
+ .build();
373
+
374
+ // HTTP tool with OAuth integration credentials (injected server-side)
375
+ const gmailSend = httpTool('gmail_send', 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send')
376
+ .describe('Send an email via Gmail')
377
+ .method('POST')
378
+ .auth({ integration: IntegrationProviderGoogle, integrationId: 'your-integration-id' })
379
+ .build();
380
+
381
+ // API key or bearer auth
382
+ const fetchData = httpTool('fetch', 'https://api.example.com/data')
383
+ .method('GET')
384
+ .auth({ apiKey: 'YOUR_KEY', header: 'X-API-Key' }) // default header: X-API-Key
385
+ .header('Accept', 'application/json')
386
+ .build();
387
+
388
+ const bearerFetch = httpTool('bearer_fetch', 'https://api.example.com')
389
+ .auth({ bearer: 'YOUR_TOKEN' })
390
+ .build();
391
+
392
+ const imageGen = appTool('generate_image', 'infsh/flux-schnell@abc123')
393
+ .param('prompt', string('Image description'))
394
+ .requireApproval()
395
+ .build();
396
+
397
+ const mcpSearch = mcpTool('notion_search', 'your-mcp-integration-id', 'search')
398
+ .describe('Search Notion pages')
399
+ .param('query', string('Search query'))
400
+ .build();
401
+
402
+ const agent = client.agents.create({
403
+ core_app: { ref: 'infsh/claude-sonnet-4@latest' },
404
+ system_prompt: 'You are helpful.',
405
+ tools: [clientTool, gmailSend, imageGen, mcpSearch],
406
+ internal_tools: internalTools().memory().build(),
407
+ });
408
+ ```
409
+
410
+ `callTool` is an alias for `httpTool`. Run `npx tsx examples/tool-builder.ts` for more schema examples (no API key required).
411
+
343
412
  ### File attachments
344
413
 
345
414
  Pass files in `sendMessage` options. `Blob` values are uploaded first; objects with a `uri` (already uploaded via `client.files.upload`) are attached as-is:
@@ -91,6 +91,9 @@ describe('createActions', () => {
91
91
  assistantMessage: makeMessage(),
92
92
  });
93
93
  mockAgentApi.submitToolResult.mockResolvedValue(undefined);
94
+ mockAgentApi.approveTool.mockResolvedValue(undefined);
95
+ mockAgentApi.rejectTool.mockResolvedValue(undefined);
96
+ mockAgentApi.alwaysAllowTool.mockResolvedValue(undefined);
94
97
  });
95
98
  describe('updateMessage (via stream listeners)', () => {
96
99
  it('should ignore messages for a different chat when IDs do not prefix-match', async () => {
@@ -400,5 +403,85 @@ describe('createActions', () => {
400
403
  });
401
404
  expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: 'submit failed' }));
402
405
  });
406
+ it('clearError should reset error and connection status to idle', () => {
407
+ const { ctx, dispatch } = createTestContext();
408
+ const { publicActions } = createActions(ctx);
409
+ publicActions.clearError();
410
+ expect(dispatch).toHaveBeenCalledWith({
411
+ type: 'SET_ERROR',
412
+ payload: undefined,
413
+ });
414
+ expect(dispatch).toHaveBeenCalledWith({
415
+ type: 'SET_CONNECTION_STATUS',
416
+ payload: 'idle',
417
+ });
418
+ });
419
+ });
420
+ describe('HIL tool actions', () => {
421
+ it('approveTool should delegate to the API', async () => {
422
+ const { ctx } = createTestContext();
423
+ const { publicActions } = createActions(ctx);
424
+ await publicActions.approveTool('inv-approve');
425
+ expect(mockAgentApi.approveTool).toHaveBeenCalledWith(ctx.client, 'inv-approve');
426
+ });
427
+ it('rejectTool should pass an optional reason', async () => {
428
+ const { ctx } = createTestContext();
429
+ const { publicActions } = createActions(ctx);
430
+ await publicActions.rejectTool('inv-reject', 'unsafe');
431
+ expect(mockAgentApi.rejectTool).toHaveBeenCalledWith(ctx.client, 'inv-reject', 'unsafe');
432
+ });
433
+ it('alwaysAllowTool should no-op without a chatId', async () => {
434
+ const { ctx } = createTestContext({ getChatId: () => null });
435
+ const { publicActions } = createActions(ctx);
436
+ await publicActions.alwaysAllowTool('inv-allow', 'my_tool');
437
+ expect(mockAgentApi.alwaysAllowTool).not.toHaveBeenCalled();
438
+ });
439
+ it('alwaysAllowTool should call API when chatId exists', async () => {
440
+ const { ctx } = createTestContext({ getChatId: () => 'chat-short' });
441
+ const { publicActions } = createActions(ctx);
442
+ await publicActions.alwaysAllowTool('inv-allow', 'my_tool');
443
+ expect(mockAgentApi.alwaysAllowTool).toHaveBeenCalledWith(ctx.client, 'chat-short', 'inv-allow', 'my_tool');
444
+ });
445
+ it('approveTool should set error state when API fails', async () => {
446
+ mockAgentApi.approveTool.mockRejectedValueOnce(new Error('approve failed'));
447
+ const onError = jest.fn();
448
+ const { ctx, dispatch } = createTestContext({ callbacks: { onError } });
449
+ const { publicActions } = createActions(ctx);
450
+ await expect(publicActions.approveTool('inv-1')).rejects.toThrow('approve failed');
451
+ expect(dispatch).toHaveBeenCalledWith({
452
+ type: 'SET_CONNECTION_STATUS',
453
+ payload: 'error',
454
+ });
455
+ expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: 'approve failed' }));
456
+ });
457
+ });
458
+ describe('setChatId', () => {
459
+ it('should no-op when the chat id is unchanged', async () => {
460
+ const { ctx } = createTestContext({ getChatId: () => 'chat-short' });
461
+ const { internalActions } = createActions(ctx);
462
+ internalActions.setChatId('chat-short');
463
+ await Promise.resolve();
464
+ expect(StreamableManager).not.toHaveBeenCalled();
465
+ });
466
+ it('should reset and stop stream when chat id is cleared', async () => {
467
+ const { ctx, dispatch } = createTestContext();
468
+ const { internalActions } = createActions(ctx);
469
+ internalActions.streamChat('chat-full-id-123');
470
+ await Promise.resolve();
471
+ internalActions.setChatId(null);
472
+ expect(streamInstances[0].stop).toHaveBeenCalled();
473
+ expect(dispatch).toHaveBeenCalledWith({ type: 'RESET' });
474
+ });
475
+ it('should start streaming when switching to a new chat id', async () => {
476
+ const { ctx, dispatch } = createTestContext({ getChatId: () => null });
477
+ const { internalActions } = createActions(ctx);
478
+ internalActions.setChatId('chat-new');
479
+ await Promise.resolve();
480
+ expect(dispatch).toHaveBeenCalledWith({
481
+ type: 'SET_CHAT_ID',
482
+ payload: 'chat-new',
483
+ });
484
+ expect(StreamableManager).toHaveBeenCalled();
485
+ });
403
486
  });
404
487
  });
@@ -1,6 +1,6 @@
1
1
  import { HttpClient } from '../http/client';
2
2
  import { FilesAPI } from '../api/files';
3
- import { sendAdHocMessage, sendTemplateMessage, sendMessage, submitToolResult, getChatStreamConfig, } from './api';
3
+ import { sendAdHocMessage, sendTemplateMessage, sendMessage, submitToolResult, approveTool, rejectTool, alwaysAllowTool, fetchChat, stopChat, getChatStreamConfig, } from './api';
4
4
  import { ToolTypeClient } from '../types';
5
5
  const mockFetch = jest.fn();
6
6
  global.fetch = mockFetch;
@@ -30,8 +30,13 @@ describe('agent/api', () => {
30
30
  jest.clearAllMocks();
31
31
  });
32
32
  describe('sendAdHocMessage', () => {
33
+ it('should return null when the API response omits messages', async () => {
34
+ mockJsonResponse({});
35
+ const result = await sendAdHocMessage(makeClient(), adHocConfig, null, 'hello');
36
+ expect(result).toBeNull();
37
+ });
33
38
  it('should strip client tool handlers from the agents/run request body', async () => {
34
- mockJsonResponse({ success: true, data: runResponse });
39
+ mockJsonResponse(runResponse);
35
40
  const handler = jest.fn().mockReturnValue('ok');
36
41
  await sendAdHocMessage(makeClient(), {
37
42
  ...adHocConfig,
@@ -58,8 +63,15 @@ describe('agent/api', () => {
58
63
  });
59
64
  });
60
65
  describe('sendTemplateMessage', () => {
66
+ it('should return null when assistant_message is missing', async () => {
67
+ mockJsonResponse({
68
+ user_message: { id: 'u1', chat_id: 'chat-1', role: 'user' },
69
+ });
70
+ const result = await sendTemplateMessage(makeClient(), { agent: 'agent-1' }, null, 'hello');
71
+ expect(result).toBeNull();
72
+ });
61
73
  it('should omit empty agent field for existing chats', async () => {
62
- mockJsonResponse({ success: true, data: runResponse });
74
+ mockJsonResponse(runResponse);
63
75
  await sendTemplateMessage(makeClient(), { agent: '' }, 'chat-existing', 'hi');
64
76
  const [, init] = mockFetch.mock.calls[0];
65
77
  const body = JSON.parse(String(init.body));
@@ -69,7 +81,7 @@ describe('agent/api', () => {
69
81
  });
70
82
  describe('sendMessage', () => {
71
83
  it('should pass FileRef attachments without uploading', async () => {
72
- mockJsonResponse({ success: true, data: runResponse });
84
+ mockJsonResponse(runResponse);
73
85
  const fileRef = {
74
86
  id: 'f1',
75
87
  uri: 'inf://files/abc',
@@ -82,6 +94,19 @@ describe('agent/api', () => {
82
94
  const body = JSON.parse(String(init.body));
83
95
  expect(body.input.attachments).toEqual([fileRef]);
84
96
  });
97
+ it('should omit attachments when every file upload fails', async () => {
98
+ const client = makeClient();
99
+ const uploadSpy = jest
100
+ .spyOn(client.files, 'upload')
101
+ .mockRejectedValue(new Error('upload failed'));
102
+ mockJsonResponse(runResponse);
103
+ const file = new File(['data'], 'doc.txt', { type: 'text/plain' });
104
+ await sendMessage(client, adHocConfig, null, 'with file', [file]);
105
+ const [, init] = mockFetch.mock.calls[0];
106
+ const body = JSON.parse(String(init.body));
107
+ expect(body.input.attachments).toBeUndefined();
108
+ uploadSpy.mockRestore();
109
+ });
85
110
  it('should upload File inputs before sending', async () => {
86
111
  const fileRecord = {
87
112
  id: 'file-1',
@@ -90,9 +115,9 @@ describe('agent/api', () => {
90
115
  upload_url: 'https://upload.example/put',
91
116
  content_type: 'text/plain',
92
117
  };
93
- mockJsonResponse({ success: true, data: [fileRecord] });
118
+ mockJsonResponse([fileRecord]);
94
119
  mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
95
- mockJsonResponse({ success: true, data: runResponse });
120
+ mockJsonResponse(runResponse);
96
121
  const file = new File(['hello'], 'hello.txt', { type: 'text/plain' });
97
122
  await sendMessage(makeClient(), adHocConfig, null, 'with file', [file]);
98
123
  expect(mockFetch).toHaveBeenCalledTimes(3);
@@ -103,14 +128,14 @@ describe('agent/api', () => {
103
128
  });
104
129
  describe('submitToolResult', () => {
105
130
  it('should wrap string results in { result }', async () => {
106
- mockJsonResponse({ success: true, data: null });
131
+ mockJsonResponse(null);
107
132
  await submitToolResult(makeClient(), 'inv-1', 'done');
108
133
  const [url, init] = mockFetch.mock.calls[0];
109
134
  expect(url).toContain('/tools/inv-1');
110
135
  expect(JSON.parse(String(init.body))).toEqual({ result: 'done' });
111
136
  });
112
137
  it('should pass structured action objects through unchanged', async () => {
113
- mockJsonResponse({ success: true, data: null });
138
+ mockJsonResponse(null);
114
139
  const payload = {
115
140
  action: { type: 'approve', payload: { ok: true } },
116
141
  };
@@ -119,6 +144,59 @@ describe('agent/api', () => {
119
144
  expect(JSON.parse(String(init.body))).toEqual(payload);
120
145
  });
121
146
  });
147
+ describe('fetchChat', () => {
148
+ it('should return chat data on success', async () => {
149
+ const chat = { id: 'chat-1', status: 'idle' };
150
+ mockJsonResponse(chat);
151
+ const result = await fetchChat(makeClient(), 'chat-1');
152
+ expect(result).toEqual(chat);
153
+ });
154
+ it('should return null and not throw when the request fails', async () => {
155
+ mockFetch.mockRejectedValueOnce(new Error('network error'));
156
+ const result = await fetchChat(makeClient(), 'chat-1');
157
+ expect(result).toBeNull();
158
+ });
159
+ });
160
+ describe('stopChat', () => {
161
+ it('should POST to /chats/{id}/stop', async () => {
162
+ mockJsonResponse(null);
163
+ await stopChat(makeClient(), 'chat-1');
164
+ const [url, init] = mockFetch.mock.calls[0];
165
+ expect(url).toContain('/chats/chat-1/stop');
166
+ expect(init.method).toBe('POST');
167
+ });
168
+ it('should swallow errors without throwing', async () => {
169
+ mockFetch.mockRejectedValueOnce(new Error('stop failed'));
170
+ await expect(stopChat(makeClient(), 'chat-1')).resolves.toBeUndefined();
171
+ });
172
+ });
173
+ describe('HIL tool approval', () => {
174
+ it('approveTool should POST to /tools/{id}/invoke', async () => {
175
+ mockJsonResponse(null);
176
+ await approveTool(makeClient(), 'inv-approve');
177
+ const [url, init] = mockFetch.mock.calls[0];
178
+ expect(url).toContain('/tools/inv-approve/invoke');
179
+ expect(init.method).toBe('POST');
180
+ });
181
+ it('rejectTool should POST reason to /tools/{id}/reject', async () => {
182
+ mockJsonResponse(null);
183
+ await rejectTool(makeClient(), 'inv-reject', 'not safe');
184
+ const [url, init] = mockFetch.mock.calls[0];
185
+ expect(url).toContain('/tools/inv-reject/reject');
186
+ expect(JSON.parse(String(init.body))).toEqual({ reason: 'not safe' });
187
+ });
188
+ it('alwaysAllowTool should POST tool_name to the chat tools endpoint', async () => {
189
+ mockJsonResponse(null);
190
+ await alwaysAllowTool(makeClient(), 'chat-1', 'inv-allow', 'browser_tool');
191
+ const [url, init] = mockFetch.mock.calls[0];
192
+ expect(url).toContain('/chats/chat-1/tools/inv-allow/always-allow');
193
+ expect(JSON.parse(String(init.body))).toEqual({ tool_name: 'browser_tool' });
194
+ });
195
+ it('should rethrow when approveTool request fails', async () => {
196
+ mockFetch.mockRejectedValueOnce(new Error('approve failed'));
197
+ await expect(approveTool(makeClient(), 'inv-1')).rejects.toThrow('approve failed');
198
+ });
199
+ });
122
200
  describe('getChatStreamConfig', () => {
123
201
  it('should delegate to HttpClient.getStreamableConfig for the chat stream path', () => {
124
202
  const client = makeClient();
@@ -53,18 +53,15 @@ describe('Agent.sendMessage (polling mode)', () => {
53
53
  const userMessage = makeMessage({ id: 'user-1', role: 'user' });
54
54
  const assistantMessage = makeMessage({ id: 'asst-1' });
55
55
  mockJsonResponse({
56
- success: true,
57
- data: { user_message: userMessage, assistant_message: assistantMessage },
56
+ user_message: userMessage, assistant_message: assistantMessage,
58
57
  });
59
- mockJsonResponse({ success: true, data: { status: ChatStatusBusy } });
58
+ mockJsonResponse({ status: ChatStatusBusy });
60
59
  mockJsonResponse({
61
- success: true,
62
- data: { id: 'chat-1', status: ChatStatusBusy, chat_messages: [] },
60
+ id: 'chat-1', status: ChatStatusBusy, chat_messages: [],
63
61
  });
64
- mockJsonResponse({ success: true, data: { status: ChatStatusIdle } });
62
+ mockJsonResponse({ status: ChatStatusIdle });
65
63
  mockJsonResponse({
66
- success: true,
67
- data: { id: 'chat-1', status: ChatStatusIdle, chat_messages: [] },
64
+ id: 'chat-1', status: ChatStatusIdle, chat_messages: [],
68
65
  });
69
66
  const onChat = jest.fn();
70
67
  const result = await agent().sendMessage('hello', { stream: false, onChat });
@@ -81,27 +78,20 @@ describe('Agent.sendMessage (polling mode)', () => {
81
78
  };
82
79
  const messageWithTool = makeMessage({ tool_invocations: [toolInvocation] });
83
80
  mockJsonResponse({
84
- success: true,
85
- data: {
86
- user_message: makeMessage({ id: 'user-1', role: 'user' }),
87
- assistant_message: makeMessage(),
88
- },
81
+ user_message: makeMessage({ id: 'user-1', role: 'user' }),
82
+ assistant_message: makeMessage(),
89
83
  });
90
- mockJsonResponse({ success: true, data: { status: ChatStatusBusy } });
84
+ mockJsonResponse({ status: ChatStatusBusy });
91
85
  mockJsonResponse({
92
- success: true,
93
- data: {
94
- id: 'chat-1',
95
- status: ChatStatusBusy,
96
- chat_messages: [messageWithTool],
97
- },
86
+ id: 'chat-1',
87
+ status: ChatStatusBusy,
88
+ chat_messages: [messageWithTool],
98
89
  });
99
90
  // Same status again — stub poll should not re-dispatch tool
100
- mockJsonResponse({ success: true, data: { status: ChatStatusBusy } });
101
- mockJsonResponse({ success: true, data: { status: ChatStatusIdle } });
91
+ mockJsonResponse({ status: ChatStatusBusy });
92
+ mockJsonResponse({ status: ChatStatusIdle });
102
93
  mockJsonResponse({
103
- success: true,
104
- data: { id: 'chat-1', status: ChatStatusIdle, chat_messages: [messageWithTool] },
94
+ id: 'chat-1', status: ChatStatusIdle, chat_messages: [messageWithTool],
105
95
  });
106
96
  const onMessage = jest.fn();
107
97
  const onToolCall = jest.fn();
@@ -117,22 +107,18 @@ describe('Agent.sendMessage (polling mode)', () => {
117
107
  const userMessage = makeMessage({ id: 'user-1', role: 'user' });
118
108
  const assistantMessage = makeMessage();
119
109
  mockJsonResponse({
120
- success: true,
121
- data: { user_message: userMessage, assistant_message: assistantMessage },
110
+ user_message: userMessage, assistant_message: assistantMessage,
122
111
  });
123
- mockJsonResponse({ success: true, data: { status: ChatStatusBusy } });
112
+ mockJsonResponse({ status: ChatStatusBusy });
124
113
  mockJsonResponse({
125
- success: true,
126
- data: { id: 'chat-1', status: ChatStatusBusy },
114
+ id: 'chat-1', status: ChatStatusBusy,
127
115
  });
128
- mockJsonResponse({ success: true, data: { status: ChatStatusIdle } });
116
+ mockJsonResponse({ status: ChatStatusIdle });
129
117
  mockJsonResponse({
130
- success: true,
131
- data: { id: 'chat-1', status: ChatStatusIdle },
118
+ id: 'chat-1', status: ChatStatusIdle,
132
119
  });
133
120
  mockJsonResponse({
134
- success: true,
135
- data: { id: 'chat-1', status: ChatStatusIdle, output: { answer: 42 } },
121
+ id: 'chat-1', status: ChatStatusIdle, output: { answer: 42 },
136
122
  });
137
123
  const output = await agent().run('compute');
138
124
  expect(output).toEqual({ answer: 42 });
@@ -155,8 +141,7 @@ describe('Agent.sendMessage (streaming mode)', () => {
155
141
  ok: true,
156
142
  status: 200,
157
143
  text: () => Promise.resolve(JSON.stringify({
158
- success: true,
159
- data: { user_message: userMessage, assistant_message: assistantMessage },
144
+ user_message: userMessage, assistant_message: assistantMessage,
160
145
  })),
161
146
  });
162
147
  }
@@ -184,11 +169,8 @@ describe('Agent.sendMessage (streaming mode)', () => {
184
169
  ok: true,
185
170
  status: 200,
186
171
  text: () => Promise.resolve(JSON.stringify({
187
- success: true,
188
- data: {
189
- user_message: makeMessage({ id: 'user-1', role: 'user' }),
190
- assistant_message: makeMessage(),
191
- },
172
+ user_message: makeMessage({ id: 'user-1', role: 'user' }),
173
+ assistant_message: makeMessage(),
192
174
  })),
193
175
  });
194
176
  }
@@ -214,21 +196,16 @@ describe('Agent.sendMessage (streaming mode)', () => {
214
196
  });
215
197
  const agentInstance = new AgentsAPI(http, new FilesAPI(http)).create('my-agent');
216
198
  mockJsonResponse({
217
- success: true,
218
- data: {
219
- user_message: makeMessage({ id: 'user-1', role: 'user' }),
220
- assistant_message: makeMessage(),
221
- },
199
+ user_message: makeMessage({ id: 'user-1', role: 'user' }),
200
+ assistant_message: makeMessage(),
222
201
  });
223
- mockJsonResponse({ success: true, data: { status: ChatStatusBusy } });
202
+ mockJsonResponse({ status: ChatStatusBusy });
224
203
  mockJsonResponse({
225
- success: true,
226
- data: { id: 'chat-1', status: ChatStatusBusy, chat_messages: [] },
204
+ id: 'chat-1', status: ChatStatusBusy, chat_messages: [],
227
205
  });
228
- mockJsonResponse({ success: true, data: { status: ChatStatusIdle } });
206
+ mockJsonResponse({ status: ChatStatusIdle });
229
207
  mockJsonResponse({
230
- success: true,
231
- data: { id: 'chat-1', status: ChatStatusIdle, chat_messages: [] },
208
+ id: 'chat-1', status: ChatStatusIdle, chat_messages: [],
232
209
  });
233
210
  await agentInstance.sendMessage('first', { stream: false });
234
211
  const callOrder = [];
@@ -239,11 +216,8 @@ describe('Agent.sendMessage (streaming mode)', () => {
239
216
  ok: true,
240
217
  status: 200,
241
218
  text: () => Promise.resolve(JSON.stringify({
242
- success: true,
243
- data: {
244
- user_message: makeMessage({ id: 'user-2', role: 'user' }),
245
- assistant_message: makeMessage({ id: 'asst-2' }),
246
- },
219
+ user_message: makeMessage({ id: 'user-2', role: 'user' }),
220
+ assistant_message: makeMessage({ id: 'asst-2' }),
247
221
  })),
248
222
  });
249
223
  }
@@ -285,21 +259,16 @@ describe('Agent.sendMessage (file attachments)', () => {
285
259
  content_type: 'application/pdf',
286
260
  };
287
261
  mockJsonResponse({
288
- success: true,
289
- data: {
290
- user_message: makeMessage({ id: 'user-1', role: 'user' }),
291
- assistant_message: makeMessage(),
292
- },
262
+ user_message: makeMessage({ id: 'user-1', role: 'user' }),
263
+ assistant_message: makeMessage(),
293
264
  });
294
- mockJsonResponse({ success: true, data: { status: ChatStatusBusy } });
265
+ mockJsonResponse({ status: ChatStatusBusy });
295
266
  mockJsonResponse({
296
- success: true,
297
- data: { id: 'chat-1', status: ChatStatusBusy, chat_messages: [] },
267
+ id: 'chat-1', status: ChatStatusBusy, chat_messages: [],
298
268
  });
299
- mockJsonResponse({ success: true, data: { status: ChatStatusIdle } });
269
+ mockJsonResponse({ status: ChatStatusIdle });
300
270
  mockJsonResponse({
301
- success: true,
302
- data: { id: 'chat-1', status: ChatStatusIdle, chat_messages: [] },
271
+ id: 'chat-1', status: ChatStatusIdle, chat_messages: [],
303
272
  });
304
273
  await agent().sendMessage('see attachments', {
305
274
  stream: false,
@@ -331,46 +300,36 @@ describe('Agent lifecycle', () => {
331
300
  it('stopChat should POST to /chats/{id}/stop when a chat exists', async () => {
332
301
  const agentInstance = agent();
333
302
  mockJsonResponse({
334
- success: true,
335
- data: {
336
- user_message: makeMessage({ id: 'user-1', role: 'user' }),
337
- assistant_message: makeMessage(),
338
- },
303
+ user_message: makeMessage({ id: 'user-1', role: 'user' }),
304
+ assistant_message: makeMessage(),
339
305
  });
340
- mockJsonResponse({ success: true, data: { status: ChatStatusBusy } });
306
+ mockJsonResponse({ status: ChatStatusBusy });
341
307
  mockJsonResponse({
342
- success: true,
343
- data: { id: 'chat-1', status: ChatStatusBusy, chat_messages: [] },
308
+ id: 'chat-1', status: ChatStatusBusy, chat_messages: [],
344
309
  });
345
- mockJsonResponse({ success: true, data: { status: ChatStatusIdle } });
310
+ mockJsonResponse({ status: ChatStatusIdle });
346
311
  mockJsonResponse({
347
- success: true,
348
- data: { id: 'chat-1', status: ChatStatusIdle, chat_messages: [] },
312
+ id: 'chat-1', status: ChatStatusIdle, chat_messages: [],
349
313
  });
350
314
  await agentInstance.sendMessage('hello', { stream: false });
351
315
  jest.clearAllMocks();
352
- mockJsonResponse({ success: true, data: null });
316
+ mockJsonResponse(null);
353
317
  await agentInstance.stopChat();
354
318
  expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/chats/chat-1/stop'), expect.anything());
355
319
  });
356
320
  it('reset should clear chat state so stopChat is a no-op', async () => {
357
321
  const agentInstance = agent();
358
322
  mockJsonResponse({
359
- success: true,
360
- data: {
361
- user_message: makeMessage({ id: 'user-1', role: 'user' }),
362
- assistant_message: makeMessage(),
363
- },
323
+ user_message: makeMessage({ id: 'user-1', role: 'user' }),
324
+ assistant_message: makeMessage(),
364
325
  });
365
- mockJsonResponse({ success: true, data: { status: ChatStatusBusy } });
326
+ mockJsonResponse({ status: ChatStatusBusy });
366
327
  mockJsonResponse({
367
- success: true,
368
- data: { id: 'chat-1', status: ChatStatusBusy, chat_messages: [] },
328
+ id: 'chat-1', status: ChatStatusBusy, chat_messages: [],
369
329
  });
370
- mockJsonResponse({ success: true, data: { status: ChatStatusIdle } });
330
+ mockJsonResponse({ status: ChatStatusIdle });
371
331
  mockJsonResponse({
372
- success: true,
373
- data: { id: 'chat-1', status: ChatStatusIdle, chat_messages: [] },
332
+ id: 'chat-1', status: ChatStatusIdle, chat_messages: [],
374
333
  });
375
334
  await agentInstance.sendMessage('hello', { stream: false });
376
335
  agentInstance.reset();
@@ -386,7 +345,7 @@ describe('Agent.submitToolResult', () => {
386
345
  it('should JSON-stringify structured action results', async () => {
387
346
  const http = new HttpClient({ apiKey: 'test-key' });
388
347
  const agentInstance = new AgentsAPI(http, new FilesAPI(http)).create('my-agent');
389
- mockJsonResponse({ success: true, data: null });
348
+ mockJsonResponse(null);
390
349
  const payload = {
391
350
  action: { type: 'form_submit', payload: { field: 'value' } },
392
351
  form_data: { field: 'value' },
@@ -397,3 +356,40 @@ describe('Agent.submitToolResult', () => {
397
356
  expect(body.result).toBe(JSON.stringify(payload));
398
357
  });
399
358
  });
359
+ describe('AgentsAPI (template CRUD)', () => {
360
+ beforeEach(() => {
361
+ jest.clearAllMocks();
362
+ });
363
+ const api = () => {
364
+ const http = new HttpClient({ apiKey: 'test-key' });
365
+ return new AgentsAPI(http, new FilesAPI(http));
366
+ };
367
+ it('should GET /agents/internal-tools for getInternalTools()', async () => {
368
+ const tools = [{ name: 'search', description: 'Search the web' }];
369
+ mockJsonResponse(tools);
370
+ const result = await api().getInternalTools();
371
+ expect(result).toEqual(tools);
372
+ const [url, init] = mockFetch.mock.calls[0];
373
+ expect(url).toContain('/agents/internal-tools');
374
+ expect(init.method).toBe('GET');
375
+ });
376
+ it('should POST team_id for transferOwnership()', async () => {
377
+ const agent = { id: 'agent-1' };
378
+ mockJsonResponse(agent);
379
+ await api().transferOwnership('agent-1', 'team-42');
380
+ const [url, init] = mockFetch.mock.calls[0];
381
+ expect(url).toContain('/agents/agent-1/transfer');
382
+ expect(JSON.parse(init.body)).toEqual({ team_id: 'team-42' });
383
+ });
384
+ it('should POST /agents for createAgent()', async () => {
385
+ const payload = { name: 'support-bot', core_app: { ref: 'app/ref' } };
386
+ const created = { id: 'agent-new', ...payload };
387
+ mockJsonResponse(created);
388
+ const result = await api().createAgent(payload);
389
+ expect(result).toEqual(created);
390
+ const [url, init] = mockFetch.mock.calls[0];
391
+ expect(url).toContain('/agents');
392
+ expect(init.method).toBe('POST');
393
+ expect(JSON.parse(init.body)).toEqual(payload);
394
+ });
395
+ });
@@ -0,0 +1 @@
1
+ export {};