@ai-ecoverse/pi-audio 1.6.0 → 1.8.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
@@ -31,6 +31,8 @@ pi-audio does not apply that patch. Hosts such as slicc-agent record monkeypatch
31
31
 
32
32
  `RealtimeLedger` meters one session. Input seconds come from `appendAudio`. Output seconds come from each `output_audio` event: PCM, mulaw, and alaw from the byte length, and a compressed encoding from character alignment. `sendText` and each delivered `inject` add one text message. Tool results do not. `close()` prices those meters with `priceAudio`. Sum that usage once. `response_done.usage` is the adapter's per-response figure, not a second copy of the session total.
33
33
 
34
+ `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.
35
+
34
36
  ## Development
35
37
 
36
38
  ```sh
package/dist/index.js CHANGED
@@ -23,7 +23,9 @@ import {
23
23
  transcribe
24
24
  } from "./registry.js";
25
25
  import { SpeechEventStream } from "./speech.js";
26
+ import { askAgent, bindThinker, talkerTools } from "./talker.js";
26
27
  import { TranscriptionEventStream } from "./transcription.js";
28
+ import { voiceTurns } from "./turns.js";
27
29
  import { addAudioCounters, priceAudio } from "./usage.js";
28
30
  import { energyVad } from "./vad.js";
29
31
  export {
@@ -32,6 +34,8 @@ export {
32
34
  TranscriptionEventStream,
33
35
  VoiceRequestError,
34
36
  addAudioCounters,
37
+ askAgent,
38
+ bindThinker,
35
39
  blankUsage,
36
40
  connectRealtime,
37
41
  convertAudio,
@@ -52,6 +56,8 @@ export {
52
56
  registerVoiceModel,
53
57
  resolveVoiceAuth,
54
58
  speak,
55
- transcribe
59
+ talkerTools,
60
+ transcribe,
61
+ voiceTurns
56
62
  };
57
63
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["export type {\n AudioChunk,\n AudioEncoding,\n AudioFormat,\n AudioSink,\n AudioSource,\n AudioTag,\n CharAlignment,\n TextSpan,\n} from './audio.ts';\nexport { resolveVoiceAuth } from './auth.ts';\nexport { blankUsage } from './blank-usage.ts';\nexport {\n convertAudio,\n convertAudioSource,\n decodeAudioBase64,\n encodeAudioBase64,\n} from './convert.ts';\nexport { VoiceRequestError } from './errors.ts';\nexport type {\n Delivery,\n DeliverySupport,\n KnownRealtimeApi,\n KnownSpeechApi,\n KnownTranscriptionApi,\n RealtimeApi,\n RealtimeModel,\n SpeechApi,\n SpeechModel,\n TranscriptionApi,\n TranscriptionModel,\n VoiceInfo,\n VoiceModel,\n VoiceModelType,\n} from './models.ts';\nexport {\n getVoiceModel,\n listVoiceModels,\n registerVoiceModel,\n} from './models.ts';\nexport type {\n DeliveryPlan,\n RealtimeApiProvider,\n RealtimeContext,\n RealtimeEvent,\n RealtimeOptions,\n RealtimeSession,\n RealtimeSocket,\n RealtimeToken,\n TruncationPlan,\n} from './realtime.ts';\nexport { planDelivery, planTruncation, RealtimeLedger, realtimeSocket } from './realtime.ts';\nexport {\n connectRealtime,\n listen,\n registerRealtimeApiProvider,\n registerSpeechApiProvider,\n registerTranscriptionApiProvider,\n speak,\n transcribe,\n} from './registry.ts';\nexport type { SpeechApiProvider, SpeechEvent, SpeechInput } from './speech.ts';\nexport { SpeechEventStream } from './speech.ts';\nexport type {\n AudioInput,\n ListenOptions,\n Transcript,\n TranscriptionApiProvider,\n TranscriptionEvent,\n TranscriptSegment,\n} from './transcription.ts';\nexport { TranscriptionEventStream } from './transcription.ts';\nexport type {\n AudioCost,\n AudioCounters,\n AudioUsage,\n RealtimeAudioCost,\n SpeechAudioCost,\n TranscriptionAudioCost,\n} from './usage.ts';\nexport { addAudioCounters, priceAudio } from './usage.ts';\nexport type { VadHook, VadUpdate } from './vad.ts';\nexport { energyVad } from './vad.ts';\n"],
5
- "mappings": "AAUA,SAAS,wBAAwB;AACjC,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB;AAiBlC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAYP,SAAS,cAAc,gBAAgB,gBAAgB,sBAAsB;AAC7E;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,yBAAyB;AASlC,SAAS,gCAAgC;AASzC,SAAS,kBAAkB,kBAAkB;AAE7C,SAAS,iBAAiB;",
4
+ "sourcesContent": ["export type {\n AudioChunk,\n AudioEncoding,\n AudioFormat,\n AudioSink,\n AudioSource,\n AudioTag,\n CharAlignment,\n TextSpan,\n} from './audio.ts';\nexport { resolveVoiceAuth } from './auth.ts';\nexport { blankUsage } from './blank-usage.ts';\nexport {\n convertAudio,\n convertAudioSource,\n decodeAudioBase64,\n encodeAudioBase64,\n} from './convert.ts';\nexport { VoiceRequestError } from './errors.ts';\nexport type {\n Delivery,\n DeliverySupport,\n KnownRealtimeApi,\n KnownSpeechApi,\n KnownTranscriptionApi,\n RealtimeApi,\n RealtimeModel,\n SpeechApi,\n SpeechModel,\n TranscriptionApi,\n TranscriptionModel,\n VoiceInfo,\n VoiceModel,\n VoiceModelType,\n} from './models.ts';\nexport {\n getVoiceModel,\n listVoiceModels,\n registerVoiceModel,\n} from './models.ts';\nexport type {\n DeliveryPlan,\n RealtimeApiProvider,\n RealtimeContext,\n RealtimeEvent,\n RealtimeOptions,\n RealtimeSession,\n RealtimeSocket,\n RealtimeToken,\n TruncationPlan,\n} from './realtime.ts';\nexport { planDelivery, planTruncation, RealtimeLedger, realtimeSocket } from './realtime.ts';\nexport {\n connectRealtime,\n listen,\n registerRealtimeApiProvider,\n registerSpeechApiProvider,\n registerTranscriptionApiProvider,\n speak,\n transcribe,\n} from './registry.ts';\nexport type { SpeechApiProvider, SpeechEvent, SpeechInput } from './speech.ts';\nexport { SpeechEventStream } from './speech.ts';\nexport type { VoiceAgentEvent, VoiceMessageChange } from './spoken.ts';\nexport type {\n BindThinkerOptions,\n BoundThinker,\n Thinker,\n ThinkerCaption,\n ThinkerPhase,\n ThinkerUpdate,\n} from './talker.ts';\nexport { askAgent, bindThinker, talkerTools } from './talker.ts';\nexport type {\n AudioInput,\n ListenOptions,\n Transcript,\n TranscriptionApiProvider,\n TranscriptionEvent,\n TranscriptSegment,\n} from './transcription.ts';\nexport { TranscriptionEventStream } from './transcription.ts';\nexport type {\n BargeIn,\n CaptionEvent,\n EchoRecommendation,\n VoiceConversation,\n VoiceCut,\n VoiceModels,\n VoicePhase,\n VoiceSession,\n VoiceTurnMeta,\n VoiceTurnOptions,\n} from './turns.ts';\nexport { voiceTurns } from './turns.ts';\nexport type {\n AudioCost,\n AudioCounters,\n AudioUsage,\n RealtimeAudioCost,\n SpeechAudioCost,\n TranscriptionAudioCost,\n} from './usage.ts';\nexport { addAudioCounters, priceAudio } from './usage.ts';\nexport type { VadHook, VadUpdate } from './vad.ts';\nexport { energyVad } from './vad.ts';\n"],
5
+ "mappings": "AAUA,SAAS,wBAAwB;AACjC,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB;AAiBlC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAYP,SAAS,cAAc,gBAAgB,gBAAgB,sBAAsB;AAC7E;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,yBAAyB;AAUlC,SAAS,UAAU,aAAa,mBAAmB;AASnD,SAAS,gCAAgC;AAazC,SAAS,kBAAkB;AAS3B,SAAS,kBAAkB,kBAAkB;AAE7C,SAAS,iBAAiB;",
6
6
  "names": []
7
7
  }
package/dist/queue.js ADDED
@@ -0,0 +1,46 @@
1
+ function pushQueue() {
2
+ const values = [];
3
+ let ended = false;
4
+ let wake;
5
+ const notify = () => {
6
+ const current = wake;
7
+ wake = void 0;
8
+ current?.();
9
+ };
10
+ return {
11
+ push(value) {
12
+ if (ended) return;
13
+ values.push(value);
14
+ notify();
15
+ },
16
+ end() {
17
+ ended = true;
18
+ notify();
19
+ },
20
+ iterable: {
21
+ async *[Symbol.asyncIterator]() {
22
+ let index = 0;
23
+ while (true) {
24
+ if (index < values.length) {
25
+ const value = values[index];
26
+ index += 1;
27
+ if (value !== void 0) yield value;
28
+ continue;
29
+ }
30
+ if (ended) return;
31
+ await new Promise((resolve) => {
32
+ wake = () => {
33
+ wake = void 0;
34
+ resolve();
35
+ };
36
+ if (index < values.length || ended) wake();
37
+ });
38
+ }
39
+ }
40
+ }
41
+ };
42
+ }
43
+ export {
44
+ pushQueue
45
+ };
46
+ //# sourceMappingURL=queue.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/queue.ts"],
4
+ "sourcesContent": ["export interface PushQueue<T> {\n push(value: T): void;\n end(): void;\n readonly iterable: AsyncIterable<T>;\n}\n\nexport function pushQueue<T>(): PushQueue<T> {\n const values: T[] = [];\n let ended = false;\n let wake: (() => void) | undefined;\n const notify = () => {\n const current = wake;\n wake = undefined;\n current?.();\n };\n return {\n push(value) {\n if (ended) return;\n values.push(value);\n notify();\n },\n end() {\n ended = true;\n notify();\n },\n iterable: {\n async *[Symbol.asyncIterator]() {\n let index = 0;\n while (true) {\n if (index < values.length) {\n const value = values[index];\n index += 1;\n if (value !== undefined) yield value;\n continue;\n }\n if (ended) return;\n await new Promise<void>((resolve) => {\n wake = () => {\n wake = undefined;\n resolve();\n };\n if (index < values.length || ended) wake();\n });\n }\n },\n },\n };\n}\n"],
5
+ "mappings": "AAMO,SAAS,YAA6B;AAC3C,QAAM,SAAc,CAAC;AACrB,MAAI,QAAQ;AACZ,MAAI;AACJ,QAAM,SAAS,MAAM;AACnB,UAAM,UAAU;AAChB,WAAO;AACP,cAAU;AAAA,EACZ;AACA,SAAO;AAAA,IACL,KAAK,OAAO;AACV,UAAI,MAAO;AACX,aAAO,KAAK,KAAK;AACjB,aAAO;AAAA,IACT;AAAA,IACA,MAAM;AACJ,cAAQ;AACR,aAAO;AAAA,IACT;AAAA,IACA,UAAU;AAAA,MACR,QAAQ,OAAO,aAAa,IAAI;AAC9B,YAAI,QAAQ;AACZ,eAAO,MAAM;AACX,cAAI,QAAQ,OAAO,QAAQ;AACzB,kBAAM,QAAQ,OAAO,KAAK;AAC1B,qBAAS;AACT,gBAAI,UAAU,OAAW,OAAM;AAC/B;AAAA,UACF;AACA,cAAI,MAAO;AACX,gBAAM,IAAI,QAAc,CAAC,YAAY;AACnC,mBAAO,MAAM;AACX,qBAAO;AACP,sBAAQ;AAAA,YACV;AACA,gBAAI,QAAQ,OAAO,UAAU,MAAO,MAAK;AAAA,UAC3C,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
package/dist/spoken.js ADDED
@@ -0,0 +1,197 @@
1
+ import { blankUsage } from "./blank-usage.js";
2
+ function createSpokenState() {
3
+ return { blocks: /* @__PURE__ */ new Map() };
4
+ }
5
+ function isCommentary(signature) {
6
+ if (!signature) return false;
7
+ try {
8
+ const parsed = JSON.parse(signature);
9
+ return parsed?.phase === "commentary";
10
+ } catch {
11
+ return false;
12
+ }
13
+ }
14
+ function spokenPieces(event, state, options = {}) {
15
+ if (event.type === "tool_execution_start") return announceOf(event, options);
16
+ if (event.entry?.id !== void 0) state.entryId = String(event.entry.id);
17
+ if (event.type === "message_end") return messagePieces(event.entry?.model, state, options, true);
18
+ if (event.type === "message_start")
19
+ return messagePieces(assistantContent(event.message), state, options, true);
20
+ if (event.type === "message_update") return changePieces(event.changes ?? [], state, options);
21
+ return [];
22
+ }
23
+ function announceOf(event, options) {
24
+ if (!options.announce || !event.toolName || !event.toolCallId) return [];
25
+ const text = options.announce({
26
+ type: "toolCall",
27
+ id: event.toolCallId,
28
+ name: event.toolName,
29
+ arguments: event.args ?? {}
30
+ });
31
+ if (!text) return [];
32
+ return [{ text, contentIndex: -1, start: 0, end: 0, kind: "announce" }];
33
+ }
34
+ function assistantContent(message) {
35
+ if (message?.role !== "assistant") return void 0;
36
+ return Array.isArray(message.content) ? message.content : void 0;
37
+ }
38
+ function messagePieces(model, state, options, close) {
39
+ const content = Array.isArray(model) ? blocksOf(model) : void 0;
40
+ if (!content) return [];
41
+ const pieces = [];
42
+ for (const [contentIndex, block] of content.entries()) {
43
+ const track = applyBlock(state, contentIndex, block);
44
+ const piece = takePiece(track, contentIndex, state, options, close);
45
+ if (piece) pieces.push(piece);
46
+ }
47
+ return pieces;
48
+ }
49
+ function blocksOf(model) {
50
+ const first = model[0];
51
+ if (!first) return void 0;
52
+ if ("role" in first) {
53
+ if (first.role !== "assistant" || !Array.isArray(first.content)) return void 0;
54
+ return first.content;
55
+ }
56
+ return model;
57
+ }
58
+ function changePieces(changes, state, options) {
59
+ const pieces = [];
60
+ for (const change of changes) pieces.push(...applyChange(change, state, options));
61
+ return pieces;
62
+ }
63
+ function applyChange(change, state, options) {
64
+ if (change.type === "message") {
65
+ return messagePieces(change.message ? [change.message] : void 0, state, options, false);
66
+ }
67
+ const contentIndex = change.contentIndex;
68
+ if (contentIndex === void 0) return [];
69
+ if (change.type === "thinking_start" || change.type === "thinking_delta") {
70
+ noteThinking(state, contentIndex, change);
71
+ return [];
72
+ }
73
+ if (change.type === "toolcall_start" || change.type === "toolcall_delta") {
74
+ state.blocks.set(contentIndex, { kind: "tool", text: "", sent: 0, ended: true });
75
+ return [];
76
+ }
77
+ const track = textTrack(state, contentIndex);
78
+ if (change.type === "text_start" || change.type === "block") noteText(track, change.block);
79
+ if (change.type === "text_delta" && change.delta) track.text += change.delta;
80
+ const piece = change.type === "text_delta" && options.speakable ? customPiece(track, contentIndex, change.delta ?? "", state, options, "text_delta") : takePiece(track, contentIndex, state, options, false);
81
+ return piece ? [piece] : [];
82
+ }
83
+ function noteThinking(state, contentIndex, change) {
84
+ const existing = state.blocks.get(contentIndex);
85
+ const text = `${existing?.kind === "thinking" ? existing.text : ""}${change.delta ?? ""}`;
86
+ const started = change.block?.type === "thinking" ? change.block.thinking : text;
87
+ state.blocks.set(contentIndex, {
88
+ kind: "thinking",
89
+ text: change.type === "thinking_start" ? started : text,
90
+ sent: change.type === "thinking_start" ? started.length : text.length,
91
+ ended: true,
92
+ ...change.block?.type === "thinking" && change.block.thinkingSignature ? { signature: change.block.thinkingSignature } : {}
93
+ });
94
+ }
95
+ function noteText(track, block) {
96
+ if (block?.type !== "text") return;
97
+ track.kind = "text";
98
+ track.text = block.text;
99
+ track.signature = block.textSignature;
100
+ }
101
+ function textTrack(state, contentIndex) {
102
+ const existing = state.blocks.get(contentIndex);
103
+ if (existing) return existing;
104
+ const created = { kind: "text", text: "", sent: 0, ended: false };
105
+ state.blocks.set(contentIndex, created);
106
+ return created;
107
+ }
108
+ function applyBlock(state, contentIndex, block) {
109
+ if (block.type === "thinking") {
110
+ const track2 = {
111
+ kind: "thinking",
112
+ text: block.thinking,
113
+ sent: block.thinking.length,
114
+ ended: true
115
+ };
116
+ state.blocks.set(contentIndex, track2);
117
+ return track2;
118
+ }
119
+ if (block.type === "toolCall") {
120
+ const track2 = { kind: "tool", text: "", sent: 0, ended: true };
121
+ state.blocks.set(contentIndex, track2);
122
+ return track2;
123
+ }
124
+ const existing = state.blocks.get(contentIndex);
125
+ const sent = existing?.kind === "text" ? existing.sent : 0;
126
+ let ended = false;
127
+ if (existing?.kind === "text") ended = existing.ended;
128
+ const track = {
129
+ kind: "text",
130
+ text: block.text,
131
+ sent,
132
+ ended,
133
+ ...block.textSignature ? { signature: block.textSignature } : {}
134
+ };
135
+ state.blocks.set(contentIndex, track);
136
+ return track;
137
+ }
138
+ function takePiece(track, contentIndex, state, options, close) {
139
+ if (track.kind !== "text") return void 0;
140
+ if (options.speakable) {
141
+ if (!close || track.ended) return void 0;
142
+ track.ended = true;
143
+ return customPiece(track, contentIndex, track.text, state, options, "text_end");
144
+ }
145
+ if (isCommentary(track.signature)) {
146
+ track.sent = track.text.length;
147
+ return void 0;
148
+ }
149
+ if (track.sent >= track.text.length) return void 0;
150
+ const start = track.sent;
151
+ const text = track.text.slice(start);
152
+ track.sent = track.text.length;
153
+ if (!text) return void 0;
154
+ return { text, contentIndex, start, end: track.sent, kind: "answer", entryId: state.entryId };
155
+ }
156
+ function customPiece(track, contentIndex, delta, state, options, type) {
157
+ if (!options.speakable) return void 0;
158
+ const partial = partialFrom(state, options.model ?? "voice");
159
+ const event = type === "text_delta" ? { type: "text_delta", contentIndex, delta, partial } : { type: "text_end", contentIndex, content: track.text, partial };
160
+ const text = options.speakable(event);
161
+ const start = track.sent;
162
+ track.sent = track.text.length;
163
+ track.ended = type === "text_end";
164
+ if (!text) return void 0;
165
+ return { text, contentIndex, start, end: track.sent, kind: "answer", entryId: state.entryId };
166
+ }
167
+ function partialFrom(state, model) {
168
+ const max = Math.max(-1, ...state.blocks.keys());
169
+ const content = [];
170
+ for (let index = 0; index <= max; index += 1) content.push(partialBlock(state.blocks.get(index)));
171
+ return {
172
+ role: "assistant",
173
+ content,
174
+ api: "pi-audio",
175
+ provider: "pi-audio",
176
+ model,
177
+ usage: blankUsage(),
178
+ stopReason: "pending",
179
+ timestamp: 0
180
+ };
181
+ }
182
+ function partialBlock(track) {
183
+ if (!track || track.kind === "thinking") return { type: "thinking", thinking: track?.text ?? "" };
184
+ if (track.kind === "tool")
185
+ return { type: "toolCall", id: "pending", name: "pending", arguments: {} };
186
+ return {
187
+ type: "text",
188
+ text: track.text,
189
+ ...track.signature ? { textSignature: track.signature } : {}
190
+ };
191
+ }
192
+ export {
193
+ createSpokenState,
194
+ isCommentary,
195
+ spokenPieces
196
+ };
197
+ //# sourceMappingURL=spoken.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/spoken.ts"],
4
+ "sourcesContent": ["import type {\n AssistantMessage,\n AssistantMessageEvent,\n JsonObject,\n TextContent,\n ThinkingContent,\n ToolCall,\n} from '@earendil-works/pi-ai';\nimport { blankUsage } from './blank-usage.ts';\n\nexport interface VoiceMessageChange {\n type: string;\n contentIndex?: number;\n delta?: string;\n block?: TextContent | ThinkingContent | ToolCall;\n message?: AssistantMessage;\n}\n\nexport interface VoiceEntryRef {\n id: string | number;\n model?: readonly {\n role: string;\n content?: readonly (TextContent | ThinkingContent | ToolCall)[];\n }[];\n}\n\nexport interface VoiceAgentEvent {\n type: string;\n changes?: readonly VoiceMessageChange[];\n message?: { role: string; content?: unknown };\n entry?: VoiceEntryRef;\n toolName?: string;\n toolCallId?: string;\n args?: JsonObject;\n attempt?: number;\n at?: number;\n}\n\nexport interface SpokenPiece {\n text: string;\n contentIndex: number;\n start: number;\n end: number;\n kind: 'answer' | 'announce';\n entryId?: string;\n}\n\ninterface BlockTrack {\n kind: 'text' | 'thinking' | 'tool';\n text: string;\n sent: number;\n signature?: string;\n ended: boolean;\n}\n\nexport interface SpokenState {\n blocks: Map<number, BlockTrack>;\n entryId?: string;\n}\n\nexport interface SpokenOptions {\n speakable?(event: AssistantMessageEvent): string | undefined;\n announce?(call: ToolCall): string | undefined;\n model?: string;\n}\n\ntype Block = TextContent | ThinkingContent | ToolCall;\n\nexport function createSpokenState(): SpokenState {\n return { blocks: new Map() };\n}\n\nexport function isCommentary(signature: string | undefined): boolean {\n if (!signature) return false;\n try {\n const parsed = JSON.parse(signature) as { phase?: unknown } | null;\n return parsed?.phase === 'commentary';\n } catch {\n return false;\n }\n}\n\nexport function spokenPieces(\n event: VoiceAgentEvent,\n state: SpokenState,\n options: SpokenOptions = {}\n): SpokenPiece[] {\n if (event.type === 'tool_execution_start') return announceOf(event, options);\n if (event.entry?.id !== undefined) state.entryId = String(event.entry.id);\n if (event.type === 'message_end') return messagePieces(event.entry?.model, state, options, true);\n if (event.type === 'message_start')\n return messagePieces(assistantContent(event.message), state, options, true);\n if (event.type === 'message_update') return changePieces(event.changes ?? [], state, options);\n return [];\n}\n\nfunction announceOf(event: VoiceAgentEvent, options: SpokenOptions): SpokenPiece[] {\n if (!options.announce || !event.toolName || !event.toolCallId) return [];\n const text = options.announce({\n type: 'toolCall',\n id: event.toolCallId,\n name: event.toolName,\n arguments: event.args ?? {},\n });\n if (!text) return [];\n return [{ text, contentIndex: -1, start: 0, end: 0, kind: 'announce' }];\n}\n\nfunction assistantContent(message: VoiceAgentEvent['message']): readonly Block[] | undefined {\n if (message?.role !== 'assistant') return undefined;\n return Array.isArray(message.content) ? (message.content as readonly Block[]) : undefined;\n}\n\nfunction messagePieces(\n model: VoiceEntryRef['model'] | readonly Block[] | undefined,\n state: SpokenState,\n options: SpokenOptions,\n close: boolean\n): SpokenPiece[] {\n const content = Array.isArray(model) ? blocksOf(model) : undefined;\n if (!content) return [];\n const pieces: SpokenPiece[] = [];\n for (const [contentIndex, block] of content.entries()) {\n const track = applyBlock(state, contentIndex, block);\n const piece = takePiece(track, contentIndex, state, options, close);\n if (piece) pieces.push(piece);\n }\n return pieces;\n}\n\nfunction blocksOf(\n model: readonly Block[] | NonNullable<VoiceEntryRef['model']>\n): readonly Block[] | undefined {\n const first = model[0] as { role?: string; content?: unknown } | Block | undefined;\n if (!first) return undefined;\n if ('role' in first) {\n if (first.role !== 'assistant' || !Array.isArray(first.content)) return undefined;\n return first.content as readonly Block[];\n }\n return model as readonly Block[];\n}\n\nfunction changePieces(\n changes: readonly VoiceMessageChange[],\n state: SpokenState,\n options: SpokenOptions\n): SpokenPiece[] {\n const pieces: SpokenPiece[] = [];\n for (const change of changes) pieces.push(...applyChange(change, state, options));\n return pieces;\n}\n\nfunction applyChange(\n change: VoiceMessageChange,\n state: SpokenState,\n options: SpokenOptions\n): SpokenPiece[] {\n if (change.type === 'message') {\n return messagePieces(change.message ? [change.message] : undefined, state, options, false);\n }\n const contentIndex = change.contentIndex;\n if (contentIndex === undefined) return [];\n if (change.type === 'thinking_start' || change.type === 'thinking_delta') {\n noteThinking(state, contentIndex, change);\n return [];\n }\n if (change.type === 'toolcall_start' || change.type === 'toolcall_delta') {\n state.blocks.set(contentIndex, { kind: 'tool', text: '', sent: 0, ended: true });\n return [];\n }\n const track = textTrack(state, contentIndex);\n if (change.type === 'text_start' || change.type === 'block') noteText(track, change.block);\n if (change.type === 'text_delta' && change.delta) track.text += change.delta;\n const piece =\n change.type === 'text_delta' && options.speakable\n ? customPiece(track, contentIndex, change.delta ?? '', state, options, 'text_delta')\n : takePiece(track, contentIndex, state, options, false);\n return piece ? [piece] : [];\n}\n\nfunction noteThinking(state: SpokenState, contentIndex: number, change: VoiceMessageChange): void {\n const existing = state.blocks.get(contentIndex);\n const text = `${existing?.kind === 'thinking' ? existing.text : ''}${change.delta ?? ''}`;\n const started = change.block?.type === 'thinking' ? change.block.thinking : text;\n state.blocks.set(contentIndex, {\n kind: 'thinking',\n text: change.type === 'thinking_start' ? started : text,\n sent: change.type === 'thinking_start' ? started.length : text.length,\n ended: true,\n ...(change.block?.type === 'thinking' && change.block.thinkingSignature\n ? { signature: change.block.thinkingSignature }\n : {}),\n });\n}\n\nfunction noteText(track: BlockTrack, block: VoiceMessageChange['block']): void {\n if (block?.type !== 'text') return;\n track.kind = 'text';\n track.text = block.text;\n track.signature = block.textSignature;\n}\n\nfunction textTrack(state: SpokenState, contentIndex: number): BlockTrack {\n const existing = state.blocks.get(contentIndex);\n if (existing) return existing;\n const created: BlockTrack = { kind: 'text', text: '', sent: 0, ended: false };\n state.blocks.set(contentIndex, created);\n return created;\n}\n\nfunction applyBlock(state: SpokenState, contentIndex: number, block: Block): BlockTrack {\n if (block.type === 'thinking') {\n const track: BlockTrack = {\n kind: 'thinking',\n text: block.thinking,\n sent: block.thinking.length,\n ended: true,\n };\n state.blocks.set(contentIndex, track);\n return track;\n }\n if (block.type === 'toolCall') {\n const track: BlockTrack = { kind: 'tool', text: '', sent: 0, ended: true };\n state.blocks.set(contentIndex, track);\n return track;\n }\n const existing = state.blocks.get(contentIndex);\n const sent = existing?.kind === 'text' ? existing.sent : 0;\n let ended = false;\n if (existing?.kind === 'text') ended = existing.ended;\n const track: BlockTrack = {\n kind: 'text',\n text: block.text,\n sent,\n ended,\n ...(block.textSignature ? { signature: block.textSignature } : {}),\n };\n state.blocks.set(contentIndex, track);\n return track;\n}\n\nfunction takePiece(\n track: BlockTrack,\n contentIndex: number,\n state: SpokenState,\n options: SpokenOptions,\n close: boolean\n): SpokenPiece | undefined {\n if (track.kind !== 'text') return undefined;\n if (options.speakable) {\n if (!close || track.ended) return undefined;\n track.ended = true;\n return customPiece(track, contentIndex, track.text, state, options, 'text_end');\n }\n if (isCommentary(track.signature)) {\n track.sent = track.text.length;\n return undefined;\n }\n if (track.sent >= track.text.length) return undefined;\n const start = track.sent;\n const text = track.text.slice(start);\n track.sent = track.text.length;\n if (!text) return undefined;\n return { text, contentIndex, start, end: track.sent, kind: 'answer', entryId: state.entryId };\n}\n\nfunction customPiece(\n track: BlockTrack,\n contentIndex: number,\n delta: string,\n state: SpokenState,\n options: SpokenOptions,\n type: 'text_delta' | 'text_end'\n): SpokenPiece | undefined {\n if (!options.speakable) return undefined;\n const partial = partialFrom(state, options.model ?? 'voice');\n const event: AssistantMessageEvent =\n type === 'text_delta'\n ? { type: 'text_delta', contentIndex, delta, partial }\n : { type: 'text_end', contentIndex, content: track.text, partial };\n const text = options.speakable(event);\n const start = track.sent;\n track.sent = track.text.length;\n track.ended = type === 'text_end';\n if (!text) return undefined;\n return { text, contentIndex, start, end: track.sent, kind: 'answer', entryId: state.entryId };\n}\n\nfunction partialFrom(state: SpokenState, model: string): AssistantMessage {\n const max = Math.max(-1, ...state.blocks.keys());\n const content: Block[] = [];\n for (let index = 0; index <= max; index += 1) content.push(partialBlock(state.blocks.get(index)));\n return {\n role: 'assistant',\n content,\n api: 'pi-audio',\n provider: 'pi-audio',\n model,\n usage: blankUsage(),\n stopReason: 'pending',\n timestamp: 0,\n };\n}\n\nfunction partialBlock(track: BlockTrack | undefined): Block {\n if (!track || track.kind === 'thinking') return { type: 'thinking', thinking: track?.text ?? '' };\n if (track.kind === 'tool')\n return { type: 'toolCall', id: 'pending', name: 'pending', arguments: {} };\n return {\n type: 'text',\n text: track.text,\n ...(track.signature ? { textSignature: track.signature } : {}),\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["track"]
7
+ }
package/dist/talker.js ADDED
@@ -0,0 +1,192 @@
1
+ import {
2
+ Type
3
+ } from "@earendil-works/pi-ai";
4
+ import { VoiceRequestError } from "./errors.js";
5
+ const heardLimit = 20;
6
+ const talkerTools = ["ask_agent", "read"];
7
+ const askAgent = {
8
+ name: "ask_agent",
9
+ description: "Hand a request to the coding agent. Use for anything that needs files, the browser, tools or more than a few seconds of thought.",
10
+ parameters: Type.Object({
11
+ request: Type.String(),
12
+ context: Type.Optional(Type.String())
13
+ })
14
+ };
15
+ class PushStream {
16
+ #queue = [];
17
+ #waiters = [];
18
+ #closed = false;
19
+ push(value) {
20
+ if (this.#closed) return;
21
+ const waiter = this.#waiters.shift();
22
+ if (waiter) waiter({ value, done: false });
23
+ else this.#queue.push(value);
24
+ }
25
+ close() {
26
+ if (this.#closed) return;
27
+ this.#closed = true;
28
+ while (this.#waiters.length > 0) this.#waiters.shift()?.({ done: true });
29
+ }
30
+ async *[Symbol.asyncIterator]() {
31
+ while (true) {
32
+ const queued = this.#queue.shift();
33
+ if (queued !== void 0) {
34
+ yield queued;
35
+ continue;
36
+ }
37
+ if (this.#closed) return;
38
+ const next = await new Promise(
39
+ (resolve) => this.#waiters.push(resolve)
40
+ );
41
+ if (next.done || next.value === void 0) return;
42
+ yield next.value;
43
+ }
44
+ }
45
+ }
46
+ function send(action) {
47
+ try {
48
+ action();
49
+ } catch (error) {
50
+ if (!(error instanceof VoiceRequestError)) throw error;
51
+ }
52
+ }
53
+ function textOf(message) {
54
+ return message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
55
+ }
56
+ function acknowledged(call, ack) {
57
+ return {
58
+ role: "toolResult",
59
+ toolCallId: call.id,
60
+ toolName: call.name,
61
+ content: [{ type: "text", text: ack }],
62
+ isError: false,
63
+ timestamp: Date.now()
64
+ };
65
+ }
66
+ function errorResult(call, error) {
67
+ return {
68
+ role: "toolResult",
69
+ toolCallId: call.id,
70
+ toolName: call.name,
71
+ content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
72
+ isError: true,
73
+ timestamp: Date.now()
74
+ };
75
+ }
76
+ function capResult(result, maxChars) {
77
+ let left = maxChars;
78
+ const content = result.content.map((part) => {
79
+ if (part.type !== "text") return part;
80
+ if (left <= 0) return { ...part, text: "" };
81
+ if (part.text.length <= left) {
82
+ left -= part.text.length;
83
+ return part;
84
+ }
85
+ const text = part.text.slice(0, left);
86
+ left = 0;
87
+ return { ...part, text };
88
+ });
89
+ return { ...result, content };
90
+ }
91
+ function bindThinker(session, thinker, direct, options) {
92
+ const captions = new PushStream();
93
+ const phases = new PushStream();
94
+ const done = run(session, thinker, direct, options, captions, phases).finally(() => {
95
+ captions.close();
96
+ phases.close();
97
+ captions.close();
98
+ phases.close();
99
+ });
100
+ return { captions, phases, done };
101
+ }
102
+ async function run(session, thinker, direct, options, captions, phases) {
103
+ const heard = [];
104
+ const running = /* @__PURE__ */ new Map();
105
+ for await (const event of session) {
106
+ if (event.type === "input_transcript" && event.final) heard.push(`user: ${event.text}`);
107
+ if (event.type === "output_transcript" && event.final) heard.push(`talker: ${event.text}`);
108
+ if (event.type === "tool_call_cancelled") {
109
+ const job2 = running.get(event.callId);
110
+ if (!job2) continue;
111
+ job2.abort.abort();
112
+ if (!job2.answered) {
113
+ job2.answered = true;
114
+ send(() => session.respond(event.callId, errorResult(job2.call, new Error("cancelled"))));
115
+ }
116
+ continue;
117
+ }
118
+ if (event.type !== "tool_call") continue;
119
+ const call = event.call;
120
+ if (call.name !== askAgent.name) {
121
+ phases.push({
122
+ phase: "tool",
123
+ toolName: call.name,
124
+ toolCallId: call.id,
125
+ args: call.arguments
126
+ });
127
+ void direct(call, AbortSignal.timeout(options.directTimeoutMs)).then((result) => {
128
+ send(() => session.respond(call.id, capResult(result, options.directMaxChars)));
129
+ phases.push({ phase: "idle" });
130
+ }).catch((error) => {
131
+ send(() => session.respond(call.id, errorResult(call, error)));
132
+ phases.push({ phase: "idle" });
133
+ });
134
+ continue;
135
+ }
136
+ const job = { abort: new AbortController(), answered: event.blocking, call };
137
+ running.set(call.id, job);
138
+ phases.push({ phase: "thinking" });
139
+ if (event.blocking) send(() => session.respond(call.id, acknowledged(call, options.ack)));
140
+ void followAsk(
141
+ session,
142
+ thinker,
143
+ options,
144
+ captions,
145
+ phases,
146
+ running,
147
+ heard,
148
+ call,
149
+ event.blocking,
150
+ job
151
+ );
152
+ }
153
+ }
154
+ async function followAsk(session, thinker, options, captions, phases, running, heard, call, blocking, job) {
155
+ try {
156
+ for await (const update of thinker.ask(
157
+ String(call.arguments.request),
158
+ heard.slice(-heardLimit),
159
+ job.abort.signal
160
+ )) {
161
+ if (job.abort.signal.aborted) break;
162
+ if (update.type === "progress") {
163
+ captions.push({ type: "thinker_progress", callId: call.id, text: update.text });
164
+ send(() => session.inject(update.text, options.progress));
165
+ continue;
166
+ }
167
+ job.answered = true;
168
+ captions.push({
169
+ type: "thinker_result",
170
+ callId: call.id,
171
+ summary: textOf(update.message),
172
+ isError: update.message.isError
173
+ });
174
+ if (blocking) send(() => session.inject(textOf(update.message), options.result));
175
+ else send(() => session.respond(call.id, update.message, options.result));
176
+ }
177
+ } catch (error) {
178
+ if (!job.abort.signal.aborted && !job.answered) {
179
+ job.answered = true;
180
+ send(() => session.respond(call.id, errorResult(call, error)));
181
+ }
182
+ } finally {
183
+ running.delete(call.id);
184
+ phases.push({ phase: "idle" });
185
+ }
186
+ }
187
+ export {
188
+ askAgent,
189
+ bindThinker,
190
+ talkerTools
191
+ };
192
+ //# sourceMappingURL=talker.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/talker.ts"],
4
+ "sourcesContent": ["import {\n type JsonObject,\n type Tool,\n type ToolCall,\n type ToolResultMessage,\n Type,\n} from '@earendil-works/pi-ai';\nimport { VoiceRequestError } from './errors.ts';\nimport type { Delivery } from './models.ts';\nimport type { RealtimeSession } from './realtime.ts';\n\nconst heardLimit = 20;\n\nexport const talkerTools = ['ask_agent', 'read'] as const;\n\nexport const askAgent: Tool = {\n name: 'ask_agent',\n description:\n 'Hand a request to the coding agent. Use for anything that needs files, the browser, tools or more than a few seconds of thought.',\n parameters: Type.Object({\n request: Type.String(),\n context: Type.Optional(Type.String()),\n }),\n};\n\nexport type ThinkerUpdate =\n | { type: 'progress'; text: string }\n | { type: 'result'; message: ToolResultMessage };\n\nexport interface Thinker {\n ask(request: string, heard: string[], signal: AbortSignal): AsyncIterable<ThinkerUpdate>;\n}\n\nexport interface BindThinkerOptions {\n progress: Delivery;\n result: Delivery;\n ack: string;\n directTimeoutMs: number;\n directMaxChars: number;\n}\n\nexport type ThinkerCaption =\n | { type: 'thinker_progress'; callId: string; text: string }\n | { type: 'thinker_result'; callId: string; summary: string; isError: boolean };\n\nexport type ThinkerPhase =\n | { phase: 'thinking' }\n | { phase: 'tool'; toolName: string; toolCallId: string; args: JsonObject }\n | { phase: 'idle' };\n\nexport interface BoundThinker {\n captions: AsyncIterable<ThinkerCaption>;\n phases: AsyncIterable<ThinkerPhase>;\n done: Promise<void>;\n}\n\nclass PushStream<T> implements AsyncIterable<T> {\n readonly #queue: T[] = [];\n readonly #waiters: Array<(result: { value?: T; done: boolean }) => void> = [];\n #closed = false;\n\n push(value: T): void {\n if (this.#closed) return;\n const waiter = this.#waiters.shift();\n if (waiter) waiter({ value, done: false });\n else this.#queue.push(value);\n }\n\n close(): void {\n if (this.#closed) return;\n this.#closed = true;\n while (this.#waiters.length > 0) this.#waiters.shift()?.({ done: true });\n }\n\n async *[Symbol.asyncIterator](): AsyncIterator<T> {\n while (true) {\n const queued = this.#queue.shift();\n if (queued !== undefined) {\n yield queued;\n continue;\n }\n if (this.#closed) return;\n const next = await new Promise<{ value?: T; done: boolean }>((resolve) =>\n this.#waiters.push(resolve)\n );\n if (next.done || next.value === undefined) return;\n yield next.value;\n }\n }\n}\n\nfunction send(action: () => void): void {\n try {\n action();\n } catch (error) {\n if (!(error instanceof VoiceRequestError)) throw error;\n }\n}\n\nfunction textOf(message: ToolResultMessage): string {\n return message.content\n .filter((part) => part.type === 'text')\n .map((part) => part.text)\n .join('\\n');\n}\n\nfunction acknowledged(call: ToolCall, ack: string): ToolResultMessage {\n return {\n role: 'toolResult',\n toolCallId: call.id,\n toolName: call.name,\n content: [{ type: 'text', text: ack }],\n isError: false,\n timestamp: Date.now(),\n };\n}\n\nfunction errorResult(call: ToolCall, error: unknown): ToolResultMessage {\n return {\n role: 'toolResult',\n toolCallId: call.id,\n toolName: call.name,\n content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],\n isError: true,\n timestamp: Date.now(),\n };\n}\n\nfunction capResult(result: ToolResultMessage, maxChars: number): ToolResultMessage {\n let left = maxChars;\n const content = result.content.map((part) => {\n if (part.type !== 'text') return part;\n if (left <= 0) return { ...part, text: '' };\n if (part.text.length <= left) {\n left -= part.text.length;\n return part;\n }\n const text = part.text.slice(0, left);\n left = 0;\n return { ...part, text };\n });\n return { ...result, content };\n}\n\ninterface AskJob {\n abort: AbortController;\n answered: boolean;\n call: ToolCall;\n}\n\nexport function bindThinker(\n session: RealtimeSession,\n thinker: Thinker,\n direct: (call: ToolCall, signal: AbortSignal) => Promise<ToolResultMessage>,\n options: BindThinkerOptions\n): BoundThinker {\n const captions = new PushStream<ThinkerCaption>();\n const phases = new PushStream<ThinkerPhase>();\n const done = run(session, thinker, direct, options, captions, phases).finally(() => {\n captions.close();\n phases.close();\n captions.close();\n phases.close();\n });\n return { captions, phases, done };\n}\n\nasync function run(\n session: RealtimeSession,\n thinker: Thinker,\n direct: (call: ToolCall, signal: AbortSignal) => Promise<ToolResultMessage>,\n options: BindThinkerOptions,\n captions: PushStream<ThinkerCaption>,\n phases: PushStream<ThinkerPhase>\n): Promise<void> {\n const heard: string[] = [];\n const running = new Map<string, AskJob>();\n for await (const event of session) {\n if (event.type === 'input_transcript' && event.final) heard.push(`user: ${event.text}`);\n if (event.type === 'output_transcript' && event.final) heard.push(`talker: ${event.text}`);\n if (event.type === 'tool_call_cancelled') {\n const job = running.get(event.callId);\n if (!job) continue;\n job.abort.abort();\n if (!job.answered) {\n job.answered = true;\n send(() => session.respond(event.callId, errorResult(job.call, new Error('cancelled'))));\n }\n continue;\n }\n if (event.type !== 'tool_call') continue;\n const call = event.call;\n if (call.name !== askAgent.name) {\n phases.push({\n phase: 'tool',\n toolName: call.name,\n toolCallId: call.id,\n args: call.arguments,\n });\n void direct(call, AbortSignal.timeout(options.directTimeoutMs))\n .then((result) => {\n send(() => session.respond(call.id, capResult(result, options.directMaxChars)));\n phases.push({ phase: 'idle' });\n })\n .catch((error: unknown) => {\n send(() => session.respond(call.id, errorResult(call, error)));\n phases.push({ phase: 'idle' });\n });\n continue;\n }\n const job: AskJob = { abort: new AbortController(), answered: event.blocking, call };\n running.set(call.id, job);\n phases.push({ phase: 'thinking' });\n if (event.blocking) send(() => session.respond(call.id, acknowledged(call, options.ack)));\n void followAsk(\n session,\n thinker,\n options,\n captions,\n phases,\n running,\n heard,\n call,\n event.blocking,\n job\n );\n }\n}\n\nasync function followAsk(\n session: RealtimeSession,\n thinker: Thinker,\n options: BindThinkerOptions,\n captions: PushStream<ThinkerCaption>,\n phases: PushStream<ThinkerPhase>,\n running: Map<string, AskJob>,\n heard: string[],\n call: ToolCall,\n blocking: boolean,\n job: AskJob\n): Promise<void> {\n try {\n for await (const update of thinker.ask(\n String(call.arguments.request),\n heard.slice(-heardLimit),\n job.abort.signal\n )) {\n if (job.abort.signal.aborted) break;\n if (update.type === 'progress') {\n captions.push({ type: 'thinker_progress', callId: call.id, text: update.text });\n send(() => session.inject(update.text, options.progress));\n continue;\n }\n job.answered = true;\n captions.push({\n type: 'thinker_result',\n callId: call.id,\n summary: textOf(update.message),\n isError: update.message.isError,\n });\n if (blocking) send(() => session.inject(textOf(update.message), options.result));\n else send(() => session.respond(call.id, update.message, options.result));\n }\n } catch (error) {\n if (!job.abort.signal.aborted && !job.answered) {\n job.answered = true;\n send(() => session.respond(call.id, errorResult(call, error)));\n }\n } finally {\n running.delete(call.id);\n phases.push({ phase: 'idle' });\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["job"]
7
+ }
package/dist/turns.js ADDED
@@ -0,0 +1,347 @@
1
+ import { VoiceRequestError } from "./errors.js";
2
+ import { pushQueue } from "./queue.js";
3
+ import {
4
+ createSpokenState,
5
+ spokenPieces
6
+ } from "./spoken.js";
7
+ function voiceTurns(models, conversation, mic, out, options, signal) {
8
+ const phases = pushQueue();
9
+ const captions = pushQueue();
10
+ const session = { models, conversation, mic, out, options, signal, phases, captions };
11
+ const done = runSession(session).finally(() => {
12
+ phases.end();
13
+ captions.end();
14
+ });
15
+ return { phases: phases.iterable, captions: captions.iterable, done };
16
+ }
17
+ async function runSession(session) {
18
+ const mode = bargeMode(session.options);
19
+ let active;
20
+ let confidence;
21
+ let reserved = 0;
22
+ let openId = "";
23
+ const openTurn = () => {
24
+ if (!openId) {
25
+ reserved += 1;
26
+ openId = String(reserved);
27
+ }
28
+ return openId;
29
+ };
30
+ const stopPlayback = session.out.onPlayback((tag) => {
31
+ if (!active || !("entryId" in tag.span)) return;
32
+ active.cut = {
33
+ entryId: tag.span.entryId,
34
+ contentIndex: tag.span.contentIndex,
35
+ heardUpTo: tag.span.end,
36
+ heardMs: session.out.playedMs
37
+ };
38
+ });
39
+ const setPhase = phaseSetter(session.phases);
40
+ try {
41
+ if (session.signal.aborted) return;
42
+ setPhase({ phase: "listening" });
43
+ for await (const event of session.models.listen(session.options.ears, session.mic, {
44
+ signal: session.signal
45
+ })) {
46
+ if (event.type === "error") {
47
+ if (event.reason === "aborted" || session.signal.aborted) return;
48
+ throw new VoiceRequestError(event.reason, event.error);
49
+ }
50
+ const captionId = event.type === "partial" || event.type === "final" || event.type === "turn_end" ? openTurn() : openId;
51
+ confidence = noteUser(session, event, captionId, confidence);
52
+ if (shouldBarge(mode, event)) active = await interrupt(session, active);
53
+ if (event.type !== "turn_end") continue;
54
+ const turnId = openId || openTurn();
55
+ openId = "";
56
+ if (mode === "ignore") active = await finishTurn(active);
57
+ else active = await interrupt(session, active);
58
+ setPhase({ phase: "heard", text: event.text });
59
+ const meta = turnMeta(session.options, confidence);
60
+ confidence = void 0;
61
+ active = startTurn(session, setPhase, turnId, event, meta);
62
+ }
63
+ } finally {
64
+ stopPlayback();
65
+ if (session.signal.aborted && active) await interrupt(session, active);
66
+ else if (active) await active.done;
67
+ setPhase({ phase: "idle" });
68
+ }
69
+ }
70
+ function turnMeta(options, confidence) {
71
+ return {
72
+ source: "voice",
73
+ transcriptionModel: options.ears.id,
74
+ ...confidence === void 0 ? {} : { confidence }
75
+ };
76
+ }
77
+ function noteUser(session, event, turnId, confidence) {
78
+ if (event.type === "partial") {
79
+ session.captions.push({
80
+ type: "user_partial",
81
+ turnId,
82
+ segmentId: event.segmentId,
83
+ text: event.text
84
+ });
85
+ }
86
+ if (event.type === "final") {
87
+ session.captions.push({
88
+ type: "user_final",
89
+ turnId,
90
+ segmentId: event.segmentId,
91
+ text: event.text,
92
+ ...event.confidence === void 0 ? {} : { confidence: event.confidence }
93
+ });
94
+ return event.confidence ?? confidence;
95
+ }
96
+ if (event.type === "turn_end") {
97
+ session.captions.push({ type: "user_final", turnId, segmentId: "turn", text: event.text });
98
+ }
99
+ return confidence;
100
+ }
101
+ async function finishTurn(active) {
102
+ if (!active) return void 0;
103
+ await active.done;
104
+ return void 0;
105
+ }
106
+ function startTurn(session, setPhase, turnId, event, meta) {
107
+ const linked = linkSignal(session.signal);
108
+ const active = {
109
+ controller: linked.controller,
110
+ unlink: linked.unlink,
111
+ turnId,
112
+ utteranceId: `${turnId}:agent`,
113
+ done: Promise.resolve()
114
+ };
115
+ active.done = performTurn(session, setPhase, active, event.text, event.at, meta).finally(() => {
116
+ linked.unlink();
117
+ });
118
+ return active;
119
+ }
120
+ async function performTurn(session, setPhase, active, text, at, meta) {
121
+ const signal = active.controller.signal;
122
+ try {
123
+ setPhase({ phase: "thinking" });
124
+ const message = { role: "user", content: text, timestamp: at };
125
+ const feed = textFeed(signal);
126
+ const maps = [];
127
+ const producing = readAgent(session, setPhase, active, message, meta, feed, maps);
128
+ const audio = session.models.speak(
129
+ session.options.mouth,
130
+ { text: feed.iterable, voice: session.options.voice },
131
+ { signal }
132
+ );
133
+ await Promise.all([producing, pipeSpeech(session, setPhase, active, audio, maps)]);
134
+ if (!signal.aborted) setPhase({ phase: "listening" });
135
+ } catch (error) {
136
+ if (signal.aborted || session.signal.aborted) return;
137
+ throw error;
138
+ }
139
+ }
140
+ async function readAgent(session, setPhase, active, message, meta, feed, maps) {
141
+ const state = createSpokenState();
142
+ let spokenChars = 0;
143
+ let generated = "";
144
+ try {
145
+ for await (const event of session.conversation.prompt(
146
+ message,
147
+ meta,
148
+ active.controller.signal
149
+ )) {
150
+ if (active.controller.signal.aborted) break;
151
+ notePhase(setPhase, event);
152
+ for (const piece of spokenPieces(event, state, session.options)) {
153
+ spokenChars += piece.text.length;
154
+ generated = rememberPiece(
155
+ session,
156
+ active,
157
+ piece,
158
+ maps,
159
+ spokenChars,
160
+ generated,
161
+ state.entryId
162
+ );
163
+ feed.push(piece.text);
164
+ }
165
+ if (state.entryId) bindEntry(maps, state.entryId);
166
+ }
167
+ } finally {
168
+ feed.end();
169
+ }
170
+ }
171
+ function rememberPiece(session, active, piece, maps, spokenEnd, generated, entryId) {
172
+ const bound = piece.entryId ?? entryId;
173
+ maps.push({
174
+ spokenStart: spokenEnd - piece.text.length,
175
+ spokenEnd,
176
+ contentIndex: piece.contentIndex,
177
+ start: piece.start,
178
+ end: piece.end,
179
+ kind: piece.kind,
180
+ ...bound ? { entryId: bound } : {}
181
+ });
182
+ if (piece.kind === "announce") {
183
+ session.captions.push({ type: "announce", turnId: active.turnId, text: piece.text });
184
+ return generated;
185
+ }
186
+ const text = generated + piece.text;
187
+ session.captions.push({
188
+ type: "agent_generated",
189
+ turnId: active.turnId,
190
+ utteranceId: active.utteranceId,
191
+ text
192
+ });
193
+ return text;
194
+ }
195
+ async function pipeSpeech(session, setPhase, active, audio, maps) {
196
+ for await (const event of audio) {
197
+ if (active.controller.signal.aborted) return;
198
+ if (event.type === "error") {
199
+ if (event.reason === "aborted") return;
200
+ throw new VoiceRequestError(event.reason, event.error);
201
+ }
202
+ if (event.type !== "audio") continue;
203
+ setPhase({ phase: "speaking" });
204
+ const span = translateSpan(event.span, maps);
205
+ const tag = {
206
+ span,
207
+ ...event.alignment ? { alignment: event.alignment } : {}
208
+ };
209
+ session.out.write(event.chunk, tag);
210
+ }
211
+ }
212
+ function translateSpan(span, maps) {
213
+ if ("entryId" in span) return span;
214
+ const map = answerMap(span, maps);
215
+ if (!map?.entryId) return span;
216
+ const spokenLen = map.spokenEnd - map.spokenStart;
217
+ const sourceLen = map.end - map.start;
218
+ const start = map.start + scale(Math.max(0, span.start - map.spokenStart), spokenLen, sourceLen);
219
+ const end = map.start + scale(Math.min(spokenLen, span.end - map.spokenStart), spokenLen, sourceLen);
220
+ return { entryId: map.entryId, contentIndex: map.contentIndex, start, end };
221
+ }
222
+ function answerMap(span, maps) {
223
+ for (let index = maps.length - 1; index >= 0; index -= 1) {
224
+ const item = maps[index];
225
+ if (item?.kind !== "answer" || !item.entryId) continue;
226
+ if (item.spokenStart < span.end && item.spokenEnd > span.start) return item;
227
+ }
228
+ return void 0;
229
+ }
230
+ function scale(offset, spokenLen, sourceLen) {
231
+ if (spokenLen <= 0) return 0;
232
+ return Math.round(offset * sourceLen / spokenLen);
233
+ }
234
+ function bindEntry(maps, entryId) {
235
+ for (const map of maps) {
236
+ if (map.kind === "answer" && map.entryId === void 0) map.entryId = entryId;
237
+ }
238
+ }
239
+ function notePhase(setPhase, event) {
240
+ if (event.type === "run_start" || event.type === "turn_start") setPhase({ phase: "thinking" });
241
+ if (event.type === "compaction_start") setPhase({ phase: "compacting" });
242
+ if (event.type === "auto_retry_start" && event.attempt !== void 0 && event.at !== void 0) {
243
+ setPhase({ phase: "retrying", attempt: event.attempt, at: event.at });
244
+ }
245
+ if (event.type === "tool_execution_start" && event.toolName && event.toolCallId) {
246
+ setPhase({
247
+ phase: "tool",
248
+ toolName: event.toolName,
249
+ toolCallId: event.toolCallId,
250
+ args: event.args ?? {}
251
+ });
252
+ }
253
+ }
254
+ async function interrupt(session, active) {
255
+ if (!active) return void 0;
256
+ active.controller.abort();
257
+ session.out.flush();
258
+ await active.done;
259
+ if (active.cut) {
260
+ session.captions.push({
261
+ type: "agent_cut",
262
+ turnId: active.turnId,
263
+ utteranceId: active.utteranceId,
264
+ heardUpTo: active.cut.heardUpTo
265
+ });
266
+ await session.conversation.truncate(active.cut);
267
+ }
268
+ return void 0;
269
+ }
270
+ function bargeMode(options) {
271
+ const mode = options.bargeIn ?? "interrupt";
272
+ if (mode !== "auto") return mode;
273
+ if (options.echoRecommendation === "interrupt") return "interrupt";
274
+ if (options.echoRecommendation === "headphones") return "ignore";
275
+ return "onWords";
276
+ }
277
+ function shouldBarge(mode, event) {
278
+ if (mode === "ignore") return false;
279
+ if (mode === "interrupt") return event.type === "speech_start";
280
+ return event.type === "partial" && event.text.trim().length > 0;
281
+ }
282
+ function phaseSetter(phases) {
283
+ let previous = "";
284
+ return (phase) => {
285
+ const key = JSON.stringify(phase);
286
+ if (key === previous) return;
287
+ previous = key;
288
+ phases.push(phase);
289
+ };
290
+ }
291
+ function linkSignal(parent) {
292
+ const controller = new AbortController();
293
+ const abort = () => controller.abort();
294
+ if (parent.aborted) controller.abort();
295
+ else parent.addEventListener("abort", abort);
296
+ return {
297
+ controller,
298
+ unlink: () => parent.removeEventListener("abort", abort)
299
+ };
300
+ }
301
+ function textFeed(signal) {
302
+ const queue = [];
303
+ let ended = false;
304
+ let wake;
305
+ const notify = () => {
306
+ const current = wake;
307
+ wake = void 0;
308
+ current?.();
309
+ };
310
+ const onAbort = () => notify();
311
+ signal.addEventListener("abort", onAbort);
312
+ return {
313
+ push(text) {
314
+ if (ended || signal.aborted || text.length === 0) return;
315
+ queue.push(text);
316
+ notify();
317
+ },
318
+ end() {
319
+ ended = true;
320
+ notify();
321
+ },
322
+ iterable: {
323
+ async *[Symbol.asyncIterator]() {
324
+ try {
325
+ while (!signal.aborted) {
326
+ const next = queue.shift();
327
+ if (next !== void 0) {
328
+ yield next;
329
+ continue;
330
+ }
331
+ if (ended) return;
332
+ await new Promise((resolve) => {
333
+ if (queue.length > 0 || ended || signal.aborted) resolve();
334
+ else wake = resolve;
335
+ });
336
+ }
337
+ } finally {
338
+ signal.removeEventListener("abort", onAbort);
339
+ }
340
+ }
341
+ }
342
+ };
343
+ }
344
+ export {
345
+ voiceTurns
346
+ };
347
+ //# sourceMappingURL=turns.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/turns.ts"],
4
+ "sourcesContent": ["import type {\n AssistantMessageEvent,\n JsonObject,\n ProviderRequestOptions,\n ToolCall,\n UserMessage,\n} from '@earendil-works/pi-ai';\nimport type { AudioSink, AudioSource, AudioTag, TextSpan } from './audio.ts';\nimport { VoiceRequestError } from './errors.ts';\nimport type { SpeechModel, TranscriptionModel } from './models.ts';\nimport { pushQueue } from './queue.ts';\nimport type { SpeechEvent, SpeechInput } from './speech.ts';\nimport {\n createSpokenState,\n type SpokenPiece,\n spokenPieces,\n type VoiceAgentEvent,\n} from './spoken.ts';\nimport type { ListenOptions, TranscriptionEvent } from './transcription.ts';\n\nexport interface VoiceTurnMeta {\n source: 'voice';\n transcriptionModel: string;\n confidence?: number;\n}\n\nexport interface VoiceCut {\n entryId: string;\n contentIndex: number;\n heardUpTo: number;\n heardMs: number;\n}\n\nexport interface VoiceConversation {\n prompt(\n message: UserMessage,\n meta: VoiceTurnMeta,\n signal: AbortSignal\n ): AsyncIterable<VoiceAgentEvent>;\n truncate(cut: VoiceCut): Promise<void>;\n}\n\nexport type BargeIn = 'interrupt' | 'onWords' | 'ignore' | 'auto';\n\nexport type EchoRecommendation = 'interrupt' | 'onWords' | 'headphones';\n\nexport interface VoiceTurnOptions {\n ears: TranscriptionModel<string>;\n mouth: SpeechModel<string>;\n voice: string;\n speakable?: (event: AssistantMessageEvent) => string | undefined;\n announce?: (call: ToolCall) => string | undefined;\n bargeIn?: BargeIn;\n echoRecommendation?: EchoRecommendation;\n}\n\nexport interface VoiceModels {\n listen(\n model: TranscriptionModel<string>,\n audio: AudioSource,\n options?: ListenOptions\n ): AsyncIterable<TranscriptionEvent>;\n speak(\n model: SpeechModel<string>,\n input: SpeechInput,\n options?: ProviderRequestOptions\n ): AsyncIterable<SpeechEvent>;\n}\n\nexport type VoicePhase =\n | { phase: 'listening' }\n | { phase: 'heard'; text: string }\n | { phase: 'thinking' }\n | { phase: 'tool'; toolName: string; toolCallId: string; args: JsonObject }\n | { phase: 'retrying'; attempt: number; at: number }\n | { phase: 'compacting' }\n | { phase: 'speaking' }\n | { phase: 'idle' };\n\nexport type CaptionEvent =\n | { type: 'user_partial'; turnId: string; segmentId: string; text: string }\n | { type: 'user_final'; turnId: string; segmentId: string; text: string; confidence?: number }\n | { type: 'agent_generated'; turnId: string; utteranceId: string; text: string }\n | { type: 'agent_spoken'; turnId: string; utteranceId: string; upTo: number }\n | { type: 'agent_cut'; turnId: string; utteranceId: string; heardUpTo: number }\n | { type: 'announce'; turnId: string; text: string }\n | { type: 'thinker_progress'; turnId: string; callId: string; text: string }\n | { type: 'thinker_result'; turnId: string; callId: string; summary: string; isError: boolean };\n\nexport interface VoiceSession {\n phases: AsyncIterable<VoicePhase>;\n captions: AsyncIterable<CaptionEvent>;\n done: Promise<void>;\n}\n\ninterface SpokenMap {\n spokenStart: number;\n spokenEnd: number;\n contentIndex: number;\n start: number;\n end: number;\n entryId?: string;\n kind: SpokenPiece['kind'];\n}\n\ninterface ActiveTurn {\n controller: AbortController;\n unlink: () => void;\n done: Promise<void>;\n turnId: string;\n utteranceId: string;\n cut?: VoiceCut;\n}\n\nexport function voiceTurns(\n models: VoiceModels,\n conversation: VoiceConversation,\n mic: AudioSource,\n out: AudioSink,\n options: VoiceTurnOptions,\n signal: AbortSignal\n): VoiceSession {\n const phases = pushQueue<VoicePhase>();\n const captions = pushQueue<CaptionEvent>();\n const session: Session = { models, conversation, mic, out, options, signal, phases, captions };\n const done = runSession(session).finally(() => {\n phases.end();\n captions.end();\n });\n return { phases: phases.iterable, captions: captions.iterable, done };\n}\n\ninterface Session {\n models: VoiceModels;\n conversation: VoiceConversation;\n mic: AudioSource;\n out: AudioSink;\n options: VoiceTurnOptions;\n signal: AbortSignal;\n phases: ReturnType<typeof pushQueue<VoicePhase>>;\n captions: ReturnType<typeof pushQueue<CaptionEvent>>;\n}\n\nasync function runSession(session: Session): Promise<void> {\n const mode = bargeMode(session.options);\n let active: ActiveTurn | undefined;\n let confidence: number | undefined;\n let reserved = 0;\n let openId = '';\n const openTurn = () => {\n if (!openId) {\n reserved += 1;\n openId = String(reserved);\n }\n return openId;\n };\n const stopPlayback = session.out.onPlayback((tag) => {\n if (!active || !('entryId' in tag.span)) return;\n active.cut = {\n entryId: tag.span.entryId,\n contentIndex: tag.span.contentIndex,\n heardUpTo: tag.span.end,\n heardMs: session.out.playedMs,\n };\n });\n const setPhase = phaseSetter(session.phases);\n try {\n if (session.signal.aborted) return;\n setPhase({ phase: 'listening' });\n for await (const event of session.models.listen(session.options.ears, session.mic, {\n signal: session.signal,\n })) {\n if (event.type === 'error') {\n if (event.reason === 'aborted' || session.signal.aborted) return;\n throw new VoiceRequestError(event.reason, event.error);\n }\n const captionId =\n event.type === 'partial' || event.type === 'final' || event.type === 'turn_end'\n ? openTurn()\n : openId;\n confidence = noteUser(session, event, captionId, confidence);\n if (shouldBarge(mode, event)) active = await interrupt(session, active);\n if (event.type !== 'turn_end') continue;\n const turnId = openId || openTurn();\n openId = '';\n if (mode === 'ignore') active = await finishTurn(active);\n else active = await interrupt(session, active);\n setPhase({ phase: 'heard', text: event.text });\n const meta = turnMeta(session.options, confidence);\n confidence = undefined;\n active = startTurn(session, setPhase, turnId, event, meta);\n }\n } finally {\n stopPlayback();\n if (session.signal.aborted && active) await interrupt(session, active);\n else if (active) await active.done;\n setPhase({ phase: 'idle' });\n }\n}\n\nfunction turnMeta(options: VoiceTurnOptions, confidence: number | undefined): VoiceTurnMeta {\n return {\n source: 'voice',\n transcriptionModel: options.ears.id,\n ...(confidence === undefined ? {} : { confidence }),\n };\n}\n\nfunction noteUser(\n session: Session,\n event: TranscriptionEvent,\n turnId: string,\n confidence: number | undefined\n): number | undefined {\n if (event.type === 'partial') {\n session.captions.push({\n type: 'user_partial',\n turnId,\n segmentId: event.segmentId,\n text: event.text,\n });\n }\n if (event.type === 'final') {\n session.captions.push({\n type: 'user_final',\n turnId,\n segmentId: event.segmentId,\n text: event.text,\n ...(event.confidence === undefined ? {} : { confidence: event.confidence }),\n });\n return event.confidence ?? confidence;\n }\n if (event.type === 'turn_end') {\n session.captions.push({ type: 'user_final', turnId, segmentId: 'turn', text: event.text });\n }\n return confidence;\n}\n\nasync function finishTurn(active: ActiveTurn | undefined): Promise<undefined> {\n if (!active) return undefined;\n await active.done;\n return undefined;\n}\n\nfunction startTurn(\n session: Session,\n setPhase: (phase: VoicePhase) => void,\n turnId: string,\n event: Extract<TranscriptionEvent, { type: 'turn_end' }>,\n meta: VoiceTurnMeta\n): ActiveTurn {\n const linked = linkSignal(session.signal);\n const active: ActiveTurn = {\n controller: linked.controller,\n unlink: linked.unlink,\n turnId,\n utteranceId: `${turnId}:agent`,\n done: Promise.resolve(),\n };\n active.done = performTurn(session, setPhase, active, event.text, event.at, meta).finally(() => {\n linked.unlink();\n });\n return active;\n}\n\nasync function performTurn(\n session: Session,\n setPhase: (phase: VoicePhase) => void,\n active: ActiveTurn,\n text: string,\n at: number,\n meta: VoiceTurnMeta\n): Promise<void> {\n const signal = active.controller.signal;\n try {\n setPhase({ phase: 'thinking' });\n const message: UserMessage = { role: 'user', content: text, timestamp: at };\n const feed = textFeed(signal);\n const maps: SpokenMap[] = [];\n const producing = readAgent(session, setPhase, active, message, meta, feed, maps);\n const audio = session.models.speak(\n session.options.mouth,\n { text: feed.iterable, voice: session.options.voice },\n { signal }\n );\n await Promise.all([producing, pipeSpeech(session, setPhase, active, audio, maps)]);\n if (!signal.aborted) setPhase({ phase: 'listening' });\n } catch (error) {\n if (signal.aborted || session.signal.aborted) return;\n throw error;\n }\n}\n\nasync function readAgent(\n session: Session,\n setPhase: (phase: VoicePhase) => void,\n active: ActiveTurn,\n message: UserMessage,\n meta: VoiceTurnMeta,\n feed: TextFeed,\n maps: SpokenMap[]\n): Promise<void> {\n const state = createSpokenState();\n let spokenChars = 0;\n let generated = '';\n try {\n for await (const event of session.conversation.prompt(\n message,\n meta,\n active.controller.signal\n )) {\n if (active.controller.signal.aborted) break;\n notePhase(setPhase, event);\n for (const piece of spokenPieces(event, state, session.options)) {\n spokenChars += piece.text.length;\n generated = rememberPiece(\n session,\n active,\n piece,\n maps,\n spokenChars,\n generated,\n state.entryId\n );\n feed.push(piece.text);\n }\n if (state.entryId) bindEntry(maps, state.entryId);\n }\n } finally {\n feed.end();\n }\n}\n\nfunction rememberPiece(\n session: Session,\n active: ActiveTurn,\n piece: SpokenPiece,\n maps: SpokenMap[],\n spokenEnd: number,\n generated: string,\n entryId: string | undefined\n): string {\n const bound = piece.entryId ?? entryId;\n maps.push({\n spokenStart: spokenEnd - piece.text.length,\n spokenEnd,\n contentIndex: piece.contentIndex,\n start: piece.start,\n end: piece.end,\n kind: piece.kind,\n ...(bound ? { entryId: bound } : {}),\n });\n if (piece.kind === 'announce') {\n session.captions.push({ type: 'announce', turnId: active.turnId, text: piece.text });\n return generated;\n }\n const text = generated + piece.text;\n session.captions.push({\n type: 'agent_generated',\n turnId: active.turnId,\n utteranceId: active.utteranceId,\n text,\n });\n return text;\n}\n\nasync function pipeSpeech(\n session: Session,\n setPhase: (phase: VoicePhase) => void,\n active: ActiveTurn,\n audio: AsyncIterable<SpeechEvent>,\n maps: SpokenMap[]\n): Promise<void> {\n for await (const event of audio) {\n if (active.controller.signal.aborted) return;\n if (event.type === 'error') {\n if (event.reason === 'aborted') return;\n throw new VoiceRequestError(event.reason, event.error);\n }\n if (event.type !== 'audio') continue;\n setPhase({ phase: 'speaking' });\n const span = translateSpan(event.span, maps);\n const tag: AudioTag = {\n span,\n ...(event.alignment ? { alignment: event.alignment } : {}),\n };\n session.out.write(event.chunk, tag);\n }\n}\n\nfunction translateSpan(span: TextSpan, maps: SpokenMap[]): TextSpan {\n if ('entryId' in span) return span;\n const map = answerMap(span, maps);\n if (!map?.entryId) return span;\n const spokenLen = map.spokenEnd - map.spokenStart;\n const sourceLen = map.end - map.start;\n const start = map.start + scale(Math.max(0, span.start - map.spokenStart), spokenLen, sourceLen);\n const end =\n map.start + scale(Math.min(spokenLen, span.end - map.spokenStart), spokenLen, sourceLen);\n return { entryId: map.entryId, contentIndex: map.contentIndex, start, end };\n}\n\nfunction answerMap(span: { start: number; end: number }, maps: SpokenMap[]): SpokenMap | undefined {\n for (let index = maps.length - 1; index >= 0; index -= 1) {\n const item = maps[index];\n if (item?.kind !== 'answer' || !item.entryId) continue;\n if (item.spokenStart < span.end && item.spokenEnd > span.start) return item;\n }\n return undefined;\n}\n\nfunction scale(offset: number, spokenLen: number, sourceLen: number): number {\n if (spokenLen <= 0) return 0;\n return Math.round((offset * sourceLen) / spokenLen);\n}\n\nfunction bindEntry(maps: SpokenMap[], entryId: string): void {\n for (const map of maps) {\n if (map.kind === 'answer' && map.entryId === undefined) map.entryId = entryId;\n }\n}\n\nfunction notePhase(setPhase: (phase: VoicePhase) => void, event: VoiceAgentEvent): void {\n if (event.type === 'run_start' || event.type === 'turn_start') setPhase({ phase: 'thinking' });\n if (event.type === 'compaction_start') setPhase({ phase: 'compacting' });\n if (event.type === 'auto_retry_start' && event.attempt !== undefined && event.at !== undefined) {\n setPhase({ phase: 'retrying', attempt: event.attempt, at: event.at });\n }\n if (event.type === 'tool_execution_start' && event.toolName && event.toolCallId) {\n setPhase({\n phase: 'tool',\n toolName: event.toolName,\n toolCallId: event.toolCallId,\n args: event.args ?? {},\n });\n }\n}\n\nasync function interrupt(session: Session, active: ActiveTurn | undefined): Promise<undefined> {\n if (!active) return undefined;\n active.controller.abort();\n session.out.flush();\n await active.done;\n if (active.cut) {\n session.captions.push({\n type: 'agent_cut',\n turnId: active.turnId,\n utteranceId: active.utteranceId,\n heardUpTo: active.cut.heardUpTo,\n });\n await session.conversation.truncate(active.cut);\n }\n return undefined;\n}\n\nfunction bargeMode(options: VoiceTurnOptions): 'interrupt' | 'onWords' | 'ignore' {\n const mode = options.bargeIn ?? 'interrupt';\n if (mode !== 'auto') return mode;\n if (options.echoRecommendation === 'interrupt') return 'interrupt';\n if (options.echoRecommendation === 'headphones') return 'ignore';\n return 'onWords';\n}\n\nfunction shouldBarge(mode: 'interrupt' | 'onWords' | 'ignore', event: TranscriptionEvent): boolean {\n if (mode === 'ignore') return false;\n if (mode === 'interrupt') return event.type === 'speech_start';\n return event.type === 'partial' && event.text.trim().length > 0;\n}\n\nfunction phaseSetter(phases: Session['phases']): (phase: VoicePhase) => void {\n let previous = '';\n return (phase) => {\n const key = JSON.stringify(phase);\n if (key === previous) return;\n previous = key;\n phases.push(phase);\n };\n}\n\nfunction linkSignal(parent: AbortSignal): { controller: AbortController; unlink: () => void } {\n const controller = new AbortController();\n const abort = () => controller.abort();\n if (parent.aborted) controller.abort();\n else parent.addEventListener('abort', abort);\n return {\n controller,\n unlink: () => parent.removeEventListener('abort', abort),\n };\n}\n\ninterface TextFeed {\n push(text: string): void;\n end(): void;\n iterable: AsyncIterable<string>;\n}\n\nfunction textFeed(signal: AbortSignal): TextFeed {\n const queue: string[] = [];\n let ended = false;\n let wake: (() => void) | undefined;\n const notify = () => {\n const current = wake;\n wake = undefined;\n current?.();\n };\n const onAbort = () => notify();\n signal.addEventListener('abort', onAbort);\n return {\n push(text) {\n if (ended || signal.aborted || text.length === 0) return;\n queue.push(text);\n notify();\n },\n end() {\n ended = true;\n notify();\n },\n iterable: {\n async *[Symbol.asyncIterator]() {\n try {\n while (!signal.aborted) {\n const next = queue.shift();\n if (next !== undefined) {\n yield next;\n continue;\n }\n if (ended) return;\n await new Promise<void>((resolve) => {\n if (queue.length > 0 || ended || signal.aborted) resolve();\n else wake = resolve;\n });\n }\n } finally {\n signal.removeEventListener('abort', onAbort);\n }\n },\n },\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -10,8 +10,13 @@ export { planDelivery, planTruncation, RealtimeLedger, realtimeSocket } from './
10
10
  export { connectRealtime, listen, registerRealtimeApiProvider, registerSpeechApiProvider, registerTranscriptionApiProvider, speak, transcribe, } from './registry.ts';
11
11
  export type { SpeechApiProvider, SpeechEvent, SpeechInput } from './speech.ts';
12
12
  export { SpeechEventStream } from './speech.ts';
13
+ export type { VoiceAgentEvent, VoiceMessageChange } from './spoken.ts';
14
+ export type { BindThinkerOptions, BoundThinker, Thinker, ThinkerCaption, ThinkerPhase, ThinkerUpdate, } from './talker.ts';
15
+ export { askAgent, bindThinker, talkerTools } from './talker.ts';
13
16
  export type { AudioInput, ListenOptions, Transcript, TranscriptionApiProvider, TranscriptionEvent, TranscriptSegment, } from './transcription.ts';
14
17
  export { TranscriptionEventStream } from './transcription.ts';
18
+ export type { BargeIn, CaptionEvent, EchoRecommendation, VoiceConversation, VoiceCut, VoiceModels, VoicePhase, VoiceSession, VoiceTurnMeta, VoiceTurnOptions, } from './turns.ts';
19
+ export { voiceTurns } from './turns.ts';
15
20
  export type { AudioCost, AudioCounters, AudioUsage, RealtimeAudioCost, SpeechAudioCost, TranscriptionAudioCost, } from './usage.ts';
16
21
  export { addAudioCounters, priceAudio } from './usage.ts';
17
22
  export type { VadHook, VadUpdate } from './vad.ts';
@@ -0,0 +1,6 @@
1
+ export interface PushQueue<T> {
2
+ push(value: T): void;
3
+ end(): void;
4
+ readonly iterable: AsyncIterable<T>;
5
+ }
6
+ export declare function pushQueue<T>(): PushQueue<T>;
@@ -0,0 +1,57 @@
1
+ import type { AssistantMessage, AssistantMessageEvent, JsonObject, TextContent, ThinkingContent, ToolCall } from '@earendil-works/pi-ai';
2
+ export interface VoiceMessageChange {
3
+ type: string;
4
+ contentIndex?: number;
5
+ delta?: string;
6
+ block?: TextContent | ThinkingContent | ToolCall;
7
+ message?: AssistantMessage;
8
+ }
9
+ export interface VoiceEntryRef {
10
+ id: string | number;
11
+ model?: readonly {
12
+ role: string;
13
+ content?: readonly (TextContent | ThinkingContent | ToolCall)[];
14
+ }[];
15
+ }
16
+ export interface VoiceAgentEvent {
17
+ type: string;
18
+ changes?: readonly VoiceMessageChange[];
19
+ message?: {
20
+ role: string;
21
+ content?: unknown;
22
+ };
23
+ entry?: VoiceEntryRef;
24
+ toolName?: string;
25
+ toolCallId?: string;
26
+ args?: JsonObject;
27
+ attempt?: number;
28
+ at?: number;
29
+ }
30
+ export interface SpokenPiece {
31
+ text: string;
32
+ contentIndex: number;
33
+ start: number;
34
+ end: number;
35
+ kind: 'answer' | 'announce';
36
+ entryId?: string;
37
+ }
38
+ interface BlockTrack {
39
+ kind: 'text' | 'thinking' | 'tool';
40
+ text: string;
41
+ sent: number;
42
+ signature?: string;
43
+ ended: boolean;
44
+ }
45
+ export interface SpokenState {
46
+ blocks: Map<number, BlockTrack>;
47
+ entryId?: string;
48
+ }
49
+ export interface SpokenOptions {
50
+ speakable?(event: AssistantMessageEvent): string | undefined;
51
+ announce?(call: ToolCall): string | undefined;
52
+ model?: string;
53
+ }
54
+ export declare function createSpokenState(): SpokenState;
55
+ export declare function isCommentary(signature: string | undefined): boolean;
56
+ export declare function spokenPieces(event: VoiceAgentEvent, state: SpokenState, options?: SpokenOptions): SpokenPiece[];
57
+ export {};
@@ -0,0 +1,48 @@
1
+ import { type JsonObject, type Tool, type ToolCall, type ToolResultMessage } from '@earendil-works/pi-ai';
2
+ import type { Delivery } from './models.ts';
3
+ import type { RealtimeSession } from './realtime.ts';
4
+ export declare const talkerTools: readonly ['ask_agent', 'read'];
5
+ export declare const askAgent: Tool;
6
+ export type ThinkerUpdate = {
7
+ type: 'progress';
8
+ text: string;
9
+ } | {
10
+ type: 'result';
11
+ message: ToolResultMessage;
12
+ };
13
+ export interface Thinker {
14
+ ask(request: string, heard: string[], signal: AbortSignal): AsyncIterable<ThinkerUpdate>;
15
+ }
16
+ export interface BindThinkerOptions {
17
+ progress: Delivery;
18
+ result: Delivery;
19
+ ack: string;
20
+ directTimeoutMs: number;
21
+ directMaxChars: number;
22
+ }
23
+ export type ThinkerCaption = {
24
+ type: 'thinker_progress';
25
+ callId: string;
26
+ text: string;
27
+ } | {
28
+ type: 'thinker_result';
29
+ callId: string;
30
+ summary: string;
31
+ isError: boolean;
32
+ };
33
+ export type ThinkerPhase = {
34
+ phase: 'thinking';
35
+ } | {
36
+ phase: 'tool';
37
+ toolName: string;
38
+ toolCallId: string;
39
+ args: JsonObject;
40
+ } | {
41
+ phase: 'idle';
42
+ };
43
+ export interface BoundThinker {
44
+ captions: AsyncIterable<ThinkerCaption>;
45
+ phases: AsyncIterable<ThinkerPhase>;
46
+ done: Promise<void>;
47
+ }
48
+ export declare function bindThinker(session: RealtimeSession, thinker: Thinker, direct: (call: ToolCall, signal: AbortSignal) => Promise<ToolResultMessage>, options: BindThinkerOptions): BoundThinker;
@@ -0,0 +1,107 @@
1
+ import type { AssistantMessageEvent, JsonObject, ProviderRequestOptions, ToolCall, UserMessage } from '@earendil-works/pi-ai';
2
+ import type { AudioSink, AudioSource } from './audio.ts';
3
+ import type { SpeechModel, TranscriptionModel } from './models.ts';
4
+ import type { SpeechEvent, SpeechInput } from './speech.ts';
5
+ import { type VoiceAgentEvent } from './spoken.ts';
6
+ import type { ListenOptions, TranscriptionEvent } from './transcription.ts';
7
+ export interface VoiceTurnMeta {
8
+ source: 'voice';
9
+ transcriptionModel: string;
10
+ confidence?: number;
11
+ }
12
+ export interface VoiceCut {
13
+ entryId: string;
14
+ contentIndex: number;
15
+ heardUpTo: number;
16
+ heardMs: number;
17
+ }
18
+ export interface VoiceConversation {
19
+ prompt(message: UserMessage, meta: VoiceTurnMeta, signal: AbortSignal): AsyncIterable<VoiceAgentEvent>;
20
+ truncate(cut: VoiceCut): Promise<void>;
21
+ }
22
+ export type BargeIn = 'interrupt' | 'onWords' | 'ignore' | 'auto';
23
+ export type EchoRecommendation = 'interrupt' | 'onWords' | 'headphones';
24
+ export interface VoiceTurnOptions {
25
+ ears: TranscriptionModel<string>;
26
+ mouth: SpeechModel<string>;
27
+ voice: string;
28
+ speakable?: (event: AssistantMessageEvent) => string | undefined;
29
+ announce?: (call: ToolCall) => string | undefined;
30
+ bargeIn?: BargeIn;
31
+ echoRecommendation?: EchoRecommendation;
32
+ }
33
+ export interface VoiceModels {
34
+ listen(model: TranscriptionModel<string>, audio: AudioSource, options?: ListenOptions): AsyncIterable<TranscriptionEvent>;
35
+ speak(model: SpeechModel<string>, input: SpeechInput, options?: ProviderRequestOptions): AsyncIterable<SpeechEvent>;
36
+ }
37
+ export type VoicePhase = {
38
+ phase: 'listening';
39
+ } | {
40
+ phase: 'heard';
41
+ text: string;
42
+ } | {
43
+ phase: 'thinking';
44
+ } | {
45
+ phase: 'tool';
46
+ toolName: string;
47
+ toolCallId: string;
48
+ args: JsonObject;
49
+ } | {
50
+ phase: 'retrying';
51
+ attempt: number;
52
+ at: number;
53
+ } | {
54
+ phase: 'compacting';
55
+ } | {
56
+ phase: 'speaking';
57
+ } | {
58
+ phase: 'idle';
59
+ };
60
+ export type CaptionEvent = {
61
+ type: 'user_partial';
62
+ turnId: string;
63
+ segmentId: string;
64
+ text: string;
65
+ } | {
66
+ type: 'user_final';
67
+ turnId: string;
68
+ segmentId: string;
69
+ text: string;
70
+ confidence?: number;
71
+ } | {
72
+ type: 'agent_generated';
73
+ turnId: string;
74
+ utteranceId: string;
75
+ text: string;
76
+ } | {
77
+ type: 'agent_spoken';
78
+ turnId: string;
79
+ utteranceId: string;
80
+ upTo: number;
81
+ } | {
82
+ type: 'agent_cut';
83
+ turnId: string;
84
+ utteranceId: string;
85
+ heardUpTo: number;
86
+ } | {
87
+ type: 'announce';
88
+ turnId: string;
89
+ text: string;
90
+ } | {
91
+ type: 'thinker_progress';
92
+ turnId: string;
93
+ callId: string;
94
+ text: string;
95
+ } | {
96
+ type: 'thinker_result';
97
+ turnId: string;
98
+ callId: string;
99
+ summary: string;
100
+ isError: boolean;
101
+ };
102
+ export interface VoiceSession {
103
+ phases: AsyncIterable<VoicePhase>;
104
+ captions: AsyncIterable<CaptionEvent>;
105
+ done: Promise<void>;
106
+ }
107
+ export declare function voiceTurns(models: VoiceModels, conversation: VoiceConversation, mic: AudioSource, out: AudioSink, options: VoiceTurnOptions, signal: AbortSignal): VoiceSession;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/pi-audio",
3
- "version": "1.6.0",
3
+ "version": "1.8.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",
@@ -57,7 +57,7 @@
57
57
  "release": "semantic-release"
58
58
  },
59
59
  "devDependencies": {
60
- "@ai-ecoverse/slicc-shared-web": "1.21.0",
60
+ "@ai-ecoverse/slicc-shared-web": "1.21.1",
61
61
  "@biomejs/biome": "2.5.15",
62
62
  "@earendil-works/pi-ai": "1.1.0",
63
63
  "@types/node": "24.19.1",