@inferencesh/sdk 0.6.52 → 0.6.54

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
@@ -176,6 +176,32 @@ const task = await client.tasks.run(
176
176
  await client.tasks.cancel(task.id);
177
177
  ```
178
178
 
179
+ ### Stream Functions (Live Sockets)
180
+
181
+ A stream function keeps a socket open with its caller for the life of the task: frames go both ways until the caller closes or the app returns. `client.live` starts the task and dials its socket; the run response carries where to dial (`task.socket`).
182
+
183
+ ```typescript
184
+ const { task, session } = await client.live(
185
+ { app: 'infsh/voice-loop', function: 'stream', input: { effect: 'robot' } },
186
+ {
187
+ onState: (state) => console.log(state), // connecting → waiting → live → ended
188
+ onBinary: (pcm) => speaker.write(new Int16Array(pcm)),
189
+ onPatch: (patch) => console.log(patch), // e.g. { frames: 120 }
190
+ }
191
+ );
192
+
193
+ session.sendBinary(micFrame); // one item of the input's binary live field
194
+ session.sendPatch({ effect: 'echo' }); // change an ordinary input while it runs
195
+ session.close(); // the function returns and the task completes
196
+ await session.ended;
197
+ ```
198
+
199
+ The session is `waiting` until the app's first frame (a cold start can take a minute) and gives up if the task ends before then. It dials again with a fresh credential when the relay restarts under it. `client.sockets.open(taskOrId, handlers)` reconnects to a running task's socket, e.g. after a page reload.
200
+
201
+ What a function's socket carries is in its schemas: a live field is `{"type": "array", "format": "stream", "items": ...}`. `splitLiveSchema(schema)` separates the ordinary fields (the request body) from the live ones, and `pcmFormat(field.media)` reads the sample rate of a PCM audio field.
202
+
203
+ On Node 18–21 there is no global `WebSocket`: pass one from the `ws` package as `{ webSocket: WebSocket }`.
204
+
179
205
  ### Sessions (Stateful Execution)
180
206
 
181
207
  Sessions allow you to maintain state across multiple task invocations. The worker stays warm between calls, preserving loaded models and in-memory state.
@@ -588,6 +614,18 @@ const agent = client.agents.create({
588
614
  | `keepalive(sessionId)` | `POST /sessions/{id}/keepalive` | Reset idle expiration |
589
615
  | `end(sessionId)` | `DELETE /sessions/{id}` | End session and release worker |
590
616
 
617
+ ### `client.sockets`
618
+
619
+ | Method | HTTP | Description |
620
+ |--------|------|-------------|
621
+ | `open(taskOrId, handlers?, options?)` | — | Dial a stream task's socket; returns a `LiveSession` |
622
+ | `get(socketId)` | `GET /sockets/{id}` | The socket and what is known of its life |
623
+ | `forTask(taskId)` | `POST /sockets/list` | The task's socket, or null |
624
+ | `access(socketId)` | `POST /sockets/{id}/access` | A fresh credential for the caller's end |
625
+ | `delete(socketId)` | `DELETE /sockets/{id}` | Delete the record |
626
+
627
+ `client.live(params, handlers?, options?)` is `tasks.run(params, { wait: false })` followed by `sockets.open`.
628
+
591
629
  ## Task Status Constants
592
630
 
593
631
  ```typescript
@@ -57,6 +57,14 @@ export interface SendMessageOptions {
57
57
  stream?: boolean;
58
58
  /** Polling interval in ms when stream is false. Overrides client default. */
59
59
  pollIntervalMs?: number;
60
+ /**
61
+ * Stops waiting for the turn. Aborting rejects sendMessage with
62
+ * `signal.reason` and closes the stream or poller; the agent keeps running
63
+ * server-side (use stopChat() to cancel it). A turn parked on a tool
64
+ * approval or an authorization counts as still running, so a client that
65
+ * cannot resolve those is the typical caller.
66
+ */
67
+ signal?: AbortSignal;
60
68
  }
61
69
  export interface AgentRunOptions extends Omit<SendMessageOptions, 'stream'> {
62
70
  /** Polling interval in ms (default: 2000) */
@@ -86,6 +94,8 @@ export declare class Agent {
86
94
  userMessage: ChatMessageDTO;
87
95
  assistantMessage: ChatMessageDTO;
88
96
  }>;
97
+ /** Resolves with `wait`, or rejects with `signal.reason` and stops the stream/poller. */
98
+ private abortable;
89
99
  /** Get chat by ID */
90
100
  getChat(chatId?: string): Promise<ChatDTO | null>;
91
101
  /** Stop the current chat generation */
@@ -59,6 +59,8 @@ export class Agent {
59
59
  /** Send a message to the agent */
60
60
  async sendMessage(text, options = {}) {
61
61
  this.dispatchedToolCalls.clear();
62
+ if (options.signal?.aborted)
63
+ throw options.signal.reason;
62
64
  const isTemplate = typeof this.config === 'string';
63
65
  const hasCallbacks = !!(options.onMessage || options.onChat || options.onToolCall || options.onDelta);
64
66
  // Process files - either already uploaded (FileDTO with uri) or needs upload (Blob)
@@ -132,10 +134,25 @@ export class Agent {
132
134
  }
133
135
  // Wait for completion
134
136
  if (waitPromise) {
135
- await waitPromise;
137
+ await this.abortable(waitPromise, options.signal);
136
138
  }
137
139
  return { userMessage: response.user_message, assistantMessage: response.assistant_message };
138
140
  }
141
+ /** Resolves with `wait`, or rejects with `signal.reason` and stops the stream/poller. */
142
+ abortable(wait, signal) {
143
+ if (!signal)
144
+ return wait;
145
+ return new Promise((resolve, reject) => {
146
+ const onAbort = () => {
147
+ this.disconnect();
148
+ reject(signal.reason);
149
+ };
150
+ if (signal.aborted)
151
+ return onAbort();
152
+ signal.addEventListener('abort', onAbort, { once: true });
153
+ wait.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort));
154
+ });
155
+ }
139
156
  /** Get chat by ID */
140
157
  async getChat(chatId) {
141
158
  const id = chatId || this.chatId;
@@ -0,0 +1,44 @@
1
+ import { HttpClient } from '../http/client';
2
+ import type { Response } from '../http/response';
3
+ import { LiveSession, type LiveHandlers, type WebSocketConstructor } from '../live/session';
4
+ import { CursorListRequest, CursorListResponse, SocketAccess, SocketDTO, TaskDTO as Task } from '../types';
5
+ import type { TasksAPI } from './tasks';
6
+ /** What identifies the socket to open: the run response (which carries the access), a task, or a task id. */
7
+ export type SocketTarget = (Pick<Task, 'id' | 'status'> & {
8
+ socket?: SocketAccess;
9
+ }) | string;
10
+ export interface OpenSocketOptions {
11
+ /**
12
+ * Follow the task while waiting for the app, and end the session if the
13
+ * task ends first (default: true). Off, a task that fails before its
14
+ * worker dials leaves the session waiting until the relay's pair timeout.
15
+ */
16
+ watchTask?: boolean;
17
+ /** The WebSocket to dial with; defaults to the runtime's global one. */
18
+ webSocket?: WebSocketConstructor;
19
+ }
20
+ /**
21
+ * Sockets API: the duplex connection of a stream task.
22
+ *
23
+ * A stream function keeps a socket open with its caller for the life of the
24
+ * task. The run response carries the caller's end (`task.socket`); `open`
25
+ * dials it and gives back a LiveSession.
26
+ */
27
+ export declare class SocketsAPI {
28
+ private readonly http;
29
+ private readonly tasks;
30
+ constructor(http: HttpClient, tasks: TasksAPI);
31
+ get(id: string): Promise<Response<SocketDTO>>;
32
+ list(params?: Partial<CursorListRequest>): Promise<Response<CursorListResponse<SocketDTO>>>;
33
+ /** The task's socket, or null when it has none (not a stream function). */
34
+ forTask(taskId: string): Promise<SocketDTO | null>;
35
+ /** A fresh credential for the caller's end, e.g. after a reload or to redial. */
36
+ access(id: string): Promise<Response<SocketAccess>>;
37
+ delete(id: string): Promise<Response<void>>;
38
+ /**
39
+ * Dials the caller's end of a stream task's socket. The session is
40
+ * `waiting` until the app's first frame, then `live`; see LiveSession.
41
+ */
42
+ open(target: SocketTarget, handlers?: LiveHandlers, options?: OpenSocketOptions): Promise<LiveSession>;
43
+ }
44
+ export declare function createSocketsAPI(http: HttpClient, tasks: TasksAPI): SocketsAPI;
@@ -0,0 +1,61 @@
1
+ import { LiveSession } from '../live/session';
2
+ import { OpEqual } from '../types';
3
+ /**
4
+ * Sockets API: the duplex connection of a stream task.
5
+ *
6
+ * A stream function keeps a socket open with its caller for the life of the
7
+ * task. The run response carries the caller's end (`task.socket`); `open`
8
+ * dials it and gives back a LiveSession.
9
+ */
10
+ export class SocketsAPI {
11
+ constructor(http, tasks) {
12
+ this.http = http;
13
+ this.tasks = tasks;
14
+ }
15
+ async get(id) {
16
+ return this.http.request('get', `/sockets/${id}`);
17
+ }
18
+ async list(params) {
19
+ return this.http.request('post', '/sockets/list', { data: params });
20
+ }
21
+ /** The task's socket, or null when it has none (not a stream function). */
22
+ async forTask(taskId) {
23
+ const res = await this.list({ limit: 1, filters: [{ field: 'task_id', operator: OpEqual, value: taskId }] });
24
+ return res.data?.items?.[0] ?? null;
25
+ }
26
+ /** A fresh credential for the caller's end, e.g. after a reload or to redial. */
27
+ async access(id) {
28
+ return this.http.request('post', `/sockets/${id}/access`);
29
+ }
30
+ async delete(id) {
31
+ return this.http.request('delete', `/sockets/${id}`);
32
+ }
33
+ /**
34
+ * Dials the caller's end of a stream task's socket. The session is
35
+ * `waiting` until the app's first frame, then `live`; see LiveSession.
36
+ */
37
+ async open(target, handlers = {}, options = {}) {
38
+ const task = typeof target === 'string' ? (await this.tasks.get(target)).data : target;
39
+ let access = typeof target === 'string' ? undefined : target.socket;
40
+ let socketId = access?.id;
41
+ if (!access) {
42
+ const socket = await this.forTask(task.id);
43
+ if (!socket)
44
+ throw new Error(`task ${task.id} has no socket: is it a stream function?`);
45
+ socketId = socket.id;
46
+ access = (await this.access(socket.id)).data;
47
+ }
48
+ const session = new LiveSession({
49
+ access,
50
+ handlers,
51
+ renew: async () => (await this.access(socketId)).data,
52
+ task: options.watchTask === false ? undefined : this.tasks.watch(task),
53
+ webSocket: options.webSocket,
54
+ });
55
+ session.connect();
56
+ return session;
57
+ }
58
+ }
59
+ export function createSocketsAPI(http, tasks) {
60
+ return new SocketsAPI(http, tasks);
61
+ }
@@ -1,13 +1,12 @@
1
1
  import { HttpClient } from '../http/client';
2
2
  import type { Response } from '../http/response';
3
3
  import { TaskDTO as Task, TaskLogsDTO, TaskTimingsDTO, ApiAppRunRequest, CursorListRequest, CursorListResponse } from '../types';
4
- export interface RunOptions {
4
+ /** How to follow a task while it runs; see TasksAPI.watch. */
5
+ export interface WatchOptions {
5
6
  /** Callback for real-time status updates */
6
7
  onUpdate?: (update: Task) => void;
7
8
  /** Callback for partial updates with list of changed fields */
8
9
  onPartialUpdate?: (update: Task, fields: string[]) => void;
9
- /** Wait for task completion (default: true) */
10
- wait?: boolean;
11
10
  /** Maximum retry attempts when using polling mode (stream: false). Default: 5 */
12
11
  maxReconnects?: number;
13
12
  /** Use SSE streaming (true) or polling (false). Overrides client default. */
@@ -17,6 +16,15 @@ export interface RunOptions {
17
16
  /** Callback for streaming delta events (token-by-token updates) */
18
17
  onDelta?: (delta: Record<string, any>, seq: number) => void;
19
18
  }
19
+ export interface RunOptions extends WatchOptions {
20
+ /** Wait for task completion (default: true) */
21
+ wait?: boolean;
22
+ }
23
+ /** A task being followed: `done` settles when it ends, `stop` ends the watch early. */
24
+ export interface TaskWatch {
25
+ done: Promise<Task>;
26
+ stop(): void;
27
+ }
20
28
  /**
21
29
  * Tasks API
22
30
  */
@@ -55,8 +63,15 @@ export declare class TasksAPI {
55
63
  * Run a task and optionally wait for completion
56
64
  */
57
65
  run(params: ApiAppRunRequest, processedInput: unknown, options?: RunOptions): Promise<Task>;
66
+ /**
67
+ * Follows a task until it ends. `done` resolves with the task when it
68
+ * completes and rejects when it fails or is cancelled; `stop` ends the
69
+ * watch early and leaves `done` pending.
70
+ */
71
+ watch(task: Pick<Task, 'id' | 'status'>, options?: WatchOptions): TaskWatch;
72
+ private watchStream;
58
73
  /** Poll GET /tasks/{id}/status until terminal, full-fetch on status change. */
59
- private pollUntilTerminal;
74
+ private watchPoll;
60
75
  /**
61
76
  * Update task visibility
62
77
  */
package/dist/api/tasks.js CHANGED
@@ -69,7 +69,7 @@ export class TasksAPI {
69
69
  * Run a task and optionally wait for completion
70
70
  */
71
71
  async run(params, processedInput, options = {}) {
72
- const { onUpdate, onPartialUpdate, onDelta, wait = true, } = options;
72
+ const { wait = true } = options;
73
73
  const resp = await this.http.request('post', '/apps/run', {
74
74
  data: {
75
75
  ...params,
@@ -81,16 +81,39 @@ export class TasksAPI {
81
81
  if (!wait) {
82
82
  return stripTask(task);
83
83
  }
84
+ return this.watch(task, options).done;
85
+ }
86
+ /**
87
+ * Follows a task until it ends. `done` resolves with the task when it
88
+ * completes and rejects when it fails or is cancelled; `stop` ends the
89
+ * watch early and leaves `done` pending.
90
+ */
91
+ watch(task, options = {}) {
84
92
  const useStream = options.stream ?? this.http.getStreamDefault();
85
- if (!useStream) {
86
- return this.pollUntilTerminal(task, options);
87
- }
88
- // Wait for completion with optional updates via NDJSON streaming
93
+ return useStream ? this.watchStream(task, options) : this.watchPoll(task, options);
94
+ }
95
+ watchStream(task, options) {
96
+ const { onUpdate, onPartialUpdate, onDelta } = options;
89
97
  // Accumulate state across partial updates to preserve fields like session_id
90
98
  let accumulatedTask = { ...task };
91
99
  const { url, headers, credentials } = this.http.getStreamableConfig(`/tasks/${task.id}/stream`);
92
- return new Promise((resolve, reject) => {
93
- const streamManager = new StreamableManager({
100
+ let streamManager;
101
+ const done = new Promise((resolve, reject) => {
102
+ const settle = (data, stripped) => {
103
+ if (parseStatus(data.status) === TaskStatusCompleted) {
104
+ streamManager.stop();
105
+ resolve(stripped);
106
+ }
107
+ else if (parseStatus(data.status) === TaskStatusFailed) {
108
+ streamManager.stop();
109
+ reject(new Error(data.error || 'task failed'));
110
+ }
111
+ else if (parseStatus(data.status) === TaskStatusCancelled) {
112
+ streamManager.stop();
113
+ reject(new Error('task cancelled'));
114
+ }
115
+ };
116
+ streamManager = new StreamableManager({
94
117
  url,
95
118
  headers,
96
119
  credentials,
@@ -100,36 +123,14 @@ export class TasksAPI {
100
123
  accumulatedTask = { ...accumulatedTask, ...data };
101
124
  const stripped = stripTask(accumulatedTask);
102
125
  onUpdate?.(stripped);
103
- if (parseStatus(data.status) === TaskStatusCompleted) {
104
- streamManager.stop();
105
- resolve(stripped);
106
- }
107
- else if (parseStatus(data.status) === TaskStatusFailed) {
108
- streamManager.stop();
109
- reject(new Error(data.error || 'task failed'));
110
- }
111
- else if (parseStatus(data.status) === TaskStatusCancelled) {
112
- streamManager.stop();
113
- reject(new Error('task cancelled'));
114
- }
126
+ settle(data, stripped);
115
127
  },
116
128
  onPartialData: (data, fields) => {
117
129
  // Merge partial update, preserving fields not in this update
118
130
  accumulatedTask = { ...accumulatedTask, ...data };
119
131
  const stripped = stripTask(accumulatedTask);
120
132
  onPartialUpdate?.(stripped, fields);
121
- if (parseStatus(data.status) === TaskStatusCompleted) {
122
- streamManager.stop();
123
- resolve(stripped);
124
- }
125
- else if (parseStatus(data.status) === TaskStatusFailed) {
126
- streamManager.stop();
127
- reject(new Error(data.error || 'task failed'));
128
- }
129
- else if (parseStatus(data.status) === TaskStatusCancelled) {
130
- streamManager.stop();
131
- reject(new Error('task cancelled'));
132
- }
133
+ settle(data, stripped);
133
134
  },
134
135
  onError: (error) => {
135
136
  reject(error);
@@ -138,14 +139,16 @@ export class TasksAPI {
138
139
  });
139
140
  streamManager.start();
140
141
  });
142
+ return { done, stop: () => streamManager.stop() };
141
143
  }
142
144
  /** Poll GET /tasks/{id}/status until terminal, full-fetch on status change. */
143
- pollUntilTerminal(task, options) {
145
+ watchPoll(task, options) {
144
146
  const { onUpdate, maxReconnects = 5 } = options;
145
147
  const intervalMs = options.pollIntervalMs ?? this.http.getPollIntervalMs();
146
148
  let prevStatus = task.status;
147
- return new Promise((resolve, reject) => {
148
- const poller = new PollManager({
149
+ let poller;
150
+ const done = new Promise((resolve, reject) => {
151
+ poller = new PollManager({
149
152
  pollFunction: async () => {
150
153
  const resp = await this.http.request('get', `/tasks/${task.id}/status`);
151
154
  return resp.data;
@@ -187,6 +190,7 @@ export class TasksAPI {
187
190
  });
188
191
  poller.start();
189
192
  });
193
+ return { done, stop: () => poller.stop() };
190
194
  }
191
195
  /**
192
196
  * Update task visibility
package/dist/index.d.ts CHANGED
@@ -5,7 +5,8 @@ export { StreamManager, type StreamManagerOptions, type PartialDataWrapper } fro
5
5
  export { StreamableManager, type StreamableManagerOptions, type StreamableMessage, streamable, streamableRaw } from './http/streamable';
6
6
  export { PollManager, type PollManagerOptions } from './http/poll';
7
7
  export { InferenceError, RequirementsNotMetException, SessionError, SessionNotFoundError, SessionExpiredError, SessionEndedError, WorkerLostError, isRequirementsNotMetException, isInferenceError, isSessionError, } from './http/errors';
8
- export { TasksAPI, type RunOptions } from './api/tasks';
8
+ export { TasksAPI, type RunOptions, type WatchOptions, type TaskWatch } from './api/tasks';
9
+ export { SocketsAPI, type SocketTarget, type OpenSocketOptions } from './api/sockets';
9
10
  export { FilesAPI, type UploadFileOptions } from './api/files';
10
11
  export { AgentsAPI, Agent, type AgentOptions, type SendMessageOptions, type AgentRunOptions, type AgentDelta } from './api/agents';
11
12
  export { SessionsAPI } from './api/sessions';
@@ -23,6 +24,10 @@ export { IntegrationsAPI } from './api/integrations';
23
24
  export { SearchAPI } from './api/search';
24
25
  export { ProjectsAPI } from './api/projects';
25
26
  export { MCPServersAPI } from './api/mcp-servers';
27
+ export { LiveSession } from './live/session';
28
+ export type { LiveState, LiveEnd, LiveHandlers, LiveSessionOptions, WebSocketLike, WebSocketConstructor } from './live/session';
29
+ export { STREAM_FORMAT, isLiveField, parseMediaType, pcmFormat, splitLiveSchema, binaryLiveField, alternativeLabel, } from './live/schema';
30
+ export type { JsonSchema, MediaType, PCMFormat, LiveField } from './live/schema';
26
31
  export { tool, appTool, agentTool, webhookTool, httpTool, callTool, mcpTool, internalTools, string, number, integer, boolean, enumOf, object, array, optional, } from './tool-builder';
27
32
  export type { ClientTool, ClientToolHandler } from './tool-builder';
28
33
  export { lifecycleHook } from './hook-builder';
@@ -35,6 +40,9 @@ export type { TaskDTO as Task } from './types';
35
40
  export type { CredentialDTO as IntegrationDTO, CredentialConfigDTO as IntegrationConfigDTO, CredentialConnectRequest as IntegrationConnectRequest, CredentialConnectResponse as IntegrationConnectResponse, CredentialCompleteOAuthRequest as IntegrationCompleteOAuthRequest, CredentialRequirement as IntegrationRequirement, CredentialStatus as IntegrationStatus, CredentialScope as IntegrationScope, CredentialGrant as IntegrationGrant, CredentialType as IntegrationAuthType, CredentialProvider as IntegrationProvider, } from './types';
36
41
  import { HttpClient, type HttpClientConfig } from './http/client';
37
42
  import { TasksAPI, RunOptions } from './api/tasks';
43
+ import { SocketsAPI } from './api/sockets';
44
+ import { LiveSession, type LiveHandlers } from './live/session';
45
+ import type { OpenSocketOptions } from './api/sockets';
38
46
  import { FilesAPI, UploadFileOptions } from './api/files';
39
47
  import { AgentsAPI, Agent, AgentOptions } from './api/agents';
40
48
  import { SessionsAPI } from './api/sessions';
@@ -108,6 +116,7 @@ export declare class Inference {
108
116
  readonly search: SearchAPI;
109
117
  readonly projects: ProjectsAPI;
110
118
  readonly mcpServers: MCPServersAPI;
119
+ readonly sockets: SocketsAPI;
111
120
  constructor(config: InferenceConfig | HttpClientConfig);
112
121
  /** @internal */
113
122
  _request<T>(method: 'get' | 'post' | 'put' | 'delete', endpoint: string, options?: {
@@ -120,6 +129,24 @@ export declare class Inference {
120
129
  * Run a task on inference.sh
121
130
  */
122
131
  run(params: ApiAppRunRequest, options?: RunOptions): Promise<Task>;
132
+ /**
133
+ * Start a stream function and open its socket. The task runs until the
134
+ * session is closed (or the app returns); `session.ended` settles then.
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * const { session } = await client.live({ app: 'infsh/voice-loop', function: 'stream', input: { effect: 'robot' } }, {
139
+ * onBinary: (pcm) => speaker.write(pcm),
140
+ * onPatch: (patch) => console.log(patch),
141
+ * });
142
+ * session.sendBinary(micFrame);
143
+ * session.close();
144
+ * ```
145
+ */
146
+ live(params: ApiAppRunRequest, handlers?: LiveHandlers, options?: OpenSocketOptions): Promise<{
147
+ task: Task;
148
+ session: LiveSession;
149
+ }>;
123
150
  /**
124
151
  * Upload a file
125
152
  */
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ export { InferenceError, RequirementsNotMetException, SessionError, SessionNotFo
8
8
  isRequirementsNotMetException, isInferenceError, isSessionError, } from './http/errors';
9
9
  // API modules
10
10
  export { TasksAPI } from './api/tasks';
11
+ export { SocketsAPI } from './api/sockets';
11
12
  export { FilesAPI } from './api/files';
12
13
  export { AgentsAPI, Agent } from './api/agents';
13
14
  export { SessionsAPI } from './api/sessions';
@@ -25,6 +26,9 @@ export { IntegrationsAPI } from './api/integrations';
25
26
  export { SearchAPI } from './api/search';
26
27
  export { ProjectsAPI } from './api/projects';
27
28
  export { MCPServersAPI } from './api/mcp-servers';
29
+ // Live: the socket of a stream task and the live fields of its schemas
30
+ export { LiveSession } from './live/session';
31
+ export { STREAM_FORMAT, isLiveField, parseMediaType, pcmFormat, splitLiveSchema, binaryLiveField, alternativeLabel, } from './live/schema';
28
32
  // Tool Builder (fluent API)
29
33
  export { tool, appTool, agentTool, webhookTool, httpTool, callTool, mcpTool, internalTools, string, number, integer, boolean, enumOf, object, array, optional, } from './tool-builder';
30
34
  // Hook Builder (fluent API)
@@ -40,6 +44,7 @@ export * from './types';
40
44
  // =============================================================================
41
45
  import { HttpClient } from './http/client';
42
46
  import { TasksAPI } from './api/tasks';
47
+ import { SocketsAPI } from './api/sockets';
43
48
  import { FilesAPI } from './api/files';
44
49
  import { AgentsAPI } from './api/agents';
45
50
  import { SessionsAPI } from './api/sessions';
@@ -100,6 +105,7 @@ export class Inference {
100
105
  this.search = new SearchAPI(this.http);
101
106
  this.projects = new ProjectsAPI(this.http);
102
107
  this.mcpServers = new MCPServersAPI(this.http);
108
+ this.sockets = new SocketsAPI(this.http, this.tasks);
103
109
  }
104
110
  // Legacy methods for backward compatibility
105
111
  /** @internal */
@@ -117,6 +123,25 @@ export class Inference {
117
123
  const processedInput = await this.files.processInput(params.input);
118
124
  return this.tasks.run(params, processedInput, options);
119
125
  }
126
+ /**
127
+ * Start a stream function and open its socket. The task runs until the
128
+ * session is closed (or the app returns); `session.ended` settles then.
129
+ *
130
+ * @example
131
+ * ```typescript
132
+ * const { session } = await client.live({ app: 'infsh/voice-loop', function: 'stream', input: { effect: 'robot' } }, {
133
+ * onBinary: (pcm) => speaker.write(pcm),
134
+ * onPatch: (patch) => console.log(patch),
135
+ * });
136
+ * session.sendBinary(micFrame);
137
+ * session.close();
138
+ * ```
139
+ */
140
+ async live(params, handlers = {}, options = {}) {
141
+ const task = await this.run(params, { wait: false });
142
+ const session = await this.sockets.open(task, handlers, options);
143
+ return { task, session };
144
+ }
120
145
  /**
121
146
  * Upload a file
122
147
  */
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Live fields of a stream function.
3
+ *
4
+ * A stream function's input and output schemas are ordinary JSON Schemas in
5
+ * which some properties are live: `{"type": "array", "format": "stream",
6
+ * "items": ...}`. Their values travel over the task's socket while it runs,
7
+ * instead of in the request body or the final output. `format: "stream"` is
8
+ * the sibling of `format: "file"`: the same media, live instead of by
9
+ * reference.
10
+ *
11
+ * On the wire a binary frame is one item of the schema's binary live field
12
+ * (there is at most one per direction), and a JSON text frame is a partial
13
+ * object keyed by property name: an item of a live field, or a new value for
14
+ * an ordinary one.
15
+ *
16
+ * The helpers are generic over the schema type so a caller with a richer
17
+ * JSON Schema type keeps it.
18
+ */
19
+ export declare const STREAM_FORMAT = "stream";
20
+ /** The part of JSON Schema these helpers read. */
21
+ export interface JsonSchema {
22
+ $ref?: string;
23
+ $defs?: Record<string, JsonSchema>;
24
+ type?: string | string[];
25
+ format?: string;
26
+ title?: string;
27
+ description?: string;
28
+ contentMediaType?: string;
29
+ const?: unknown;
30
+ items?: JsonSchema | JsonSchema[];
31
+ properties?: Record<string, JsonSchema>;
32
+ required?: string[];
33
+ anyOf?: JsonSchema[];
34
+ oneOf?: JsonSchema[];
35
+ }
36
+ export declare function isLiveField(schema: JsonSchema | undefined | null): boolean;
37
+ export interface MediaType {
38
+ /** e.g. "audio/pcm" */
39
+ type: string;
40
+ /** e.g. {format: "s16le", rate: "16000", channels: "1"} */
41
+ params: Record<string, string>;
42
+ }
43
+ export declare function parseMediaType(value: string | undefined): MediaType | null;
44
+ export interface PCMFormat {
45
+ sampleRate: number;
46
+ channels: number;
47
+ }
48
+ /** The PCM format of a media type, or null when it is not 16-bit PCM audio. */
49
+ export declare function pcmFormat(media: MediaType | null): PCMFormat | null;
50
+ export interface LiveField<S extends JsonSchema = JsonSchema> {
51
+ key: string;
52
+ title: string;
53
+ description?: string;
54
+ /** Items are binary frames. */
55
+ binary: boolean;
56
+ /** Set when binary. */
57
+ media: MediaType | null;
58
+ /**
59
+ * What one item can be, references resolved: the alternatives of an anyOf,
60
+ * or the single item schema. Empty for a binary field.
61
+ */
62
+ alternatives: S[];
63
+ }
64
+ /**
65
+ * Splits a function schema into what a form renders (the ordinary
66
+ * properties) and what the socket carries (the live ones).
67
+ */
68
+ export declare function splitLiveSchema<S extends JsonSchema>(schema: S | undefined | null): {
69
+ ordinary: S | null;
70
+ live: LiveField<S>[];
71
+ };
72
+ /** The schema's one binary live field. A binary frame carries no field name. */
73
+ export declare function binaryLiveField<S extends JsonSchema>(live: LiveField<S>[]): LiveField<S> | null;
74
+ /** A label for one alternative of a JSON live field: its `type` const, else its title. */
75
+ export declare function alternativeLabel(schema: JsonSchema, index: number): string;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Live fields of a stream function.
3
+ *
4
+ * A stream function's input and output schemas are ordinary JSON Schemas in
5
+ * which some properties are live: `{"type": "array", "format": "stream",
6
+ * "items": ...}`. Their values travel over the task's socket while it runs,
7
+ * instead of in the request body or the final output. `format: "stream"` is
8
+ * the sibling of `format: "file"`: the same media, live instead of by
9
+ * reference.
10
+ *
11
+ * On the wire a binary frame is one item of the schema's binary live field
12
+ * (there is at most one per direction), and a JSON text frame is a partial
13
+ * object keyed by property name: an item of a live field, or a new value for
14
+ * an ordinary one.
15
+ *
16
+ * The helpers are generic over the schema type so a caller with a richer
17
+ * JSON Schema type keeps it.
18
+ */
19
+ export const STREAM_FORMAT = 'stream';
20
+ export function isLiveField(schema) {
21
+ return !!schema && schema.format === STREAM_FORMAT;
22
+ }
23
+ export function parseMediaType(value) {
24
+ if (!value)
25
+ return null;
26
+ const [type, ...rest] = value.split(';').map((part) => part.trim());
27
+ if (!type)
28
+ return null;
29
+ const params = {};
30
+ for (const part of rest) {
31
+ const eq = part.indexOf('=');
32
+ if (eq > 0)
33
+ params[part.slice(0, eq).trim().toLowerCase()] = part.slice(eq + 1).trim();
34
+ }
35
+ return { type: type.toLowerCase(), params };
36
+ }
37
+ /** The PCM format of a media type, or null when it is not 16-bit PCM audio. */
38
+ export function pcmFormat(media) {
39
+ if (!media || media.type !== 'audio/pcm')
40
+ return null;
41
+ if (media.params.format && media.params.format !== 's16le')
42
+ return null;
43
+ const sampleRate = Number(media.params.rate ?? 16000);
44
+ const channels = Number(media.params.channels ?? 1);
45
+ if (!Number.isFinite(sampleRate) || sampleRate <= 0 || !Number.isFinite(channels) || channels <= 0)
46
+ return null;
47
+ return { sampleRate, channels };
48
+ }
49
+ /** Resolves a `#/$defs/` reference against the root, recursively. */
50
+ function deref(schema, root) {
51
+ if (!schema.$ref || !root.$defs)
52
+ return schema;
53
+ const name = schema.$ref.replace('#/$defs/', '');
54
+ const target = root.$defs[name];
55
+ if (!target)
56
+ return schema;
57
+ // The reference's own fields (title, description) win over the target's.
58
+ return { ...deref(target, root), ...schema };
59
+ }
60
+ function itemAlternatives(items, root) {
61
+ const resolved = deref(items, root);
62
+ const options = (resolved.anyOf ?? resolved.oneOf);
63
+ if (options?.length)
64
+ return options.map((option) => deref(option, root));
65
+ return [resolved];
66
+ }
67
+ /**
68
+ * Splits a function schema into what a form renders (the ordinary
69
+ * properties) and what the socket carries (the live ones).
70
+ */
71
+ export function splitLiveSchema(schema) {
72
+ if (!schema?.properties)
73
+ return { ordinary: schema ?? null, live: [] };
74
+ const ordinary = {};
75
+ const live = [];
76
+ for (const [key, property] of Object.entries(schema.properties)) {
77
+ if (!isLiveField(property)) {
78
+ ordinary[key] = property;
79
+ continue;
80
+ }
81
+ const items = ((Array.isArray(property.items) ? property.items[0] : property.items) ?? {});
82
+ const resolvedItems = deref(items, schema);
83
+ const binary = resolvedItems.format === 'binary';
84
+ live.push({
85
+ key,
86
+ title: property.title ?? key,
87
+ description: property.description,
88
+ binary,
89
+ media: binary ? parseMediaType(resolvedItems.contentMediaType) : null,
90
+ alternatives: binary ? [] : itemAlternatives(items, schema),
91
+ });
92
+ }
93
+ return {
94
+ ordinary: { ...schema, properties: ordinary, required: schema.required?.filter((key) => key in ordinary) },
95
+ live,
96
+ };
97
+ }
98
+ /** The schema's one binary live field. A binary frame carries no field name. */
99
+ export function binaryLiveField(live) {
100
+ return live.find((field) => field.binary) ?? null;
101
+ }
102
+ /** A label for one alternative of a JSON live field: its `type` const, else its title. */
103
+ export function alternativeLabel(schema, index) {
104
+ const tag = schema.properties?.type?.const;
105
+ if (typeof tag === 'string')
106
+ return tag;
107
+ return schema.title ?? `option ${index + 1}`;
108
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * One end of a stream task's socket, as the caller holds it.
3
+ *
4
+ * The task's run response carries where to dial and a short-lived credential
5
+ * (SocketAccess). The relay pairs this connection with the worker's. Until the
6
+ * app's first frame arrives nobody may be on the other end yet (the worker can
7
+ * still be pulling an image), so the session is `waiting`, not `live`.
8
+ *
9
+ * Frames follow the function's schemas (see ./schema): a binary frame is one
10
+ * item of the binary live field, a text frame is a JSON object keyed by field
11
+ * name.
12
+ */
13
+ import type { SocketAccess } from '../types';
14
+ export type LiveState = 'connecting' | 'waiting' | 'live' | 'ended';
15
+ export interface LiveEnd {
16
+ /** The WebSocket close code, or 1006 when the session ended without one. */
17
+ code: number;
18
+ reason: string;
19
+ /** The caller asked for it. */
20
+ byCaller: boolean;
21
+ /** The task ended before the app connected, so the session gave up waiting. */
22
+ taskEnded: boolean;
23
+ }
24
+ export interface LiveHandlers {
25
+ onState?: (state: LiveState, end?: LiveEnd) => void;
26
+ /** An item of the output's binary live field. */
27
+ onBinary?: (data: ArrayBuffer) => void;
28
+ /** A partial output object keyed by field name. */
29
+ onPatch?: (patch: Record<string, unknown>) => void;
30
+ }
31
+ /**
32
+ * Follows the task the socket belongs to. `done` settles when the task ends
33
+ * (rejected when it failed or was cancelled); `stop` ends the watch early and
34
+ * leaves `done` pending.
35
+ */
36
+ export interface TaskWatch {
37
+ done: Promise<unknown>;
38
+ stop(): void;
39
+ }
40
+ /** The part of the WebSocket API the session uses; browsers, Node 22+ and `ws` all provide it. */
41
+ export interface WebSocketLike {
42
+ binaryType: string;
43
+ readyState: number;
44
+ onopen: ((event: unknown) => void) | null;
45
+ onmessage: ((event: {
46
+ data: unknown;
47
+ }) => void) | null;
48
+ onclose: ((event: {
49
+ code: number;
50
+ reason: string;
51
+ }) => void) | null;
52
+ send(data: string | ArrayBuffer | ArrayBufferView): void;
53
+ close(code?: number, reason?: string): void;
54
+ }
55
+ export type WebSocketConstructor = new (url: string) => WebSocketLike;
56
+ export interface LiveSessionOptions {
57
+ access: SocketAccess;
58
+ handlers?: LiveHandlers;
59
+ /** Issues a fresh credential (POST /sockets/{id}/access) for a redial. */
60
+ renew?: () => Promise<SocketAccess>;
61
+ /**
62
+ * Ends the session when the task ends before the app connected. Without it
63
+ * a task that fails before its worker dials leaves the caller waiting on
64
+ * the relay until the pair timeout.
65
+ */
66
+ task?: TaskWatch;
67
+ /** The WebSocket to dial with; defaults to the runtime's global one. */
68
+ webSocket?: WebSocketConstructor;
69
+ }
70
+ export declare class LiveSession {
71
+ private ws;
72
+ private current;
73
+ private closedByCaller;
74
+ private redials;
75
+ private access;
76
+ private readonly handlers;
77
+ private readonly renew;
78
+ private readonly task;
79
+ private readonly WebSocket;
80
+ private resolveEnded;
81
+ /** Settles when the session has ended, however it ended. */
82
+ readonly ended: Promise<LiveEnd>;
83
+ constructor(options: LiveSessionOptions);
84
+ get state(): LiveState;
85
+ get isOpen(): boolean;
86
+ /** Dials the relay. The session reports its progress through onState. */
87
+ connect(): void;
88
+ private watched;
89
+ private watchTask;
90
+ private redial;
91
+ private setState;
92
+ private end;
93
+ private closeSocket;
94
+ /** One item of the input's binary live field. */
95
+ sendBinary(data: ArrayBuffer | ArrayBufferView): void;
96
+ /** A partial input object keyed by field name. */
97
+ sendPatch(patch: Record<string, unknown>): void;
98
+ /** Ends the stream; the function returns and the task completes. */
99
+ close(): void;
100
+ }
@@ -0,0 +1,139 @@
1
+ const WS_OPEN = 1;
2
+ // 1012: the relay is restarting and closed an end that still waited for its
3
+ // peer. 1013: the peer did not come in time. Both mean "dial again" while the
4
+ // task is alive, and neither means anything once frames have flowed.
5
+ const REDIAL_CODES = new Set([1012, 1013]);
6
+ const MAX_REDIALS = 5;
7
+ function globalWebSocket() {
8
+ const ctor = globalThis.WebSocket;
9
+ if (!ctor) {
10
+ throw new Error('no WebSocket in this runtime: pass one (e.g. from the "ws" package) as webSocket');
11
+ }
12
+ return ctor;
13
+ }
14
+ export class LiveSession {
15
+ constructor(options) {
16
+ this.ws = null;
17
+ this.current = 'connecting';
18
+ this.closedByCaller = false;
19
+ this.redials = 0;
20
+ this.watched = false;
21
+ this.access = options.access;
22
+ this.handlers = options.handlers ?? {};
23
+ this.renew = options.renew;
24
+ this.task = options.task;
25
+ this.WebSocket = options.webSocket ?? globalWebSocket();
26
+ this.ended = new Promise((resolve) => {
27
+ this.resolveEnded = resolve;
28
+ });
29
+ }
30
+ get state() {
31
+ return this.current;
32
+ }
33
+ get isOpen() {
34
+ return this.ws?.readyState === WS_OPEN;
35
+ }
36
+ /** Dials the relay. The session reports its progress through onState. */
37
+ connect() {
38
+ this.setState('connecting');
39
+ this.watchTask();
40
+ // A browser cannot set headers on a WebSocket, so the credential rides in
41
+ // the query, as the relay documents.
42
+ const ws = new this.WebSocket(`${this.access.url}?access_token=${encodeURIComponent(this.access.token)}`);
43
+ ws.binaryType = 'arraybuffer';
44
+ this.ws = ws;
45
+ ws.onopen = () => this.setState('waiting');
46
+ ws.onmessage = (event) => {
47
+ if (this.current !== 'live') {
48
+ this.setState('live');
49
+ this.task?.stop(); // the app is there; the task's fate now shows on the socket
50
+ }
51
+ if (typeof event.data === 'string') {
52
+ let patch;
53
+ try {
54
+ patch = JSON.parse(event.data);
55
+ }
56
+ catch {
57
+ patch = { text: event.data };
58
+ }
59
+ if (patch && typeof patch === 'object' && !Array.isArray(patch)) {
60
+ this.handlers.onPatch?.(patch);
61
+ }
62
+ }
63
+ else {
64
+ this.handlers.onBinary?.(event.data);
65
+ }
66
+ };
67
+ ws.onclose = (event) => {
68
+ if (this.ws !== ws)
69
+ return;
70
+ this.ws = null;
71
+ const waiting = this.current !== 'live';
72
+ if (!this.closedByCaller && waiting && REDIAL_CODES.has(event.code) && this.redials < MAX_REDIALS) {
73
+ this.redials += 1;
74
+ void this.redial();
75
+ return;
76
+ }
77
+ this.end({ code: event.code, reason: event.reason, byCaller: this.closedByCaller, taskEnded: false });
78
+ };
79
+ }
80
+ watchTask() {
81
+ if (!this.task || this.watched)
82
+ return;
83
+ this.watched = true;
84
+ const endedBeforeLive = (reason) => {
85
+ if (this.current === 'ended' || this.current === 'live')
86
+ return;
87
+ this.closeSocket(1000, 'task ended');
88
+ this.end({ code: 1000, reason, byCaller: false, taskEnded: true });
89
+ };
90
+ this.task.done.then(() => endedBeforeLive('the task ended before the app connected'), (err) => endedBeforeLive(err instanceof Error ? err.message : String(err)));
91
+ }
92
+ async redial() {
93
+ try {
94
+ if (this.renew)
95
+ this.access = await this.renew();
96
+ if (this.current !== 'ended' && !this.closedByCaller)
97
+ this.connect();
98
+ }
99
+ catch (err) {
100
+ this.end({ code: 1006, reason: err instanceof Error ? err.message : 'could not redial', byCaller: false, taskEnded: false });
101
+ }
102
+ }
103
+ setState(state, end) {
104
+ this.current = state;
105
+ this.handlers.onState?.(state, end);
106
+ }
107
+ end(end) {
108
+ if (this.current === 'ended')
109
+ return;
110
+ this.task?.stop();
111
+ this.setState('ended', end);
112
+ this.resolveEnded(end);
113
+ }
114
+ closeSocket(code, reason) {
115
+ const ws = this.ws;
116
+ this.ws = null; // its onclose must not end the session a second time
117
+ ws?.close(code, reason);
118
+ }
119
+ /** One item of the input's binary live field. */
120
+ sendBinary(data) {
121
+ if (this.isOpen)
122
+ this.ws.send(data);
123
+ }
124
+ /** A partial input object keyed by field name. */
125
+ sendPatch(patch) {
126
+ if (this.isOpen)
127
+ this.ws.send(JSON.stringify(patch));
128
+ }
129
+ /** Ends the stream; the function returns and the task completes. */
130
+ close() {
131
+ this.closedByCaller = true;
132
+ if (this.ws) {
133
+ this.ws.close(1000, 'done'); // onclose ends the session
134
+ }
135
+ else {
136
+ this.end({ code: 1000, reason: 'done', byCaller: true, taskEnded: false });
137
+ }
138
+ }
139
+ }
package/dist/types.d.ts CHANGED
@@ -1618,6 +1618,11 @@ export interface ChatDTO extends BaseModelDTO, PermissionModelDTO {
1618
1618
  context?: {
1619
1619
  [key: string]: string;
1620
1620
  };
1621
+ /**
1622
+ * ChannelContext names the channel this chat came through (slack, a
1623
+ * wearable's tag, ...). Unset for chats started in the app or the SDK.
1624
+ */
1625
+ channel_context?: ChannelContext;
1621
1626
  agent_id?: string;
1622
1627
  agent?: AgentDTO;
1623
1628
  agent_version_id?: string;
@@ -4704,6 +4709,7 @@ export declare const NotificationTypePaymentFailed: NotificationType;
4704
4709
  export declare const NotificationTypeUsageSummary: NotificationType;
4705
4710
  export declare const NotificationTypeSpendingLimit: NotificationType;
4706
4711
  export declare const NotificationTypeInvoice: NotificationType;
4712
+ export declare const NotificationTypeCreditNote: NotificationType;
4707
4713
  export declare const NotificationTypeSubscriptionCreated: NotificationType;
4708
4714
  export declare const NotificationTypeSubscriptionCredit: NotificationType;
4709
4715
  export declare const NotificationTypeSubscriptionCanceled: NotificationType;
package/dist/types.js CHANGED
@@ -859,6 +859,7 @@ export const NotificationTypePaymentFailed = "payment_failed";
859
859
  export const NotificationTypeUsageSummary = "usage_summary";
860
860
  export const NotificationTypeSpendingLimit = "spending_limit";
861
861
  export const NotificationTypeInvoice = "invoice";
862
+ export const NotificationTypeCreditNote = "credit_note";
862
863
  export const NotificationTypeSubscriptionCreated = "subscription_created";
863
864
  export const NotificationTypeSubscriptionCredit = "subscription_credit";
864
865
  export const NotificationTypeSubscriptionCanceled = "subscription_canceled";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inferencesh/sdk",
3
- "version": "0.6.52",
3
+ "version": "0.6.54",
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",