@runtypelabs/persona 4.2.1 → 4.3.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/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 +32 -32
- 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 +23 -23
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +32 -32
- 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 +27 -27
- package/dist/theme-editor-preview.d.cts +76 -1
- package/dist/theme-editor-preview.d.ts +76 -1
- package/dist/theme-editor-preview.js +29 -29
- 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/dist/widget.css +3 -0
- package/package.json +1 -1
- package/src/client.test.ts +221 -29
- package/src/client.ts +71 -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/styles/widget.css +3 -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/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/styles/widget.css
CHANGED
|
@@ -1745,6 +1745,9 @@
|
|
|
1745
1745
|
/* Make message bubble position relative for overlay positioning */
|
|
1746
1746
|
.persona-message-bubble {
|
|
1747
1747
|
position: relative;
|
|
1748
|
+
/* Break long unbreakable tokens (e.g. URLs, package names) so they wrap
|
|
1749
|
+
inside the bubble's max-width instead of overflowing horizontally. */
|
|
1750
|
+
overflow-wrap: break-word;
|
|
1748
1751
|
}
|
|
1749
1752
|
|
|
1750
1753
|
/* Fade-in animation for action buttons */
|
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
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Target resolution for the normalized, backend-neutral `target` field.
|
|
3
|
+
*
|
|
4
|
+
* `target` is a single string that selects which backend resource a widget
|
|
5
|
+
* talks to, optimized for a browser widget (always serializable, no live
|
|
6
|
+
* objects). Three shapes are supported:
|
|
7
|
+
*
|
|
8
|
+
* - Runtype TypeID (no prefix): `"agent_…"` / `"flow_…"` route to the
|
|
9
|
+
* Runtype agent/flow paths. The TypeID prefix is self-describing, so no
|
|
10
|
+
* wrapper is needed for the common case.
|
|
11
|
+
* - Provider-prefixed: `"<provider>:<id>"` is handed to the matching
|
|
12
|
+
* `targetProviders[provider]` resolver, which returns the dispatch payload
|
|
13
|
+
* fragment for that backend (e.g. `eve`, `langgraph`). `"runtype:…"` is a
|
|
14
|
+
* built-in that re-detects a TypeID.
|
|
15
|
+
* - Bare name: `"support"` requires a `targetProviders.default` resolver,
|
|
16
|
+
* otherwise it throws (a bare name is ambiguous without one).
|
|
17
|
+
*
|
|
18
|
+
* Resolvers are registered, not passed as the value, which keeps `target`
|
|
19
|
+
* itself a plain string that survives script-tag installs, `data-config`,
|
|
20
|
+
* persisted state, and codegen.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Resolver for a provider-prefixed (or default) target id. */
|
|
24
|
+
export type TargetResolver = (id: string) => { payload: Record<string, unknown> };
|
|
25
|
+
|
|
26
|
+
/** Normalized routing produced from a `target` string. */
|
|
27
|
+
export type ResolvedTarget =
|
|
28
|
+
| { kind: "agentId"; agentId: string }
|
|
29
|
+
| { kind: "flowId"; flowId: string }
|
|
30
|
+
| { kind: "payload"; payload: Record<string, unknown> };
|
|
31
|
+
|
|
32
|
+
const RUNTYPE_AGENT_PREFIX = "agent_";
|
|
33
|
+
const RUNTYPE_FLOW_PREFIX = "flow_";
|
|
34
|
+
|
|
35
|
+
function detectRuntypeTypeId(id: string): ResolvedTarget | null {
|
|
36
|
+
if (id.startsWith(RUNTYPE_AGENT_PREFIX)) return { kind: "agentId", agentId: id };
|
|
37
|
+
if (id.startsWith(RUNTYPE_FLOW_PREFIX)) return { kind: "flowId", flowId: id };
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve a `target` string into normalized routing. Pure and synchronous so
|
|
43
|
+
* it can run on the dispatch hot path and be unit-tested in isolation.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveTarget(
|
|
46
|
+
target: string,
|
|
47
|
+
targetProviders?: Record<string, TargetResolver>,
|
|
48
|
+
): ResolvedTarget {
|
|
49
|
+
const trimmed = target.trim();
|
|
50
|
+
if (!trimmed) {
|
|
51
|
+
throw new Error("[Persona] `target` is empty.");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const colon = trimmed.indexOf(":");
|
|
55
|
+
if (colon > 0) {
|
|
56
|
+
const prefix = trimmed.slice(0, colon);
|
|
57
|
+
const rest = trimmed.slice(colon + 1);
|
|
58
|
+
|
|
59
|
+
// Built-in: an explicit `runtype:` prefix wrapping a TypeID.
|
|
60
|
+
if (prefix === "runtype") {
|
|
61
|
+
const detected = detectRuntypeTypeId(rest);
|
|
62
|
+
if (detected) return detected;
|
|
63
|
+
throw new Error(
|
|
64
|
+
`[Persona] target "runtype:${rest}" is not a valid Runtype agent_/flow_ id.`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const resolver = targetProviders?.[prefix];
|
|
69
|
+
if (!resolver) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`[Persona] No target provider registered for "${prefix}". ` +
|
|
72
|
+
`Add a \`targetProviders.${prefix}\` resolver, or use a Runtype agent_/flow_ id.`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return { kind: "payload", payload: resolver(rest).payload };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// No prefix: a bare Runtype TypeID is self-describing.
|
|
79
|
+
const detected = detectRuntypeTypeId(trimmed);
|
|
80
|
+
if (detected) return detected;
|
|
81
|
+
|
|
82
|
+
// Bare, non-TypeID name: only resolvable via an explicit default resolver.
|
|
83
|
+
const fallback = targetProviders?.default;
|
|
84
|
+
if (fallback) return { kind: "payload", payload: fallback(trimmed).payload };
|
|
85
|
+
|
|
86
|
+
throw new Error(
|
|
87
|
+
`[Persona] target "${trimmed}" has no provider prefix and is not a Runtype agent_/flow_ id. ` +
|
|
88
|
+
`Use "<provider>:${trimmed}", a Runtype TypeID, or register a \`targetProviders.default\` resolver.`,
|
|
89
|
+
);
|
|
90
|
+
}
|