@runtypelabs/persona 3.17.0 → 3.18.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.
Files changed (43) hide show
  1. package/README.md +142 -0
  2. package/dist/animations/glyph-cycle.d.cts +1 -1
  3. package/dist/animations/glyph-cycle.d.ts +1 -1
  4. package/dist/animations/{types-HPZY7oAI.d.cts → types-cwY5HaFD.d.cts} +25 -0
  5. package/dist/animations/{types-HPZY7oAI.d.ts → types-cwY5HaFD.d.ts} +25 -0
  6. package/dist/animations/wipe.d.cts +1 -1
  7. package/dist/animations/wipe.d.ts +1 -1
  8. package/dist/index.cjs +47 -47
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +300 -1
  11. package/dist/index.d.ts +300 -1
  12. package/dist/index.global.js +75 -75
  13. package/dist/index.global.js.map +1 -1
  14. package/dist/index.js +47 -47
  15. package/dist/index.js.map +1 -1
  16. package/dist/theme-editor.cjs +1432 -159
  17. package/dist/theme-editor.d.cts +218 -0
  18. package/dist/theme-editor.d.ts +218 -0
  19. package/dist/theme-editor.js +1432 -159
  20. package/dist/theme-reference.cjs +1 -1
  21. package/dist/theme-reference.d.cts +14 -0
  22. package/dist/theme-reference.d.ts +14 -0
  23. package/dist/widget.css +432 -0
  24. package/package.json +1 -1
  25. package/src/client.test.ts +134 -0
  26. package/src/client.ts +71 -0
  27. package/src/components/ask-user-question-bubble.test.ts +583 -0
  28. package/src/components/ask-user-question-bubble.ts +924 -0
  29. package/src/components/messages.ts +33 -1
  30. package/src/components/panel.ts +41 -4
  31. package/src/defaults.ts +21 -0
  32. package/src/index.ts +16 -1
  33. package/src/plugins/types.ts +57 -0
  34. package/src/session.test.ts +183 -0
  35. package/src/session.ts +242 -3
  36. package/src/styles/widget.css +432 -0
  37. package/src/types/theme.ts +15 -0
  38. package/src/types.ts +150 -0
  39. package/src/ui.ask-user-question-plugin.test.ts +649 -0
  40. package/src/ui.ts +631 -5
  41. package/src/utils/storage.ts +10 -2
  42. package/src/utils/theme.test.ts +36 -0
  43. package/src/utils/tokens.ts +23 -0
@@ -2717,3 +2717,137 @@ describe('AgentWidgetClient - stopReason propagation', () => {
2717
2717
  });
2718
2718
  });
2719
2719
 
2720
+ // ============================================================================
2721
+ // step_await (LOCAL tool pause) + resumeFlow
2722
+ // ============================================================================
2723
+
2724
+ describe('AgentWidgetClient — step_await parsing', () => {
2725
+ const buildStepAwaitStream = (payload: Record<string, unknown>): ReadableStream<Uint8Array> => {
2726
+ const encoder = new TextEncoder();
2727
+ const body = `event: step_await\ndata: ${JSON.stringify({ type: 'step_await', ...payload })}\n\n`;
2728
+ return new ReadableStream({
2729
+ start(controller) {
2730
+ controller.enqueue(encoder.encode(body));
2731
+ controller.close();
2732
+ },
2733
+ });
2734
+ };
2735
+
2736
+ it('emits a complete tool message with awaitingLocalTool=true for local_tool_required', async () => {
2737
+ global.fetch = vi.fn().mockResolvedValue({
2738
+ ok: true,
2739
+ body: buildStepAwaitStream({
2740
+ awaitReason: 'local_tool_required',
2741
+ id: 'step-1',
2742
+ name: 'Test Step',
2743
+ stepType: 'prompt',
2744
+ index: 0,
2745
+ toolId: 'runtime_ask_user_question_123',
2746
+ toolName: 'ask_user_question',
2747
+ executionId: 'exec_abc',
2748
+ parameters: {
2749
+ questions: [{ question: 'Who?', options: [{ label: 'A' }, { label: 'B' }] }],
2750
+ },
2751
+ }),
2752
+ });
2753
+
2754
+ const client = new AgentWidgetClient({ apiUrl: 'http://localhost:8000' });
2755
+ const events: AgentWidgetEvent[] = [];
2756
+ await client.dispatch(
2757
+ { messages: [{ id: 'u1', role: 'user', content: 'hi', createdAt: new Date().toISOString() }] },
2758
+ (e) => events.push(e)
2759
+ );
2760
+
2761
+ const toolMsg = events
2762
+ .filter((e) => e.type === 'message')
2763
+ .map((e) => (e as { message: AgentWidgetMessage }).message)
2764
+ .find((m) => m.variant === 'tool' && m.toolCall?.name === 'ask_user_question');
2765
+
2766
+ expect(toolMsg).toBeDefined();
2767
+ expect(toolMsg!.toolCall!.id).toBe('runtime_ask_user_question_123');
2768
+ expect(toolMsg!.toolCall!.status).toBe('complete');
2769
+ expect(toolMsg!.toolCall!.args).toMatchObject({
2770
+ questions: [{ question: 'Who?', options: [{ label: 'A' }, { label: 'B' }] }],
2771
+ });
2772
+ expect(toolMsg!.agentMetadata?.executionId).toBe('exec_abc');
2773
+ expect(toolMsg!.agentMetadata?.awaitingLocalTool).toBe(true);
2774
+ });
2775
+
2776
+ it('ignores step_await events whose awaitReason is not local_tool_required', async () => {
2777
+ global.fetch = vi.fn().mockResolvedValue({
2778
+ ok: true,
2779
+ body: buildStepAwaitStream({
2780
+ awaitReason: 'approval_required',
2781
+ toolId: 't1',
2782
+ toolName: 'some_tool',
2783
+ executionId: 'exec_abc',
2784
+ parameters: {},
2785
+ }),
2786
+ });
2787
+
2788
+ const client = new AgentWidgetClient({ apiUrl: 'http://localhost:8000' });
2789
+ const events: AgentWidgetEvent[] = [];
2790
+ await client.dispatch(
2791
+ { messages: [{ id: 'u1', role: 'user', content: 'hi', createdAt: new Date().toISOString() }] },
2792
+ (e) => events.push(e)
2793
+ );
2794
+
2795
+ const toolMsg = events
2796
+ .filter((e) => e.type === 'message')
2797
+ .map((e) => (e as { message: AgentWidgetMessage }).message)
2798
+ .find((m) => m.agentMetadata?.awaitingLocalTool);
2799
+ expect(toolMsg).toBeUndefined();
2800
+ });
2801
+ });
2802
+
2803
+ describe('AgentWidgetClient.resumeFlow', () => {
2804
+ it('POSTs to ${apiUrl}/resume with the expected body shape', async () => {
2805
+ let capturedUrl: string | undefined;
2806
+ let capturedBody: Record<string, unknown> | undefined;
2807
+ let capturedHeaders: Record<string, string> | undefined;
2808
+ global.fetch = vi.fn().mockImplementation(async (url: string, init: RequestInit) => {
2809
+ capturedUrl = url;
2810
+ capturedBody = JSON.parse(init.body as string);
2811
+ capturedHeaders = init.headers as Record<string, string>;
2812
+ return { ok: true, body: null };
2813
+ });
2814
+
2815
+ const client = new AgentWidgetClient({ apiUrl: 'https://api.runtype.com/v1/dispatch' });
2816
+ await client.resumeFlow('exec_xyz', { ["ask_user_question"]: 'Hobbyists' });
2817
+
2818
+ expect(capturedUrl).toBe('https://api.runtype.com/v1/dispatch/resume');
2819
+ expect(capturedBody).toEqual({
2820
+ executionId: 'exec_xyz',
2821
+ toolOutputs: { ["ask_user_question"]: 'Hobbyists' },
2822
+ streamResponse: true,
2823
+ });
2824
+ expect(capturedHeaders!['Content-Type']).toBe('application/json');
2825
+ });
2826
+
2827
+ it('honors a custom streamResponse option', async () => {
2828
+ let capturedBody: Record<string, unknown> | undefined;
2829
+ global.fetch = vi.fn().mockImplementation(async (_url: string, init: RequestInit) => {
2830
+ capturedBody = JSON.parse(init.body as string);
2831
+ return { ok: true, body: null };
2832
+ });
2833
+
2834
+ const client = new AgentWidgetClient({ apiUrl: 'http://localhost:43111/api/chat/dispatch' });
2835
+ await client.resumeFlow('exec_abc', { t: 'ok' }, { streamResponse: false });
2836
+
2837
+ expect(capturedBody!.streamResponse).toBe(false);
2838
+ });
2839
+
2840
+ it('derives the URL correctly for proxy-style dispatch paths', async () => {
2841
+ let capturedUrl: string | undefined;
2842
+ global.fetch = vi.fn().mockImplementation(async (url: string) => {
2843
+ capturedUrl = url;
2844
+ return { ok: true, body: null };
2845
+ });
2846
+
2847
+ const client = new AgentWidgetClient({ apiUrl: 'http://localhost:43111/api/chat/dispatch' });
2848
+ await client.resumeFlow('exec_abc', {});
2849
+
2850
+ expect(capturedUrl).toBe('http://localhost:43111/api/chat/dispatch/resume');
2851
+ });
2852
+ });
2853
+
package/src/client.ts CHANGED
@@ -775,6 +775,49 @@ export class AgentWidgetClient {
775
775
  });
776
776
  }
777
777
 
778
+ /**
779
+ * Resume a paused flow execution by supplying outputs for LOCAL
780
+ * (client-executed) tools. Used by the built-in `ask_user_question`
781
+ * answer-pill sheet, but generic enough for any LOCAL tool.
782
+ *
783
+ * Posts to the upstream `/resume` endpoint (the dispatch URL with
784
+ * `/dispatch` replaced by `/resume` — works for both direct-to-Runtype
785
+ * and the persona proxy) and returns the raw Response so the caller can
786
+ * pipe its SSE body through `connectStream()`.
787
+ *
788
+ * @param executionId - The paused execution id carried on `step_await`.
789
+ * @param toolOutputs - Map keyed by tool name → the tool's result value.
790
+ */
791
+ public async resumeFlow(
792
+ executionId: string,
793
+ toolOutputs: Record<string, unknown>,
794
+ options?: { streamResponse?: boolean }
795
+ ): Promise<Response> {
796
+ const trimmed = this.config.apiUrl?.replace(/\/+$/, '') || DEFAULT_CLIENT_API_BASE;
797
+ // Runtype mounts POST /resume as a child route of /v1/dispatch, so the
798
+ // final URL is always `${apiUrl}/resume`. Proxies should follow the
799
+ // same shape (`/api/chat/dispatch/resume`) to keep the widget agnostic.
800
+ const url = `${trimmed}/resume`;
801
+
802
+ let headers: Record<string, string> = {
803
+ 'Content-Type': 'application/json',
804
+ ...this.headers
805
+ };
806
+ if (this.getHeaders) {
807
+ Object.assign(headers, await this.getHeaders());
808
+ }
809
+
810
+ return fetch(url, {
811
+ method: 'POST',
812
+ headers,
813
+ body: JSON.stringify({
814
+ executionId,
815
+ toolOutputs,
816
+ streamResponse: options?.streamResponse ?? true,
817
+ }),
818
+ });
819
+ }
820
+
778
821
  private async buildAgentPayload(
779
822
  messages: AgentWidgetMessage[]
780
823
  ): Promise<AgentWidgetAgentRequestPayload> {
@@ -1722,6 +1765,34 @@ export class AgentWidgetClient {
1722
1765
  if (callKey) {
1723
1766
  toolContext.byCall.delete(callKey);
1724
1767
  }
1768
+ } else if (payloadType === "step_await" && payload.awaitReason === "local_tool_required" && payload.toolName) {
1769
+ // LOCAL tool pause. Runtype's prompt step throws LocalToolRequiredError
1770
+ // when the model calls a tool with `toolType: "local"`. The server
1771
+ // emits step_await with the tool name, params, and execution id; the
1772
+ // execution pauses until the client POSTs /resume with toolOutputs.
1773
+ //
1774
+ // Upsert a fully-populated tool-variant message so the existing
1775
+ // ask_user_question bubble + sheet paths fire. Mark the message with
1776
+ // `awaitingLocalTool: true` so the UI knows to resolve via
1777
+ // resumeFlow rather than the legacy sendMessage fallback.
1778
+ const toolId = (payload.toolId as string) ?? `local-${nextSequence()}`;
1779
+ const toolMessage = ensureToolMessage(toolId);
1780
+ const tool = toolMessage.toolCall ?? { id: toolId, status: "pending" as const };
1781
+ tool.name = payload.toolName as string;
1782
+ tool.args = payload.parameters;
1783
+ tool.status = "complete";
1784
+ tool.chunks = tool.chunks ?? [];
1785
+ tool.startedAt =
1786
+ tool.startedAt ?? resolveTimestamp(payload.startedAt ?? payload.timestamp);
1787
+ tool.completedAt = tool.completedAt ?? tool.startedAt;
1788
+ toolMessage.toolCall = tool;
1789
+ toolMessage.streaming = false;
1790
+ toolMessage.agentMetadata = {
1791
+ ...toolMessage.agentMetadata,
1792
+ executionId: (payload.executionId as string) ?? toolMessage.agentMetadata?.executionId,
1793
+ awaitingLocalTool: true,
1794
+ };
1795
+ emitMessage(toolMessage);
1725
1796
  } else if (payloadType === "text_start") {
1726
1797
  // Lifecycle event: a new text segment is beginning (emitted at tool boundaries).
1727
1798
  // When toolContext is present this fired inside a nested flow — it must not