@steerable/agent-ui 0.2.3 → 0.2.5
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/cards/ActionSegmentCard.d.ts +18 -0
- package/dist/cards/ActionSegmentCard.d.ts.map +1 -0
- package/dist/cards/ActionSegmentCard.js +26 -0
- package/dist/cards/ActionSegmentCard.js.map +1 -0
- package/dist/cards/AnalysisDocumentCard.d.ts +33 -0
- package/dist/cards/AnalysisDocumentCard.d.ts.map +1 -0
- package/dist/cards/AnalysisDocumentCard.js +11 -0
- package/dist/cards/AnalysisDocumentCard.js.map +1 -0
- package/dist/cards/AskUserQuestionsCard.d.ts +43 -0
- package/dist/cards/AskUserQuestionsCard.d.ts.map +1 -0
- package/dist/cards/AskUserQuestionsCard.js +171 -0
- package/dist/cards/AskUserQuestionsCard.js.map +1 -0
- package/dist/cards/CoverageReportCard.d.ts +41 -0
- package/dist/cards/CoverageReportCard.d.ts.map +1 -0
- package/dist/cards/CoverageReportCard.js +55 -0
- package/dist/cards/CoverageReportCard.js.map +1 -0
- package/dist/cards/OrchestrationPlanCard.d.ts +60 -0
- package/dist/cards/OrchestrationPlanCard.d.ts.map +1 -0
- package/dist/cards/OrchestrationPlanCard.js +103 -0
- package/dist/cards/OrchestrationPlanCard.js.map +1 -0
- package/dist/cards/PlanSelectorCard.d.ts +40 -0
- package/dist/cards/PlanSelectorCard.d.ts.map +1 -0
- package/dist/cards/PlanSelectorCard.js +106 -0
- package/dist/cards/PlanSelectorCard.js.map +1 -0
- package/dist/cards/PlanStepsCard.d.ts +18 -0
- package/dist/cards/PlanStepsCard.d.ts.map +1 -0
- package/dist/cards/PlanStepsCard.js +25 -0
- package/dist/cards/PlanStepsCard.js.map +1 -0
- package/dist/cards/QuizCard.d.ts +44 -0
- package/dist/cards/QuizCard.d.ts.map +1 -0
- package/dist/cards/QuizCard.js +113 -0
- package/dist/cards/QuizCard.js.map +1 -0
- package/dist/cards/ResearchPlanCard.d.ts +23 -0
- package/dist/cards/ResearchPlanCard.d.ts.map +1 -0
- package/dist/cards/ResearchPlanCard.js +50 -0
- package/dist/cards/ResearchPlanCard.js.map +1 -0
- package/dist/cards/SearchSourcesCard.d.ts +29 -0
- package/dist/cards/SearchSourcesCard.d.ts.map +1 -0
- package/dist/cards/SearchSourcesCard.js +83 -0
- package/dist/cards/SearchSourcesCard.js.map +1 -0
- package/dist/cards/SuggestedRepliesCard.d.ts +18 -0
- package/dist/cards/SuggestedRepliesCard.d.ts.map +1 -0
- package/dist/cards/SuggestedRepliesCard.js +14 -0
- package/dist/cards/SuggestedRepliesCard.js.map +1 -0
- package/dist/cards/SummaryMessageCard.d.ts +40 -0
- package/dist/cards/SummaryMessageCard.d.ts.map +1 -0
- package/dist/cards/SummaryMessageCard.js +56 -0
- package/dist/cards/SummaryMessageCard.js.map +1 -0
- package/dist/cards/ThinkingProcessCard.d.ts +46 -0
- package/dist/cards/ThinkingProcessCard.d.ts.map +1 -0
- package/dist/cards/ThinkingProcessCard.js +79 -0
- package/dist/cards/ThinkingProcessCard.js.map +1 -0
- package/dist/cards/ToolExecutionCard.d.ts +21 -0
- package/dist/cards/ToolExecutionCard.d.ts.map +1 -0
- package/dist/cards/ToolExecutionCard.js +65 -0
- package/dist/cards/ToolExecutionCard.js.map +1 -0
- package/dist/cards/icons.d.ts +38 -0
- package/dist/cards/icons.d.ts.map +1 -0
- package/dist/cards/icons.js +87 -0
- package/dist/cards/icons.js.map +1 -0
- package/dist/cards/index.d.ts +38 -0
- package/dist/cards/index.d.ts.map +1 -0
- package/dist/cards/index.js +24 -0
- package/dist/cards/index.js.map +1 -0
- package/dist/cards/types.d.ts +24 -0
- package/dist/cards/types.d.ts.map +1 -0
- package/dist/cards/types.js +7 -0
- package/dist/cards/types.js.map +1 -0
- package/dist/components/AgentSelector.d.ts +23 -0
- package/dist/components/AgentSelector.d.ts.map +1 -0
- package/dist/components/AgentSelector.js +63 -0
- package/dist/components/AgentSelector.js.map +1 -0
- package/dist/components/AgentSelector.stories.d.ts +12 -0
- package/dist/components/AgentSelector.stories.d.ts.map +1 -0
- package/dist/components/AgentSelector.stories.js +95 -0
- package/dist/components/AgentSelector.stories.js.map +1 -0
- package/dist/components/ChatPanel.d.ts +138 -21
- package/dist/components/ChatPanel.d.ts.map +1 -1
- package/dist/components/ChatPanel.js +181 -26
- package/dist/components/ChatPanel.js.map +1 -1
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/components/index.js.map +1 -1
- package/dist/hooks/useChatStream.d.ts +15 -0
- package/dist/hooks/useChatStream.d.ts.map +1 -1
- package/dist/hooks/useChatStream.js +13 -1
- package/dist/hooks/useChatStream.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/state/ChatSessionProvider.d.ts +59 -0
- package/dist/state/ChatSessionProvider.d.ts.map +1 -0
- package/dist/state/ChatSessionProvider.js +71 -0
- package/dist/state/ChatSessionProvider.js.map +1 -0
- package/dist/state/MockChatStreamTransport.d.ts +54 -0
- package/dist/state/MockChatStreamTransport.d.ts.map +1 -0
- package/dist/state/MockChatStreamTransport.js +86 -0
- package/dist/state/MockChatStreamTransport.js.map +1 -0
- package/dist/state/MockChatStreamTransport.stories.d.ts +13 -0
- package/dist/state/MockChatStreamTransport.stories.d.ts.map +1 -0
- package/dist/state/MockChatStreamTransport.stories.js +60 -0
- package/dist/state/MockChatStreamTransport.stories.js.map +1 -0
- package/dist/state/bridgeLegacySSE.d.ts +42 -0
- package/dist/state/bridgeLegacySSE.d.ts.map +1 -0
- package/dist/state/bridgeLegacySSE.js +102 -0
- package/dist/state/bridgeLegacySSE.js.map +1 -0
- package/dist/state/index.d.ts +32 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +24 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/parseSSE.d.ts +50 -0
- package/dist/state/parseSSE.d.ts.map +1 -0
- package/dist/state/parseSSE.js +112 -0
- package/dist/state/parseSSE.js.map +1 -0
- package/dist/state/useChatComposer.d.ts +51 -0
- package/dist/state/useChatComposer.d.ts.map +1 -0
- package/dist/state/useChatComposer.js +82 -0
- package/dist/state/useChatComposer.js.map +1 -0
- package/dist/state/useChatList.d.ts +73 -0
- package/dist/state/useChatList.d.ts.map +1 -0
- package/dist/state/useChatList.js +161 -0
- package/dist/state/useChatList.js.map +1 -0
- package/dist/state/useChatSession.d.ts +29 -0
- package/dist/state/useChatSession.d.ts.map +1 -0
- package/dist/state/useChatSession.js +37 -0
- package/dist/state/useChatSession.js.map +1 -0
- package/dist/state/useScrollLock.d.ts +35 -0
- package/dist/state/useScrollLock.d.ts.map +1 -0
- package/dist/state/useScrollLock.js +74 -0
- package/dist/state/useScrollLock.js.map +1 -0
- package/dist/state/useToolCallStream.d.ts +38 -0
- package/dist/state/useToolCallStream.d.ts.map +1 -0
- package/dist/state/useToolCallStream.js +74 -0
- package/dist/state/useToolCallStream.js.map +1 -0
- package/package.json +11 -3
- package/src/cards/ActionSegmentCard.tsx +60 -0
- package/src/cards/AnalysisDocumentCard.tsx +81 -0
- package/src/cards/AskUserQuestionsCard.tsx +354 -0
- package/src/cards/CoverageReportCard.tsx +215 -0
- package/src/cards/OrchestrationPlanCard.tsx +328 -0
- package/src/cards/PlanSelectorCard.tsx +312 -0
- package/src/cards/PlanStepsCard.tsx +52 -0
- package/src/cards/QuizCard.tsx +309 -0
- package/src/cards/ResearchPlanCard.tsx +169 -0
- package/src/cards/SearchSourcesCard.tsx +244 -0
- package/src/cards/SuggestedRepliesCard.tsx +48 -0
- package/src/cards/SummaryMessageCard.tsx +145 -0
- package/src/cards/ThinkingProcessCard.tsx +155 -0
- package/src/cards/ToolExecutionCard.tsx +152 -0
- package/src/cards/cards.test.tsx +492 -0
- package/src/cards/icons.tsx +249 -0
- package/src/cards/index.ts +53 -0
- package/src/cards/types.ts +27 -0
- package/src/components/AgentSelector.stories.tsx +113 -0
- package/src/components/AgentSelector.test.tsx +70 -0
- package/src/components/AgentSelector.tsx +147 -0
- package/src/components/ChatPanel.test.tsx +130 -0
- package/src/components/ChatPanel.tsx +612 -138
- package/src/components/index.ts +13 -1
- package/src/hooks/useChatStream.test.ts +78 -0
- package/src/hooks/useChatStream.ts +32 -1
- package/src/index.ts +43 -0
- package/src/state/ChatSessionProvider.test.tsx +85 -0
- package/src/state/ChatSessionProvider.tsx +98 -0
- package/src/state/MockChatStreamTransport.stories.tsx +95 -0
- package/src/state/MockChatStreamTransport.test.ts +163 -0
- package/src/state/MockChatStreamTransport.ts +123 -0
- package/src/state/bridgeLegacySSE.test.ts +134 -0
- package/src/state/bridgeLegacySSE.ts +134 -0
- package/src/state/index.ts +82 -0
- package/src/state/parseSSE.test.ts +123 -0
- package/src/state/parseSSE.ts +123 -0
- package/src/state/useChatComposer.test.ts +145 -0
- package/src/state/useChatComposer.ts +117 -0
- package/src/state/useChatList.test.ts +135 -0
- package/src/state/useChatList.ts +220 -0
- package/src/state/useChatSession.test.ts +61 -0
- package/src/state/useChatSession.ts +64 -0
- package/src/state/useScrollLock.test.ts +99 -0
- package/src/state/useScrollLock.ts +105 -0
- package/src/state/useToolCallStream.test.ts +86 -0
- package/src/state/useToolCallStream.ts +110 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MockChatStreamTransport` — canned-script transport for stories, tests, and
|
|
3
|
+
* the framework's runnable web-shell example.
|
|
4
|
+
*
|
|
5
|
+
* Replays a deterministic list of `SSEEvent` per call to `stream()`. Optional
|
|
6
|
+
* per-event delays make streaming feel real in demos. Loop the same script
|
|
7
|
+
* forever, or vary by index — the constructor accepts either an array (one
|
|
8
|
+
* script per turn) or a function `(turn: number) => SSEEvent[]`.
|
|
9
|
+
*
|
|
10
|
+
* const transport = new MockChatStreamTransport({
|
|
11
|
+
* scripts: [
|
|
12
|
+
* [{ type: 'content', content: 'Hello, ' }, { type: 'content', content: 'world!' }, { type: 'done' }],
|
|
13
|
+
* ],
|
|
14
|
+
* defaultDelayMs: 25,
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* const { messages, sendUserMessage } = useChatStream({ transport });
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { SSEEvent } from '@steerable/agent-protocol';
|
|
21
|
+
import type {
|
|
22
|
+
ChatStreamSendInput,
|
|
23
|
+
ChatStreamTransport,
|
|
24
|
+
} from '../hooks/useChatStream.js';
|
|
25
|
+
|
|
26
|
+
export type MockScriptStep =
|
|
27
|
+
| SSEEvent
|
|
28
|
+
| { event: SSEEvent; delayMs?: number };
|
|
29
|
+
|
|
30
|
+
export type MockScript = MockScriptStep[];
|
|
31
|
+
|
|
32
|
+
export interface MockChatStreamTransportOptions {
|
|
33
|
+
/**
|
|
34
|
+
* A list of scripts (one per turn), or a callback that returns a script for
|
|
35
|
+
* the current turn. Required.
|
|
36
|
+
*/
|
|
37
|
+
scripts: MockScript[] | ((turn: number, input: ChatStreamSendInput) => MockScript);
|
|
38
|
+
/** Delay between events when the step doesn't specify one. Defaults to 0. */
|
|
39
|
+
defaultDelayMs?: number;
|
|
40
|
+
/**
|
|
41
|
+
* What to do when there are fewer scripts than turns:
|
|
42
|
+
* - 'cycle' (default): wrap modulo `scripts.length`.
|
|
43
|
+
* - 'last': stay on the last script.
|
|
44
|
+
* - 'empty': emit only a `done` event.
|
|
45
|
+
*/
|
|
46
|
+
exhaustionPolicy?: 'cycle' | 'last' | 'empty';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class MockChatStreamTransport implements ChatStreamTransport {
|
|
50
|
+
private turn = 0;
|
|
51
|
+
/** Set of in-flight cancel signals, exposed for tests. */
|
|
52
|
+
readonly cancelled = new Set<number>();
|
|
53
|
+
|
|
54
|
+
constructor(private readonly options: MockChatStreamTransportOptions) {}
|
|
55
|
+
|
|
56
|
+
/** Reset the turn counter — useful between fixture-driven test cases. */
|
|
57
|
+
reset(): void {
|
|
58
|
+
this.turn = 0;
|
|
59
|
+
this.cancelled.clear();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async stream(
|
|
63
|
+
input: ChatStreamSendInput,
|
|
64
|
+
onEvent: (event: SSEEvent) => void,
|
|
65
|
+
): Promise<() => void> {
|
|
66
|
+
const turn = this.turn++;
|
|
67
|
+
const script = this.resolveScript(turn, input);
|
|
68
|
+
let cancelled = false;
|
|
69
|
+
const cancel = () => {
|
|
70
|
+
cancelled = true;
|
|
71
|
+
this.cancelled.add(turn);
|
|
72
|
+
};
|
|
73
|
+
void this.run(script, onEvent, () => cancelled);
|
|
74
|
+
// Return synchronously so the framework hook can capture the cancel handle
|
|
75
|
+
// without waiting for the full script.
|
|
76
|
+
return cancel;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private resolveScript(turn: number, input: ChatStreamSendInput): MockScript {
|
|
80
|
+
const { scripts, exhaustionPolicy = 'cycle' } = this.options;
|
|
81
|
+
if (typeof scripts === 'function') return scripts(turn, input);
|
|
82
|
+
if (scripts.length === 0) return [{ type: 'done' }];
|
|
83
|
+
if (turn < scripts.length) return scripts[turn];
|
|
84
|
+
switch (exhaustionPolicy) {
|
|
85
|
+
case 'last':
|
|
86
|
+
return scripts[scripts.length - 1];
|
|
87
|
+
case 'empty':
|
|
88
|
+
return [{ type: 'done' }];
|
|
89
|
+
case 'cycle':
|
|
90
|
+
default:
|
|
91
|
+
return scripts[turn % scripts.length];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private async run(
|
|
96
|
+
script: MockScript,
|
|
97
|
+
onEvent: (event: SSEEvent) => void,
|
|
98
|
+
isCancelled: () => boolean,
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
const defaultDelay = this.options.defaultDelayMs ?? 0;
|
|
101
|
+
for (const step of script) {
|
|
102
|
+
if (isCancelled()) return;
|
|
103
|
+
const { event, delayMs } = normaliseStep(step, defaultDelay);
|
|
104
|
+
if (delayMs > 0) await sleep(delayMs);
|
|
105
|
+
if (isCancelled()) return;
|
|
106
|
+
onEvent(event);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normaliseStep(
|
|
112
|
+
step: MockScriptStep,
|
|
113
|
+
defaultDelayMs: number,
|
|
114
|
+
): { event: SSEEvent; delayMs: number } {
|
|
115
|
+
if ('event' in step && step.event && typeof step.event === 'object') {
|
|
116
|
+
return { event: step.event, delayMs: step.delayMs ?? defaultDelayMs };
|
|
117
|
+
}
|
|
118
|
+
return { event: step as SSEEvent, delayMs: defaultDelayMs };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function sleep(ms: number): Promise<void> {
|
|
122
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
123
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `bridgeLegacySSE`. The bridge is the union of the deeppath cloud
|
|
3
|
+
* legacy normaliser and the deeppath-agent local-backend normaliser; we cover
|
|
4
|
+
* the canonical shapes for each profile.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from 'vitest';
|
|
8
|
+
import { bridgeLegacySSE } from './bridgeLegacySSE';
|
|
9
|
+
|
|
10
|
+
describe('bridgeLegacySSE', () => {
|
|
11
|
+
describe('canonical SSEEvent', () => {
|
|
12
|
+
it('passes through known typed events untouched', () => {
|
|
13
|
+
const ev = bridgeLegacySSE({ type: 'content', content: 'hi' });
|
|
14
|
+
expect(ev).toEqual({ type: 'content', content: 'hi' });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('routes tool_call payloads as-is for useChatStream to unpack', () => {
|
|
18
|
+
const ev = bridgeLegacySSE({
|
|
19
|
+
type: 'tool_call',
|
|
20
|
+
payload: { id: 'c1', name: 'get_weather', arguments: {} },
|
|
21
|
+
});
|
|
22
|
+
expect(ev?.type).toBe('tool_call');
|
|
23
|
+
expect(ev?.payload).toEqual({ id: 'c1', name: 'get_weather', arguments: {} });
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('deeppath cloud legacy', () => {
|
|
28
|
+
it('maps bare `{content}` deltas to type:content', () => {
|
|
29
|
+
expect(bridgeLegacySSE({ content: 'Hello' })).toEqual({
|
|
30
|
+
type: 'content',
|
|
31
|
+
content: 'Hello',
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('maps bare `{error}` to type:error', () => {
|
|
36
|
+
expect(bridgeLegacySSE({ error: 'boom' })).toEqual({
|
|
37
|
+
type: 'error',
|
|
38
|
+
message: 'boom',
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('coerces bare strings (rare legacy path)', () => {
|
|
43
|
+
expect(bridgeLegacySSE('hi')).toEqual({ type: 'content', content: 'hi' });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('drops empty bare strings', () => {
|
|
47
|
+
expect(bridgeLegacySSE('')).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('deeppath-agent local backend', () => {
|
|
52
|
+
it('completion:completed → done', () => {
|
|
53
|
+
expect(
|
|
54
|
+
bridgeLegacySSE({ type: 'completion', status: 'completed' }),
|
|
55
|
+
).toEqual({ type: 'done', payload: { type: 'completion', status: 'completed' } });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('completion:failed → done', () => {
|
|
59
|
+
const ev = bridgeLegacySSE({ type: 'completion', status: 'failed' });
|
|
60
|
+
expect(ev?.type).toBe('done');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('completion:budget_exhausted → budget_exhausted', () => {
|
|
64
|
+
const ev = bridgeLegacySSE({
|
|
65
|
+
type: 'completion',
|
|
66
|
+
status: 'budget_exhausted',
|
|
67
|
+
reason: 'token limit',
|
|
68
|
+
});
|
|
69
|
+
expect(ev).toEqual({ type: 'budget_exhausted', message: 'token limit' });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('completion:executing → agent/round_end', () => {
|
|
73
|
+
const ev = bridgeLegacySSE({ type: 'completion', status: 'executing' });
|
|
74
|
+
expect(ev?.type).toBe('agent');
|
|
75
|
+
expect(ev?.event).toBe('round_end');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('user_message is suppressed (echo from the backend)', () => {
|
|
79
|
+
expect(
|
|
80
|
+
bridgeLegacySSE({ type: 'user_message', message: { content: 'hi' } }),
|
|
81
|
+
).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('executed_actions → agent/executed_actions', () => {
|
|
85
|
+
const ev = bridgeLegacySSE({ type: 'executed_actions', actions: [] });
|
|
86
|
+
expect(ev?.type).toBe('agent');
|
|
87
|
+
expect(ev?.event).toBe('executed_actions');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('message_id → agent/message_id', () => {
|
|
91
|
+
const ev = bridgeLegacySSE({ type: 'message_id', messageId: 'm1' });
|
|
92
|
+
expect(ev?.type).toBe('agent');
|
|
93
|
+
expect(ev?.event).toBe('message_id');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('typed error envelope (data.type === "error")', () => {
|
|
97
|
+
const ev = bridgeLegacySSE({ type: 'error', message: 'kaboom' });
|
|
98
|
+
expect(ev).toEqual({ type: 'error', message: 'kaboom' });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('event: name overrides', () => {
|
|
103
|
+
it('event=error wins over the inner payload type', () => {
|
|
104
|
+
const ev = bridgeLegacySSE({ message: 'transport-level error' }, 'error');
|
|
105
|
+
expect(ev).toEqual({ type: 'error', message: 'transport-level error' });
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('unknown envelope passthrough', () => {
|
|
110
|
+
it('repackages unknown `type` as agent/<type> by default', () => {
|
|
111
|
+
const ev = bridgeLegacySSE({ type: 'stage-complete', stage: 'planning' });
|
|
112
|
+
expect(ev?.type).toBe('agent');
|
|
113
|
+
expect(ev?.event).toBe('stage-complete');
|
|
114
|
+
expect(ev?.payload).toEqual({ type: 'stage-complete', stage: 'planning' });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('respects passthroughUnknownAsAgent=false', () => {
|
|
118
|
+
const ev = bridgeLegacySSE(
|
|
119
|
+
{ type: 'wholly-unknown' },
|
|
120
|
+
undefined,
|
|
121
|
+
{ passthroughUnknownAsAgent: false, profile: 'deeppathCloud' },
|
|
122
|
+
);
|
|
123
|
+
expect(ev).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('null inputs', () => {
|
|
128
|
+
it('returns null for non-object, non-string inputs', () => {
|
|
129
|
+
expect(bridgeLegacySSE(null)).toBeNull();
|
|
130
|
+
expect(bridgeLegacySSE(undefined)).toBeNull();
|
|
131
|
+
expect(bridgeLegacySSE(42)).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bridgeLegacySSE` — turn the heterogeneous wire envelopes that deeppath's
|
|
3
|
+
* Next.js API and deeppath-agent's local backend speak into the canonical
|
|
4
|
+
* `SSEEvent` consumed by `useChatStream`.
|
|
5
|
+
*
|
|
6
|
+
* Lifted from:
|
|
7
|
+
* - deeppath/apps/web/src/lib/utils/framework-sse-bridge.ts (normalize())
|
|
8
|
+
* - deeppath-agent/apps/web/src/lib/chat-transport.ts (normaliseLocalPayload())
|
|
9
|
+
*
|
|
10
|
+
* The two implementations are merged here, with deeppath-agent's richer
|
|
11
|
+
* `completion` / `executed_actions` / `user_message` / `message_id` handling
|
|
12
|
+
* promoted as the default and the older deeppath wire shapes added as
|
|
13
|
+
* `EnvelopeProfile.deeppathCloud`. A consumer picks a profile (or "auto"),
|
|
14
|
+
* and the bridge does the rest.
|
|
15
|
+
*
|
|
16
|
+
* The bridge is intentionally pure — no I/O, no DOM, no electron — so it can
|
|
17
|
+
* back any transport (fetch SSE, IPC streaming, mocked fixtures, JSON-RPC).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { SSEEvent } from '@steerable/agent-protocol';
|
|
21
|
+
|
|
22
|
+
export type EnvelopeProfile =
|
|
23
|
+
| 'auto'
|
|
24
|
+
/** Next.js `/api/chats/:id/send` legacy wire — `{content}` deltas, `{error}` envelopes. */
|
|
25
|
+
| 'deeppathCloud'
|
|
26
|
+
/** Electron local-backend wire — typed completion/executed_actions/user_message. */
|
|
27
|
+
| 'deeppathLocal';
|
|
28
|
+
|
|
29
|
+
export interface BridgeLegacySSEOptions {
|
|
30
|
+
profile?: EnvelopeProfile;
|
|
31
|
+
/**
|
|
32
|
+
* If we see a typed event with `type` outside the canonical SSEEvent union
|
|
33
|
+
* (e.g. `stage-complete`, `executed_actions`), repackage it as an `agent`
|
|
34
|
+
* event whose `payload` is the raw envelope. Defaults to `true` so the
|
|
35
|
+
* framework `useChatStream`'s `onUnknownEvent` channel sees them.
|
|
36
|
+
*/
|
|
37
|
+
passthroughUnknownAsAgent?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const KNOWN_EVENT_TYPES = new Set<SSEEvent['type']>([
|
|
41
|
+
'content',
|
|
42
|
+
'error',
|
|
43
|
+
'agent',
|
|
44
|
+
'orchestration',
|
|
45
|
+
'loader-hint',
|
|
46
|
+
'keepalive',
|
|
47
|
+
'done',
|
|
48
|
+
'budget_exhausted',
|
|
49
|
+
'tool_call',
|
|
50
|
+
'tool_result',
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Convert one parsed envelope (the JSON value of `data:` plus the optional
|
|
55
|
+
* `event:` name) into zero or one `SSEEvent`. Returns null if the envelope is
|
|
56
|
+
* deliberately suppressed (e.g. `user_message` echoes, since useChatStream
|
|
57
|
+
* already appends the user message locally).
|
|
58
|
+
*/
|
|
59
|
+
export function bridgeLegacySSE(
|
|
60
|
+
parsed: unknown,
|
|
61
|
+
eventName?: string,
|
|
62
|
+
options: BridgeLegacySSEOptions = {},
|
|
63
|
+
): SSEEvent | null {
|
|
64
|
+
const profile = options.profile ?? 'auto';
|
|
65
|
+
const passthroughUnknown = options.passthroughUnknownAsAgent ?? true;
|
|
66
|
+
|
|
67
|
+
if (typeof parsed === 'string') {
|
|
68
|
+
return parsed.length > 0 ? { type: 'content', content: parsed } : null;
|
|
69
|
+
}
|
|
70
|
+
if (parsed === null || typeof parsed !== 'object') {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const data = parsed as Record<string, unknown>;
|
|
74
|
+
|
|
75
|
+
if (eventName === 'error') {
|
|
76
|
+
return {
|
|
77
|
+
type: 'error',
|
|
78
|
+
message: typeof data.message === 'string' ? data.message : 'unknown error',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Legacy: `{ content: '...' }` carries an incremental token (deeppath cloud).
|
|
83
|
+
if (typeof data.content === 'string' && !('type' in data)) {
|
|
84
|
+
return { type: 'content', content: data.content };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Legacy: `{ error: '...' }` is an inline error envelope (deeppath cloud).
|
|
88
|
+
if (typeof data.error === 'string' && !('type' in data)) {
|
|
89
|
+
return { type: 'error', message: data.error };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Canonical typed events.
|
|
93
|
+
if (typeof data.type === 'string') {
|
|
94
|
+
if (KNOWN_EVENT_TYPES.has(data.type as SSEEvent['type'])) {
|
|
95
|
+
return data as unknown as SSEEvent;
|
|
96
|
+
}
|
|
97
|
+
if (profile === 'deeppathLocal' || profile === 'auto') {
|
|
98
|
+
const mapped = mapLocalBackendEnvelope(data);
|
|
99
|
+
if (mapped !== undefined) return mapped;
|
|
100
|
+
}
|
|
101
|
+
if (passthroughUnknown) {
|
|
102
|
+
return { type: 'agent', event: data.type, payload: data };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function mapLocalBackendEnvelope(
|
|
109
|
+
data: Record<string, unknown>,
|
|
110
|
+
): SSEEvent | null | undefined {
|
|
111
|
+
switch (data.type as string) {
|
|
112
|
+
case 'completion': {
|
|
113
|
+
const status = data.status as string;
|
|
114
|
+
if (status === 'completed' || status === 'failed') {
|
|
115
|
+
return { type: 'done', payload: data };
|
|
116
|
+
}
|
|
117
|
+
if (status === 'budget_exhausted') {
|
|
118
|
+
return {
|
|
119
|
+
type: 'budget_exhausted',
|
|
120
|
+
message: typeof data.reason === 'string' ? data.reason : undefined,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return { type: 'agent', event: 'round_end', payload: data };
|
|
124
|
+
}
|
|
125
|
+
case 'executed_actions':
|
|
126
|
+
return { type: 'agent', event: 'executed_actions', payload: data };
|
|
127
|
+
case 'user_message':
|
|
128
|
+
return null;
|
|
129
|
+
case 'message_id':
|
|
130
|
+
return { type: 'agent', event: 'message_id', payload: data };
|
|
131
|
+
default:
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@steerable/agent-ui/state` — chat-state primitives lifted out of deeppath
|
|
3
|
+
* and deeppath-agent. Composes with the existing `hooks/` exports.
|
|
4
|
+
*
|
|
5
|
+
* Subpath import recommended:
|
|
6
|
+
*
|
|
7
|
+
* import { useChatSession, MockChatStreamTransport } from '@steerable/agent-ui/state';
|
|
8
|
+
*
|
|
9
|
+
* but the same identifiers are also re-exported from the package root for
|
|
10
|
+
* back-compat.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { SSEParser, parseSSEData } from './parseSSE.js';
|
|
14
|
+
export type { SSEFrame, SSEParserOptions } from './parseSSE.js';
|
|
15
|
+
|
|
16
|
+
export { bridgeLegacySSE } from './bridgeLegacySSE.js';
|
|
17
|
+
export type { EnvelopeProfile, BridgeLegacySSEOptions } from './bridgeLegacySSE.js';
|
|
18
|
+
|
|
19
|
+
export { MockChatStreamTransport } from './MockChatStreamTransport.js';
|
|
20
|
+
export type {
|
|
21
|
+
MockScript,
|
|
22
|
+
MockScriptStep,
|
|
23
|
+
MockChatStreamTransportOptions,
|
|
24
|
+
} from './MockChatStreamTransport.js';
|
|
25
|
+
|
|
26
|
+
export { useChatComposer } from './useChatComposer.js';
|
|
27
|
+
export type {
|
|
28
|
+
UseChatComposerOptions,
|
|
29
|
+
UseChatComposerReturn,
|
|
30
|
+
} from './useChatComposer.js';
|
|
31
|
+
|
|
32
|
+
export { useChatList } from './useChatList.js';
|
|
33
|
+
export type {
|
|
34
|
+
ChatListTransport,
|
|
35
|
+
UseChatListOptions,
|
|
36
|
+
UseChatListReturn,
|
|
37
|
+
} from './useChatList.js';
|
|
38
|
+
|
|
39
|
+
export { useChatSession } from './useChatSession.js';
|
|
40
|
+
export type {
|
|
41
|
+
UseChatSessionOptions,
|
|
42
|
+
UseChatSessionReturn,
|
|
43
|
+
} from './useChatSession.js';
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
ChatSessionProvider,
|
|
47
|
+
useChatSessionContext,
|
|
48
|
+
useOptionalChatSession,
|
|
49
|
+
useChatSessionSlice,
|
|
50
|
+
} from './ChatSessionProvider.js';
|
|
51
|
+
export type {
|
|
52
|
+
ChatSessionContextValue,
|
|
53
|
+
ChatSessionProviderProps,
|
|
54
|
+
} from './ChatSessionProvider.js';
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
useToolCallStream,
|
|
58
|
+
useToolCallStatus,
|
|
59
|
+
} from './useToolCallStream.js';
|
|
60
|
+
export type {
|
|
61
|
+
ToolCallEntry,
|
|
62
|
+
UseToolCallStreamOptions,
|
|
63
|
+
UseToolCallStreamReturn,
|
|
64
|
+
} from './useToolCallStream.js';
|
|
65
|
+
|
|
66
|
+
export { useScrollLock } from './useScrollLock.js';
|
|
67
|
+
export type {
|
|
68
|
+
UseScrollLockOptions,
|
|
69
|
+
UseScrollLockReturn,
|
|
70
|
+
} from './useScrollLock.js';
|
|
71
|
+
|
|
72
|
+
// Re-export the canonical chat-stream surface so callers can stay on a single
|
|
73
|
+
// import root if they don't want to think about subpaths.
|
|
74
|
+
export {
|
|
75
|
+
useChatStream,
|
|
76
|
+
} from '../hooks/useChatStream.js';
|
|
77
|
+
export type {
|
|
78
|
+
UseChatStreamOptions,
|
|
79
|
+
UseChatStreamReturn,
|
|
80
|
+
ChatStreamSendInput,
|
|
81
|
+
ChatStreamTransport,
|
|
82
|
+
} from '../hooks/useChatStream.js';
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `SSEParser` and `parseSSEData`.
|
|
3
|
+
*
|
|
4
|
+
* The parser owns the wire-level framing: split on `\r?\n\r?\n`, accumulate
|
|
5
|
+
* multi-line `data:` payloads, surface `[DONE]` via `onComplete`. We cover the
|
|
6
|
+
* shapes deeppath and deeppath-agent legacy parsers handled, so the cutover
|
|
7
|
+
* doesn't lose any wire variants.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { SSEParser, parseSSEData } from './parseSSE';
|
|
12
|
+
|
|
13
|
+
describe('SSEParser', () => {
|
|
14
|
+
it('emits one frame per blank-line-separated block', () => {
|
|
15
|
+
const onFrame = vi.fn();
|
|
16
|
+
const p = new SSEParser({ onFrame });
|
|
17
|
+
p.feed('data: {"content":"foo"}\n\ndata: {"content":"bar"}\n\n');
|
|
18
|
+
expect(onFrame).toHaveBeenCalledTimes(2);
|
|
19
|
+
expect(onFrame.mock.calls[0][0].data).toBe('{"content":"foo"}');
|
|
20
|
+
expect(onFrame.mock.calls[1][0].data).toBe('{"content":"bar"}');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('handles split chunks across frame boundaries', () => {
|
|
24
|
+
const onFrame = vi.fn();
|
|
25
|
+
const p = new SSEParser({ onFrame });
|
|
26
|
+
p.feed('data: {"content":"hello,');
|
|
27
|
+
expect(onFrame).not.toHaveBeenCalled();
|
|
28
|
+
p.feed(' world!"}\n\n');
|
|
29
|
+
expect(onFrame).toHaveBeenCalledTimes(1);
|
|
30
|
+
expect(onFrame.mock.calls[0][0].data).toBe('{"content":"hello, world!"}');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('joins multi-line data: per SSE spec (newline-joined)', () => {
|
|
34
|
+
const onFrame = vi.fn();
|
|
35
|
+
const p = new SSEParser({ onFrame });
|
|
36
|
+
p.feed('data: line one\ndata: line two\n\n');
|
|
37
|
+
expect(onFrame.mock.calls[0][0].data).toBe('line one\nline two');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('captures event:, id:, retry:', () => {
|
|
41
|
+
const onFrame = vi.fn();
|
|
42
|
+
const p = new SSEParser({ onFrame });
|
|
43
|
+
p.feed('event: error\nid: 42\nretry: 5000\ndata: {"message":"boom"}\n\n');
|
|
44
|
+
const frame = onFrame.mock.calls[0][0];
|
|
45
|
+
expect(frame.event).toBe('error');
|
|
46
|
+
expect(frame.id).toBe('42');
|
|
47
|
+
expect(frame.retry).toBe(5000);
|
|
48
|
+
expect(frame.data).toBe('{"message":"boom"}');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('triggers onComplete on the canonical [DONE] terminator', () => {
|
|
52
|
+
const onFrame = vi.fn();
|
|
53
|
+
const onComplete = vi.fn();
|
|
54
|
+
const p = new SSEParser({ onFrame, onComplete });
|
|
55
|
+
p.feed('data: {"content":"x"}\n\ndata: [DONE]\n\n');
|
|
56
|
+
expect(onFrame).toHaveBeenCalledTimes(1);
|
|
57
|
+
expect(onComplete).toHaveBeenCalledTimes(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('triggers onComplete on quoted "[DONE]"', () => {
|
|
61
|
+
const onComplete = vi.fn();
|
|
62
|
+
const p = new SSEParser({ onFrame: () => {}, onComplete });
|
|
63
|
+
p.feed('data: "[DONE]"\n\n');
|
|
64
|
+
expect(onComplete).toHaveBeenCalledOnce();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('ignores comment lines (leading colon)', () => {
|
|
68
|
+
const onFrame = vi.fn();
|
|
69
|
+
const p = new SSEParser({ onFrame });
|
|
70
|
+
p.feed(': keep-alive comment\ndata: real\n\n');
|
|
71
|
+
expect(onFrame).toHaveBeenCalledTimes(1);
|
|
72
|
+
expect(onFrame.mock.calls[0][0].data).toBe('real');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('flushes trailing buffered frame on end()', () => {
|
|
76
|
+
const onFrame = vi.fn();
|
|
77
|
+
const p = new SSEParser({ onFrame });
|
|
78
|
+
p.feed('data: orphan'); // no terminator
|
|
79
|
+
expect(onFrame).not.toHaveBeenCalled();
|
|
80
|
+
p.end();
|
|
81
|
+
expect(onFrame).toHaveBeenCalledTimes(1);
|
|
82
|
+
expect(onFrame.mock.calls[0][0].data).toBe('orphan');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('supports \\r\\n\\r\\n frame separators (Windows / some proxies)', () => {
|
|
86
|
+
const onFrame = vi.fn();
|
|
87
|
+
const p = new SSEParser({ onFrame });
|
|
88
|
+
p.feed('data: one\r\n\r\ndata: two\r\n\r\n');
|
|
89
|
+
expect(onFrame).toHaveBeenCalledTimes(2);
|
|
90
|
+
expect(onFrame.mock.calls[0][0].data).toBe('one');
|
|
91
|
+
expect(onFrame.mock.calls[1][0].data).toBe('two');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('stops emitting after cleanup()', () => {
|
|
95
|
+
const onFrame = vi.fn();
|
|
96
|
+
const p = new SSEParser({ onFrame });
|
|
97
|
+
p.cleanup();
|
|
98
|
+
p.feed('data: ignored\n\n');
|
|
99
|
+
expect(onFrame).not.toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('reports parsing exceptions via onError', () => {
|
|
103
|
+
const onError = vi.fn();
|
|
104
|
+
const p = new SSEParser({
|
|
105
|
+
onFrame: () => {
|
|
106
|
+
throw new Error('downstream blew up');
|
|
107
|
+
},
|
|
108
|
+
onError,
|
|
109
|
+
});
|
|
110
|
+
p.feed('data: x\n\n');
|
|
111
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
112
|
+
expect(onError.mock.calls[0][0].message).toBe('downstream blew up');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('parseSSEData', () => {
|
|
117
|
+
it('parses JSON payloads', () => {
|
|
118
|
+
expect(parseSSEData('{"a":1}')).toEqual({ a: 1 });
|
|
119
|
+
});
|
|
120
|
+
it('returns the original string when not JSON (bare text deltas)', () => {
|
|
121
|
+
expect(parseSSEData('hello')).toBe('hello');
|
|
122
|
+
});
|
|
123
|
+
});
|