@steerable/agent-ui 0.7.4 → 0.8.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.
|
@@ -48,6 +48,21 @@ function reducer(state, action) {
|
|
|
48
48
|
next[idx] = patched;
|
|
49
49
|
return { messages: next };
|
|
50
50
|
}
|
|
51
|
+
case 'replace-content': {
|
|
52
|
+
// A retried attempt restarts the reply, so the accumulated text is no
|
|
53
|
+
// longer an append target: replacing it is the only way the live view
|
|
54
|
+
// can match what the backend will persist.
|
|
55
|
+
const idx = findLastAssistantIndex(state.messages);
|
|
56
|
+
if (idx === -1)
|
|
57
|
+
return state;
|
|
58
|
+
const patched = {
|
|
59
|
+
...state.messages[idx],
|
|
60
|
+
content: action.content,
|
|
61
|
+
};
|
|
62
|
+
const next = state.messages.slice();
|
|
63
|
+
next[idx] = patched;
|
|
64
|
+
return { messages: next };
|
|
65
|
+
}
|
|
51
66
|
case 'append-tool-call': {
|
|
52
67
|
const idx = findLastAssistantIndex(state.messages);
|
|
53
68
|
if (idx === -1)
|
|
@@ -202,6 +217,18 @@ export function useChatStream(options) {
|
|
|
202
217
|
return;
|
|
203
218
|
}
|
|
204
219
|
case 'agent':
|
|
220
|
+
// `stream_restart`: the backend discarded the attempt it was
|
|
221
|
+
// streaming and started the request again. It carries the
|
|
222
|
+
// authoritative replacement text rather than a length to cut, so
|
|
223
|
+
// the live view cannot drift from the persisted reply. Handled here
|
|
224
|
+
// because the text lives in this hook's state; still forwarded, as
|
|
225
|
+
// every other `agent` event is.
|
|
226
|
+
if (event.event === 'stream_restart') {
|
|
227
|
+
const content = typeof event.payload?.content === 'string' ? event.payload.content : '';
|
|
228
|
+
dispatch({ type: 'replace-content', content });
|
|
229
|
+
}
|
|
230
|
+
options.onUnknownEvent?.(event);
|
|
231
|
+
return;
|
|
205
232
|
case 'orchestration':
|
|
206
233
|
case 'loader-hint':
|
|
207
234
|
case 'keepalive':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerable/agent-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Steerable framework Tier 4 — headless React hooks (useAgentSession, useChatStream, useToolCallStatus) and Tailwind-themed components (ChatPanel, MessageList, OrchestrationPlanCard, ToolCallRenderer, SSEStreamView) for embedding Steerable agents in any React app.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://steerableframework.com/storybook/",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"README.md"
|
|
58
58
|
],
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@steerable/agent-protocol": "0.
|
|
60
|
+
"@steerable/agent-protocol": "0.8.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"react": ">=18.0.0",
|
|
@@ -185,6 +185,33 @@ describe('useChatStream', () => {
|
|
|
185
185
|
expect(onUnknownEvent.mock.calls[1]?.[0].type).toBe('agent');
|
|
186
186
|
});
|
|
187
187
|
|
|
188
|
+
it('stream_restart replaces the streamed text instead of appending to it', async () => {
|
|
189
|
+
// A retry after partial output discards the abandoned attempt. The host
|
|
190
|
+
// has already handed those deltas over, so the only way the live view can
|
|
191
|
+
// match the persisted reply is to take the replacement text.
|
|
192
|
+
const { transport } = makeTransport([
|
|
193
|
+
[
|
|
194
|
+
{ type: 'content', content: 'half a sen' },
|
|
195
|
+
{ type: 'agent', event: 'stream_restart', payload: { content: '' } },
|
|
196
|
+
{ type: 'content', content: 'complete reply' },
|
|
197
|
+
{ type: 'done' },
|
|
198
|
+
],
|
|
199
|
+
]);
|
|
200
|
+
const onUnknownEvent = vi.fn();
|
|
201
|
+
const { result } = renderHook(() => useChatStream({ transport, onUnknownEvent }));
|
|
202
|
+
|
|
203
|
+
await act(async () => {
|
|
204
|
+
await result.current.sendUserMessage({ content: 'go' });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await waitFor(() => {
|
|
208
|
+
const last = result.current.messages.at(-1);
|
|
209
|
+
expect(last?.content).toBe('complete reply');
|
|
210
|
+
});
|
|
211
|
+
// Still forwarded: callers that track agent events keep seeing them.
|
|
212
|
+
expect(onUnknownEvent.mock.calls.some(([e]) => e.event === 'stream_restart')).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
188
215
|
it('emits an error overlay onto the assistant message when the stream throws', async () => {
|
|
189
216
|
const transport: ChatStreamTransport = {
|
|
190
217
|
stream: async () => {
|
|
@@ -150,6 +150,7 @@ type Action =
|
|
|
150
150
|
| { type: 'insert-before-last-assistant'; message: ChatMessage }
|
|
151
151
|
| { type: 'patch-last-assistant'; patch: Partial<ChatMessage> }
|
|
152
152
|
| { type: 'append-content'; delta: string }
|
|
153
|
+
| { type: 'replace-content'; content: string }
|
|
153
154
|
| { type: 'append-tool-call'; call: ToolCall }
|
|
154
155
|
| { type: 'attach-tool-result'; result: ToolResult }
|
|
155
156
|
| { type: 'finalize-assistant' };
|
|
@@ -194,6 +195,20 @@ function reducer(state: State, action: Action): State {
|
|
|
194
195
|
next[idx] = patched;
|
|
195
196
|
return { messages: next };
|
|
196
197
|
}
|
|
198
|
+
case 'replace-content': {
|
|
199
|
+
// A retried attempt restarts the reply, so the accumulated text is no
|
|
200
|
+
// longer an append target: replacing it is the only way the live view
|
|
201
|
+
// can match what the backend will persist.
|
|
202
|
+
const idx = findLastAssistantIndex(state.messages);
|
|
203
|
+
if (idx === -1) return state;
|
|
204
|
+
const patched: ChatMessage = {
|
|
205
|
+
...state.messages[idx],
|
|
206
|
+
content: action.content,
|
|
207
|
+
} as ChatMessage;
|
|
208
|
+
const next = state.messages.slice();
|
|
209
|
+
next[idx] = patched;
|
|
210
|
+
return { messages: next };
|
|
211
|
+
}
|
|
197
212
|
case 'append-tool-call': {
|
|
198
213
|
const idx = findLastAssistantIndex(state.messages);
|
|
199
214
|
if (idx === -1) return state;
|
|
@@ -354,6 +369,19 @@ export function useChatStream(
|
|
|
354
369
|
return;
|
|
355
370
|
}
|
|
356
371
|
case 'agent':
|
|
372
|
+
// `stream_restart`: the backend discarded the attempt it was
|
|
373
|
+
// streaming and started the request again. It carries the
|
|
374
|
+
// authoritative replacement text rather than a length to cut, so
|
|
375
|
+
// the live view cannot drift from the persisted reply. Handled here
|
|
376
|
+
// because the text lives in this hook's state; still forwarded, as
|
|
377
|
+
// every other `agent` event is.
|
|
378
|
+
if (event.event === 'stream_restart') {
|
|
379
|
+
const content =
|
|
380
|
+
typeof event.payload?.content === 'string' ? event.payload.content : '';
|
|
381
|
+
dispatch({ type: 'replace-content', content });
|
|
382
|
+
}
|
|
383
|
+
options.onUnknownEvent?.(event);
|
|
384
|
+
return;
|
|
357
385
|
case 'orchestration':
|
|
358
386
|
case 'loader-hint':
|
|
359
387
|
case 'keepalive':
|