@inferencesh/sdk 0.8.0 → 0.8.1

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.
@@ -1,6 +1,7 @@
1
1
  import { HttpClient } from '../http/client';
2
2
  import type { Response } from '../http/response';
3
3
  import { LiveSession, type LiveHandlers, type WebSocketConstructor } from '../live/session';
4
+ import type { JsonSchema } from '../live/schema';
4
5
  import { CursorListRequest, CursorListResponse, SocketAccess, SocketDTO, TaskDTO as Task } from '../types';
5
6
  import type { TasksAPI } from './tasks';
6
7
  /** What identifies the socket to open: the run response (which carries the access), a task, or a task id. */
@@ -16,6 +17,10 @@ export interface OpenSocketOptions {
16
17
  watchTask?: boolean;
17
18
  /** The WebSocket to dial with; defaults to the runtime's global one. */
18
19
  webSocket?: WebSocketConstructor;
20
+ /** The function's input schema: `session.sendField` routes by it. */
21
+ inputSchema?: JsonSchema | null;
22
+ /** The function's output schema: frames arrive at `onUpdate` as fields. */
23
+ outputSchema?: JsonSchema | null;
19
24
  }
20
25
  /**
21
26
  * Sockets API: the duplex connection of a stream task.
@@ -54,6 +54,8 @@ export class SocketsAPI {
54
54
  renew: async () => (await this.access(access.id)).data,
55
55
  task: options.watchTask === false ? undefined : this.tasks.watch(task),
56
56
  webSocket: options.webSocket,
57
+ inputSchema: options.inputSchema,
58
+ outputSchema: options.outputSchema,
57
59
  });
58
60
  session.connect();
59
61
  return session;
package/dist/index.d.ts CHANGED
@@ -25,8 +25,8 @@ export { SearchAPI } from './api/search';
25
25
  export { ProjectsAPI } from './api/projects';
26
26
  export { MCPServersAPI } from './api/mcp-servers';
27
27
  export { LiveSession, accessUrl } from './live/session';
28
- export type { LiveState, LiveEnd, LiveHandlers, LiveSessionOptions, WebSocketLike, WebSocketConstructor } from './live/session';
29
- export { STREAM_FORMAT, CLEAR_KEY, isLiveField, parseMediaType, pcmFormat, splitLiveSchema, binaryLiveField, alternativeLabel, } from './live/schema';
28
+ export type { LiveState, LiveEnd, LiveHandlers, LiveSessionOptions, LiveUpdate, WebSocketLike, WebSocketConstructor } from './live/session';
29
+ export { STREAM_FORMAT, CLEAR_KEY, ERROR_KEY, isLiveField, parseMediaType, pcmFormat, splitLiveSchema, binaryLiveField, alternativeLabel, alternativeTag, } from './live/schema';
30
30
  export type { JsonSchema, MediaType, PCMFormat, LiveField } from './live/schema';
31
31
  export { tool, appTool, agentTool, webhookTool, httpTool, callTool, mcpTool, internalTools, string, number, integer, boolean, enumOf, object, array, optional, } from './tool-builder';
32
32
  export type { ClientTool, ClientToolHandler } from './tool-builder';
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ export { ProjectsAPI } from './api/projects';
28
28
  export { MCPServersAPI } from './api/mcp-servers';
29
29
  // Live: the socket of a stream task and the live fields of its schemas
30
30
  export { LiveSession, accessUrl } from './live/session';
31
- export { STREAM_FORMAT, CLEAR_KEY, isLiveField, parseMediaType, pcmFormat, splitLiveSchema, binaryLiveField, alternativeLabel, } from './live/schema';
31
+ export { STREAM_FORMAT, CLEAR_KEY, ERROR_KEY, isLiveField, parseMediaType, pcmFormat, splitLiveSchema, binaryLiveField, alternativeLabel, alternativeTag, } from './live/schema';
32
32
  // Tool Builder (fluent API)
33
33
  export { tool, appTool, agentTool, webhookTool, httpTool, callTool, mcpTool, internalTools, string, number, integer, boolean, enumOf, object, array, optional, } from './tool-builder';
34
34
  // Hook Builder (fluent API)
@@ -17,11 +17,13 @@
17
17
  * JSON Schema type keeps it.
18
18
  */
19
19
  export declare const STREAM_FORMAT = "stream";
20
+ /** `{"$clear": "audio"}`: drop what has been buffered of a live output field. */
21
+ export declare const CLEAR_KEY = "$clear";
20
22
  /**
21
- * A control frame, `{"$clear": "audio"}`: drop what has been buffered of a
22
- * live output field. Reserved keys start with `$`, which no field name can.
23
+ * `{"$error": {"field": ..., "message": ...}}`: a refused frame, or anything
24
+ * else the caller should be told went wrong. The stream goes on.
23
25
  */
24
- export declare const CLEAR_KEY = "$clear";
26
+ export declare const ERROR_KEY = "$error";
25
27
  /** The part of JSON Schema these helpers read. */
26
28
  export interface JsonSchema {
27
29
  $ref?: string;
@@ -37,6 +39,9 @@ export interface JsonSchema {
37
39
  required?: string[];
38
40
  anyOf?: JsonSchema[];
39
41
  oneOf?: JsonSchema[];
42
+ discriminator?: {
43
+ propertyName?: string;
44
+ };
40
45
  }
41
46
  export declare function isLiveField(schema: JsonSchema | undefined | null): boolean;
42
47
  export interface MediaType {
@@ -65,6 +70,8 @@ export interface LiveField<S extends JsonSchema = JsonSchema> {
65
70
  * or the single item schema. Empty for a binary field.
66
71
  */
67
72
  alternatives: S[];
73
+ /** The property that tells the alternatives apart, when the schema names one. */
74
+ discriminator?: string;
68
75
  }
69
76
  /**
70
77
  * Splits a function schema into what a form renders (the ordinary
@@ -76,5 +83,10 @@ export declare function splitLiveSchema<S extends JsonSchema>(schema: S | undefi
76
83
  };
77
84
  /** The schema's one binary live field. A binary frame carries no field name. */
78
85
  export declare function binaryLiveField<S extends JsonSchema>(live: LiveField<S>[]): LiveField<S> | null;
79
- /** A label for one alternative of a JSON live field: its `type` const, else its title. */
80
- export declare function alternativeLabel(schema: JsonSchema, index: number): string;
86
+ /**
87
+ * The property whose constant names this alternative: the schema's
88
+ * discriminator, else `type`, else the first property with a constant.
89
+ */
90
+ export declare function alternativeTag(schema: JsonSchema, discriminator?: string): string | undefined;
91
+ /** A label for one alternative of a JSON live field: the constant that tags it, else its title. */
92
+ export declare function alternativeLabel(schema: JsonSchema, index: number, discriminator?: string): string;
@@ -17,11 +17,15 @@
17
17
  * JSON Schema type keeps it.
18
18
  */
19
19
  export const STREAM_FORMAT = 'stream';
20
+ // Control frames. Reserved keys start with `$`, which no field name can, so a
21
+ // control frame is never mistaken for an output field.
22
+ /** `{"$clear": "audio"}`: drop what has been buffered of a live output field. */
23
+ export const CLEAR_KEY = '$clear';
20
24
  /**
21
- * A control frame, `{"$clear": "audio"}`: drop what has been buffered of a
22
- * live output field. Reserved keys start with `$`, which no field name can.
25
+ * `{"$error": {"field": ..., "message": ...}}`: a refused frame, or anything
26
+ * else the caller should be told went wrong. The stream goes on.
23
27
  */
24
- export const CLEAR_KEY = '$clear';
28
+ export const ERROR_KEY = '$error';
25
29
  export function isLiveField(schema) {
26
30
  return !!schema && schema.format === STREAM_FORMAT;
27
31
  }
@@ -99,6 +103,7 @@ export function splitLiveSchema(schema) {
99
103
  binary,
100
104
  media: binary ? parseMediaType(resolvedItems.contentMediaType) : null,
101
105
  alternatives: binary ? [] : itemAlternatives(items, schema),
106
+ discriminator: binary ? undefined : resolvedItems.discriminator?.propertyName,
102
107
  });
103
108
  }
104
109
  return {
@@ -110,10 +115,21 @@ export function splitLiveSchema(schema) {
110
115
  export function binaryLiveField(live) {
111
116
  return live.find((field) => field.binary) ?? null;
112
117
  }
113
- /** A label for one alternative of a JSON live field: its `type` const, else its title. */
114
- export function alternativeLabel(schema, index) {
115
- const tag = schema.properties?.type?.const;
116
- if (typeof tag === 'string')
117
- return tag;
118
+ /**
119
+ * The property whose constant names this alternative: the schema's
120
+ * discriminator, else `type`, else the first property with a constant.
121
+ */
122
+ export function alternativeTag(schema, discriminator) {
123
+ const constants = Object.entries(schema.properties ?? {})
124
+ .filter(([, property]) => property && 'const' in property)
125
+ .map(([key]) => key);
126
+ return [discriminator, 'type', ...constants].find((key) => !!key && constants.includes(key));
127
+ }
128
+ /** A label for one alternative of a JSON live field: the constant that tags it, else its title. */
129
+ export function alternativeLabel(schema, index, discriminator) {
130
+ const tag = alternativeTag(schema, discriminator);
131
+ const value = tag ? schema.properties?.[tag]?.const : undefined;
132
+ if (typeof value === 'string')
133
+ return value;
118
134
  return schema.title ?? `option ${index + 1}`;
119
135
  }
@@ -11,6 +11,8 @@
11
11
  * name.
12
12
  */
13
13
  import type { SocketAccess } from '../types';
14
+ import type { TaskWatch as TasksWatch } from '../api/tasks';
15
+ import { type JsonSchema } from './schema';
14
16
  export type LiveState = 'connecting' | 'waiting' | 'live' | 'ended';
15
17
  export interface LiveEnd {
16
18
  /** The WebSocket close code, or 1006 when the session ended without one. */
@@ -33,16 +35,37 @@ export interface LiveHandlers {
33
35
  * handler the control frame reaches onPatch as `{"$clear": field}`.
34
36
  */
35
37
  onClear?: (field: string) => void;
38
+ /**
39
+ * The app refused a frame, or has something to report, and goes on:
40
+ * `{"$error": {field, message}}`. Apps on SDKs before 0.7.2 / 0.9.2 send it
41
+ * as `{"error": ...}`, which counts too unless the output has an `error`
42
+ * field. Without this handler it stays in the patch.
43
+ */
44
+ onError?: (field: string | null, message: string) => void;
45
+ /** A text frame that is not a JSON object: text the app sent as text. */
46
+ onText?: (text: string) => void;
47
+ /**
48
+ * With `outputSchema`, each frame mapped to its output field: an item of a
49
+ * live field (an ArrayBuffer for the binary one) or a new value of an
50
+ * ordinary field. Takes the place of onBinary and onPatch where those are
51
+ * not set.
52
+ */
53
+ onUpdate?: (update: LiveUpdate) => void;
54
+ }
55
+ /** One thing the app sent, mapped to its output field. */
56
+ export interface LiveUpdate {
57
+ field: string;
58
+ value: unknown;
36
59
  }
37
60
  /**
38
- * Follows the task the socket belongs to. `done` settles when the task ends
39
- * (rejected when it failed or was cancelled); `stop` ends the watch early and
40
- * leaves `done` pending.
61
+ * Follows the task the socket belongs to (what `client.tasks.watch` returns):
62
+ * `done` settles when the task ends, rejected when it failed or was
63
+ * cancelled; `stop` ends the watch early and leaves `done` pending. Only
64
+ * settling matters here, so any promise will do.
41
65
  */
42
- export interface TaskWatch {
66
+ export type TaskWatch = Pick<TasksWatch, 'stop'> & {
43
67
  done: Promise<unknown>;
44
- stop(): void;
45
- }
68
+ };
46
69
  /** The part of the WebSocket API the session uses; browsers, Node 22+ and `ws` all provide it. */
47
70
  export interface WebSocketLike {
48
71
  binaryType: string;
@@ -72,6 +95,10 @@ export interface LiveSessionOptions {
72
95
  task?: TaskWatch;
73
96
  /** The WebSocket to dial with; defaults to the runtime's global one. */
74
97
  webSocket?: WebSocketConstructor;
98
+ /** The function's input schema: `sendField` routes by it. */
99
+ inputSchema?: JsonSchema | null;
100
+ /** The function's output schema: frames arrive at onUpdate as fields. */
101
+ outputSchema?: JsonSchema | null;
75
102
  }
76
103
  /**
77
104
  * Where to dial. A browser cannot set headers on a WebSocket, so the
@@ -88,6 +115,11 @@ export declare class LiveSession {
88
115
  private readonly renew;
89
116
  private readonly task;
90
117
  private readonly WebSocket;
118
+ private readonly mapped;
119
+ private readonly outputBinary;
120
+ private readonly outputFields;
121
+ private readonly inputKnown;
122
+ private readonly inputBinary;
91
123
  private resolveEnded;
92
124
  /** Settles when the session has ended, however it ended. */
93
125
  readonly ended: Promise<LiveEnd>;
@@ -96,6 +128,10 @@ export declare class LiveSession {
96
128
  get isOpen(): boolean;
97
129
  /** Dials the relay. The session reports its progress through onState. */
98
130
  connect(): void;
131
+ private deliverText;
132
+ /** `{"error": {"message": ...}}` from an app on an older SDK, unless the output has an `error` field. */
133
+ private legacyError;
134
+ private deliverBinary;
99
135
  private watched;
100
136
  private watchTask;
101
137
  private redial;
@@ -104,6 +140,12 @@ export declare class LiveSession {
104
140
  private closeSocket;
105
141
  /** One item of the input's binary live field. */
106
142
  sendBinary(data: ArrayBuffer | ArrayBufferView): void;
143
+ /**
144
+ * One item of an input live field, or a new value of an ordinary one: a
145
+ * binary frame for the binary live field, a JSON frame otherwise. Needs
146
+ * `inputSchema`.
147
+ */
148
+ sendField(field: string, value: unknown): void;
107
149
  /** A partial input object keyed by field name. */
108
150
  sendPatch(patch: Record<string, unknown>): void;
109
151
  /** Ends the stream; the function returns and the task completes. */
@@ -1,4 +1,4 @@
1
- import { CLEAR_KEY } from './schema';
1
+ import { binaryLiveField, CLEAR_KEY, ERROR_KEY, splitLiveSchema } from './schema';
2
2
  const WS_OPEN = 1;
3
3
  // 1012: the relay is restarting and closed an end that still waited for its
4
4
  // peer. 1013: the peer did not come in time. Both mean "dial again" while the
@@ -32,6 +32,11 @@ export class LiveSession {
32
32
  this.renew = options.renew;
33
33
  this.task = options.task;
34
34
  this.WebSocket = options.webSocket ?? globalWebSocket();
35
+ this.mapped = !!options.outputSchema;
36
+ this.outputBinary = binaryLiveField(splitLiveSchema(options.outputSchema).live)?.key;
37
+ this.outputFields = new Set(Object.keys(options.outputSchema?.properties ?? {}));
38
+ this.inputKnown = !!options.inputSchema;
39
+ this.inputBinary = binaryLiveField(splitLiveSchema(options.inputSchema).live)?.key;
35
40
  this.ended = new Promise((resolve) => {
36
41
  this.resolveEnded = resolve;
37
42
  });
@@ -55,29 +60,10 @@ export class LiveSession {
55
60
  this.setState('live');
56
61
  this.task?.stop(); // the app is there; the task's fate now shows on the socket
57
62
  }
58
- if (typeof event.data === 'string') {
59
- let patch;
60
- try {
61
- patch = JSON.parse(event.data);
62
- }
63
- catch {
64
- patch = { text: event.data };
65
- }
66
- if (patch && typeof patch === 'object' && !Array.isArray(patch)) {
67
- const record = patch;
68
- const clear = record[CLEAR_KEY];
69
- if (this.handlers.onClear && typeof clear === 'string') {
70
- this.handlers.onClear(clear);
71
- delete record[CLEAR_KEY];
72
- if (Object.keys(record).length === 0)
73
- return;
74
- }
75
- this.handlers.onPatch?.(record);
76
- }
77
- }
78
- else {
79
- this.handlers.onBinary?.(event.data);
80
- }
63
+ if (typeof event.data === 'string')
64
+ this.deliverText(event.data);
65
+ else
66
+ this.deliverBinary(event.data);
81
67
  };
82
68
  ws.onclose = (event) => {
83
69
  if (this.ws !== ws)
@@ -92,6 +78,53 @@ export class LiveSession {
92
78
  this.end({ code: event.code, reason: event.reason, byCaller: this.closedByCaller, taskEnded: false });
93
79
  };
94
80
  }
81
+ deliverText(text) {
82
+ let patch;
83
+ try {
84
+ patch = JSON.parse(text);
85
+ }
86
+ catch {
87
+ patch = undefined;
88
+ }
89
+ if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
90
+ this.handlers.onText?.(text); // text the app sent as text
91
+ return;
92
+ }
93
+ const record = patch;
94
+ const arrivedEmpty = Object.keys(record).length === 0;
95
+ const { onClear, onError } = this.handlers;
96
+ if (onClear && typeof record[CLEAR_KEY] === 'string') {
97
+ onClear(record[CLEAR_KEY]);
98
+ delete record[CLEAR_KEY];
99
+ }
100
+ if (onError) {
101
+ const key = ERROR_KEY in record ? ERROR_KEY : this.legacyError(record) ? 'error' : undefined;
102
+ if (key) {
103
+ const err = record[key];
104
+ delete record[key];
105
+ const { field = null, message } = (err && typeof err === 'object' ? err : { message: String(err) });
106
+ onError(field, typeof message === 'string' ? message : JSON.stringify(err));
107
+ }
108
+ }
109
+ if (!arrivedEmpty && Object.keys(record).length === 0)
110
+ return; // it was only control frames
111
+ if (this.handlers.onPatch)
112
+ this.handlers.onPatch(record);
113
+ else if (this.mapped)
114
+ for (const [field, value] of Object.entries(record))
115
+ this.handlers.onUpdate?.({ field, value });
116
+ }
117
+ /** `{"error": {"message": ...}}` from an app on an older SDK, unless the output has an `error` field. */
118
+ legacyError(record) {
119
+ const err = record.error;
120
+ return !!err && typeof err === 'object' && 'message' in err && !this.outputFields.has('error');
121
+ }
122
+ deliverBinary(data) {
123
+ if (this.handlers.onBinary)
124
+ this.handlers.onBinary(data);
125
+ else if (this.mapped && this.outputBinary)
126
+ this.handlers.onUpdate?.({ field: this.outputBinary, value: data });
127
+ }
95
128
  watchTask() {
96
129
  if (!this.task || this.watched)
97
130
  return;
@@ -136,6 +169,19 @@ export class LiveSession {
136
169
  if (this.isOpen)
137
170
  this.ws.send(data);
138
171
  }
172
+ /**
173
+ * One item of an input live field, or a new value of an ordinary one: a
174
+ * binary frame for the binary live field, a JSON frame otherwise. Needs
175
+ * `inputSchema`.
176
+ */
177
+ sendField(field, value) {
178
+ if (!this.inputKnown)
179
+ throw new Error("sendField needs the function's inputSchema");
180
+ if (field === this.inputBinary)
181
+ this.sendBinary(value);
182
+ else
183
+ this.sendPatch({ [field]: value });
184
+ }
139
185
  /** A partial input object keyed by field name. */
140
186
  sendPatch(patch) {
141
187
  if (this.isOpen)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inferencesh/sdk",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
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",