@frontal-labs/react 0.0.0-stage → 0.2.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/README.md CHANGED
@@ -1,3 +1,191 @@
1
- # Temporary Holding Version
1
+ # @frontal-labs/react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ React hooks for the Frontal SDK. Thin, transport-agnostic, no framework
4
+ lock-in: `useChat` talks to any route that returns
5
+ `toUIMessageStreamResponse(...)`; the other hooks take objects from
6
+ `new Frontal(...)`.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ bun add @frontal-labs/sdk @frontal-labs/react
12
+ ```
13
+
14
+ Peer dependency: `react >= 18`.
15
+
16
+ ## Chat in two files (Next.js App Router)
17
+
18
+ **`app/api/chat/route.ts`** — the server side. `toUIMessageStreamResponse`
19
+ turns `ai.streamText` into a streaming `Response`; model errors arrive as
20
+ frames the UI can render and retry, not as broken connections.
21
+
22
+ ```ts
23
+ import { Frontal, toUIMessageStreamResponse } from "@frontal-labs/sdk";
24
+
25
+ const f = new Frontal({ apiKey: process.env.FRONTAL_API_KEY! });
26
+
27
+ export async function POST(req: Request) {
28
+ const { messages } = (await req.json()) as {
29
+ messages: Array<{ role: string; content: string }>;
30
+ };
31
+ // Never trust roles from the browser: keep the system prompt server-side.
32
+ const history = messages
33
+ .filter((m) => m.role === "user" || m.role === "assistant")
34
+ .map((m) => ({ role: m.role as "user" | "assistant", content: m.content }));
35
+
36
+ return toUIMessageStreamResponse(
37
+ f.ai.streamText({
38
+ model: "claude-sonnet-5",
39
+ prompt: [{ role: "system", content: "You are a helpful assistant." }, ...history],
40
+ streamRetries: 1,
41
+ })
42
+ );
43
+ }
44
+ ```
45
+
46
+ **`app/page.tsx`** — the client side.
47
+
48
+ ```tsx
49
+ "use client";
50
+ import { useChat } from "@frontal-labs/react";
51
+ import { useState } from "react";
52
+
53
+ export default function Chat() {
54
+ const { messages, send, status, error, retry, stop } = useChat({ api: "/api/chat" });
55
+ const [input, setInput] = useState("");
56
+
57
+ return (
58
+ <main>
59
+ {messages.map((m) => (
60
+ <div key={m.id}>
61
+ <b>{m.role}:</b>{" "}
62
+ {m.parts.map((p, i) =>
63
+ p.type === "text" ? (
64
+ <span key={i}>{p.text}</span>
65
+ ) : p.type === "tool-call" ? (
66
+ <code key={i}>{p.toolName}({JSON.stringify(p.input)})</code>
67
+ ) : (
68
+ <em key={i}>{p.error.message}{p.error.retryable ? " (retryable)" : ""}</em>
69
+ )
70
+ )}
71
+ </div>
72
+ ))}
73
+ {error?.retryable && <button onClick={() => retry()}>Retry</button>}
74
+ <form
75
+ onSubmit={(e) => {
76
+ e.preventDefault();
77
+ void send(input);
78
+ setInput("");
79
+ }}
80
+ >
81
+ <input value={input} onChange={(e) => setInput(e.target.value)} />
82
+ <button disabled={status === "streaming"}>Send</button>
83
+ {status === "streaming" && <button type="button" onClick={stop}>Stop</button>}
84
+ </form>
85
+ </main>
86
+ );
87
+ }
88
+ ```
89
+
90
+ That is the whole chatbot. A copy-paste version lives in
91
+ [`templates/chatbot`](../../templates/chatbot).
92
+
93
+ ## Hooks
94
+
95
+ ### `useChat(options)`
96
+
97
+ | Option | Purpose |
98
+ | --- | --- |
99
+ | `api` | Route URL (default `/api/chat`) **or** a `(messages, { signal }) => Promise<Response>` transport |
100
+ | `id`, `body`, `headers` | Sent with every request |
101
+ | `fetch` | Custom fetch (tests, auth) |
102
+ | `onError(err)` | Every error frame (`err.code`, `err.retryable`, `err.fix`) |
103
+ | `onFinish(message)` | When the assistant message completes |
104
+
105
+ Returns `{ messages, status, error, send, stop, retry, setMessages }`.
106
+ `messages[i].parts` is a discriminated union of `text`, `tool-call`
107
+ (`state: "pending" | "done" | "error"`) and `error`.
108
+
109
+ ### `useAgentRun(agent, options)`
110
+
111
+ Agents are event-driven, not chat: `trigger(event, payload)` starts a run and
112
+ streams it. With a `stateSchema` on the accessor, `state` is typed.
113
+
114
+ ```tsx
115
+ import { Frontal } from "@frontal-labs/sdk";
116
+ import { useAgentRun } from "@frontal-labs/react";
117
+ import { z } from "zod";
118
+
119
+ const f = new Frontal({ apiKey: process.env.NEXT_PUBLIC_FRONTAL_API_KEY! });
120
+ const triage = f.agents.use("agt_ticket_triager", {
121
+ stateSchema: z.object({ tier: z.string(), score: z.number() }),
122
+ });
123
+
124
+ export function TriagePanel() {
125
+ const { trigger, events, state, status, error } = useAgentRun(triage);
126
+ return (
127
+ <div>
128
+ <button onClick={() => trigger("support.ticket.created", { ticketId: "t_987" })}>
129
+ Run
130
+ </button>
131
+ <p>{status}{state ? ` — tier ${state.tier} (${state.score})` : ""}</p>
132
+ {error && <p>{error.code}: {error.fix ?? error.message}</p>}
133
+ <ul>{events.map((e, i) => <li key={i}>{e.type === "event" ? e.event : e.type}</li>)}</ul>
134
+ </div>
135
+ );
136
+ }
137
+ ```
138
+
139
+ ### `useWorkflowApprovals(workflows, options)`
140
+
141
+ The human side of an agent's `approveWhen` contract.
142
+
143
+ ```tsx
144
+ import { Frontal } from "@frontal-labs/sdk";
145
+ import { useWorkflowApprovals } from "@frontal-labs/react";
146
+
147
+ const f = new Frontal({ apiKey: process.env.NEXT_PUBLIC_FRONTAL_API_KEY! });
148
+
149
+ export function Inbox() {
150
+ const { approvals, approve, reject, loading } = useWorkflowApprovals(f.workflows, {
151
+ pollIntervalMs: 5_000,
152
+ });
153
+ if (loading && approvals.length === 0) return <p>Loading…</p>;
154
+ return (
155
+ <ul>
156
+ {approvals.map((a) => (
157
+ <li key={a.id}>
158
+ {a.stepId}
159
+ <button onClick={() => approve(a.id, "LGTM")}>Approve</button>
160
+ <button onClick={() => reject(a.id)}>Reject</button>
161
+ </li>
162
+ ))}
163
+ </ul>
164
+ );
165
+ }
166
+ ```
167
+
168
+ ## Testing
169
+
170
+ Everything runs against mocks — no gateway, no key:
171
+
172
+ ```ts
173
+ import { AISdk, toUIMessageStreamResponse } from "@frontal-labs/ai";
174
+ import { createTestClient, mockLanguageModel } from "@frontal-labs/testing";
175
+
176
+ const model = mockLanguageModel({ doGenerate: () => ({ text: "Hello!" }) });
177
+ const ai = new AISdk(createTestClient(model.routes).client.httpClient);
178
+
179
+ // A fake /api/chat for useChat({ fetch })
180
+ const fetchImpl: typeof fetch = async () =>
181
+ toUIMessageStreamResponse(ai.streamText({ model: "m", prompt: "hi" }));
182
+ ```
183
+
184
+ See `tests/` in this package for `renderHook` examples.
185
+
186
+ ## Environment Variables
187
+
188
+ | Variable | Required | Description |
189
+ | --- | --- | --- |
190
+ | `FRONTAL_API_KEY` | Server routes | API key (`frt_...`); never ship it to the browser |
191
+ | `NEXT_PUBLIC_FRONTAL_API_KEY` | Client hooks | A browser-scoped key, if your deployment issues them |
package/dist/index.cjs ADDED
@@ -0,0 +1,257 @@
1
+ "use client";
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ useAgentRun: () => useAgentRun,
25
+ useChat: () => useChat,
26
+ useWorkflowApprovals: () => useWorkflowApprovals
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/use-agent-run.ts
31
+ var import_react = require("react");
32
+ function useAgentRun(agent, options = {}) {
33
+ const [run, setRun] = (0, import_react.useState)(void 0);
34
+ const [events, setEvents] = (0, import_react.useState)([]);
35
+ const [state, setState] = (0, import_react.useState)(void 0);
36
+ const [status, setStatus] = (0, import_react.useState)("idle");
37
+ const [error, setError] = (0, import_react.useState)(void 0);
38
+ const abortRef = (0, import_react.useRef)(void 0);
39
+ const watch = (0, import_react.useCallback)(
40
+ async (runId) => {
41
+ abortRef.current?.abort();
42
+ const controller = new AbortController();
43
+ abortRef.current = controller;
44
+ setEvents([]);
45
+ setError(void 0);
46
+ setStatus("streaming");
47
+ let failed = false;
48
+ for await (const part of agent.watch(runId, {
49
+ signal: controller.signal
50
+ })) {
51
+ setEvents((prev) => [...prev, part]);
52
+ if (part.type === "abort") continue;
53
+ if (part.type === "state") setState(part.state);
54
+ if (part.type === "error") {
55
+ failed = true;
56
+ setError(part.error);
57
+ options.onError?.(part.error);
58
+ }
59
+ }
60
+ if (abortRef.current === controller) {
61
+ abortRef.current = void 0;
62
+ setStatus(failed ? "error" : "completed");
63
+ }
64
+ },
65
+ [agent, options.onError]
66
+ );
67
+ const trigger = (0, import_react.useCallback)(
68
+ async (event, payload = {}) => {
69
+ setStatus("starting");
70
+ setError(void 0);
71
+ try {
72
+ const started = await agent.message(event, payload);
73
+ setRun(started);
74
+ await watch(started.id);
75
+ return started;
76
+ } catch (err) {
77
+ const e = err;
78
+ setError(e);
79
+ options.onError?.(e);
80
+ setStatus("error");
81
+ return void 0;
82
+ }
83
+ },
84
+ [agent, options.onError, watch]
85
+ );
86
+ const stop = (0, import_react.useCallback)(() => {
87
+ abortRef.current?.abort();
88
+ abortRef.current = void 0;
89
+ setStatus("idle");
90
+ }, []);
91
+ return { run, events, state, status, error, trigger, watch, stop };
92
+ }
93
+
94
+ // src/use-chat.ts
95
+ var import_ai = require("@frontal-labs/ai");
96
+ var import_react2 = require("react");
97
+ function toWireMessages(messages) {
98
+ return messages.map((m) => ({ role: m.role, content: m.text }));
99
+ }
100
+ function useChat(options = {}) {
101
+ const {
102
+ api = "/api/chat",
103
+ id,
104
+ initialMessages = [],
105
+ body,
106
+ headers,
107
+ fetch: fetchImpl,
108
+ onError,
109
+ onFinish
110
+ } = options;
111
+ const [messages, setMessages] = (0, import_react2.useState)(initialMessages);
112
+ const [status, setStatus] = (0, import_react2.useState)("idle");
113
+ const [error, setError] = (0, import_react2.useState)(void 0);
114
+ const abortRef = (0, import_react2.useRef)(void 0);
115
+ const messagesRef = (0, import_react2.useRef)(messages);
116
+ messagesRef.current = messages;
117
+ const extrasRef = (0, import_react2.useRef)({ body, headers });
118
+ extrasRef.current = { body, headers };
119
+ const transport = (0, import_react2.useMemo)(() => {
120
+ if (typeof api === "function") return api;
121
+ return async (msgs, init) => (fetchImpl ?? fetch)(api, {
122
+ method: "POST",
123
+ headers: {
124
+ "content-type": "application/json",
125
+ ...extrasRef.current.headers
126
+ },
127
+ body: JSON.stringify({
128
+ id,
129
+ messages: toWireMessages(msgs),
130
+ ...extrasRef.current.body
131
+ }),
132
+ signal: init.signal
133
+ });
134
+ }, [api, fetchImpl, id]);
135
+ const run = (0, import_react2.useCallback)(
136
+ async (history) => {
137
+ abortRef.current?.abort();
138
+ const controller = new AbortController();
139
+ abortRef.current = controller;
140
+ setError(void 0);
141
+ setStatus("submitted");
142
+ let assistant = (0, import_ai.createUIMessage)("assistant");
143
+ setMessages([...history, assistant]);
144
+ try {
145
+ const response = await transport(history, {
146
+ signal: controller.signal
147
+ });
148
+ setStatus("streaming");
149
+ for await (const frame of (0, import_ai.parseUIMessageStream)(response, {
150
+ signal: controller.signal
151
+ })) {
152
+ if (frame.type === "error") {
153
+ setError(frame.error);
154
+ onError?.(frame.error);
155
+ }
156
+ assistant = (0, import_ai.applyUIFrame)(assistant, frame);
157
+ const snapshot = assistant;
158
+ setMessages([...history, snapshot]);
159
+ }
160
+ } catch (err) {
161
+ const e = {
162
+ code: "TRANSPORT_ERROR",
163
+ message: err instanceof Error ? err.message : String(err),
164
+ retryable: true
165
+ };
166
+ setError(e);
167
+ onError?.(e);
168
+ assistant = (0, import_ai.applyUIFrame)(assistant, { type: "error", error: e });
169
+ setMessages([...history, assistant]);
170
+ } finally {
171
+ if (abortRef.current === controller) abortRef.current = void 0;
172
+ }
173
+ setStatus(
174
+ assistant.parts.some((p) => p.type === "error") ? "error" : "idle"
175
+ );
176
+ onFinish?.(assistant);
177
+ },
178
+ [onError, onFinish, transport]
179
+ );
180
+ const send = (0, import_react2.useCallback)(
181
+ async (text) => {
182
+ const history = [...messagesRef.current, (0, import_ai.createUIMessage)("user", text)];
183
+ await run(history);
184
+ },
185
+ [run]
186
+ );
187
+ const retry = (0, import_react2.useCallback)(async () => {
188
+ const current = messagesRef.current;
189
+ const lastUser = [...current].reverse().findIndex((m) => m.role === "user");
190
+ if (lastUser === -1) return;
191
+ const cut = current.length - lastUser;
192
+ await run(current.slice(0, cut));
193
+ }, [run]);
194
+ const stop = (0, import_react2.useCallback)(() => {
195
+ abortRef.current?.abort();
196
+ abortRef.current = void 0;
197
+ }, []);
198
+ return { messages, status, error, send, stop, retry, setMessages };
199
+ }
200
+
201
+ // src/use-workflow-run.ts
202
+ var import_react3 = require("react");
203
+ function useWorkflowApprovals(workflows, options = {}) {
204
+ const { status = "pending", pollIntervalMs = 0, enabled = true } = options;
205
+ const [approvals, setApprovals] = (0, import_react3.useState)([]);
206
+ const [loading, setLoading] = (0, import_react3.useState)(false);
207
+ const [error, setError] = (0, import_react3.useState)(void 0);
208
+ const refresh = (0, import_react3.useCallback)(async () => {
209
+ setLoading(true);
210
+ try {
211
+ const page = await workflows.approvals.list({ status });
212
+ setApprovals(page.data);
213
+ setError(void 0);
214
+ } catch (err) {
215
+ setError(err);
216
+ } finally {
217
+ setLoading(false);
218
+ }
219
+ }, [status, workflows]);
220
+ (0, import_react3.useEffect)(() => {
221
+ if (!enabled) return;
222
+ void refresh();
223
+ if (!pollIntervalMs) return;
224
+ const timer = setInterval(() => void refresh(), pollIntervalMs);
225
+ return () => clearInterval(timer);
226
+ }, [enabled, pollIntervalMs, refresh]);
227
+ const act = (0, import_react3.useCallback)(
228
+ async (fn, id) => {
229
+ try {
230
+ const updated = await fn();
231
+ setApprovals(
232
+ (prev) => prev.map((a) => a.id === id ? updated : a).filter((a) => a.status === status)
233
+ );
234
+ return updated;
235
+ } catch (err) {
236
+ setError(err);
237
+ return void 0;
238
+ }
239
+ },
240
+ [status]
241
+ );
242
+ const approve = (0, import_react3.useCallback)(
243
+ (id, comment) => act(() => workflows.approvals.approve(id, comment), id),
244
+ [act, workflows]
245
+ );
246
+ const reject = (0, import_react3.useCallback)(
247
+ (id, comment) => act(() => workflows.approvals.reject(id, comment), id),
248
+ [act, workflows]
249
+ );
250
+ return { approvals, loading, error, refresh, approve, reject };
251
+ }
252
+ // Annotate the CommonJS export names for ESM import in node:
253
+ 0 && (module.exports = {
254
+ useAgentRun,
255
+ useChat,
256
+ useWorkflowApprovals
257
+ });
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @frontal-labs/react
3
+ *
4
+ * React hooks for the Frontal SDK. Transport-agnostic: `useChat` talks to
5
+ * any route that returns `toUIMessageStreamResponse(...)`; the other hooks
6
+ * take SDK objects from `new Frontal(...)`.
7
+ */
8
+ export type { UIError, UIMessage, UIMessagePart } from "@frontal-labs/ai";
9
+ export type { AgentRunStatus, UseAgentRunOptions, UseAgentRunResult, } from "./use-agent-run";
10
+ export { useAgentRun } from "./use-agent-run";
11
+ export type { ChatStatus, ChatTransport, UseChatOptions, UseChatResult, } from "./use-chat";
12
+ export { useChat } from "./use-chat";
13
+ export type { UseWorkflowApprovalsOptions, UseWorkflowApprovalsResult, } from "./use-workflow-run";
14
+ export { useWorkflowApprovals } from "./use-workflow-run";
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC1E,YAAY,EACV,cAAc,EACd,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,YAAY,EACV,UAAU,EACV,aAAa,EACb,cAAc,EACd,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,YAAY,EACV,2BAA2B,EAC3B,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,233 @@
1
+ "use client";
2
+
3
+ // src/use-agent-run.ts
4
+ import { useCallback, useRef, useState } from "react";
5
+ function useAgentRun(agent, options = {}) {
6
+ const [run, setRun] = useState(void 0);
7
+ const [events, setEvents] = useState([]);
8
+ const [state, setState] = useState(void 0);
9
+ const [status, setStatus] = useState("idle");
10
+ const [error, setError] = useState(void 0);
11
+ const abortRef = useRef(void 0);
12
+ const watch = useCallback(
13
+ async (runId) => {
14
+ abortRef.current?.abort();
15
+ const controller = new AbortController();
16
+ abortRef.current = controller;
17
+ setEvents([]);
18
+ setError(void 0);
19
+ setStatus("streaming");
20
+ let failed = false;
21
+ for await (const part of agent.watch(runId, {
22
+ signal: controller.signal
23
+ })) {
24
+ setEvents((prev) => [...prev, part]);
25
+ if (part.type === "abort") continue;
26
+ if (part.type === "state") setState(part.state);
27
+ if (part.type === "error") {
28
+ failed = true;
29
+ setError(part.error);
30
+ options.onError?.(part.error);
31
+ }
32
+ }
33
+ if (abortRef.current === controller) {
34
+ abortRef.current = void 0;
35
+ setStatus(failed ? "error" : "completed");
36
+ }
37
+ },
38
+ [agent, options.onError]
39
+ );
40
+ const trigger = useCallback(
41
+ async (event, payload = {}) => {
42
+ setStatus("starting");
43
+ setError(void 0);
44
+ try {
45
+ const started = await agent.message(event, payload);
46
+ setRun(started);
47
+ await watch(started.id);
48
+ return started;
49
+ } catch (err) {
50
+ const e = err;
51
+ setError(e);
52
+ options.onError?.(e);
53
+ setStatus("error");
54
+ return void 0;
55
+ }
56
+ },
57
+ [agent, options.onError, watch]
58
+ );
59
+ const stop = useCallback(() => {
60
+ abortRef.current?.abort();
61
+ abortRef.current = void 0;
62
+ setStatus("idle");
63
+ }, []);
64
+ return { run, events, state, status, error, trigger, watch, stop };
65
+ }
66
+
67
+ // src/use-chat.ts
68
+ import {
69
+ applyUIFrame,
70
+ createUIMessage,
71
+ parseUIMessageStream
72
+ } from "@frontal-labs/ai";
73
+ import { useCallback as useCallback2, useMemo, useRef as useRef2, useState as useState2 } from "react";
74
+ function toWireMessages(messages) {
75
+ return messages.map((m) => ({ role: m.role, content: m.text }));
76
+ }
77
+ function useChat(options = {}) {
78
+ const {
79
+ api = "/api/chat",
80
+ id,
81
+ initialMessages = [],
82
+ body,
83
+ headers,
84
+ fetch: fetchImpl,
85
+ onError,
86
+ onFinish
87
+ } = options;
88
+ const [messages, setMessages] = useState2(initialMessages);
89
+ const [status, setStatus] = useState2("idle");
90
+ const [error, setError] = useState2(void 0);
91
+ const abortRef = useRef2(void 0);
92
+ const messagesRef = useRef2(messages);
93
+ messagesRef.current = messages;
94
+ const extrasRef = useRef2({ body, headers });
95
+ extrasRef.current = { body, headers };
96
+ const transport = useMemo(() => {
97
+ if (typeof api === "function") return api;
98
+ return async (msgs, init) => (fetchImpl ?? fetch)(api, {
99
+ method: "POST",
100
+ headers: {
101
+ "content-type": "application/json",
102
+ ...extrasRef.current.headers
103
+ },
104
+ body: JSON.stringify({
105
+ id,
106
+ messages: toWireMessages(msgs),
107
+ ...extrasRef.current.body
108
+ }),
109
+ signal: init.signal
110
+ });
111
+ }, [api, fetchImpl, id]);
112
+ const run = useCallback2(
113
+ async (history) => {
114
+ abortRef.current?.abort();
115
+ const controller = new AbortController();
116
+ abortRef.current = controller;
117
+ setError(void 0);
118
+ setStatus("submitted");
119
+ let assistant = createUIMessage("assistant");
120
+ setMessages([...history, assistant]);
121
+ try {
122
+ const response = await transport(history, {
123
+ signal: controller.signal
124
+ });
125
+ setStatus("streaming");
126
+ for await (const frame of parseUIMessageStream(response, {
127
+ signal: controller.signal
128
+ })) {
129
+ if (frame.type === "error") {
130
+ setError(frame.error);
131
+ onError?.(frame.error);
132
+ }
133
+ assistant = applyUIFrame(assistant, frame);
134
+ const snapshot = assistant;
135
+ setMessages([...history, snapshot]);
136
+ }
137
+ } catch (err) {
138
+ const e = {
139
+ code: "TRANSPORT_ERROR",
140
+ message: err instanceof Error ? err.message : String(err),
141
+ retryable: true
142
+ };
143
+ setError(e);
144
+ onError?.(e);
145
+ assistant = applyUIFrame(assistant, { type: "error", error: e });
146
+ setMessages([...history, assistant]);
147
+ } finally {
148
+ if (abortRef.current === controller) abortRef.current = void 0;
149
+ }
150
+ setStatus(
151
+ assistant.parts.some((p) => p.type === "error") ? "error" : "idle"
152
+ );
153
+ onFinish?.(assistant);
154
+ },
155
+ [onError, onFinish, transport]
156
+ );
157
+ const send = useCallback2(
158
+ async (text) => {
159
+ const history = [...messagesRef.current, createUIMessage("user", text)];
160
+ await run(history);
161
+ },
162
+ [run]
163
+ );
164
+ const retry = useCallback2(async () => {
165
+ const current = messagesRef.current;
166
+ const lastUser = [...current].reverse().findIndex((m) => m.role === "user");
167
+ if (lastUser === -1) return;
168
+ const cut = current.length - lastUser;
169
+ await run(current.slice(0, cut));
170
+ }, [run]);
171
+ const stop = useCallback2(() => {
172
+ abortRef.current?.abort();
173
+ abortRef.current = void 0;
174
+ }, []);
175
+ return { messages, status, error, send, stop, retry, setMessages };
176
+ }
177
+
178
+ // src/use-workflow-run.ts
179
+ import { useCallback as useCallback3, useEffect, useState as useState3 } from "react";
180
+ function useWorkflowApprovals(workflows, options = {}) {
181
+ const { status = "pending", pollIntervalMs = 0, enabled = true } = options;
182
+ const [approvals, setApprovals] = useState3([]);
183
+ const [loading, setLoading] = useState3(false);
184
+ const [error, setError] = useState3(void 0);
185
+ const refresh = useCallback3(async () => {
186
+ setLoading(true);
187
+ try {
188
+ const page = await workflows.approvals.list({ status });
189
+ setApprovals(page.data);
190
+ setError(void 0);
191
+ } catch (err) {
192
+ setError(err);
193
+ } finally {
194
+ setLoading(false);
195
+ }
196
+ }, [status, workflows]);
197
+ useEffect(() => {
198
+ if (!enabled) return;
199
+ void refresh();
200
+ if (!pollIntervalMs) return;
201
+ const timer = setInterval(() => void refresh(), pollIntervalMs);
202
+ return () => clearInterval(timer);
203
+ }, [enabled, pollIntervalMs, refresh]);
204
+ const act = useCallback3(
205
+ async (fn, id) => {
206
+ try {
207
+ const updated = await fn();
208
+ setApprovals(
209
+ (prev) => prev.map((a) => a.id === id ? updated : a).filter((a) => a.status === status)
210
+ );
211
+ return updated;
212
+ } catch (err) {
213
+ setError(err);
214
+ return void 0;
215
+ }
216
+ },
217
+ [status]
218
+ );
219
+ const approve = useCallback3(
220
+ (id, comment) => act(() => workflows.approvals.approve(id, comment), id),
221
+ [act, workflows]
222
+ );
223
+ const reject = useCallback3(
224
+ (id, comment) => act(() => workflows.approvals.reject(id, comment), id),
225
+ [act, workflows]
226
+ );
227
+ return { approvals, loading, error, refresh, approve, reject };
228
+ }
229
+ export {
230
+ useAgentRun,
231
+ useChat,
232
+ useWorkflowApprovals
233
+ };