@inferencesh/sdk 0.6.51 → 0.6.53
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 +56 -18
- package/dist/agent/actions.js +28 -13
- package/dist/agent/reducer.js +10 -7
- package/dist/agent/types.d.ts +4 -1
- package/dist/api/agents.d.ts +21 -1
- package/dist/api/agents.js +25 -1
- package/dist/api/integrations.d.ts +6 -6
- 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 +30 -2
- 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 +324 -126
- package/dist/types.js +107 -38
- 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.
|
|
@@ -363,7 +389,7 @@ import {
|
|
|
363
389
|
mcpTool,
|
|
364
390
|
internalTools,
|
|
365
391
|
string,
|
|
366
|
-
|
|
392
|
+
CredentialProviderGoogle,
|
|
367
393
|
} from '@inferencesh/sdk';
|
|
368
394
|
|
|
369
395
|
const clientTool = tool('get_weather')
|
|
@@ -375,7 +401,7 @@ const clientTool = tool('get_weather')
|
|
|
375
401
|
const gmailSend = httpTool('gmail_send', 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send')
|
|
376
402
|
.describe('Send an email via Gmail')
|
|
377
403
|
.method('POST')
|
|
378
|
-
.auth({ integration:
|
|
404
|
+
.auth({ integration: CredentialProviderGoogle, integrationId: 'your-integration-id' })
|
|
379
405
|
.build();
|
|
380
406
|
|
|
381
407
|
// API key or bearer auth
|
|
@@ -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
|
|
@@ -606,24 +644,24 @@ if (task.status === TaskStatusCompleted) {
|
|
|
606
644
|
|
|
607
645
|
## Integration Constants
|
|
608
646
|
|
|
609
|
-
`
|
|
647
|
+
`CredentialDTO` fields (`provider`, `type`, `auth`, `status`) use typed string unions exported as constants:
|
|
610
648
|
|
|
611
649
|
```typescript
|
|
612
|
-
import type {
|
|
650
|
+
import type { CredentialDTO } from '@inferencesh/sdk';
|
|
613
651
|
import {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
652
|
+
CredentialProviderGoogle,
|
|
653
|
+
CredentialTypeOAuth,
|
|
654
|
+
CredentialStatusConnected,
|
|
655
|
+
CredentialStatusDisconnected,
|
|
656
|
+
CredentialStatusExpired,
|
|
657
|
+
CredentialStatusError,
|
|
620
658
|
isRequirementsNotMetException,
|
|
621
659
|
} from '@inferencesh/sdk';
|
|
622
660
|
|
|
623
|
-
function isGoogleConnected(integration:
|
|
661
|
+
function isGoogleConnected(integration: CredentialDTO): boolean {
|
|
624
662
|
return (
|
|
625
|
-
integration.provider ===
|
|
626
|
-
integration.status ===
|
|
663
|
+
integration.provider === CredentialProviderGoogle &&
|
|
664
|
+
integration.status === CredentialStatusConnected
|
|
627
665
|
);
|
|
628
666
|
}
|
|
629
667
|
|
|
@@ -633,7 +671,7 @@ try {
|
|
|
633
671
|
} catch (error) {
|
|
634
672
|
if (isRequirementsNotMetException(error)) {
|
|
635
673
|
for (const req of error.errors) {
|
|
636
|
-
if (req.type === 'integration' && req.action?.provider ===
|
|
674
|
+
if (req.type === 'integration' && req.action?.provider === CredentialProviderGoogle) {
|
|
637
675
|
// User must connect Google — see https://inference.sh/docs/extend/integrations
|
|
638
676
|
}
|
|
639
677
|
}
|
|
@@ -643,9 +681,9 @@ try {
|
|
|
643
681
|
|
|
644
682
|
| Constant group | Values |
|
|
645
683
|
|----------------|--------|
|
|
646
|
-
| `
|
|
647
|
-
| `
|
|
648
|
-
| `
|
|
684
|
+
| `CredentialProvider*` | `google`, `slack`, `notion`, `github`, `x`, `microsoft`, `salesforce`, `discord`, `gcp`, `mcp`, `reddit` |
|
|
685
|
+
| `CredentialType*` | `service_account`, `oauth`, `api_key`, `wif`, `mcp` |
|
|
686
|
+
| `CredentialStatus*` | `connected`, `disconnected`, `expired`, `error` |
|
|
649
687
|
|
|
650
688
|
## Instance Status Constants
|
|
651
689
|
|
|
@@ -698,7 +736,7 @@ import type {
|
|
|
698
736
|
Task,
|
|
699
737
|
ApiAppRunRequest,
|
|
700
738
|
RunOptions,
|
|
701
|
-
|
|
739
|
+
CredentialDTO,
|
|
702
740
|
AgentTool,
|
|
703
741
|
} from '@inferencesh/sdk';
|
|
704
742
|
```
|
package/dist/agent/actions.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Action creators that handle side effects (API calls, streaming).
|
|
5
5
|
* These are created once per provider instance with access to dispatch.
|
|
6
6
|
*/
|
|
7
|
-
import { AgentRunStateWorking, AgentRunStateSubmitted, AgentRunStateInputRequired, ToolInvocationStatusAwaitingInput, ToolInvocationStatusInProgress, ToolTypeClient, } from '../types';
|
|
7
|
+
import { AgentRunStateWorking, AgentRunStateSubmitted, AgentRunStateInputRequired, ToolInvocationStatusAwaitingInput, ToolInvocationStatusInProgress, ToolTypeClient, ChatMessageStatusReady, ChatMessageStatusFailed, ChatMessageStatusCancelled, } from '../types';
|
|
8
8
|
import { isChatBusy } from '../utils';
|
|
9
9
|
import { StreamableManager } from '../http/streamable';
|
|
10
10
|
import { PollManager } from '../http/poll';
|
|
@@ -139,17 +139,19 @@ export function createActions(ctx) {
|
|
|
139
139
|
checkTurnEnd(chatData);
|
|
140
140
|
}
|
|
141
141
|
});
|
|
142
|
-
// Token-by-token streaming state,
|
|
143
|
-
|
|
142
|
+
// Token-by-token streaming state, one accumulator per message being
|
|
143
|
+
// streamed. Deltas name their message (DeltaEvent.resource_id), so state
|
|
144
|
+
// never leaks between messages the way a single shared accumulator allowed.
|
|
145
|
+
const deltaAccums = new Map();
|
|
144
146
|
// Listen for ChatMessage updates
|
|
145
147
|
manager.addEventListener('chat_messages', (message, fields) => {
|
|
146
|
-
// A
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
148
|
+
// A message that has reached a terminal state will receive no further
|
|
149
|
+
// deltas, so its accumulator is done. This bounds the map by the number
|
|
150
|
+
// of messages streaming at once rather than by chat length.
|
|
151
|
+
if (message.status === ChatMessageStatusReady
|
|
152
|
+
|| message.status === ChatMessageStatusFailed
|
|
153
|
+
|| message.status === ChatMessageStatusCancelled) {
|
|
154
|
+
deltaAccums.delete(message.id);
|
|
153
155
|
}
|
|
154
156
|
updateMessage(message, fields);
|
|
155
157
|
});
|
|
@@ -163,10 +165,23 @@ export function createActions(ctx) {
|
|
|
163
165
|
checkTurnEnd({ ...currentChat, active_run: run });
|
|
164
166
|
});
|
|
165
167
|
manager.addEventListener('delta', (evt) => {
|
|
166
|
-
if (evt
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
if (!evt || !evt.delta)
|
|
169
|
+
return;
|
|
170
|
+
// On a chat stream every task is created with an execution edge, so a
|
|
171
|
+
// delta with no resource id means something is wrong upstream — a missing
|
|
172
|
+
// or ambiguous edge, or a failed lookup. Guessing a target there would
|
|
173
|
+
// reintroduce exactly the misattribution this field exists to end, so
|
|
174
|
+
// drop it: the text still lands when the message itself arrives.
|
|
175
|
+
const messageId = evt.resource_id;
|
|
176
|
+
if (!messageId)
|
|
177
|
+
return;
|
|
178
|
+
let accum = deltaAccums.get(messageId);
|
|
179
|
+
if (!accum) {
|
|
180
|
+
accum = createLLMDeltaAccumulator();
|
|
181
|
+
deltaAccums.set(messageId, accum);
|
|
169
182
|
}
|
|
183
|
+
accum.apply(evt.delta);
|
|
184
|
+
dispatch({ type: 'DELTA_TOKEN', payload: { messageId, output: accum.toOutput() } });
|
|
170
185
|
});
|
|
171
186
|
setStreamManager(manager);
|
|
172
187
|
manager.start();
|
package/dist/agent/reducer.js
CHANGED
|
@@ -85,16 +85,19 @@ export function chatReducer(state, action) {
|
|
|
85
85
|
messages: [...state.messages, action.payload].sort((a, b) => a.order - b.order),
|
|
86
86
|
};
|
|
87
87
|
case 'DELTA_TOKEN': {
|
|
88
|
-
const output = action.payload;
|
|
88
|
+
const { messageId, output } = action.payload;
|
|
89
89
|
const msgs = state.messages;
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
// Apply to the message the delta names — never to a guessed one. A
|
|
91
|
+
// message we have not received yet (or an unnamed delta) is dropped
|
|
92
|
+
// rather than misattributed; the text still arrives with the message.
|
|
93
|
+
const target = msgs.find(m => m.id === messageId);
|
|
94
|
+
if (!target)
|
|
92
95
|
return state;
|
|
93
|
-
const textBlock =
|
|
96
|
+
const textBlock = target.content?.find(c => c.type === 'text');
|
|
94
97
|
const newContent = textBlock
|
|
95
|
-
?
|
|
96
|
-
: [{ type: 'text', text: output.response }, ...
|
|
97
|
-
const newMessages = msgs.map(m => m.id ===
|
|
98
|
+
? target.content.map(c => c.type === 'text' ? { ...c, text: output.response } : c)
|
|
99
|
+
: [{ type: 'text', text: output.response }, ...target.content];
|
|
100
|
+
const newMessages = msgs.map(m => m.id === target.id ? { ...target, content: newContent } : m);
|
|
98
101
|
return { ...state, messages: newMessages };
|
|
99
102
|
}
|
|
100
103
|
case 'SET_CONNECTION_STATUS':
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -220,7 +220,10 @@ export type ChatAction = {
|
|
|
220
220
|
payload: ChatMessageDTO;
|
|
221
221
|
} | {
|
|
222
222
|
type: 'DELTA_TOKEN';
|
|
223
|
-
payload:
|
|
223
|
+
payload: {
|
|
224
|
+
messageId: string;
|
|
225
|
+
output: Record<string, any>;
|
|
226
|
+
};
|
|
224
227
|
} | {
|
|
225
228
|
type: 'SET_CONNECTION_STATUS';
|
|
226
229
|
payload: ChatStatus;
|
package/dist/api/agents.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { HttpClient } from '../http/client';
|
|
2
2
|
import type { Response } from '../http/response';
|
|
3
3
|
import { FilesAPI } from './files';
|
|
4
|
-
import { ChatDTO, ChatMessageDTO, AgentConfigInput as AgentConfig, AgentDTO, AgentVersionDTO, CreateAgentRequest, FileDTO as File, InterruptDTO, CursorListRequest, CursorListResponse } from '../types';
|
|
4
|
+
import { ChatDTO, ChatMessageDTO, LLMDelta, LLMOutput, AgentConfigInput as AgentConfig, AgentDTO, AgentVersionDTO, CreateAgentRequest, FileDTO as File, InterruptDTO, CursorListRequest, CursorListResponse } from '../types';
|
|
5
5
|
/** Internal tool definition returned by getInternalTools */
|
|
6
6
|
export interface InternalToolDefinition {
|
|
7
7
|
id: string;
|
|
@@ -20,6 +20,20 @@ export interface AgentOptions {
|
|
|
20
20
|
/** Per-chat context variables — resolved in call tool URL templates ({{context.X}}) */
|
|
21
21
|
context?: Record<string, string>;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* One streamed token batch for a message, with everything received for that
|
|
25
|
+
* message so far. `output.response` is the assistant text as it grows.
|
|
26
|
+
*/
|
|
27
|
+
export interface AgentDelta {
|
|
28
|
+
/** The chat message the tokens belong to (normally this turn's assistant message) */
|
|
29
|
+
messageId: string;
|
|
30
|
+
/** This batch alone */
|
|
31
|
+
delta: LLMDelta;
|
|
32
|
+
/** All batches for the message merged, in the shape of the message's final output */
|
|
33
|
+
output: LLMOutput;
|
|
34
|
+
/** Producer sequence number, monotonically increasing per message */
|
|
35
|
+
seq: number;
|
|
36
|
+
}
|
|
23
37
|
export interface SendMessageOptions {
|
|
24
38
|
/** File attachments - Blob (will be uploaded) or FileDTO (already uploaded, has uri) */
|
|
25
39
|
files?: (Blob | File)[];
|
|
@@ -27,6 +41,12 @@ export interface SendMessageOptions {
|
|
|
27
41
|
onMessage?: (message: ChatMessageDTO) => void;
|
|
28
42
|
/** Callback for chat updates */
|
|
29
43
|
onChat?: (chat: ChatDTO) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Callback for token-by-token output while the assistant message is being
|
|
46
|
+
* generated. Streaming mode only: with `stream: false` there are no deltas
|
|
47
|
+
* and the message arrives whole through onMessage.
|
|
48
|
+
*/
|
|
49
|
+
onDelta?: (delta: AgentDelta) => void;
|
|
30
50
|
/** Callback when a client tool needs execution */
|
|
31
51
|
onToolCall?: (invocation: {
|
|
32
52
|
id: string;
|
package/dist/api/agents.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { StreamableManager } from '../http/streamable';
|
|
2
2
|
import { PollManager } from '../http/poll';
|
|
3
|
+
import { createLLMDeltaAccumulator } from '../delta';
|
|
3
4
|
import { ChatMessageStatusCancelled, ChatMessageStatusFailed, ChatMessageStatusReady, ToolTypeClient, ToolInvocationStatusAwaitingInput, ToolInvocationStatusInProgress, } from '../types';
|
|
4
5
|
import { isChatBusy } from '../utils';
|
|
5
6
|
const terminalMessageStatuses = new Set([ChatMessageStatusReady, ChatMessageStatusFailed, ChatMessageStatusCancelled]);
|
|
@@ -59,7 +60,7 @@ export class Agent {
|
|
|
59
60
|
async sendMessage(text, options = {}) {
|
|
60
61
|
this.dispatchedToolCalls.clear();
|
|
61
62
|
const isTemplate = typeof this.config === 'string';
|
|
62
|
-
const hasCallbacks = !!(options.onMessage || options.onChat || options.onToolCall);
|
|
63
|
+
const hasCallbacks = !!(options.onMessage || options.onChat || options.onToolCall || options.onDelta);
|
|
63
64
|
// Process files - either already uploaded (FileDTO with uri) or needs upload (Blob)
|
|
64
65
|
let imageUris;
|
|
65
66
|
let fileUris;
|
|
@@ -227,7 +228,30 @@ export class Agent {
|
|
|
227
228
|
else
|
|
228
229
|
idlePending = false;
|
|
229
230
|
});
|
|
231
|
+
// One accumulator per message being streamed, keyed by the message id
|
|
232
|
+
// the delta names, so a tool-call-only turn never shows the previous
|
|
233
|
+
// message's text. Same attribution rule as the React hooks.
|
|
234
|
+
const deltaAccums = new Map();
|
|
235
|
+
this.stream.addEventListener('delta', (evt) => {
|
|
236
|
+
if (!options.onDelta || !evt?.delta || !evt.resource_id)
|
|
237
|
+
return;
|
|
238
|
+
let accum = deltaAccums.get(evt.resource_id);
|
|
239
|
+
if (!accum) {
|
|
240
|
+
accum = createLLMDeltaAccumulator();
|
|
241
|
+
deltaAccums.set(evt.resource_id, accum);
|
|
242
|
+
}
|
|
243
|
+
accum.apply(evt.delta);
|
|
244
|
+
options.onDelta({
|
|
245
|
+
messageId: evt.resource_id,
|
|
246
|
+
delta: evt.delta,
|
|
247
|
+
output: accum.toOutput(),
|
|
248
|
+
seq: evt.seq,
|
|
249
|
+
});
|
|
250
|
+
});
|
|
230
251
|
this.stream.addEventListener('chat_messages', (message) => {
|
|
252
|
+
// A terminal message receives no further deltas.
|
|
253
|
+
if (terminalMessageStatuses.has(message.status))
|
|
254
|
+
deltaAccums.delete(message.id);
|
|
231
255
|
gate.observeMessage(message);
|
|
232
256
|
options.onMessage?.(message);
|
|
233
257
|
if (idlePending && gate.settled)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HttpClient } from '../http/client';
|
|
2
2
|
import type { Response } from '../http/response';
|
|
3
|
-
import {
|
|
3
|
+
import { CredentialDTO, CredentialConfigDTO, CredentialConnectRequest, CredentialConnectResponse, CursorListRequest, CursorListResponse } from '../types';
|
|
4
4
|
/**
|
|
5
5
|
* Integrations API
|
|
6
6
|
*/
|
|
@@ -10,15 +10,15 @@ export declare class IntegrationsAPI {
|
|
|
10
10
|
/**
|
|
11
11
|
* List integrations with cursor-based pagination
|
|
12
12
|
*/
|
|
13
|
-
list(params?: Partial<CursorListRequest>): Promise<Response<CursorListResponse<
|
|
13
|
+
list(params?: Partial<CursorListRequest>): Promise<Response<CursorListResponse<CredentialDTO>>>;
|
|
14
14
|
/**
|
|
15
15
|
* Get available integrations
|
|
16
16
|
*/
|
|
17
|
-
listAvailable(): Promise<Response<
|
|
17
|
+
listAvailable(): Promise<Response<CredentialConfigDTO[]>>;
|
|
18
18
|
/**
|
|
19
19
|
* Get integration configs
|
|
20
20
|
*/
|
|
21
|
-
getConfigs(): Promise<Response<
|
|
21
|
+
getConfigs(): Promise<Response<CredentialConfigDTO[]>>;
|
|
22
22
|
/**
|
|
23
23
|
* Get capabilities
|
|
24
24
|
*/
|
|
@@ -30,11 +30,11 @@ export declare class IntegrationsAPI {
|
|
|
30
30
|
/**
|
|
31
31
|
* Connect an integration
|
|
32
32
|
*/
|
|
33
|
-
connect(data:
|
|
33
|
+
connect(data: CredentialConnectRequest): Promise<Response<CredentialConnectResponse>>;
|
|
34
34
|
/**
|
|
35
35
|
* Get an integration by provider key
|
|
36
36
|
*/
|
|
37
|
-
get(provider: string): Promise<Response<
|
|
37
|
+
get(provider: string): Promise<Response<CredentialDTO>>;
|
|
38
38
|
/**
|
|
39
39
|
* Disconnect an integration
|
|
40
40
|
*/
|
|
@@ -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
|