@inferencesh/sdk 0.5.3 → 0.5.5

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/README.md CHANGED
@@ -264,6 +264,28 @@ await agent.sendMessage('What is the weather in Paris?', {
264
264
  });
265
265
  ```
266
266
 
267
+ ### Structured Output
268
+
269
+ Use `output_schema` to get structured JSON responses:
270
+
271
+ ```typescript
272
+ const agent = client.agents.create({
273
+ core_app: { ref: 'infsh/claude-sonnet-4@latest' },
274
+ output_schema: {
275
+ type: 'object',
276
+ properties: {
277
+ summary: { type: 'string' },
278
+ sentiment: { type: 'string', enum: ['positive', 'negative', 'neutral'] },
279
+ confidence: { type: 'number' },
280
+ },
281
+ required: ['summary', 'sentiment', 'confidence'],
282
+ },
283
+ internal_tools: { finish: true },
284
+ });
285
+
286
+ const response = await agent.sendMessage('Analyze: Great product!');
287
+ ```
288
+
267
289
  ### Agent Methods
268
290
 
269
291
  | Method | Description |
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { ToolInvocationStatusAwaitingInput, ToolTypeClient, } from '../types';
8
8
  import { StreamManager } from '../http/stream';
9
+ import { PollManager } from '../http/poll';
9
10
  import { isAdHocConfig, extractClientToolHandlers } from './types';
10
11
  import * as api from './api';
11
12
  // =============================================================================
@@ -16,7 +17,7 @@ const dispatchedToolInvocations = new Set();
16
17
  // Action Creators
17
18
  // =============================================================================
18
19
  export function createActions(ctx) {
19
- const { client, dispatch, getConfig, getChatId, getClientToolHandlers, getStreamManager, setStreamManager, callbacks } = ctx;
20
+ const { client, dispatch, getConfig, getChatId, getClientToolHandlers, getStreamManager, setStreamManager, getStreamEnabled, getPollIntervalMs, callbacks } = ctx;
20
21
  // =========================================================================
21
22
  // Internal helpers
22
23
  // =========================================================================
@@ -92,6 +93,11 @@ export function createActions(ctx) {
92
93
  callbacks.onStatusChange?.('idle');
93
94
  return;
94
95
  }
96
+ if (!getStreamEnabled()) {
97
+ // Polling mode
98
+ pollChat(id);
99
+ return;
100
+ }
95
101
  // Single unified stream with TypedEvents (both Chat and ChatMessage events)
96
102
  const manager = new StreamManager({
97
103
  createEventSource: () => api.createUnifiedStream(client, id),
@@ -129,6 +135,53 @@ export function createActions(ctx) {
129
135
  setStreamManager(manager);
130
136
  manager.connect();
131
137
  };
138
+ /** Poll-based alternative to streaming for restricted environments */
139
+ const pollChat = (id) => {
140
+ let prevStatus = null;
141
+ const manager = new PollManager({
142
+ pollFunction: () => client.http.request('get', `/chats/${id}/status`),
143
+ intervalMs: getPollIntervalMs(),
144
+ onData: async (statusData) => {
145
+ if (statusData.status === prevStatus)
146
+ return;
147
+ prevStatus = statusData.status;
148
+ // Status changed — fetch full chat
149
+ try {
150
+ const chat = await api.fetchChat(client, id);
151
+ if (chat) {
152
+ setChat(chat);
153
+ if (chat.chat_messages) {
154
+ for (const message of chat.chat_messages) {
155
+ updateMessage(message);
156
+ }
157
+ }
158
+ }
159
+ }
160
+ catch (err) {
161
+ console.warn('[AgentSDK] Poll fetch error:', err);
162
+ callbacks.onError?.(err instanceof Error ? err : new Error(String(err)));
163
+ }
164
+ },
165
+ onStart: () => {
166
+ dispatch({ type: 'SET_STATUS', payload: 'streaming' });
167
+ callbacks.onStatusChange?.('streaming');
168
+ },
169
+ onStop: () => {
170
+ if (getStreamManager()) {
171
+ setStreamManager(undefined);
172
+ dispatch({ type: 'SET_STATUS', payload: 'idle' });
173
+ dispatch({ type: 'SET_IS_GENERATING', payload: false });
174
+ callbacks.onStatusChange?.('idle');
175
+ }
176
+ },
177
+ onError: (error) => {
178
+ console.warn('[AgentSDK] Poll error:', error);
179
+ callbacks.onError?.(error);
180
+ },
181
+ });
182
+ setStreamManager(manager);
183
+ manager.start();
184
+ };
132
185
  const stopStream = () => {
133
186
  const manager = getStreamManager();
134
187
  // Clear ref first so onStop callback (from manager.stop) is a no-op
@@ -27,7 +27,7 @@ import type { AgentChatProviderProps } from './types';
27
27
  * }
28
28
  * ```
29
29
  */
30
- export declare function AgentChatProvider({ client, agentConfig, chatId, clientToolHandlers: extraHandlers, onChatCreated, onStatusChange, onError, children, }: AgentChatProviderProps): import("react/jsx-runtime").JSX.Element;
30
+ export declare function AgentChatProvider({ client, agentConfig, chatId, clientToolHandlers: extraHandlers, onChatCreated, onStatusChange, onError, stream, pollIntervalMs, children, }: AgentChatProviderProps): import("react/jsx-runtime").JSX.Element;
31
31
  export declare namespace AgentChatProvider {
32
32
  var displayName: string;
33
33
  }
@@ -39,13 +39,15 @@ function mergeHandlers(base, extra) {
39
39
  * }
40
40
  * ```
41
41
  */
42
- export function AgentChatProvider({ client, agentConfig, chatId, clientToolHandlers: extraHandlers, onChatCreated, onStatusChange, onError, children, }) {
42
+ export function AgentChatProvider({ client, agentConfig, chatId, clientToolHandlers: extraHandlers, onChatCreated, onStatusChange, onError, stream, pollIntervalMs, children, }) {
43
43
  // Core state via useReducer
44
44
  const [state, dispatch] = useReducer(chatReducer, initialState);
45
45
  // Refs for mutable values that actions need access to
46
46
  const configRef = useRef(agentConfig);
47
47
  const chatIdRef = useRef(chatId ?? null);
48
48
  const streamManagerRef = useRef(undefined);
49
+ const streamRef = useRef(stream);
50
+ const pollIntervalMsRef = useRef(pollIntervalMs);
49
51
  const clientToolHandlersRef = useRef(mergeHandlers(getClientToolHandlers(agentConfig), extraHandlers));
50
52
  const callbacksRef = useRef({ onChatCreated, onStatusChange, onError });
51
53
  // Keep refs in sync with props
@@ -56,6 +58,10 @@ export function AgentChatProvider({ client, agentConfig, chatId, clientToolHandl
56
58
  useEffect(() => {
57
59
  callbacksRef.current = { onChatCreated, onStatusChange, onError };
58
60
  }, [onChatCreated, onStatusChange, onError]);
61
+ useEffect(() => {
62
+ streamRef.current = stream;
63
+ pollIntervalMsRef.current = pollIntervalMs;
64
+ }, [stream, pollIntervalMs]);
59
65
  // Keep chatIdRef synced with state
60
66
  useEffect(() => {
61
67
  chatIdRef.current = state.chatId;
@@ -69,6 +75,8 @@ export function AgentChatProvider({ client, agentConfig, chatId, clientToolHandl
69
75
  getClientToolHandlers: () => clientToolHandlersRef.current,
70
76
  getStreamManager: () => streamManagerRef.current,
71
77
  setStreamManager: (manager) => { streamManagerRef.current = manager; },
78
+ getStreamEnabled: () => streamRef.current ?? client.http.getStreamDefault(),
79
+ getPollIntervalMs: () => pollIntervalMsRef.current ?? client.http.getPollIntervalMs(),
72
80
  callbacks: callbacksRef.current,
73
81
  }), [client]);
74
82
  // Re-bind callbacks when they change
@@ -7,6 +7,7 @@ import type { Dispatch } from 'react';
7
7
  import type { ChatDTO, ChatMessageDTO, AgentTool, AgentConfig as GeneratedAgentConfig, CoreAppConfig } from '../types';
8
8
  import type { HttpClient } from '../http/client';
9
9
  import type { StreamManager } from '../http/stream';
10
+ import type { PollManager } from '../http/poll';
10
11
  /**
11
12
  * Minimal file interface returned by upload (just needs uri and content_type)
12
13
  */
@@ -20,7 +21,7 @@ export interface UploadedFile {
20
21
  */
21
22
  export interface AgentClient {
22
23
  /** HTTP client for API requests */
23
- http: Pick<HttpClient, 'request' | 'createEventSource'>;
24
+ http: Pick<HttpClient, 'request' | 'createEventSource' | 'getStreamDefault' | 'getPollIntervalMs'>;
24
25
  /** Files API for uploads */
25
26
  files: {
26
27
  upload: (data: string | Blob | globalThis.File) => Promise<UploadedFile>;
@@ -142,6 +143,10 @@ export interface AgentChatProviderProps {
142
143
  onStatusChange?: (status: ChatStatus) => void;
143
144
  /** Callback when an error occurs */
144
145
  onError?: (error: Error) => void;
146
+ /** Use SSE streaming (true, default) or polling (false) for real-time updates */
147
+ stream?: boolean;
148
+ /** Polling interval in ms when stream is false (default: 2000) */
149
+ pollIntervalMs?: number;
145
150
  /** Children */
146
151
  children: React.ReactNode;
147
152
  }
@@ -203,14 +208,18 @@ export type ChatAction = {
203
208
  /**
204
209
  * Context for action creators
205
210
  */
211
+ /** Union of manager types used for real-time updates */
212
+ export type UpdateManager = StreamManager<unknown> | PollManager<unknown>;
206
213
  export interface ActionsContext {
207
214
  client: AgentClient;
208
215
  dispatch: Dispatch<ChatAction>;
209
216
  getConfig: () => AgentOptions | null;
210
217
  getChatId: () => string | null;
211
218
  getClientToolHandlers: () => Map<string, ClientToolHandlerFn>;
212
- getStreamManager: () => StreamManager<unknown> | undefined;
213
- setStreamManager: (manager: StreamManager<unknown> | undefined) => void;
219
+ getStreamManager: () => UpdateManager | undefined;
220
+ setStreamManager: (manager: UpdateManager | undefined) => void;
221
+ getStreamEnabled: () => boolean;
222
+ getPollIntervalMs: () => number;
214
223
  callbacks: {
215
224
  onChatCreated?: (chatId: string) => void;
216
225
  onStatusChange?: (status: ChatStatus) => void;
@@ -27,6 +27,10 @@ export interface SendMessageOptions {
27
27
  name: string;
28
28
  args: Record<string, unknown>;
29
29
  }) => void;
30
+ /** Use SSE streaming (true) or polling (false). Overrides client default. */
31
+ stream?: boolean;
32
+ /** Polling interval in ms when stream is false. Overrides client default. */
33
+ pollIntervalMs?: number;
30
34
  }
31
35
  /**
32
36
  * Agent for chat interactions
@@ -40,6 +44,7 @@ export declare class Agent {
40
44
  private readonly agentName;
41
45
  private chatId;
42
46
  private stream;
47
+ private poller;
43
48
  private dispatchedToolCalls;
44
49
  /** @internal */
45
50
  constructor(http: HttpClient, files: FilesAPI, config: string | AgentConfig, options?: AgentOptions);
@@ -64,7 +69,7 @@ export declare class Agent {
64
69
  };
65
70
  form_data?: Record<string, unknown>;
66
71
  }): Promise<void>;
67
- /** Stop streaming and cleanup */
72
+ /** Stop streaming/polling and cleanup */
68
73
  disconnect(): void;
69
74
  /** Reset the agent (start fresh chat) */
70
75
  reset(): void;
@@ -74,6 +79,8 @@ export declare class Agent {
74
79
  startStreaming(options?: Omit<SendMessageOptions, 'files'>): void;
75
80
  /** Stream events until chat becomes idle */
76
81
  private streamUntilIdle;
82
+ /** Poll until chat becomes idle, dispatching callbacks on changes */
83
+ private pollUntilIdle;
77
84
  }
78
85
  /**
79
86
  * Agents API
@@ -1,4 +1,5 @@
1
1
  import { StreamManager } from '../http/stream';
2
+ import { PollManager } from '../http/poll';
2
3
  import { ToolTypeClient, ToolInvocationStatusAwaitingInput, } from '../types';
3
4
  /**
4
5
  * Agent for chat interactions
@@ -10,6 +11,7 @@ export class Agent {
10
11
  constructor(http, files, config, options) {
11
12
  this.chatId = null;
12
13
  this.stream = null;
14
+ this.poller = null;
13
15
  this.dispatchedToolCalls = new Set();
14
16
  this.http = http;
15
17
  this.files = files;
@@ -59,24 +61,28 @@ export class Agent {
59
61
  agent_name: this.agentName ?? this.config.name,
60
62
  input: { text, images: imageUris, files: fileUris, role: 'user', context: [], system_prompt: '', context_size: 0 },
61
63
  };
62
- // For existing chats with callbacks: Start streaming BEFORE POST so we don't miss updates
63
- let streamPromise = null;
64
+ const useStream = options.stream ?? this.http.getStreamDefault();
65
+ const waitFn = useStream
66
+ ? (opts) => this.streamUntilIdle(opts)
67
+ : (opts) => this.pollUntilIdle(opts);
68
+ // For existing chats with callbacks: Start waiting BEFORE POST so we don't miss updates
69
+ let waitPromise = null;
64
70
  if (this.chatId && hasCallbacks) {
65
- streamPromise = this.streamUntilIdle(options);
71
+ waitPromise = waitFn(options);
66
72
  }
67
73
  // Make the POST request
68
74
  const response = await this.http.request('post', '/agents/run', { data: body });
69
- // For new chats: Set chatId and start streaming immediately after POST
75
+ // For new chats: Set chatId and start waiting immediately after POST
70
76
  const isNewChat = !this.chatId && response.assistant_message.chat_id;
71
77
  if (isNewChat) {
72
78
  this.chatId = response.assistant_message.chat_id;
73
79
  if (hasCallbacks) {
74
- streamPromise = this.streamUntilIdle(options);
80
+ waitPromise = waitFn(options);
75
81
  }
76
82
  }
77
- // Wait for streaming to complete
78
- if (streamPromise) {
79
- await streamPromise;
83
+ // Wait for completion
84
+ if (waitPromise) {
85
+ await waitPromise;
80
86
  }
81
87
  return { userMessage: response.user_message, assistantMessage: response.assistant_message };
82
88
  }
@@ -100,10 +106,12 @@ export class Agent {
100
106
  const result = typeof resultOrAction === 'string' ? resultOrAction : JSON.stringify(resultOrAction);
101
107
  await this.http.request('post', `/tools/${toolInvocationId}`, { data: { result } });
102
108
  }
103
- /** Stop streaming and cleanup */
109
+ /** Stop streaming/polling and cleanup */
104
110
  disconnect() {
105
111
  this.stream?.stop();
106
112
  this.stream = null;
113
+ this.poller?.stop();
114
+ this.poller = null;
107
115
  }
108
116
  /** Reset the agent (start fresh chat) */
109
117
  reset() {
@@ -155,6 +163,73 @@ export class Agent {
155
163
  this.stream.connect();
156
164
  });
157
165
  }
166
+ /** Poll until chat becomes idle, dispatching callbacks on changes */
167
+ pollUntilIdle(options) {
168
+ if (!this.chatId)
169
+ return Promise.resolve();
170
+ const intervalMs = options.pollIntervalMs ?? this.http.getPollIntervalMs();
171
+ let prevStatus = null;
172
+ let knownMessageIds = new Set();
173
+ return new Promise((resolve) => {
174
+ this.poller?.stop();
175
+ this.poller = new PollManager({
176
+ pollFunction: async () => {
177
+ // Lightweight status check first
178
+ const status = await this.http.request('get', `/chats/${this.chatId}/status`);
179
+ if (status.status === prevStatus) {
180
+ // No change — return a stub to skip processing
181
+ return { status: status.status };
182
+ }
183
+ // Status changed — fetch full chat
184
+ return this.http.request('get', `/chats/${this.chatId}`);
185
+ },
186
+ intervalMs,
187
+ onData: (chat) => {
188
+ if (chat.status === prevStatus && !chat.chat_messages)
189
+ return;
190
+ prevStatus = chat.status;
191
+ options.onChat?.(chat);
192
+ // Dispatch new/updated messages
193
+ if (chat.chat_messages && options.onMessage) {
194
+ for (const message of chat.chat_messages) {
195
+ if (!knownMessageIds.has(message.id)) {
196
+ knownMessageIds.add(message.id);
197
+ options.onMessage(message);
198
+ }
199
+ else {
200
+ // Re-dispatch for potential updates
201
+ options.onMessage(message);
202
+ }
203
+ // Handle client tool invocations
204
+ if (message.tool_invocations && options.onToolCall) {
205
+ for (const inv of message.tool_invocations) {
206
+ if (this.dispatchedToolCalls.has(inv.id))
207
+ continue;
208
+ if (inv.type === ToolTypeClient && inv.status === ToolInvocationStatusAwaitingInput) {
209
+ this.dispatchedToolCalls.add(inv.id);
210
+ options.onToolCall({
211
+ id: inv.id,
212
+ name: inv.function?.name || '',
213
+ args: inv.function?.arguments || {},
214
+ });
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }
220
+ if (chat.status === 'idle') {
221
+ this.poller?.stop();
222
+ this.poller = null;
223
+ resolve();
224
+ }
225
+ },
226
+ onError: (error) => {
227
+ console.warn('[Agent] Poll error:', error);
228
+ },
229
+ });
230
+ this.poller.start();
231
+ });
232
+ }
158
233
  }
159
234
  /**
160
235
  * Agents API
@@ -13,6 +13,10 @@ export interface RunOptions {
13
13
  maxReconnects?: number;
14
14
  /** Delay between reconnection attempts in ms (default: 1000) */
15
15
  reconnectDelayMs?: number;
16
+ /** Use SSE streaming (true) or polling (false). Overrides client default. */
17
+ stream?: boolean;
18
+ /** Polling interval in ms when stream is false. Overrides client default. */
19
+ pollIntervalMs?: number;
16
20
  }
17
21
  /**
18
22
  * Tasks API
@@ -52,6 +56,8 @@ export declare class TasksAPI {
52
56
  * Run a task and optionally wait for completion
53
57
  */
54
58
  run(params: ApiAppRunRequest, processedInput: unknown, options?: RunOptions): Promise<Task>;
59
+ /** Poll GET /tasks/{id}/status until terminal, full-fetch on status change. */
60
+ private pollUntilTerminal;
55
61
  /**
56
62
  * Update task visibility
57
63
  */
package/dist/api/tasks.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { StreamManager } from '../http/stream';
2
+ import { PollManager } from '../http/poll';
2
3
  import { TaskStatusCompleted, TaskStatusFailed, TaskStatusCancelled, } from '../types';
3
4
  //TODO: This is ugly...
4
5
  function stripTask(task) {
@@ -78,7 +79,11 @@ export class TasksAPI {
78
79
  if (!wait) {
79
80
  return stripTask(task);
80
81
  }
81
- // Wait for completion with optional updates
82
+ const useStream = options.stream ?? this.http.getStreamDefault();
83
+ if (!useStream) {
84
+ return this.pollUntilTerminal(task, options);
85
+ }
86
+ // Wait for completion with optional updates via SSE
82
87
  // Accumulate state across partial updates to preserve fields like session_id
83
88
  let accumulatedTask = { ...task };
84
89
  return new Promise((resolve, reject) => {
@@ -131,6 +136,51 @@ export class TasksAPI {
131
136
  streamManager.connect();
132
137
  });
133
138
  }
139
+ /** Poll GET /tasks/{id}/status until terminal, full-fetch on status change. */
140
+ pollUntilTerminal(task, options) {
141
+ const { onUpdate, maxReconnects = 5 } = options;
142
+ const intervalMs = options.pollIntervalMs ?? this.http.getPollIntervalMs();
143
+ let prevStatus = task.status;
144
+ return new Promise((resolve, reject) => {
145
+ const poller = new PollManager({
146
+ pollFunction: () => this.http.request('get', `/tasks/${task.id}/status`),
147
+ intervalMs,
148
+ maxRetries: maxReconnects,
149
+ onData: async (statusData) => {
150
+ if (statusData.status === prevStatus)
151
+ return;
152
+ prevStatus = statusData.status;
153
+ // Status changed — fetch full task
154
+ try {
155
+ const fullTask = await this.http.request('get', `/tasks/${task.id}`);
156
+ const stripped = stripTask(fullTask);
157
+ onUpdate?.(stripped);
158
+ if (fullTask.status === TaskStatusCompleted) {
159
+ poller.stop();
160
+ resolve(stripped);
161
+ }
162
+ else if (fullTask.status === TaskStatusFailed) {
163
+ poller.stop();
164
+ reject(new Error(fullTask.error || 'task failed'));
165
+ }
166
+ else if (fullTask.status === TaskStatusCancelled) {
167
+ poller.stop();
168
+ reject(new Error('task cancelled'));
169
+ }
170
+ }
171
+ catch (err) {
172
+ poller.stop();
173
+ reject(err instanceof Error ? err : new Error(String(err)));
174
+ }
175
+ },
176
+ onError: (error) => {
177
+ reject(error);
178
+ poller.stop();
179
+ },
180
+ });
181
+ poller.start();
182
+ });
183
+ }
134
184
  /**
135
185
  * Update task visibility
136
186
  */
@@ -25,6 +25,16 @@ export interface HttpClientConfig {
25
25
  * Can be used for retry logic, auth refresh, OTP handling, etc.
26
26
  */
27
27
  onError?: ErrorHandler;
28
+ /**
29
+ * Use polling instead of SSE for real-time updates (default: true = SSE).
30
+ * Set to false for environments that can't maintain long-lived connections
31
+ * (Convex actions, Cloudflare Workers, restricted edge runtimes).
32
+ */
33
+ stream?: boolean;
34
+ /**
35
+ * Polling interval in milliseconds when stream is false (default: 2000).
36
+ */
37
+ pollIntervalMs?: number;
28
38
  }
29
39
  /**
30
40
  * Low-level HTTP client for inference.sh API
@@ -40,9 +50,15 @@ export declare class HttpClient {
40
50
  private readonly customHeaders;
41
51
  private readonly credentials;
42
52
  private readonly onError;
53
+ private readonly streamDefault;
54
+ private readonly pollInterval;
43
55
  constructor(config: HttpClientConfig);
44
56
  /** Get the base URL */
45
57
  getBaseUrl(): string;
58
+ /** Whether SSE streaming is the default transport (true) or polling (false) */
59
+ getStreamDefault(): boolean;
60
+ /** Get the default poll interval in ms */
61
+ getPollIntervalMs(): number;
46
62
  /** Check if in proxy mode */
47
63
  isProxyMode(): boolean;
48
64
  /** Resolve dynamic headers */
@@ -19,11 +19,21 @@ export class HttpClient {
19
19
  this.customHeaders = config.headers || {};
20
20
  this.credentials = config.credentials || 'include';
21
21
  this.onError = config.onError;
22
+ this.streamDefault = config.stream ?? true;
23
+ this.pollInterval = config.pollIntervalMs ?? 2000;
22
24
  }
23
25
  /** Get the base URL */
24
26
  getBaseUrl() {
25
27
  return this.baseUrl;
26
28
  }
29
+ /** Whether SSE streaming is the default transport (true) or polling (false) */
30
+ getStreamDefault() {
31
+ return this.streamDefault;
32
+ }
33
+ /** Get the default poll interval in ms */
34
+ getPollIntervalMs() {
35
+ return this.pollInterval;
36
+ }
27
37
  /** Check if in proxy mode */
28
38
  isProxyMode() {
29
39
  return !!this.proxyUrl;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * PollManager — polling transport alternative to StreamManager.
3
+ *
4
+ * Used when long-lived SSE connections are unavailable (Convex actions,
5
+ * Cloudflare Workers, restricted edge runtimes).
6
+ */
7
+ export interface PollManagerOptions<T> {
8
+ /** Function that fetches the latest data each poll cycle */
9
+ pollFunction: () => Promise<T>;
10
+ /** Milliseconds between polls (default 2000) */
11
+ intervalMs?: number;
12
+ /** Maximum consecutive errors before giving up (default 5) */
13
+ maxRetries?: number;
14
+ /** Delay after an error before retrying (default 1000) */
15
+ retryDelayMs?: number;
16
+ /** Called with data on every successful poll */
17
+ onData?: (data: T) => void;
18
+ /** Called when an error occurs */
19
+ onError?: (error: Error) => void;
20
+ /** Called when polling starts */
21
+ onStart?: () => void;
22
+ /** Called when polling stops (intentionally or after max retries) */
23
+ onStop?: () => void;
24
+ }
25
+ export declare class PollManager<T> {
26
+ private options;
27
+ private interval;
28
+ private retryTimeout;
29
+ private consecutiveErrors;
30
+ private isStopped;
31
+ private polling;
32
+ constructor(options: PollManagerOptions<T>);
33
+ /** Start polling. First poll is immediate, then at intervalMs. */
34
+ start(): void;
35
+ /** Stop polling and clean up. */
36
+ stop(): void;
37
+ private cleanup;
38
+ private poll;
39
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * PollManager — polling transport alternative to StreamManager.
3
+ *
4
+ * Used when long-lived SSE connections are unavailable (Convex actions,
5
+ * Cloudflare Workers, restricted edge runtimes).
6
+ */
7
+ export class PollManager {
8
+ constructor(options) {
9
+ this.interval = null;
10
+ this.retryTimeout = null;
11
+ this.consecutiveErrors = 0;
12
+ this.isStopped = true;
13
+ this.polling = false;
14
+ this.options = {
15
+ intervalMs: 2000,
16
+ maxRetries: 5,
17
+ retryDelayMs: 1000,
18
+ ...options,
19
+ };
20
+ }
21
+ /** Start polling. First poll is immediate, then at intervalMs. */
22
+ start() {
23
+ if (!this.isStopped)
24
+ return;
25
+ this.isStopped = false;
26
+ this.consecutiveErrors = 0;
27
+ this.options.onStart?.();
28
+ this.poll(); // immediate first poll
29
+ this.interval = setInterval(() => this.poll(), this.options.intervalMs);
30
+ }
31
+ /** Stop polling and clean up. */
32
+ stop() {
33
+ if (this.isStopped)
34
+ return;
35
+ this.isStopped = true;
36
+ this.cleanup();
37
+ this.options.onStop?.();
38
+ }
39
+ cleanup() {
40
+ if (this.interval) {
41
+ clearInterval(this.interval);
42
+ this.interval = null;
43
+ }
44
+ if (this.retryTimeout) {
45
+ clearTimeout(this.retryTimeout);
46
+ this.retryTimeout = null;
47
+ }
48
+ }
49
+ async poll() {
50
+ if (this.isStopped || this.polling)
51
+ return;
52
+ this.polling = true;
53
+ try {
54
+ const data = await this.options.pollFunction();
55
+ this.polling = false;
56
+ if (this.isStopped)
57
+ return;
58
+ this.consecutiveErrors = 0;
59
+ this.options.onData?.(data);
60
+ }
61
+ catch (err) {
62
+ this.polling = false;
63
+ if (this.isStopped)
64
+ return;
65
+ const error = err instanceof Error ? err : new Error(String(err));
66
+ this.consecutiveErrors++;
67
+ this.options.onError?.(error);
68
+ if (this.consecutiveErrors >= this.options.maxRetries) {
69
+ this.stop();
70
+ }
71
+ }
72
+ }
73
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { HttpClient, HttpClientConfig, ErrorHandler, createHttpClient } from './http/client';
2
2
  export { StreamManager, StreamManagerOptions, PartialDataWrapper } from './http/stream';
3
+ export { PollManager, PollManagerOptions } from './http/poll';
3
4
  export { InferenceError, RequirementsNotMetException, SessionError, SessionNotFoundError, SessionExpiredError, SessionEndedError, WorkerLostError, } from './http/errors';
4
5
  export { TasksAPI, RunOptions } from './api/tasks';
5
6
  export { FilesAPI, UploadFileOptions } from './api/files';
@@ -35,6 +36,15 @@ export interface InferenceConfig {
35
36
  * When set, requests are routed through your proxy server to protect API keys.
36
37
  */
37
38
  proxyUrl?: string;
39
+ /**
40
+ * Use SSE streaming (true, default) or polling (false) for real-time updates.
41
+ * Set to false for environments that can't maintain long-lived connections.
42
+ */
43
+ stream?: boolean;
44
+ /**
45
+ * Polling interval in milliseconds when stream is false (default: 2000).
46
+ */
47
+ pollIntervalMs?: number;
38
48
  }
39
49
  /**
40
50
  * Inference.sh SDK Client
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // HTTP utilities
2
2
  export { HttpClient, createHttpClient } from './http/client';
3
3
  export { StreamManager } from './http/stream';
4
+ export { PollManager } from './http/poll';
4
5
  export { InferenceError, RequirementsNotMetException, SessionError, SessionNotFoundError, SessionExpiredError, SessionEndedError, WorkerLostError, } from './http/errors';
5
6
  // API modules
6
7
  export { TasksAPI } from './api/tasks';
@@ -49,6 +50,8 @@ export class Inference {
49
50
  headers: 'headers' in config ? config.headers : undefined,
50
51
  credentials: 'credentials' in config ? config.credentials : undefined,
51
52
  onError: 'onError' in config ? config.onError : undefined,
53
+ stream: 'stream' in config ? config.stream : undefined,
54
+ pollIntervalMs: 'pollIntervalMs' in config ? config.pollIntervalMs : undefined,
52
55
  });
53
56
  this.files = new FilesAPI(this.http);
54
57
  this.tasks = new TasksAPI(this.http);
@@ -28,6 +28,8 @@ function toJsonSchema(params) {
28
28
  if (schema.properties) {
29
29
  const nested = toJsonSchema(schema.properties);
30
30
  prop.properties = nested.properties;
31
+ if (nested.required.length > 0)
32
+ prop.required = nested.required;
31
33
  }
32
34
  if (schema.items) {
33
35
  const itemSchema = toJsonSchema({ _item: schema.items });
@@ -149,8 +149,19 @@ describe('ClientToolBuilder (tool)', () => {
149
149
  name: { type: 'string', description: 'Name' },
150
150
  email: { type: 'string', description: 'Email' },
151
151
  },
152
+ required: ['name', 'email'],
152
153
  });
153
154
  });
155
+ it('propagates required into nested objects with optional fields', () => {
156
+ const t = tool('update_user')
157
+ .param('user', object({
158
+ name: string('Name'),
159
+ bio: optional(string('Bio')),
160
+ }))
161
+ .build();
162
+ const userProp = t.client?.input_schema.properties?.user;
163
+ expect(userProp.required).toEqual(['name']);
164
+ });
154
165
  it('creates tool with array parameters', () => {
155
166
  const t = tool('process_items')
156
167
  .param('items', array(string('Item'), 'List of items'))
package/dist/types.d.ts CHANGED
@@ -946,6 +946,14 @@ export interface PermissionModelDTO {
946
946
  team?: TeamRelationDTO;
947
947
  visibility: Visibility;
948
948
  }
949
+ /**
950
+ * ResourceStatusDTO is a lightweight status-only response for polling transports.
951
+ */
952
+ export interface ResourceStatusDTO {
953
+ id: string;
954
+ status: any;
955
+ updated_at: string;
956
+ }
949
957
  export type ChatStatus = string;
950
958
  export declare const ChatStatusBusy: ChatStatus;
951
959
  export declare const ChatStatusIdle: ChatStatus;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inferencesh/sdk",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
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",