@ai-ecoverse/pi-audio 1.6.0 → 1.7.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 +2 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +2 -2
- package/dist/talker.js +192 -0
- package/dist/talker.js.map +7 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/talker.d.ts +48 -0
- package/package.json +1 -1
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,6 +23,7 @@ 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";
|
|
27
28
|
import { addAudioCounters, priceAudio } from "./usage.js";
|
|
28
29
|
import { energyVad } from "./vad.js";
|
|
@@ -32,6 +33,8 @@ export {
|
|
|
32
33
|
TranscriptionEventStream,
|
|
33
34
|
VoiceRequestError,
|
|
34
35
|
addAudioCounters,
|
|
36
|
+
askAgent,
|
|
37
|
+
bindThinker,
|
|
35
38
|
blankUsage,
|
|
36
39
|
connectRealtime,
|
|
37
40
|
convertAudio,
|
|
@@ -52,6 +55,7 @@ export {
|
|
|
52
55
|
registerVoiceModel,
|
|
53
56
|
resolveVoiceAuth,
|
|
54
57
|
speak,
|
|
58
|
+
talkerTools,
|
|
55
59
|
transcribe
|
|
56
60
|
};
|
|
57
61
|
//# 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 {\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 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,UAAU,aAAa,mBAAmB;AASnD,SAAS,gCAAgC;AASzC,SAAS,kBAAkB,kBAAkB;AAE7C,SAAS,iBAAiB;",
|
|
6
6
|
"names": []
|
|
7
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/types/index.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ 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 { BindThinkerOptions, BoundThinker, Thinker, ThinkerCaption, ThinkerPhase, ThinkerUpdate, } from './talker.ts';
|
|
14
|
+
export { askAgent, bindThinker, talkerTools } from './talker.ts';
|
|
13
15
|
export type { AudioInput, ListenOptions, Transcript, TranscriptionApiProvider, TranscriptionEvent, TranscriptSegment, } from './transcription.ts';
|
|
14
16
|
export { TranscriptionEventStream } from './transcription.ts';
|
|
15
17
|
export type { AudioCost, AudioCounters, AudioUsage, RealtimeAudioCost, SpeechAudioCost, TranscriptionAudioCost, } from './usage.ts';
|
|
@@ -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;
|
package/package.json
CHANGED