@runtypelabs/persona 3.25.0 → 3.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "3.25.0",
3
+ "version": "3.27.0",
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",
@@ -99,7 +99,7 @@
99
99
  "access": "public"
100
100
  },
101
101
  "scripts": {
102
- "build": "rimraf dist && pnpm run build:styles && pnpm run build:client && pnpm run build:installer && pnpm run build:theme-ref && pnpm run build:theme-editor && pnpm run build:testing && pnpm run build:smart-dom-reader && pnpm run build:animations",
102
+ "build": "rimraf dist && pnpm run build:styles && pnpm run build:client && pnpm run build:installer && pnpm run build:launcher && pnpm run build:theme-ref && pnpm run build:theme-editor && pnpm run build:testing && pnpm run build:smart-dom-reader && pnpm run build:animations",
103
103
  "build:theme-editor": "tsup src/theme-editor.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
104
104
  "build:testing": "tsup src/testing.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
105
105
  "build:smart-dom-reader": "tsup src/smart-dom-reader.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
@@ -108,11 +108,15 @@
108
108
  "build:styles": "node -e \"const fs=require('fs');fs.mkdirSync('dist',{recursive:true});fs.copyFileSync('src/styles/widget.css','dist/widget.css');\"",
109
109
  "build:client": "tsup src/index.ts --format esm,cjs --minify --sourcemap --splitting false --dts --loader \".css=text\" && tsup src/index-global.ts --format iife --global-name AgentWidget --minify --sourcemap --splitting false --out-dir dist --loader \".css=text\" && node -e \"const fs=require('fs');for(const ext of ['.global.js','.global.js.map']){const from='dist/index-global'+ext;if(fs.existsSync(from))fs.renameSync(from,'dist/index'+ext);}\"",
110
110
  "build:installer": "tsup src/install.ts --format iife --global-name SiteAgentInstaller --out-dir dist --minify --sourcemap --no-splitting",
111
+ "build:launcher": "tsup src/launcher-global.ts --format iife --global-name AgentWidgetLauncher --minify --sourcemap --splitting false --out-dir dist && node -e \"const fs=require('fs');for(const ext of ['.global.js','.global.js.map']){const from='dist/launcher-global'+ext;if(fs.existsSync(from))fs.renameSync(from,'dist/launcher'+ext);}\"",
111
112
  "lint": "eslint . --ext .ts",
112
- "typecheck": "tsc --noEmit",
113
+ "typecheck": "pnpm run check:runtype-types && tsc --noEmit",
113
114
  "test": "vitest",
114
115
  "test:ui": "vitest --ui",
115
116
  "test:run": "vitest run",
116
- "size": "size-limit"
117
+ "size": "size-limit",
118
+ "fetch:runtype-openapi": "node scripts/fetch-runtype-openapi.mjs",
119
+ "generate:runtype-types": "pnpm run fetch:runtype-openapi && node scripts/generate-runtype-openapi-types.mjs",
120
+ "check:runtype-types": "pnpm run fetch:runtype-openapi && node scripts/generate-runtype-openapi-types.mjs --check"
117
121
  }
118
122
  }
@@ -1633,6 +1633,55 @@ describe('AgentWidgetClient - partId Text/Tool Interleaving', () => {
1633
1633
  expect(seqTool).toBeLessThan(seq1);
1634
1634
  });
1635
1635
 
1636
+ it('should not emit a whitespace-only assistant bubble before a leading tool call', async () => {
1637
+ const events: AgentWidgetEvent[] = [];
1638
+
1639
+ const encoder = new TextEncoder();
1640
+ global.fetch = vi.fn().mockImplementation(async () => {
1641
+ const stream = new ReadableStream({
1642
+ start(controller) {
1643
+ const e = (eventType: string, data: Record<string, unknown>) =>
1644
+ controller.enqueue(encoder.encode(`event: ${eventType}\ndata: ${JSON.stringify({ type: eventType, ...data })}\n\n`));
1645
+
1646
+ e('flow_start', { flowId: 'f1', flowName: 'Test', totalSteps: 1 });
1647
+ e('step_start', { id: 's1', name: 'Prompt', stepType: 'prompt', index: 1, totalSteps: 1 });
1648
+ // Tool UI is the first meaningful output. Some providers still emit
1649
+ // newline-only text lifecycle events around the tool boundary; those
1650
+ // must not become an empty assistant message bubble.
1651
+ e('tool_start', { toolId: 'tc_1', name: 'add_to_cart', toolType: 'local', sequenceIndex: 4 });
1652
+ e('text_start', { partId: 'text_0', messageId: 'msg_s1', seq: 1 });
1653
+ e('step_delta', { id: 's1', text: '\n', partId: 'text_0', messageId: 'msg_s1', seq: 2 });
1654
+ e('text_end', { partId: 'text_0', messageId: 'msg_s1', seq: 3 });
1655
+ e('tool_complete', { toolId: 'tc_1', name: 'add_to_cart', success: true, completedAt: new Date().toISOString(), executionTime: 20, sequenceIndex: 5 });
1656
+ e('flow_complete', { success: true });
1657
+ controller.close();
1658
+ }
1659
+ });
1660
+ return { ok: true, body: stream };
1661
+ });
1662
+
1663
+ const client = new AgentWidgetClient({ apiUrl: 'http://localhost:8000' });
1664
+ await client.dispatch(
1665
+ { messages: [{ id: 'usr_1', role: 'user', content: 'Add to cart', createdAt: new Date().toISOString() }] },
1666
+ (event) => events.push(event)
1667
+ );
1668
+
1669
+ const messageEvents = events.filter(e => e.type === 'message');
1670
+ const messagesById = new Map<string, AgentWidgetMessage>();
1671
+ for (const event of messageEvents) {
1672
+ if (event.type === 'message') messagesById.set(event.message.id, event.message);
1673
+ }
1674
+
1675
+ const allMessages = Array.from(messagesById.values());
1676
+ const assistantTexts = allMessages.filter(m => m.role === 'assistant' && !m.variant);
1677
+ const toolMsgs = allMessages.filter(m => m.variant === 'tool');
1678
+
1679
+ expect(assistantTexts).toHaveLength(0);
1680
+ expect(toolMsgs).toHaveLength(1);
1681
+ expect(toolMsgs[0].toolCall?.name).toBe('add_to_cart');
1682
+ expect(toolMsgs[0].toolCall?.status).toBe('complete');
1683
+ });
1684
+
1636
1685
  it('should not split when partId is absent (backward compatible)', async () => {
1637
1686
  const events: AgentWidgetEvent[] = [];
1638
1687
 
@@ -3097,7 +3146,7 @@ describe('AgentWidgetClient.resumeFlow', () => {
3097
3146
 
3098
3147
  const client = new AgentWidgetClient({
3099
3148
  clientToken: 'ct_live_demo',
3100
- apiUrl: 'https://api.runtype-staging.com',
3149
+ apiUrl: 'https://api.runtype.com',
3101
3150
  });
3102
3151
  // Simulate an initialized client session (resumeFlow reads sessionId off it).
3103
3152
  (client as unknown as { clientSession: { sessionId: string; expiresAt: Date } }).clientSession = {
@@ -3108,7 +3157,7 @@ describe('AgentWidgetClient.resumeFlow', () => {
3108
3157
  await client.resumeFlow('exec_xyz', { toolu_A: { ok: true } });
3109
3158
 
3110
3159
  // Session-authed sibling of /v1/client/chat — no Bearer key, sessionId in body.
3111
- expect(capturedUrl).toBe('https://api.runtype-staging.com/v1/client/resume');
3160
+ expect(capturedUrl).toBe('https://api.runtype.com/v1/client/resume');
3112
3161
  expect(capturedBody).toEqual({
3113
3162
  executionId: 'exec_xyz',
3114
3163
  toolOutputs: { toolu_A: { ok: true } },
@@ -3127,7 +3176,7 @@ describe('AgentWidgetClient.resumeFlow', () => {
3127
3176
 
3128
3177
  const client = new AgentWidgetClient({
3129
3178
  clientToken: 'ct_live_demo',
3130
- apiUrl: 'https://api.runtype-staging.com/v1/dispatch',
3179
+ apiUrl: 'https://api.runtype.com/v1/dispatch',
3131
3180
  });
3132
3181
  // A live session so initSession() short-circuits instead of fetching /init.
3133
3182
  (client as unknown as { clientSession: { sessionId: string; expiresAt: Date } }).clientSession = {
@@ -3136,7 +3185,7 @@ describe('AgentWidgetClient.resumeFlow', () => {
3136
3185
  };
3137
3186
  await client.resumeFlow('exec_abc', {});
3138
3187
 
3139
- expect(capturedUrl).toBe('https://api.runtype-staging.com/v1/client/resume');
3188
+ expect(capturedUrl).toBe('https://api.runtype.com/v1/client/resume');
3140
3189
  });
3141
3190
 
3142
3191
  it('refreshes the session via initSession() before resuming when stale (BugBot r3367875360)', async () => {
@@ -3148,7 +3197,7 @@ describe('AgentWidgetClient.resumeFlow', () => {
3148
3197
 
3149
3198
  const client = new AgentWidgetClient({
3150
3199
  clientToken: 'ct_live_demo',
3151
- apiUrl: 'https://api.runtype-staging.com',
3200
+ apiUrl: 'https://api.runtype.com',
3152
3201
  });
3153
3202
  // A stale session that has already expired — a long WebMCP approval wait can
3154
3203
  // outlive it, so resumeFlow must not trust this.clientSession directly.
@@ -3797,7 +3846,7 @@ describe('AgentWidgetClient - diff-only clientTools (client-token)', () => {
3797
3846
  function makeClient(tools: unknown[]) {
3798
3847
  const client = new AgentWidgetClient({
3799
3848
  clientToken: 'ct_live_demo',
3800
- apiUrl: 'https://api.runtype-staging.com',
3849
+ apiUrl: 'https://api.runtype.com',
3801
3850
  });
3802
3851
  (client as unknown as { clientSession: { sessionId: string; expiresAt: Date } }).clientSession = {
3803
3852
  sessionId: 'cs_diff_1',
@@ -3855,7 +3904,7 @@ describe('AgentWidgetClient - diff-only clientTools (client-token)', () => {
3855
3904
  const live = [...TOOLS];
3856
3905
  const client = new AgentWidgetClient({
3857
3906
  clientToken: 'ct_live_demo',
3858
- apiUrl: 'https://api.runtype-staging.com',
3907
+ apiUrl: 'https://api.runtype.com',
3859
3908
  });
3860
3909
  (client as unknown as { clientSession: { sessionId: string; expiresAt: Date } }).clientSession = {
3861
3910
  sessionId: 'cs_diff_1',
@@ -3882,7 +3931,7 @@ describe('AgentWidgetClient - diff-only clientTools (client-token)', () => {
3882
3931
  ];
3883
3932
  const client = new AgentWidgetClient({
3884
3933
  clientToken: 'ct_live_demo',
3885
- apiUrl: 'https://api.runtype-staging.com',
3934
+ apiUrl: 'https://api.runtype.com',
3886
3935
  });
3887
3936
  (client as unknown as { clientSession: { sessionId: string; expiresAt: Date } }).clientSession = {
3888
3937
  sessionId: 'cs_diff_1',
@@ -3978,7 +4027,7 @@ describe('AgentWidgetClient - diff-only clientTools (client-token)', () => {
3978
4027
  });
3979
4028
  const client = new AgentWidgetClient({
3980
4029
  clientToken: 'ct_live_demo',
3981
- apiUrl: 'https://api.runtype-staging.com',
4030
+ apiUrl: 'https://api.runtype.com',
3982
4031
  });
3983
4032
  // Pre-seed a stale fingerprint bound to a prior session (no live session, so
3984
4033
  // initSession() takes the mint path and fetches /client/init).
package/src/client.ts CHANGED
@@ -1297,7 +1297,25 @@ export class AgentWidgetClient {
1297
1297
  };
1298
1298
  };
1299
1299
 
1300
+ const shouldEmitMessage = (msg: AgentWidgetMessage): boolean => {
1301
+ if (msg.role !== "assistant" || msg.variant) return true;
1302
+
1303
+ const hasContentParts =
1304
+ Array.isArray(msg.contentParts) && msg.contentParts.length > 0;
1305
+ const hasRawContent =
1306
+ typeof msg.rawContent === "string" && msg.rawContent.trim() !== "";
1307
+ const hasVisibleText =
1308
+ typeof msg.content === "string" && msg.content.trim() !== "";
1309
+
1310
+ // Do not surface assistant text bubbles that only contain whitespace.
1311
+ // Some providers emit newline-only text parts around a leading tool call;
1312
+ // rendering those as normal messages creates an empty bubble above the
1313
+ // tool card. Keep media/component/stop-reason messages renderable.
1314
+ return hasVisibleText || hasContentParts || hasRawContent || Boolean(msg.stopReason);
1315
+ };
1316
+
1300
1317
  const emitMessage = (msg: AgentWidgetMessage) => {
1318
+ if (!shouldEmitMessage(msg)) return;
1301
1319
  onEvent({
1302
1320
  type: "message",
1303
1321
  message: cloneMessage(msg)