open-agents-ai 0.55.0 → 0.56.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 (2) hide show
  1. package/dist/index.js +204 -7
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -16726,6 +16726,62 @@ transcribe-cli error: ${transcribeCliError}` : "";
16726
16726
  this.pendingText = "";
16727
16727
  return text;
16728
16728
  }
16729
+ /**
16730
+ * Create a standalone transcriber for call sessions.
16731
+ * No microphone capture — just an ASR engine you feed PCM chunks to.
16732
+ * Returns an EventEmitter with .write(pcm: Buffer) and 'transcript' events.
16733
+ * Caller is responsible for calling .stop() when done.
16734
+ */
16735
+ async createCallTranscriber() {
16736
+ let tc = await this.loadTranscribeCli();
16737
+ if (!tc && _bgInstallPromise) {
16738
+ await _bgInstallPromise;
16739
+ this.transcribeCliAvailable = null;
16740
+ tc = await this.loadTranscribeCli();
16741
+ }
16742
+ if (!tc) {
16743
+ try {
16744
+ execSync18("npm i -g transcribe-cli", { stdio: "pipe", timeout: 18e4 });
16745
+ this.transcribeCliAvailable = null;
16746
+ tc = await this.loadTranscribeCli();
16747
+ } catch {
16748
+ }
16749
+ }
16750
+ if (tc?.TranscribeLive) {
16751
+ try {
16752
+ const transcriber = new tc.TranscribeLive({
16753
+ model: this.config.model,
16754
+ sampleRate: 16e3,
16755
+ channels: 1,
16756
+ sampleWidth: 2,
16757
+ chunkDuration: 3
16758
+ });
16759
+ await new Promise((resolve28, reject) => {
16760
+ const timeout = setTimeout(() => reject(new Error("Model load timeout (60s)")), 6e4);
16761
+ transcriber.on("ready", () => {
16762
+ clearTimeout(timeout);
16763
+ resolve28();
16764
+ });
16765
+ transcriber.on("error", (err) => {
16766
+ clearTimeout(timeout);
16767
+ reject(err);
16768
+ });
16769
+ });
16770
+ return transcriber;
16771
+ } catch {
16772
+ }
16773
+ }
16774
+ const scriptPath2 = findLiveWhisperScript();
16775
+ if (!scriptPath2)
16776
+ return null;
16777
+ try {
16778
+ const fallback = new WhisperFallbackTranscriber(this.config.model, scriptPath2);
16779
+ await fallback.start();
16780
+ return fallback;
16781
+ } catch {
16782
+ return null;
16783
+ }
16784
+ }
16729
16785
  /**
16730
16786
  * Transcribe a file (audio or video) using transcribe-cli.
16731
16787
  * Returns the transcription result.
@@ -17284,8 +17340,8 @@ function renderSlashHelp() {
17284
17340
  ["/listen manual", "Manual mode \u2014 press Enter to submit (turns off auto)"],
17285
17341
  ["/listen auto", "Auto-submit after 3s silence (blinking \u25CF indicator)"],
17286
17342
  ["/listen stop", "Stop listening"],
17287
- ["/call", "Start voice call session (cloudflared tunnel + PCM frontend)"],
17288
- ["/call stop", "Stop active call session"],
17343
+ ["/call", "Start voice call session (cloudflared tunnel + ASR/TTS)"],
17344
+ ["/hangup", "End active call session"],
17289
17345
  ["/cost", "Show session token cost breakdown"],
17290
17346
  ["/evaluate", "Evaluate last completed task (LLM quality scoring)"],
17291
17347
  ["/task-type", "Set task type (code, document, analysis, plan, general, auto)"],
@@ -17675,6 +17731,7 @@ var init_render = __esm({
17675
17731
  "/voice",
17676
17732
  "/listen",
17677
17733
  "/call",
17734
+ "/hangup",
17678
17735
  "/stream",
17679
17736
  "/verbose",
17680
17737
  "/dream",
@@ -17878,6 +17935,11 @@ function connect() {
17878
17935
  if (typeof evt.data === 'string') {
17879
17936
  try {
17880
17937
  const msg = JSON.parse(evt.data);
17938
+ if (msg.type === 'keepalive') {
17939
+ // Respond to keep connection alive through proxies
17940
+ if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'pong' }));
17941
+ return;
17942
+ }
17881
17943
  if (msg.type === 'transcript') {
17882
17944
  addTranscript(msg.speaker, msg.text);
17883
17945
  } else if (msg.type === 'speaking_start') {
@@ -18002,11 +18064,11 @@ function renderVoiceSessionStart(tunnelUrl) {
18002
18064
  process.stdout.write(`
18003
18065
  ${c2.cyan("\u2601")} ${c2.bold("Live Voice Session")}
18004
18066
  `);
18005
- process.stdout.write(` ${c2.dim("\u23BF")} URL: ${c2.cyan(tunnelUrl)}
18067
+ process.stdout.write(` ${c2.dim("\u23BF")} ${c2.cyan(tunnelUrl)}
18006
18068
  `);
18007
18069
  process.stdout.write(` ${c2.dim("\u23BF")} Bidirectional PCM audio + live transcription
18008
18070
  `);
18009
- process.stdout.write(` ${c2.dim("\u23BF")} Use /listen stop to end session
18071
+ process.stdout.write(` ${c2.dim("\u23BF")} /hangup to end session (auto-closes after 1 min idle)
18010
18072
 
18011
18073
  `);
18012
18074
  }
@@ -18023,6 +18085,12 @@ function renderVoiceSessionUser(action, username) {
18023
18085
  process.stdout.write(` ${c2.dim("\u23BF")} ${c2.cyan("\u2601")} ${icon} ${username} ${action}
18024
18086
  `);
18025
18087
  }
18088
+ function renderVoiceSessionTranscript(speaker, text) {
18089
+ const label = speaker === "user" ? c2.yellow("user") : c2.cyan("agent");
18090
+ const preview = text.length > 80 ? text.slice(0, 77) + "..." : text;
18091
+ process.stdout.write(` ${c2.dim("\u23BF")} ${c2.cyan("\u2601")} [${label}] ${preview}
18092
+ `);
18093
+ }
18026
18094
  var VoiceSession;
18027
18095
  var init_voice_session = __esm({
18028
18096
  "packages/cli/dist/tui/voice-session.js"() {
@@ -18034,7 +18102,10 @@ var init_voice_session = __esm({
18034
18102
  cloudflaredProcess = null;
18035
18103
  wsClients = /* @__PURE__ */ new Map();
18036
18104
  runtimeTimer = null;
18105
+ idleTimer = null;
18037
18106
  ttsSpeaking = false;
18107
+ /** Idle timeout before auto-closing (ms). Default 60s — no users connected → session ends. */
18108
+ idleTimeoutMs = 6e4;
18038
18109
  /** Callback invoked when user audio chunk arrives (PCM 16kHz 16-bit mono) */
18039
18110
  onUserAudio = null;
18040
18111
  /** Callback invoked when user speech is transcribed */
@@ -18068,6 +18139,8 @@ var init_voice_session = __esm({
18068
18139
  const port = await this.findFreePort();
18069
18140
  this.state.localPort = port;
18070
18141
  this.server = createServer((req, res) => this.handleHTTP(req, res));
18142
+ this.server.timeout = 0;
18143
+ this.server.keepAliveTimeout = 0;
18071
18144
  this.server.on("upgrade", (req, socket, head) => {
18072
18145
  if (req.url === "/ws") {
18073
18146
  this.handleWebSocketUpgrade(req, socket, head);
@@ -18093,7 +18166,8 @@ var init_voice_session = __esm({
18093
18166
  this.emit("tick", this.runtime);
18094
18167
  }, 1e3);
18095
18168
  this.emit("started", this.state.tunnelUrl);
18096
- return `Voice session started: ${this.state.tunnelUrl}`;
18169
+ this.resetIdleTimer();
18170
+ return this.state.tunnelUrl;
18097
18171
  }
18098
18172
  /**
18099
18173
  * Stop the voice session.
@@ -18106,6 +18180,10 @@ var init_voice_session = __esm({
18106
18180
  clearInterval(this.runtimeTimer);
18107
18181
  this.runtimeTimer = null;
18108
18182
  }
18183
+ if (this.idleTimer) {
18184
+ clearTimeout(this.idleTimer);
18185
+ this.idleTimer = null;
18186
+ }
18109
18187
  for (const [id, socket] of this.wsClients) {
18110
18188
  try {
18111
18189
  const closeFrame = createWebSocketFrame(8, Buffer.alloc(0));
@@ -18199,7 +18277,8 @@ var init_voice_session = __esm({
18199
18277
  const magic = "258EAFA5-E914-47DA-95CA-5AB9DC085B62";
18200
18278
  const accept = createHash("sha1").update(key + magic).digest("base64");
18201
18279
  socket.setNoDelay(true);
18202
- socket.setKeepAlive(true, 3e4);
18280
+ socket.setKeepAlive(true, 1e4);
18281
+ socket.setTimeout(0);
18203
18282
  socket.write(`HTTP/1.1 101 Switching Protocols\r
18204
18283
  Upgrade: websocket\r
18205
18284
  Connection: Upgrade\r
@@ -18210,6 +18289,7 @@ Sec-WebSocket-Accept: ${accept}\r
18210
18289
  this.wsClients.set(clientId, socket);
18211
18290
  this.state.connectedUsers.set(clientId, { username: "web-user", connectedAt: Date.now() });
18212
18291
  this.emit("userConnected", clientId, "web-user");
18292
+ this.resetIdleTimer();
18213
18293
  try {
18214
18294
  socket.write(createWebSocketFrame(9, Buffer.from("keepalive")));
18215
18295
  } catch {
@@ -18221,10 +18301,11 @@ Sec-WebSocket-Accept: ${accept}\r
18221
18301
  }
18222
18302
  try {
18223
18303
  socket.write(createWebSocketFrame(9, Buffer.from("keepalive")));
18304
+ socket.write(createWebSocketFrame(1, Buffer.from(JSON.stringify({ type: "keepalive" }))));
18224
18305
  } catch {
18225
18306
  clearInterval(pingInterval);
18226
18307
  }
18227
- }, 25e3);
18308
+ }, 5e3);
18228
18309
  let frameBuffer = Buffer.alloc(0);
18229
18310
  socket.on("data", (data) => {
18230
18311
  frameBuffer = Buffer.concat([frameBuffer, data]);
@@ -18277,11 +18358,13 @@ Sec-WebSocket-Accept: ${accept}\r
18277
18358
  this.wsClients.delete(clientId);
18278
18359
  this.state.connectedUsers.delete(clientId);
18279
18360
  this.emit("userDisconnected", clientId);
18361
+ this.resetIdleTimer();
18280
18362
  });
18281
18363
  socket.on("error", () => {
18282
18364
  clearInterval(pingInterval);
18283
18365
  this.wsClients.delete(clientId);
18284
18366
  this.state.connectedUsers.delete(clientId);
18367
+ this.resetIdleTimer();
18285
18368
  });
18286
18369
  if (head.length > 0) {
18287
18370
  socket.emit("data", head);
@@ -18324,6 +18407,27 @@ Sec-WebSocket-Accept: ${accept}\r
18324
18407
  });
18325
18408
  });
18326
18409
  }
18410
+ // ── Idle timer ───────────────────────────────────────────────────────
18411
+ /**
18412
+ * Reset idle timer. Called when users connect/disconnect.
18413
+ * When no users are connected, starts a countdown to auto-close.
18414
+ * When a user connects, cancels any pending auto-close.
18415
+ */
18416
+ resetIdleTimer() {
18417
+ if (this.idleTimer) {
18418
+ clearTimeout(this.idleTimer);
18419
+ this.idleTimer = null;
18420
+ }
18421
+ if (this.state.connectedUsers.size === 0) {
18422
+ this.idleTimer = setTimeout(() => {
18423
+ if (this.state.active && this.state.connectedUsers.size === 0) {
18424
+ this.emit("idle_timeout");
18425
+ this.stop().catch(() => {
18426
+ });
18427
+ }
18428
+ }, this.idleTimeoutMs);
18429
+ }
18430
+ }
18327
18431
  // ── Helpers ───────────────────────────────────────────────────────────
18328
18432
  findFreePort() {
18329
18433
  return new Promise((resolve28, reject) => {
@@ -20782,6 +20886,19 @@ async function handleSlashCommand(input, ctx) {
20782
20886
  }
20783
20887
  return "handled";
20784
20888
  }
20889
+ case "hangup": {
20890
+ if (!ctx.callStop) {
20891
+ renderWarning("Call mode not available in this context.");
20892
+ return "handled";
20893
+ }
20894
+ if (ctx.isCallActive?.()) {
20895
+ await ctx.callStop();
20896
+ renderInfo("Call session ended.");
20897
+ } else {
20898
+ renderWarning("No active call session.");
20899
+ }
20900
+ return "handled";
20901
+ }
20785
20902
  case "bruteforce":
20786
20903
  case "brute": {
20787
20904
  const isOn = ctx.bruteForceToggle();
@@ -23168,6 +23285,12 @@ var init_voice = __esm({
23168
23285
  enabled = false;
23169
23286
  modelId = "glados";
23170
23287
  ready = false;
23288
+ /**
23289
+ * Callback fired with PCM Int16 data whenever TTS synthesizes audio.
23290
+ * Used by VoiceSession to stream TTS output to WebSocket clients.
23291
+ * Set this to wire TTS → call session broadcast.
23292
+ */
23293
+ onPCMOutput = null;
23171
23294
  session = null;
23172
23295
  // ort.InferenceSession
23173
23296
  ort = null;
@@ -23399,6 +23522,14 @@ var init_voice = __esm({
23399
23522
  const audioData = result["output"].data;
23400
23523
  if (audioData.length === 0)
23401
23524
  return;
23525
+ if (this.onPCMOutput) {
23526
+ const int16 = new Int16Array(audioData.length);
23527
+ for (let i = 0; i < audioData.length; i++) {
23528
+ const s = Math.max(-1, Math.min(1, audioData[i]));
23529
+ int16[i] = s < 0 ? s * 32768 : s * 32767;
23530
+ }
23531
+ this.onPCMOutput(Buffer.from(int16.buffer), this.config.audio.sample_rate);
23532
+ }
23402
23533
  const wavPath = join34(tmpdir6(), `oa-voice-${Date.now()}.wav`);
23403
23534
  this.writeWav(audioData, this.config.audio.sample_rate, wavPath);
23404
23535
  await this.playWav(wavPath);
@@ -27495,6 +27626,8 @@ with summary "no_reply" to silently skip without responding.
27495
27626
  callUrlGetter = null;
27496
27627
  /** Callback to start a call session and return the URL (wired from interactive.ts) */
27497
27628
  callStarter = null;
27629
+ /** Callback to stop a call session (wired from interactive.ts) */
27630
+ callStopper = null;
27498
27631
  /** Callback to write content into the scrollable TUI waterfall area (wired from interactive.ts) */
27499
27632
  writeContent = null;
27500
27633
  /** Media cache — fileUniqueId → cache entry */
@@ -27538,6 +27671,10 @@ with summary "no_reply" to silently skip without responding.
27538
27671
  setCallStarter(starter) {
27539
27672
  this.callStarter = starter;
27540
27673
  }
27674
+ /** Register callback to stop a call session */
27675
+ setCallStopper(stopper) {
27676
+ this.callStopper = stopper;
27677
+ }
27541
27678
  /** Register callback to write content into the scrollable TUI area (status bar guard) */
27542
27679
  setWriteContent(fn) {
27543
27680
  this.writeContent = fn;
@@ -27698,6 +27835,16 @@ with summary "no_reply" to silently skip without responding.
27698
27835
  const isAdmin = this.isAdminUser(msg);
27699
27836
  const toolContext = this.resolveToolContext(msg, isAdmin);
27700
27837
  const isAdminDM = toolContext === "telegram-admin-dm";
27838
+ if (msg.text.trim().toLowerCase() === "/hangup" && isAdmin && this.callStopper) {
27839
+ const callUrl = this.callUrlGetter?.();
27840
+ if (callUrl) {
27841
+ await this.callStopper();
27842
+ await this.sendMessage(msg.chatId, "Call session ended.");
27843
+ } else {
27844
+ await this.sendMessage(msg.chatId, "No active call session.");
27845
+ }
27846
+ return;
27847
+ }
27701
27848
  if (msg.text.trim().toLowerCase() === "/call" && this.callUrlGetter) {
27702
27849
  const callUrl = this.callUrlGetter();
27703
27850
  if (callUrl) {
@@ -30854,6 +31001,7 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
30854
31001
  }
30855
31002
  telegramBridge.setCallUrlGetter(() => voiceSession?.tunnelUrl ?? null);
30856
31003
  telegramBridge.setCallStarter(async () => commandCtx.callStart());
31004
+ telegramBridge.setCallStopper(async () => commandCtx.callStop());
30857
31005
  telegramBridge.setCommandHandler(async (input) => {
30858
31006
  const captured = [];
30859
31007
  const origWrite = process.stdout.write;
@@ -31021,20 +31169,66 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
31021
31169
  return text ? `Stopped. Last transcript: "${text}"` : "Stopped listening.";
31022
31170
  },
31023
31171
  // Voice call session — standalone cloudflared tunnel for /call
31172
+ // Wires: WebSocket audio → ASR transcription → agent input → TTS → WebSocket broadcast
31024
31173
  async callStart() {
31025
31174
  if (voiceSession?.isActive) {
31026
31175
  return voiceSession.tunnelUrl;
31027
31176
  }
31028
31177
  voiceSession = new VoiceSession();
31178
+ const callState = { transcriber: null };
31179
+ const engine = getListenEngine();
31180
+ voiceSession.onUserAudio = (pcmChunk, userId) => {
31181
+ if (callState.transcriber)
31182
+ callState.transcriber.write(pcmChunk);
31183
+ };
31029
31184
  voiceSession.on("userConnected", (id, username) => {
31030
31185
  writeContent(() => renderVoiceSessionUser("connected", username));
31186
+ if (!callState.transcriber) {
31187
+ engine.createCallTranscriber().then((t) => {
31188
+ if (!t || !voiceSession?.isActive)
31189
+ return;
31190
+ callState.transcriber = t;
31191
+ callState.transcriber.on("transcript", (evt) => {
31192
+ if (!evt.text?.trim() || !voiceSession?.isActive)
31193
+ return;
31194
+ const text = evt.text.trim();
31195
+ writeContent(() => renderVoiceSessionTranscript("user", text));
31196
+ voiceSession?.sendTranscript("user", text);
31197
+ if (evt.isFinal) {
31198
+ rl.write(text + "\n");
31199
+ }
31200
+ });
31201
+ writeContent(() => renderInfo("Call ASR ready \u2014 listening for speech"));
31202
+ }).catch((err) => {
31203
+ writeContent(() => renderWarning(`Call ASR unavailable: ${err instanceof Error ? err.message : String(err)}`));
31204
+ });
31205
+ }
31031
31206
  });
31032
31207
  voiceSession.on("userDisconnected", (id) => {
31033
31208
  writeContent(() => renderVoiceSessionUser("disconnected", id));
31034
31209
  });
31210
+ voiceSession.on("idle_timeout", () => {
31211
+ writeContent(() => renderWarning("Call session auto-closed (1 min idle \u2014 no users connected)"));
31212
+ if (callState.transcriber) {
31213
+ callState.transcriber.stop();
31214
+ callState.transcriber = null;
31215
+ }
31216
+ voiceSession = null;
31217
+ });
31035
31218
  try {
31036
31219
  const tunnelUrl = await voiceSession.start();
31037
31220
  writeContent(() => renderVoiceSessionStart(tunnelUrl));
31221
+ if (voiceEngine.enabled && voiceEngine.ready) {
31222
+ const session = voiceSession;
31223
+ voiceEngine.onPCMOutput = (pcm, sampleRate) => {
31224
+ if (session?.isActive) {
31225
+ session.sendSpeakingState(true);
31226
+ session.sendAudioToClients(pcm);
31227
+ const durationMs = pcm.length / 2 / sampleRate * 1e3;
31228
+ setTimeout(() => session.sendSpeakingState(false), durationMs);
31229
+ }
31230
+ };
31231
+ }
31038
31232
  if (telegramBridge?.isActive && savedSettings.telegramAdmin) {
31039
31233
  const adminChatId = parseInt(savedSettings.telegramAdmin, 10);
31040
31234
  if (!isNaN(adminChatId)) {
@@ -31045,6 +31239,8 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
31045
31239
  return tunnelUrl;
31046
31240
  } catch (err) {
31047
31241
  writeContent(() => renderWarning(`Voice session failed: ${err instanceof Error ? err.message : String(err)}`));
31242
+ callState.transcriber?.stop();
31243
+ callState.transcriber = null;
31048
31244
  voiceSession = null;
31049
31245
  return null;
31050
31246
  }
@@ -31055,6 +31251,7 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
31055
31251
  await voiceSession.stop();
31056
31252
  writeContent(() => renderVoiceSessionStop(runtime));
31057
31253
  voiceSession = null;
31254
+ voiceEngine.onPCMOutput = null;
31058
31255
  }
31059
31256
  },
31060
31257
  isCallActive() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.55.0",
3
+ "version": "0.56.0",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",