@durion/react 0.1.0
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/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/gateway-v0/stream-state-query-fn.d.ts +9 -0
- package/dist/gateway-v0/stream-state-query-fn.js +28 -0
- package/dist/gateway-v0/urls.d.ts +11 -0
- package/dist/gateway-v0/urls.js +46 -0
- package/dist/gateway-v0/useGatewayV0StreamState.d.ts +24 -0
- package/dist/gateway-v0/useGatewayV0StreamState.js +26 -0
- package/dist/gateway-v0/useGatewayV0TokenStream.d.ts +16 -0
- package/dist/gateway-v0/useGatewayV0TokenStream.js +12 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +32 -0
- package/dist/useRunStream.d.ts +50 -0
- package/dist/useRunStream.js +210 -0
- package/dist/useSendSignal.d.ts +25 -0
- package/dist/useSendSignal.js +57 -0
- package/dist/useWorkflowStreamState.d.ts +25 -0
- package/dist/useWorkflowStreamState.js +72 -0
- package/dist/useWorkflowTokenStream.d.ts +31 -0
- package/dist/useWorkflowTokenStream.js +104 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Durion contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# @durion/react
|
|
2
|
+
|
|
3
|
+
Universal React hooks for **Durion workflows** (Temporal + Gateway HTTP API).
|
|
4
|
+
|
|
5
|
+
## Quick Start — `useRunStream` (recommended)
|
|
6
|
+
|
|
7
|
+
A single hook that merges SSE token streaming + polled stream-state. Pass a `runId` and get back real-time text, run status, and metadata.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { useRunStream, useSendSignal } from '@durion/react';
|
|
11
|
+
|
|
12
|
+
function ChatResponse({ runId }: { runId: string }) {
|
|
13
|
+
const { text, status, run, error } = useRunStream(runId, {
|
|
14
|
+
baseURL: '', // same-origin or your gateway URL
|
|
15
|
+
accessToken: token, // optional
|
|
16
|
+
pollIntervalMs: 1000,
|
|
17
|
+
onToken: (delta) => console.log(delta),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const { send } = useSendSignal({ baseURL: '' });
|
|
21
|
+
|
|
22
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
23
|
+
if (status === 'waiting_for_input') {
|
|
24
|
+
return <button onClick={() => send(runId, { approved: true })}>Approve</button>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return <div>{text}</div>;
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### What `useRunStream` does internally
|
|
32
|
+
|
|
33
|
+
1. Opens `EventSource` to `GET /v0/runs/:id/token-stream` → accumulates text deltas
|
|
34
|
+
2. Polls `GET /v0/runs/:id/stream-state` → provides run metadata (status, step count, messages)
|
|
35
|
+
3. Falls back to polled `partialReply` if SSE misses early tokens
|
|
36
|
+
|
|
37
|
+
### `useSendSignal`
|
|
38
|
+
|
|
39
|
+
Sends signals (e.g. HITL input) to a running workflow via `POST /v0/runs/:id/signal`.
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
const { send, isSending, error } = useSendSignal({ baseURL: '', accessToken });
|
|
43
|
+
await send(runId, { approved: true });
|
|
44
|
+
await send(runId, 'some text', 'custom:signal-name'); // custom signal name
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Low-level hooks (escape hatches)
|
|
50
|
+
|
|
51
|
+
For non-gateway or custom paths:
|
|
52
|
+
|
|
53
|
+
| Concern | Hook | Notes |
|
|
54
|
+
|---------|------|-------|
|
|
55
|
+
| Token SSE | **`useWorkflowTokenStream`** | Requires `getTokenStreamUrl(runId)`. Has `subscribeThenStart` for zero-drop. |
|
|
56
|
+
| Polled UI state | **`useWorkflowStreamState`** | Requires custom `queryFn`. |
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Gateway v0 helpers
|
|
61
|
+
|
|
62
|
+
URL builders and pre-wired wrappers for the standard Gateway API v0 paths:
|
|
63
|
+
|
|
64
|
+
| Helper | Purpose |
|
|
65
|
+
|--------|---------|
|
|
66
|
+
| `useGatewayV0TokenStream` | SSE via `GET /v0/runs/:id/token-stream` |
|
|
67
|
+
| `useGatewayV0StreamState` | Polls `GET /v0/runs/:id/stream-state` |
|
|
68
|
+
| `gatewayV0WorkflowsStartUrl` | URL builder for `POST /v0/workflows/start` |
|
|
69
|
+
| `gatewayV0SignalUrl` | URL builder for `POST /v0/runs/:id/signal` |
|
|
70
|
+
| `gatewayV0ResultUrl` | URL builder for `GET /v0/runs/:id/result` |
|
|
71
|
+
| `createGatewayV0StreamStateQueryFn` | Factory for poll `queryFn` |
|
|
72
|
+
|
|
73
|
+
> **Note:** These are now considered low-level. Prefer `useRunStream` for new code.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Run-scoped streaming
|
|
78
|
+
|
|
79
|
+
`useRunStream` takes a `runId`, optional `baseURL`, and optional gateway token — the same shape you’d use for any client that opens an SSE URL and polls run metadata against **your** HTTP API (e.g. Gateway API v0), not a vendor-hosted endpoint.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Peer dependency
|
|
84
|
+
|
|
85
|
+
- `react` ^18 or ^19
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { StreamState } from '@durion/sdk';
|
|
2
|
+
export interface GatewayV0StreamStateQueryFnOptions {
|
|
3
|
+
accessToken?: string;
|
|
4
|
+
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Default `queryFn` for {@link useGatewayV0StreamState}: `GET /v0/runs/:id/stream-state`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createGatewayV0StreamStateQueryFn(baseURL: string, options?: GatewayV0StreamStateQueryFnOptions): (workflowId: string, signal: AbortSignal) => Promise<StreamState>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createGatewayV0StreamStateQueryFn = createGatewayV0StreamStateQueryFn;
|
|
4
|
+
const urls_1 = require("./urls");
|
|
5
|
+
async function resolveHeaders(h) {
|
|
6
|
+
if (h == null)
|
|
7
|
+
return undefined;
|
|
8
|
+
return typeof h === 'function' ? await h() : h;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Default `queryFn` for {@link useGatewayV0StreamState}: `GET /v0/runs/:id/stream-state`.
|
|
12
|
+
*/
|
|
13
|
+
function createGatewayV0StreamStateQueryFn(baseURL, options) {
|
|
14
|
+
return async (workflowId, signal) => {
|
|
15
|
+
const url = (0, urls_1.gatewayV0StreamStateUrl)(baseURL, workflowId);
|
|
16
|
+
const extra = await resolveHeaders(options?.headers);
|
|
17
|
+
const headers = new Headers(extra);
|
|
18
|
+
if (options?.accessToken != null && options.accessToken !== '') {
|
|
19
|
+
headers.set('Authorization', `Bearer ${options.accessToken}`);
|
|
20
|
+
}
|
|
21
|
+
const res = await fetch(url, { signal, headers });
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
const text = await res.text().catch(() => '');
|
|
24
|
+
throw new Error(`Stream state failed (${res.status}): ${text || res.statusText}`);
|
|
25
|
+
}
|
|
26
|
+
return (await res.json());
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Join `baseURL` (no trailing slash) with an absolute path. */
|
|
2
|
+
export declare function trimGatewayBase(url: string): string;
|
|
3
|
+
export declare function gatewayV0StreamStateUrl(baseURL: string, runId: string): string;
|
|
4
|
+
export declare function gatewayV0TokenStreamUrl(baseURL: string, runId: string, options?: {
|
|
5
|
+
accessToken?: string;
|
|
6
|
+
}): string;
|
|
7
|
+
export declare function gatewayV0SignalUrl(baseURL: string, runId: string): string;
|
|
8
|
+
export declare function gatewayV0ResultUrl(baseURL: string, runId: string): string;
|
|
9
|
+
export declare function gatewayV0RunDescribeUrl(baseURL: string, runId: string): string;
|
|
10
|
+
export declare function gatewayV0WorkflowsStartUrl(baseURL: string): string;
|
|
11
|
+
export declare function gatewayV0AgentsStartUrl(baseURL: string): string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.trimGatewayBase = trimGatewayBase;
|
|
4
|
+
exports.gatewayV0StreamStateUrl = gatewayV0StreamStateUrl;
|
|
5
|
+
exports.gatewayV0TokenStreamUrl = gatewayV0TokenStreamUrl;
|
|
6
|
+
exports.gatewayV0SignalUrl = gatewayV0SignalUrl;
|
|
7
|
+
exports.gatewayV0ResultUrl = gatewayV0ResultUrl;
|
|
8
|
+
exports.gatewayV0RunDescribeUrl = gatewayV0RunDescribeUrl;
|
|
9
|
+
exports.gatewayV0WorkflowsStartUrl = gatewayV0WorkflowsStartUrl;
|
|
10
|
+
exports.gatewayV0AgentsStartUrl = gatewayV0AgentsStartUrl;
|
|
11
|
+
/** Join `baseURL` (no trailing slash) with an absolute path. */
|
|
12
|
+
function trimGatewayBase(url) {
|
|
13
|
+
return url.replace(/\/$/, '');
|
|
14
|
+
}
|
|
15
|
+
function joinBasePath(baseURL, path) {
|
|
16
|
+
const base = trimGatewayBase(baseURL);
|
|
17
|
+
return base.length > 0 ? `${base}${path}` : path;
|
|
18
|
+
}
|
|
19
|
+
const RUNS = '/v0/runs';
|
|
20
|
+
const WORKFLOWS = '/v0/workflows';
|
|
21
|
+
function gatewayV0StreamStateUrl(baseURL, runId) {
|
|
22
|
+
return joinBasePath(baseURL, `${RUNS}/${encodeURIComponent(runId)}/stream-state`);
|
|
23
|
+
}
|
|
24
|
+
function gatewayV0TokenStreamUrl(baseURL, runId, options) {
|
|
25
|
+
const u = joinBasePath(baseURL, `${RUNS}/${encodeURIComponent(runId)}/token-stream`);
|
|
26
|
+
const token = options?.accessToken;
|
|
27
|
+
if (token == null || token === '')
|
|
28
|
+
return u;
|
|
29
|
+
const sep = u.includes('?') ? '&' : '?';
|
|
30
|
+
return `${u}${sep}access_token=${encodeURIComponent(token)}`;
|
|
31
|
+
}
|
|
32
|
+
function gatewayV0SignalUrl(baseURL, runId) {
|
|
33
|
+
return joinBasePath(baseURL, `${RUNS}/${encodeURIComponent(runId)}/signal`);
|
|
34
|
+
}
|
|
35
|
+
function gatewayV0ResultUrl(baseURL, runId) {
|
|
36
|
+
return joinBasePath(baseURL, `${RUNS}/${encodeURIComponent(runId)}/result`);
|
|
37
|
+
}
|
|
38
|
+
function gatewayV0RunDescribeUrl(baseURL, runId) {
|
|
39
|
+
return joinBasePath(baseURL, `${RUNS}/${encodeURIComponent(runId)}`);
|
|
40
|
+
}
|
|
41
|
+
function gatewayV0WorkflowsStartUrl(baseURL) {
|
|
42
|
+
return joinBasePath(baseURL, `${WORKFLOWS}/start`);
|
|
43
|
+
}
|
|
44
|
+
function gatewayV0AgentsStartUrl(baseURL) {
|
|
45
|
+
return joinBasePath(baseURL, '/v0/agents/start');
|
|
46
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { StreamState } from '@durion/sdk';
|
|
2
|
+
export interface UseGatewayV0StreamStateOptions {
|
|
3
|
+
workflowId?: string | null;
|
|
4
|
+
/** API origin, no trailing slash. Use `''` for same-origin. */
|
|
5
|
+
baseURL: string;
|
|
6
|
+
pollIntervalMs?: number;
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
/** Sent as `Authorization: Bearer` on poll requests. */
|
|
9
|
+
accessToken?: string;
|
|
10
|
+
extraHeaders?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
11
|
+
/**
|
|
12
|
+
* Replace the default Gateway v0 poll entirely (escape hatch).
|
|
13
|
+
* When set, `baseURL` / `accessToken` / `extraHeaders` are ignored for fetching.
|
|
14
|
+
*/
|
|
15
|
+
queryFn?: (workflowId: string, signal: AbortSignal) => Promise<StreamState>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Polls `GET /v0/runs/:id/stream-state` (Gateway API v0). See `docs/gateway-api-v0.md` in the repo.
|
|
19
|
+
*/
|
|
20
|
+
export declare function useGatewayV0StreamState(options: UseGatewayV0StreamStateOptions): {
|
|
21
|
+
state: StreamState | null;
|
|
22
|
+
error: Error | null;
|
|
23
|
+
loading: boolean;
|
|
24
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useGatewayV0StreamState = useGatewayV0StreamState;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const useWorkflowStreamState_1 = require("../useWorkflowStreamState");
|
|
6
|
+
const stream_state_query_fn_1 = require("./stream-state-query-fn");
|
|
7
|
+
/**
|
|
8
|
+
* Polls `GET /v0/runs/:id/stream-state` (Gateway API v0). See `docs/gateway-api-v0.md` in the repo.
|
|
9
|
+
*/
|
|
10
|
+
function useGatewayV0StreamState(options) {
|
|
11
|
+
const { baseURL, accessToken, extraHeaders, queryFn: userQueryFn, workflowId, pollIntervalMs, enabled, } = options;
|
|
12
|
+
const queryFn = (0, react_1.useMemo)(() => {
|
|
13
|
+
if (userQueryFn)
|
|
14
|
+
return userQueryFn;
|
|
15
|
+
return (0, stream_state_query_fn_1.createGatewayV0StreamStateQueryFn)(baseURL, {
|
|
16
|
+
accessToken,
|
|
17
|
+
headers: extraHeaders,
|
|
18
|
+
});
|
|
19
|
+
}, [userQueryFn, baseURL, accessToken, extraHeaders]);
|
|
20
|
+
return (0, useWorkflowStreamState_1.useWorkflowStreamState)({
|
|
21
|
+
workflowId,
|
|
22
|
+
pollIntervalMs,
|
|
23
|
+
enabled,
|
|
24
|
+
queryFn,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface UseGatewayV0TokenStreamOptions {
|
|
2
|
+
/** API origin, no trailing slash. Use `''` for same-origin (e.g. Vite proxy). */
|
|
3
|
+
baseURL: string;
|
|
4
|
+
/** When set, appended as `access_token` query (SSE; browsers cannot set Authorization on EventSource). */
|
|
5
|
+
accessToken?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Token SSE for Gateway API v0 (`GET /v0/runs/:id/token-stream`). Thin wrapper over {@link useWorkflowTokenStream}. */
|
|
8
|
+
export declare function useGatewayV0TokenStream(options: UseGatewayV0TokenStreamOptions): {
|
|
9
|
+
text: string;
|
|
10
|
+
status: import("../useWorkflowTokenStream").WorkflowTokenStreamStatus;
|
|
11
|
+
error: Error | null;
|
|
12
|
+
isStreaming: boolean;
|
|
13
|
+
subscribeThenStart: (workflowId: string, afterConnected: () => void | Promise<void>) => void;
|
|
14
|
+
reset: () => void;
|
|
15
|
+
close: () => void;
|
|
16
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useGatewayV0TokenStream = useGatewayV0TokenStream;
|
|
4
|
+
const useWorkflowTokenStream_1 = require("../useWorkflowTokenStream");
|
|
5
|
+
const urls_1 = require("./urls");
|
|
6
|
+
/** Token SSE for Gateway API v0 (`GET /v0/runs/:id/token-stream`). Thin wrapper over {@link useWorkflowTokenStream}. */
|
|
7
|
+
function useGatewayV0TokenStream(options) {
|
|
8
|
+
const { baseURL, accessToken } = options;
|
|
9
|
+
return (0, useWorkflowTokenStream_1.useWorkflowTokenStream)({
|
|
10
|
+
getTokenStreamUrl: (runId) => (0, urls_1.gatewayV0TokenStreamUrl)(baseURL, runId, { accessToken }),
|
|
11
|
+
});
|
|
12
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { useRunStream } from './useRunStream';
|
|
2
|
+
export type { UseRunStreamOptions, UseRunStreamReturn, RunStreamStatus, } from './useRunStream';
|
|
3
|
+
export { useSendSignal } from './useSendSignal';
|
|
4
|
+
export type { UseSendSignalOptions, UseSendSignalReturn, } from './useSendSignal';
|
|
5
|
+
export { useWorkflowStreamState } from './useWorkflowStreamState';
|
|
6
|
+
export type { UseWorkflowStreamStateOptions } from './useWorkflowStreamState';
|
|
7
|
+
export { useWorkflowTokenStream } from './useWorkflowTokenStream';
|
|
8
|
+
export type { UseWorkflowTokenStreamOptions, WorkflowTokenStreamStatus, } from './useWorkflowTokenStream';
|
|
9
|
+
export { trimGatewayBase, gatewayV0StreamStateUrl, gatewayV0TokenStreamUrl, gatewayV0SignalUrl, gatewayV0ResultUrl, gatewayV0RunDescribeUrl, gatewayV0WorkflowsStartUrl, gatewayV0AgentsStartUrl, } from './gateway-v0/urls';
|
|
10
|
+
export { createGatewayV0StreamStateQueryFn } from './gateway-v0/stream-state-query-fn';
|
|
11
|
+
export type { GatewayV0StreamStateQueryFnOptions } from './gateway-v0/stream-state-query-fn';
|
|
12
|
+
export { useGatewayV0TokenStream } from './gateway-v0/useGatewayV0TokenStream';
|
|
13
|
+
export type { UseGatewayV0TokenStreamOptions } from './gateway-v0/useGatewayV0TokenStream';
|
|
14
|
+
export { useGatewayV0StreamState } from './gateway-v0/useGatewayV0StreamState';
|
|
15
|
+
export type { UseGatewayV0StreamStateOptions } from './gateway-v0/useGatewayV0StreamState';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Recommended API — unified hooks
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.useGatewayV0StreamState = exports.useGatewayV0TokenStream = exports.createGatewayV0StreamStateQueryFn = exports.gatewayV0AgentsStartUrl = exports.gatewayV0WorkflowsStartUrl = exports.gatewayV0RunDescribeUrl = exports.gatewayV0ResultUrl = exports.gatewayV0SignalUrl = exports.gatewayV0TokenStreamUrl = exports.gatewayV0StreamStateUrl = exports.trimGatewayBase = exports.useWorkflowTokenStream = exports.useWorkflowStreamState = exports.useSendSignal = exports.useRunStream = void 0;
|
|
7
|
+
var useRunStream_1 = require("./useRunStream");
|
|
8
|
+
Object.defineProperty(exports, "useRunStream", { enumerable: true, get: function () { return useRunStream_1.useRunStream; } });
|
|
9
|
+
var useSendSignal_1 = require("./useSendSignal");
|
|
10
|
+
Object.defineProperty(exports, "useSendSignal", { enumerable: true, get: function () { return useSendSignal_1.useSendSignal; } });
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Low-level hooks (escape hatches)
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
var useWorkflowStreamState_1 = require("./useWorkflowStreamState");
|
|
15
|
+
Object.defineProperty(exports, "useWorkflowStreamState", { enumerable: true, get: function () { return useWorkflowStreamState_1.useWorkflowStreamState; } });
|
|
16
|
+
var useWorkflowTokenStream_1 = require("./useWorkflowTokenStream");
|
|
17
|
+
Object.defineProperty(exports, "useWorkflowTokenStream", { enumerable: true, get: function () { return useWorkflowTokenStream_1.useWorkflowTokenStream; } });
|
|
18
|
+
var urls_1 = require("./gateway-v0/urls");
|
|
19
|
+
Object.defineProperty(exports, "trimGatewayBase", { enumerable: true, get: function () { return urls_1.trimGatewayBase; } });
|
|
20
|
+
Object.defineProperty(exports, "gatewayV0StreamStateUrl", { enumerable: true, get: function () { return urls_1.gatewayV0StreamStateUrl; } });
|
|
21
|
+
Object.defineProperty(exports, "gatewayV0TokenStreamUrl", { enumerable: true, get: function () { return urls_1.gatewayV0TokenStreamUrl; } });
|
|
22
|
+
Object.defineProperty(exports, "gatewayV0SignalUrl", { enumerable: true, get: function () { return urls_1.gatewayV0SignalUrl; } });
|
|
23
|
+
Object.defineProperty(exports, "gatewayV0ResultUrl", { enumerable: true, get: function () { return urls_1.gatewayV0ResultUrl; } });
|
|
24
|
+
Object.defineProperty(exports, "gatewayV0RunDescribeUrl", { enumerable: true, get: function () { return urls_1.gatewayV0RunDescribeUrl; } });
|
|
25
|
+
Object.defineProperty(exports, "gatewayV0WorkflowsStartUrl", { enumerable: true, get: function () { return urls_1.gatewayV0WorkflowsStartUrl; } });
|
|
26
|
+
Object.defineProperty(exports, "gatewayV0AgentsStartUrl", { enumerable: true, get: function () { return urls_1.gatewayV0AgentsStartUrl; } });
|
|
27
|
+
var stream_state_query_fn_1 = require("./gateway-v0/stream-state-query-fn");
|
|
28
|
+
Object.defineProperty(exports, "createGatewayV0StreamStateQueryFn", { enumerable: true, get: function () { return stream_state_query_fn_1.createGatewayV0StreamStateQueryFn; } });
|
|
29
|
+
var useGatewayV0TokenStream_1 = require("./gateway-v0/useGatewayV0TokenStream");
|
|
30
|
+
Object.defineProperty(exports, "useGatewayV0TokenStream", { enumerable: true, get: function () { return useGatewayV0TokenStream_1.useGatewayV0TokenStream; } });
|
|
31
|
+
var useGatewayV0StreamState_1 = require("./gateway-v0/useGatewayV0StreamState");
|
|
32
|
+
Object.defineProperty(exports, "useGatewayV0StreamState", { enumerable: true, get: function () { return useGatewayV0StreamState_1.useGatewayV0StreamState; } });
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { StreamState } from '@durion/sdk';
|
|
2
|
+
export type RunStreamStatus = 'idle' | 'connecting' | 'streaming' | 'waiting_for_input' | 'completed' | 'error';
|
|
3
|
+
export interface UseRunStreamOptions {
|
|
4
|
+
/** API origin, no trailing slash. Use `''` for same-origin (e.g. Vite proxy). */
|
|
5
|
+
baseURL: string;
|
|
6
|
+
/**
|
|
7
|
+
* Optional auth token.
|
|
8
|
+
* - SSE: appended as `access_token` query param (EventSource cannot set headers).
|
|
9
|
+
* - Poll: sent as `Authorization: Bearer`.
|
|
10
|
+
*/
|
|
11
|
+
accessToken?: string;
|
|
12
|
+
/** Polling interval for stream-state in ms. Default 1500. */
|
|
13
|
+
pollIntervalMs?: number;
|
|
14
|
+
/** Throttle SSE renders in ms. Default 0 (every frame). */
|
|
15
|
+
throttleInMs?: number;
|
|
16
|
+
/** Called for every text-delta token from SSE. */
|
|
17
|
+
onToken?: (delta: string) => void;
|
|
18
|
+
/** Called when the run status changes. */
|
|
19
|
+
onStatusChange?: (status: RunStreamStatus) => void;
|
|
20
|
+
/** When false, disables both SSE and polling. Default true when runId is set. */
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface UseRunStreamReturn {
|
|
24
|
+
/** Accumulated LLM text (SSE deltas preferred, polled partialReply as fallback). */
|
|
25
|
+
text: string;
|
|
26
|
+
/** High-level run status. */
|
|
27
|
+
status: RunStreamStatus;
|
|
28
|
+
/** Full StreamState from polling (messages, currentStep, etc). */
|
|
29
|
+
run: StreamState | null;
|
|
30
|
+
/** Latest error, if any. */
|
|
31
|
+
error: Error | null;
|
|
32
|
+
/** True while SSE is open or connecting. */
|
|
33
|
+
isStreaming: boolean;
|
|
34
|
+
/** Manually close the SSE stream. */
|
|
35
|
+
close: () => void;
|
|
36
|
+
/** Reset all state (for reuse with a new run). */
|
|
37
|
+
reset: () => void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Unified hook that combines SSE token streaming with polled stream-state.
|
|
41
|
+
*
|
|
42
|
+
* Internally opens an `EventSource` to `/v0/runs/:id/token-stream` for real-time
|
|
43
|
+
* text deltas **and** polls `/v0/runs/:id/stream-state` for run metadata (status,
|
|
44
|
+
* currentStep, messages, HITL flags). The `text` return merges both: SSE deltas are
|
|
45
|
+
* preferred, with the polled `partialReply` used as a fallback for tokens that the
|
|
46
|
+
* SSE connection missed.
|
|
47
|
+
*
|
|
48
|
+
* Same mental model as other run-scoped stream hooks: `runId`, optional `baseURL`, token for auth.
|
|
49
|
+
*/
|
|
50
|
+
export declare function useRunStream(runId: string | null | undefined, options: UseRunStreamOptions): UseRunStreamReturn;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useRunStream = useRunStream;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const urls_1 = require("./gateway-v0/urls");
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Hook
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
/**
|
|
10
|
+
* Unified hook that combines SSE token streaming with polled stream-state.
|
|
11
|
+
*
|
|
12
|
+
* Internally opens an `EventSource` to `/v0/runs/:id/token-stream` for real-time
|
|
13
|
+
* text deltas **and** polls `/v0/runs/:id/stream-state` for run metadata (status,
|
|
14
|
+
* currentStep, messages, HITL flags). The `text` return merges both: SSE deltas are
|
|
15
|
+
* preferred, with the polled `partialReply` used as a fallback for tokens that the
|
|
16
|
+
* SSE connection missed.
|
|
17
|
+
*
|
|
18
|
+
* Same mental model as other run-scoped stream hooks: `runId`, optional `baseURL`, token for auth.
|
|
19
|
+
*/
|
|
20
|
+
function useRunStream(runId, options) {
|
|
21
|
+
const { baseURL, accessToken, pollIntervalMs = 1500, throttleInMs = 0, onToken, onStatusChange, enabled: enabledOpt, } = options;
|
|
22
|
+
const active = enabledOpt !== false && runId != null && String(runId).length > 0;
|
|
23
|
+
// ---- State ---------------------------------------------------------------
|
|
24
|
+
const [sseText, setSseText] = (0, react_1.useState)('');
|
|
25
|
+
const [status, setStatusRaw] = (0, react_1.useState)('idle');
|
|
26
|
+
const [run, setRun] = (0, react_1.useState)(null);
|
|
27
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
28
|
+
// Stable refs for callbacks (avoid re-subscribing on every render)
|
|
29
|
+
const onTokenRef = (0, react_1.useRef)(onToken);
|
|
30
|
+
onTokenRef.current = onToken;
|
|
31
|
+
const onStatusChangeRef = (0, react_1.useRef)(onStatusChange);
|
|
32
|
+
onStatusChangeRef.current = onStatusChange;
|
|
33
|
+
// Whether SSE has received at least one delta (used for fallback logic)
|
|
34
|
+
const sseReceivedRef = (0, react_1.useRef)(false);
|
|
35
|
+
// Throttle support
|
|
36
|
+
const throttlePendingRef = (0, react_1.useRef)('');
|
|
37
|
+
const throttleTimerRef = (0, react_1.useRef)(null);
|
|
38
|
+
const setStatus = (0, react_1.useCallback)((s) => {
|
|
39
|
+
setStatusRaw((prev) => {
|
|
40
|
+
if (prev === s)
|
|
41
|
+
return prev;
|
|
42
|
+
onStatusChangeRef.current?.(s);
|
|
43
|
+
return s;
|
|
44
|
+
});
|
|
45
|
+
}, []);
|
|
46
|
+
// ---- SSE EventSource -----------------------------------------------------
|
|
47
|
+
const esRef = (0, react_1.useRef)(null);
|
|
48
|
+
const sseClosedCleanlyRef = (0, react_1.useRef)(false);
|
|
49
|
+
const closeSSE = (0, react_1.useCallback)(() => {
|
|
50
|
+
if (throttleTimerRef.current) {
|
|
51
|
+
clearTimeout(throttleTimerRef.current);
|
|
52
|
+
throttleTimerRef.current = null;
|
|
53
|
+
}
|
|
54
|
+
// Flush any remaining throttled text
|
|
55
|
+
if (throttlePendingRef.current) {
|
|
56
|
+
const pending = throttlePendingRef.current;
|
|
57
|
+
throttlePendingRef.current = '';
|
|
58
|
+
setSseText((t) => t + pending);
|
|
59
|
+
}
|
|
60
|
+
esRef.current?.close();
|
|
61
|
+
esRef.current = null;
|
|
62
|
+
}, []);
|
|
63
|
+
// ---- Public API ----------------------------------------------------------
|
|
64
|
+
const close = (0, react_1.useCallback)(() => {
|
|
65
|
+
closeSSE();
|
|
66
|
+
}, [closeSSE]);
|
|
67
|
+
const reset = (0, react_1.useCallback)(() => {
|
|
68
|
+
closeSSE();
|
|
69
|
+
sseClosedCleanlyRef.current = false;
|
|
70
|
+
sseReceivedRef.current = false;
|
|
71
|
+
throttlePendingRef.current = '';
|
|
72
|
+
setSseText('');
|
|
73
|
+
setRun(null);
|
|
74
|
+
setError(null);
|
|
75
|
+
setStatus('idle');
|
|
76
|
+
}, [closeSSE, setStatus]);
|
|
77
|
+
// ---- SSE lifecycle -------------------------------------------------------
|
|
78
|
+
(0, react_1.useEffect)(() => {
|
|
79
|
+
if (!active || !runId)
|
|
80
|
+
return;
|
|
81
|
+
sseClosedCleanlyRef.current = false;
|
|
82
|
+
sseReceivedRef.current = false;
|
|
83
|
+
throttlePendingRef.current = '';
|
|
84
|
+
setSseText('');
|
|
85
|
+
setError(null);
|
|
86
|
+
setStatus('connecting');
|
|
87
|
+
const url = (0, urls_1.gatewayV0TokenStreamUrl)(baseURL, runId, { accessToken });
|
|
88
|
+
const es = new EventSource(url);
|
|
89
|
+
esRef.current = es;
|
|
90
|
+
const flushThrottle = () => {
|
|
91
|
+
if (throttlePendingRef.current) {
|
|
92
|
+
const pending = throttlePendingRef.current;
|
|
93
|
+
throttlePendingRef.current = '';
|
|
94
|
+
setSseText((t) => t + pending);
|
|
95
|
+
}
|
|
96
|
+
throttleTimerRef.current = null;
|
|
97
|
+
};
|
|
98
|
+
es.addEventListener('open', () => {
|
|
99
|
+
setStatus('streaming');
|
|
100
|
+
});
|
|
101
|
+
es.addEventListener('message', (ev) => {
|
|
102
|
+
try {
|
|
103
|
+
const part = JSON.parse(ev.data);
|
|
104
|
+
if (part.type === 'text-delta' && part.delta) {
|
|
105
|
+
sseReceivedRef.current = true;
|
|
106
|
+
onTokenRef.current?.(part.delta);
|
|
107
|
+
if (throttleInMs > 0) {
|
|
108
|
+
throttlePendingRef.current += part.delta;
|
|
109
|
+
if (!throttleTimerRef.current) {
|
|
110
|
+
throttleTimerRef.current = setTimeout(flushThrottle, throttleInMs);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
setSseText((t) => t + part.delta);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (part.type === 'finish') {
|
|
118
|
+
sseClosedCleanlyRef.current = true;
|
|
119
|
+
flushThrottle();
|
|
120
|
+
setStatus('completed');
|
|
121
|
+
closeSSE();
|
|
122
|
+
}
|
|
123
|
+
if (part.type === 'error') {
|
|
124
|
+
setError(new Error(part.error ?? 'Stream error'));
|
|
125
|
+
setStatus('error');
|
|
126
|
+
closeSSE();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
/* ignore malformed SSE */
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
es.addEventListener('error', () => {
|
|
134
|
+
if (sseClosedCleanlyRef.current) {
|
|
135
|
+
closeSSE();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
closeSSE();
|
|
139
|
+
// Don't set error status if we never got streaming data — polling will handle it
|
|
140
|
+
if (sseReceivedRef.current) {
|
|
141
|
+
setStatus('error');
|
|
142
|
+
setError((prev) => prev ?? new Error('EventSource error'));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
return () => {
|
|
146
|
+
closeSSE();
|
|
147
|
+
};
|
|
148
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
149
|
+
}, [active, runId, baseURL, accessToken]);
|
|
150
|
+
// ---- Poll stream-state ---------------------------------------------------
|
|
151
|
+
(0, react_1.useEffect)(() => {
|
|
152
|
+
if (!active || !runId) {
|
|
153
|
+
setRun(null);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const ac = new AbortController();
|
|
157
|
+
let timer;
|
|
158
|
+
const tick = async () => {
|
|
159
|
+
try {
|
|
160
|
+
const url = (0, urls_1.gatewayV0StreamStateUrl)(baseURL, runId);
|
|
161
|
+
const headers = new Headers();
|
|
162
|
+
if (accessToken) {
|
|
163
|
+
headers.set('Authorization', `Bearer ${accessToken}`);
|
|
164
|
+
}
|
|
165
|
+
const res = await fetch(url, { signal: ac.signal, headers });
|
|
166
|
+
if (!res.ok)
|
|
167
|
+
return; // silent — SSE or next poll will recover
|
|
168
|
+
const state = (await res.json());
|
|
169
|
+
if (ac.signal.aborted)
|
|
170
|
+
return;
|
|
171
|
+
setRun(state);
|
|
172
|
+
// Sync status from polled state when SSE hasn't reported
|
|
173
|
+
if (state.status === 'waiting_for_input') {
|
|
174
|
+
setStatus('waiting_for_input');
|
|
175
|
+
}
|
|
176
|
+
else if (state.status === 'completed') {
|
|
177
|
+
setStatus('completed');
|
|
178
|
+
}
|
|
179
|
+
else if (state.status === 'error') {
|
|
180
|
+
setStatus('error');
|
|
181
|
+
}
|
|
182
|
+
// Stop polling once terminal
|
|
183
|
+
if (state.status === 'completed' || state.status === 'error') {
|
|
184
|
+
if (timer)
|
|
185
|
+
clearInterval(timer);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
if (e.name === 'AbortError')
|
|
190
|
+
return;
|
|
191
|
+
// Non-fatal — SSE is primary; poll failures are silent
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
void tick();
|
|
195
|
+
timer = setInterval(() => void tick(), pollIntervalMs);
|
|
196
|
+
return () => {
|
|
197
|
+
ac.abort();
|
|
198
|
+
if (timer)
|
|
199
|
+
clearInterval(timer);
|
|
200
|
+
};
|
|
201
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
202
|
+
}, [active, runId, baseURL, accessToken, pollIntervalMs]);
|
|
203
|
+
// ---- Compute final text --------------------------------------------------
|
|
204
|
+
// SSE text takes priority; fall back to polled partialReply when SSE hasn't received data
|
|
205
|
+
const text = sseReceivedRef.current || sseText.length > 0
|
|
206
|
+
? sseText
|
|
207
|
+
: run?.partialReply ?? '';
|
|
208
|
+
const isStreaming = status === 'streaming' || status === 'connecting';
|
|
209
|
+
return { text, status, run, error, isStreaming, close, reset };
|
|
210
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface UseSendSignalOptions {
|
|
2
|
+
/** API origin, no trailing slash. Use `''` for same-origin. */
|
|
3
|
+
baseURL: string;
|
|
4
|
+
/** Sent as `Authorization: Bearer` on signal requests. */
|
|
5
|
+
accessToken?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface UseSendSignalReturn {
|
|
8
|
+
/** Send a signal to a running workflow. Default signal name is `durion:user-input`. */
|
|
9
|
+
send: (runId: string, data: unknown, signalName?: string) => Promise<void>;
|
|
10
|
+
/** True while the signal POST is in flight. */
|
|
11
|
+
isSending: boolean;
|
|
12
|
+
/** Last error from a failed send, if any. */
|
|
13
|
+
error: Error | null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Simple hook for sending signals (e.g. HITL input) to a running workflow.
|
|
17
|
+
*
|
|
18
|
+
* Wraps `POST /v0/runs/:id/signal` with loading/error state.
|
|
19
|
+
*
|
|
20
|
+
* ```tsx
|
|
21
|
+
* const { send, isSending, error } = useSendSignal({ baseURL: '' });
|
|
22
|
+
* await send(runId, { approved: true });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function useSendSignal(options: UseSendSignalOptions): UseSendSignalReturn;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useSendSignal = useSendSignal;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const urls_1 = require("./gateway-v0/urls");
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Hook
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
/**
|
|
10
|
+
* Simple hook for sending signals (e.g. HITL input) to a running workflow.
|
|
11
|
+
*
|
|
12
|
+
* Wraps `POST /v0/runs/:id/signal` with loading/error state.
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* const { send, isSending, error } = useSendSignal({ baseURL: '' });
|
|
16
|
+
* await send(runId, { approved: true });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
function useSendSignal(options) {
|
|
20
|
+
const { baseURL, accessToken } = options;
|
|
21
|
+
const [isSending, setIsSending] = (0, react_1.useState)(false);
|
|
22
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
23
|
+
// Stable refs to avoid stale closures
|
|
24
|
+
const baseURLRef = (0, react_1.useRef)(baseURL);
|
|
25
|
+
baseURLRef.current = baseURL;
|
|
26
|
+
const accessTokenRef = (0, react_1.useRef)(accessToken);
|
|
27
|
+
accessTokenRef.current = accessToken;
|
|
28
|
+
const send = (0, react_1.useCallback)(async (runId, data, signalName = 'durion:user-input') => {
|
|
29
|
+
setIsSending(true);
|
|
30
|
+
setError(null);
|
|
31
|
+
try {
|
|
32
|
+
const url = (0, urls_1.gatewayV0SignalUrl)(baseURLRef.current, runId);
|
|
33
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
34
|
+
if (accessTokenRef.current) {
|
|
35
|
+
headers['Authorization'] = `Bearer ${accessTokenRef.current}`;
|
|
36
|
+
}
|
|
37
|
+
const res = await fetch(url, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers,
|
|
40
|
+
body: JSON.stringify({ name: signalName, data }),
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
const body = await res.text().catch(() => '');
|
|
44
|
+
throw new Error(`Signal failed (${res.status}): ${body || res.statusText}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
49
|
+
setError(err);
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
setIsSending(false);
|
|
54
|
+
}
|
|
55
|
+
}, []);
|
|
56
|
+
return { send, isSending, error };
|
|
57
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { StreamState } from '@durion/sdk';
|
|
2
|
+
export interface UseWorkflowStreamStateOptions {
|
|
3
|
+
/** Workflow execution id (Temporal workflow id). */
|
|
4
|
+
workflowId?: string | null;
|
|
5
|
+
/** Polling interval in ms. Default 1500. */
|
|
6
|
+
pollIntervalMs?: number;
|
|
7
|
+
/**
|
|
8
|
+
* Poll your backend for `StreamState` JSON (e.g. Temporal `streamState` query behind HTTP).
|
|
9
|
+
* Not streaming — interval polling only. Implement with `fetch` to **your** route(s).
|
|
10
|
+
*/
|
|
11
|
+
queryFn: (workflowId: string, signal: AbortSignal) => Promise<StreamState>;
|
|
12
|
+
/** When false, no polling. Default true when workflowId is set. */
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Polls JSON workflow UI state (status, partial reply, HITL flags, …) via **`queryFn`** — not SSE.
|
|
17
|
+
*
|
|
18
|
+
* `loading` is **only** true until the first successful fetch for the current `workflowId`
|
|
19
|
+
* (background interval polls do not flip it — avoids UI flicker).
|
|
20
|
+
*/
|
|
21
|
+
export declare function useWorkflowStreamState(options: UseWorkflowStreamStateOptions): {
|
|
22
|
+
state: StreamState | null;
|
|
23
|
+
error: Error | null;
|
|
24
|
+
loading: boolean;
|
|
25
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useWorkflowStreamState = useWorkflowStreamState;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
/**
|
|
6
|
+
* Polls JSON workflow UI state (status, partial reply, HITL flags, …) via **`queryFn`** — not SSE.
|
|
7
|
+
*
|
|
8
|
+
* `loading` is **only** true until the first successful fetch for the current `workflowId`
|
|
9
|
+
* (background interval polls do not flip it — avoids UI flicker).
|
|
10
|
+
*/
|
|
11
|
+
function useWorkflowStreamState(options) {
|
|
12
|
+
const { workflowId, pollIntervalMs = 1500, queryFn: userQueryFn, enabled: enabledOpt, } = options;
|
|
13
|
+
const enabled = enabledOpt !== false && workflowId != null && String(workflowId).length > 0;
|
|
14
|
+
const [state, setState] = (0, react_1.useState)(null);
|
|
15
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
16
|
+
const [loading, setLoading] = (0, react_1.useState)(false);
|
|
17
|
+
const queryFnRef = (0, react_1.useRef)(userQueryFn);
|
|
18
|
+
queryFnRef.current = userQueryFn;
|
|
19
|
+
/** After first successful poll for this workflowId, background ticks keep loading false. */
|
|
20
|
+
const hasDataRef = (0, react_1.useRef)(false);
|
|
21
|
+
(0, react_1.useEffect)(() => {
|
|
22
|
+
if (!workflowId) {
|
|
23
|
+
hasDataRef.current = false;
|
|
24
|
+
setState(null);
|
|
25
|
+
setError(null);
|
|
26
|
+
setLoading(false);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!enabled) {
|
|
30
|
+
setLoading(false);
|
|
31
|
+
setError(null);
|
|
32
|
+
setState(null);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
hasDataRef.current = false;
|
|
36
|
+
setState(null);
|
|
37
|
+
setError(null);
|
|
38
|
+
const ac = new AbortController();
|
|
39
|
+
let timer;
|
|
40
|
+
const tick = async () => {
|
|
41
|
+
try {
|
|
42
|
+
if (!hasDataRef.current) {
|
|
43
|
+
setLoading(true);
|
|
44
|
+
}
|
|
45
|
+
setError(null);
|
|
46
|
+
const next = await queryFnRef.current(workflowId, ac.signal);
|
|
47
|
+
if (!ac.signal.aborted) {
|
|
48
|
+
setState(next);
|
|
49
|
+
hasDataRef.current = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
if (e.name === 'AbortError')
|
|
54
|
+
return;
|
|
55
|
+
if (!ac.signal.aborted)
|
|
56
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
if (!ac.signal.aborted)
|
|
60
|
+
setLoading(false);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
void tick();
|
|
64
|
+
timer = setInterval(() => void tick(), pollIntervalMs);
|
|
65
|
+
return () => {
|
|
66
|
+
ac.abort();
|
|
67
|
+
if (timer)
|
|
68
|
+
clearInterval(timer);
|
|
69
|
+
};
|
|
70
|
+
}, [enabled, workflowId, pollIntervalMs]);
|
|
71
|
+
return { state, error, loading };
|
|
72
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type WorkflowTokenStreamStatus = 'idle' | 'connecting' | 'streaming' | 'done' | 'error';
|
|
2
|
+
export interface UseWorkflowTokenStreamOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the **full URL** of the token SSE endpoint for a workflow run (your server’s contract).
|
|
5
|
+
* You own host, path, and auth — pass the final string the browser’s `EventSource` should open.
|
|
6
|
+
*/
|
|
7
|
+
getTokenStreamUrl: (workflowId: string) => string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Subscribes to LLM token deltas over SSE at **`getTokenStreamUrl(workflowId)`** (your server).
|
|
11
|
+
* Expects Vercel AI UI–style `text-delta` / `finish` messages; accumulates deltas into `text`.
|
|
12
|
+
*
|
|
13
|
+
* **Subscribe-before-start:** use {@link subscribeThenStart} so the EventSource is open *before* you
|
|
14
|
+
* start the workflow (or next model round), so early chunks aren’t missed — same “connect, then run”
|
|
15
|
+
* ordering you’d use with any ephemeral SSE channel.
|
|
16
|
+
*/
|
|
17
|
+
export declare function useWorkflowTokenStream(options: UseWorkflowTokenStreamOptions): {
|
|
18
|
+
/** Accumulated text from `text-delta` parts. */
|
|
19
|
+
text: string;
|
|
20
|
+
status: WorkflowTokenStreamStatus;
|
|
21
|
+
error: Error | null;
|
|
22
|
+
/** True while waiting for open or actively receiving deltas (until `done` / `error`). */
|
|
23
|
+
isStreaming: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Open SSE for `workflowId`, then run `afterConnected` (e.g. start workflow).
|
|
26
|
+
* Call again for each new model round (e.g. after HITL reject).
|
|
27
|
+
*/
|
|
28
|
+
subscribeThenStart: (workflowId: string, afterConnected: () => void | Promise<void>) => void;
|
|
29
|
+
reset: () => void;
|
|
30
|
+
close: () => void;
|
|
31
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useWorkflowTokenStream = useWorkflowTokenStream;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
/**
|
|
6
|
+
* Subscribes to LLM token deltas over SSE at **`getTokenStreamUrl(workflowId)`** (your server).
|
|
7
|
+
* Expects Vercel AI UI–style `text-delta` / `finish` messages; accumulates deltas into `text`.
|
|
8
|
+
*
|
|
9
|
+
* **Subscribe-before-start:** use {@link subscribeThenStart} so the EventSource is open *before* you
|
|
10
|
+
* start the workflow (or next model round), so early chunks aren’t missed — same “connect, then run”
|
|
11
|
+
* ordering you’d use with any ephemeral SSE channel.
|
|
12
|
+
*/
|
|
13
|
+
function useWorkflowTokenStream(options) {
|
|
14
|
+
const { getTokenStreamUrl } = options;
|
|
15
|
+
const [text, setText] = (0, react_1.useState)('');
|
|
16
|
+
const [status, setStatus] = (0, react_1.useState)('idle');
|
|
17
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
18
|
+
const esRef = (0, react_1.useRef)(null);
|
|
19
|
+
const closedCleanlyRef = (0, react_1.useRef)(false);
|
|
20
|
+
const close = (0, react_1.useCallback)(() => {
|
|
21
|
+
esRef.current?.close();
|
|
22
|
+
esRef.current = null;
|
|
23
|
+
}, []);
|
|
24
|
+
const reset = (0, react_1.useCallback)(() => {
|
|
25
|
+
close();
|
|
26
|
+
closedCleanlyRef.current = false;
|
|
27
|
+
setText('');
|
|
28
|
+
setStatus('idle');
|
|
29
|
+
setError(null);
|
|
30
|
+
}, [close]);
|
|
31
|
+
(0, react_1.useEffect)(() => () => close(), [close]);
|
|
32
|
+
const getTokenStreamUrlRef = (0, react_1.useRef)(getTokenStreamUrl);
|
|
33
|
+
getTokenStreamUrlRef.current = getTokenStreamUrl;
|
|
34
|
+
const subscribeThenStart = (0, react_1.useCallback)((workflowId, afterConnected) => {
|
|
35
|
+
close();
|
|
36
|
+
closedCleanlyRef.current = false;
|
|
37
|
+
setText('');
|
|
38
|
+
setError(null);
|
|
39
|
+
setStatus('connecting');
|
|
40
|
+
const url = getTokenStreamUrlRef.current(workflowId);
|
|
41
|
+
const es = new EventSource(url);
|
|
42
|
+
esRef.current = es;
|
|
43
|
+
es.addEventListener('open', () => {
|
|
44
|
+
void (async () => {
|
|
45
|
+
try {
|
|
46
|
+
await afterConnected();
|
|
47
|
+
setStatus('streaming');
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
51
|
+
setError(err);
|
|
52
|
+
setStatus('error');
|
|
53
|
+
close();
|
|
54
|
+
}
|
|
55
|
+
})();
|
|
56
|
+
});
|
|
57
|
+
es.addEventListener('message', (ev) => {
|
|
58
|
+
try {
|
|
59
|
+
const part = JSON.parse(ev.data);
|
|
60
|
+
if (part.type === 'text-delta' && part.delta) {
|
|
61
|
+
setText((t) => t + part.delta);
|
|
62
|
+
}
|
|
63
|
+
if (part.type === 'finish') {
|
|
64
|
+
closedCleanlyRef.current = true;
|
|
65
|
+
setStatus('done');
|
|
66
|
+
close();
|
|
67
|
+
}
|
|
68
|
+
if (part.type === 'error') {
|
|
69
|
+
setError(new Error(part.error ?? 'Stream error'));
|
|
70
|
+
setStatus('error');
|
|
71
|
+
close();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
/* ignore malformed SSE */
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
es.addEventListener('error', () => {
|
|
79
|
+
if (closedCleanlyRef.current) {
|
|
80
|
+
close();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
close();
|
|
84
|
+
setStatus('error');
|
|
85
|
+
setError((prev) => prev ?? new Error('EventSource error'));
|
|
86
|
+
});
|
|
87
|
+
}, [close]);
|
|
88
|
+
const isStreaming = status === 'streaming' || status === 'connecting';
|
|
89
|
+
return {
|
|
90
|
+
/** Accumulated text from `text-delta` parts. */
|
|
91
|
+
text,
|
|
92
|
+
status,
|
|
93
|
+
error,
|
|
94
|
+
/** True while waiting for open or actively receiving deltas (until `done` / `error`). */
|
|
95
|
+
isStreaming,
|
|
96
|
+
/**
|
|
97
|
+
* Open SSE for `workflowId`, then run `afterConnected` (e.g. start workflow).
|
|
98
|
+
* Call again for each new model round (e.g. after HITL reject).
|
|
99
|
+
*/
|
|
100
|
+
subscribeThenStart,
|
|
101
|
+
reset,
|
|
102
|
+
close,
|
|
103
|
+
};
|
|
104
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@durion/react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Durion React hooks for workflow stream state + Gateway v0 token SSE",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/shaibusunnuma/durion.git",
|
|
9
|
+
"directory": "packages/react"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/shaibusunnuma/durion#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/shaibusunnuma/durion/issues"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"main": "dist/index.js",
|
|
22
|
+
"types": "dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc",
|
|
32
|
+
"prepublishOnly": "npm run build",
|
|
33
|
+
"test": "vitest run"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
37
|
+
"@durion/sdk": "^0.1.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@durion/sdk": "^0.1.0",
|
|
41
|
+
"@types/react": "^19.0.0",
|
|
42
|
+
"react": "^19.0.0",
|
|
43
|
+
"typescript": "^5.9.3",
|
|
44
|
+
"vitest": "^4.1.0"
|
|
45
|
+
}
|
|
46
|
+
}
|