@runtypelabs/persona 4.2.0 → 4.3.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 +22 -6
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-B_xbfvR0.d.cts → types-C6tFDxKy.d.cts} +1 -1
- package/dist/animations/{types-B_xbfvR0.d.ts → types-C6tFDxKy.d.ts} +1 -1
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/codegen.cjs +6 -6
- package/dist/codegen.js +9 -9
- package/dist/index.cjs +44 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +101 -3
- package/dist/index.d.ts +101 -3
- package/dist/index.global.js +33 -33
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +44 -44
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +76 -1
- package/dist/smart-dom-reader.d.ts +76 -1
- package/dist/theme-editor-preview.cjs +35 -35
- package/dist/theme-editor-preview.d.cts +76 -1
- package/dist/theme-editor-preview.d.ts +76 -1
- package/dist/theme-editor-preview.js +37 -37
- package/dist/theme-editor.cjs +1 -1
- package/dist/theme-editor.d.cts +76 -1
- package/dist/theme-editor.d.ts +76 -1
- package/dist/theme-editor.js +1 -1
- package/package.json +1 -1
- package/src/client.test.ts +349 -29
- package/src/client.ts +96 -24
- package/src/defaults.ts +2 -0
- package/src/index-core.ts +2 -0
- package/src/install.ts +9 -1
- package/src/session.ts +6 -3
- package/src/session.voice.test.ts +65 -0
- package/src/types.ts +57 -2
- package/src/utils/__fixtures__/unified-translator.oracle.ts +3 -3
- package/src/utils/code-generators.ts +10 -0
- package/src/utils/target.test.ts +51 -0
- package/src/utils/target.ts +90 -0
package/src/client.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
WebMcpConfirmHandler
|
|
25
25
|
} from "./types";
|
|
26
26
|
import { WebMcpBridge, computeClientToolsFingerprint, isWebMcpToolName } from "./webmcp-bridge";
|
|
27
|
+
import { resolveTarget } from "./utils/target";
|
|
27
28
|
import { builtInClientToolsForDispatch } from "./ask-user-question-tool";
|
|
28
29
|
import {
|
|
29
30
|
extractTextFromJson,
|
|
@@ -185,6 +186,11 @@ export class AgentWidgetClient {
|
|
|
185
186
|
private readonly webMcpBridge: WebMcpBridge | null;
|
|
186
187
|
|
|
187
188
|
constructor(private config: AgentWidgetConfig = {}) {
|
|
189
|
+
if (config.target && (config.agentId || config.flowId || config.agent)) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
"[Persona] `target` is mutually exclusive with `agentId`, `flowId`, and `agent`. Set only one routing field.",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
188
194
|
this.apiUrl = config.apiUrl ?? DEFAULT_ENDPOINT;
|
|
189
195
|
this.headers = {
|
|
190
196
|
"Content-Type": "application/json",
|
|
@@ -284,11 +290,33 @@ export class AgentWidgetClient {
|
|
|
284
290
|
return !!this.config.clientToken;
|
|
285
291
|
}
|
|
286
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Resolve the effective backend routing for the current config. Combines the
|
|
295
|
+
* explicit `agentId`/`flowId` fields with the normalized `target` string
|
|
296
|
+
* (resolved via `resolveTarget`). Computed on demand so it stays correct
|
|
297
|
+
* across `update()`; the `target`/explicit-field conflict is rejected in the
|
|
298
|
+
* constructor, so at most one source is set here.
|
|
299
|
+
*/
|
|
300
|
+
private routing(): {
|
|
301
|
+
agentId?: string;
|
|
302
|
+
flowId?: string;
|
|
303
|
+
targetPayload?: Record<string, unknown>;
|
|
304
|
+
} {
|
|
305
|
+
const { agentId, flowId, target, targetProviders } = this.config;
|
|
306
|
+
if (!target) {
|
|
307
|
+
return { agentId, flowId };
|
|
308
|
+
}
|
|
309
|
+
const resolved = resolveTarget(target, targetProviders);
|
|
310
|
+
if (resolved.kind === "agentId") return { agentId: resolved.agentId };
|
|
311
|
+
if (resolved.kind === "flowId") return { flowId: resolved.flowId };
|
|
312
|
+
return { targetPayload: resolved.payload };
|
|
313
|
+
}
|
|
314
|
+
|
|
287
315
|
/**
|
|
288
316
|
* Check if operating in agent execution mode
|
|
289
317
|
*/
|
|
290
318
|
public isAgentMode(): boolean {
|
|
291
|
-
return !!this.config.agent;
|
|
319
|
+
return !!(this.config.agent || this.routing().agentId);
|
|
292
320
|
}
|
|
293
321
|
|
|
294
322
|
/**
|
|
@@ -346,9 +374,11 @@ export class AgentWidgetClient {
|
|
|
346
374
|
// Get stored session_id if available (for session resumption)
|
|
347
375
|
const storedSessionId = this.config.getStoredSessionId?.() || null;
|
|
348
376
|
|
|
377
|
+
const routed = this.routing();
|
|
378
|
+
const sessionTargetId = routed.agentId ?? routed.flowId;
|
|
349
379
|
const requestBody: Record<string, unknown> = {
|
|
350
380
|
token: this.config.clientToken,
|
|
351
|
-
...(
|
|
381
|
+
...(sessionTargetId && { flowId: sessionTargetId }),
|
|
352
382
|
...(storedSessionId && { sessionId: storedSessionId }),
|
|
353
383
|
};
|
|
354
384
|
|
|
@@ -584,12 +614,12 @@ export class AgentWidgetClient {
|
|
|
584
614
|
* Send a message - handles both proxy and client token modes
|
|
585
615
|
*/
|
|
586
616
|
public async dispatch(options: DispatchOptions, onEvent: SSEHandler) {
|
|
587
|
-
if (this.isAgentMode()) {
|
|
588
|
-
return this.dispatchAgent(options, onEvent);
|
|
589
|
-
}
|
|
590
617
|
if (this.isClientTokenMode()) {
|
|
591
618
|
return this.dispatchClientToken(options, onEvent);
|
|
592
619
|
}
|
|
620
|
+
if (this.isAgentMode()) {
|
|
621
|
+
return this.dispatchAgent(options, onEvent);
|
|
622
|
+
}
|
|
593
623
|
return this.dispatchProxy(options, onEvent);
|
|
594
624
|
}
|
|
595
625
|
|
|
@@ -1040,7 +1070,8 @@ export class AgentWidgetClient {
|
|
|
1040
1070
|
private async buildAgentPayload(
|
|
1041
1071
|
messages: AgentWidgetMessage[]
|
|
1042
1072
|
): Promise<AgentWidgetAgentRequestPayload> {
|
|
1043
|
-
|
|
1073
|
+
const routedAgentId = this.routing().agentId;
|
|
1074
|
+
if (!this.config.agent && !routedAgentId) {
|
|
1044
1075
|
throw new Error('Agent configuration required for agent mode');
|
|
1045
1076
|
}
|
|
1046
1077
|
|
|
@@ -1062,7 +1093,7 @@ export class AgentWidgetClient {
|
|
|
1062
1093
|
}));
|
|
1063
1094
|
|
|
1064
1095
|
const payload: AgentWidgetAgentRequestPayload = {
|
|
1065
|
-
agent: this.config.agent,
|
|
1096
|
+
agent: this.config.agent ?? { agentId: routedAgentId! },
|
|
1066
1097
|
messages: normalizedMessages,
|
|
1067
1098
|
options: {
|
|
1068
1099
|
streamResponse: true,
|
|
@@ -1134,11 +1165,27 @@ export class AgentWidgetClient {
|
|
|
1134
1165
|
createdAt: message.createdAt
|
|
1135
1166
|
}));
|
|
1136
1167
|
|
|
1168
|
+
const routed = this.routing();
|
|
1137
1169
|
const payload: AgentWidgetRequestPayload = {
|
|
1138
1170
|
messages: normalizedMessages,
|
|
1139
|
-
...(
|
|
1171
|
+
...(routed.agentId
|
|
1172
|
+
? { agent: { agentId: routed.agentId } }
|
|
1173
|
+
: routed.flowId
|
|
1174
|
+
? { flowId: routed.flowId }
|
|
1175
|
+
: {})
|
|
1140
1176
|
};
|
|
1141
1177
|
|
|
1178
|
+
// Custom-provider targets (e.g. `eve:support`) resolve to a payload
|
|
1179
|
+
// fragment that is merged into the dispatch body so a BYO backend can read
|
|
1180
|
+
// whatever routing keys its resolver chose. `messages` is authoritative and
|
|
1181
|
+
// can never be overridden by a resolver.
|
|
1182
|
+
if (routed.targetPayload) {
|
|
1183
|
+
for (const [key, value] of Object.entries(routed.targetPayload)) {
|
|
1184
|
+
if (key === "messages") continue;
|
|
1185
|
+
(payload as Record<string, unknown>)[key] = value;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1142
1189
|
// Client tools: same built-in + WebMCP merge as buildAgentPayload
|
|
1143
1190
|
// (flow-dispatch path).
|
|
1144
1191
|
const clientTools = [
|
|
@@ -1378,10 +1425,10 @@ export class AgentWidgetClient {
|
|
|
1378
1425
|
// Reference to track assistant message for custom event handler
|
|
1379
1426
|
const assistantMessageRef = { current: null as AgentWidgetMessage | null };
|
|
1380
1427
|
// Segmentation state for the `parseSSEEvent` extensibility callback (the
|
|
1381
|
-
// consumer's own `partId` field) — independent of the
|
|
1428
|
+
// consumer's own `partId` field) — independent of the wire.
|
|
1382
1429
|
const customParsePartId = { current: null as string | null };
|
|
1383
1430
|
// Unified text-channel block id (from `text_start`/`text_delta` `id`). Drives
|
|
1384
|
-
// bubble-id segmentation on the
|
|
1431
|
+
// bubble-id segmentation on the wire in place of the legacy `partId`:
|
|
1385
1432
|
// a new block id means a new bubble, sealed at `text_complete`/tool boundaries.
|
|
1386
1433
|
let currentTextBlockId: string | null = null;
|
|
1387
1434
|
// Raw text accumulated for the open flow block before its bubble is
|
|
@@ -1389,7 +1436,7 @@ export class AgentWidgetClient {
|
|
|
1389
1436
|
let pendingFlowRaw = "";
|
|
1390
1437
|
// Nested flow-as-tool attribution (PR #4602): a text/reasoning block whose
|
|
1391
1438
|
// `parentToolCallId` matches a `tool_start.toolCallId` belongs to a flow
|
|
1392
|
-
// running as that tool. Keyed by the
|
|
1439
|
+
// running as that tool. Keyed by the wire block id, these route the block's
|
|
1393
1440
|
// deltas into a message tagged `agentMetadata.parentToolId` (the parent tool's
|
|
1394
1441
|
// row) instead of the top-level assistant/reasoning channel.
|
|
1395
1442
|
const nestedBlockParent = new Map<string, string>();
|
|
@@ -1769,7 +1816,7 @@ export class AgentWidgetClient {
|
|
|
1769
1816
|
// `text_start`/`text_complete`) and can be structured JSON, so each block
|
|
1770
1817
|
// runs through the per-bubble structured-content parser — agent text stays
|
|
1771
1818
|
// plain. This is the legacy step_delta parser core, re-keyed from `partId`
|
|
1772
|
-
// to the
|
|
1819
|
+
// to the wire block-id bubble. The caller materializes the bubble lazily
|
|
1773
1820
|
// (whitespace-only blocks around tool boundaries never leave a stray bubble)
|
|
1774
1821
|
// and `step_complete.result.response` reconciles the authoritative final.
|
|
1775
1822
|
let lastSealedFlowBubble: AgentWidgetMessage | null = null;
|
|
@@ -1947,16 +1994,16 @@ export class AgentWidgetClient {
|
|
|
1947
1994
|
return message;
|
|
1948
1995
|
};
|
|
1949
1996
|
|
|
1950
|
-
// Ready queue of parsed
|
|
1951
|
-
//
|
|
1997
|
+
// Ready queue of parsed wire frames awaiting a drain. The API streams the
|
|
1998
|
+
// 33-event wire vocabulary; each frame is parsed in the SSE loop
|
|
1952
1999
|
// below and rendered directly by the handler (no translation bridge), then
|
|
1953
|
-
// pushed here. The
|
|
2000
|
+
// pushed here. The wire stream is a single, in-order SSE connection, so
|
|
1954
2001
|
// frames drain straight through with no reordering.
|
|
1955
2002
|
const seqReadyQueue: Array<{ payloadType: string; payload: any }> = [];
|
|
1956
2003
|
// Declared here so later closures can reference it; assigned after all
|
|
1957
2004
|
// handler-scoped variables are initialised (before the SSE loop).
|
|
1958
2005
|
let drainReadyQueue: () => void;
|
|
1959
|
-
// Per-stream media-block buffer: the
|
|
2006
|
+
// Per-stream media-block buffer: the media triad
|
|
1960
2007
|
// (media_start/media_delta/media_complete) is reassembled here into a single
|
|
1961
2008
|
// synthetic message at media_complete, keyed by the block id.
|
|
1962
2009
|
const mediaBuffers = new Map<
|
|
@@ -1967,8 +2014,17 @@ export class AgentWidgetClient {
|
|
|
1967
2014
|
// `turn_start` advancing the iteration rotates the bubble in 'separate' mode.
|
|
1968
2015
|
let lastIterationSeen = 0;
|
|
1969
2016
|
// Execution kind, resolved from the leading `execution_start` frame. Drives
|
|
1970
|
-
// the agent-vs-flow branches that the single
|
|
2017
|
+
// the agent-vs-flow branches that the single wire vocabulary collapses.
|
|
1971
2018
|
let executionKind: "agent" | "flow" = "agent";
|
|
2019
|
+
// Whether `executionKind` was set authoritatively by an `execution_start`
|
|
2020
|
+
// frame. Continuation streams (e.g. a tool-driven `/resume`) do NOT re-emit
|
|
2021
|
+
// `execution_start`, so a fresh `streamResponse` for the continuation starts
|
|
2022
|
+
// with the default `"agent"`. For a flow that mis-routes the final
|
|
2023
|
+
// prompt-step finalization and duplicates the last message (the streamed
|
|
2024
|
+
// text block is sealed, then `step_complete.result.response` re-renders it as
|
|
2025
|
+
// a second bubble). When `execution_start` is absent we recover the flow kind
|
|
2026
|
+
// from the first flow `step_*` frame below.
|
|
2027
|
+
let executionKindResolved = false;
|
|
1972
2028
|
// Open turn id (from `turn_start`). Unified text/reasoning deltas carry their
|
|
1973
2029
|
// own block id, not the turn id, so the turn id is threaded onto agentMetadata
|
|
1974
2030
|
// from here.
|
|
@@ -1987,6 +2043,21 @@ export class AgentWidgetClient {
|
|
|
1987
2043
|
const payloadType = seqReadyQueue[i].payloadType;
|
|
1988
2044
|
const payload = seqReadyQueue[i].payload;
|
|
1989
2045
|
|
|
2046
|
+
// Recover the execution kind on continuation streams that omit
|
|
2047
|
+
// `execution_start` (e.g. a tool-driven `/resume`). Flow `step_*` frames
|
|
2048
|
+
// carry a `stepType`; agent loops never do (they use `turn_*`). Without
|
|
2049
|
+
// this, the continuation defaults to `"agent"` and a flow's final
|
|
2050
|
+
// prompt-step finalization is duplicated. We only infer when no
|
|
2051
|
+
// `execution_start` resolved the kind, so an explicit `agent` is never
|
|
2052
|
+
// overridden.
|
|
2053
|
+
if (
|
|
2054
|
+
!executionKindResolved &&
|
|
2055
|
+
executionKind !== "flow" &&
|
|
2056
|
+
typeof (payload as { stepType?: unknown }).stepType === "string"
|
|
2057
|
+
) {
|
|
2058
|
+
executionKind = "flow";
|
|
2059
|
+
}
|
|
2060
|
+
|
|
1990
2061
|
if (payloadType === "reasoning_start") {
|
|
1991
2062
|
// Nested flow-as-tool thinking (PR #4602): route to the parent tool's row.
|
|
1992
2063
|
const rStartBlockId = typeof payload.id === "string" ? payload.id : null;
|
|
@@ -2474,7 +2545,7 @@ export class AgentWidgetClient {
|
|
|
2474
2545
|
}
|
|
2475
2546
|
|
|
2476
2547
|
// A failed step (`success:false`) — including the legacy `step_error`
|
|
2477
|
-
// event, which the
|
|
2548
|
+
// event, which the wire encoder folds into a failed `step_complete`
|
|
2478
2549
|
// — surfaces as a terminal error and finalizes the stream.
|
|
2479
2550
|
if (payload.success === false) {
|
|
2480
2551
|
const e = payload.error;
|
|
@@ -2543,6 +2614,7 @@ export class AgentWidgetClient {
|
|
|
2543
2614
|
// ================================================================
|
|
2544
2615
|
} else if (payloadType === "execution_start") {
|
|
2545
2616
|
executionKind = payload.kind === "flow" ? "flow" : "agent";
|
|
2617
|
+
executionKindResolved = true;
|
|
2546
2618
|
if (executionKind === "agent") {
|
|
2547
2619
|
agentExecution = {
|
|
2548
2620
|
executionId: payload.executionId,
|
|
@@ -2595,7 +2667,7 @@ export class AgentWidgetClient {
|
|
|
2595
2667
|
// Authoritative args are set at tool_start; nothing to render here.
|
|
2596
2668
|
continue;
|
|
2597
2669
|
} else if (payloadType === "turn_complete") {
|
|
2598
|
-
// Reasoning is sealed by its own reasoning_complete
|
|
2670
|
+
// Reasoning is sealed by its own reasoning_complete on the wire
|
|
2599
2671
|
// vocabulary; this only attaches the turn-level stopReason to the
|
|
2600
2672
|
// assistant message produced by this turn. Falls back to
|
|
2601
2673
|
// lastAssistantInTurn when the bubble was sealed at a tool boundary
|
|
@@ -2615,7 +2687,7 @@ export class AgentWidgetClient {
|
|
|
2615
2687
|
}
|
|
2616
2688
|
if (openTurnId === payload.id) openTurnId = null;
|
|
2617
2689
|
} else if (payloadType === "media_start") {
|
|
2618
|
-
// Open a
|
|
2690
|
+
// Open a media block; buffer fragments until media_complete.
|
|
2619
2691
|
const id = String(payload.id);
|
|
2620
2692
|
mediaBuffers.set(id, {
|
|
2621
2693
|
mediaType: typeof payload.mediaType === "string" ? payload.mediaType : undefined,
|
|
@@ -2651,7 +2723,7 @@ export class AgentWidgetClient {
|
|
|
2651
2723
|
if (completeData) {
|
|
2652
2724
|
reconstructed = { type: "media", data: completeData, mediaType: completeMediaType };
|
|
2653
2725
|
} else if (completeUrl) {
|
|
2654
|
-
// The
|
|
2726
|
+
// The wire is mediaType-only; a URL part with no declared MIME
|
|
2655
2727
|
// arrives as the bare bucket hint "image" (per the API encoder). Treat
|
|
2656
2728
|
// that — and any real `image/*` — as a hosted image so we don't misroute
|
|
2657
2729
|
// generated images into the file bucket.
|
|
@@ -2807,7 +2879,7 @@ export class AgentWidgetClient {
|
|
|
2807
2879
|
|
|
2808
2880
|
onEvent({ type: "status", status: "idle" });
|
|
2809
2881
|
} else if (payloadType === "execution_error") {
|
|
2810
|
-
// Terminal failure. The
|
|
2882
|
+
// Terminal failure. The non-terminal `error` is handled
|
|
2811
2883
|
// separately (recoverable → warn).
|
|
2812
2884
|
const errorMessage = typeof payload.error === 'string'
|
|
2813
2885
|
? payload.error
|
|
@@ -3022,7 +3094,7 @@ export class AgentWidgetClient {
|
|
|
3022
3094
|
// retrying" — and the execution continues, so it must NOT surface as a
|
|
3023
3095
|
// fatal error or finalize the stream. The API routes terminal failures
|
|
3024
3096
|
// through `execution_error`. Only an explicit `recoverable: false`
|
|
3025
|
-
// promotes
|
|
3097
|
+
// promotes an `error` to terminal.
|
|
3026
3098
|
if (
|
|
3027
3099
|
payload.recoverable === false &&
|
|
3028
3100
|
payload.error != null &&
|
|
@@ -3140,7 +3212,7 @@ export class AgentWidgetClient {
|
|
|
3140
3212
|
if (handled) continue; // Skip default handling if custom handler processed it
|
|
3141
3213
|
}
|
|
3142
3214
|
|
|
3143
|
-
// The wire is the
|
|
3215
|
+
// The wire is the wire vocabulary; the handler consumes it
|
|
3144
3216
|
// natively. The stream is single-connection and in order, so each frame
|
|
3145
3217
|
// drains straight through.
|
|
3146
3218
|
seqReadyQueue.push({ payloadType, payload });
|
package/src/defaults.ts
CHANGED
|
@@ -72,6 +72,8 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
|
|
|
72
72
|
apiUrl: "https://api.runtype.com/api/chat/dispatch",
|
|
73
73
|
// Client token mode defaults (optional, only used when clientToken is set)
|
|
74
74
|
clientToken: undefined,
|
|
75
|
+
agentId: undefined,
|
|
76
|
+
target: undefined,
|
|
75
77
|
theme: undefined,
|
|
76
78
|
darkTheme: undefined,
|
|
77
79
|
colorScheme: "light",
|
package/src/index-core.ts
CHANGED
package/src/install.ts
CHANGED
|
@@ -17,6 +17,7 @@ interface SiteAgentInstallConfig {
|
|
|
17
17
|
// Client token mode options (can also be set via data attributes)
|
|
18
18
|
clientToken?: string;
|
|
19
19
|
flowId?: string;
|
|
20
|
+
agentId?: string;
|
|
20
21
|
apiUrl?: string;
|
|
21
22
|
// Optional query param key that gates widget installation in preview mode
|
|
22
23
|
previewQueryParam?: string;
|
|
@@ -69,7 +70,7 @@ declare global {
|
|
|
69
70
|
|
|
70
71
|
/**
|
|
71
72
|
* Read configuration from data attributes on the current script tag.
|
|
72
|
-
* Supports: data-config (JSON), data-runtype-token, data-flow-id, data-api-url
|
|
73
|
+
* Supports: data-config (JSON), data-runtype-token, data-flow-id, data-agent-id, data-api-url
|
|
73
74
|
*/
|
|
74
75
|
const getConfigFromScript = (): Partial<SiteAgentInstallConfig> => {
|
|
75
76
|
// Try to get the current script element
|
|
@@ -112,6 +113,12 @@ declare global {
|
|
|
112
113
|
scriptConfig.flowId = flowId;
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
// Optional agent ID
|
|
117
|
+
const agentId = script.getAttribute('data-agent-id');
|
|
118
|
+
if (agentId) {
|
|
119
|
+
scriptConfig.agentId = agentId;
|
|
120
|
+
}
|
|
121
|
+
|
|
115
122
|
// Optional API URL override
|
|
116
123
|
const apiUrl = script.getAttribute('data-api-url');
|
|
117
124
|
if (apiUrl) {
|
|
@@ -357,6 +364,7 @@ declare global {
|
|
|
357
364
|
if (config.apiUrl && !widgetConfig.apiUrl) widgetConfig.apiUrl = config.apiUrl;
|
|
358
365
|
if (config.clientToken && !widgetConfig.clientToken) widgetConfig.clientToken = config.clientToken;
|
|
359
366
|
if (config.flowId && !widgetConfig.flowId) widgetConfig.flowId = config.flowId;
|
|
367
|
+
if (config.agentId && !widgetConfig.agentId) widgetConfig.agentId = config.agentId;
|
|
360
368
|
|
|
361
369
|
const hasApiConfig = !!(widgetConfig.apiUrl || widgetConfig.clientToken);
|
|
362
370
|
return { target, widgetConfig, hasApiConfig };
|
package/src/session.ts
CHANGED
|
@@ -70,6 +70,9 @@ const CONNECTION_CONFIG_KEYS = [
|
|
|
70
70
|
"apiUrl",
|
|
71
71
|
"clientToken",
|
|
72
72
|
"flowId",
|
|
73
|
+
"agentId",
|
|
74
|
+
"target",
|
|
75
|
+
"targetProviders",
|
|
73
76
|
"agent",
|
|
74
77
|
"agentOptions",
|
|
75
78
|
"headers",
|
|
@@ -286,7 +289,7 @@ export class AgentWidgetSession {
|
|
|
286
289
|
// clientToken) falls back to the browser voice, so the fetch would be wasted.
|
|
287
290
|
const host = tts.host ?? this.config.apiUrl;
|
|
288
291
|
const agentId =
|
|
289
|
-
tts.agentId ?? this.config.voiceRecognition?.provider?.runtype?.agentId;
|
|
292
|
+
tts.agentId ?? this.config.voiceRecognition?.provider?.runtype?.agentId ?? this.config.agentId;
|
|
290
293
|
if (!host || !agentId || !this.config.clientToken) return;
|
|
291
294
|
void loadRuntypeTts().catch(() => {});
|
|
292
295
|
}
|
|
@@ -415,7 +418,7 @@ export class AgentWidgetSession {
|
|
|
415
418
|
if (tts?.provider === "runtype") {
|
|
416
419
|
const host = tts.host ?? this.config.apiUrl;
|
|
417
420
|
const agentId =
|
|
418
|
-
tts.agentId ?? this.config.voiceRecognition?.provider?.runtype?.agentId;
|
|
421
|
+
tts.agentId ?? this.config.voiceRecognition?.provider?.runtype?.agentId ?? this.config.agentId;
|
|
419
422
|
const clientToken = this.config.clientToken;
|
|
420
423
|
const wantFallback = tts.browserFallback !== false;
|
|
421
424
|
|
|
@@ -659,7 +662,7 @@ export class AgentWidgetSession {
|
|
|
659
662
|
return {
|
|
660
663
|
type: 'runtype',
|
|
661
664
|
runtype: {
|
|
662
|
-
agentId: providerConfig.runtype?.agentId
|
|
665
|
+
agentId: providerConfig.runtype?.agentId ?? this.config.agentId ?? '',
|
|
663
666
|
// Default credentials/endpoint from the widget config so the minimum
|
|
664
667
|
// voice config collapses to just `agentId`.
|
|
665
668
|
clientToken: providerConfig.runtype?.clientToken ?? this.config.clientToken,
|
|
@@ -49,6 +49,7 @@ vi.mock('./voice', async (importOriginal) => {
|
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
import { AgentWidgetSession } from './session';
|
|
52
|
+
import { setRuntypeTtsLoader } from './voice/runtype-tts-loader';
|
|
52
53
|
|
|
53
54
|
describe('AgentWidgetSession - realtime voice onTranscript (Option B)', () => {
|
|
54
55
|
let session: AgentWidgetSession;
|
|
@@ -144,3 +145,67 @@ describe('AgentWidgetSession - realtime voice onTranscript (Option B)', () => {
|
|
|
144
145
|
expect(metricsSeen).toEqual([{ llmMs: 100, totalMs: 250 }]);
|
|
145
146
|
});
|
|
146
147
|
});
|
|
148
|
+
|
|
149
|
+
describe('AgentWidgetSession - Runtype TTS config', () => {
|
|
150
|
+
it('uses top-level agentId as the default Runtype TTS agent', async () => {
|
|
151
|
+
let capturedOptions: { agentId?: string; clientToken?: string; host?: string } | null = null;
|
|
152
|
+
|
|
153
|
+
class FakeRuntypeSpeechEngine {
|
|
154
|
+
readonly id = 'runtype';
|
|
155
|
+
readonly supportsPause = false;
|
|
156
|
+
|
|
157
|
+
constructor(options: { agentId?: string; clientToken?: string; host?: string }) {
|
|
158
|
+
capturedOptions = options;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
speak() {}
|
|
162
|
+
pause() {}
|
|
163
|
+
resume() {}
|
|
164
|
+
stop() {}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
class FakeFallbackSpeechEngine {}
|
|
168
|
+
|
|
169
|
+
setRuntypeTtsLoader(async () => ({
|
|
170
|
+
RuntypeSpeechEngine: FakeRuntypeSpeechEngine as any,
|
|
171
|
+
FallbackSpeechEngine: FakeFallbackSpeechEngine as any,
|
|
172
|
+
}));
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const session = new AgentWidgetSession(
|
|
176
|
+
{
|
|
177
|
+
apiUrl: 'https://api.runtype.com',
|
|
178
|
+
clientToken: 'ct_live_demo',
|
|
179
|
+
agentId: 'agent_top_level',
|
|
180
|
+
textToSpeech: { enabled: true, provider: 'runtype', browserFallback: false },
|
|
181
|
+
initialMessages: [
|
|
182
|
+
{
|
|
183
|
+
id: 'assistant-1',
|
|
184
|
+
role: 'assistant',
|
|
185
|
+
content: 'Read this',
|
|
186
|
+
createdAt: new Date().toISOString(),
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
onMessagesChanged: () => {},
|
|
192
|
+
onStatusChanged: () => {},
|
|
193
|
+
onStreamingChanged: () => {},
|
|
194
|
+
onError: () => {},
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
session.toggleReadAloud('assistant-1');
|
|
199
|
+
await Promise.resolve();
|
|
200
|
+
await Promise.resolve();
|
|
201
|
+
|
|
202
|
+
expect(capturedOptions).toMatchObject({
|
|
203
|
+
agentId: 'agent_top_level',
|
|
204
|
+
clientToken: 'ct_live_demo',
|
|
205
|
+
host: 'https://api.runtype.com',
|
|
206
|
+
});
|
|
207
|
+
} finally {
|
|
208
|
+
setRuntypeTtsLoader(null);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
});
|
package/src/types.ts
CHANGED
|
@@ -5,6 +5,9 @@ import type {
|
|
|
5
5
|
RuntypeClientFeedbackRequest,
|
|
6
6
|
RuntypeStopReasonKind,
|
|
7
7
|
} from "./generated/runtype-openapi-contract";
|
|
8
|
+
import type { TargetResolver } from "./utils/target";
|
|
9
|
+
|
|
10
|
+
export type { TargetResolver, ResolvedTarget } from "./utils/target";
|
|
8
11
|
|
|
9
12
|
// ============================================================================
|
|
10
13
|
// Multi-Modal Content Types
|
|
@@ -100,6 +103,7 @@ export type AgentWidgetRequestPayloadMessage = {
|
|
|
100
103
|
export type AgentWidgetRequestPayload = {
|
|
101
104
|
messages: AgentWidgetRequestPayloadMessage[];
|
|
102
105
|
flowId?: string;
|
|
106
|
+
agent?: { agentId: string };
|
|
103
107
|
context?: Record<string, unknown>;
|
|
104
108
|
metadata?: Record<string, unknown>;
|
|
105
109
|
/** Per-turn template variables for /v1/client/chat (merged as root-level {{var}} in Runtype). */
|
|
@@ -212,6 +216,11 @@ export type AgentConfig = {
|
|
|
212
216
|
loopConfig?: AgentLoopConfig;
|
|
213
217
|
};
|
|
214
218
|
|
|
219
|
+
export type SavedAgentConfig = {
|
|
220
|
+
/** Persisted Runtype agent ID to execute. */
|
|
221
|
+
agentId: string;
|
|
222
|
+
};
|
|
223
|
+
|
|
215
224
|
/**
|
|
216
225
|
* Options for agent execution requests.
|
|
217
226
|
*/
|
|
@@ -230,7 +239,7 @@ export type AgentRequestOptions = {
|
|
|
230
239
|
* Request payload for agent execution mode.
|
|
231
240
|
*/
|
|
232
241
|
export type AgentWidgetAgentRequestPayload = {
|
|
233
|
-
agent: AgentConfig;
|
|
242
|
+
agent: AgentConfig | SavedAgentConfig;
|
|
234
243
|
messages: AgentWidgetRequestPayloadMessage[];
|
|
235
244
|
options: AgentRequestOptions;
|
|
236
245
|
context?: Record<string, unknown>;
|
|
@@ -3573,6 +3582,52 @@ export type AgentWidgetLoadingIndicatorConfig = {
|
|
|
3573
3582
|
export type AgentWidgetConfig = {
|
|
3574
3583
|
apiUrl?: string;
|
|
3575
3584
|
flowId?: string;
|
|
3585
|
+
/**
|
|
3586
|
+
* Persisted Runtype agent ID to execute.
|
|
3587
|
+
*
|
|
3588
|
+
* This is the primary Runtype entry point for simple agent-backed widgets.
|
|
3589
|
+
* It is mutually exclusive with `flowId` and with inline `agent` configs.
|
|
3590
|
+
* Voice and Runtype TTS providers inherit this value unless they specify their
|
|
3591
|
+
* own feature-scoped agent ID.
|
|
3592
|
+
*/
|
|
3593
|
+
agentId?: string;
|
|
3594
|
+
/**
|
|
3595
|
+
* Normalized, backend-neutral routing target. A single string that selects
|
|
3596
|
+
* the backend resource to talk to, optimized for portability across
|
|
3597
|
+
* frameworks (it is always serializable: no live objects).
|
|
3598
|
+
*
|
|
3599
|
+
* Shapes:
|
|
3600
|
+
* - Runtype TypeID (no prefix): `"agent_…"` / `"flow_…"` route to the
|
|
3601
|
+
* Runtype agent/flow paths (the prefix is self-describing).
|
|
3602
|
+
* - Provider-prefixed: `"<provider>:<id>"` is resolved by
|
|
3603
|
+
* `targetProviders[provider]` (e.g. `"eve:support"`). `"runtype:…"` is a
|
|
3604
|
+
* built-in that re-detects a TypeID.
|
|
3605
|
+
* - Bare name: `"support"` requires a `targetProviders.default` resolver.
|
|
3606
|
+
*
|
|
3607
|
+
* Mutually exclusive with `agentId`, `flowId`, and inline `agent`. Prefer
|
|
3608
|
+
* `target` for backend-agnostic apps; use `agentId`/`flowId` for the explicit
|
|
3609
|
+
* Runtype path.
|
|
3610
|
+
*
|
|
3611
|
+
* @example
|
|
3612
|
+
* ```typescript
|
|
3613
|
+
* config: { target: "agent_01k..." } // Runtype agent
|
|
3614
|
+
* config: { target: "flow_01k..." } // Runtype flow
|
|
3615
|
+
* config: { // custom backend
|
|
3616
|
+
* apiUrl: "/api/chat",
|
|
3617
|
+
* target: "eve:support",
|
|
3618
|
+
* targetProviders: { eve: (id) => ({ payload: { assistant: id } }) },
|
|
3619
|
+
* }
|
|
3620
|
+
* ```
|
|
3621
|
+
*/
|
|
3622
|
+
target?: string;
|
|
3623
|
+
/**
|
|
3624
|
+
* Prefix-keyed resolvers for `target` strings of the form
|
|
3625
|
+
* `"<provider>:<id>"`. Each resolver maps the id to the dispatch-payload
|
|
3626
|
+
* fragment its backend expects. Register a `default` resolver to also accept
|
|
3627
|
+
* bare (unprefixed, non-TypeID) target names. The built-in `runtype` provider
|
|
3628
|
+
* is always available and does not need to be registered.
|
|
3629
|
+
*/
|
|
3630
|
+
targetProviders?: Record<string, TargetResolver>;
|
|
3576
3631
|
/**
|
|
3577
3632
|
* Override the assistant-bubble copy shown when a dispatch fails before any
|
|
3578
3633
|
* response streams back (connection refused, CORS, 4xx/5xx, malformed
|
|
@@ -4265,7 +4320,7 @@ export type AgentWidgetReasoning = {
|
|
|
4265
4320
|
status: "pending" | "streaming" | "complete";
|
|
4266
4321
|
chunks: string[];
|
|
4267
4322
|
/**
|
|
4268
|
-
* Reasoning channel scope (
|
|
4323
|
+
* Reasoning channel scope (wire spec). `"turn"` is ordinary per-turn
|
|
4269
4324
|
* thinking; `"loop"` is a cross-iteration agent reflection (the fold that
|
|
4270
4325
|
* replaced the legacy `agent_reflection` event). Absent for legacy streams.
|
|
4271
4326
|
*/
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TEST ORACLE — vendored copy of the runtype-core `createUnifiedEventWrite`
|
|
3
|
-
* translator (the api-side legacy →
|
|
3
|
+
* translator (the api-side legacy → wire 33-event encoder).
|
|
4
4
|
*
|
|
5
|
-
* NOT shipped. Used only by
|
|
6
|
-
* reference: `legacy frames → THIS oracle →
|
|
5
|
+
* NOT shipped. Used only by legacy-wire handler tests as the inverse
|
|
6
|
+
* reference: `legacy frames → THIS oracle → wire frames → client handlers
|
|
7
7
|
* → legacy frames'` must be semantically equal to the original. Keeping a real
|
|
8
8
|
* copy here means the round-trip test fails loudly if the api mapping and the
|
|
9
9
|
* widget bridge ever drift.
|
|
@@ -605,6 +605,8 @@ function generateESMCode(config: any, options?: CodeGeneratorOptions): string {
|
|
|
605
605
|
|
|
606
606
|
if (config.apiUrl) lines.push(` apiUrl: "${config.apiUrl}",`);
|
|
607
607
|
if (config.clientToken) lines.push(` clientToken: "${config.clientToken}",`);
|
|
608
|
+
if (config.agentId) lines.push(` agentId: "${config.agentId}",`);
|
|
609
|
+
if (config.target) lines.push(` target: "${config.target}",`);
|
|
608
610
|
if (config.flowId) lines.push(` flowId: "${config.flowId}",`);
|
|
609
611
|
if (shouldEmitParserType) lines.push(` parserType: "${parserType}",`);
|
|
610
612
|
|
|
@@ -751,6 +753,8 @@ function generateReactComponentCode(config: any, options?: CodeGeneratorOptions)
|
|
|
751
753
|
|
|
752
754
|
if (config.apiUrl) lines.push(` apiUrl: "${config.apiUrl}",`);
|
|
753
755
|
if (config.clientToken) lines.push(` clientToken: "${config.clientToken}",`);
|
|
756
|
+
if (config.agentId) lines.push(` agentId: "${config.agentId}",`);
|
|
757
|
+
if (config.target) lines.push(` target: "${config.target}",`);
|
|
754
758
|
if (config.flowId) lines.push(` flowId: "${config.flowId}",`);
|
|
755
759
|
if (shouldEmitParserType) lines.push(` parserType: "${parserType}",`);
|
|
756
760
|
|
|
@@ -1015,6 +1019,8 @@ function generateReactAdvancedCode(config: any, options?: CodeGeneratorOptions):
|
|
|
1015
1019
|
|
|
1016
1020
|
if (config.apiUrl) lines.push(` apiUrl: "${config.apiUrl}",`);
|
|
1017
1021
|
if (config.clientToken) lines.push(` clientToken: "${config.clientToken}",`);
|
|
1022
|
+
if (config.agentId) lines.push(` agentId: "${config.agentId}",`);
|
|
1023
|
+
if (config.target) lines.push(` target: "${config.target}",`);
|
|
1018
1024
|
if (config.flowId) lines.push(` flowId: "${config.flowId}",`);
|
|
1019
1025
|
|
|
1020
1026
|
if (config.theme && typeof config.theme === "object" && Object.keys(config.theme).length > 0) {
|
|
@@ -1270,6 +1276,8 @@ function buildSerializableConfig(config: any): Record<string, any> {
|
|
|
1270
1276
|
|
|
1271
1277
|
if (config.apiUrl) serializableConfig.apiUrl = config.apiUrl;
|
|
1272
1278
|
if (config.clientToken) serializableConfig.clientToken = config.clientToken;
|
|
1279
|
+
if (config.agentId) serializableConfig.agentId = config.agentId;
|
|
1280
|
+
if (config.target) serializableConfig.target = config.target;
|
|
1273
1281
|
if (config.flowId) serializableConfig.flowId = config.flowId;
|
|
1274
1282
|
if (shouldEmitParserType) serializableConfig.parserType = parserType;
|
|
1275
1283
|
if (config.theme) serializableConfig.theme = config.theme;
|
|
@@ -1419,6 +1427,8 @@ function generateScriptManualCode(config: any, options?: CodeGeneratorOptions):
|
|
|
1419
1427
|
|
|
1420
1428
|
if (config.apiUrl) lines.push(` apiUrl: "${config.apiUrl}",`);
|
|
1421
1429
|
if (config.clientToken) lines.push(` clientToken: "${config.clientToken}",`);
|
|
1430
|
+
if (config.agentId) lines.push(` agentId: "${config.agentId}",`);
|
|
1431
|
+
if (config.target) lines.push(` target: "${config.target}",`);
|
|
1422
1432
|
if (config.flowId) lines.push(` flowId: "${config.flowId}",`);
|
|
1423
1433
|
if (shouldEmitParserType) lines.push(` parserType: "${parserType}",`);
|
|
1424
1434
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { resolveTarget } from "./target";
|
|
3
|
+
|
|
4
|
+
describe("resolveTarget", () => {
|
|
5
|
+
it("resolves a Runtype agent TypeID to agentId routing", () => {
|
|
6
|
+
expect(resolveTarget("agent_01k")).toEqual({
|
|
7
|
+
kind: "agentId",
|
|
8
|
+
agentId: "agent_01k",
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("resolves a Runtype flow TypeID to flowId routing", () => {
|
|
13
|
+
expect(resolveTarget("flow_01k")).toEqual({
|
|
14
|
+
kind: "flowId",
|
|
15
|
+
flowId: "flow_01k",
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("accepts an explicit runtype: prefix on a TypeID", () => {
|
|
20
|
+
expect(resolveTarget("runtype:agent_01k")).toEqual({
|
|
21
|
+
kind: "agentId",
|
|
22
|
+
agentId: "agent_01k",
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("routes a provider-prefixed target through its registered resolver", () => {
|
|
27
|
+
const resolved = resolveTarget("eve:support", {
|
|
28
|
+
eve: (id) => ({ payload: { assistant: id } }),
|
|
29
|
+
});
|
|
30
|
+
expect(resolved).toEqual({ kind: "payload", payload: { assistant: "support" } });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("uses a default resolver for a bare name when registered", () => {
|
|
34
|
+
const resolved = resolveTarget("support", {
|
|
35
|
+
default: (id) => ({ payload: { target: id } }),
|
|
36
|
+
});
|
|
37
|
+
expect(resolved).toEqual({ kind: "payload", payload: { target: "support" } });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("throws for an unknown provider prefix", () => {
|
|
41
|
+
expect(() => resolveTarget("eve:support")).toThrow(/no target provider/i);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("throws for a bare name with no TypeID and no default resolver", () => {
|
|
45
|
+
expect(() => resolveTarget("support")).toThrow(/support/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("throws for an empty target", () => {
|
|
49
|
+
expect(() => resolveTarget(" ")).toThrow(/empty/i);
|
|
50
|
+
});
|
|
51
|
+
});
|