@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 +7 -1
- package/README.md +69 -0
- package/dist/agent/actions.test.js +83 -0
- package/dist/agent/api.test.js +86 -8
- package/dist/api/agents.test.js +88 -92
- package/dist/api/apps.test.d.ts +1 -0
- package/dist/api/apps.test.js +67 -0
- package/dist/api/chats.test.d.ts +1 -0
- package/dist/api/chats.test.js +33 -0
- package/dist/api/engines.test.d.ts +1 -0
- package/dist/api/engines.test.js +55 -0
- package/dist/api/files.test.js +3 -6
- package/dist/api/flow-runs.test.d.ts +1 -0
- package/dist/api/flow-runs.test.js +55 -0
- package/dist/api/flows.test.d.ts +1 -0
- package/dist/api/flows.test.js +43 -0
- package/dist/api/sessions.test.js +4 -4
- package/dist/api/tasks.test.js +40 -22
- package/dist/client.test.js +8 -8
- package/dist/http/client.js +5 -26
- package/dist/http/client.test.js +51 -13
- package/dist/proxy/express.test.d.ts +1 -0
- package/dist/proxy/express.test.js +106 -0
- package/dist/proxy/index.test.js +10 -1
- package/dist/types.d.ts +683 -18
- package/dist/types.js +122 -6
- package/package.json +1 -1
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.
|
|
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
|
});
|
package/dist/agent/api.test.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
118
|
+
mockJsonResponse([fileRecord]);
|
|
94
119
|
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
95
|
-
mockJsonResponse(
|
|
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(
|
|
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(
|
|
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();
|
package/dist/api/agents.test.js
CHANGED
|
@@ -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
|
-
|
|
57
|
-
data: { user_message: userMessage, assistant_message: assistantMessage },
|
|
56
|
+
user_message: userMessage, assistant_message: assistantMessage,
|
|
58
57
|
});
|
|
59
|
-
mockJsonResponse({
|
|
58
|
+
mockJsonResponse({ status: ChatStatusBusy });
|
|
60
59
|
mockJsonResponse({
|
|
61
|
-
|
|
62
|
-
data: { id: 'chat-1', status: ChatStatusBusy, chat_messages: [] },
|
|
60
|
+
id: 'chat-1', status: ChatStatusBusy, chat_messages: [],
|
|
63
61
|
});
|
|
64
|
-
mockJsonResponse({
|
|
62
|
+
mockJsonResponse({ status: ChatStatusIdle });
|
|
65
63
|
mockJsonResponse({
|
|
66
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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({
|
|
84
|
+
mockJsonResponse({ status: ChatStatusBusy });
|
|
91
85
|
mockJsonResponse({
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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({
|
|
101
|
-
mockJsonResponse({
|
|
91
|
+
mockJsonResponse({ status: ChatStatusBusy });
|
|
92
|
+
mockJsonResponse({ status: ChatStatusIdle });
|
|
102
93
|
mockJsonResponse({
|
|
103
|
-
|
|
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
|
-
|
|
121
|
-
data: { user_message: userMessage, assistant_message: assistantMessage },
|
|
110
|
+
user_message: userMessage, assistant_message: assistantMessage,
|
|
122
111
|
});
|
|
123
|
-
mockJsonResponse({
|
|
112
|
+
mockJsonResponse({ status: ChatStatusBusy });
|
|
124
113
|
mockJsonResponse({
|
|
125
|
-
|
|
126
|
-
data: { id: 'chat-1', status: ChatStatusBusy },
|
|
114
|
+
id: 'chat-1', status: ChatStatusBusy,
|
|
127
115
|
});
|
|
128
|
-
mockJsonResponse({
|
|
116
|
+
mockJsonResponse({ status: ChatStatusIdle });
|
|
129
117
|
mockJsonResponse({
|
|
130
|
-
|
|
131
|
-
data: { id: 'chat-1', status: ChatStatusIdle },
|
|
118
|
+
id: 'chat-1', status: ChatStatusIdle,
|
|
132
119
|
});
|
|
133
120
|
mockJsonResponse({
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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({
|
|
202
|
+
mockJsonResponse({ status: ChatStatusBusy });
|
|
224
203
|
mockJsonResponse({
|
|
225
|
-
|
|
226
|
-
data: { id: 'chat-1', status: ChatStatusBusy, chat_messages: [] },
|
|
204
|
+
id: 'chat-1', status: ChatStatusBusy, chat_messages: [],
|
|
227
205
|
});
|
|
228
|
-
mockJsonResponse({
|
|
206
|
+
mockJsonResponse({ status: ChatStatusIdle });
|
|
229
207
|
mockJsonResponse({
|
|
230
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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({
|
|
265
|
+
mockJsonResponse({ status: ChatStatusBusy });
|
|
295
266
|
mockJsonResponse({
|
|
296
|
-
|
|
297
|
-
data: { id: 'chat-1', status: ChatStatusBusy, chat_messages: [] },
|
|
267
|
+
id: 'chat-1', status: ChatStatusBusy, chat_messages: [],
|
|
298
268
|
});
|
|
299
|
-
mockJsonResponse({
|
|
269
|
+
mockJsonResponse({ status: ChatStatusIdle });
|
|
300
270
|
mockJsonResponse({
|
|
301
|
-
|
|
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
|
-
|
|
335
|
-
|
|
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({
|
|
306
|
+
mockJsonResponse({ status: ChatStatusBusy });
|
|
341
307
|
mockJsonResponse({
|
|
342
|
-
|
|
343
|
-
data: { id: 'chat-1', status: ChatStatusBusy, chat_messages: [] },
|
|
308
|
+
id: 'chat-1', status: ChatStatusBusy, chat_messages: [],
|
|
344
309
|
});
|
|
345
|
-
mockJsonResponse({
|
|
310
|
+
mockJsonResponse({ status: ChatStatusIdle });
|
|
346
311
|
mockJsonResponse({
|
|
347
|
-
|
|
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(
|
|
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
|
-
|
|
360
|
-
|
|
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({
|
|
326
|
+
mockJsonResponse({ status: ChatStatusBusy });
|
|
366
327
|
mockJsonResponse({
|
|
367
|
-
|
|
368
|
-
data: { id: 'chat-1', status: ChatStatusBusy, chat_messages: [] },
|
|
328
|
+
id: 'chat-1', status: ChatStatusBusy, chat_messages: [],
|
|
369
329
|
});
|
|
370
|
-
mockJsonResponse({
|
|
330
|
+
mockJsonResponse({ status: ChatStatusIdle });
|
|
371
331
|
mockJsonResponse({
|
|
372
|
-
|
|
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(
|
|
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 {};
|