@runtypelabs/persona 4.2.0 → 4.2.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/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/index.cjs +42 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.global.js +33 -33
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +42 -42
- package/dist/index.js.map +1 -1
- package/dist/theme-editor-preview.cjs +31 -31
- package/dist/theme-editor-preview.js +31 -31
- package/package.json +1 -1
- package/src/client.test.ts +128 -0
- package/src/client.ts +25 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runtypelabs/persona",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.1",
|
|
4
4
|
"description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
package/src/client.test.ts
CHANGED
|
@@ -1784,6 +1784,134 @@ describe('AgentWidgetClient - partId Text/Tool Interleaving', () => {
|
|
|
1784
1784
|
});
|
|
1785
1785
|
});
|
|
1786
1786
|
|
|
1787
|
+
describe('AgentWidgetClient - flow continuation stream without execution_start', () => {
|
|
1788
|
+
// A tool-driven `/resume` continues a flow on a brand-new stream that does NOT
|
|
1789
|
+
// re-emit `execution_start` (`flow_start`). Each stream resolves its execution
|
|
1790
|
+
// kind independently and defaults to `"agent"`, so the continuation used to
|
|
1791
|
+
// mis-route the final prompt-step finalization: the streamed text block was
|
|
1792
|
+
// sealed via the agent path (which never records it as the sealed flow bubble),
|
|
1793
|
+
// then `step_complete.result.response` re-rendered the same text as a SECOND
|
|
1794
|
+
// bubble. The client now recovers the flow kind from the leading `step_*`
|
|
1795
|
+
// frame (a `stepType` is flow-only), so the finalization reconciles in place.
|
|
1796
|
+
function collectAssistantTexts(events: AgentWidgetEvent[]) {
|
|
1797
|
+
const messagesById = new Map<string, AgentWidgetMessage>();
|
|
1798
|
+
for (const event of events) {
|
|
1799
|
+
if (event.type === 'message') messagesById.set(event.message.id, event.message);
|
|
1800
|
+
}
|
|
1801
|
+
return Array.from(messagesById.values())
|
|
1802
|
+
.filter(m => m.role === 'assistant' && !m.variant)
|
|
1803
|
+
.sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0));
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
it('does not duplicate the final message when a flow resumes without execution_start', async () => {
|
|
1807
|
+
const events: AgentWidgetEvent[] = [];
|
|
1808
|
+
|
|
1809
|
+
// No `flow_start`/`execution_start`: this is exactly the wire a tool-driven
|
|
1810
|
+
// `/resume` continuation delivers.
|
|
1811
|
+
global.fetch = createAgentStreamFetch([
|
|
1812
|
+
sseEvent('step_start', { id: 's1', name: 'Prompt', stepType: 'prompt', index: 0, totalSteps: 1 }),
|
|
1813
|
+
sseEvent('text_start', { messageId: 'msg_s1' }),
|
|
1814
|
+
sseEvent('step_delta', { id: 's1', text: 'Done! Added to your cart.' }),
|
|
1815
|
+
sseEvent('text_end', { messageId: 'msg_s1' }),
|
|
1816
|
+
// The authoritative final response mirrors the streamed text.
|
|
1817
|
+
sseEvent('step_complete', { id: 's1', name: 'Prompt', stepType: 'prompt', success: true, result: { response: 'Done! Added to your cart.' }, executionTime: 500 }),
|
|
1818
|
+
sseEvent('flow_complete', { success: true }),
|
|
1819
|
+
]);
|
|
1820
|
+
|
|
1821
|
+
const client = new AgentWidgetClient({ apiUrl: 'http://localhost:8000' });
|
|
1822
|
+
await client.dispatch(
|
|
1823
|
+
{ messages: [{ id: 'usr_1', role: 'user', content: 'add it to my cart', createdAt: new Date().toISOString() }] },
|
|
1824
|
+
(event) => events.push(event)
|
|
1825
|
+
);
|
|
1826
|
+
|
|
1827
|
+
const assistantTexts = collectAssistantTexts(events);
|
|
1828
|
+
|
|
1829
|
+
// Exactly ONE assistant bubble — not duplicated by the step_complete finalization.
|
|
1830
|
+
expect(assistantTexts.length).toBe(1);
|
|
1831
|
+
expect(assistantTexts[0].content).toBe('Done! Added to your cart.');
|
|
1832
|
+
expect(assistantTexts[0].streaming).toBe(false);
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
it('still reconciles in place when execution_start IS present (initial flow stream)', async () => {
|
|
1836
|
+
const events: AgentWidgetEvent[] = [];
|
|
1837
|
+
|
|
1838
|
+
// Same shape but WITH flow_start — the pre-existing, already-correct path.
|
|
1839
|
+
global.fetch = createAgentStreamFetch([
|
|
1840
|
+
sseEvent('flow_start', { flowId: 'f1', flowName: 'Test', totalSteps: 1, executionId: 'exec_f1' }),
|
|
1841
|
+
sseEvent('step_start', { id: 's1', name: 'Prompt', stepType: 'prompt', index: 0, totalSteps: 1 }),
|
|
1842
|
+
sseEvent('text_start', { messageId: 'msg_s1' }),
|
|
1843
|
+
sseEvent('step_delta', { id: 's1', text: 'Done! Added to your cart.' }),
|
|
1844
|
+
sseEvent('text_end', { messageId: 'msg_s1' }),
|
|
1845
|
+
sseEvent('step_complete', { id: 's1', name: 'Prompt', stepType: 'prompt', success: true, result: { response: 'Done! Added to your cart.' }, executionTime: 500 }),
|
|
1846
|
+
sseEvent('flow_complete', { success: true }),
|
|
1847
|
+
]);
|
|
1848
|
+
|
|
1849
|
+
const client = new AgentWidgetClient({ apiUrl: 'http://localhost:8000' });
|
|
1850
|
+
await client.dispatch(
|
|
1851
|
+
{ messages: [{ id: 'usr_1', role: 'user', content: 'add it to my cart', createdAt: new Date().toISOString() }] },
|
|
1852
|
+
(event) => events.push(event)
|
|
1853
|
+
);
|
|
1854
|
+
|
|
1855
|
+
const assistantTexts = collectAssistantTexts(events);
|
|
1856
|
+
expect(assistantTexts.length).toBe(1);
|
|
1857
|
+
expect(assistantTexts[0].content).toBe('Done! Added to your cart.');
|
|
1858
|
+
expect(assistantTexts[0].streaming).toBe(false);
|
|
1859
|
+
});
|
|
1860
|
+
|
|
1861
|
+
it('does not misclassify an agent stream as a flow (no stepType present)', async () => {
|
|
1862
|
+
const events: AgentWidgetEvent[] = [];
|
|
1863
|
+
const execId = 'exec_a1';
|
|
1864
|
+
|
|
1865
|
+
// Agent loops use `agent_*`/turn_* frames and never carry a `stepType`, so the
|
|
1866
|
+
// flow recovery must not engage. A single streamed turn yields exactly one
|
|
1867
|
+
// bubble (regression guard so the recovery can't over-fire on agents).
|
|
1868
|
+
global.fetch = createAgentStreamFetch([
|
|
1869
|
+
sseEvent('agent_start', {
|
|
1870
|
+
executionId: execId, agentId: 'virtual', agentName: 'Test',
|
|
1871
|
+
maxTurns: 1, startedAt: new Date().toISOString(), seq: 1,
|
|
1872
|
+
}),
|
|
1873
|
+
sseEvent('agent_iteration_start', {
|
|
1874
|
+
executionId: execId, iteration: 1, maxTurns: 1,
|
|
1875
|
+
startedAt: new Date().toISOString(), seq: 2,
|
|
1876
|
+
}),
|
|
1877
|
+
sseEvent('agent_turn_start', {
|
|
1878
|
+
executionId: execId, iteration: 1, turnIndex: 0,
|
|
1879
|
+
role: 'assistant', turnId: 'turn_1', seq: 3,
|
|
1880
|
+
}),
|
|
1881
|
+
sseEvent('agent_turn_delta', {
|
|
1882
|
+
executionId: execId, iteration: 1, delta: 'Hi there!',
|
|
1883
|
+
contentType: 'text', turnId: 'turn_1', seq: 4,
|
|
1884
|
+
}),
|
|
1885
|
+
sseEvent('agent_turn_complete', {
|
|
1886
|
+
executionId: execId, iteration: 1, role: 'assistant',
|
|
1887
|
+
turnId: 'turn_1', completedAt: new Date().toISOString(), seq: 5,
|
|
1888
|
+
}),
|
|
1889
|
+
sseEvent('agent_iteration_complete', {
|
|
1890
|
+
executionId: execId, iteration: 1, toolCallsMade: 0,
|
|
1891
|
+
stopConditionMet: false, completedAt: new Date().toISOString(), seq: 6,
|
|
1892
|
+
}),
|
|
1893
|
+
sseEvent('agent_complete', {
|
|
1894
|
+
executionId: execId, agentId: 'virtual', success: true,
|
|
1895
|
+
iterations: 1, stopReason: 'max_iterations',
|
|
1896
|
+
completedAt: new Date().toISOString(), seq: 7,
|
|
1897
|
+
}),
|
|
1898
|
+
]);
|
|
1899
|
+
|
|
1900
|
+
const client = new AgentWidgetClient({
|
|
1901
|
+
apiUrl: 'http://localhost:8000',
|
|
1902
|
+
agent: { name: 'Test', model: 'openai:gpt-4o-mini', systemPrompt: 'test' },
|
|
1903
|
+
});
|
|
1904
|
+
await client.dispatch(
|
|
1905
|
+
{ messages: [{ id: 'usr_1', role: 'user', content: 'hi', createdAt: new Date().toISOString() }] },
|
|
1906
|
+
(event) => events.push(event)
|
|
1907
|
+
);
|
|
1908
|
+
|
|
1909
|
+
const assistantTexts = collectAssistantTexts(events);
|
|
1910
|
+
expect(assistantTexts.length).toBe(1);
|
|
1911
|
+
expect(assistantTexts[0].content).toBe('Hi there!');
|
|
1912
|
+
});
|
|
1913
|
+
});
|
|
1914
|
+
|
|
1787
1915
|
describe('AgentWidgetClient - nested flow-as-tool (parentToolCallId)', () => {
|
|
1788
1916
|
// PR #4602: a flow running as a tool enriches its streamed text/reasoning with
|
|
1789
1917
|
// toolContext.toolId; the unified wire surfaces it as text_start/reasoning_start
|
package/src/client.ts
CHANGED
|
@@ -1969,6 +1969,15 @@ export class AgentWidgetClient {
|
|
|
1969
1969
|
// Execution kind, resolved from the leading `execution_start` frame. Drives
|
|
1970
1970
|
// the agent-vs-flow branches that the single unified vocabulary collapses.
|
|
1971
1971
|
let executionKind: "agent" | "flow" = "agent";
|
|
1972
|
+
// Whether `executionKind` was set authoritatively by an `execution_start`
|
|
1973
|
+
// frame. Continuation streams (e.g. a tool-driven `/resume`) do NOT re-emit
|
|
1974
|
+
// `execution_start`, so a fresh `streamResponse` for the continuation starts
|
|
1975
|
+
// with the default `"agent"`. For a flow that mis-routes the final
|
|
1976
|
+
// prompt-step finalization and duplicates the last message (the streamed
|
|
1977
|
+
// text block is sealed, then `step_complete.result.response` re-renders it as
|
|
1978
|
+
// a second bubble). When `execution_start` is absent we recover the flow kind
|
|
1979
|
+
// from the first flow `step_*` frame below.
|
|
1980
|
+
let executionKindResolved = false;
|
|
1972
1981
|
// Open turn id (from `turn_start`). Unified text/reasoning deltas carry their
|
|
1973
1982
|
// own block id, not the turn id, so the turn id is threaded onto agentMetadata
|
|
1974
1983
|
// from here.
|
|
@@ -1987,6 +1996,21 @@ export class AgentWidgetClient {
|
|
|
1987
1996
|
const payloadType = seqReadyQueue[i].payloadType;
|
|
1988
1997
|
const payload = seqReadyQueue[i].payload;
|
|
1989
1998
|
|
|
1999
|
+
// Recover the execution kind on continuation streams that omit
|
|
2000
|
+
// `execution_start` (e.g. a tool-driven `/resume`). Flow `step_*` frames
|
|
2001
|
+
// carry a `stepType`; agent loops never do (they use `turn_*`). Without
|
|
2002
|
+
// this, the continuation defaults to `"agent"` and a flow's final
|
|
2003
|
+
// prompt-step finalization is duplicated. We only infer when no
|
|
2004
|
+
// `execution_start` resolved the kind, so an explicit `agent` is never
|
|
2005
|
+
// overridden.
|
|
2006
|
+
if (
|
|
2007
|
+
!executionKindResolved &&
|
|
2008
|
+
executionKind !== "flow" &&
|
|
2009
|
+
typeof (payload as { stepType?: unknown }).stepType === "string"
|
|
2010
|
+
) {
|
|
2011
|
+
executionKind = "flow";
|
|
2012
|
+
}
|
|
2013
|
+
|
|
1990
2014
|
if (payloadType === "reasoning_start") {
|
|
1991
2015
|
// Nested flow-as-tool thinking (PR #4602): route to the parent tool's row.
|
|
1992
2016
|
const rStartBlockId = typeof payload.id === "string" ? payload.id : null;
|
|
@@ -2543,6 +2567,7 @@ export class AgentWidgetClient {
|
|
|
2543
2567
|
// ================================================================
|
|
2544
2568
|
} else if (payloadType === "execution_start") {
|
|
2545
2569
|
executionKind = payload.kind === "flow" ? "flow" : "agent";
|
|
2570
|
+
executionKindResolved = true;
|
|
2546
2571
|
if (executionKind === "agent") {
|
|
2547
2572
|
agentExecution = {
|
|
2548
2573
|
executionId: payload.executionId,
|