@ai-ecoverse/pi-audio 1.16.0 → 1.17.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 CHANGED
@@ -48,6 +48,8 @@ pi-audio does not apply that patch. Hosts such as slicc-agent record monkeypatch
48
48
 
49
49
  `mintRealtimeToken(model, constraints, options)` returns `{ value, expiresAt, model }` and calls `options.fetch` (or `globalThis.fetch`). OpenAI `POST /v1/realtime/client_secrets` sends a realtime session with instructions, voice, and tools. xAI `POST /v1/realtime/client_secrets` sends only `expires_after.seconds` of 300. Gemini `POST /v1beta/auth_tokens` locks `models/<id>`, instructions, voice, and tools in `liveConnectConstraints` and returns `name`, which the live adapter sends as `access_token`. ElevenLabs `GET /v1/convai/conversation/get-signed-url` takes `agent_id` (or `GET /conversation/token` when the only transport is WebRTC) and expires in 15 minutes. Fish `POST /v1/agent/sessions` sends `agent_id` plus `overrides.system_prompt` and `overrides.voice_id`. `value` is the LiveKit participant token. `expiresAt` is milliseconds. A numeric `expires_at` below `1e12` is unix seconds.
50
50
 
51
+ `@ai-ecoverse/pi-audio/elevenlabs-agents` registers `elevenlabs-agent`, a hosted cascade (`provisioning: "agent"`, `truncation: "server"`, `liveToolUpdate: false`). `provisionAgent` creates or updates the agent and reuses it when the spec hash matches. pi tools become client tools. A string `llm` is a vendor model name. A `CustomLlm` sets `llm` to `custom-llm` and sends `custom_llm.url`, `model_id`, `api_type`, and `request_headers` (ElevenLabs `api_key` is a workspace secret reference, not a raw token). `connectRealtime` uses a `RealtimeToken` from `mintRealtimeToken` (`get-signed-url`) when one is passed. Otherwise an API key provisions the agent and mints the signed URL. Silent `inject` sends `contextual_update` with `context_id` `pi-audio`. `close()` prices `audio.inputSeconds` from the wall-clock session at $0.08 per minute.
52
+
51
53
  `talkerTools` is `ask_agent` and `read`. pi-audio defines `askAgent` (a pi `Tool` whose parameters are `request` and optional `context`) and the `Thinker` interface. The host implements `Thinker` and supplies `read`. `bindThinker` runs direct tools with `directTimeoutMs` and truncates their text to `directMaxChars`. On a blocking `ask_agent` it answers with `ack` immediately and injects the result later. On an async call it `respond`s with the result and `options.result`. Progress is injected with `options.progress`. `tool_call_cancelled` aborts the thinker. The binding also yields `thinker_progress` / `thinker_result` captions and `thinking` / `tool` / `idle` phases.
52
54
 
53
55
  ## Development
@@ -0,0 +1,172 @@
1
+ import { blankUsage } from "../blank-usage.js";
2
+ import { decodeAudioBase64 } from "../convert.js";
3
+ function none() {
4
+ return { events: [], replies: [] };
5
+ }
6
+ function isRecord(value) {
7
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8
+ }
9
+ function text(value) {
10
+ return typeof value === "string" ? value : "";
11
+ }
12
+ function transcript(itemId, value, final) {
13
+ return { type: "output_transcript", itemId, text: value, final };
14
+ }
15
+ function done(itemId, interrupted) {
16
+ return { type: "response_done", itemId, interrupted, usage: blankUsage() };
17
+ }
18
+ function numbers(value) {
19
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "number")) return void 0;
20
+ return value;
21
+ }
22
+ function alignmentOf(value) {
23
+ if (!isRecord(value)) return void 0;
24
+ const startMs = numbers(value.char_start_times_ms);
25
+ const durationMs = numbers(value.char_durations_ms);
26
+ if (startMs === void 0 || durationMs === void 0) return void 0;
27
+ return { startMs, durationMs };
28
+ }
29
+ function pong(_turn, message) {
30
+ const nested = isRecord(message.ping_event) ? message.ping_event : void 0;
31
+ const raw = nested === void 0 ? message.event_id : nested.event_id;
32
+ const eventId = typeof raw === "number" ? raw : 0;
33
+ return { events: [], replies: [{ type: "pong", event_id: eventId }] };
34
+ }
35
+ function metadata(_turn, message) {
36
+ const event = isRecord(message.conversation_initiation_metadata_event) ? message.conversation_initiation_metadata_event : void 0;
37
+ const sessionId = text(event?.conversation_id);
38
+ return {
39
+ events: [
40
+ { type: "session_start", sessionId: sessionId.length > 0 ? sessionId : "conversation" }
41
+ ],
42
+ replies: []
43
+ };
44
+ }
45
+ function userTranscript(_turn, message) {
46
+ const event = isRecord(message.user_transcription_event) ? message.user_transcription_event : void 0;
47
+ return {
48
+ events: [
49
+ {
50
+ type: "input_transcript",
51
+ itemId: "user",
52
+ text: text(event?.user_transcript),
53
+ final: true
54
+ }
55
+ ],
56
+ replies: []
57
+ };
58
+ }
59
+ function chatPart(_turn, message) {
60
+ const part = isRecord(message.text_response_part) ? message.text_response_part : void 0;
61
+ if (part === void 0) return none();
62
+ const kind = text(part.type);
63
+ if (kind === "start" || kind.length === 0) return none();
64
+ const itemId = text(part.response_id);
65
+ return {
66
+ events: [transcript(itemId.length > 0 ? itemId : "agent", text(part.text), kind === "stop")],
67
+ replies: []
68
+ };
69
+ }
70
+ function agentResponse(_turn, message) {
71
+ const event = isRecord(message.agent_response_event) ? message.agent_response_event : void 0;
72
+ return { events: [transcript("agent", text(event?.agent_response), true)], replies: [] };
73
+ }
74
+ function correction(_turn, message) {
75
+ const event = isRecord(message.agent_response_correction_event) ? message.agent_response_correction_event : void 0;
76
+ return {
77
+ events: [transcript("agent", text(event?.corrected_agent_response), true)],
78
+ replies: []
79
+ };
80
+ }
81
+ function audio(turn, message, format) {
82
+ const event = isRecord(message.audio_event) ? message.audio_event : void 0;
83
+ const eventId = typeof event?.event_id === "number" ? event.event_id : 0;
84
+ if (turn.dropAudio || eventId <= turn.lastInterruptId) return none();
85
+ const alignment = alignmentOf(event?.alignment);
86
+ const chars = isRecord(event?.alignment) ? event.alignment.chars : void 0;
87
+ const end = Array.isArray(chars) ? chars.length : 0;
88
+ const itemId = String(eventId);
89
+ const first = alignment?.startMs[0];
90
+ const spoken = {
91
+ type: "output_audio",
92
+ itemId,
93
+ chunk: {
94
+ data: decodeAudioBase64(text(event?.audio_base_64)),
95
+ format,
96
+ timestamp: typeof first === "number" ? first : 0
97
+ }
98
+ };
99
+ if (alignment !== void 0) spoken.alignment = alignment;
100
+ if (end > 0) spoken.span = { itemId, start: 0, end };
101
+ return { events: [spoken], replies: [] };
102
+ }
103
+ function interruption(turn, message) {
104
+ const event = isRecord(message.interruption_event) ? message.interruption_event : void 0;
105
+ const eventId = typeof event?.event_id === "number" ? event.event_id : turn.lastInterruptId;
106
+ turn.lastInterruptId = eventId;
107
+ turn.vad = true;
108
+ return { events: [{ type: "input_speech_start" }, done(String(eventId), true)], replies: [] };
109
+ }
110
+ function vad(turn, message) {
111
+ const event = isRecord(message.vad_score_event) ? message.vad_score_event : void 0;
112
+ const score = typeof event?.vad_score === "number" ? event.vad_score : 0;
113
+ const speaking = score > 0.5;
114
+ if (!speaking) {
115
+ turn.vad = false;
116
+ return none();
117
+ }
118
+ if (turn.vad) return none();
119
+ turn.vad = true;
120
+ return { events: [{ type: "input_speech_start" }], replies: [] };
121
+ }
122
+ function toolCall(_turn, message) {
123
+ const event = isRecord(message.client_tool_call) ? message.client_tool_call : void 0;
124
+ const name = text(event?.tool_name);
125
+ const id = text(event?.tool_call_id);
126
+ if (name.length === 0 || id.length === 0) return none();
127
+ const parameters = isRecord(event?.parameters) ? event.parameters : {};
128
+ const call = {
129
+ type: "toolCall",
130
+ id,
131
+ name,
132
+ arguments: parameters
133
+ };
134
+ return { events: [{ type: "tool_call", call, blocking: true }], replies: [] };
135
+ }
136
+ function complete(turn, _message) {
137
+ turn.dropAudio = false;
138
+ return { events: [done("agent", false)], replies: [] };
139
+ }
140
+ function clientError(_turn, message) {
141
+ const event = isRecord(message.client_error_event) ? message.client_error_event : void 0;
142
+ const reason = text(event?.message);
143
+ return {
144
+ events: [{ type: "error", error: reason.length > 0 ? reason : "client error", fatal: false }],
145
+ replies: []
146
+ };
147
+ }
148
+ const handlers = {
149
+ ping: pong,
150
+ conversation_initiation_metadata: metadata,
151
+ user_transcript: userTranscript,
152
+ agent_chat_response_part: chatPart,
153
+ agent_response: agentResponse,
154
+ agent_response_correction: correction,
155
+ audio,
156
+ interruption,
157
+ vad_score: vad,
158
+ client_tool_call: toolCall,
159
+ agent_response_complete: complete,
160
+ client_error: clientError
161
+ };
162
+ function ingestElevenLabs(turn, message, format) {
163
+ const type = message.type;
164
+ if (typeof type !== "string") return none();
165
+ const handler = handlers[type];
166
+ if (handler === void 0) return none();
167
+ return handler(turn, message, format);
168
+ }
169
+ export {
170
+ ingestElevenLabs
171
+ };
172
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/elevenlabs-agents/events.ts"],
4
+ "sourcesContent": ["import type { JsonObject, ToolCall } from '@earendil-works/pi-ai';\nimport type { AudioFormat, CharAlignment } from '../audio.ts';\nimport { blankUsage } from '../blank-usage.ts';\nimport { decodeAudioBase64 } from '../convert.ts';\nimport type { RealtimeEvent } from '../realtime.ts';\n\nexport interface TalkTurn {\n vad: boolean;\n dropAudio: boolean;\n lastInterruptId: number;\n}\n\nexport interface Ingested {\n events: RealtimeEvent[];\n replies: JsonObject[];\n}\n\nfunction none(): Ingested {\n return { events: [], replies: [] };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value : '';\n}\n\nfunction transcript(itemId: string, value: string, final: boolean): RealtimeEvent {\n return { type: 'output_transcript', itemId, text: value, final };\n}\n\nfunction done(itemId: string, interrupted: boolean): RealtimeEvent {\n return { type: 'response_done', itemId, interrupted, usage: blankUsage() };\n}\n\nfunction numbers(value: unknown): number[] | undefined {\n if (!Array.isArray(value) || value.some((item) => typeof item !== 'number')) return undefined;\n return value as number[];\n}\n\nfunction alignmentOf(value: unknown): CharAlignment | undefined {\n if (!isRecord(value)) return undefined;\n const startMs = numbers(value.char_start_times_ms);\n const durationMs = numbers(value.char_durations_ms);\n if (startMs === undefined || durationMs === undefined) return undefined;\n return { startMs, durationMs };\n}\n\nfunction pong(_turn: TalkTurn, message: Record<string, unknown>): Ingested {\n const nested = isRecord(message.ping_event) ? message.ping_event : undefined;\n const raw = nested === undefined ? message.event_id : nested.event_id;\n const eventId = typeof raw === 'number' ? raw : 0;\n return { events: [], replies: [{ type: 'pong', event_id: eventId }] };\n}\n\nfunction metadata(_turn: TalkTurn, message: Record<string, unknown>): Ingested {\n const event = isRecord(message.conversation_initiation_metadata_event)\n ? message.conversation_initiation_metadata_event\n : undefined;\n const sessionId = text(event?.conversation_id);\n return {\n events: [\n { type: 'session_start', sessionId: sessionId.length > 0 ? sessionId : 'conversation' },\n ],\n replies: [],\n };\n}\n\nfunction userTranscript(_turn: TalkTurn, message: Record<string, unknown>): Ingested {\n const event = isRecord(message.user_transcription_event)\n ? message.user_transcription_event\n : undefined;\n return {\n events: [\n {\n type: 'input_transcript',\n itemId: 'user',\n text: text(event?.user_transcript),\n final: true,\n },\n ],\n replies: [],\n };\n}\n\nfunction chatPart(_turn: TalkTurn, message: Record<string, unknown>): Ingested {\n const part = isRecord(message.text_response_part) ? message.text_response_part : undefined;\n if (part === undefined) return none();\n const kind = text(part.type);\n if (kind === 'start' || kind.length === 0) return none();\n const itemId = text(part.response_id);\n return {\n events: [transcript(itemId.length > 0 ? itemId : 'agent', text(part.text), kind === 'stop')],\n replies: [],\n };\n}\n\nfunction agentResponse(_turn: TalkTurn, message: Record<string, unknown>): Ingested {\n const event = isRecord(message.agent_response_event) ? message.agent_response_event : undefined;\n return { events: [transcript('agent', text(event?.agent_response), true)], replies: [] };\n}\n\nfunction correction(_turn: TalkTurn, message: Record<string, unknown>): Ingested {\n const event = isRecord(message.agent_response_correction_event)\n ? message.agent_response_correction_event\n : undefined;\n return {\n events: [transcript('agent', text(event?.corrected_agent_response), true)],\n replies: [],\n };\n}\n\nfunction audio(turn: TalkTurn, message: Record<string, unknown>, format: AudioFormat): Ingested {\n const event = isRecord(message.audio_event) ? message.audio_event : undefined;\n const eventId = typeof event?.event_id === 'number' ? event.event_id : 0;\n if (turn.dropAudio || eventId <= turn.lastInterruptId) return none();\n const alignment = alignmentOf(event?.alignment);\n const chars = isRecord(event?.alignment) ? event.alignment.chars : undefined;\n const end = Array.isArray(chars) ? chars.length : 0;\n const itemId = String(eventId);\n const first = alignment?.startMs[0];\n const spoken: RealtimeEvent = {\n type: 'output_audio',\n itemId,\n chunk: {\n data: decodeAudioBase64(text(event?.audio_base_64)),\n format,\n timestamp: typeof first === 'number' ? first : 0,\n },\n };\n if (alignment !== undefined) spoken.alignment = alignment;\n if (end > 0) spoken.span = { itemId, start: 0, end };\n return { events: [spoken], replies: [] };\n}\n\nfunction interruption(turn: TalkTurn, message: Record<string, unknown>): Ingested {\n const event = isRecord(message.interruption_event) ? message.interruption_event : undefined;\n const eventId = typeof event?.event_id === 'number' ? event.event_id : turn.lastInterruptId;\n turn.lastInterruptId = eventId;\n turn.vad = true;\n return { events: [{ type: 'input_speech_start' }, done(String(eventId), true)], replies: [] };\n}\n\nfunction vad(turn: TalkTurn, message: Record<string, unknown>): Ingested {\n const event = isRecord(message.vad_score_event) ? message.vad_score_event : undefined;\n const score = typeof event?.vad_score === 'number' ? event.vad_score : 0;\n const speaking = score > 0.5;\n if (!speaking) {\n turn.vad = false;\n return none();\n }\n if (turn.vad) return none();\n turn.vad = true;\n return { events: [{ type: 'input_speech_start' }], replies: [] };\n}\n\nfunction toolCall(_turn: TalkTurn, message: Record<string, unknown>): Ingested {\n const event = isRecord(message.client_tool_call) ? message.client_tool_call : undefined;\n const name = text(event?.tool_name);\n const id = text(event?.tool_call_id);\n if (name.length === 0 || id.length === 0) return none();\n const parameters = isRecord(event?.parameters) ? event.parameters : {};\n const call: ToolCall = {\n type: 'toolCall',\n id,\n name,\n arguments: parameters as JsonObject,\n };\n return { events: [{ type: 'tool_call', call, blocking: true }], replies: [] };\n}\n\nfunction complete(turn: TalkTurn, _message: Record<string, unknown>): Ingested {\n turn.dropAudio = false;\n return { events: [done('agent', false)], replies: [] };\n}\n\nfunction clientError(_turn: TalkTurn, message: Record<string, unknown>): Ingested {\n const event = isRecord(message.client_error_event) ? message.client_error_event : undefined;\n const reason = text(event?.message);\n return {\n events: [{ type: 'error', error: reason.length > 0 ? reason : 'client error', fatal: false }],\n replies: [],\n };\n}\n\nconst handlers: Record<\n string,\n (turn: TalkTurn, message: Record<string, unknown>, format: AudioFormat) => Ingested\n> = {\n ping: pong,\n conversation_initiation_metadata: metadata,\n user_transcript: userTranscript,\n agent_chat_response_part: chatPart,\n agent_response: agentResponse,\n agent_response_correction: correction,\n audio,\n interruption,\n vad_score: vad,\n client_tool_call: toolCall,\n agent_response_complete: complete,\n client_error: clientError,\n};\n\nexport function ingestElevenLabs(\n turn: TalkTurn,\n message: Record<string, unknown>,\n format: AudioFormat\n): Ingested {\n const type = message.type;\n if (typeof type !== 'string') return none();\n const handler = handlers[type];\n if (handler === undefined) return none();\n return handler(turn, message, format);\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,58 @@
1
+ import { registerVoiceModel } from "../models.js";
2
+ import { registerRealtimeApiProvider } from "../registry.js";
3
+ import { connectElevenLabs } from "./session.js";
4
+ const voices = [
5
+ { id: "21m00Tcm4TlvDq8ikWAM", name: "Rachel" },
6
+ { id: "EXAVITQu4vr4xnSDxMaL", name: "Sarah" },
7
+ { id: "pNInz6obpgDQGcFmaJgB", name: "Adam" }
8
+ ];
9
+ function pcm16(sampleRate) {
10
+ return { encoding: "pcm16", sampleRate, channels: 1 };
11
+ }
12
+ const elevenLabsAgent = {
13
+ id: "elevenlabs-agent",
14
+ name: "ElevenLabs Agent",
15
+ api: "elevenlabs-agents",
16
+ provider: "elevenlabs",
17
+ baseUrl: "wss://api.elevenlabs.io/v1/convai/conversation",
18
+ type: "realtime",
19
+ transports: ["websocket"],
20
+ toolCalls: "blocking",
21
+ inputFormat: pcm16(16e3),
22
+ outputFormat: pcm16(16e3),
23
+ maxSessionMs: 6e5,
24
+ resumable: false,
25
+ ephemeralAuth: true,
26
+ voices,
27
+ cascade: { stt: "scribe_realtime", llm: "gemini-2.5-flash", tts: "eleven_flash_v2_5" },
28
+ provisioning: "agent",
29
+ delivery: { interrupt: "approximate", when_idle: "approximate", silent: "native" },
30
+ truncation: "server",
31
+ liveToolUpdate: false,
32
+ toolTimeoutMs: { min: 1e3, max: 12e4 },
33
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
34
+ audioCost: { perInputMinute: 0.08 }
35
+ };
36
+ function registerElevenLabsAgents(options = {}) {
37
+ const unregisterModel = registerVoiceModel(elevenLabsAgent);
38
+ const unregisterApi = registerRealtimeApiProvider({
39
+ api: "elevenlabs-agents",
40
+ connect: (model, context, connectOptions) => connectElevenLabs(model, context, connectOptions, options)
41
+ });
42
+ return () => {
43
+ unregisterModel();
44
+ unregisterApi();
45
+ };
46
+ }
47
+ import { hashHostedSpec, memoryHostedCache } from "../hosted-agent.js";
48
+ import { agentBody, httpsOrigin, provisionAgent } from "./provision.js";
49
+ export {
50
+ agentBody,
51
+ elevenLabsAgent,
52
+ hashHostedSpec,
53
+ httpsOrigin,
54
+ memoryHostedCache,
55
+ provisionAgent,
56
+ registerElevenLabsAgents
57
+ };
58
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/elevenlabs-agents/index.ts"],
4
+ "sourcesContent": ["import type { RealtimeModel } from '../models.ts';\nimport { registerVoiceModel } from '../models.ts';\nimport { registerRealtimeApiProvider } from '../registry.ts';\nimport type { ElevenLabsOptions } from './session.ts';\nimport { connectElevenLabs } from './session.ts';\n\nconst voices = [\n { id: '21m00Tcm4TlvDq8ikWAM', name: 'Rachel' },\n { id: 'EXAVITQu4vr4xnSDxMaL', name: 'Sarah' },\n { id: 'pNInz6obpgDQGcFmaJgB', name: 'Adam' },\n];\n\nfunction pcm16(sampleRate: number) {\n return { encoding: 'pcm16' as const, sampleRate, channels: 1 as const };\n}\n\nexport const elevenLabsAgent: RealtimeModel<'elevenlabs-agents'> = {\n id: 'elevenlabs-agent',\n name: 'ElevenLabs Agent',\n api: 'elevenlabs-agents',\n provider: 'elevenlabs',\n baseUrl: 'wss://api.elevenlabs.io/v1/convai/conversation',\n type: 'realtime',\n transports: ['websocket'],\n toolCalls: 'blocking',\n inputFormat: pcm16(16000),\n outputFormat: pcm16(16000),\n maxSessionMs: 600_000,\n resumable: false,\n ephemeralAuth: true,\n voices,\n cascade: { stt: 'scribe_realtime', llm: 'gemini-2.5-flash', tts: 'eleven_flash_v2_5' },\n provisioning: 'agent',\n delivery: { interrupt: 'approximate', when_idle: 'approximate', silent: 'native' },\n truncation: 'server',\n liveToolUpdate: false,\n toolTimeoutMs: { min: 1000, max: 120_000 },\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n audioCost: { perInputMinute: 0.08 },\n};\n\nexport function registerElevenLabsAgents(options: ElevenLabsOptions = {}): () => void {\n const unregisterModel = registerVoiceModel(elevenLabsAgent);\n const unregisterApi = registerRealtimeApiProvider({\n api: 'elevenlabs-agents',\n connect: (model, context, connectOptions) =>\n connectElevenLabs(model, context, connectOptions, options),\n });\n return () => {\n unregisterModel();\n unregisterApi();\n };\n}\n\nexport type { CustomLlm, HostedAgent, HostedAgentSpec, HostedCache } from '../hosted-agent.ts';\nexport { hashHostedSpec, memoryHostedCache } from '../hosted-agent.ts';\nexport type { ProvisionOptions } from './provision.ts';\nexport { agentBody, httpsOrigin, provisionAgent } from './provision.ts';\nexport type { ElevenLabsOptions, VoiceWireFactory } from './session.ts';\n"],
5
+ "mappings": "AACA,SAAS,0BAA0B;AACnC,SAAS,mCAAmC;AAE5C,SAAS,yBAAyB;AAElC,MAAM,SAAS;AAAA,EACb,EAAE,IAAI,wBAAwB,MAAM,SAAS;AAAA,EAC7C,EAAE,IAAI,wBAAwB,MAAM,QAAQ;AAAA,EAC5C,EAAE,IAAI,wBAAwB,MAAM,OAAO;AAC7C;AAEA,SAAS,MAAM,YAAoB;AACjC,SAAO,EAAE,UAAU,SAAkB,YAAY,UAAU,EAAW;AACxE;AAEO,MAAM,kBAAsD;AAAA,EACjE,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,KAAK;AAAA,EACL,UAAU;AAAA,EACV,SAAS;AAAA,EACT,MAAM;AAAA,EACN,YAAY,CAAC,WAAW;AAAA,EACxB,WAAW;AAAA,EACX,aAAa,MAAM,IAAK;AAAA,EACxB,cAAc,MAAM,IAAK;AAAA,EACzB,cAAc;AAAA,EACd,WAAW;AAAA,EACX,eAAe;AAAA,EACf;AAAA,EACA,SAAS,EAAE,KAAK,mBAAmB,KAAK,oBAAoB,KAAK,oBAAoB;AAAA,EACrF,cAAc;AAAA,EACd,UAAU,EAAE,WAAW,eAAe,WAAW,eAAe,QAAQ,SAAS;AAAA,EACjF,YAAY;AAAA,EACZ,gBAAgB;AAAA,EAChB,eAAe,EAAE,KAAK,KAAM,KAAK,KAAQ;AAAA,EACzC,MAAM,EAAE,OAAO,GAAG,QAAQ,GAAG,WAAW,GAAG,YAAY,EAAE;AAAA,EACzD,WAAW,EAAE,gBAAgB,KAAK;AACpC;AAEO,SAAS,yBAAyB,UAA6B,CAAC,GAAe;AACpF,QAAM,kBAAkB,mBAAmB,eAAe;AAC1D,QAAM,gBAAgB,4BAA4B;AAAA,IAChD,KAAK;AAAA,IACL,SAAS,CAAC,OAAO,SAAS,mBACxB,kBAAkB,OAAO,SAAS,gBAAgB,OAAO;AAAA,EAC7D,CAAC;AACD,SAAO,MAAM;AACX,oBAAgB;AAChB,kBAAc;AAAA,EAChB;AACF;AAGA,SAAS,gBAAgB,yBAAyB;AAElD,SAAS,WAAW,aAAa,sBAAsB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,167 @@
1
+ import { VoiceRequestError } from "../errors.js";
2
+ import {
3
+ hashHostedSpec,
4
+ hostedCacheKey,
5
+ memoryHostedCache
6
+ } from "../hosted-agent.js";
7
+ const defaultCache = memoryHostedCache();
8
+ const clientEvents = [
9
+ "conversation_initiation_metadata",
10
+ "ping",
11
+ "audio",
12
+ "interruption",
13
+ "user_transcript",
14
+ "agent_response",
15
+ "agent_response_correction",
16
+ "client_tool_call",
17
+ "vad_score",
18
+ "agent_chat_response_part",
19
+ "agent_response_complete"
20
+ ];
21
+ function isRecord(value) {
22
+ return typeof value === "object" && value !== null && !Array.isArray(value);
23
+ }
24
+ function asVoice(error) {
25
+ if (error instanceof VoiceRequestError) return error;
26
+ if (error instanceof Error && error.name === "AbortError") {
27
+ return new VoiceRequestError("aborted", "session aborted");
28
+ }
29
+ if (error instanceof Error) return new VoiceRequestError("error", error.message);
30
+ return new VoiceRequestError("error", String(error));
31
+ }
32
+ function httpsOrigin(baseUrl) {
33
+ const url = new URL(baseUrl);
34
+ let protocol = url.protocol;
35
+ if (protocol === "wss:") protocol = "https:";
36
+ if (protocol === "ws:") protocol = "http:";
37
+ return `${protocol}//${url.host}`;
38
+ }
39
+ function mergedHeaders(options, defaults) {
40
+ const headers = { ...defaults };
41
+ const extra = options.headers;
42
+ if (extra === void 0) return headers;
43
+ for (const [key, value] of Object.entries(extra)) {
44
+ if (value === null) delete headers[key];
45
+ else headers[key] = value;
46
+ }
47
+ return headers;
48
+ }
49
+ function apiKey(options) {
50
+ const key = options.apiKey;
51
+ if (typeof key === "string" && key.length > 0) return key;
52
+ throw new VoiceRequestError("error", "missing realtime credential");
53
+ }
54
+ function timeoutSeconds(timeoutMs) {
55
+ if (timeoutMs === void 0) return 20;
56
+ return Math.min(120, Math.max(1, Math.round(timeoutMs / 1e3)));
57
+ }
58
+ function clientTool(tool, timeoutMs) {
59
+ return {
60
+ type: "client",
61
+ name: tool.name,
62
+ description: tool.description,
63
+ expects_response: true,
64
+ response_timeout_secs: timeoutSeconds(timeoutMs),
65
+ parameters: tool.parameters
66
+ };
67
+ }
68
+ function customLlmFields(llm) {
69
+ const custom = {
70
+ url: llm.baseUrl,
71
+ model_id: llm.id,
72
+ api_type: llm.api === "openai-responses" ? "responses" : "chat_completions"
73
+ };
74
+ const headers = llm.headers;
75
+ if (headers !== void 0 && Object.keys(headers).length > 0) custom.request_headers = headers;
76
+ return { llm: "custom-llm", custom_llm: custom };
77
+ }
78
+ function promptFields(spec) {
79
+ const llm = typeof spec.llm === "string" ? { llm: spec.llm } : customLlmFields(spec.llm);
80
+ return {
81
+ prompt: spec.instructions,
82
+ tools: spec.tools.map((tool) => clientTool(tool, spec.clientToolTimeoutMs)),
83
+ ...llm
84
+ };
85
+ }
86
+ function agentBody(spec) {
87
+ const agent = { prompt: promptFields(spec) };
88
+ if (spec.firstMessage !== void 0) agent.first_message = spec.firstMessage;
89
+ return {
90
+ name: spec.name,
91
+ conversation_config: {
92
+ agent,
93
+ tts: { voice_id: spec.voice },
94
+ conversation: { client_events: clientEvents }
95
+ },
96
+ platform_settings: {
97
+ overrides: {
98
+ conversation_config_override: {
99
+ agent: { prompt: { prompt: true }, first_message: true },
100
+ tts: { voice_id: true }
101
+ },
102
+ custom_llm_extra_body: true
103
+ }
104
+ }
105
+ };
106
+ }
107
+ async function requestJson(fetchImpl, url, init) {
108
+ let response;
109
+ try {
110
+ response = await fetchImpl(url, init);
111
+ } catch (error) {
112
+ throw asVoice(error);
113
+ }
114
+ if (!response.ok) {
115
+ throw new VoiceRequestError("error", `agent request failed (${response.status})`);
116
+ }
117
+ let json;
118
+ try {
119
+ json = await response.json();
120
+ } catch (error) {
121
+ throw asVoice(error);
122
+ }
123
+ if (!isRecord(json)) throw new VoiceRequestError("error", "agent response was not an object");
124
+ return json;
125
+ }
126
+ function agentIdFrom(json, fallback) {
127
+ const id = json.agent_id;
128
+ if (typeof id === "string" && id.length > 0) return id;
129
+ if (fallback !== void 0) return fallback;
130
+ throw new VoiceRequestError("error", "agent response had no agent_id");
131
+ }
132
+ async function provisionAgent(model, spec, options = {}) {
133
+ if (options.signal?.aborted === true) throw new VoiceRequestError("aborted", "session aborted");
134
+ const cache = options.cache ?? defaultCache;
135
+ const now = options.now ?? Date.now;
136
+ const key = hostedCacheKey(model.provider, spec.name);
137
+ const hash = hashHostedSpec(spec);
138
+ const cached = cache.get(key);
139
+ if (cached !== void 0 && cached.specHash === hash) return cached;
140
+ const existing = cached?.agentId ?? options.agentId;
141
+ const origin = httpsOrigin(model.baseUrl);
142
+ const url = existing === void 0 ? `${origin}/v1/convai/agents/create` : `${origin}/v1/convai/agents/${encodeURIComponent(existing)}`;
143
+ const headers = mergedHeaders(options, {
144
+ "Content-Type": "application/json",
145
+ "xi-api-key": apiKey(options)
146
+ });
147
+ const fetchImpl = options.fetch ?? globalThis.fetch;
148
+ const json = await requestJson(fetchImpl, url, {
149
+ method: existing === void 0 ? "POST" : "PATCH",
150
+ headers,
151
+ body: JSON.stringify(agentBody(spec)),
152
+ signal: options.signal
153
+ });
154
+ const agent = {
155
+ agentId: agentIdFrom(json, existing),
156
+ specHash: hash,
157
+ publishedAt: now()
158
+ };
159
+ cache.set(key, agent);
160
+ return agent;
161
+ }
162
+ export {
163
+ agentBody,
164
+ httpsOrigin,
165
+ provisionAgent
166
+ };
167
+ //# sourceMappingURL=provision.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/elevenlabs-agents/provision.ts"],
4
+ "sourcesContent": ["import type { JsonObject, ProviderRequestOptions, Tool } from '@earendil-works/pi-ai';\nimport { VoiceRequestError } from '../errors.ts';\nimport {\n type CustomLlm,\n type HostedAgent,\n type HostedAgentSpec,\n type HostedCache,\n hashHostedSpec,\n hostedCacheKey,\n memoryHostedCache,\n} from '../hosted-agent.ts';\nimport type { RealtimeModel } from '../models.ts';\n\nexport interface ProvisionOptions extends ProviderRequestOptions {\n agentId?: string;\n now?: () => number;\n cache?: HostedCache;\n}\n\nconst defaultCache = memoryHostedCache();\n\nconst clientEvents = [\n 'conversation_initiation_metadata',\n 'ping',\n 'audio',\n 'interruption',\n 'user_transcript',\n 'agent_response',\n 'agent_response_correction',\n 'client_tool_call',\n 'vad_score',\n 'agent_chat_response_part',\n 'agent_response_complete',\n];\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction asVoice(error: unknown): VoiceRequestError {\n if (error instanceof VoiceRequestError) return error;\n if (error instanceof Error && error.name === 'AbortError') {\n return new VoiceRequestError('aborted', 'session aborted');\n }\n if (error instanceof Error) return new VoiceRequestError('error', error.message);\n return new VoiceRequestError('error', String(error));\n}\n\nexport function httpsOrigin(baseUrl: string): string {\n const url = new URL(baseUrl);\n let protocol = url.protocol;\n if (protocol === 'wss:') protocol = 'https:';\n if (protocol === 'ws:') protocol = 'http:';\n return `${protocol}//${url.host}`;\n}\n\nfunction mergedHeaders(\n options: ProvisionOptions,\n defaults: Record<string, string>\n): Record<string, string> {\n const headers = { ...defaults };\n const extra = options.headers;\n if (extra === undefined) return headers;\n for (const [key, value] of Object.entries(extra)) {\n if (value === null) delete headers[key];\n else headers[key] = value;\n }\n return headers;\n}\n\nfunction apiKey(options: ProvisionOptions): string {\n const key = options.apiKey;\n if (typeof key === 'string' && key.length > 0) return key;\n throw new VoiceRequestError('error', 'missing realtime credential');\n}\n\nfunction timeoutSeconds(timeoutMs: number | undefined): number {\n if (timeoutMs === undefined) return 20;\n return Math.min(120, Math.max(1, Math.round(timeoutMs / 1000)));\n}\n\nfunction clientTool(tool: Tool, timeoutMs: number | undefined): JsonObject {\n return {\n type: 'client',\n name: tool.name,\n description: tool.description,\n expects_response: true,\n response_timeout_secs: timeoutSeconds(timeoutMs),\n parameters: tool.parameters as unknown as JsonObject,\n };\n}\n\nfunction customLlmFields(llm: CustomLlm): JsonObject {\n const custom: JsonObject = {\n url: llm.baseUrl,\n model_id: llm.id,\n api_type: llm.api === 'openai-responses' ? 'responses' : 'chat_completions',\n };\n const headers = llm.headers;\n if (headers !== undefined && Object.keys(headers).length > 0) custom.request_headers = headers;\n return { llm: 'custom-llm', custom_llm: custom };\n}\n\nfunction promptFields(spec: HostedAgentSpec): JsonObject {\n const llm = typeof spec.llm === 'string' ? { llm: spec.llm } : customLlmFields(spec.llm);\n return {\n prompt: spec.instructions,\n tools: spec.tools.map((tool) => clientTool(tool, spec.clientToolTimeoutMs)),\n ...llm,\n };\n}\n\nexport function agentBody(spec: HostedAgentSpec): JsonObject {\n const agent: JsonObject = { prompt: promptFields(spec) };\n if (spec.firstMessage !== undefined) agent.first_message = spec.firstMessage;\n return {\n name: spec.name,\n conversation_config: {\n agent,\n tts: { voice_id: spec.voice },\n conversation: { client_events: clientEvents },\n },\n platform_settings: {\n overrides: {\n conversation_config_override: {\n agent: { prompt: { prompt: true }, first_message: true },\n tts: { voice_id: true },\n },\n custom_llm_extra_body: true,\n },\n },\n };\n}\n\nasync function requestJson(\n fetchImpl: typeof fetch,\n url: string,\n init: RequestInit\n): Promise<Record<string, unknown>> {\n let response: Response;\n try {\n response = await fetchImpl(url, init);\n } catch (error) {\n throw asVoice(error);\n }\n if (!response.ok) {\n throw new VoiceRequestError('error', `agent request failed (${response.status})`);\n }\n let json: unknown;\n try {\n json = await response.json();\n } catch (error) {\n throw asVoice(error);\n }\n if (!isRecord(json)) throw new VoiceRequestError('error', 'agent response was not an object');\n return json;\n}\n\nfunction agentIdFrom(json: Record<string, unknown>, fallback: string | undefined): string {\n const id = json.agent_id;\n if (typeof id === 'string' && id.length > 0) return id;\n if (fallback !== undefined) return fallback;\n throw new VoiceRequestError('error', 'agent response had no agent_id');\n}\n\nexport async function provisionAgent(\n model: RealtimeModel<string>,\n spec: HostedAgentSpec,\n options: ProvisionOptions = {}\n): Promise<HostedAgent> {\n if (options.signal?.aborted === true) throw new VoiceRequestError('aborted', 'session aborted');\n const cache = options.cache ?? defaultCache;\n const now = options.now ?? Date.now;\n const key = hostedCacheKey(model.provider, spec.name);\n const hash = hashHostedSpec(spec);\n const cached = cache.get(key);\n if (cached !== undefined && cached.specHash === hash) return cached;\n const existing = cached?.agentId ?? options.agentId;\n const origin = httpsOrigin(model.baseUrl);\n const url =\n existing === undefined\n ? `${origin}/v1/convai/agents/create`\n : `${origin}/v1/convai/agents/${encodeURIComponent(existing)}`;\n const headers = mergedHeaders(options, {\n 'Content-Type': 'application/json',\n 'xi-api-key': apiKey(options),\n });\n const fetchImpl = options.fetch ?? globalThis.fetch;\n const json = await requestJson(fetchImpl, url, {\n method: existing === undefined ? 'POST' : 'PATCH',\n headers,\n body: JSON.stringify(agentBody(spec)),\n signal: options.signal,\n });\n const agent: HostedAgent = {\n agentId: agentIdFrom(json, existing),\n specHash: hash,\n publishedAt: now(),\n };\n cache.set(key, agent);\n return agent;\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,272 @@
1
+ import { encodeAudioBase64 } from "../convert.js";
2
+ import { VoiceRequestError } from "../errors.js";
3
+ import {
4
+ openVoiceSocket
5
+ } from "../openai-realtime/socket.js";
6
+ import { pushQueue } from "../queue.js";
7
+ import { planTruncation, RealtimeLedger, realtimeSocket } from "../realtime.js";
8
+ import { mintRealtimeToken } from "../tokens.js";
9
+ import { priceAudio } from "../usage.js";
10
+ import { ingestElevenLabs } from "./events.js";
11
+ import { provisionAgent } from "./provision.js";
12
+ function isRecord(value) {
13
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14
+ }
15
+ function resultText(result) {
16
+ const parts = [];
17
+ for (const part of result.content) {
18
+ if (part.type === "text") parts.push(part.text);
19
+ }
20
+ return parts.join("\n");
21
+ }
22
+ function injectMessage(value, delivery) {
23
+ if (delivery === "silent") {
24
+ return { type: "contextual_update", text: value, context_id: "pi-audio" };
25
+ }
26
+ const text = delivery === "when_idle" ? `[agent update] ${value}` : value;
27
+ return { type: "user_message", text };
28
+ }
29
+ function toolResult(callId, result) {
30
+ return {
31
+ type: "client_tool_result",
32
+ tool_call_id: callId,
33
+ result: resultText(result),
34
+ is_error: result.isError
35
+ };
36
+ }
37
+ function talkerLlm(model) {
38
+ const cascade = model.cascade;
39
+ if (cascade === void 0) return "gemini-2.5-flash";
40
+ return cascade.llm;
41
+ }
42
+ function specFromContext(model, context) {
43
+ return {
44
+ name: model.name,
45
+ instructions: context.instructions,
46
+ tools: context.tools,
47
+ voice: context.voice,
48
+ llm: talkerLlm(model)
49
+ };
50
+ }
51
+ function hasKey(options) {
52
+ return typeof options?.apiKey === "string" && options.apiKey.length > 0;
53
+ }
54
+ function agentFromToken(value) {
55
+ if (!value.startsWith("ws")) return void 0;
56
+ const id = new URL(value).searchParams.get("agent_id");
57
+ return id === null ? void 0 : id;
58
+ }
59
+ function conversationUrl(model, agentId, token) {
60
+ const url = new URL(model.baseUrl);
61
+ url.searchParams.set("agent_id", agentId);
62
+ if (token !== void 0) url.searchParams.set("token", token);
63
+ return url.toString();
64
+ }
65
+ function socketHeaders(options) {
66
+ const headers = {};
67
+ const extra = options?.headers;
68
+ if (extra === void 0) return headers;
69
+ for (const [key, value] of Object.entries(extra)) {
70
+ if (value === null) delete headers[key];
71
+ else headers[key] = value;
72
+ }
73
+ return headers;
74
+ }
75
+ function placeholder(agentId) {
76
+ return { agentId, specHash: "", publishedAt: 0 };
77
+ }
78
+ async function ensureAgent(model, context, options, settings) {
79
+ const token = options?.token?.value;
80
+ if (typeof token === "string" && token.length > 0) {
81
+ return placeholder(agentFromToken(token) ?? settings.agentId ?? model.id);
82
+ }
83
+ if (!hasKey(options)) return placeholder(settings.agentId ?? model.id);
84
+ const provision = {
85
+ ...options,
86
+ agentId: settings.agentId,
87
+ cache: settings.cache,
88
+ now: settings.now
89
+ };
90
+ return provisionAgent(model, specFromContext(model, context), provision);
91
+ }
92
+ async function socketUrl(model, agentId, context, options) {
93
+ const token = options?.token?.value;
94
+ if (typeof token === "string" && token.startsWith("ws")) return token;
95
+ if (typeof token === "string" && token.length > 0) return conversationUrl(model, agentId, token);
96
+ if (!hasKey(options)) return conversationUrl(model, agentId);
97
+ const minted = await mintRealtimeToken(
98
+ model,
99
+ {
100
+ agentId,
101
+ instructions: context.instructions,
102
+ voice: context.voice,
103
+ tools: context.tools
104
+ },
105
+ options
106
+ );
107
+ return minted.value;
108
+ }
109
+ function initiation(context, firstMessage) {
110
+ const agent = { prompt: { prompt: context.instructions } };
111
+ if (firstMessage !== void 0) agent.first_message = firstMessage;
112
+ return {
113
+ type: "conversation_initiation_client_data",
114
+ conversation_config_override: { agent, tts: { voice_id: context.voice } },
115
+ dynamic_variables: {},
116
+ custom_llm_extra_body: {}
117
+ };
118
+ }
119
+ class ElevenLabsSession {
120
+ model;
121
+ #context;
122
+ #wire;
123
+ #now;
124
+ #started;
125
+ #ledger = new RealtimeLedger();
126
+ #events = pushQueue();
127
+ #turn = { vad: false, dropAudio: false, lastInterruptId: -1 };
128
+ #closed = false;
129
+ #usage;
130
+ constructor(model, context, wire, now) {
131
+ this.model = model;
132
+ this.#context = context;
133
+ this.#wire = wire;
134
+ this.#now = now;
135
+ this.#started = now();
136
+ }
137
+ start(firstMessage) {
138
+ this.#send(initiation(this.#context, firstMessage));
139
+ void this.#read();
140
+ }
141
+ #assertOpen() {
142
+ if (this.#closed) throw new VoiceRequestError("error", "session is closed");
143
+ }
144
+ #send(event) {
145
+ this.#wire.send(JSON.stringify(event));
146
+ }
147
+ #emit(event) {
148
+ if (this.#closed) return;
149
+ this.#ledger.observe(event);
150
+ this.#events.push(event);
151
+ }
152
+ appendAudio(chunk) {
153
+ this.#assertOpen();
154
+ this.#ledger.addInput(chunk);
155
+ this.#send({ user_audio_chunk: encodeAudioBase64(chunk.data) });
156
+ }
157
+ sendText(value) {
158
+ this.#assertOpen();
159
+ this.#ledger.addText();
160
+ this.#send({ type: "user_message", text: value });
161
+ }
162
+ inject(value, delivery) {
163
+ this.#assertOpen();
164
+ this.#ledger.submit(this.model.delivery, delivery, () => {
165
+ this.#assertOpen();
166
+ this.#ledger.addText();
167
+ this.#send(injectMessage(value, delivery));
168
+ });
169
+ }
170
+ respond(callId, result, delivery) {
171
+ this.#assertOpen();
172
+ const send = () => {
173
+ this.#send(toolResult(callId, result));
174
+ };
175
+ if (delivery === void 0) {
176
+ send();
177
+ return;
178
+ }
179
+ this.#ledger.submit(this.model.delivery, delivery, () => {
180
+ this.#assertOpen();
181
+ send();
182
+ });
183
+ }
184
+ interrupt(heard) {
185
+ this.#assertOpen();
186
+ planTruncation(this.model.truncation, heard);
187
+ this.#turn.dropAudio = true;
188
+ this.#ledger.settle();
189
+ }
190
+ update(patch) {
191
+ this.#assertOpen();
192
+ if (patch.tools !== void 0 && this.model.liveToolUpdate === false) {
193
+ throw new VoiceRequestError("error", "live tool update is unsupported");
194
+ }
195
+ if (patch.tools !== void 0) this.#context.tools = patch.tools;
196
+ if (patch.instructions !== void 0) {
197
+ this.#context.instructions = patch.instructions;
198
+ this.#send({
199
+ type: "contextual_update",
200
+ text: patch.instructions,
201
+ context_id: "pi-audio"
202
+ });
203
+ }
204
+ if (patch.voice !== void 0) this.#context.voice = patch.voice;
205
+ }
206
+ async close() {
207
+ if (this.#usage !== void 0) return { usage: this.#usage };
208
+ this.#ledger.settle();
209
+ this.#closed = true;
210
+ this.#wire.close();
211
+ this.#events.end();
212
+ const inputSeconds = Math.max(0, this.#now() - this.#started) / 1e3;
213
+ this.#usage = priceAudio(this.model.audioCost, {
214
+ inputSeconds,
215
+ outputSeconds: this.#ledger.outputSeconds,
216
+ textMessages: this.#ledger.textMessages
217
+ });
218
+ return { usage: this.#usage };
219
+ }
220
+ [Symbol.asyncIterator]() {
221
+ return this.#events.iterable[Symbol.asyncIterator]();
222
+ }
223
+ #onRaw(raw) {
224
+ let parsed;
225
+ try {
226
+ parsed = JSON.parse(raw);
227
+ } catch {
228
+ this.#emit({ type: "error", error: "invalid realtime event", fatal: false });
229
+ return;
230
+ }
231
+ if (!isRecord(parsed)) return;
232
+ const ingested = ingestElevenLabs(this.#turn, parsed, this.model.outputFormat);
233
+ for (const reply of ingested.replies) this.#send(reply);
234
+ for (const event of ingested.events) this.#emit(event);
235
+ }
236
+ #fail(error) {
237
+ const message = error instanceof Error ? error.message : String(error);
238
+ this.#emit({ type: "error", error: message, fatal: true });
239
+ }
240
+ async #read() {
241
+ try {
242
+ for await (const raw of this.#wire.incoming) this.#onRaw(raw);
243
+ } catch (error) {
244
+ this.#fail(error);
245
+ } finally {
246
+ this.#closed = true;
247
+ this.#events.end();
248
+ }
249
+ }
250
+ }
251
+ async function connectElevenLabs(model, context, options, settings = {}) {
252
+ if (options?.signal?.aborted === true) throw new VoiceRequestError("aborted", "session aborted");
253
+ const agent = await ensureAgent(model, context, options, settings);
254
+ const url = await socketUrl(model, agent.agentId, context, options);
255
+ const routed = realtimeSocket(url, options, []);
256
+ const target = {
257
+ url: routed.url,
258
+ protocols: routed.protocols,
259
+ headers: socketHeaders(options)
260
+ };
261
+ const open = settings.wire !== void 0 ? settings.wire : (wireTarget) => openVoiceSocket(wireTarget, settings.socket);
262
+ const now = settings.now !== void 0 ? settings.now : Date.now;
263
+ const session = new ElevenLabsSession(model, context, open(target), now);
264
+ session.start(settings.firstMessage);
265
+ return session;
266
+ }
267
+ export {
268
+ connectElevenLabs,
269
+ specFromContext,
270
+ talkerLlm
271
+ };
272
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/elevenlabs-agents/session.ts"],
4
+ "sourcesContent": ["import type { JsonObject, ToolResultMessage, Usage } from '@earendil-works/pi-ai';\nimport type { AudioChunk } from '../audio.ts';\nimport { encodeAudioBase64 } from '../convert.ts';\nimport { VoiceRequestError } from '../errors.ts';\nimport type { HostedAgent, HostedAgentSpec, HostedCache } from '../hosted-agent.ts';\nimport type { Delivery, RealtimeModel } from '../models.ts';\nimport {\n openVoiceSocket,\n type VoiceSocketConstructor,\n type VoiceWire,\n type VoiceWireTarget,\n} from '../openai-realtime/socket.ts';\nimport { pushQueue } from '../queue.ts';\nimport type {\n RealtimeContext,\n RealtimeEvent,\n RealtimeOptions,\n RealtimeSession,\n} from '../realtime.ts';\nimport { planTruncation, RealtimeLedger, realtimeSocket } from '../realtime.ts';\nimport { mintRealtimeToken } from '../tokens.ts';\nimport { priceAudio } from '../usage.ts';\nimport { ingestElevenLabs, type TalkTurn } from './events.ts';\nimport { type ProvisionOptions, provisionAgent } from './provision.ts';\n\nexport type VoiceWireFactory = (target: VoiceWireTarget) => VoiceWire;\n\nexport interface ElevenLabsOptions {\n wire?: VoiceWireFactory;\n socket?: VoiceSocketConstructor;\n cache?: HostedCache;\n agentId?: string;\n now?: () => number;\n firstMessage?: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction resultText(result: ToolResultMessage): string {\n const parts: string[] = [];\n for (const part of result.content) {\n if (part.type === 'text') parts.push(part.text);\n }\n return parts.join('\\n');\n}\n\nfunction injectMessage(value: string, delivery: Delivery): JsonObject {\n if (delivery === 'silent') {\n return { type: 'contextual_update', text: value, context_id: 'pi-audio' };\n }\n const text = delivery === 'when_idle' ? `[agent update] ${value}` : value;\n return { type: 'user_message', text };\n}\n\nfunction toolResult(callId: string, result: ToolResultMessage): JsonObject {\n return {\n type: 'client_tool_result',\n tool_call_id: callId,\n result: resultText(result),\n is_error: result.isError,\n };\n}\n\nexport function talkerLlm(model: RealtimeModel<string>): string {\n const cascade = model.cascade;\n if (cascade === undefined) return 'gemini-2.5-flash';\n return cascade.llm;\n}\n\nexport function specFromContext(\n model: RealtimeModel<string>,\n context: RealtimeContext\n): HostedAgentSpec {\n return {\n name: model.name,\n instructions: context.instructions,\n tools: context.tools,\n voice: context.voice,\n llm: talkerLlm(model),\n };\n}\n\nfunction hasKey(options: RealtimeOptions | undefined): boolean {\n return typeof options?.apiKey === 'string' && options.apiKey.length > 0;\n}\n\nfunction agentFromToken(value: string): string | undefined {\n if (!value.startsWith('ws')) return undefined;\n const id = new URL(value).searchParams.get('agent_id');\n return id === null ? undefined : id;\n}\n\nfunction conversationUrl(model: RealtimeModel<string>, agentId: string, token?: string): string {\n const url = new URL(model.baseUrl);\n url.searchParams.set('agent_id', agentId);\n if (token !== undefined) url.searchParams.set('token', token);\n return url.toString();\n}\n\nfunction socketHeaders(options: RealtimeOptions | undefined): Record<string, string> {\n const headers: Record<string, string> = {};\n const extra = options?.headers;\n if (extra === undefined) return headers;\n for (const [key, value] of Object.entries(extra)) {\n if (value === null) delete headers[key];\n else headers[key] = value;\n }\n return headers;\n}\n\nfunction placeholder(agentId: string): HostedAgent {\n return { agentId, specHash: '', publishedAt: 0 };\n}\n\nasync function ensureAgent(\n model: RealtimeModel<string>,\n context: RealtimeContext,\n options: RealtimeOptions | undefined,\n settings: ElevenLabsOptions\n): Promise<HostedAgent> {\n const token = options?.token?.value;\n if (typeof token === 'string' && token.length > 0) {\n return placeholder(agentFromToken(token) ?? settings.agentId ?? model.id);\n }\n if (!hasKey(options)) return placeholder(settings.agentId ?? model.id);\n const provision: ProvisionOptions = {\n ...options,\n agentId: settings.agentId,\n cache: settings.cache,\n now: settings.now,\n };\n return provisionAgent(model, specFromContext(model, context), provision);\n}\n\nasync function socketUrl(\n model: RealtimeModel<string>,\n agentId: string,\n context: RealtimeContext,\n options: RealtimeOptions | undefined\n): Promise<string> {\n const token = options?.token?.value;\n if (typeof token === 'string' && token.startsWith('ws')) return token;\n if (typeof token === 'string' && token.length > 0) return conversationUrl(model, agentId, token);\n if (!hasKey(options)) return conversationUrl(model, agentId);\n const minted = await mintRealtimeToken(\n model,\n {\n agentId,\n instructions: context.instructions,\n voice: context.voice,\n tools: context.tools,\n },\n options\n );\n return minted.value;\n}\n\nfunction initiation(context: RealtimeContext, firstMessage: string | undefined): JsonObject {\n const agent: JsonObject = { prompt: { prompt: context.instructions } };\n if (firstMessage !== undefined) agent.first_message = firstMessage;\n return {\n type: 'conversation_initiation_client_data',\n conversation_config_override: { agent, tts: { voice_id: context.voice } },\n dynamic_variables: {},\n custom_llm_extra_body: {},\n };\n}\n\nclass ElevenLabsSession implements RealtimeSession {\n readonly model: RealtimeModel<string>;\n readonly #context: RealtimeContext;\n readonly #wire: VoiceWire;\n readonly #now: () => number;\n readonly #started: number;\n readonly #ledger = new RealtimeLedger();\n readonly #events = pushQueue<RealtimeEvent>();\n readonly #turn: TalkTurn = { vad: false, dropAudio: false, lastInterruptId: -1 };\n #closed = false;\n #usage: Usage | undefined;\n\n constructor(\n model: RealtimeModel<string>,\n context: RealtimeContext,\n wire: VoiceWire,\n now: () => number\n ) {\n this.model = model;\n this.#context = context;\n this.#wire = wire;\n this.#now = now;\n this.#started = now();\n }\n\n start(firstMessage: string | undefined): void {\n this.#send(initiation(this.#context, firstMessage));\n void this.#read();\n }\n\n #assertOpen(): void {\n if (this.#closed) throw new VoiceRequestError('error', 'session is closed');\n }\n\n #send(event: JsonObject): void {\n this.#wire.send(JSON.stringify(event));\n }\n\n #emit(event: RealtimeEvent): void {\n if (this.#closed) return;\n this.#ledger.observe(event);\n this.#events.push(event);\n }\n\n appendAudio(chunk: AudioChunk): void {\n this.#assertOpen();\n this.#ledger.addInput(chunk);\n this.#send({ user_audio_chunk: encodeAudioBase64(chunk.data) });\n }\n\n sendText(value: string): void {\n this.#assertOpen();\n this.#ledger.addText();\n this.#send({ type: 'user_message', text: value });\n }\n\n inject(value: string, delivery: Delivery): void {\n this.#assertOpen();\n this.#ledger.submit(this.model.delivery, delivery, () => {\n this.#assertOpen();\n this.#ledger.addText();\n this.#send(injectMessage(value, delivery));\n });\n }\n\n respond(callId: string, result: ToolResultMessage, delivery?: Delivery): void {\n this.#assertOpen();\n const send = () => {\n this.#send(toolResult(callId, result));\n };\n if (delivery === undefined) {\n send();\n return;\n }\n this.#ledger.submit(this.model.delivery, delivery, () => {\n this.#assertOpen();\n send();\n });\n }\n\n interrupt(heard?: { itemId: string; heardMs: number }): void {\n this.#assertOpen();\n planTruncation(this.model.truncation, heard);\n this.#turn.dropAudio = true;\n this.#ledger.settle();\n }\n\n update(patch: Partial<Pick<RealtimeContext, 'instructions' | 'tools' | 'voice'>>): void {\n this.#assertOpen();\n if (patch.tools !== undefined && this.model.liveToolUpdate === false) {\n throw new VoiceRequestError('error', 'live tool update is unsupported');\n }\n if (patch.tools !== undefined) this.#context.tools = patch.tools;\n if (patch.instructions !== undefined) {\n this.#context.instructions = patch.instructions;\n this.#send({\n type: 'contextual_update',\n text: patch.instructions,\n context_id: 'pi-audio',\n });\n }\n if (patch.voice !== undefined) this.#context.voice = patch.voice;\n }\n\n async close(): Promise<{ usage: Usage }> {\n if (this.#usage !== undefined) return { usage: this.#usage };\n this.#ledger.settle();\n this.#closed = true;\n this.#wire.close();\n this.#events.end();\n const inputSeconds = Math.max(0, this.#now() - this.#started) / 1000;\n this.#usage = priceAudio(this.model.audioCost, {\n inputSeconds,\n outputSeconds: this.#ledger.outputSeconds,\n textMessages: this.#ledger.textMessages,\n });\n return { usage: this.#usage };\n }\n\n [Symbol.asyncIterator](): AsyncIterator<RealtimeEvent> {\n return this.#events.iterable[Symbol.asyncIterator]();\n }\n\n #onRaw(raw: string): void {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n this.#emit({ type: 'error', error: 'invalid realtime event', fatal: false });\n return;\n }\n if (!isRecord(parsed)) return;\n const ingested = ingestElevenLabs(this.#turn, parsed, this.model.outputFormat);\n for (const reply of ingested.replies) this.#send(reply);\n for (const event of ingested.events) this.#emit(event);\n }\n\n #fail(error: unknown): void {\n const message = error instanceof Error ? error.message : String(error);\n this.#emit({ type: 'error', error: message, fatal: true });\n }\n\n async #read(): Promise<void> {\n try {\n for await (const raw of this.#wire.incoming) this.#onRaw(raw);\n } catch (error) {\n this.#fail(error);\n } finally {\n this.#closed = true;\n this.#events.end();\n }\n }\n}\n\nexport async function connectElevenLabs(\n model: RealtimeModel<string>,\n context: RealtimeContext,\n options: RealtimeOptions | undefined,\n settings: ElevenLabsOptions = {}\n): Promise<RealtimeSession> {\n if (options?.signal?.aborted === true) throw new VoiceRequestError('aborted', 'session aborted');\n const agent = await ensureAgent(model, context, options, settings);\n const url = await socketUrl(model, agent.agentId, context, options);\n const routed = realtimeSocket(url, options, []);\n const target: VoiceWireTarget = {\n url: routed.url,\n protocols: routed.protocols,\n headers: socketHeaders(options),\n };\n const open =\n settings.wire !== undefined\n ? settings.wire\n : (wireTarget: VoiceWireTarget) => openVoiceSocket(wireTarget, settings.socket);\n const now = settings.now !== undefined ? settings.now : Date.now;\n const session = new ElevenLabsSession(model, context, open(target), now);\n session.start(settings.firstMessage);\n return session;\n}\n"],
5
+ "mappings": "AAEA,SAAS,yBAAyB;AAClC,SAAS,yBAAyB;AAGlC;AAAA,EACE;AAAA,OAIK;AACP,SAAS,iBAAiB;AAO1B,SAAS,gBAAgB,gBAAgB,sBAAsB;AAC/D,SAAS,yBAAyB;AAClC,SAAS,kBAAkB;AAC3B,SAAS,wBAAuC;AAChD,SAAgC,sBAAsB;AAatD,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,WAAW,QAAmC;AACrD,QAAM,QAAkB,CAAC;AACzB,aAAW,QAAQ,OAAO,SAAS;AACjC,QAAI,KAAK,SAAS,OAAQ,OAAM,KAAK,KAAK,IAAI;AAAA,EAChD;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,cAAc,OAAe,UAAgC;AACpE,MAAI,aAAa,UAAU;AACzB,WAAO,EAAE,MAAM,qBAAqB,MAAM,OAAO,YAAY,WAAW;AAAA,EAC1E;AACA,QAAM,OAAO,aAAa,cAAc,kBAAkB,KAAK,KAAK;AACpE,SAAO,EAAE,MAAM,gBAAgB,KAAK;AACtC;AAEA,SAAS,WAAW,QAAgB,QAAuC;AACzE,SAAO;AAAA,IACL,MAAM;AAAA,IACN,cAAc;AAAA,IACd,QAAQ,WAAW,MAAM;AAAA,IACzB,UAAU,OAAO;AAAA,EACnB;AACF;AAEO,SAAS,UAAU,OAAsC;AAC9D,QAAM,UAAU,MAAM;AACtB,MAAI,YAAY,OAAW,QAAO;AAClC,SAAO,QAAQ;AACjB;AAEO,SAAS,gBACd,OACA,SACiB;AACjB,SAAO;AAAA,IACL,MAAM,MAAM;AAAA,IACZ,cAAc,QAAQ;AAAA,IACtB,OAAO,QAAQ;AAAA,IACf,OAAO,QAAQ;AAAA,IACf,KAAK,UAAU,KAAK;AAAA,EACtB;AACF;AAEA,SAAS,OAAO,SAA+C;AAC7D,SAAO,OAAO,SAAS,WAAW,YAAY,QAAQ,OAAO,SAAS;AACxE;AAEA,SAAS,eAAe,OAAmC;AACzD,MAAI,CAAC,MAAM,WAAW,IAAI,EAAG,QAAO;AACpC,QAAM,KAAK,IAAI,IAAI,KAAK,EAAE,aAAa,IAAI,UAAU;AACrD,SAAO,OAAO,OAAO,SAAY;AACnC;AAEA,SAAS,gBAAgB,OAA8B,SAAiB,OAAwB;AAC9F,QAAM,MAAM,IAAI,IAAI,MAAM,OAAO;AACjC,MAAI,aAAa,IAAI,YAAY,OAAO;AACxC,MAAI,UAAU,OAAW,KAAI,aAAa,IAAI,SAAS,KAAK;AAC5D,SAAO,IAAI,SAAS;AACtB;AAEA,SAAS,cAAc,SAA8D;AACnF,QAAM,UAAkC,CAAC;AACzC,QAAM,QAAQ,SAAS;AACvB,MAAI,UAAU,OAAW,QAAO;AAChC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,UAAU,KAAM,QAAO,QAAQ,GAAG;AAAA,QACjC,SAAQ,GAAG,IAAI;AAAA,EACtB;AACA,SAAO;AACT;AAEA,SAAS,YAAY,SAA8B;AACjD,SAAO,EAAE,SAAS,UAAU,IAAI,aAAa,EAAE;AACjD;AAEA,eAAe,YACb,OACA,SACA,SACA,UACsB;AACtB,QAAM,QAAQ,SAAS,OAAO;AAC9B,MAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAAG;AACjD,WAAO,YAAY,eAAe,KAAK,KAAK,SAAS,WAAW,MAAM,EAAE;AAAA,EAC1E;AACA,MAAI,CAAC,OAAO,OAAO,EAAG,QAAO,YAAY,SAAS,WAAW,MAAM,EAAE;AACrE,QAAM,YAA8B;AAAA,IAClC,GAAG;AAAA,IACH,SAAS,SAAS;AAAA,IAClB,OAAO,SAAS;AAAA,IAChB,KAAK,SAAS;AAAA,EAChB;AACA,SAAO,eAAe,OAAO,gBAAgB,OAAO,OAAO,GAAG,SAAS;AACzE;AAEA,eAAe,UACb,OACA,SACA,SACA,SACiB;AACjB,QAAM,QAAQ,SAAS,OAAO;AAC9B,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,IAAI,EAAG,QAAO;AAChE,MAAI,OAAO,UAAU,YAAY,MAAM,SAAS,EAAG,QAAO,gBAAgB,OAAO,SAAS,KAAK;AAC/F,MAAI,CAAC,OAAO,OAAO,EAAG,QAAO,gBAAgB,OAAO,OAAO;AAC3D,QAAM,SAAS,MAAM;AAAA,IACnB;AAAA,IACA;AAAA,MACE;AAAA,MACA,cAAc,QAAQ;AAAA,MACtB,OAAO,QAAQ;AAAA,MACf,OAAO,QAAQ;AAAA,IACjB;AAAA,IACA;AAAA,EACF;AACA,SAAO,OAAO;AAChB;AAEA,SAAS,WAAW,SAA0B,cAA8C;AAC1F,QAAM,QAAoB,EAAE,QAAQ,EAAE,QAAQ,QAAQ,aAAa,EAAE;AACrE,MAAI,iBAAiB,OAAW,OAAM,gBAAgB;AACtD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,8BAA8B,EAAE,OAAO,KAAK,EAAE,UAAU,QAAQ,MAAM,EAAE;AAAA,IACxE,mBAAmB,CAAC;AAAA,IACpB,uBAAuB,CAAC;AAAA,EAC1B;AACF;AAEA,MAAM,kBAA6C;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU,IAAI,eAAe;AAAA,EAC7B,UAAU,UAAyB;AAAA,EACnC,QAAkB,EAAE,KAAK,OAAO,WAAW,OAAO,iBAAiB,GAAG;AAAA,EAC/E,UAAU;AAAA,EACV;AAAA,EAEA,YACE,OACA,SACA,MACA,KACA;AACA,SAAK,QAAQ;AACb,SAAK,WAAW;AAChB,SAAK,QAAQ;AACb,SAAK,OAAO;AACZ,SAAK,WAAW,IAAI;AAAA,EACtB;AAAA,EAEA,MAAM,cAAwC;AAC5C,SAAK,MAAM,WAAW,KAAK,UAAU,YAAY,CAAC;AAClD,SAAK,KAAK,MAAM;AAAA,EAClB;AAAA,EAEA,cAAoB;AAClB,QAAI,KAAK,QAAS,OAAM,IAAI,kBAAkB,SAAS,mBAAmB;AAAA,EAC5E;AAAA,EAEA,MAAM,OAAyB;AAC7B,SAAK,MAAM,KAAK,KAAK,UAAU,KAAK,CAAC;AAAA,EACvC;AAAA,EAEA,MAAM,OAA4B;AAChC,QAAI,KAAK,QAAS;AAClB,SAAK,QAAQ,QAAQ,KAAK;AAC1B,SAAK,QAAQ,KAAK,KAAK;AAAA,EACzB;AAAA,EAEA,YAAY,OAAyB;AACnC,SAAK,YAAY;AACjB,SAAK,QAAQ,SAAS,KAAK;AAC3B,SAAK,MAAM,EAAE,kBAAkB,kBAAkB,MAAM,IAAI,EAAE,CAAC;AAAA,EAChE;AAAA,EAEA,SAAS,OAAqB;AAC5B,SAAK,YAAY;AACjB,SAAK,QAAQ,QAAQ;AACrB,SAAK,MAAM,EAAE,MAAM,gBAAgB,MAAM,MAAM,CAAC;AAAA,EAClD;AAAA,EAEA,OAAO,OAAe,UAA0B;AAC9C,SAAK,YAAY;AACjB,SAAK,QAAQ,OAAO,KAAK,MAAM,UAAU,UAAU,MAAM;AACvD,WAAK,YAAY;AACjB,WAAK,QAAQ,QAAQ;AACrB,WAAK,MAAM,cAAc,OAAO,QAAQ,CAAC;AAAA,IAC3C,CAAC;AAAA,EACH;AAAA,EAEA,QAAQ,QAAgB,QAA2B,UAA2B;AAC5E,SAAK,YAAY;AACjB,UAAM,OAAO,MAAM;AACjB,WAAK,MAAM,WAAW,QAAQ,MAAM,CAAC;AAAA,IACvC;AACA,QAAI,aAAa,QAAW;AAC1B,WAAK;AACL;AAAA,IACF;AACA,SAAK,QAAQ,OAAO,KAAK,MAAM,UAAU,UAAU,MAAM;AACvD,WAAK,YAAY;AACjB,WAAK;AAAA,IACP,CAAC;AAAA,EACH;AAAA,EAEA,UAAU,OAAmD;AAC3D,SAAK,YAAY;AACjB,mBAAe,KAAK,MAAM,YAAY,KAAK;AAC3C,SAAK,MAAM,YAAY;AACvB,SAAK,QAAQ,OAAO;AAAA,EACtB;AAAA,EAEA,OAAO,OAAiF;AACtF,SAAK,YAAY;AACjB,QAAI,MAAM,UAAU,UAAa,KAAK,MAAM,mBAAmB,OAAO;AACpE,YAAM,IAAI,kBAAkB,SAAS,iCAAiC;AAAA,IACxE;AACA,QAAI,MAAM,UAAU,OAAW,MAAK,SAAS,QAAQ,MAAM;AAC3D,QAAI,MAAM,iBAAiB,QAAW;AACpC,WAAK,SAAS,eAAe,MAAM;AACnC,WAAK,MAAM;AAAA,QACT,MAAM;AAAA,QACN,MAAM,MAAM;AAAA,QACZ,YAAY;AAAA,MACd,CAAC;AAAA,IACH;AACA,QAAI,MAAM,UAAU,OAAW,MAAK,SAAS,QAAQ,MAAM;AAAA,EAC7D;AAAA,EAEA,MAAM,QAAmC;AACvC,QAAI,KAAK,WAAW,OAAW,QAAO,EAAE,OAAO,KAAK,OAAO;AAC3D,SAAK,QAAQ,OAAO;AACpB,SAAK,UAAU;AACf,SAAK,MAAM,MAAM;AACjB,SAAK,QAAQ,IAAI;AACjB,UAAM,eAAe,KAAK,IAAI,GAAG,KAAK,KAAK,IAAI,KAAK,QAAQ,IAAI;AAChE,SAAK,SAAS,WAAW,KAAK,MAAM,WAAW;AAAA,MAC7C;AAAA,MACA,eAAe,KAAK,QAAQ;AAAA,MAC5B,cAAc,KAAK,QAAQ;AAAA,IAC7B,CAAC;AACD,WAAO,EAAE,OAAO,KAAK,OAAO;AAAA,EAC9B;AAAA,EAEA,CAAC,OAAO,aAAa,IAAkC;AACrD,WAAO,KAAK,QAAQ,SAAS,OAAO,aAAa,EAAE;AAAA,EACrD;AAAA,EAEA,OAAO,KAAmB;AACxB,QAAI;AACJ,QAAI;AACF,eAAS,KAAK,MAAM,GAAG;AAAA,IACzB,QAAQ;AACN,WAAK,MAAM,EAAE,MAAM,SAAS,OAAO,0BAA0B,OAAO,MAAM,CAAC;AAC3E;AAAA,IACF;AACA,QAAI,CAAC,SAAS,MAAM,EAAG;AACvB,UAAM,WAAW,iBAAiB,KAAK,OAAO,QAAQ,KAAK,MAAM,YAAY;AAC7E,eAAW,SAAS,SAAS,QAAS,MAAK,MAAM,KAAK;AACtD,eAAW,SAAS,SAAS,OAAQ,MAAK,MAAM,KAAK;AAAA,EACvD;AAAA,EAEA,MAAM,OAAsB;AAC1B,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,SAAK,MAAM,EAAE,MAAM,SAAS,OAAO,SAAS,OAAO,KAAK,CAAC;AAAA,EAC3D;AAAA,EAEA,MAAM,QAAuB;AAC3B,QAAI;AACF,uBAAiB,OAAO,KAAK,MAAM,SAAU,MAAK,OAAO,GAAG;AAAA,IAC9D,SAAS,OAAO;AACd,WAAK,MAAM,KAAK;AAAA,IAClB,UAAE;AACA,WAAK,UAAU;AACf,WAAK,QAAQ,IAAI;AAAA,IACnB;AAAA,EACF;AACF;AAEA,eAAsB,kBACpB,OACA,SACA,SACA,WAA8B,CAAC,GACL;AAC1B,MAAI,SAAS,QAAQ,YAAY,KAAM,OAAM,IAAI,kBAAkB,WAAW,iBAAiB;AAC/F,QAAM,QAAQ,MAAM,YAAY,OAAO,SAAS,SAAS,QAAQ;AACjE,QAAM,MAAM,MAAM,UAAU,OAAO,MAAM,SAAS,SAAS,OAAO;AAClE,QAAM,SAAS,eAAe,KAAK,SAAS,CAAC,CAAC;AAC9C,QAAM,SAA0B;AAAA,IAC9B,KAAK,OAAO;AAAA,IACZ,WAAW,OAAO;AAAA,IAClB,SAAS,cAAc,OAAO;AAAA,EAChC;AACA,QAAM,OACJ,SAAS,SAAS,SACd,SAAS,OACT,CAAC,eAAgC,gBAAgB,YAAY,SAAS,MAAM;AAClF,QAAM,MAAM,SAAS,QAAQ,SAAY,SAAS,MAAM,KAAK;AAC7D,QAAM,UAAU,IAAI,kBAAkB,OAAO,SAAS,KAAK,MAAM,GAAG,GAAG;AACvE,UAAQ,MAAM,SAAS,YAAY;AACnC,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,35 @@
1
+ import { createHash } from "node:crypto";
2
+ function memoryHostedCache() {
3
+ const agents = /* @__PURE__ */ new Map();
4
+ return {
5
+ get(key) {
6
+ return agents.get(key);
7
+ },
8
+ set(key, agent) {
9
+ agents.set(key, agent);
10
+ }
11
+ };
12
+ }
13
+ function hostedCacheKey(provider, name) {
14
+ return `${provider}\0${name}`;
15
+ }
16
+ function stable(value) {
17
+ if (Array.isArray(value)) return value.map(stable);
18
+ if (value === null || typeof value !== "object") return value;
19
+ const source = value;
20
+ const out = {};
21
+ for (const key of Object.keys(source).sort()) {
22
+ const next = source[key];
23
+ if (next !== void 0) out[key] = stable(next);
24
+ }
25
+ return out;
26
+ }
27
+ function hashHostedSpec(spec) {
28
+ return createHash("sha256").update(JSON.stringify(stable(spec))).digest("hex");
29
+ }
30
+ export {
31
+ hashHostedSpec,
32
+ hostedCacheKey,
33
+ memoryHostedCache
34
+ };
35
+ //# sourceMappingURL=hosted-agent.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/hosted-agent.ts"],
4
+ "sourcesContent": ["import { createHash } from 'node:crypto';\nimport type { Model, Tool } from '@earendil-works/pi-ai';\n\nexport type CustomLlm = Pick<\n Model<'openai-completions' | 'openai-responses'>,\n 'id' | 'api' | 'baseUrl' | 'headers'\n>;\n\nexport interface HostedAgentSpec {\n name: string;\n instructions: string;\n tools: Tool[];\n voice: string;\n llm: string | CustomLlm;\n firstMessage?: string;\n clientToolTimeoutMs?: number;\n}\n\nexport interface HostedAgent {\n agentId: string;\n specHash: string;\n publishedAt: number;\n}\n\nexport interface HostedCache {\n get(key: string): HostedAgent | undefined;\n set(key: string, agent: HostedAgent): void;\n}\n\nexport function memoryHostedCache(): HostedCache {\n const agents = new Map<string, HostedAgent>();\n return {\n get(key) {\n return agents.get(key);\n },\n set(key, agent) {\n agents.set(key, agent);\n },\n };\n}\n\nexport function hostedCacheKey(provider: string, name: string): string {\n return `${provider}\\0${name}`;\n}\n\nfunction stable(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(stable);\n if (value === null || typeof value !== 'object') return value;\n const source = value as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(source).sort()) {\n const next = source[key];\n if (next !== undefined) out[key] = stable(next);\n }\n return out;\n}\n\nexport function hashHostedSpec(spec: HostedAgentSpec): string {\n return createHash('sha256')\n .update(JSON.stringify(stable(spec)))\n .digest('hex');\n}\n"],
5
+ "mappings": "AAAA,SAAS,kBAAkB;AA6BpB,SAAS,oBAAiC;AAC/C,QAAM,SAAS,oBAAI,IAAyB;AAC5C,SAAO;AAAA,IACL,IAAI,KAAK;AACP,aAAO,OAAO,IAAI,GAAG;AAAA,IACvB;AAAA,IACA,IAAI,KAAK,OAAO;AACd,aAAO,IAAI,KAAK,KAAK;AAAA,IACvB;AAAA,EACF;AACF;AAEO,SAAS,eAAe,UAAkB,MAAsB;AACrE,SAAO,GAAG,QAAQ,KAAK,IAAI;AAC7B;AAEA,SAAS,OAAO,OAAyB;AACvC,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,MAAM;AACjD,MAAI,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO;AACxD,QAAM,SAAS;AACf,QAAM,MAA+B,CAAC;AACtC,aAAW,OAAO,OAAO,KAAK,MAAM,EAAE,KAAK,GAAG;AAC5C,UAAM,OAAO,OAAO,GAAG;AACvB,QAAI,SAAS,OAAW,KAAI,GAAG,IAAI,OAAO,IAAI;AAAA,EAChD;AACA,SAAO;AACT;AAEO,SAAS,eAAe,MAA+B;AAC5D,SAAO,WAAW,QAAQ,EACvB,OAAO,KAAK,UAAU,OAAO,IAAI,CAAC,CAAC,EACnC,OAAO,KAAK;AACjB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,13 @@
1
+ import type { JsonObject } from '@earendil-works/pi-ai';
2
+ import type { AudioFormat } from '../audio.ts';
3
+ import type { RealtimeEvent } from '../realtime.ts';
4
+ export interface TalkTurn {
5
+ vad: boolean;
6
+ dropAudio: boolean;
7
+ lastInterruptId: number;
8
+ }
9
+ export interface Ingested {
10
+ events: RealtimeEvent[];
11
+ replies: JsonObject[];
12
+ }
13
+ export declare function ingestElevenLabs(turn: TalkTurn, message: Record<string, unknown>, format: AudioFormat): Ingested;
@@ -0,0 +1,9 @@
1
+ import type { RealtimeModel } from '../models.ts';
2
+ import type { ElevenLabsOptions } from './session.ts';
3
+ export declare const elevenLabsAgent: RealtimeModel<'elevenlabs-agents'>;
4
+ export declare function registerElevenLabsAgents(options?: ElevenLabsOptions): () => void;
5
+ export type { CustomLlm, HostedAgent, HostedAgentSpec, HostedCache } from '../hosted-agent.ts';
6
+ export { hashHostedSpec, memoryHostedCache } from '../hosted-agent.ts';
7
+ export type { ProvisionOptions } from './provision.ts';
8
+ export { agentBody, httpsOrigin, provisionAgent } from './provision.ts';
9
+ export type { ElevenLabsOptions, VoiceWireFactory } from './session.ts';
@@ -0,0 +1,11 @@
1
+ import type { JsonObject, ProviderRequestOptions } from '@earendil-works/pi-ai';
2
+ import { type HostedAgent, type HostedAgentSpec, type HostedCache } from '../hosted-agent.ts';
3
+ import type { RealtimeModel } from '../models.ts';
4
+ export interface ProvisionOptions extends ProviderRequestOptions {
5
+ agentId?: string;
6
+ now?: () => number;
7
+ cache?: HostedCache;
8
+ }
9
+ export declare function httpsOrigin(baseUrl: string): string;
10
+ export declare function agentBody(spec: HostedAgentSpec): JsonObject;
11
+ export declare function provisionAgent(model: RealtimeModel<string>, spec: HostedAgentSpec, options?: ProvisionOptions): Promise<HostedAgent>;
@@ -0,0 +1,16 @@
1
+ import type { HostedAgentSpec, HostedCache } from '../hosted-agent.ts';
2
+ import type { RealtimeModel } from '../models.ts';
3
+ import { type VoiceSocketConstructor, type VoiceWire, type VoiceWireTarget } from '../openai-realtime/socket.ts';
4
+ import type { RealtimeContext, RealtimeOptions, RealtimeSession } from '../realtime.ts';
5
+ export type VoiceWireFactory = (target: VoiceWireTarget) => VoiceWire;
6
+ export interface ElevenLabsOptions {
7
+ wire?: VoiceWireFactory;
8
+ socket?: VoiceSocketConstructor;
9
+ cache?: HostedCache;
10
+ agentId?: string;
11
+ now?: () => number;
12
+ firstMessage?: string;
13
+ }
14
+ export declare function talkerLlm(model: RealtimeModel<string>): string;
15
+ export declare function specFromContext(model: RealtimeModel<string>, context: RealtimeContext): HostedAgentSpec;
16
+ export declare function connectElevenLabs(model: RealtimeModel<string>, context: RealtimeContext, options: RealtimeOptions | undefined, settings?: ElevenLabsOptions): Promise<RealtimeSession>;
@@ -0,0 +1,23 @@
1
+ import type { Model, Tool } from '@earendil-works/pi-ai';
2
+ export type CustomLlm = Pick<Model<'openai-completions' | 'openai-responses'>, 'id' | 'api' | 'baseUrl' | 'headers'>;
3
+ export interface HostedAgentSpec {
4
+ name: string;
5
+ instructions: string;
6
+ tools: Tool[];
7
+ voice: string;
8
+ llm: string | CustomLlm;
9
+ firstMessage?: string;
10
+ clientToolTimeoutMs?: number;
11
+ }
12
+ export interface HostedAgent {
13
+ agentId: string;
14
+ specHash: string;
15
+ publishedAt: number;
16
+ }
17
+ export interface HostedCache {
18
+ get(key: string): HostedAgent | undefined;
19
+ set(key: string, agent: HostedAgent): void;
20
+ }
21
+ export declare function memoryHostedCache(): HostedCache;
22
+ export declare function hostedCacheKey(provider: string, name: string): string;
23
+ export declare function hashHostedSpec(spec: HostedAgentSpec): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/pi-audio",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "Voice bindings for pi: speech-to-text, text-to-speech and realtime speech for pi-coding-agent plugins and slicc-agent",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -61,6 +61,10 @@
61
61
  "./gemini-live": {
62
62
  "types": "./dist/types/gemini-live/index.d.ts",
63
63
  "default": "./dist/gemini-live/index.js"
64
+ },
65
+ "./elevenlabs-agents": {
66
+ "types": "./dist/types/elevenlabs-agents/index.d.ts",
67
+ "default": "./dist/elevenlabs-agents/index.js"
64
68
  }
65
69
  },
66
70
  "files": [