@inferencesh/sdk 0.5.2 → 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
  // =========================================================================
@@ -48,7 +49,8 @@ export function createActions(ctx) {
48
49
  if (!handler) {
49
50
  console.warn(`[AgentSDK] No handler for client tool: ${functionName}`);
50
51
  api.submitToolResult(client, invocation.id, JSON.stringify({
51
- error: `No handler registered for tool: ${functionName}`
52
+ status: 'not_available',
53
+ message: `Client tool "${functionName}" is not available in this environment`,
52
54
  }));
53
55
  continue;
54
56
  }
@@ -77,10 +79,24 @@ export function createActions(ctx) {
77
79
  setStreamManager(undefined);
78
80
  dispatch({ type: 'SET_STATUS', payload: 'connecting' });
79
81
  callbacks.onStatusChange?.('connecting');
80
- // Fetch initial chat
81
- const chat = await api.fetchChat(client, id);
82
- if (chat) {
83
- setChat(chat);
82
+ try {
83
+ // Fetch initial chat
84
+ const chat = await api.fetchChat(client, id);
85
+ if (chat) {
86
+ setChat(chat);
87
+ }
88
+ }
89
+ catch (error) {
90
+ console.error('[AgentSDK] Failed to fetch chat:', error);
91
+ dispatch({ type: 'SET_STATUS', payload: 'idle' });
92
+ dispatch({ type: 'SET_IS_GENERATING', payload: false });
93
+ callbacks.onStatusChange?.('idle');
94
+ return;
95
+ }
96
+ if (!getStreamEnabled()) {
97
+ // Polling mode
98
+ pollChat(id);
99
+ return;
84
100
  }
85
101
  // Single unified stream with TypedEvents (both Chat and ChatMessage events)
86
102
  const manager = new StreamManager({
@@ -96,6 +112,17 @@ export function createActions(ctx) {
96
112
  dispatch({ type: 'SET_STATUS', payload: 'streaming' });
97
113
  callbacks.onStatusChange?.('streaming');
98
114
  },
115
+ onStop: () => {
116
+ // Only reset if this is an unexpected stop (stream died, max reconnects exhausted).
117
+ // If stopStream() was called intentionally, it clears the manager ref first,
118
+ // so getStreamManager() will be undefined and we skip the duplicate dispatch.
119
+ if (getStreamManager()) {
120
+ setStreamManager(undefined);
121
+ dispatch({ type: 'SET_STATUS', payload: 'idle' });
122
+ dispatch({ type: 'SET_IS_GENERATING', payload: false });
123
+ callbacks.onStatusChange?.('idle');
124
+ }
125
+ },
99
126
  });
100
127
  // Listen for Chat object updates (status changes)
101
128
  manager.addEventListener('chats', (chatData) => {
@@ -108,12 +135,60 @@ export function createActions(ctx) {
108
135
  setStreamManager(manager);
109
136
  manager.connect();
110
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
+ };
111
185
  const stopStream = () => {
112
186
  const manager = getStreamManager();
187
+ // Clear ref first so onStop callback (from manager.stop) is a no-op
188
+ setStreamManager(undefined);
113
189
  if (manager) {
114
190
  manager.stop();
115
191
  }
116
- setStreamManager(undefined);
117
192
  dispatch({ type: 'SET_STATUS', payload: 'idle' });
118
193
  dispatch({ type: 'SET_IS_GENERATING', payload: false });
119
194
  callbacks.onStatusChange?.('idle');
@@ -150,6 +225,12 @@ export function createActions(ctx) {
150
225
  streamChat(newChatId);
151
226
  }
152
227
  }
228
+ else {
229
+ // API returned no result — reset status so we don't get stuck
230
+ dispatch({ type: 'SET_STATUS', payload: 'idle' });
231
+ dispatch({ type: 'SET_IS_GENERATING', payload: false });
232
+ callbacks.onStatusChange?.('idle');
233
+ }
153
234
  }
154
235
  catch (error) {
155
236
  console.error('[AgentSDK] Failed to send message:', error);
@@ -27,7 +27,7 @@ import type { AgentChatProviderProps } from './types';
27
27
  * }
28
28
  * ```
29
29
  */
30
- export declare function AgentChatProvider({ client, agentConfig, chatId, 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
  }
@@ -9,6 +9,14 @@ import { useReducer, useRef, useEffect, useMemo } from 'react';
9
9
  import { AgentChatContext } from './context';
10
10
  import { chatReducer, initialState } from './reducer';
11
11
  import { createActions, getClientToolHandlers } from './actions';
12
+ function mergeHandlers(base, extra) {
13
+ if (!extra || extra.size === 0)
14
+ return base;
15
+ const merged = new Map(base);
16
+ for (const [k, v] of extra)
17
+ merged.set(k, v);
18
+ return merged;
19
+ }
12
20
  /**
13
21
  * AgentChatProvider - Provides chat state and actions to children
14
22
  *
@@ -31,23 +39,29 @@ import { createActions, getClientToolHandlers } from './actions';
31
39
  * }
32
40
  * ```
33
41
  */
34
- export function AgentChatProvider({ client, agentConfig, chatId, onChatCreated, onStatusChange, onError, children, }) {
42
+ export function AgentChatProvider({ client, agentConfig, chatId, clientToolHandlers: extraHandlers, onChatCreated, onStatusChange, onError, stream, pollIntervalMs, children, }) {
35
43
  // Core state via useReducer
36
44
  const [state, dispatch] = useReducer(chatReducer, initialState);
37
45
  // Refs for mutable values that actions need access to
38
46
  const configRef = useRef(agentConfig);
39
47
  const chatIdRef = useRef(chatId ?? null);
40
48
  const streamManagerRef = useRef(undefined);
41
- const clientToolHandlersRef = useRef(getClientToolHandlers(agentConfig));
49
+ const streamRef = useRef(stream);
50
+ const pollIntervalMsRef = useRef(pollIntervalMs);
51
+ const clientToolHandlersRef = useRef(mergeHandlers(getClientToolHandlers(agentConfig), extraHandlers));
42
52
  const callbacksRef = useRef({ onChatCreated, onStatusChange, onError });
43
53
  // Keep refs in sync with props
44
54
  useEffect(() => {
45
55
  configRef.current = agentConfig;
46
- clientToolHandlersRef.current = getClientToolHandlers(agentConfig);
47
- }, [agentConfig]);
56
+ clientToolHandlersRef.current = mergeHandlers(getClientToolHandlers(agentConfig), extraHandlers);
57
+ }, [agentConfig, extraHandlers]);
48
58
  useEffect(() => {
49
59
  callbacksRef.current = { onChatCreated, onStatusChange, onError };
50
60
  }, [onChatCreated, onStatusChange, onError]);
61
+ useEffect(() => {
62
+ streamRef.current = stream;
63
+ pollIntervalMsRef.current = pollIntervalMs;
64
+ }, [stream, pollIntervalMs]);
51
65
  // Keep chatIdRef synced with state
52
66
  useEffect(() => {
53
67
  chatIdRef.current = state.chatId;
@@ -61,6 +75,8 @@ export function AgentChatProvider({ client, agentConfig, chatId, onChatCreated,
61
75
  getClientToolHandlers: () => clientToolHandlersRef.current,
62
76
  getStreamManager: () => streamManagerRef.current,
63
77
  setStreamManager: (manager) => { streamManagerRef.current = manager; },
78
+ getStreamEnabled: () => streamRef.current ?? client.http.getStreamDefault(),
79
+ getPollIntervalMs: () => pollIntervalMsRef.current ?? client.http.getPollIntervalMs(),
64
80
  callbacks: callbacksRef.current,
65
81
  }), [client]);
66
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>;
@@ -134,12 +135,18 @@ export interface AgentChatProviderProps {
134
135
  agentConfig: AgentOptions;
135
136
  /** Optional existing chat ID to continue */
136
137
  chatId?: string;
138
+ /** Extra client tool handlers, merged with any from agentConfig. Use with TemplateAgentConfig to handle tools defined server-side. */
139
+ clientToolHandlers?: Map<string, ClientToolHandlerFn>;
137
140
  /** Callback when a new chat is created */
138
141
  onChatCreated?: (chatId: string) => void;
139
142
  /** Callback when chat status changes */
140
143
  onStatusChange?: (status: ChatStatus) => void;
141
144
  /** Callback when an error occurs */
142
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;
143
150
  /** Children */
144
151
  children: React.ReactNode;
145
152
  }
@@ -201,14 +208,18 @@ export type ChatAction = {
201
208
  /**
202
209
  * Context for action creators
203
210
  */
211
+ /** Union of manager types used for real-time updates */
212
+ export type UpdateManager = StreamManager<unknown> | PollManager<unknown>;
204
213
  export interface ActionsContext {
205
214
  client: AgentClient;
206
215
  dispatch: Dispatch<ChatAction>;
207
216
  getConfig: () => AgentOptions | null;
208
217
  getChatId: () => string | null;
209
218
  getClientToolHandlers: () => Map<string, ClientToolHandlerFn>;
210
- getStreamManager: () => StreamManager<unknown> | undefined;
211
- setStreamManager: (manager: StreamManager<unknown> | undefined) => void;
219
+ getStreamManager: () => UpdateManager | undefined;
220
+ setStreamManager: (manager: UpdateManager | undefined) => void;
221
+ getStreamEnabled: () => boolean;
222
+ getPollIntervalMs: () => number;
212
223
  callbacks: {
213
224
  onChatCreated?: (chatId: string) => void;
214
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
@@ -7,6 +7,7 @@ export interface InternalToolsConfig {
7
7
  widget?: boolean;
8
8
  finish?: boolean;
9
9
  skills?: boolean;
10
+ host_context?: boolean;
10
11
  }
11
12
  /**
12
13
  * ToolType represents the type of tool (used in both AgentTool definition and ToolInvocation)
@@ -396,6 +397,16 @@ export interface ApiAgentRunRequest {
396
397
  */
397
398
  stream?: boolean;
398
399
  }
400
+ /**
401
+ * EmbedAgentRunRequest is the embed variant of ApiAgentRunRequest.
402
+ * Only template agents are supported (no ad-hoc configs).
403
+ */
404
+ export interface EmbedAgentRunRequest {
405
+ chat_id?: string;
406
+ agent: string;
407
+ input: ChatTaskInput;
408
+ stream?: boolean;
409
+ }
399
410
  export interface CreateAgentMessageRequest {
400
411
  chat_id?: string;
401
412
  agent_id?: string;
@@ -508,6 +519,26 @@ export interface CreateAgentRequest {
508
519
  */
509
520
  export interface SDKTypes {
510
521
  }
522
+ export interface SkillPublishRequest {
523
+ name: string;
524
+ description: string;
525
+ category: string;
526
+ repo_url: string;
527
+ license: string;
528
+ allowed_tools: string;
529
+ compatibility: string;
530
+ instructions: string;
531
+ files: SkillFile[];
532
+ metadata?: {
533
+ [key: string]: string;
534
+ };
535
+ /**
536
+ * Spec fields for roundtrip fidelity
537
+ */
538
+ disable_model_invocation: boolean;
539
+ user_invocable?: boolean;
540
+ context: string;
541
+ }
511
542
  export interface CheckoutCreateRequest {
512
543
  amount: number;
513
544
  success_url: string;
@@ -915,6 +946,14 @@ export interface PermissionModelDTO {
915
946
  team?: TeamRelationDTO;
916
947
  visibility: Visibility;
917
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
+ }
918
957
  export type ChatStatus = string;
919
958
  export declare const ChatStatusBusy: ChatStatus;
920
959
  export declare const ChatStatusIdle: ChatStatus;
@@ -1745,6 +1784,16 @@ export interface InstanceEnvVar {
1745
1784
  name: string;
1746
1785
  value: string;
1747
1786
  }
1787
+ /**
1788
+ * SkillFile represents a file in the skill directory (stored as JSONB in skill_versions)
1789
+ */
1790
+ export interface SkillFile {
1791
+ path: string;
1792
+ uri?: string;
1793
+ size: number;
1794
+ hash: string;
1795
+ content?: string;
1796
+ }
1748
1797
  /**
1749
1798
  * Hardware/System related types
1750
1799
  */
@@ -2092,6 +2141,7 @@ export interface ToolInvocationDTO extends BaseModel, PermissionModelDTO {
2092
2141
  chat_message_id: string;
2093
2142
  tool_invocation_id: string;
2094
2143
  type: ToolType;
2144
+ display_name?: string;
2095
2145
  execution_id?: string;
2096
2146
  function: ToolInvocationFunction;
2097
2147
  status: ToolInvocationStatus;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inferencesh/sdk",
3
- "version": "0.5.2",
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",