@steerable/agent-ui 0.2.0 → 0.2.4
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/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 +71 -0
- package/dist/state/useChatList.d.ts.map +1 -0
- package/dist/state/useChatList.js +158 -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 +10 -2
- 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/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 +217 -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,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
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `parseSSE` — wire-level Server-Sent-Events parser.
|
|
3
|
+
*
|
|
4
|
+
* Feed it raw text chunks from any source (fetch ReadableStream chunks,
|
|
5
|
+
* Electron IPC `webContents.send` payloads, mocked fixtures, …). It buffers,
|
|
6
|
+
* splits on the `\r?\n\r?\n` frame terminator, and emits one
|
|
7
|
+
* `{ event?, data?, id?, retry? }` per complete frame. JSON parsing of `data`
|
|
8
|
+
* is intentionally out of scope; downstream layers like {@link bridgeLegacySSE}
|
|
9
|
+
* handle that.
|
|
10
|
+
*
|
|
11
|
+
* Replaces deeppath's `SSEParser` and deeppath-agent's `LocalBackendSseParser`
|
|
12
|
+
* with a single dependency-free implementation that compiles to both Node and
|
|
13
|
+
* the browser. Heartbeat-timeout watchdogs (deeppath had a 30 s gate) live on
|
|
14
|
+
* the transport, not here — this class is a pure parser.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface SSEFrame {
|
|
18
|
+
/** The `event: …` field; undefined for default `message` events. */
|
|
19
|
+
event?: string;
|
|
20
|
+
/** Concatenated `data: …` lines (newline-joined per the SSE spec). */
|
|
21
|
+
data?: string;
|
|
22
|
+
/** The `id: …` field. */
|
|
23
|
+
id?: string;
|
|
24
|
+
/** The `retry: …` field, as milliseconds. */
|
|
25
|
+
retry?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SSEParserOptions {
|
|
29
|
+
onFrame: (frame: SSEFrame) => void;
|
|
30
|
+
onError?: (err: Error) => void;
|
|
31
|
+
/** Called when the stream emits the canonical `data: [DONE]` terminator. */
|
|
32
|
+
onComplete?: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class SSEParser {
|
|
36
|
+
private buffer = '';
|
|
37
|
+
private completed = false;
|
|
38
|
+
|
|
39
|
+
constructor(private readonly options: SSEParserOptions) {}
|
|
40
|
+
|
|
41
|
+
/** Feed one chunk. Safe to call with partial UTF-8 strings (we don't decode). */
|
|
42
|
+
feed(chunk: string): void {
|
|
43
|
+
if (this.completed) return;
|
|
44
|
+
this.buffer += chunk;
|
|
45
|
+
const parts = this.buffer.split(/\r?\n\r?\n/);
|
|
46
|
+
this.buffer = parts.pop() ?? '';
|
|
47
|
+
for (const frame of parts) {
|
|
48
|
+
if (frame.length > 0) this.processFrame(frame);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Call when the source signals end-of-stream. Flushes any trailing frame. */
|
|
53
|
+
end(): void {
|
|
54
|
+
if (this.completed) return;
|
|
55
|
+
if (this.buffer.trim().length > 0) this.processFrame(this.buffer);
|
|
56
|
+
this.buffer = '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Drop all state and stop emitting. */
|
|
60
|
+
cleanup(): void {
|
|
61
|
+
this.buffer = '';
|
|
62
|
+
this.completed = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private processFrame(raw: string): void {
|
|
66
|
+
const frame: SSEFrame = {};
|
|
67
|
+
const dataLines: string[] = [];
|
|
68
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
69
|
+
if (line.length === 0) continue;
|
|
70
|
+
// Comments per the SSE spec — leading `:` lines.
|
|
71
|
+
if (line.startsWith(':')) continue;
|
|
72
|
+
const colon = line.indexOf(':');
|
|
73
|
+
const field = colon === -1 ? line : line.substring(0, colon);
|
|
74
|
+
let value = colon === -1 ? '' : line.substring(colon + 1);
|
|
75
|
+
if (value.startsWith(' ')) value = value.substring(1);
|
|
76
|
+
switch (field) {
|
|
77
|
+
case 'event':
|
|
78
|
+
frame.event = value;
|
|
79
|
+
break;
|
|
80
|
+
case 'data':
|
|
81
|
+
dataLines.push(value);
|
|
82
|
+
break;
|
|
83
|
+
case 'id':
|
|
84
|
+
frame.id = value;
|
|
85
|
+
break;
|
|
86
|
+
case 'retry': {
|
|
87
|
+
const ms = Number.parseInt(value, 10);
|
|
88
|
+
if (Number.isFinite(ms)) frame.retry = ms;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
default:
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (dataLines.length === 0 && frame.event === undefined && frame.id === undefined) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (dataLines.length > 0) frame.data = dataLines.join('\n');
|
|
99
|
+
|
|
100
|
+
if (frame.data === '[DONE]' || frame.data === '"[DONE]"') {
|
|
101
|
+
this.completed = true;
|
|
102
|
+
this.options.onComplete?.();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
this.options.onFrame(frame);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
this.options.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Tolerant JSON parser for SSE `data:` payloads — returns the raw string when
|
|
115
|
+
* the body isn't JSON, which deeppath relies on for bare-text content deltas.
|
|
116
|
+
*/
|
|
117
|
+
export function parseSSEData(data: string): unknown {
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(data);
|
|
120
|
+
} catch {
|
|
121
|
+
return data;
|
|
122
|
+
}
|
|
123
|
+
}
|