@runtypelabs/persona 3.9.2 → 3.10.1
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.cjs +45 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +148 -0
- package/dist/index.d.ts +148 -0
- package/dist/index.global.js +67 -64
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +45 -42
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +959 -214
- package/dist/theme-editor.d.cts +157 -3
- package/dist/theme-editor.d.ts +157 -3
- package/dist/theme-editor.js +955 -214
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +8 -0
- package/dist/theme-reference.d.ts +8 -0
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +154 -0
- package/package.json +1 -1
- package/src/client.test.ts +312 -1
- package/src/client.ts +247 -24
- package/src/components/messages.ts +1 -1
- package/src/components/reasoning-bubble.ts +117 -28
- package/src/components/tool-bubble.ts +161 -27
- package/src/defaults.ts +12 -0
- package/src/styles/widget.css +154 -0
- package/src/theme-editor/index.ts +5 -0
- package/src/theme-editor/preview-utils.test.ts +58 -0
- package/src/theme-editor/preview-utils.ts +220 -4
- package/src/theme-editor/sections.test.ts +20 -0
- package/src/theme-editor/sections.ts +10 -0
- package/src/theme-reference.ts +8 -3
- package/src/tool-call-display-defaults.test.ts +23 -0
- package/src/types.ts +155 -0
- package/src/ui.attachments-drop.test.ts +188 -0
- package/src/ui.scroll.test.ts +150 -0
- package/src/ui.tool-display.test.ts +204 -0
- package/src/ui.ts +275 -7
- package/src/utils/message-fingerprint.test.ts +17 -0
- package/src/utils/message-fingerprint.ts +13 -1
package/src/client.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
-
import { AgentWidgetClient } from './client';
|
|
2
|
+
import { AgentWidgetClient, preferFinalStructuredContent } from './client';
|
|
3
3
|
import { AgentWidgetEvent, AgentWidgetMessage } from './types';
|
|
4
4
|
import { createJsonStreamParser } from './utils/formatting';
|
|
5
5
|
|
|
@@ -1838,5 +1838,316 @@ describe('AgentWidgetClient - partId Text/Tool Interleaving', () => {
|
|
|
1838
1838
|
// Tool message exists
|
|
1839
1839
|
expect(toolMsgs.length).toBe(1);
|
|
1840
1840
|
});
|
|
1841
|
+
|
|
1842
|
+
async function runSealedSegmentReconciliationTest(opts: {
|
|
1843
|
+
parserMatchContent: string;
|
|
1844
|
+
stepCompleteResponse: string;
|
|
1845
|
+
expectedRawContent: string;
|
|
1846
|
+
}) {
|
|
1847
|
+
vi.useFakeTimers();
|
|
1848
|
+
const events: AgentWidgetEvent[] = [];
|
|
1849
|
+
|
|
1850
|
+
const delayedJsonParser = () => {
|
|
1851
|
+
let extractedText: string | null = null;
|
|
1852
|
+
return {
|
|
1853
|
+
processChunk: (accumulatedContent: string) =>
|
|
1854
|
+
new Promise<{ text: string; raw: string } | null>((resolve) => {
|
|
1855
|
+
setTimeout(() => {
|
|
1856
|
+
if (accumulatedContent === opts.parserMatchContent) {
|
|
1857
|
+
extractedText = 'Tool returned a result.';
|
|
1858
|
+
resolve({ text: extractedText, raw: accumulatedContent });
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
resolve(null);
|
|
1862
|
+
}, 0);
|
|
1863
|
+
}),
|
|
1864
|
+
getExtractedText: () => extractedText,
|
|
1865
|
+
close: async () => {}
|
|
1866
|
+
};
|
|
1867
|
+
};
|
|
1868
|
+
|
|
1869
|
+
const encoder = new TextEncoder();
|
|
1870
|
+
global.fetch = vi.fn().mockImplementation(async () => {
|
|
1871
|
+
const stream = new ReadableStream({
|
|
1872
|
+
start(controller) {
|
|
1873
|
+
const e = (eventType: string, data: Record<string, unknown>) =>
|
|
1874
|
+
controller.enqueue(encoder.encode(`event: ${eventType}\ndata: ${JSON.stringify({ type: eventType, ...data })}\n\n`));
|
|
1875
|
+
|
|
1876
|
+
e('flow_start', { flowId: 'f1', flowName: 'Test', totalSteps: 1 });
|
|
1877
|
+
e('tool_start', { toolId: 'tc_1', name: 'test_tool', toolType: 'custom', startedAt: new Date().toISOString() });
|
|
1878
|
+
e('tool_complete', { toolId: 'tc_1', name: 'test_tool', success: true, completedAt: new Date().toISOString(), executionTime: 0 });
|
|
1879
|
+
e('text_start', { partId: 'text_1', messageId: 'msg_s1', seq: 1 });
|
|
1880
|
+
e('step_delta', {
|
|
1881
|
+
id: 's1',
|
|
1882
|
+
text: '{"text":"Tool returned a re',
|
|
1883
|
+
partId: 'text_1',
|
|
1884
|
+
messageId: 'msg_s1',
|
|
1885
|
+
seq: 2
|
|
1886
|
+
});
|
|
1887
|
+
e('text_end', { partId: 'text_1', messageId: 'msg_s1', seq: 3 });
|
|
1888
|
+
e('step_complete', {
|
|
1889
|
+
id: 's1',
|
|
1890
|
+
name: 'Response',
|
|
1891
|
+
stepType: 'prompt',
|
|
1892
|
+
success: true,
|
|
1893
|
+
result: { response: opts.stepCompleteResponse },
|
|
1894
|
+
executionTime: 500
|
|
1895
|
+
});
|
|
1896
|
+
e('flow_complete', { success: true });
|
|
1897
|
+
controller.close();
|
|
1898
|
+
}
|
|
1899
|
+
});
|
|
1900
|
+
return { ok: true, body: stream };
|
|
1901
|
+
});
|
|
1902
|
+
|
|
1903
|
+
const client = new AgentWidgetClient({
|
|
1904
|
+
apiUrl: 'http://localhost:8000',
|
|
1905
|
+
streamParser: delayedJsonParser
|
|
1906
|
+
});
|
|
1907
|
+
await client.dispatch(
|
|
1908
|
+
{ messages: [{ id: 'usr_1', role: 'user', content: 'Call tool', createdAt: new Date().toISOString() }] },
|
|
1909
|
+
(event) => events.push(event)
|
|
1910
|
+
);
|
|
1911
|
+
await vi.runAllTimersAsync();
|
|
1912
|
+
vi.useRealTimers();
|
|
1913
|
+
|
|
1914
|
+
const messageEvents = events.filter(e => e.type === 'message');
|
|
1915
|
+
const messagesById = new Map<string, AgentWidgetMessage>();
|
|
1916
|
+
for (const event of messageEvents) {
|
|
1917
|
+
if (event.type === 'message') messagesById.set(event.message.id, event.message);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
const allMessages = Array.from(messagesById.values());
|
|
1921
|
+
const assistantTexts = allMessages
|
|
1922
|
+
.filter(m => m.role === 'assistant' && !m.variant)
|
|
1923
|
+
.sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0));
|
|
1924
|
+
const toolMsgs = allMessages.filter(m => m.variant === 'tool');
|
|
1925
|
+
|
|
1926
|
+
expect(assistantTexts.length).toBe(1);
|
|
1927
|
+
expect(assistantTexts[0].content).toBe('Tool returned a result.');
|
|
1928
|
+
expect(assistantTexts[0].rawContent).toBe(opts.expectedRawContent);
|
|
1929
|
+
expect(assistantTexts[0].streaming).toBe(false);
|
|
1930
|
+
expect(toolMsgs.length).toBe(1);
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
it('should reconcile a sealed text segment with async parser output from step_complete', async () => {
|
|
1934
|
+
await runSealedSegmentReconciliationTest({
|
|
1935
|
+
parserMatchContent: '{"text":"Tool returned a result."',
|
|
1936
|
+
stepCompleteResponse: 'Tool returned a result.',
|
|
1937
|
+
expectedRawContent: '{"text":"Tool returned a re',
|
|
1938
|
+
});
|
|
1939
|
+
});
|
|
1940
|
+
|
|
1941
|
+
it('should prefer the authoritative final structured response when reconciling a sealed segment', async () => {
|
|
1942
|
+
await runSealedSegmentReconciliationTest({
|
|
1943
|
+
parserMatchContent: '{"text":"Tool returned a result."}',
|
|
1944
|
+
stepCompleteResponse: '{"text":"Tool returned a result."}',
|
|
1945
|
+
expectedRawContent: '{"text":"Tool returned a result."}',
|
|
1946
|
+
});
|
|
1947
|
+
});
|
|
1948
|
+
});
|
|
1949
|
+
|
|
1950
|
+
describe('preferFinalStructuredContent', () => {
|
|
1951
|
+
it('returns finalString when rawBuffer is undefined', () => {
|
|
1952
|
+
expect(preferFinalStructuredContent(undefined, 'hello')).toBe('hello');
|
|
1953
|
+
});
|
|
1954
|
+
|
|
1955
|
+
it('returns finalString when rawBuffer is empty/whitespace', () => {
|
|
1956
|
+
expect(preferFinalStructuredContent('', 'hello')).toBe('hello');
|
|
1957
|
+
expect(preferFinalStructuredContent(' ', 'hello')).toBe('hello');
|
|
1958
|
+
});
|
|
1959
|
+
|
|
1960
|
+
it('returns rawBuffer when finalString is empty/whitespace', () => {
|
|
1961
|
+
expect(preferFinalStructuredContent('{"text":"hi"}', '')).toBe('{"text":"hi"}');
|
|
1962
|
+
expect(preferFinalStructuredContent('{"text":"hi"}', ' ')).toBe('{"text":"hi"}');
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
it('returns rawBuffer when final is plain text (not structured)', () => {
|
|
1966
|
+
expect(preferFinalStructuredContent('{"text":"hi"}', 'plain text')).toBe('{"text":"hi"}');
|
|
1967
|
+
});
|
|
1968
|
+
|
|
1969
|
+
it('returns finalString when raw is plain text but final is structured', () => {
|
|
1970
|
+
expect(preferFinalStructuredContent('partial plain', '{"text":"hi"}')).toBe('{"text":"hi"}');
|
|
1971
|
+
});
|
|
1972
|
+
|
|
1973
|
+
it('returns finalString when both are identical structured content', () => {
|
|
1974
|
+
const json = '{"text":"hello"}';
|
|
1975
|
+
expect(preferFinalStructuredContent(json, json)).toBe(json);
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
it('returns finalString when final is a superset of the raw buffer', () => {
|
|
1979
|
+
expect(preferFinalStructuredContent(
|
|
1980
|
+
'{"text":"hel',
|
|
1981
|
+
'{"text":"hello"}'
|
|
1982
|
+
)).toBe('{"text":"hello"}');
|
|
1983
|
+
});
|
|
1984
|
+
|
|
1985
|
+
it('returns finalString when final is parseable JSON but raw is not', () => {
|
|
1986
|
+
expect(preferFinalStructuredContent(
|
|
1987
|
+
'{"text":"hel',
|
|
1988
|
+
'{"text":"hello"}'
|
|
1989
|
+
)).toBe('{"text":"hello"}');
|
|
1990
|
+
});
|
|
1991
|
+
|
|
1992
|
+
it('returns rawBuffer when both are structured but neither is a prefix of the other and both parse', () => {
|
|
1993
|
+
expect(preferFinalStructuredContent(
|
|
1994
|
+
'{"text":"segment two"}',
|
|
1995
|
+
'{"text":"full response with segment one and two"}'
|
|
1996
|
+
)).toBe('{"text":"segment two"}');
|
|
1997
|
+
});
|
|
1998
|
+
|
|
1999
|
+
it('returns rawBuffer when both are structured, different, and raw parses', () => {
|
|
2000
|
+
expect(preferFinalStructuredContent(
|
|
2001
|
+
'{"text":"short"}',
|
|
2002
|
+
'{"text":"different content"}'
|
|
2003
|
+
)).toBe('{"text":"short"}');
|
|
2004
|
+
});
|
|
2005
|
+
|
|
2006
|
+
it('returns finalString when final parses but raw does not (partial JSON)', () => {
|
|
2007
|
+
expect(preferFinalStructuredContent(
|
|
2008
|
+
'{"text":"incomp',
|
|
2009
|
+
'{"text":"complete"}'
|
|
2010
|
+
)).toBe('{"text":"complete"}');
|
|
2011
|
+
});
|
|
2012
|
+
|
|
2013
|
+
it('handles XML-shaped content as structured', () => {
|
|
2014
|
+
expect(preferFinalStructuredContent(
|
|
2015
|
+
'<response>partial',
|
|
2016
|
+
'<response>full</response>'
|
|
2017
|
+
)).toBe('<response>partial');
|
|
2018
|
+
});
|
|
2019
|
+
|
|
2020
|
+
it('handles array-shaped JSON content as structured', () => {
|
|
2021
|
+
expect(preferFinalStructuredContent(
|
|
2022
|
+
'[{"text":"par',
|
|
2023
|
+
'[{"text":"partial"}]'
|
|
2024
|
+
)).toBe('[{"text":"partial"}]');
|
|
2025
|
+
});
|
|
2026
|
+
});
|
|
2027
|
+
|
|
2028
|
+
describe('AgentWidgetClient - Out-of-Order Sequence Reordering', () => {
|
|
2029
|
+
it('should reorder step_delta chunks by seq when events arrive out of order', async () => {
|
|
2030
|
+
const events: AgentWidgetEvent[] = [];
|
|
2031
|
+
|
|
2032
|
+
const client = new AgentWidgetClient({ apiUrl: 'http://localhost:8000' });
|
|
2033
|
+
|
|
2034
|
+
global.fetch = vi.fn().mockImplementation(async () => {
|
|
2035
|
+
const encoder = new TextEncoder();
|
|
2036
|
+
const stream = new ReadableStream({
|
|
2037
|
+
start(controller) {
|
|
2038
|
+
const e = (data: any) => {
|
|
2039
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
|
|
2040
|
+
};
|
|
2041
|
+
e({ type: 'flow_start', flowId: 'f1', flowName: 'Test', totalSteps: 1 });
|
|
2042
|
+
e({ type: 'step_start', id: 's1', name: 'Prompt', stepType: 'prompt', index: 1, totalSteps: 1 });
|
|
2043
|
+
// Send chunks out of order (seq 3 before seq 2)
|
|
2044
|
+
e({ type: 'step_delta', id: 's1', text: 'Hello', partId: 'text_0', seq: 1 });
|
|
2045
|
+
e({ type: 'step_delta', id: 's1', text: ' world', partId: 'text_0', seq: 3 });
|
|
2046
|
+
e({ type: 'step_delta', id: 's1', text: ' beautiful', partId: 'text_0', seq: 2 });
|
|
2047
|
+
e({ type: 'step_delta', id: 's1', text: '!', partId: 'text_0', seq: 4 });
|
|
2048
|
+
e({ type: 'step_complete', id: 's1', name: 'Prompt', success: true });
|
|
2049
|
+
e({ type: 'flow_complete', success: true });
|
|
2050
|
+
controller.close();
|
|
2051
|
+
},
|
|
2052
|
+
});
|
|
2053
|
+
return { ok: true, body: stream };
|
|
2054
|
+
});
|
|
2055
|
+
|
|
2056
|
+
await client.dispatch({ messages: [] }, (event) => events.push(event));
|
|
2057
|
+
|
|
2058
|
+
const messageEvents = events.filter(
|
|
2059
|
+
(e): e is AgentWidgetEvent & { type: 'message' } => e.type === 'message'
|
|
2060
|
+
);
|
|
2061
|
+
const finalMessages = messageEvents.filter((e) => !e.message.streaming);
|
|
2062
|
+
expect(finalMessages.length).toBeGreaterThan(0);
|
|
2063
|
+
|
|
2064
|
+
const lastFinal = finalMessages[finalMessages.length - 1];
|
|
2065
|
+
// Content should be in seq order, not arrival order
|
|
2066
|
+
expect(lastFinal.message.content).toBe('Hello beautiful world!');
|
|
2067
|
+
});
|
|
2068
|
+
|
|
2069
|
+
it('should reorder reason_delta chunks by sequenceIndex', async () => {
|
|
2070
|
+
const events: AgentWidgetEvent[] = [];
|
|
2071
|
+
|
|
2072
|
+
const client = new AgentWidgetClient({ apiUrl: 'http://localhost:8000' });
|
|
2073
|
+
|
|
2074
|
+
global.fetch = vi.fn().mockImplementation(async () => {
|
|
2075
|
+
const encoder = new TextEncoder();
|
|
2076
|
+
const stream = new ReadableStream({
|
|
2077
|
+
start(controller) {
|
|
2078
|
+
const e = (data: any) => {
|
|
2079
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
|
|
2080
|
+
};
|
|
2081
|
+
e({ type: 'flow_start', flowId: 'f1', flowName: 'Test', totalSteps: 1 });
|
|
2082
|
+
e({ type: 'step_start', id: 's1', name: 'Prompt', stepType: 'prompt', index: 1, totalSteps: 1 });
|
|
2083
|
+
e({ type: 'reason_start', reasoningId: 'r1', hidden: false, done: false });
|
|
2084
|
+
// Send reasoning chunks out of order
|
|
2085
|
+
e({ type: 'reason_delta', reasoningId: 'r1', reasoningText: 'I ', hidden: false, done: false, sequenceIndex: 1 });
|
|
2086
|
+
e({ type: 'reason_delta', reasoningId: 'r1', reasoningText: 'about', hidden: false, done: false, sequenceIndex: 3 });
|
|
2087
|
+
e({ type: 'reason_delta', reasoningId: 'r1', reasoningText: 'think ', hidden: false, done: false, sequenceIndex: 2 });
|
|
2088
|
+
e({ type: 'reason_delta', reasoningId: 'r1', reasoningText: ' this.', hidden: false, done: false, sequenceIndex: 4 });
|
|
2089
|
+
e({ type: 'reason_complete', reasoningId: 'r1', hidden: false, done: true });
|
|
2090
|
+
e({ type: 'step_delta', id: 's1', text: 'Result', partId: 'text_0' });
|
|
2091
|
+
e({ type: 'step_complete', id: 's1', name: 'Prompt', success: true });
|
|
2092
|
+
e({ type: 'flow_complete', success: true });
|
|
2093
|
+
controller.close();
|
|
2094
|
+
},
|
|
2095
|
+
});
|
|
2096
|
+
return { ok: true, body: stream };
|
|
2097
|
+
});
|
|
2098
|
+
|
|
2099
|
+
await client.dispatch({ messages: [] }, (event) => events.push(event));
|
|
2100
|
+
|
|
2101
|
+
const messageEvents = events.filter(
|
|
2102
|
+
(e): e is AgentWidgetEvent & { type: 'message' } => e.type === 'message'
|
|
2103
|
+
);
|
|
2104
|
+
const reasoningMsgs = messageEvents.filter(
|
|
2105
|
+
(e) => e.message.reasoning && e.message.reasoning.chunks.length > 0
|
|
2106
|
+
);
|
|
2107
|
+
expect(reasoningMsgs.length).toBeGreaterThan(0);
|
|
2108
|
+
|
|
2109
|
+
const lastReasoning = reasoningMsgs[reasoningMsgs.length - 1];
|
|
2110
|
+
// Reasoning chunks should be in sequenceIndex order
|
|
2111
|
+
const fullReasoning = lastReasoning.message.reasoning!.chunks.join('');
|
|
2112
|
+
expect(fullReasoning).toBe('I think about this.');
|
|
2113
|
+
});
|
|
2114
|
+
|
|
2115
|
+
it('should handle step_delta without seq gracefully (no reordering)', async () => {
|
|
2116
|
+
const events: AgentWidgetEvent[] = [];
|
|
2117
|
+
|
|
2118
|
+
const client = new AgentWidgetClient({ apiUrl: 'http://localhost:8000' });
|
|
2119
|
+
|
|
2120
|
+
global.fetch = vi.fn().mockImplementation(async () => {
|
|
2121
|
+
const encoder = new TextEncoder();
|
|
2122
|
+
const stream = new ReadableStream({
|
|
2123
|
+
start(controller) {
|
|
2124
|
+
const e = (data: any) => {
|
|
2125
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
|
|
2126
|
+
};
|
|
2127
|
+
e({ type: 'flow_start', flowId: 'f1', flowName: 'Test', totalSteps: 1 });
|
|
2128
|
+
e({ type: 'step_start', id: 's1', name: 'Prompt', stepType: 'prompt', index: 1, totalSteps: 1 });
|
|
2129
|
+
// No seq field — should append in arrival order
|
|
2130
|
+
e({ type: 'step_delta', id: 's1', text: 'Hello ' });
|
|
2131
|
+
e({ type: 'step_delta', id: 's1', text: 'world' });
|
|
2132
|
+
e({ type: 'step_delta', id: 's1', text: '!' });
|
|
2133
|
+
e({ type: 'step_complete', id: 's1', name: 'Prompt', success: true });
|
|
2134
|
+
e({ type: 'flow_complete', success: true });
|
|
2135
|
+
controller.close();
|
|
2136
|
+
},
|
|
2137
|
+
});
|
|
2138
|
+
return { ok: true, body: stream };
|
|
2139
|
+
});
|
|
2140
|
+
|
|
2141
|
+
await client.dispatch({ messages: [] }, (event) => events.push(event));
|
|
2142
|
+
|
|
2143
|
+
const messageEvents = events.filter(
|
|
2144
|
+
(e): e is AgentWidgetEvent & { type: 'message' } => e.type === 'message'
|
|
2145
|
+
);
|
|
2146
|
+
const finalMessages = messageEvents.filter((e) => !e.message.streaming);
|
|
2147
|
+
expect(finalMessages.length).toBeGreaterThan(0);
|
|
2148
|
+
|
|
2149
|
+
const lastFinal = finalMessages[finalMessages.length - 1];
|
|
2150
|
+
expect(lastFinal.message.content).toBe('Hello world!');
|
|
2151
|
+
});
|
|
1841
2152
|
});
|
|
1842
2153
|
|