@strand-js/react 0.1.1 → 0.1.3
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/dist/index.d.mts +86 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +310 -0
- package/dist/index.mjs +279 -0
- package/package.json +3 -2
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { StrandClient, ToolDefinition, Message, StreamingStatus, TokenUsage } from '@strand-js/core';
|
|
4
|
+
|
|
5
|
+
interface StrandProviderProps {
|
|
6
|
+
client: StrandClient;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
declare function StrandProvider({ client, children }: StrandProviderProps): react.JSX.Element;
|
|
10
|
+
|
|
11
|
+
interface ConversationOptions {
|
|
12
|
+
system?: string;
|
|
13
|
+
tools?: ToolDefinition[];
|
|
14
|
+
onToolCall?: (name: string, args: Record<string, unknown>, output: unknown) => void;
|
|
15
|
+
context?: Record<string, unknown>;
|
|
16
|
+
sessionId?: string;
|
|
17
|
+
onFinish?: (message: Message) => void;
|
|
18
|
+
onError?: (error: Error) => void;
|
|
19
|
+
client?: StrandClient;
|
|
20
|
+
}
|
|
21
|
+
interface ConversationResult {
|
|
22
|
+
messages: Message[];
|
|
23
|
+
send: (content: string) => void;
|
|
24
|
+
status: StreamingStatus;
|
|
25
|
+
isPending: boolean;
|
|
26
|
+
isStreaming: boolean;
|
|
27
|
+
isIdle: boolean;
|
|
28
|
+
isDone: boolean;
|
|
29
|
+
error: Error | null;
|
|
30
|
+
cancel: () => void;
|
|
31
|
+
clear: () => void;
|
|
32
|
+
tokenUsage: TokenUsage;
|
|
33
|
+
}
|
|
34
|
+
declare function useConversation(options?: ConversationOptions): ConversationResult;
|
|
35
|
+
|
|
36
|
+
interface ToolCallResult<TInput = unknown, TOutput = unknown> {
|
|
37
|
+
status: 'idle' | 'pending' | 'running' | 'done' | 'failed';
|
|
38
|
+
input: TInput | null;
|
|
39
|
+
output: TOutput | null;
|
|
40
|
+
error: Error | null;
|
|
41
|
+
isRunning: boolean;
|
|
42
|
+
}
|
|
43
|
+
declare function useToolCall<TInput = unknown, TOutput = unknown>(toolName: string, options?: {
|
|
44
|
+
client?: StrandClient;
|
|
45
|
+
}): ToolCallResult<TInput, TOutput>;
|
|
46
|
+
|
|
47
|
+
interface AgentStep {
|
|
48
|
+
index: number;
|
|
49
|
+
toolName: string;
|
|
50
|
+
input: Record<string, unknown> | null;
|
|
51
|
+
output: unknown | null;
|
|
52
|
+
status: 'running' | 'done' | 'failed';
|
|
53
|
+
}
|
|
54
|
+
type AgentStatus = 'idle' | 'running' | 'paused' | 'done' | 'failed';
|
|
55
|
+
interface AgentSessionOptions {
|
|
56
|
+
system?: string;
|
|
57
|
+
maxSteps?: number;
|
|
58
|
+
tools?: ToolDefinition[];
|
|
59
|
+
onToolCall?: (name: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
60
|
+
onStep?: (step: AgentStep) => void;
|
|
61
|
+
onComplete?: (result: string) => void;
|
|
62
|
+
client?: StrandClient;
|
|
63
|
+
}
|
|
64
|
+
interface AgentSessionResult {
|
|
65
|
+
status: AgentStatus;
|
|
66
|
+
steps: AgentStep[];
|
|
67
|
+
currentStep: AgentStep | null;
|
|
68
|
+
stepCount: number;
|
|
69
|
+
run: (goal: string) => void;
|
|
70
|
+
pause: () => void;
|
|
71
|
+
resume: () => void;
|
|
72
|
+
cancel: () => void;
|
|
73
|
+
result: string | null;
|
|
74
|
+
error: Error | null;
|
|
75
|
+
}
|
|
76
|
+
declare function useAgentSession(options?: AgentSessionOptions): AgentSessionResult;
|
|
77
|
+
|
|
78
|
+
interface StreamingTextResult {
|
|
79
|
+
text: string;
|
|
80
|
+
delta: string;
|
|
81
|
+
isDone: boolean;
|
|
82
|
+
isStreaming: boolean;
|
|
83
|
+
}
|
|
84
|
+
declare function useStreamingText(stream: ReadableStream<string> | null): StreamingTextResult;
|
|
85
|
+
|
|
86
|
+
export { type AgentSessionOptions, type AgentSessionResult, type ConversationOptions, type ConversationResult, StrandProvider, type StreamingTextResult, type ToolCallResult, useAgentSession, useConversation, useStreamingText, useToolCall };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { StrandClient, ToolDefinition, Message, StreamingStatus, TokenUsage } from '@strand-js/core';
|
|
4
|
+
|
|
5
|
+
interface StrandProviderProps {
|
|
6
|
+
client: StrandClient;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
declare function StrandProvider({ client, children }: StrandProviderProps): react.JSX.Element;
|
|
10
|
+
|
|
11
|
+
interface ConversationOptions {
|
|
12
|
+
system?: string;
|
|
13
|
+
tools?: ToolDefinition[];
|
|
14
|
+
onToolCall?: (name: string, args: Record<string, unknown>, output: unknown) => void;
|
|
15
|
+
context?: Record<string, unknown>;
|
|
16
|
+
sessionId?: string;
|
|
17
|
+
onFinish?: (message: Message) => void;
|
|
18
|
+
onError?: (error: Error) => void;
|
|
19
|
+
client?: StrandClient;
|
|
20
|
+
}
|
|
21
|
+
interface ConversationResult {
|
|
22
|
+
messages: Message[];
|
|
23
|
+
send: (content: string) => void;
|
|
24
|
+
status: StreamingStatus;
|
|
25
|
+
isPending: boolean;
|
|
26
|
+
isStreaming: boolean;
|
|
27
|
+
isIdle: boolean;
|
|
28
|
+
isDone: boolean;
|
|
29
|
+
error: Error | null;
|
|
30
|
+
cancel: () => void;
|
|
31
|
+
clear: () => void;
|
|
32
|
+
tokenUsage: TokenUsage;
|
|
33
|
+
}
|
|
34
|
+
declare function useConversation(options?: ConversationOptions): ConversationResult;
|
|
35
|
+
|
|
36
|
+
interface ToolCallResult<TInput = unknown, TOutput = unknown> {
|
|
37
|
+
status: 'idle' | 'pending' | 'running' | 'done' | 'failed';
|
|
38
|
+
input: TInput | null;
|
|
39
|
+
output: TOutput | null;
|
|
40
|
+
error: Error | null;
|
|
41
|
+
isRunning: boolean;
|
|
42
|
+
}
|
|
43
|
+
declare function useToolCall<TInput = unknown, TOutput = unknown>(toolName: string, options?: {
|
|
44
|
+
client?: StrandClient;
|
|
45
|
+
}): ToolCallResult<TInput, TOutput>;
|
|
46
|
+
|
|
47
|
+
interface AgentStep {
|
|
48
|
+
index: number;
|
|
49
|
+
toolName: string;
|
|
50
|
+
input: Record<string, unknown> | null;
|
|
51
|
+
output: unknown | null;
|
|
52
|
+
status: 'running' | 'done' | 'failed';
|
|
53
|
+
}
|
|
54
|
+
type AgentStatus = 'idle' | 'running' | 'paused' | 'done' | 'failed';
|
|
55
|
+
interface AgentSessionOptions {
|
|
56
|
+
system?: string;
|
|
57
|
+
maxSteps?: number;
|
|
58
|
+
tools?: ToolDefinition[];
|
|
59
|
+
onToolCall?: (name: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
60
|
+
onStep?: (step: AgentStep) => void;
|
|
61
|
+
onComplete?: (result: string) => void;
|
|
62
|
+
client?: StrandClient;
|
|
63
|
+
}
|
|
64
|
+
interface AgentSessionResult {
|
|
65
|
+
status: AgentStatus;
|
|
66
|
+
steps: AgentStep[];
|
|
67
|
+
currentStep: AgentStep | null;
|
|
68
|
+
stepCount: number;
|
|
69
|
+
run: (goal: string) => void;
|
|
70
|
+
pause: () => void;
|
|
71
|
+
resume: () => void;
|
|
72
|
+
cancel: () => void;
|
|
73
|
+
result: string | null;
|
|
74
|
+
error: Error | null;
|
|
75
|
+
}
|
|
76
|
+
declare function useAgentSession(options?: AgentSessionOptions): AgentSessionResult;
|
|
77
|
+
|
|
78
|
+
interface StreamingTextResult {
|
|
79
|
+
text: string;
|
|
80
|
+
delta: string;
|
|
81
|
+
isDone: boolean;
|
|
82
|
+
isStreaming: boolean;
|
|
83
|
+
}
|
|
84
|
+
declare function useStreamingText(stream: ReadableStream<string> | null): StreamingTextResult;
|
|
85
|
+
|
|
86
|
+
export { type AgentSessionOptions, type AgentSessionResult, type ConversationOptions, type ConversationResult, StrandProvider, type StreamingTextResult, type ToolCallResult, useAgentSession, useConversation, useStreamingText, useToolCall };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
StrandProvider: () => StrandProvider,
|
|
24
|
+
useAgentSession: () => useAgentSession,
|
|
25
|
+
useConversation: () => useConversation,
|
|
26
|
+
useStreamingText: () => useStreamingText,
|
|
27
|
+
useToolCall: () => useToolCall
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/StrandProvider.tsx
|
|
32
|
+
var import_react = require("react");
|
|
33
|
+
var import_core = require("@strand-js/core");
|
|
34
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
35
|
+
var StrandContext = (0, import_react.createContext)(null);
|
|
36
|
+
function StrandProvider({ client, children }) {
|
|
37
|
+
const [toolStore] = (0, import_react.useState)(() => new import_core.ToolCallStore());
|
|
38
|
+
const value = (0, import_react.useMemo)(() => ({ client, toolStore }), [client, toolStore]);
|
|
39
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StrandContext.Provider, { value, children });
|
|
40
|
+
}
|
|
41
|
+
function useStrandContext(explicitClient) {
|
|
42
|
+
const ctx = (0, import_react.useContext)(StrandContext);
|
|
43
|
+
if (ctx) {
|
|
44
|
+
return explicitClient ? { client: explicitClient, toolStore: ctx.toolStore } : ctx;
|
|
45
|
+
}
|
|
46
|
+
if (explicitClient) {
|
|
47
|
+
return { client: explicitClient, toolStore: new import_core.ToolCallStore() };
|
|
48
|
+
}
|
|
49
|
+
throw new Error(
|
|
50
|
+
"[strand] No StrandClient found. Wrap your app in <StrandProvider client={client}> or pass a client prop to the hook."
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/useConversation.ts
|
|
55
|
+
var import_react2 = require("react");
|
|
56
|
+
var import_core2 = require("@strand-js/core");
|
|
57
|
+
function useConversation(options = {}) {
|
|
58
|
+
const { client, toolStore } = useStrandContext(options.client);
|
|
59
|
+
const machineRef = (0, import_react2.useRef)(null);
|
|
60
|
+
if (!machineRef.current) {
|
|
61
|
+
machineRef.current = new import_core2.SessionStateMachine(options.sessionId);
|
|
62
|
+
}
|
|
63
|
+
const machine = machineRef.current;
|
|
64
|
+
const [session, setSession] = (0, import_react2.useState)(() => ({ ...machine.session }));
|
|
65
|
+
(0, import_react2.useEffect)(() => {
|
|
66
|
+
setSession({ ...machine.session });
|
|
67
|
+
return machine.subscribe((s) => setSession({ ...s }));
|
|
68
|
+
}, [machine]);
|
|
69
|
+
const optionsRef = (0, import_react2.useRef)(options);
|
|
70
|
+
(0, import_react2.useEffect)(() => {
|
|
71
|
+
optionsRef.current = options;
|
|
72
|
+
});
|
|
73
|
+
const abortRef = (0, import_react2.useRef)(null);
|
|
74
|
+
const send = (0, import_react2.useCallback)((content) => {
|
|
75
|
+
const { context, onFinish, onError, onToolCall } = optionsRef.current;
|
|
76
|
+
machine.addUserMessage(content);
|
|
77
|
+
machine.transition("submitting");
|
|
78
|
+
const abort = new AbortController();
|
|
79
|
+
abortRef.current = abort;
|
|
80
|
+
(async () => {
|
|
81
|
+
try {
|
|
82
|
+
const generator = client.send(machine.session.messages, {
|
|
83
|
+
context,
|
|
84
|
+
signal: abort.signal
|
|
85
|
+
});
|
|
86
|
+
for await (const event of generator) {
|
|
87
|
+
if (abort.signal.aborted) break;
|
|
88
|
+
(0, import_core2.processWireEvent)(event, machine, toolStore);
|
|
89
|
+
if (event.type === "strand:tool-result" && onToolCall) {
|
|
90
|
+
const tc = machine.session.messages.flatMap((m) => m.toolCalls ?? []).find((t) => t.id === event.toolCallId);
|
|
91
|
+
if (tc) onToolCall(tc.name, tc.input, event.result);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (abort.signal.aborted) {
|
|
95
|
+
machine.transition("idle");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (onFinish) {
|
|
99
|
+
const last = machine.session.messages.at(-1);
|
|
100
|
+
if (last?.role === "assistant") onFinish(last);
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (abort.signal.aborted) {
|
|
104
|
+
machine.transition("idle");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
108
|
+
machine.transition("error", error);
|
|
109
|
+
onError?.(error);
|
|
110
|
+
}
|
|
111
|
+
})();
|
|
112
|
+
}, [client, machine, toolStore]);
|
|
113
|
+
const cancel = (0, import_react2.useCallback)(() => {
|
|
114
|
+
abortRef.current?.abort();
|
|
115
|
+
}, []);
|
|
116
|
+
const clear = (0, import_react2.useCallback)(() => {
|
|
117
|
+
machine.clear();
|
|
118
|
+
toolStore.resetAll();
|
|
119
|
+
}, [machine, toolStore]);
|
|
120
|
+
return {
|
|
121
|
+
messages: session.messages,
|
|
122
|
+
status: session.status,
|
|
123
|
+
isPending: session.status === "submitting",
|
|
124
|
+
isStreaming: session.status === "streaming",
|
|
125
|
+
isIdle: session.status === "idle",
|
|
126
|
+
isDone: session.status === "done",
|
|
127
|
+
error: session.error,
|
|
128
|
+
tokenUsage: session.tokenUsage,
|
|
129
|
+
send,
|
|
130
|
+
cancel,
|
|
131
|
+
clear
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/useToolCall.ts
|
|
136
|
+
var import_react3 = require("react");
|
|
137
|
+
function useToolCall(toolName, options) {
|
|
138
|
+
const { toolStore } = useStrandContext(options?.client);
|
|
139
|
+
const [state, setState] = (0, import_react3.useState)(
|
|
140
|
+
() => toolStore.getState(toolName)
|
|
141
|
+
);
|
|
142
|
+
(0, import_react3.useEffect)(() => {
|
|
143
|
+
setState(toolStore.getState(toolName));
|
|
144
|
+
return toolStore.subscribe(toolName, (s) => setState(s));
|
|
145
|
+
}, [toolStore, toolName]);
|
|
146
|
+
return {
|
|
147
|
+
status: state.status,
|
|
148
|
+
input: state.input,
|
|
149
|
+
output: state.output,
|
|
150
|
+
error: state.error,
|
|
151
|
+
isRunning: state.status === "running"
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/useAgentSession.ts
|
|
156
|
+
var import_react4 = require("react");
|
|
157
|
+
var import_core3 = require("@strand-js/core");
|
|
158
|
+
function useAgentSession(options = {}) {
|
|
159
|
+
const { client, toolStore } = useStrandContext(options.client);
|
|
160
|
+
const machineRef = (0, import_react4.useRef)(null);
|
|
161
|
+
if (!machineRef.current) machineRef.current = new import_core3.SessionStateMachine();
|
|
162
|
+
const machine = machineRef.current;
|
|
163
|
+
const [agentStatus, setAgentStatus] = (0, import_react4.useState)("idle");
|
|
164
|
+
const [steps, setSteps] = (0, import_react4.useState)([]);
|
|
165
|
+
const [result, setResult] = (0, import_react4.useState)(null);
|
|
166
|
+
const [error, setError] = (0, import_react4.useState)(null);
|
|
167
|
+
const abortRef = (0, import_react4.useRef)(null);
|
|
168
|
+
const optionsRef = (0, import_react4.useRef)(options);
|
|
169
|
+
(0, import_react4.useEffect)(() => {
|
|
170
|
+
optionsRef.current = options;
|
|
171
|
+
});
|
|
172
|
+
const activeStepsRef = (0, import_react4.useRef)(/* @__PURE__ */ new Map());
|
|
173
|
+
const run = (0, import_react4.useCallback)((goal) => {
|
|
174
|
+
setAgentStatus("running");
|
|
175
|
+
setSteps([]);
|
|
176
|
+
setResult(null);
|
|
177
|
+
setError(null);
|
|
178
|
+
activeStepsRef.current.clear();
|
|
179
|
+
machine.clear();
|
|
180
|
+
machine.addUserMessage(goal);
|
|
181
|
+
const abort = new AbortController();
|
|
182
|
+
abortRef.current = abort;
|
|
183
|
+
(async () => {
|
|
184
|
+
try {
|
|
185
|
+
const generator = client.send(machine.session.messages, {
|
|
186
|
+
context: void 0,
|
|
187
|
+
signal: abort.signal
|
|
188
|
+
});
|
|
189
|
+
for await (const event of generator) {
|
|
190
|
+
if (abort.signal.aborted) break;
|
|
191
|
+
(0, import_core3.processWireEvent)(event, machine, toolStore);
|
|
192
|
+
if (event.type === "strand:tool-start") {
|
|
193
|
+
const idx = activeStepsRef.current.size;
|
|
194
|
+
activeStepsRef.current.set(event.toolCallId, idx);
|
|
195
|
+
const step = {
|
|
196
|
+
index: idx,
|
|
197
|
+
toolName: event.toolName,
|
|
198
|
+
input: null,
|
|
199
|
+
output: null,
|
|
200
|
+
status: "running"
|
|
201
|
+
};
|
|
202
|
+
setSteps((prev) => {
|
|
203
|
+
const next = [...prev];
|
|
204
|
+
next[idx] = step;
|
|
205
|
+
return next;
|
|
206
|
+
});
|
|
207
|
+
optionsRef.current.onStep?.(step);
|
|
208
|
+
}
|
|
209
|
+
if (event.type === "strand:tool-input-done") {
|
|
210
|
+
const idx = activeStepsRef.current.get(event.toolCallId);
|
|
211
|
+
if (idx !== void 0) {
|
|
212
|
+
setSteps((prev) => {
|
|
213
|
+
const next = [...prev];
|
|
214
|
+
next[idx] = { ...next[idx], input: event.input };
|
|
215
|
+
return next;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (event.type === "strand:tool-result") {
|
|
220
|
+
const idx = activeStepsRef.current.get(event.toolCallId);
|
|
221
|
+
if (idx !== void 0) {
|
|
222
|
+
setSteps((prev) => {
|
|
223
|
+
const next = [...prev];
|
|
224
|
+
next[idx] = { ...next[idx], output: event.result, status: "done" };
|
|
225
|
+
return next;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (abort.signal.aborted) {
|
|
231
|
+
setAgentStatus("idle");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const finalText = machine.session.messages.filter((m) => m.role === "assistant" && m.content).map((m) => m.content).join("");
|
|
235
|
+
setResult(finalText);
|
|
236
|
+
setAgentStatus("idle");
|
|
237
|
+
optionsRef.current.onComplete?.(finalText);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
if (abort.signal.aborted) {
|
|
240
|
+
setAgentStatus("idle");
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
244
|
+
setError(e);
|
|
245
|
+
setAgentStatus("failed");
|
|
246
|
+
}
|
|
247
|
+
})();
|
|
248
|
+
}, [client, machine, toolStore]);
|
|
249
|
+
const cancel = (0, import_react4.useCallback)(() => {
|
|
250
|
+
abortRef.current?.abort();
|
|
251
|
+
}, []);
|
|
252
|
+
const pause = (0, import_react4.useCallback)(() => {
|
|
253
|
+
abortRef.current?.abort();
|
|
254
|
+
setAgentStatus("paused");
|
|
255
|
+
}, []);
|
|
256
|
+
const resume = (0, import_react4.useCallback)(() => {
|
|
257
|
+
}, []);
|
|
258
|
+
const currentStep = steps.find((s) => s.status === "running") ?? null;
|
|
259
|
+
return { status: agentStatus, steps, currentStep, stepCount: steps.length, run, pause, resume, cancel, result, error };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/useStreamingText.ts
|
|
263
|
+
var import_react5 = require("react");
|
|
264
|
+
function useStreamingText(stream) {
|
|
265
|
+
const [state, setState] = (0, import_react5.useState)({
|
|
266
|
+
text: "",
|
|
267
|
+
delta: "",
|
|
268
|
+
isDone: false,
|
|
269
|
+
isStreaming: false
|
|
270
|
+
});
|
|
271
|
+
const streamRef = (0, import_react5.useRef)(stream);
|
|
272
|
+
(0, import_react5.useEffect)(() => {
|
|
273
|
+
streamRef.current = stream;
|
|
274
|
+
if (!stream) return;
|
|
275
|
+
let cancelled = false;
|
|
276
|
+
setState({ text: "", delta: "", isDone: false, isStreaming: true });
|
|
277
|
+
(async () => {
|
|
278
|
+
const reader = stream.getReader();
|
|
279
|
+
let accumulated = "";
|
|
280
|
+
try {
|
|
281
|
+
while (true) {
|
|
282
|
+
const { done, value } = await reader.read();
|
|
283
|
+
if (cancelled) break;
|
|
284
|
+
if (done) {
|
|
285
|
+
setState((s) => ({ ...s, isDone: true, isStreaming: false }));
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
accumulated += value;
|
|
289
|
+
setState({ text: accumulated, delta: value, isDone: false, isStreaming: true });
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
if (!cancelled) setState((s) => ({ ...s, isDone: true, isStreaming: false }));
|
|
293
|
+
} finally {
|
|
294
|
+
reader.releaseLock();
|
|
295
|
+
}
|
|
296
|
+
})();
|
|
297
|
+
return () => {
|
|
298
|
+
cancelled = true;
|
|
299
|
+
};
|
|
300
|
+
}, [stream]);
|
|
301
|
+
return state;
|
|
302
|
+
}
|
|
303
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
304
|
+
0 && (module.exports = {
|
|
305
|
+
StrandProvider,
|
|
306
|
+
useAgentSession,
|
|
307
|
+
useConversation,
|
|
308
|
+
useStreamingText,
|
|
309
|
+
useToolCall
|
|
310
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// src/StrandProvider.tsx
|
|
2
|
+
import { createContext, useContext, useState, useMemo } from "react";
|
|
3
|
+
import { ToolCallStore } from "@strand-js/core";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
var StrandContext = createContext(null);
|
|
6
|
+
function StrandProvider({ client, children }) {
|
|
7
|
+
const [toolStore] = useState(() => new ToolCallStore());
|
|
8
|
+
const value = useMemo(() => ({ client, toolStore }), [client, toolStore]);
|
|
9
|
+
return /* @__PURE__ */ jsx(StrandContext.Provider, { value, children });
|
|
10
|
+
}
|
|
11
|
+
function useStrandContext(explicitClient) {
|
|
12
|
+
const ctx = useContext(StrandContext);
|
|
13
|
+
if (ctx) {
|
|
14
|
+
return explicitClient ? { client: explicitClient, toolStore: ctx.toolStore } : ctx;
|
|
15
|
+
}
|
|
16
|
+
if (explicitClient) {
|
|
17
|
+
return { client: explicitClient, toolStore: new ToolCallStore() };
|
|
18
|
+
}
|
|
19
|
+
throw new Error(
|
|
20
|
+
"[strand] No StrandClient found. Wrap your app in <StrandProvider client={client}> or pass a client prop to the hook."
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/useConversation.ts
|
|
25
|
+
import { useState as useState2, useCallback, useRef, useEffect } from "react";
|
|
26
|
+
import { SessionStateMachine, processWireEvent } from "@strand-js/core";
|
|
27
|
+
function useConversation(options = {}) {
|
|
28
|
+
const { client, toolStore } = useStrandContext(options.client);
|
|
29
|
+
const machineRef = useRef(null);
|
|
30
|
+
if (!machineRef.current) {
|
|
31
|
+
machineRef.current = new SessionStateMachine(options.sessionId);
|
|
32
|
+
}
|
|
33
|
+
const machine = machineRef.current;
|
|
34
|
+
const [session, setSession] = useState2(() => ({ ...machine.session }));
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
setSession({ ...machine.session });
|
|
37
|
+
return machine.subscribe((s) => setSession({ ...s }));
|
|
38
|
+
}, [machine]);
|
|
39
|
+
const optionsRef = useRef(options);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
optionsRef.current = options;
|
|
42
|
+
});
|
|
43
|
+
const abortRef = useRef(null);
|
|
44
|
+
const send = useCallback((content) => {
|
|
45
|
+
const { context, onFinish, onError, onToolCall } = optionsRef.current;
|
|
46
|
+
machine.addUserMessage(content);
|
|
47
|
+
machine.transition("submitting");
|
|
48
|
+
const abort = new AbortController();
|
|
49
|
+
abortRef.current = abort;
|
|
50
|
+
(async () => {
|
|
51
|
+
try {
|
|
52
|
+
const generator = client.send(machine.session.messages, {
|
|
53
|
+
context,
|
|
54
|
+
signal: abort.signal
|
|
55
|
+
});
|
|
56
|
+
for await (const event of generator) {
|
|
57
|
+
if (abort.signal.aborted) break;
|
|
58
|
+
processWireEvent(event, machine, toolStore);
|
|
59
|
+
if (event.type === "strand:tool-result" && onToolCall) {
|
|
60
|
+
const tc = machine.session.messages.flatMap((m) => m.toolCalls ?? []).find((t) => t.id === event.toolCallId);
|
|
61
|
+
if (tc) onToolCall(tc.name, tc.input, event.result);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (abort.signal.aborted) {
|
|
65
|
+
machine.transition("idle");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (onFinish) {
|
|
69
|
+
const last = machine.session.messages.at(-1);
|
|
70
|
+
if (last?.role === "assistant") onFinish(last);
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (abort.signal.aborted) {
|
|
74
|
+
machine.transition("idle");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
78
|
+
machine.transition("error", error);
|
|
79
|
+
onError?.(error);
|
|
80
|
+
}
|
|
81
|
+
})();
|
|
82
|
+
}, [client, machine, toolStore]);
|
|
83
|
+
const cancel = useCallback(() => {
|
|
84
|
+
abortRef.current?.abort();
|
|
85
|
+
}, []);
|
|
86
|
+
const clear = useCallback(() => {
|
|
87
|
+
machine.clear();
|
|
88
|
+
toolStore.resetAll();
|
|
89
|
+
}, [machine, toolStore]);
|
|
90
|
+
return {
|
|
91
|
+
messages: session.messages,
|
|
92
|
+
status: session.status,
|
|
93
|
+
isPending: session.status === "submitting",
|
|
94
|
+
isStreaming: session.status === "streaming",
|
|
95
|
+
isIdle: session.status === "idle",
|
|
96
|
+
isDone: session.status === "done",
|
|
97
|
+
error: session.error,
|
|
98
|
+
tokenUsage: session.tokenUsage,
|
|
99
|
+
send,
|
|
100
|
+
cancel,
|
|
101
|
+
clear
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/useToolCall.ts
|
|
106
|
+
import { useState as useState3, useEffect as useEffect2 } from "react";
|
|
107
|
+
function useToolCall(toolName, options) {
|
|
108
|
+
const { toolStore } = useStrandContext(options?.client);
|
|
109
|
+
const [state, setState] = useState3(
|
|
110
|
+
() => toolStore.getState(toolName)
|
|
111
|
+
);
|
|
112
|
+
useEffect2(() => {
|
|
113
|
+
setState(toolStore.getState(toolName));
|
|
114
|
+
return toolStore.subscribe(toolName, (s) => setState(s));
|
|
115
|
+
}, [toolStore, toolName]);
|
|
116
|
+
return {
|
|
117
|
+
status: state.status,
|
|
118
|
+
input: state.input,
|
|
119
|
+
output: state.output,
|
|
120
|
+
error: state.error,
|
|
121
|
+
isRunning: state.status === "running"
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/useAgentSession.ts
|
|
126
|
+
import { useState as useState4, useCallback as useCallback2, useRef as useRef2, useEffect as useEffect3 } from "react";
|
|
127
|
+
import { SessionStateMachine as SessionStateMachine2, processWireEvent as processWireEvent2 } from "@strand-js/core";
|
|
128
|
+
function useAgentSession(options = {}) {
|
|
129
|
+
const { client, toolStore } = useStrandContext(options.client);
|
|
130
|
+
const machineRef = useRef2(null);
|
|
131
|
+
if (!machineRef.current) machineRef.current = new SessionStateMachine2();
|
|
132
|
+
const machine = machineRef.current;
|
|
133
|
+
const [agentStatus, setAgentStatus] = useState4("idle");
|
|
134
|
+
const [steps, setSteps] = useState4([]);
|
|
135
|
+
const [result, setResult] = useState4(null);
|
|
136
|
+
const [error, setError] = useState4(null);
|
|
137
|
+
const abortRef = useRef2(null);
|
|
138
|
+
const optionsRef = useRef2(options);
|
|
139
|
+
useEffect3(() => {
|
|
140
|
+
optionsRef.current = options;
|
|
141
|
+
});
|
|
142
|
+
const activeStepsRef = useRef2(/* @__PURE__ */ new Map());
|
|
143
|
+
const run = useCallback2((goal) => {
|
|
144
|
+
setAgentStatus("running");
|
|
145
|
+
setSteps([]);
|
|
146
|
+
setResult(null);
|
|
147
|
+
setError(null);
|
|
148
|
+
activeStepsRef.current.clear();
|
|
149
|
+
machine.clear();
|
|
150
|
+
machine.addUserMessage(goal);
|
|
151
|
+
const abort = new AbortController();
|
|
152
|
+
abortRef.current = abort;
|
|
153
|
+
(async () => {
|
|
154
|
+
try {
|
|
155
|
+
const generator = client.send(machine.session.messages, {
|
|
156
|
+
context: void 0,
|
|
157
|
+
signal: abort.signal
|
|
158
|
+
});
|
|
159
|
+
for await (const event of generator) {
|
|
160
|
+
if (abort.signal.aborted) break;
|
|
161
|
+
processWireEvent2(event, machine, toolStore);
|
|
162
|
+
if (event.type === "strand:tool-start") {
|
|
163
|
+
const idx = activeStepsRef.current.size;
|
|
164
|
+
activeStepsRef.current.set(event.toolCallId, idx);
|
|
165
|
+
const step = {
|
|
166
|
+
index: idx,
|
|
167
|
+
toolName: event.toolName,
|
|
168
|
+
input: null,
|
|
169
|
+
output: null,
|
|
170
|
+
status: "running"
|
|
171
|
+
};
|
|
172
|
+
setSteps((prev) => {
|
|
173
|
+
const next = [...prev];
|
|
174
|
+
next[idx] = step;
|
|
175
|
+
return next;
|
|
176
|
+
});
|
|
177
|
+
optionsRef.current.onStep?.(step);
|
|
178
|
+
}
|
|
179
|
+
if (event.type === "strand:tool-input-done") {
|
|
180
|
+
const idx = activeStepsRef.current.get(event.toolCallId);
|
|
181
|
+
if (idx !== void 0) {
|
|
182
|
+
setSteps((prev) => {
|
|
183
|
+
const next = [...prev];
|
|
184
|
+
next[idx] = { ...next[idx], input: event.input };
|
|
185
|
+
return next;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (event.type === "strand:tool-result") {
|
|
190
|
+
const idx = activeStepsRef.current.get(event.toolCallId);
|
|
191
|
+
if (idx !== void 0) {
|
|
192
|
+
setSteps((prev) => {
|
|
193
|
+
const next = [...prev];
|
|
194
|
+
next[idx] = { ...next[idx], output: event.result, status: "done" };
|
|
195
|
+
return next;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (abort.signal.aborted) {
|
|
201
|
+
setAgentStatus("idle");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const finalText = machine.session.messages.filter((m) => m.role === "assistant" && m.content).map((m) => m.content).join("");
|
|
205
|
+
setResult(finalText);
|
|
206
|
+
setAgentStatus("idle");
|
|
207
|
+
optionsRef.current.onComplete?.(finalText);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
if (abort.signal.aborted) {
|
|
210
|
+
setAgentStatus("idle");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
214
|
+
setError(e);
|
|
215
|
+
setAgentStatus("failed");
|
|
216
|
+
}
|
|
217
|
+
})();
|
|
218
|
+
}, [client, machine, toolStore]);
|
|
219
|
+
const cancel = useCallback2(() => {
|
|
220
|
+
abortRef.current?.abort();
|
|
221
|
+
}, []);
|
|
222
|
+
const pause = useCallback2(() => {
|
|
223
|
+
abortRef.current?.abort();
|
|
224
|
+
setAgentStatus("paused");
|
|
225
|
+
}, []);
|
|
226
|
+
const resume = useCallback2(() => {
|
|
227
|
+
}, []);
|
|
228
|
+
const currentStep = steps.find((s) => s.status === "running") ?? null;
|
|
229
|
+
return { status: agentStatus, steps, currentStep, stepCount: steps.length, run, pause, resume, cancel, result, error };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/useStreamingText.ts
|
|
233
|
+
import { useState as useState5, useEffect as useEffect4, useRef as useRef3 } from "react";
|
|
234
|
+
function useStreamingText(stream) {
|
|
235
|
+
const [state, setState] = useState5({
|
|
236
|
+
text: "",
|
|
237
|
+
delta: "",
|
|
238
|
+
isDone: false,
|
|
239
|
+
isStreaming: false
|
|
240
|
+
});
|
|
241
|
+
const streamRef = useRef3(stream);
|
|
242
|
+
useEffect4(() => {
|
|
243
|
+
streamRef.current = stream;
|
|
244
|
+
if (!stream) return;
|
|
245
|
+
let cancelled = false;
|
|
246
|
+
setState({ text: "", delta: "", isDone: false, isStreaming: true });
|
|
247
|
+
(async () => {
|
|
248
|
+
const reader = stream.getReader();
|
|
249
|
+
let accumulated = "";
|
|
250
|
+
try {
|
|
251
|
+
while (true) {
|
|
252
|
+
const { done, value } = await reader.read();
|
|
253
|
+
if (cancelled) break;
|
|
254
|
+
if (done) {
|
|
255
|
+
setState((s) => ({ ...s, isDone: true, isStreaming: false }));
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
accumulated += value;
|
|
259
|
+
setState({ text: accumulated, delta: value, isDone: false, isStreaming: true });
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
if (!cancelled) setState((s) => ({ ...s, isDone: true, isStreaming: false }));
|
|
263
|
+
} finally {
|
|
264
|
+
reader.releaseLock();
|
|
265
|
+
}
|
|
266
|
+
})();
|
|
267
|
+
return () => {
|
|
268
|
+
cancelled = true;
|
|
269
|
+
};
|
|
270
|
+
}, [stream]);
|
|
271
|
+
return state;
|
|
272
|
+
}
|
|
273
|
+
export {
|
|
274
|
+
StrandProvider,
|
|
275
|
+
useAgentSession,
|
|
276
|
+
useConversation,
|
|
277
|
+
useStreamingText,
|
|
278
|
+
useToolCall
|
|
279
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strand-js/react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"license": "MIT",
|
|
4
5
|
"description": "React hooks for Strand — useConversation, useToolCall, useAgentSession, useStreamingText",
|
|
5
6
|
"main": "./dist/index.js",
|
|
6
7
|
"module": "./dist/index.mjs",
|
|
@@ -16,7 +17,7 @@
|
|
|
16
17
|
"dist"
|
|
17
18
|
],
|
|
18
19
|
"dependencies": {
|
|
19
|
-
"@strand-js/core": "0.1.
|
|
20
|
+
"@strand-js/core": "0.1.3"
|
|
20
21
|
},
|
|
21
22
|
"peerDependencies": {
|
|
22
23
|
"react": "^18.0.0",
|