@fonoster/autopilot 0.27.0 → 0.30.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 (138) hide show
  1. package/README.md +14 -51
  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/types.d.ts +1 -1
  8. package/dist/models/LanguageModelFactory.d.ts +5 -24
  9. package/dist/models/LanguageModelFactory.js +0 -1
  10. package/dist/models/createLanguageModel.d.ts +2 -2
  11. package/dist/models/evaluations/createEvalEffectiveConfig.d.ts +10 -106
  12. package/dist/models/evaluations/createEvalEffectiveConfig.js +47 -7
  13. package/dist/models/evaluations/createEvaluateIntelligence.d.ts +10 -20
  14. package/dist/models/evaluations/createEvaluateIntelligence.js +32 -23
  15. package/dist/models/evaluations/evalTestCases.d.ts +2 -2
  16. package/dist/models/evaluations/extractAiResponse.d.ts +2 -3
  17. package/dist/models/evaluations/index.d.ts +1 -0
  18. package/dist/models/evaluations/index.js +1 -0
  19. package/dist/models/evaluations/runEval.d.ts +2 -20
  20. package/dist/models/evaluations/session/EvalSession.d.ts +73 -0
  21. package/dist/models/evaluations/session/EvalSession.js +134 -0
  22. package/dist/models/evaluations/session/evaluateSessionScenario.d.ts +58 -0
  23. package/dist/models/evaluations/session/evaluateSessionScenario.js +182 -0
  24. package/dist/{vad → models/evaluations/session}/index.d.ts +3 -2
  25. package/dist/{vad → models/evaluations/session}/index.js +3 -2
  26. package/dist/models/evaluations/session/runSessionEval.d.ts +17 -0
  27. package/dist/models/evaluations/session/runSessionEval.js +59 -0
  28. package/dist/models/evaluations/types.d.ts +7 -6
  29. package/dist/models/types.d.ts +19 -1
  30. package/dist/session/Agent.d.ts +138 -0
  31. package/dist/session/Agent.js +474 -0
  32. package/dist/{loadAssistantFromAPI.d.ts → session/AsyncQueue.d.ts} +12 -7
  33. package/dist/session/AsyncQueue.js +67 -0
  34. package/dist/{machine/guards/index.d.ts → session/config.d.ts} +2 -2
  35. package/dist/session/config.js +27 -0
  36. package/dist/session/convertLegacyAssistant.d.ts +77 -0
  37. package/dist/session/convertLegacyAssistant.js +118 -0
  38. package/dist/session/createAutopilot.d.ts +47 -0
  39. package/dist/session/createAutopilot.js +119 -0
  40. package/dist/session/hosted.d.ts +456 -0
  41. package/dist/session/hosted.js +506 -0
  42. package/dist/{assistants → session}/index.d.ts +11 -2
  43. package/dist/{machine/actors → session}/index.js +11 -3
  44. package/dist/{machine/actions/appendSpeech.js → session/knowledge.d.ts} +14 -17
  45. package/dist/session/knowledge.js +10 -0
  46. package/dist/session/llm.d.ts +86 -0
  47. package/dist/session/llm.js +83 -0
  48. package/dist/session/machine.d.ts +206 -0
  49. package/dist/session/machine.js +126 -0
  50. package/dist/session/main.js +44 -0
  51. package/dist/session/sentences.d.ts +5 -0
  52. package/dist/session/sentences.js +53 -0
  53. package/dist/{machine/actors/index.d.ts → session/think.d.ts} +2 -3
  54. package/dist/{machine/actions/announceSystemError.js → session/think.js} +8 -10
  55. package/dist/session/tools.d.ts +22 -0
  56. package/dist/session/tools.js +83 -0
  57. package/dist/tools/builtin/transferToolDefinition.js +1 -1
  58. package/dist/voice/index.d.ts +0 -1
  59. package/dist/voice/index.js +0 -1
  60. package/dist/voice/types.d.ts +5 -18
  61. package/package.json +13 -14
  62. package/dist/Autopilot.d.ts +0 -14
  63. package/dist/Autopilot.js +0 -133
  64. package/dist/assistants/index.js +0 -36
  65. package/dist/assistants/loadAndValidateAssistant.d.ts +0 -3
  66. package/dist/assistants/loadAndValidateAssistant.js +0 -49
  67. package/dist/assistants/types.d.ts +0 -24
  68. package/dist/assistants/types.js +0 -2
  69. package/dist/envs.d.ts +0 -15
  70. package/dist/envs.js +0 -78
  71. package/dist/handleVoiceRequest.d.ts +0 -3
  72. package/dist/handleVoiceRequest.js +0 -167
  73. package/dist/loadAssistantConfigFromFile.d.ts +0 -3
  74. package/dist/loadAssistantConfigFromFile.js +0 -68
  75. package/dist/loadAssistantFromAPI.js +0 -98
  76. package/dist/machine/actions/announceSystemError.d.ts +0 -4
  77. package/dist/machine/actions/appendSpeech.d.ts +0 -1
  78. package/dist/machine/actions/cleanSpeech.d.ts +0 -1
  79. package/dist/machine/actions/cleanSpeech.js +0 -23
  80. package/dist/machine/actions/goodbye.d.ts +0 -4
  81. package/dist/machine/actions/goodbye.js +0 -31
  82. package/dist/machine/actions/increaseIdleTimeoutCount.d.ts +0 -1
  83. package/dist/machine/actions/increaseIdleTimeoutCount.js +0 -31
  84. package/dist/machine/actions/index.d.ts +0 -26
  85. package/dist/machine/actions/index.js +0 -37
  86. package/dist/machine/actions/interruptPlayback.d.ts +0 -4
  87. package/dist/machine/actions/interruptPlayback.js +0 -43
  88. package/dist/machine/actions/resetIdleTimeoutCount.d.ts +0 -1
  89. package/dist/machine/actions/resetIdleTimeoutCount.js +0 -31
  90. package/dist/machine/actions/resetState.d.ts +0 -1
  91. package/dist/machine/actions/resetState.js +0 -32
  92. package/dist/machine/actors/doAnnounceIdleTimeout.d.ts +0 -4
  93. package/dist/machine/actors/doAnnounceIdleTimeout.js +0 -41
  94. package/dist/machine/actors/doGreetUser.d.ts +0 -4
  95. package/dist/machine/actors/doGreetUser.js +0 -45
  96. package/dist/machine/actors/doProcessUserRequest.d.ts +0 -4
  97. package/dist/machine/actors/doProcessUserRequest.js +0 -75
  98. package/dist/machine/context.d.ts +0 -52
  99. package/dist/machine/context.js +0 -28
  100. package/dist/machine/delays.d.ts +0 -30
  101. package/dist/machine/delays.js +0 -37
  102. package/dist/machine/guards/hasSpeechResult.d.ts +0 -4
  103. package/dist/machine/guards/hasSpeechResult.js +0 -30
  104. package/dist/machine/guards/idleTimeoutCountExceedsMax.d.ts +0 -4
  105. package/dist/machine/guards/idleTimeoutCountExceedsMax.js +0 -31
  106. package/dist/machine/guards/index.js +0 -25
  107. package/dist/machine/index.d.ts +0 -19
  108. package/dist/machine/index.js +0 -23
  109. package/dist/machine/machine.d.ts +0 -408
  110. package/dist/machine/machine.js +0 -206
  111. package/dist/machine/setup.d.ts +0 -328
  112. package/dist/machine/setup.js +0 -116
  113. package/dist/machine/types.d.ts +0 -54
  114. package/dist/machine/types.js +0 -2
  115. package/dist/sendConversationEndedEvent.d.ts +0 -32
  116. package/dist/sendConversationEndedEvent.js +0 -66
  117. package/dist/sendConversationStartedEvent.d.ts +0 -6
  118. package/dist/sendConversationStartedEvent.js +0 -63
  119. package/dist/server.d.ts +0 -2
  120. package/dist/server.js +0 -25
  121. package/dist/types.d.ts +0 -40
  122. package/dist/types.js +0 -8
  123. package/dist/vad/SileroVad.d.ts +0 -18
  124. package/dist/vad/SileroVad.js +0 -40
  125. package/dist/vad/SileroVadModel.d.ts +0 -14
  126. package/dist/vad/SileroVadModel.js +0 -83
  127. package/dist/vad/chunkToFloat32Array.d.ts +0 -20
  128. package/dist/vad/chunkToFloat32Array.js +0 -34
  129. package/dist/vad/createVad.d.ts +0 -3
  130. package/dist/vad/createVad.js +0 -126
  131. package/dist/vad/types.d.ts +0 -60
  132. package/dist/vad/types.js +0 -2
  133. package/dist/vadWorker.js +0 -36
  134. package/dist/voice/Voice.d.ts +0 -61
  135. package/dist/voice/Voice.js +0 -96
  136. package/dist/voiceServerSetup.d.ts +0 -2
  137. package/dist/voiceServerSetup.js +0 -29
  138. /package/dist/{vadWorker.d.ts → session/main.d.ts} +0 -0
@@ -0,0 +1,138 @@
1
+ import { SessionAssistant } from "./config";
2
+ import { KnowledgePort } from "./knowledge";
3
+ import { ChatMessage, Think } from "./think";
4
+ import { ToolPort } from "./tools";
5
+ import type { Session } from "@fonoster/voice";
6
+ export type SpokenSource = "greeting" | "reply" | "idle" | "error";
7
+ export type AgentEvent = {
8
+ type: "TURN_USER_STARTED";
9
+ } | {
10
+ type: "TURN_LIKELY_ENDED";
11
+ text: string;
12
+ } | {
13
+ type: "TURN_RESUMED";
14
+ } | {
15
+ type: "TURN_ENDED";
16
+ text: string;
17
+ } | {
18
+ type: "TURN_BACKCHANNEL";
19
+ } | {
20
+ type: "REPLY_SPEAKING";
21
+ }
22
+ /** An output finished; `source` says which, so stale events from an earlier phase are ignored. */
23
+ | {
24
+ type: "SPOKEN";
25
+ source: SpokenSource;
26
+ } | {
27
+ type: "IDLE";
28
+ } | {
29
+ type: "MAX_DURATION";
30
+ } | {
31
+ type: "HANGUP_REQUESTED";
32
+ } | {
33
+ type: "TRANSFER_REQUESTED";
34
+ } | {
35
+ type: "SESSION_END";
36
+ };
37
+ /** One turn's trace for the call record (rework-autopilot A10). */
38
+ export type TurnTrace = {
39
+ userText: string;
40
+ replyText: string;
41
+ heardText: string;
42
+ interrupted: boolean;
43
+ toolCalls: {
44
+ name: string;
45
+ ok: boolean;
46
+ }[];
47
+ /** ms from turn.ended to the first token, and to the first speak sent. */
48
+ firstTokenMs?: number;
49
+ firstSpeakMs?: number;
50
+ };
51
+ export interface Recorder {
52
+ turn(trace: TurnTrace): void;
53
+ note(event: Record<string, unknown>): void;
54
+ }
55
+ /** Writes turn traces and notes to the call record through the session (record.note). */
56
+ export declare function sessionRecorder(session: Session): Recorder;
57
+ export declare const logRecorder: Recorder;
58
+ /** The side effects of Autopilot: everything the state machine asks it to do on the session. */
59
+ export declare class Agent {
60
+ private readonly session;
61
+ private readonly assistant;
62
+ private readonly think;
63
+ private readonly tools;
64
+ private readonly recorder;
65
+ private readonly knowledge?;
66
+ readonly history: ChatMessage[];
67
+ private reply?;
68
+ private inflight?;
69
+ private prepared?;
70
+ private idleTimer?;
71
+ private maxTimer?;
72
+ private idleCount;
73
+ private resumes;
74
+ /** Whether a reply is prepared on turn.likely_ended (off at high patience or once adapted). */
75
+ private prepareEarly;
76
+ private stopped;
77
+ send: (event: AgentEvent) => void;
78
+ constructor(session: Session, assistant: SessionAssistant, think: Think, tools: ToolPort, recorder?: Recorder, knowledge?: KnowledgePort);
79
+ get settings(): {
80
+ firstMessage?: string;
81
+ systemPrompt?: string;
82
+ goodbyeMessage?: string;
83
+ systemErrorMessage?: string;
84
+ initialDtmf?: string;
85
+ maxSessionDuration?: number;
86
+ transferOptions?: {
87
+ message?: string;
88
+ phoneNumber?: string;
89
+ };
90
+ idleOptions?: {
91
+ message?: string;
92
+ timeout?: number;
93
+ maxTimeoutCount?: number;
94
+ };
95
+ allowUserBargeIn?: boolean;
96
+ patience?: "low" | "normal" | "high";
97
+ hints?: string[];
98
+ filler?: {
99
+ url?: string;
100
+ };
101
+ voice?: string;
102
+ language?: string;
103
+ vad?: any;
104
+ maxSpeechWaitTimeout?: any;
105
+ };
106
+ /** Greeting: subscribe to the estimate, send initial DTMF, speak the first message. */
107
+ greet(): Promise<void>;
108
+ stop(): void;
109
+ armIdle(): void;
110
+ disarmIdle(): void;
111
+ /** The next idle timeout ends the call (the goodbye counts as the last announcement). */
112
+ idleExhausted(): boolean;
113
+ sayIdle(): Promise<void>;
114
+ /** turn.likely_ended: start generating silently (when preparing early). */
115
+ prepare(text: string): void;
116
+ /** turn.resumed: the caller kept talking; the prepared reply is dropped. */
117
+ resumed(): void;
118
+ clear(): Promise<void>;
119
+ abortReply(): void;
120
+ /** turn.ended: answer the caller, streaming sentences into one speak id. */
121
+ answer(text: string): Promise<void>;
122
+ private respond;
123
+ private speakRound;
124
+ private runTools;
125
+ goodbye(): Promise<void>;
126
+ transfer(): Promise<void>;
127
+ private messages;
128
+ /** Knowledge for this turn, as a system message for the request only (never in history). */
129
+ private contextFor;
130
+ private toolSpecs;
131
+ private startStream;
132
+ private takePrepared;
133
+ private discardPrepared;
134
+ private pump;
135
+ private speechOptions;
136
+ /** Runs session work; a session that has ended stops everything quietly. */
137
+ private safely;
138
+ }
@@ -0,0 +1,474 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Agent = exports.logRecorder = 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 logger_1 = require("@fonoster/logger");
24
+ const voice_1 = require("@fonoster/voice");
25
+ const AsyncQueue_1 = require("./AsyncQueue");
26
+ const knowledge_1 = require("./knowledge");
27
+ const sentences_1 = require("./sentences");
28
+ const logger = (0, logger_1.getLogger)({ service: "autopilot", filePath: __filename });
29
+ const MAX_TOOL_ROUNDS = 4;
30
+ const BUILT_IN_TOOLS = [
31
+ {
32
+ name: "hangup",
33
+ description: "End the call when the conversation is over.",
34
+ parameters: { type: "object", properties: {} }
35
+ },
36
+ {
37
+ name: "transfer",
38
+ description: "Transfer the caller to an operator (a person).",
39
+ parameters: { type: "object", properties: {} }
40
+ }
41
+ ];
42
+ /** Writes turn traces and notes to the call record through the session (record.note). */
43
+ function sessionRecorder(session) {
44
+ const write = (data) => void session.note(data).catch(() => undefined);
45
+ return {
46
+ turn: (trace) => write({ kind: "autopilot.turn", ...trace }),
47
+ note: (event) => write({ kind: "autopilot.note", ...event })
48
+ };
49
+ }
50
+ exports.logRecorder = {
51
+ turn: (trace) => logger.verbose("autopilot turn", trace),
52
+ note: (event) => logger.verbose("autopilot note", event)
53
+ };
54
+ /** The side effects of Autopilot: everything the state machine asks it to do on the session. */
55
+ class Agent {
56
+ constructor(session, assistant, think, tools, recorder = exports.logRecorder, knowledge) {
57
+ this.session = session;
58
+ this.assistant = assistant;
59
+ this.think = think;
60
+ this.tools = tools;
61
+ this.recorder = recorder;
62
+ this.knowledge = knowledge;
63
+ this.history = [];
64
+ this.idleCount = 0;
65
+ this.resumes = 0;
66
+ this.stopped = false;
67
+ this.send = () => undefined;
68
+ this.prepareEarly = assistant.conversationSettings.patience !== "high";
69
+ }
70
+ get settings() {
71
+ return this.assistant.conversationSettings;
72
+ }
73
+ // ---- lifecycle ----------------------------------------------------------------------
74
+ /** Greeting: subscribe to the estimate, send initial DTMF, speak the first message. */
75
+ async greet() {
76
+ this.maxTimer = setTimeout(() => this.send({ type: "MAX_DURATION" }), this.settings.maxSessionDuration);
77
+ await this.safely(async () => {
78
+ await this.session.subscribe(["turn.estimate"]);
79
+ // The gateway's Pacing policy owns turn timing; Autopilot only tells it how patient to be.
80
+ // Recognition hints (names, brands) add to the app's defaults at the gateway (VO-B3).
81
+ const hints = this.settings.hints?.length
82
+ ? this.settings.hints
83
+ : undefined;
84
+ if (this.settings.patience !== "normal" || hints)
85
+ await this.session.listen({
86
+ ...(this.settings.patience !== "normal"
87
+ ? { patience: this.settings.patience }
88
+ : {}),
89
+ ...(hints ? { hints } : {})
90
+ });
91
+ if (this.settings.initialDtmf)
92
+ await this.session.playDtmf(this.settings.initialDtmf);
93
+ if (this.settings.firstMessage) {
94
+ const r = await this.session.say(this.settings.firstMessage, this.speechOptions());
95
+ this.history.push({ role: "assistant", content: r.heardText });
96
+ }
97
+ this.send({ type: "SPOKEN", source: "greeting" });
98
+ });
99
+ }
100
+ stop() {
101
+ this.stopped = true;
102
+ this.abortReply();
103
+ this.discardPrepared();
104
+ clearTimeout(this.idleTimer);
105
+ clearTimeout(this.maxTimer);
106
+ }
107
+ // ---- idle -------------------------------------------------------------------------
108
+ armIdle() {
109
+ clearTimeout(this.idleTimer);
110
+ const idle = this.settings.idleOptions;
111
+ if (idle)
112
+ this.idleTimer = setTimeout(() => this.send({ type: "IDLE" }), idle.timeout);
113
+ }
114
+ disarmIdle() {
115
+ clearTimeout(this.idleTimer);
116
+ }
117
+ /** The next idle timeout ends the call (the goodbye counts as the last announcement). */
118
+ idleExhausted() {
119
+ const max = this.settings.idleOptions?.maxTimeoutCount ?? 2;
120
+ return this.idleCount + 1 >= max;
121
+ }
122
+ async sayIdle() {
123
+ this.idleCount++;
124
+ await this.safely(async () => {
125
+ await this.session.say(this.settings.idleOptions.message, this.speechOptions());
126
+ this.send({ type: "SPOKEN", source: "idle" });
127
+ });
128
+ }
129
+ // ---- pacing -----------------------------------------------------------------------
130
+ /** turn.likely_ended: start generating silently (when preparing early). */
131
+ prepare(text) {
132
+ if (!this.prepareEarly || !text.trim())
133
+ return;
134
+ this.discardPrepared();
135
+ const abort = new AbortController();
136
+ const chunks = new AsyncQueue_1.AsyncQueue();
137
+ const prepared = { text, abort, chunks, context: [] };
138
+ this.prepared = prepared;
139
+ void (async () => {
140
+ prepared.context = await this.contextFor(text, abort.signal);
141
+ if (abort.signal.aborted)
142
+ return chunks.end();
143
+ const messages = [
144
+ ...this.messages(prepared.context),
145
+ { role: "user", content: text }
146
+ ];
147
+ await this.pump(this.think.stream(messages, this.toolSpecs(), abort.signal), chunks);
148
+ })();
149
+ }
150
+ /** turn.resumed: the caller kept talking; the prepared reply is dropped. */
151
+ resumed() {
152
+ this.discardPrepared();
153
+ this.resumes++;
154
+ if (this.resumes >= 3 && this.prepareEarly) {
155
+ // A caller who pauses often: stop preparing early for the rest of this call. The gateway
156
+ // adapts its own turn timing within the call (TT-B6), so Autopilot does not also change
157
+ // the patience it asked for (AP-B2).
158
+ this.prepareEarly = false;
159
+ this.recorder.note({
160
+ type: "pacing.adapted",
161
+ prepare: "on turn.ended",
162
+ resumes: this.resumes
163
+ });
164
+ }
165
+ }
166
+ // ---- replies ----------------------------------------------------------------------
167
+ async clear() {
168
+ await this.safely(() => this.session.stopSay());
169
+ }
170
+ abortReply() {
171
+ this.reply?.abort();
172
+ this.reply = undefined;
173
+ }
174
+ /** turn.ended: answer the caller, streaming sentences into one speak id. */
175
+ answer(text) {
176
+ const previous = this.inflight;
177
+ this.abortReply();
178
+ const abort = new AbortController();
179
+ this.reply = abort;
180
+ const run = this.respond(text, abort, previous);
181
+ this.inflight = run;
182
+ return run;
183
+ }
184
+ async respond(text, abort, previous) {
185
+ // Let a cancelled reply settle first, so what the caller heard of it is recorded before
186
+ // this turn (honest history, A6).
187
+ await previous?.catch(() => undefined);
188
+ this.idleCount = 0;
189
+ const turnEnded = Date.now();
190
+ this.history.push({ role: "user", content: text });
191
+ if (abort.signal.aborted)
192
+ return;
193
+ const trace = {
194
+ userText: text,
195
+ replyText: "",
196
+ heardText: "",
197
+ interrupted: false,
198
+ toolCalls: []
199
+ };
200
+ let preempt = false;
201
+ if (this.settings.filler) {
202
+ preempt = true;
203
+ void this.session
204
+ .play(this.settings.filler.url, { interruptible: true })
205
+ .catch(() => undefined);
206
+ }
207
+ try {
208
+ const prepared = this.takePrepared(text);
209
+ const context = prepared
210
+ ? prepared.context
211
+ : await this.contextFor(text, abort.signal);
212
+ let source = prepared?.chunks ?? this.startStream(abort.signal, context);
213
+ for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
214
+ const outcome = await this.speakRound(source, abort, preempt, turnEnded, trace);
215
+ preempt = false;
216
+ if (abort.signal.aborted &&
217
+ !outcome.interrupted &&
218
+ !outcome.completed) {
219
+ // Cancelled by a new turn: only what the caller already heard is kept.
220
+ if (outcome.heard)
221
+ this.history.push({ role: "assistant", content: outcome.heard });
222
+ return;
223
+ }
224
+ this.history.push({
225
+ role: "assistant",
226
+ content: outcome.heard,
227
+ ...(outcome.toolCalls.length ? { tool_calls: outcome.toolCalls } : {})
228
+ });
229
+ if (outcome.interrupted ||
230
+ !outcome.toolCalls.length ||
231
+ abort.signal.aborted)
232
+ break;
233
+ const builtIn = outcome.toolCalls.find((c) => c.name === "hangup" || c.name === "transfer");
234
+ if (builtIn) {
235
+ this.recorder.turn({
236
+ ...trace,
237
+ toolCalls: [...trace.toolCalls, { name: builtIn.name, ok: true }]
238
+ });
239
+ this.send({
240
+ type: builtIn.name === "hangup"
241
+ ? "HANGUP_REQUESTED"
242
+ : "TRANSFER_REQUESTED"
243
+ });
244
+ return;
245
+ }
246
+ await this.runTools(outcome.toolCalls, abort, trace);
247
+ if (abort.signal.aborted)
248
+ return;
249
+ source = this.startStream(abort.signal, context);
250
+ }
251
+ this.recorder.turn(trace);
252
+ if (this.reply === abort)
253
+ this.send({ type: "SPOKEN", source: "reply" });
254
+ }
255
+ catch (e) {
256
+ if (abort.signal.aborted ||
257
+ e instanceof voice_1.SessionEndedError ||
258
+ this.stopped)
259
+ return;
260
+ logger.error(`autopilot reply failed: ${e.message}`);
261
+ await this.safely(async () => {
262
+ await this.session.say(this.settings.systemErrorMessage, this.speechOptions());
263
+ this.send({ type: "SPOKEN", source: "error" });
264
+ });
265
+ }
266
+ }
267
+ async speakRound(source, abort, preempt, turnEnded, trace) {
268
+ const text = new AsyncQueue_1.AsyncQueue();
269
+ let toolCalls = [];
270
+ let full = "";
271
+ const pump = (async () => {
272
+ try {
273
+ for await (const chunk of source) {
274
+ if (abort.signal.aborted)
275
+ break;
276
+ if (chunk.type === "text") {
277
+ if (trace.firstTokenMs === undefined)
278
+ trace.firstTokenMs = Date.now() - turnEnded;
279
+ full += chunk.text;
280
+ text.push(chunk.text);
281
+ }
282
+ else
283
+ toolCalls = chunk.calls;
284
+ }
285
+ text.end();
286
+ }
287
+ catch (e) {
288
+ text.fail(e);
289
+ }
290
+ })();
291
+ const iterator = (0, sentences_1.sentences)(text)[Symbol.asyncIterator]();
292
+ const first = await iterator.next();
293
+ let heard = "";
294
+ let interrupted = false;
295
+ let completed = false;
296
+ if (!first.done && !abort.signal.aborted) {
297
+ this.send({ type: "REPLY_SPEAKING" });
298
+ trace.firstSpeakMs = Date.now() - turnEnded;
299
+ const pieces = (async function* () {
300
+ yield first.value;
301
+ for (;;) {
302
+ const next = await iterator.next();
303
+ if (next.done)
304
+ return;
305
+ yield next.value;
306
+ }
307
+ })();
308
+ const result = await this.session.say(pieces, {
309
+ ...this.speechOptions(),
310
+ preempt: preempt || undefined
311
+ });
312
+ heard = result.heardText;
313
+ interrupted = result.interrupted;
314
+ completed = !interrupted;
315
+ if (interrupted)
316
+ abort.abort();
317
+ }
318
+ await pump;
319
+ trace.replyText += full;
320
+ trace.heardText += heard;
321
+ trace.interrupted || (trace.interrupted = interrupted);
322
+ return { heard, interrupted, completed, toolCalls };
323
+ }
324
+ async runTools(calls, abort, trace) {
325
+ for (const call of calls) {
326
+ const tool = this.assistant.languageModel.tools.find((t) => t.name === call.name);
327
+ // The start message is spoken while the tool runs; the gateway queues the reply after it.
328
+ if (tool?.requestStartMessage) {
329
+ void this.session
330
+ .say(tool.requestStartMessage, this.speechOptions())
331
+ .catch(() => undefined);
332
+ }
333
+ let args = {};
334
+ try {
335
+ args = call.arguments ? JSON.parse(call.arguments) : {};
336
+ }
337
+ catch {
338
+ args = {};
339
+ }
340
+ const outcome = await this.tools.run(call.name, args, abort.signal);
341
+ trace.toolCalls.push({ name: call.name, ok: outcome.ok });
342
+ this.history.push({
343
+ role: "tool",
344
+ tool_call_id: call.id,
345
+ content: JSON.stringify(outcome.ok ? outcome.result : { error: outcome.error })
346
+ });
347
+ }
348
+ }
349
+ // ---- endings ----------------------------------------------------------------------
350
+ async goodbye() {
351
+ this.abortReply();
352
+ this.discardPrepared();
353
+ clearTimeout(this.idleTimer);
354
+ await this.safely(async () => {
355
+ await this.session.say(this.settings.goodbyeMessage, {
356
+ ...this.speechOptions(),
357
+ interruptible: false
358
+ });
359
+ await this.session.hangup();
360
+ });
361
+ }
362
+ async transfer() {
363
+ this.abortReply();
364
+ clearTimeout(this.idleTimer);
365
+ const options = this.settings.transferOptions;
366
+ await this.safely(async () => {
367
+ if (!options) {
368
+ await this.session.say(this.settings.systemErrorMessage, this.speechOptions());
369
+ this.send({ type: "SPOKEN", source: "error" });
370
+ return;
371
+ }
372
+ await this.session.say(options.message, {
373
+ ...this.speechOptions(),
374
+ interruptible: false
375
+ });
376
+ await this.session.transfer(options.phoneNumber);
377
+ });
378
+ }
379
+ // ---- internals --------------------------------------------------------------------
380
+ messages(context = []) {
381
+ return [
382
+ { role: "system", content: this.settings.systemPrompt },
383
+ ...context,
384
+ ...this.history
385
+ ];
386
+ }
387
+ /** Knowledge for this turn, as a system message for the request only (never in history). */
388
+ async contextFor(question, signal) {
389
+ if (!this.knowledge)
390
+ return [];
391
+ const timeout = new Promise((resolve) => setTimeout(() => resolve(""), knowledge_1.KNOWLEDGE_TIMEOUT_MS));
392
+ try {
393
+ const found = await Promise.race([
394
+ this.knowledge.find(question, signal),
395
+ timeout
396
+ ]);
397
+ return found?.trim()
398
+ ? [
399
+ {
400
+ role: "system",
401
+ content: `Relevant information:\n${found.trim()}`
402
+ }
403
+ ]
404
+ : [];
405
+ }
406
+ catch {
407
+ return [];
408
+ }
409
+ }
410
+ toolSpecs() {
411
+ const own = this.assistant.languageModel.tools.map((t) => ({
412
+ name: t.name,
413
+ description: t.description,
414
+ parameters: t.parameters
415
+ }));
416
+ const builtIn = BUILT_IN_TOOLS.filter((b) => b.name !== "transfer" || this.settings.transferOptions);
417
+ return [...own, ...builtIn];
418
+ }
419
+ startStream(signal, context = []) {
420
+ const chunks = new AsyncQueue_1.AsyncQueue();
421
+ void this.pump(this.think.stream(this.messages(context), this.toolSpecs(), signal), chunks);
422
+ return chunks;
423
+ }
424
+ takePrepared(text) {
425
+ const p = this.prepared;
426
+ this.prepared = undefined;
427
+ if (!p)
428
+ return undefined;
429
+ if (p.text === text && !p.abort.signal.aborted) {
430
+ // The reply prepared on turn.likely_ended is still valid; adopt its abort controller.
431
+ this.reply?.signal.addEventListener("abort", () => p.abort.abort(), {
432
+ once: true
433
+ });
434
+ return p;
435
+ }
436
+ p.abort.abort();
437
+ return undefined;
438
+ }
439
+ discardPrepared() {
440
+ this.prepared?.abort.abort();
441
+ this.prepared = undefined;
442
+ }
443
+ async pump(stream, into) {
444
+ try {
445
+ for await (const chunk of stream)
446
+ into.push(chunk);
447
+ into.end();
448
+ }
449
+ catch (e) {
450
+ into.fail(e);
451
+ }
452
+ }
453
+ speechOptions() {
454
+ return {
455
+ voice: this.settings.voice,
456
+ language: this.settings.language,
457
+ interruptible: this.settings.allowUserBargeIn
458
+ };
459
+ }
460
+ /** Runs session work; a session that has ended stops everything quietly. */
461
+ async safely(fn) {
462
+ if (this.stopped)
463
+ return;
464
+ try {
465
+ await fn();
466
+ }
467
+ catch (e) {
468
+ if (e instanceof voice_1.SessionEndedError || this.stopped)
469
+ return;
470
+ logger.error(`autopilot session error: ${e.message}`);
471
+ }
472
+ }
473
+ }
474
+ 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;