@inferencesh/sdk 0.6.8 → 0.6.12

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 (76) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/README.md +70 -1
  3. package/dist/agent/actions.js +3 -3
  4. package/dist/agent/actions.test.js +89 -0
  5. package/dist/agent/api.test.js +86 -8
  6. package/dist/api/agents.d.ts +10 -0
  7. package/dist/api/agents.js +17 -3
  8. package/dist/api/agents.test.js +303 -92
  9. package/dist/api/api-keys.d.ts +24 -0
  10. package/dist/api/api-keys.js +26 -0
  11. package/dist/api/api-keys.test.d.ts +1 -0
  12. package/dist/api/api-keys.test.js +44 -0
  13. package/dist/api/apps.js +1 -1
  14. package/dist/api/apps.test.d.ts +1 -0
  15. package/dist/api/apps.test.js +135 -0
  16. package/dist/api/chats.d.ts +14 -0
  17. package/dist/api/chats.js +18 -0
  18. package/dist/api/chats.test.d.ts +1 -0
  19. package/dist/api/chats.test.js +85 -0
  20. package/dist/api/engines.d.ts +12 -0
  21. package/dist/api/engines.js +18 -0
  22. package/dist/api/engines.test.d.ts +1 -0
  23. package/dist/api/engines.test.js +133 -0
  24. package/dist/api/files.test.js +186 -6
  25. package/dist/api/flow-runs.test.d.ts +1 -0
  26. package/dist/api/flow-runs.test.js +97 -0
  27. package/dist/api/flows.d.ts +4 -0
  28. package/dist/api/flows.js +6 -0
  29. package/dist/api/flows.test.d.ts +1 -0
  30. package/dist/api/flows.test.js +118 -0
  31. package/dist/api/integrations.d.ts +41 -0
  32. package/dist/api/integrations.js +56 -0
  33. package/dist/api/integrations.test.d.ts +1 -0
  34. package/dist/api/integrations.test.js +109 -0
  35. package/dist/api/knowledge.d.ts +112 -0
  36. package/dist/api/knowledge.js +163 -0
  37. package/dist/api/knowledge.test.d.ts +1 -0
  38. package/dist/api/knowledge.test.js +94 -0
  39. package/dist/api/mcp-servers.d.ts +45 -0
  40. package/dist/api/mcp-servers.js +62 -0
  41. package/dist/api/mcp-servers.test.d.ts +1 -0
  42. package/dist/api/mcp-servers.test.js +64 -0
  43. package/dist/api/projects.d.ts +29 -0
  44. package/dist/api/projects.js +38 -0
  45. package/dist/api/projects.test.d.ts +1 -0
  46. package/dist/api/projects.test.js +52 -0
  47. package/dist/api/search.d.ts +21 -0
  48. package/dist/api/search.js +20 -0
  49. package/dist/api/search.test.d.ts +1 -0
  50. package/dist/api/search.test.js +39 -0
  51. package/dist/api/secrets.d.ts +29 -0
  52. package/dist/api/secrets.js +38 -0
  53. package/dist/api/secrets.test.d.ts +1 -0
  54. package/dist/api/secrets.test.js +61 -0
  55. package/dist/api/sessions.test.js +4 -4
  56. package/dist/api/tasks.d.ts +13 -1
  57. package/dist/api/tasks.js +18 -0
  58. package/dist/api/tasks.test.js +174 -22
  59. package/dist/api/teams.d.ts +71 -0
  60. package/dist/api/teams.js +92 -0
  61. package/dist/api/teams.test.d.ts +1 -0
  62. package/dist/api/teams.test.js +79 -0
  63. package/dist/client.test.js +8 -8
  64. package/dist/http/client.d.ts +5 -0
  65. package/dist/http/client.js +46 -48
  66. package/dist/http/client.test.js +296 -13
  67. package/dist/http/errors.test.js +13 -0
  68. package/dist/index.d.ts +25 -0
  69. package/dist/index.js +25 -0
  70. package/dist/proxy/express.test.d.ts +1 -0
  71. package/dist/proxy/express.test.js +106 -0
  72. package/dist/proxy/index.test.js +10 -1
  73. package/dist/tool-builder.test.js +11 -1
  74. package/dist/types.d.ts +793 -23
  75. package/dist/types.js +180 -8
  76. package/package.json +4 -4
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
@@ -7,7 +7,7 @@
7
7
 
8
8
  official javascript/typescript sdk for [inference.sh](https://inference.sh) — the ai agent runtime for serverless ai inference.
9
9
 
10
- run ai models, build ai agents, and deploy generative ai applications with a simple api. access 250+ models including flux, stable diffusion, llms (claude, gpt, gemini), video generation (veo, seedance), and more.
10
+ run ai models, build ai agents, and deploy generative ai applications with a simple api. access models including flux, stable diffusion, llms (claude, gpt, gemini), video generation (veo, seedance), and more.
11
11
 
12
12
  ## Installation
13
13
 
@@ -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:
@@ -4,7 +4,7 @@
4
4
  * Action creators that handle side effects (API calls, streaming).
5
5
  * These are created once per provider instance with access to dispatch.
6
6
  */
7
- import { ToolInvocationStatusAwaitingInput, ToolTypeClient, ChatStatusBusy, } from '../types';
7
+ import { ToolInvocationStatusAwaitingInput, ToolInvocationStatusInProgress, ToolTypeClient, ChatStatusBusy, } from '../types';
8
8
  import { StreamableManager } from '../http/streamable';
9
9
  import { PollManager } from '../http/poll';
10
10
  import { isAdHocConfig, extractClientToolHandlers } from './types';
@@ -35,12 +35,12 @@ export function createActions(ctx) {
35
35
  if (chatId && message.chat_id !== chatId && !message.chat_id.startsWith(chatId))
36
36
  return;
37
37
  dispatch({ type: 'UPDATE_MESSAGE', payload: message });
38
- // Check for client tool invocations that need execution
38
+ // Dispatch client tool handlers when ready (in_progress or awaiting_input for backwards compat)
39
39
  const clientToolHandlers = getClientToolHandlers();
40
40
  if (message.tool_invocations && chatId && clientToolHandlers.size > 0) {
41
41
  for (const invocation of message.tool_invocations) {
42
42
  if (invocation.type === ToolTypeClient &&
43
- invocation.status === ToolInvocationStatusAwaitingInput) {
43
+ (invocation.status === ToolInvocationStatusInProgress || invocation.status === ToolInvocationStatusAwaitingInput)) {
44
44
  // Skip if already dispatched
45
45
  if (dispatchedToolInvocations.has(invocation.id)) {
46
46
  continue;
@@ -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 () => {
@@ -388,6 +391,12 @@ describe('createActions', () => {
388
391
  publicActions.stopGeneration();
389
392
  expect(mockAgentApi.stopChat).toHaveBeenCalledWith(ctx.client, 'chat-short');
390
393
  });
394
+ it('stopGeneration should no-op when there is no chatId', () => {
395
+ const { ctx } = createTestContext({ getChatId: () => null });
396
+ const { publicActions } = createActions(ctx);
397
+ publicActions.stopGeneration();
398
+ expect(mockAgentApi.stopChat).not.toHaveBeenCalled();
399
+ });
391
400
  it('submitToolResult should set error state when API fails', async () => {
392
401
  mockAgentApi.submitToolResult.mockRejectedValueOnce(new Error('submit failed'));
393
402
  const onError = jest.fn();
@@ -400,5 +409,85 @@ describe('createActions', () => {
400
409
  });
401
410
  expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: 'submit failed' }));
402
411
  });
412
+ it('clearError should reset error and connection status to idle', () => {
413
+ const { ctx, dispatch } = createTestContext();
414
+ const { publicActions } = createActions(ctx);
415
+ publicActions.clearError();
416
+ expect(dispatch).toHaveBeenCalledWith({
417
+ type: 'SET_ERROR',
418
+ payload: undefined,
419
+ });
420
+ expect(dispatch).toHaveBeenCalledWith({
421
+ type: 'SET_CONNECTION_STATUS',
422
+ payload: 'idle',
423
+ });
424
+ });
425
+ });
426
+ describe('HIL tool actions', () => {
427
+ it('approveTool should delegate to the API', async () => {
428
+ const { ctx } = createTestContext();
429
+ const { publicActions } = createActions(ctx);
430
+ await publicActions.approveTool('inv-approve');
431
+ expect(mockAgentApi.approveTool).toHaveBeenCalledWith(ctx.client, 'inv-approve');
432
+ });
433
+ it('rejectTool should pass an optional reason', async () => {
434
+ const { ctx } = createTestContext();
435
+ const { publicActions } = createActions(ctx);
436
+ await publicActions.rejectTool('inv-reject', 'unsafe');
437
+ expect(mockAgentApi.rejectTool).toHaveBeenCalledWith(ctx.client, 'inv-reject', 'unsafe');
438
+ });
439
+ it('alwaysAllowTool should no-op without a chatId', async () => {
440
+ const { ctx } = createTestContext({ getChatId: () => null });
441
+ const { publicActions } = createActions(ctx);
442
+ await publicActions.alwaysAllowTool('inv-allow', 'my_tool');
443
+ expect(mockAgentApi.alwaysAllowTool).not.toHaveBeenCalled();
444
+ });
445
+ it('alwaysAllowTool should call API when chatId exists', async () => {
446
+ const { ctx } = createTestContext({ getChatId: () => 'chat-short' });
447
+ const { publicActions } = createActions(ctx);
448
+ await publicActions.alwaysAllowTool('inv-allow', 'my_tool');
449
+ expect(mockAgentApi.alwaysAllowTool).toHaveBeenCalledWith(ctx.client, 'chat-short', 'inv-allow', 'my_tool');
450
+ });
451
+ it('approveTool should set error state when API fails', async () => {
452
+ mockAgentApi.approveTool.mockRejectedValueOnce(new Error('approve failed'));
453
+ const onError = jest.fn();
454
+ const { ctx, dispatch } = createTestContext({ callbacks: { onError } });
455
+ const { publicActions } = createActions(ctx);
456
+ await expect(publicActions.approveTool('inv-1')).rejects.toThrow('approve failed');
457
+ expect(dispatch).toHaveBeenCalledWith({
458
+ type: 'SET_CONNECTION_STATUS',
459
+ payload: 'error',
460
+ });
461
+ expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: 'approve failed' }));
462
+ });
463
+ });
464
+ describe('setChatId', () => {
465
+ it('should no-op when the chat id is unchanged', async () => {
466
+ const { ctx } = createTestContext({ getChatId: () => 'chat-short' });
467
+ const { internalActions } = createActions(ctx);
468
+ internalActions.setChatId('chat-short');
469
+ await Promise.resolve();
470
+ expect(StreamableManager).not.toHaveBeenCalled();
471
+ });
472
+ it('should reset and stop stream when chat id is cleared', async () => {
473
+ const { ctx, dispatch } = createTestContext();
474
+ const { internalActions } = createActions(ctx);
475
+ internalActions.streamChat('chat-full-id-123');
476
+ await Promise.resolve();
477
+ internalActions.setChatId(null);
478
+ expect(streamInstances[0].stop).toHaveBeenCalled();
479
+ expect(dispatch).toHaveBeenCalledWith({ type: 'RESET' });
480
+ });
481
+ it('should start streaming when switching to a new chat id', async () => {
482
+ const { ctx, dispatch } = createTestContext({ getChatId: () => null });
483
+ const { internalActions } = createActions(ctx);
484
+ internalActions.setChatId('chat-new');
485
+ await Promise.resolve();
486
+ expect(dispatch).toHaveBeenCalledWith({
487
+ type: 'SET_CHAT_ID',
488
+ payload: 'chat-new',
489
+ });
490
+ expect(StreamableManager).toHaveBeenCalled();
491
+ });
403
492
  });
404
493
  });
@@ -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();
@@ -144,10 +144,20 @@ export declare class AgentsAPI {
144
144
  * Get a specific agent version
145
145
  */
146
146
  getVersion(agentId: string, versionId: string): Promise<AgentVersionDTO>;
147
+ /**
148
+ * Get an agent by namespace/name (e.g., "inference/my-agent")
149
+ */
150
+ getByName(namespace: string, name: string): Promise<AgentDTO>;
147
151
  /**
148
152
  * Get internal tools for the agent
149
153
  */
150
154
  getInternalTools(): Promise<InternalToolDefinition[]>;
155
+ /**
156
+ * Get A2A protocol agent card for an agent.
157
+ * Returns raw JSON (not wrapped in {data:...}) for direct upload to GCP Producer Portal.
158
+ * Spec: https://a2a-protocol.org/latest/specification/
159
+ */
160
+ getA2ACard(agentId: string): Promise<Record<string, unknown>>;
151
161
  /**
152
162
  * Create an agent instance for chat interactions
153
163
  */
@@ -1,6 +1,6 @@
1
1
  import { StreamableManager } from '../http/streamable';
2
2
  import { PollManager } from '../http/poll';
3
- import { ToolTypeClient, ToolInvocationStatusAwaitingInput, ChatStatusBusy, } from '../types';
3
+ import { ToolTypeClient, ToolInvocationStatusAwaitingInput, ToolInvocationStatusInProgress, ChatStatusBusy, } from '../types';
4
4
  /**
5
5
  * Agent for chat interactions
6
6
  *
@@ -166,7 +166,7 @@ export class Agent {
166
166
  for (const inv of message.tool_invocations) {
167
167
  if (this.dispatchedToolCalls.has(inv.id))
168
168
  continue;
169
- if (inv.type === ToolTypeClient && inv.status === ToolInvocationStatusAwaitingInput) {
169
+ if (inv.type === ToolTypeClient && (inv.status === ToolInvocationStatusInProgress || inv.status === ToolInvocationStatusAwaitingInput)) {
170
170
  this.dispatchedToolCalls.add(inv.id);
171
171
  options.onToolCall({
172
172
  id: inv.id,
@@ -222,7 +222,7 @@ export class Agent {
222
222
  for (const inv of message.tool_invocations) {
223
223
  if (this.dispatchedToolCalls.has(inv.id))
224
224
  continue;
225
- if (inv.type === ToolTypeClient && inv.status === ToolInvocationStatusAwaitingInput) {
225
+ if (inv.type === ToolTypeClient && (inv.status === ToolInvocationStatusInProgress || inv.status === ToolInvocationStatusAwaitingInput)) {
226
226
  this.dispatchedToolCalls.add(inv.id);
227
227
  options.onToolCall({
228
228
  id: inv.id,
@@ -319,12 +319,26 @@ export class AgentsAPI {
319
319
  async getVersion(agentId, versionId) {
320
320
  return this.http.request('get', `/agents/${agentId}/versions/${versionId}`);
321
321
  }
322
+ /**
323
+ * Get an agent by namespace/name (e.g., "inference/my-agent")
324
+ */
325
+ async getByName(namespace, name) {
326
+ return this.http.request('get', `/agents/${namespace}/${name}`);
327
+ }
322
328
  /**
323
329
  * Get internal tools for the agent
324
330
  */
325
331
  async getInternalTools() {
326
332
  return this.http.request('get', '/agents/internal-tools');
327
333
  }
334
+ /**
335
+ * Get A2A protocol agent card for an agent.
336
+ * Returns raw JSON (not wrapped in {data:...}) for direct upload to GCP Producer Portal.
337
+ * Spec: https://a2a-protocol.org/latest/specification/
338
+ */
339
+ async getA2ACard(agentId) {
340
+ return this.http.request('get', `/agents/${agentId}/card`);
341
+ }
328
342
  // ==========================================================================
329
343
  // Agent Runtime (chat interactions)
330
344
  // ==========================================================================