@fonoster/autopilot 0.27.0 → 0.31.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.
Files changed (153) hide show
  1. package/README.md +15 -52
  2. package/dist/index.d.ts +0 -5
  3. package/dist/index.js +0 -5
  4. package/dist/knowledge/AbstractKnowledgeBase.d.ts +3 -2
  5. package/dist/knowledge/AbstractKnowledgeBase.js +2 -7
  6. package/dist/knowledge/FilesKnowledgeBase.d.ts +1 -1
  7. package/dist/knowledge/MeteredEmbeddings.d.ts +47 -0
  8. package/dist/knowledge/MeteredEmbeddings.js +58 -0
  9. package/dist/knowledge/types.d.ts +1 -1
  10. package/dist/models/LanguageModelFactory.d.ts +5 -24
  11. package/dist/models/LanguageModelFactory.js +0 -1
  12. package/dist/models/createLanguageModel.d.ts +2 -2
  13. package/dist/models/createLanguageModel.js +2 -2
  14. package/dist/models/evaluations/createEvalEffectiveConfig.d.ts +10 -106
  15. package/dist/models/evaluations/createEvalEffectiveConfig.js +47 -7
  16. package/dist/models/evaluations/createEvaluateIntelligence.d.ts +21 -20
  17. package/dist/models/evaluations/createEvaluateIntelligence.js +53 -23
  18. package/dist/models/evaluations/createTestTextSimilarity.d.ts +7 -1
  19. package/dist/models/evaluations/createTestTextSimilarity.js +19 -1
  20. package/dist/models/evaluations/evalTestCases.d.ts +2 -2
  21. package/dist/models/evaluations/evalTestCases.js +7 -7
  22. package/dist/models/evaluations/evaluateScenario.js +2 -2
  23. package/dist/models/evaluations/evaluateStep.d.ts +1 -1
  24. package/dist/models/evaluations/evaluateStep.js +2 -2
  25. package/dist/models/evaluations/extractAiResponse.d.ts +2 -3
  26. package/dist/models/evaluations/extractAiResponse.js +3 -3
  27. package/dist/models/evaluations/index.d.ts +1 -0
  28. package/dist/models/evaluations/index.js +1 -0
  29. package/dist/models/evaluations/runEval.d.ts +2 -20
  30. package/dist/models/evaluations/runEval.js +7 -7
  31. package/dist/models/evaluations/session/EvalSession.d.ts +73 -0
  32. package/dist/models/evaluations/session/EvalSession.js +134 -0
  33. package/dist/models/evaluations/session/evaluateSessionScenario.d.ts +58 -0
  34. package/dist/models/evaluations/session/evaluateSessionScenario.js +182 -0
  35. package/dist/{vad → models/evaluations/session}/index.d.ts +3 -2
  36. package/dist/{vad → models/evaluations/session}/index.js +3 -2
  37. package/dist/models/evaluations/session/runSessionEval.d.ts +20 -0
  38. package/dist/models/evaluations/session/runSessionEval.js +59 -0
  39. package/dist/models/evaluations/textSimilaryPrompt.d.ts +1 -1
  40. package/dist/models/evaluations/textSimilaryPrompt.js +1 -1
  41. package/dist/models/evaluations/types.d.ts +7 -6
  42. package/dist/models/types.d.ts +19 -1
  43. package/dist/session/Agent.d.ts +173 -0
  44. package/dist/session/Agent.js +548 -0
  45. package/dist/{loadAssistantFromAPI.d.ts → session/AsyncQueue.d.ts} +12 -7
  46. package/dist/session/AsyncQueue.js +67 -0
  47. package/dist/{machine/guards/index.d.ts → session/config.d.ts} +2 -2
  48. package/dist/session/config.js +27 -0
  49. package/dist/session/createAutopilot.d.ts +48 -0
  50. package/dist/session/createAutopilot.js +151 -0
  51. package/dist/session/hosted.d.ts +454 -0
  52. package/dist/session/hosted.js +501 -0
  53. package/dist/{assistants → session}/index.d.ts +11 -2
  54. package/dist/{machine/actors → session}/index.js +11 -3
  55. package/dist/{machine/actions/appendSpeech.js → session/knowledge.d.ts} +14 -17
  56. package/dist/session/knowledge.js +10 -0
  57. package/dist/session/llm.d.ts +113 -0
  58. package/dist/session/llm.js +127 -0
  59. package/dist/session/machine.d.ts +206 -0
  60. package/dist/session/machine.js +126 -0
  61. package/dist/session/main.js +45 -0
  62. package/dist/session/meteredThink.d.ts +45 -0
  63. package/dist/session/meteredThink.js +125 -0
  64. package/dist/session/sentences.d.ts +5 -0
  65. package/dist/session/sentences.js +53 -0
  66. package/dist/session/telemetry.d.ts +34 -0
  67. package/dist/session/telemetry.js +55 -0
  68. package/dist/{machine/actors/index.d.ts → session/think.d.ts} +2 -3
  69. package/dist/{machine/actions/announceSystemError.js → session/think.js} +8 -10
  70. package/dist/session/tools.d.ts +22 -0
  71. package/dist/session/tools.js +83 -0
  72. package/dist/tools/builtin/transferToolDefinition.js +1 -1
  73. package/dist/voice/index.d.ts +0 -1
  74. package/dist/voice/index.js +0 -1
  75. package/dist/voice/types.d.ts +5 -18
  76. package/package.json +15 -16
  77. package/dist/Autopilot.d.ts +0 -14
  78. package/dist/Autopilot.js +0 -133
  79. package/dist/assistants/index.js +0 -36
  80. package/dist/assistants/loadAndValidateAssistant.d.ts +0 -3
  81. package/dist/assistants/loadAndValidateAssistant.js +0 -49
  82. package/dist/assistants/types.d.ts +0 -24
  83. package/dist/assistants/types.js +0 -2
  84. package/dist/envs.d.ts +0 -15
  85. package/dist/envs.js +0 -78
  86. package/dist/handleVoiceRequest.d.ts +0 -3
  87. package/dist/handleVoiceRequest.js +0 -167
  88. package/dist/loadAssistantConfigFromFile.d.ts +0 -3
  89. package/dist/loadAssistantConfigFromFile.js +0 -68
  90. package/dist/loadAssistantFromAPI.js +0 -98
  91. package/dist/machine/actions/announceSystemError.d.ts +0 -4
  92. package/dist/machine/actions/appendSpeech.d.ts +0 -1
  93. package/dist/machine/actions/cleanSpeech.d.ts +0 -1
  94. package/dist/machine/actions/cleanSpeech.js +0 -23
  95. package/dist/machine/actions/goodbye.d.ts +0 -4
  96. package/dist/machine/actions/goodbye.js +0 -31
  97. package/dist/machine/actions/increaseIdleTimeoutCount.d.ts +0 -1
  98. package/dist/machine/actions/increaseIdleTimeoutCount.js +0 -31
  99. package/dist/machine/actions/index.d.ts +0 -26
  100. package/dist/machine/actions/index.js +0 -37
  101. package/dist/machine/actions/interruptPlayback.d.ts +0 -4
  102. package/dist/machine/actions/interruptPlayback.js +0 -43
  103. package/dist/machine/actions/resetIdleTimeoutCount.d.ts +0 -1
  104. package/dist/machine/actions/resetIdleTimeoutCount.js +0 -31
  105. package/dist/machine/actions/resetState.d.ts +0 -1
  106. package/dist/machine/actions/resetState.js +0 -32
  107. package/dist/machine/actors/doAnnounceIdleTimeout.d.ts +0 -4
  108. package/dist/machine/actors/doAnnounceIdleTimeout.js +0 -41
  109. package/dist/machine/actors/doGreetUser.d.ts +0 -4
  110. package/dist/machine/actors/doGreetUser.js +0 -45
  111. package/dist/machine/actors/doProcessUserRequest.d.ts +0 -4
  112. package/dist/machine/actors/doProcessUserRequest.js +0 -75
  113. package/dist/machine/context.d.ts +0 -52
  114. package/dist/machine/context.js +0 -28
  115. package/dist/machine/delays.d.ts +0 -30
  116. package/dist/machine/delays.js +0 -37
  117. package/dist/machine/guards/hasSpeechResult.d.ts +0 -4
  118. package/dist/machine/guards/hasSpeechResult.js +0 -30
  119. package/dist/machine/guards/idleTimeoutCountExceedsMax.d.ts +0 -4
  120. package/dist/machine/guards/idleTimeoutCountExceedsMax.js +0 -31
  121. package/dist/machine/guards/index.js +0 -25
  122. package/dist/machine/index.d.ts +0 -19
  123. package/dist/machine/index.js +0 -23
  124. package/dist/machine/machine.d.ts +0 -408
  125. package/dist/machine/machine.js +0 -206
  126. package/dist/machine/setup.d.ts +0 -328
  127. package/dist/machine/setup.js +0 -116
  128. package/dist/machine/types.d.ts +0 -54
  129. package/dist/machine/types.js +0 -2
  130. package/dist/sendConversationEndedEvent.d.ts +0 -32
  131. package/dist/sendConversationEndedEvent.js +0 -66
  132. package/dist/sendConversationStartedEvent.d.ts +0 -6
  133. package/dist/sendConversationStartedEvent.js +0 -63
  134. package/dist/server.d.ts +0 -2
  135. package/dist/server.js +0 -25
  136. package/dist/types.d.ts +0 -40
  137. package/dist/types.js +0 -8
  138. package/dist/vad/SileroVad.d.ts +0 -18
  139. package/dist/vad/SileroVad.js +0 -40
  140. package/dist/vad/SileroVadModel.d.ts +0 -14
  141. package/dist/vad/SileroVadModel.js +0 -83
  142. package/dist/vad/chunkToFloat32Array.d.ts +0 -20
  143. package/dist/vad/chunkToFloat32Array.js +0 -34
  144. package/dist/vad/createVad.d.ts +0 -3
  145. package/dist/vad/createVad.js +0 -126
  146. package/dist/vad/types.d.ts +0 -60
  147. package/dist/vad/types.js +0 -2
  148. package/dist/vadWorker.js +0 -36
  149. package/dist/voice/Voice.d.ts +0 -61
  150. package/dist/voice/Voice.js +0 -96
  151. package/dist/voiceServerSetup.d.ts +0 -2
  152. package/dist/voiceServerSetup.js +0 -29
  153. /package/dist/{vadWorker.d.ts → session/main.d.ts} +0 -0
@@ -0,0 +1,548 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Agent = exports.logRecorder = exports.DEFAULT_TRANSFER_FAILED_MESSAGE = void 0;
4
+ exports.sessionRecorder = sessionRecorder;
5
+ /**
6
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
7
+ * http://github.com/fonoster/fonoster
8
+ *
9
+ * This file is part of Fonoster
10
+ *
11
+ * Licensed under the MIT License (the "License");
12
+ * you may not use this file except in compliance with
13
+ * the License. You may obtain a copy of the License at
14
+ *
15
+ * https://opensource.org/licenses/MIT
16
+ *
17
+ * Unless required by applicable law or agreed to in writing, software
18
+ * distributed under the License is distributed on an "AS IS" BASIS,
19
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20
+ * See the License for the specific language governing permissions and
21
+ * limitations under the License.
22
+ */
23
+ const common_1 = require("@fonoster/common");
24
+ const logger_1 = require("@fonoster/logger");
25
+ const voice_1 = require("@fonoster/voice");
26
+ const AsyncQueue_1 = require("./AsyncQueue");
27
+ const knowledge_1 = require("./knowledge");
28
+ const sentences_1 = require("./sentences");
29
+ const telemetry_1 = require("./telemetry");
30
+ /** What Autopilot says when a transfer fails and the agent sets no `failedMessage`. */
31
+ exports.DEFAULT_TRANSFER_FAILED_MESSAGE = "Sorry, I couldn't connect you. How else can I help?";
32
+ const logger = (0, logger_1.getLogger)({ service: "autopilot", filePath: __filename });
33
+ const MAX_TOOL_ROUNDS = 4;
34
+ const BUILT_IN_TOOLS = [
35
+ {
36
+ name: "hangup",
37
+ description: "End the call when the conversation is over.",
38
+ parameters: { type: "object", properties: {} }
39
+ },
40
+ {
41
+ name: "transfer",
42
+ description: "Transfer the caller to an operator (a person).",
43
+ parameters: { type: "object", properties: {} }
44
+ }
45
+ ];
46
+ /** Writes turn traces and notes to the call record through the session (record.note). */
47
+ function sessionRecorder(session) {
48
+ const write = (data) => void session.note(data).catch(() => undefined);
49
+ return {
50
+ turn: (trace) => write({ kind: "autopilot.turn", ...trace }),
51
+ note: (event) => write({ kind: "autopilot.note", ...event }),
52
+ usage: (lines) => void session.reportUsage(lines).catch(() => undefined)
53
+ };
54
+ }
55
+ exports.logRecorder = {
56
+ turn: (trace) => logger.verbose("autopilot turn", trace),
57
+ note: (event) => logger.verbose("autopilot note", event)
58
+ };
59
+ /** The side effects of Autopilot: everything the state machine asks it to do on the session. */
60
+ class Agent {
61
+ constructor(session, agent, think, tools, recorder = exports.logRecorder, knowledge) {
62
+ this.session = session;
63
+ this.agent = agent;
64
+ this.think = think;
65
+ this.tools = tools;
66
+ this.recorder = recorder;
67
+ this.knowledge = knowledge;
68
+ this.history = [];
69
+ this.idleCount = 0;
70
+ this.resumes = 0;
71
+ this.stopped = false;
72
+ this.send = () => undefined;
73
+ this.prepareEarly = agent.conversationSettings.patience !== "high";
74
+ }
75
+ get settings() {
76
+ return this.agent.conversationSettings;
77
+ }
78
+ // ---- lifecycle ----------------------------------------------------------------------
79
+ /** Greeting: subscribe to the estimate, send initial DTMF, speak the first message. */
80
+ async greet() {
81
+ this.maxTimer = setTimeout(() => this.send({ type: "MAX_DURATION" }), this.settings.maxSessionDuration);
82
+ await this.safely(async () => {
83
+ await this.session.subscribe(["turn.estimate"]);
84
+ // AppBridge's Pacing policy owns turn timing; Autopilot only tells it how patient to be.
85
+ // Recognition hints (names, brands) add to the app's defaults at AppBridge (VO-B3).
86
+ const hints = this.settings.hints?.length
87
+ ? this.settings.hints
88
+ : undefined;
89
+ if (this.settings.patience !== "normal" || hints)
90
+ await this.session.listen({
91
+ ...(this.settings.patience !== "normal"
92
+ ? { patience: this.settings.patience }
93
+ : {}),
94
+ ...(hints ? { hints } : {})
95
+ });
96
+ if (this.settings.initialDtmf)
97
+ await this.session.playDtmf(this.settings.initialDtmf);
98
+ if (this.settings.firstMessage) {
99
+ const r = await this.session.say(this.settings.firstMessage, this.speechOptions());
100
+ this.history.push({ role: "assistant", content: r.heardText });
101
+ }
102
+ this.send({ type: "SPOKEN", source: "greeting" });
103
+ });
104
+ }
105
+ stop() {
106
+ this.stopped = true;
107
+ this.abortReply();
108
+ this.discardPrepared();
109
+ clearTimeout(this.idleTimer);
110
+ clearTimeout(this.maxTimer);
111
+ }
112
+ // ---- idle -------------------------------------------------------------------------
113
+ armIdle() {
114
+ clearTimeout(this.idleTimer);
115
+ const idle = this.settings.idleOptions;
116
+ if (idle)
117
+ this.idleTimer = setTimeout(() => this.send({ type: "IDLE" }), idle.timeout);
118
+ }
119
+ disarmIdle() {
120
+ clearTimeout(this.idleTimer);
121
+ }
122
+ /** The next idle timeout ends the call (the goodbye counts as the last announcement). */
123
+ idleExhausted() {
124
+ const max = this.settings.idleOptions?.maxTimeoutCount ?? 2;
125
+ return this.idleCount + 1 >= max;
126
+ }
127
+ async sayIdle() {
128
+ this.idleCount++;
129
+ await this.safely(async () => {
130
+ await this.session.say(this.settings.idleOptions.message, this.speechOptions());
131
+ this.send({ type: "SPOKEN", source: "idle" });
132
+ });
133
+ }
134
+ // ---- pacing -----------------------------------------------------------------------
135
+ /** turn.likely_ended: start generating silently (when preparing early). */
136
+ prepare(text) {
137
+ if (!this.prepareEarly || !text.trim())
138
+ return;
139
+ this.discardPrepared();
140
+ const abort = new AbortController();
141
+ const chunks = new AsyncQueue_1.AsyncQueue();
142
+ const prepared = { text, abort, chunks, context: [] };
143
+ this.prepared = prepared;
144
+ void (async () => {
145
+ prepared.context = await this.contextFor(text, abort.signal);
146
+ if (abort.signal.aborted)
147
+ return chunks.end();
148
+ const messages = [
149
+ ...this.messages(prepared.context),
150
+ { role: "user", content: text }
151
+ ];
152
+ await this.pump(this.traced(messages, abort.signal), chunks);
153
+ })();
154
+ }
155
+ /** turn.resumed: the caller kept talking; the prepared reply is dropped. */
156
+ resumed() {
157
+ this.discardPrepared();
158
+ this.resumes++;
159
+ if (this.resumes >= 3 && this.prepareEarly) {
160
+ // A caller who pauses often: stop preparing early for the rest of this call. AppBridge
161
+ // adapts its own turn timing within the call (TT-B6), so Autopilot does not also change
162
+ // the patience it asked for (AP-B2).
163
+ this.prepareEarly = false;
164
+ this.recorder.note({
165
+ type: "pacing.adapted",
166
+ prepare: "on turn.ended",
167
+ resumes: this.resumes
168
+ });
169
+ }
170
+ }
171
+ // ---- replies ----------------------------------------------------------------------
172
+ async clear() {
173
+ await this.safely(() => this.session.stopSay());
174
+ }
175
+ abortReply() {
176
+ this.reply?.abort();
177
+ this.reply = undefined;
178
+ }
179
+ /** turn.ended: answer the caller, streaming sentences into one speak id. */
180
+ answer(text) {
181
+ const previous = this.inflight;
182
+ this.abortReply();
183
+ const abort = new AbortController();
184
+ this.reply = abort;
185
+ const run = this.respond(text, abort, previous);
186
+ this.inflight = run;
187
+ return run;
188
+ }
189
+ /** One turn (STT final, LLM, TTS) as an autopilot.turn span. */
190
+ async respond(text, abort, previous) {
191
+ const callRef = this.session.callRef;
192
+ const span = telemetry_1.telemetry.startSpan("autopilot.turn", { [common_1.CALL_REF_ATTR]: callRef }, telemetry_1.telemetry.traceparent(callRef));
193
+ try {
194
+ await this.respondTurn(text, abort, previous);
195
+ span.end({ "turn.cancelled": abort.signal.aborted });
196
+ }
197
+ catch (e) {
198
+ span.end({}, e.message);
199
+ throw e;
200
+ }
201
+ finally {
202
+ telemetry_1.telemetry.count("fonoster.autopilot.turns", 1);
203
+ }
204
+ }
205
+ async respondTurn(text, abort, previous) {
206
+ // Let a cancelled reply settle first, so what the caller heard of it is recorded before
207
+ // this turn (honest history, A6).
208
+ await previous?.catch(() => undefined);
209
+ this.idleCount = 0;
210
+ const turnEnded = Date.now();
211
+ this.history.push({ role: "user", content: text });
212
+ if (abort.signal.aborted)
213
+ return;
214
+ const trace = {
215
+ userText: text,
216
+ replyText: "",
217
+ heardText: "",
218
+ interrupted: false,
219
+ toolCalls: []
220
+ };
221
+ let preempt = false;
222
+ if (this.settings.filler) {
223
+ preempt = true;
224
+ void this.session
225
+ .play(this.settings.filler.url, { interruptible: true })
226
+ .catch(() => undefined);
227
+ }
228
+ try {
229
+ const prepared = this.takePrepared(text);
230
+ const context = prepared
231
+ ? prepared.context
232
+ : await this.contextFor(text, abort.signal);
233
+ let source = prepared?.chunks ?? this.startStream(abort.signal, context);
234
+ for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
235
+ const outcome = await this.speakRound(source, abort, preempt, turnEnded, trace);
236
+ preempt = false;
237
+ if (abort.signal.aborted &&
238
+ !outcome.interrupted &&
239
+ !outcome.completed) {
240
+ // Cancelled by a new turn: only what the caller already heard is kept.
241
+ if (outcome.heard)
242
+ this.history.push({ role: "assistant", content: outcome.heard });
243
+ return;
244
+ }
245
+ this.history.push({
246
+ role: "assistant",
247
+ content: outcome.heard,
248
+ ...(outcome.toolCalls.length ? { tool_calls: outcome.toolCalls } : {})
249
+ });
250
+ if (outcome.interrupted ||
251
+ !outcome.toolCalls.length ||
252
+ abort.signal.aborted)
253
+ break;
254
+ const builtIn = outcome.toolCalls.find((c) => c.name === "hangup" || c.name === "transfer");
255
+ if (builtIn) {
256
+ if (builtIn.name === "transfer")
257
+ this.transferCallId = builtIn.id;
258
+ this.recorder.turn({
259
+ ...trace,
260
+ toolCalls: [...trace.toolCalls, { name: builtIn.name, ok: true }]
261
+ });
262
+ this.send({
263
+ type: builtIn.name === "hangup"
264
+ ? "HANGUP_REQUESTED"
265
+ : "TRANSFER_REQUESTED"
266
+ });
267
+ return;
268
+ }
269
+ await this.runTools(outcome.toolCalls, abort, trace);
270
+ if (abort.signal.aborted)
271
+ return;
272
+ source = this.startStream(abort.signal, context);
273
+ }
274
+ this.recorder.turn(trace);
275
+ if (this.reply === abort)
276
+ this.send({ type: "SPOKEN", source: "reply" });
277
+ }
278
+ catch (e) {
279
+ if (abort.signal.aborted ||
280
+ e instanceof voice_1.SessionEndedError ||
281
+ this.stopped)
282
+ return;
283
+ logger.error(`autopilot reply failed: ${e.message}`);
284
+ await this.safely(async () => {
285
+ await this.session.say(this.settings.systemErrorMessage, this.speechOptions());
286
+ this.send({ type: "SPOKEN", source: "error" });
287
+ });
288
+ }
289
+ }
290
+ async speakRound(source, abort, preempt, turnEnded, trace) {
291
+ const text = new AsyncQueue_1.AsyncQueue();
292
+ let toolCalls = [];
293
+ let full = "";
294
+ const pump = (async () => {
295
+ try {
296
+ for await (const chunk of source) {
297
+ if (abort.signal.aborted)
298
+ break;
299
+ if (chunk.type === "text") {
300
+ if (trace.firstTokenMs === undefined)
301
+ trace.firstTokenMs = Date.now() - turnEnded;
302
+ full += chunk.text;
303
+ text.push(chunk.text);
304
+ }
305
+ else if (chunk.type === "tool_calls")
306
+ toolCalls = chunk.calls;
307
+ }
308
+ text.end();
309
+ }
310
+ catch (e) {
311
+ text.fail(e);
312
+ }
313
+ })();
314
+ const iterator = (0, sentences_1.sentences)(text)[Symbol.asyncIterator]();
315
+ const first = await iterator.next();
316
+ let heard = "";
317
+ let interrupted = false;
318
+ let completed = false;
319
+ if (!first.done && !abort.signal.aborted) {
320
+ this.send({ type: "REPLY_SPEAKING" });
321
+ trace.firstSpeakMs = Date.now() - turnEnded;
322
+ const pieces = (async function* () {
323
+ yield first.value;
324
+ for (;;) {
325
+ const next = await iterator.next();
326
+ if (next.done)
327
+ return;
328
+ yield next.value;
329
+ }
330
+ })();
331
+ const result = await this.session.say(pieces, {
332
+ ...this.speechOptions(),
333
+ preempt: preempt || undefined
334
+ });
335
+ heard = result.heardText;
336
+ interrupted = result.interrupted;
337
+ completed = !interrupted;
338
+ if (interrupted)
339
+ abort.abort();
340
+ }
341
+ await pump;
342
+ trace.replyText += full;
343
+ trace.heardText += heard;
344
+ trace.interrupted || (trace.interrupted = interrupted);
345
+ return { heard, interrupted, completed, toolCalls };
346
+ }
347
+ async runTools(calls, abort, trace) {
348
+ for (const call of calls) {
349
+ const tool = this.agent.languageModel.tools.find((t) => t.name === call.name);
350
+ // The start message is spoken while the tool runs; AppBridge queues the reply after it.
351
+ if (tool?.requestStartMessage) {
352
+ void this.session
353
+ .say(tool.requestStartMessage, this.speechOptions())
354
+ .catch(() => undefined);
355
+ }
356
+ let args = {};
357
+ try {
358
+ args = call.arguments ? JSON.parse(call.arguments) : {};
359
+ }
360
+ catch {
361
+ args = {};
362
+ }
363
+ const outcome = await this.tools.run(call.name, args, abort.signal);
364
+ trace.toolCalls.push({ name: call.name, ok: outcome.ok });
365
+ this.history.push({
366
+ role: "tool",
367
+ tool_call_id: call.id,
368
+ content: JSON.stringify(outcome.ok ? outcome.result : { error: outcome.error })
369
+ });
370
+ }
371
+ }
372
+ // ---- endings ----------------------------------------------------------------------
373
+ async goodbye() {
374
+ this.abortReply();
375
+ this.discardPrepared();
376
+ clearTimeout(this.idleTimer);
377
+ await this.safely(async () => {
378
+ await this.session.say(this.settings.goodbyeMessage, {
379
+ ...this.speechOptions(),
380
+ interruptible: false
381
+ });
382
+ await this.session.hangup();
383
+ });
384
+ }
385
+ async transfer() {
386
+ this.abortReply();
387
+ clearTimeout(this.idleTimer);
388
+ const options = this.settings.transferOptions;
389
+ await this.safely(async () => {
390
+ if (!options) {
391
+ await this.session.say(this.settings.systemErrorMessage, this.speechOptions());
392
+ this.send({ type: "SPOKEN", source: "error" });
393
+ return;
394
+ }
395
+ await this.session.say(options.message, {
396
+ ...this.speechOptions(),
397
+ interruptible: false
398
+ });
399
+ try {
400
+ await this.session.transfer(options.phoneNumber);
401
+ }
402
+ catch (e) {
403
+ if (!(e instanceof voice_1.SessionError) || e.code !== "transfer_failed")
404
+ throw e;
405
+ await this.transferFailed(options.failedMessage);
406
+ }
407
+ });
408
+ }
409
+ /**
410
+ * The transfer failed: say so and keep helping (autopilot "Transfer fails"). The model learns
411
+ * the outcome as its transfer call's result, and the machine returns to listening.
412
+ */
413
+ async transferFailed(message) {
414
+ const callId = this.transferCallId;
415
+ this.transferCallId = undefined;
416
+ if (callId)
417
+ this.history.push({
418
+ role: "tool",
419
+ tool_call_id: callId,
420
+ content: JSON.stringify({ error: "the transfer failed" })
421
+ });
422
+ const r = await this.session.say(message || exports.DEFAULT_TRANSFER_FAILED_MESSAGE, this.speechOptions());
423
+ this.history.push({ role: "assistant", content: r.heardText });
424
+ this.send({ type: "SPOKEN", source: "error" });
425
+ }
426
+ // ---- internals --------------------------------------------------------------------
427
+ messages(context = []) {
428
+ return [
429
+ { role: "system", content: this.settings.systemPrompt },
430
+ ...context,
431
+ ...this.history
432
+ ];
433
+ }
434
+ /** Knowledge for this turn, as a system message for the request only (never in history). */
435
+ async contextFor(question, signal) {
436
+ if (!this.knowledge)
437
+ return [];
438
+ const timeout = new Promise((resolve) => setTimeout(() => resolve(""), knowledge_1.KNOWLEDGE_TIMEOUT_MS));
439
+ try {
440
+ const found = await Promise.race([
441
+ this.knowledge.find(question, signal),
442
+ timeout
443
+ ]);
444
+ return found?.trim()
445
+ ? [
446
+ {
447
+ role: "system",
448
+ content: `Relevant information:\n${found.trim()}`
449
+ }
450
+ ]
451
+ : [];
452
+ }
453
+ catch {
454
+ return [];
455
+ }
456
+ }
457
+ toolSpecs() {
458
+ const own = this.agent.languageModel.tools.map((t) => ({
459
+ name: t.name,
460
+ description: t.description,
461
+ parameters: t.parameters
462
+ }));
463
+ const builtIn = BUILT_IN_TOOLS.filter((b) => b.name !== "transfer" || this.settings.transferOptions);
464
+ return [...own, ...builtIn];
465
+ }
466
+ startStream(signal, context = []) {
467
+ const chunks = new AsyncQueue_1.AsyncQueue();
468
+ void this.pump(this.traced(this.messages(context), signal), chunks);
469
+ return chunks;
470
+ }
471
+ /** think.stream inside an autopilot.llm.request span, with request and latency metrics. */
472
+ async *traced(messages, signal) {
473
+ const callRef = this.session.callRef;
474
+ const span = telemetry_1.telemetry.startSpan("autopilot.llm.request", { [common_1.CALL_REF_ATTR]: callRef, "llm.messages": messages.length }, telemetry_1.telemetry.traceparent(callRef));
475
+ const started = Date.now();
476
+ let first;
477
+ let failure;
478
+ try {
479
+ for await (const chunk of this.think.stream(messages, this.toolSpecs(), signal)) {
480
+ first ?? (first = Date.now() - started);
481
+ yield chunk;
482
+ }
483
+ }
484
+ catch (e) {
485
+ failure = e.message;
486
+ throw e;
487
+ }
488
+ finally {
489
+ telemetry_1.telemetry.count("fonoster.autopilot.llm.requests", 1);
490
+ if (first !== undefined)
491
+ telemetry_1.telemetry.count("fonoster.autopilot.llm.latency_ms", first);
492
+ span.end({
493
+ "llm.first_chunk_ms": first ?? -1,
494
+ "llm.cancelled": signal.aborted
495
+ }, failure);
496
+ }
497
+ }
498
+ takePrepared(text) {
499
+ const p = this.prepared;
500
+ this.prepared = undefined;
501
+ if (!p)
502
+ return undefined;
503
+ if (p.text === text && !p.abort.signal.aborted) {
504
+ // The reply prepared on turn.likely_ended is still valid; adopt its abort controller.
505
+ this.reply?.signal.addEventListener("abort", () => p.abort.abort(), {
506
+ once: true
507
+ });
508
+ return p;
509
+ }
510
+ p.abort.abort();
511
+ return undefined;
512
+ }
513
+ discardPrepared() {
514
+ this.prepared?.abort.abort();
515
+ this.prepared = undefined;
516
+ }
517
+ async pump(stream, into) {
518
+ try {
519
+ for await (const chunk of stream)
520
+ into.push(chunk);
521
+ into.end();
522
+ }
523
+ catch (e) {
524
+ into.fail(e);
525
+ }
526
+ }
527
+ speechOptions() {
528
+ return {
529
+ voice: this.settings.voice,
530
+ language: this.settings.language,
531
+ interruptible: this.settings.allowUserBargeIn
532
+ };
533
+ }
534
+ /** Runs session work; a session that has ended stops everything quietly. */
535
+ async safely(fn) {
536
+ if (this.stopped)
537
+ return;
538
+ try {
539
+ await fn();
540
+ }
541
+ catch (e) {
542
+ if (e instanceof voice_1.SessionEndedError || this.stopped)
543
+ return;
544
+ logger.error(`autopilot session error: ${e.message}`);
545
+ }
546
+ }
547
+ }
548
+ exports.Agent = Agent;
@@ -16,10 +16,15 @@
16
16
  * See the License for the specific language governing permissions and
17
17
  * limitations under the License.
18
18
  */
19
- import { VoiceRequest } from "@fonoster/common";
20
- import { AssistantConfig } from "./assistants";
21
- declare function loadAssistantFromAPI(req: VoiceRequest, integrations: {
22
- productRef: string;
23
- credentials: Record<string, unknown>;
24
- }[]): Promise<AssistantConfig>;
25
- export { loadAssistantFromAPI };
19
+ /** A push-based async iterable: producers push, one consumer iterates. */
20
+ export declare class AsyncQueue<T> implements AsyncIterable<T> {
21
+ private items;
22
+ private waiting?;
23
+ private failWaiting?;
24
+ private ended;
25
+ private error?;
26
+ push(item: T): void;
27
+ end(): void;
28
+ fail(error: Error): void;
29
+ [Symbol.asyncIterator](): AsyncIterator<T>;
30
+ }
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AsyncQueue = void 0;
4
+ /**
5
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
6
+ * http://github.com/fonoster/fonoster
7
+ *
8
+ * This file is part of Fonoster
9
+ *
10
+ * Licensed under the MIT License (the "License");
11
+ * you may not use this file except in compliance with
12
+ * the License. You may obtain a copy of the License at
13
+ *
14
+ * https://opensource.org/licenses/MIT
15
+ *
16
+ * Unless required by applicable law or agreed to in writing, software
17
+ * distributed under the License is distributed on an "AS IS" BASIS,
18
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
+ * See the License for the specific language governing permissions and
20
+ * limitations under the License.
21
+ */
22
+ /** A push-based async iterable: producers push, one consumer iterates. */
23
+ class AsyncQueue {
24
+ constructor() {
25
+ this.items = [];
26
+ this.ended = false;
27
+ }
28
+ push(item) {
29
+ if (this.ended)
30
+ return;
31
+ if (this.waiting) {
32
+ const resolve = this.waiting;
33
+ this.waiting = undefined;
34
+ resolve({ value: item, done: false });
35
+ }
36
+ else
37
+ this.items.push(item);
38
+ }
39
+ end() {
40
+ this.ended = true;
41
+ this.waiting?.({ value: undefined, done: true });
42
+ this.waiting = undefined;
43
+ }
44
+ fail(error) {
45
+ this.error = error;
46
+ this.ended = true;
47
+ this.failWaiting?.(error);
48
+ this.waiting = undefined;
49
+ }
50
+ [Symbol.asyncIterator]() {
51
+ return {
52
+ next: () => {
53
+ if (this.items.length)
54
+ return Promise.resolve({ value: this.items.shift(), done: false });
55
+ if (this.error)
56
+ return Promise.reject(this.error);
57
+ if (this.ended)
58
+ return Promise.resolve({ value: undefined, done: true });
59
+ return new Promise((resolve, reject) => {
60
+ this.waiting = resolve;
61
+ this.failWaiting = reject;
62
+ });
63
+ }
64
+ };
65
+ }
66
+ }
67
+ exports.AsyncQueue = AsyncQueue;
@@ -16,5 +16,5 @@
16
16
  * See the License for the specific language governing permissions and
17
17
  * limitations under the License.
18
18
  */
19
- export { idleTimeoutCountExceedsMax } from "./idleTimeoutCountExceedsMax";
20
- export { hasSpeechResult } from "./hasSpeechResult";
19
+ export { sessionAgentSchema, sessionConversationSettingsSchema, sessionLanguageModelSchema } from "@fonoster/common";
20
+ export type { SessionAgent, SessionConversationSettings, SessionLanguageModel, ToolDefinition } from "@fonoster/common";
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sessionLanguageModelSchema = exports.sessionConversationSettingsSchema = exports.sessionAgentSchema = void 0;
4
+ /**
5
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
6
+ * http://github.com/fonoster/fonoster
7
+ *
8
+ * This file is part of Fonoster
9
+ *
10
+ * Licensed under the MIT License (the "License");
11
+ * you may not use this file except in compliance with
12
+ * the License. You may obtain a copy of the License at
13
+ *
14
+ * https://opensource.org/licenses/MIT
15
+ *
16
+ * Unless required by applicable law or agreed to in writing, software
17
+ * distributed under the License is distributed on an "AS IS" BASIS,
18
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
+ * See the License for the specific language governing permissions and
20
+ * limitations under the License.
21
+ */
22
+ // The session agent schema lives in @fonoster/common (rework-autopilot 5.1), so the control
23
+ // plane validates agents with the same rules Autopilot runs with.
24
+ var common_1 = require("@fonoster/common");
25
+ Object.defineProperty(exports, "sessionAgentSchema", { enumerable: true, get: function () { return common_1.sessionAgentSchema; } });
26
+ Object.defineProperty(exports, "sessionConversationSettingsSchema", { enumerable: true, get: function () { return common_1.sessionConversationSettingsSchema; } });
27
+ Object.defineProperty(exports, "sessionLanguageModelSchema", { enumerable: true, get: function () { return common_1.sessionLanguageModelSchema; } });