@inferencesh/sdk 0.23.0 → 0.24.0

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,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - Client tools can return files. A `ClientToolHandlerFn` may return `{ result: string; attachments?: FileRef[] }` (new `ClientToolResult` type) besides a string; the files are uploaded first with POST /files (`client.files.upload`, the chat's `uploadFile`) and sent as `attachments` with the result. Images reach the model as image input when it supports images. `submitToolResult` (agent api, `Agent`, `AgentsAPI`, the React chat actions) takes optional `attachments`, sent only when non-empty. `ToolResultRequest.attachments` is added by hand to the generated types until the next `make types` picks it up from the API. String returns work as before.
13
+
10
14
  ## [0.23.0] - 2026-10-11
11
15
 
12
16
  ### Changed
@@ -16,6 +16,15 @@ import { isInferenceError } from '../http/errors.js';
16
16
  // =============================================================================
17
17
  // Action Creators
18
18
  // =============================================================================
19
+ /** Up to about ten minutes of backoff (1s doubling to 30s, jittered) before a tool result is given up. */
20
+ const TOOL_RESULT_MAX_ATTEMPTS = 25;
21
+ /** A failure a retry can get past: no response at all, a 5xx, a 408 or a 429. */
22
+ function isTransient(err) {
23
+ const status = err?.statusCode;
24
+ if (typeof status !== 'number')
25
+ return true;
26
+ return status >= 500 || status === 408 || status === 429;
27
+ }
19
28
  export function createActions(ctx) {
20
29
  const dispatchedToolInvocations = new Set();
21
30
  const { client, dispatch, getState, getConfig, getChatId, getClientToolHandlers, getStreamManager, setStreamManager, getStreamEnabled, getPollIntervalMs, callbacks } = ctx;
@@ -24,6 +33,28 @@ export function createActions(ctx) {
24
33
  // is no longer current (stopped, reset, switched to another chat) leaves
25
34
  // state alone, including work it started before that.
26
35
  let streamGeneration = 0;
36
+ // A client tool's result is the only way its turn continues: the server
37
+ // waits on it. A result lost to a network drop left the turn stuck on
38
+ // "running" for good, so it is sent again until it lands or is refused.
39
+ const deliverToolResult = async (invocationId, result, attachments) => {
40
+ for (let attempt = 1;; attempt++) {
41
+ try {
42
+ if (attachments?.length)
43
+ await api.submitToolResult(client, invocationId, result, attachments);
44
+ else
45
+ await api.submitToolResult(client, invocationId, result);
46
+ return;
47
+ }
48
+ catch (err) {
49
+ if (!isTransient(err) || attempt >= TOOL_RESULT_MAX_ATTEMPTS) {
50
+ console.error(`[AgentSDK] Failed to submit tool result for ${invocationId}:`, err);
51
+ return;
52
+ }
53
+ const step = Math.min(30000, 1000 * 2 ** (attempt - 1));
54
+ await new Promise((r) => setTimeout(r, step / 2 + Math.random() * (step / 2)));
55
+ }
56
+ }
57
+ };
27
58
  const checkTurnEnd = (chat) => {
28
59
  const busy = isChatBusy(chat);
29
60
  if (prevChatWasBusy && !busy) {
@@ -63,7 +94,7 @@ export function createActions(ctx) {
63
94
  const handler = clientToolHandlers.get(functionName);
64
95
  if (!handler) {
65
96
  console.warn(`[AgentSDK] No handler for client tool: ${functionName}`);
66
- api.submitToolResult(client, invocation.id, JSON.stringify({
97
+ deliverToolResult(invocation.id, JSON.stringify({
67
98
  status: 'not_available',
68
99
  message: `Client tool "${functionName}" is not available in this environment`,
69
100
  }));
@@ -73,12 +104,12 @@ export function createActions(ctx) {
73
104
  // Use Promise.resolve to handle both sync and async handlers
74
105
  const args = invocation.function?.arguments || {};
75
106
  Promise.resolve(handler(args))
76
- .then((result) => {
77
- api.submitToolResult(client, invocation.id, result);
78
- })
107
+ .then((output) => typeof output === 'string'
108
+ ? deliverToolResult(invocation.id, output)
109
+ : deliverToolResult(invocation.id, output.result, output.attachments))
79
110
  .catch((error) => {
80
111
  console.error(`[AgentSDK] Client tool ${functionName} error:`, error);
81
- api.submitToolResult(client, invocation.id, JSON.stringify({
112
+ return deliverToolResult(invocation.id, JSON.stringify({
82
113
  error: String(error)
83
114
  }));
84
115
  });
@@ -381,9 +412,9 @@ export function createActions(ctx) {
381
412
  dispatch({ type: 'SET_ERROR', payload: undefined });
382
413
  dispatch({ type: 'SET_CONNECTION_STATUS', payload: 'idle' });
383
414
  },
384
- submitToolResult: async (toolInvocationId, result) => {
415
+ submitToolResult: async (toolInvocationId, result, attachments) => {
385
416
  try {
386
- await api.submitToolResult(client, toolInvocationId, result);
417
+ await api.submitToolResult(client, toolInvocationId, result, attachments);
387
418
  }
388
419
  catch (error) {
389
420
  console.error('[AgentSDK] Failed to submit tool result:', error);
@@ -45,7 +45,7 @@ export declare function submitToolResult(client: AgentClient, toolInvocationId:
45
45
  payload?: Record<string, unknown>;
46
46
  };
47
47
  form_data?: Record<string, unknown>;
48
- }): Promise<void>;
48
+ }, attachments?: FileRef[]): Promise<void>;
49
49
  /**
50
50
  * Approve a call awaiting approval; the api then runs it.
51
51
  *
package/dist/agent/api.js CHANGED
@@ -144,8 +144,11 @@ export async function setAgent(client, chatId, agentRef) {
144
144
  // =========================================================================
145
145
  // Tool operations
146
146
  // =========================================================================
147
- export async function submitToolResult(client, toolInvocationId, resultOrAction) {
148
- const data = typeof resultOrAction === 'string' ? { result: resultOrAction } : resultOrAction;
147
+ export async function submitToolResult(client, toolInvocationId, resultOrAction, attachments) {
148
+ const data = typeof resultOrAction === 'string' ? { result: resultOrAction } : { ...resultOrAction };
149
+ if (attachments && attachments.length > 0) {
150
+ data.attachments = attachments;
151
+ }
149
152
  await client.http.request('post', `/tools/${toolInvocationId}`, { data });
150
153
  }
151
154
  /**
@@ -46,6 +46,6 @@
46
46
  export { AgentChatProvider } from './provider.js';
47
47
  export { useAgentChat, useAgentActions, useAgentClient, useAgentChatContext, useMessage, } from './hooks.js';
48
48
  export { AgentChatContext, type AgentChatContextValue } from './context.js';
49
- export type { AgentClient, FileRef, AdHocAgentConfig, TemplateAgentConfig, AgentOptions, AgentChatState, AgentChatActions, AlwaysAllowChoice, ChatStatus, AgentChatProviderProps, ClientTool, ClientToolHandlerFn, } from './types.js';
49
+ export type { AgentClient, FileRef, AdHocAgentConfig, TemplateAgentConfig, AgentOptions, AgentChatState, AgentChatActions, AlwaysAllowChoice, ChatStatus, AgentChatProviderProps, ClientTool, ClientToolHandlerFn, ClientToolResult, } from './types.js';
50
50
  export { isAdHocConfig, isTemplateConfig, isClientTool, extractToolSchemas, extractClientToolHandlers, } from './types.js';
51
51
  export { MCPMethodElicitationCreate, parseMCPInputState, elicitParams, isURLElicitation, buildMCPInputResult, type MCPInputState, type ElicitRequestParams, type ElicitRequestedSchema, type ElicitPropertySchema, } from './mcp-input.js';
@@ -129,7 +129,7 @@ export interface AgentChatActions {
129
129
  /** Clear the current error */
130
130
  clearError: () => void;
131
131
  /** Submit a tool result (for widgets/awaiting input) - usually called automatically by SDK */
132
- submitToolResult: (toolInvocationId: string, result: string) => Promise<void>;
132
+ submitToolResult: (toolInvocationId: string, result: string, attachments?: FileRef[]) => Promise<void>;
133
133
  /** Answer an MCP tool call's input requests (AwaitingInput status with MCPInputState data) */
134
134
  submitMCPInput: (toolInvocationId: string, responses: Record<string, ElicitResult>) => Promise<void>;
135
135
  /**
@@ -221,10 +221,23 @@ export interface AgentChatProviderProps {
221
221
  /** Children */
222
222
  children: React.ReactNode;
223
223
  }
224
+ /**
225
+ * What a client tool handler returns: the result text, or the text plus files.
226
+ *
227
+ * `attachments` are files already uploaded with POST /files (for example with
228
+ * `client.files.upload(dataUrlOrBlob)` or the chat's `uploadFile`), passed as
229
+ * `FileRef`s, the same as message attachments. An image attachment reaches the
230
+ * model as image input when the model supports images; other files are listed
231
+ * on the tool message.
232
+ */
233
+ export type ClientToolResult = string | {
234
+ result: string;
235
+ attachments?: FileRef[];
236
+ };
224
237
  /**
225
238
  * Client tool handler function - takes args and returns a result (sync or async)
226
239
  */
227
- export type ClientToolHandlerFn = (args: Record<string, unknown>) => Promise<string> | string;
240
+ export type ClientToolHandlerFn = (args: Record<string, unknown>) => Promise<ClientToolResult> | ClientToolResult;
228
241
  /**
229
242
  * Client tool definition - combines schema with handler
230
243
  */
@@ -2,7 +2,7 @@ import { HttpClient } from '../http/client.js';
2
2
  import type { Response } from '../http/response.js';
3
3
  import type { AgentUpdateBody } from './write-bodies.js';
4
4
  import { FilesAPI } from './files.js';
5
- import { ChannelContext, ChatDTO, ChatMessageDTO, LLMDelta, LLMOutput, AgentConfigInput as AgentConfig, AgentDTO, AgentVersionDTO, CreateAgentRequest, FileDTO as File, InterruptDTO, CursorListRequest, CursorListResponse } from '../types.js';
5
+ import { ChannelContext, ChatDTO, ChatMessageDTO, LLMDelta, LLMOutput, AgentConfigInput as AgentConfig, AgentDTO, AgentVersionDTO, CreateAgentRequest, FileDTO as File, InterruptDTO, CursorListRequest, CursorListResponse, FileRef } from '../types.js';
6
6
  /** Internal tool definition returned by getInternalTools */
7
7
  export interface InternalToolDefinition {
8
8
  id: string;
@@ -108,7 +108,9 @@ export declare class Agent {
108
108
  /** Stop the current chat generation */
109
109
  stopChat(): Promise<void>;
110
110
  /**
111
- * Submit a tool result
111
+ * Submit a tool result. `attachments` are files uploaded with POST /files
112
+ * (`files.upload`) that the tool returns; images reach the model as image
113
+ * input when it supports images.
112
114
  */
113
115
  submitToolResult(toolInvocationId: string, resultOrAction: string | {
114
116
  action: {
@@ -116,7 +118,7 @@ export declare class Agent {
116
118
  payload?: Record<string, unknown>;
117
119
  };
118
120
  form_data?: Record<string, unknown>;
119
- }): Promise<void>;
121
+ }, attachments?: FileRef[]): Promise<void>;
120
122
  /** Stop streaming/polling and cleanup */
121
123
  disconnect(): void;
122
124
  /**
@@ -205,7 +207,9 @@ export declare class AgentsAPI {
205
207
  */
206
208
  create(config: string | AgentConfig, options?: AgentOptions): Agent;
207
209
  /**
208
- * Submit a tool result
210
+ * Submit a tool result. `attachments` are files uploaded with POST /files
211
+ * (`files.upload`) that the tool returns; images reach the model as image
212
+ * input when it supports images.
209
213
  */
210
214
  submitToolResult(toolInvocationId: string, resultOrAction: string | {
211
215
  action: {
@@ -213,7 +217,7 @@ export declare class AgentsAPI {
213
217
  payload?: Record<string, unknown>;
214
218
  };
215
219
  form_data?: Record<string, unknown>;
216
- }): Promise<void>;
220
+ }, attachments?: FileRef[]): Promise<void>;
217
221
  /**
218
222
  * Resolve an interrupt gate on an agent run.
219
223
  *
@@ -172,11 +172,17 @@ export class Agent {
172
172
  await this.http.request('post', `/chats/${this.chatId}/stop`);
173
173
  }
174
174
  /**
175
- * Submit a tool result
175
+ * Submit a tool result. `attachments` are files uploaded with POST /files
176
+ * (`files.upload`) that the tool returns; images reach the model as image
177
+ * input when it supports images.
176
178
  */
177
- async submitToolResult(toolInvocationId, resultOrAction) {
179
+ async submitToolResult(toolInvocationId, resultOrAction, attachments) {
178
180
  const result = typeof resultOrAction === 'string' ? resultOrAction : JSON.stringify(resultOrAction);
179
- await this.http.request('post', `/tools/${toolInvocationId}`, { data: { result } });
181
+ const data = { result };
182
+ if (attachments && attachments.length > 0) {
183
+ data.attachments = attachments;
184
+ }
185
+ await this.http.request('post', `/tools/${toolInvocationId}`, { data });
180
186
  }
181
187
  /** Stop streaming/polling and cleanup */
182
188
  disconnect() {
@@ -496,11 +502,17 @@ export class AgentsAPI {
496
502
  return new Agent(this.http, this.files, config, options);
497
503
  }
498
504
  /**
499
- * Submit a tool result
505
+ * Submit a tool result. `attachments` are files uploaded with POST /files
506
+ * (`files.upload`) that the tool returns; images reach the model as image
507
+ * input when it supports images.
500
508
  */
501
- async submitToolResult(toolInvocationId, resultOrAction) {
509
+ async submitToolResult(toolInvocationId, resultOrAction, attachments) {
502
510
  const result = typeof resultOrAction === 'string' ? resultOrAction : JSON.stringify(resultOrAction);
503
- await this.http.request('post', `/tools/${toolInvocationId}`, { data: { result } });
511
+ const data = { result };
512
+ if (attachments && attachments.length > 0) {
513
+ data.attachments = attachments;
514
+ }
515
+ await this.http.request('post', `/tools/${toolInvocationId}`, { data });
504
516
  }
505
517
  /**
506
518
  * Resolve an interrupt gate on an agent run.
package/dist/types.d.ts CHANGED
@@ -502,10 +502,17 @@ export interface CreateAgentMessageResponse {
502
502
  assistant_message?: ChatMessageDTO;
503
503
  }
504
504
  /**
505
- * ToolResultRequest represents a tool result submission
505
+ * ToolResultRequest is the body of POST /tools/{id}: a client's result for a
506
+ * tool call it ran.
506
507
  */
507
508
  export interface ToolResultRequest {
508
509
  result: string;
510
+ /**
511
+ * Attachments are files uploaded with POST /files, as in MessageInput.
512
+ * The tool message carries them; images reach the model as image input
513
+ * when it accepts images.
514
+ */
515
+ attachments?: FileRef[];
509
516
  }
510
517
  /**
511
518
  * PartialFile is the clean DTO version (no gorm tags).
@@ -5640,6 +5647,16 @@ export interface DecisionVisionInput extends DecisionInput {
5640
5647
  */
5641
5648
  images?: string[];
5642
5649
  }
5650
+ /**
5651
+ * DecisionVideoInput is the input of a decision model that also watches
5652
+ * videos: a DecisionVisionInput plus the videos the questions are about.
5653
+ */
5654
+ export interface DecisionVideoInput extends DecisionVisionInput {
5655
+ /**
5656
+ * Videos are file URIs. Every question sees them, after the images.
5657
+ */
5658
+ videos?: string[];
5659
+ }
5643
5660
  /**
5644
5661
  * DecisionChoiceAnswer is the answer to a choice question.
5645
5662
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inferencesh/sdk",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Official JavaScript/TypeScript SDK for inference.sh - Run AI models with a simple API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",