@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 +38 -0
- package/dist/api/agents.d.ts +10 -0
- package/dist/api/agents.js +18 -1
- package/dist/api/sockets.d.ts +44 -0
- package/dist/api/sockets.js +61 -0
- package/dist/api/tasks.d.ts +19 -4
- package/dist/api/tasks.js +38 -34
- package/dist/index.d.ts +28 -1
- package/dist/index.js +25 -0
- package/dist/live/schema.d.ts +75 -0
- package/dist/live/schema.js +108 -0
- package/dist/live/session.d.ts +100 -0
- package/dist/live/session.js +139 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.js +1 -0
- package/package.json +1 -1
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
|
package/dist/api/agents.d.ts
CHANGED
|
@@ -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 */
|
package/dist/api/agents.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/api/tasks.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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";
|