@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.
- package/README.md +142 -0
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-HPZY7oAI.d.cts → types-cwY5HaFD.d.cts} +25 -0
- package/dist/animations/{types-HPZY7oAI.d.ts → types-cwY5HaFD.d.ts} +25 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/index.cjs +47 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +300 -1
- package/dist/index.d.ts +300 -1
- package/dist/index.global.js +75 -75
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +47 -47
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +1432 -159
- package/dist/theme-editor.d.cts +218 -0
- package/dist/theme-editor.d.ts +218 -0
- package/dist/theme-editor.js +1432 -159
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +14 -0
- package/dist/theme-reference.d.ts +14 -0
- package/dist/widget.css +432 -0
- package/package.json +1 -1
- package/src/client.test.ts +134 -0
- package/src/client.ts +71 -0
- package/src/components/ask-user-question-bubble.test.ts +583 -0
- package/src/components/ask-user-question-bubble.ts +924 -0
- package/src/components/messages.ts +33 -1
- package/src/components/panel.ts +41 -4
- package/src/defaults.ts +21 -0
- package/src/index.ts +16 -1
- package/src/plugins/types.ts +57 -0
- package/src/session.test.ts +183 -0
- package/src/session.ts +242 -3
- package/src/styles/widget.css +432 -0
- package/src/types/theme.ts +15 -0
- package/src/types.ts +150 -0
- package/src/ui.ask-user-question-plugin.test.ts +649 -0
- package/src/ui.ts +631 -5
- package/src/utils/storage.ts +10 -2
- package/src/utils/theme.test.ts +36 -0
- package/src/utils/tokens.ts +23 -0
package/src/client.test.ts
CHANGED
|
@@ -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
|