open-agents-ai 0.103.79 → 0.103.81

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 +245 -84
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -19134,7 +19134,8 @@ If you're stuck, try a completely different approach. Do NOT repeat what failed
19134
19134
  if (!choice)
19135
19135
  break;
19136
19136
  const msg = choice.message;
19137
- const isEmptyResponse = (!msg.content || !msg.content.trim()) && (!msg.toolCalls || msg.toolCalls.length === 0);
19137
+ const isThinkOnly = response._thinkOnly === true;
19138
+ const isEmptyResponse = !isThinkOnly && (!msg.content || !msg.content.trim()) && (!msg.toolCalls || msg.toolCalls.length === 0);
19138
19139
  if (isEmptyResponse) {
19139
19140
  consecutiveEmptyResponses++;
19140
19141
  if (consecutiveEmptyResponses >= 2) {
@@ -20970,6 +20971,8 @@ ${transcript}`
20970
20971
  }
20971
20972
  this.emit({ type: "stream_end", content, turn, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
20972
20973
  const cleanContent = content.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
20974
+ const hadThinking = content.includes("<think>") && content.includes("</think>");
20975
+ const thinkOnlyResponse = hadThinking && !cleanContent;
20973
20976
  const toolCalls = toolCallAccumulators.size > 0 ? Array.from(toolCallAccumulators.values()).map((tc) => {
20974
20977
  let args;
20975
20978
  try {
@@ -20980,10 +20983,20 @@ ${transcript}`
20980
20983
  }
20981
20984
  return { id: tc.id, name: tc.name, arguments: args };
20982
20985
  }) : void 0;
20983
- return {
20984
- choices: [{ message: { content: cleanContent || null, toolCalls } }],
20986
+ const resp = {
20987
+ choices: [{
20988
+ message: {
20989
+ // For think-only responses, include a marker so the model knows it already
20990
+ // thought about this (prevents infinite think loops without visible output)
20991
+ content: thinkOnlyResponse ? "[Your previous response was internal reasoning only. Now respond with visible text or a tool call.]" : cleanContent || null,
20992
+ toolCalls
20993
+ }
20994
+ }],
20985
20995
  usage: streamUsage
20986
20996
  };
20997
+ if (thinkOnlyResponse)
20998
+ resp._thinkOnly = true;
20999
+ return resp;
20987
21000
  }
20988
21001
  };
20989
21002
  OllamaAgenticBackend = class {
@@ -38944,6 +38957,19 @@ var init_voice = __esm({
38944
38957
  this.config = null;
38945
38958
  this.mlxActive = false;
38946
38959
  this.luxttsActive = false;
38960
+ if (this._luxttsDaemon && !this._luxttsDaemon.killed) {
38961
+ try {
38962
+ this._luxttsDaemon.stdin.write('{"action":"quit"}\n');
38963
+ } catch {
38964
+ }
38965
+ setTimeout(() => {
38966
+ try {
38967
+ this._luxttsDaemon?.kill();
38968
+ } catch {
38969
+ }
38970
+ }, 2e3);
38971
+ }
38972
+ this._luxttsDaemon = null;
38947
38973
  }
38948
38974
  // -------------------------------------------------------------------------
38949
38975
  // Text chunking for long TTS input
@@ -39471,6 +39497,23 @@ Error: ${err2 instanceof Error ? err2.message : String(err2)}`);
39471
39497
  if (existsSync34(venvPy)) {
39472
39498
  try {
39473
39499
  execSync27(`${venvPy} -c "import sys; sys.path.insert(0, '${luxttsRepoDir()}'); from zipvoice.luxvoice import LuxTTS; print('ok')"`, { stdio: "pipe", timeout: 3e4 });
39500
+ let hasCudaSys = false;
39501
+ try {
39502
+ execSync27("nvidia-smi", { stdio: "pipe", timeout: 5e3 });
39503
+ hasCudaSys = true;
39504
+ } catch {
39505
+ }
39506
+ if (hasCudaSys) {
39507
+ try {
39508
+ const torchCheck = execSync27(`${venvPy} -c "import torch; print('cuda' if torch.cuda.is_available() else 'cpu')"`, { stdio: "pipe", timeout: 15e3 }).toString().trim();
39509
+ if (torchCheck === "cpu") {
39510
+ renderWarning("GPU detected but PyTorch is CPU-only. Reinstalling with CUDA support...");
39511
+ execSync27(`${JSON.stringify(venvPy)} -m pip install --quiet --force-reinstall torch torchaudio --index-url https://download.pytorch.org/whl/cu124`, { stdio: "pipe", timeout: 6e5 });
39512
+ renderInfo("PyTorch reinstalled with CUDA GPU support.");
39513
+ }
39514
+ } catch {
39515
+ }
39516
+ }
39474
39517
  this.writeLuxttsInferScript();
39475
39518
  this.autoDetectCloneRef();
39476
39519
  return;
@@ -39490,14 +39533,33 @@ Error: ${err2 instanceof Error ? err2.message : String(err2)}`);
39490
39533
  throw new Error(`Failed to create venv: ${err instanceof Error ? err.message : String(err)}`);
39491
39534
  }
39492
39535
  }
39493
- renderInfo(" Installing PyTorch (CPU)...");
39536
+ let hasCuda = false;
39494
39537
  try {
39495
- execSync27(`${JSON.stringify(venvPy)} -m pip install --quiet torch torchaudio --index-url https://download.pytorch.org/whl/cpu`, { stdio: "pipe", timeout: 6e5 });
39496
- } catch (err) {
39538
+ execSync27("nvidia-smi", { stdio: "pipe", timeout: 5e3 });
39539
+ hasCuda = true;
39540
+ } catch {
39541
+ }
39542
+ if (hasCuda) {
39543
+ renderInfo(" Installing PyTorch (CUDA GPU)...");
39497
39544
  try {
39498
- execSync27(`${JSON.stringify(venvPy)} -m pip install --quiet torch torchaudio`, { stdio: "pipe", timeout: 6e5 });
39499
- } catch (err2) {
39500
- throw new Error(`Failed to install PyTorch: ${err2 instanceof Error ? err2.message : String(err2)}`);
39545
+ execSync27(`${JSON.stringify(venvPy)} -m pip install --quiet torch torchaudio --index-url https://download.pytorch.org/whl/cu124`, { stdio: "pipe", timeout: 6e5 });
39546
+ } catch {
39547
+ try {
39548
+ execSync27(`${JSON.stringify(venvPy)} -m pip install --quiet torch torchaudio`, { stdio: "pipe", timeout: 6e5 });
39549
+ } catch (err2) {
39550
+ throw new Error(`Failed to install PyTorch (CUDA): ${err2 instanceof Error ? err2.message : String(err2)}`);
39551
+ }
39552
+ }
39553
+ } else {
39554
+ renderInfo(" Installing PyTorch (CPU \u2014 no NVIDIA GPU detected)...");
39555
+ try {
39556
+ execSync27(`${JSON.stringify(venvPy)} -m pip install --quiet torch torchaudio --index-url https://download.pytorch.org/whl/cpu`, { stdio: "pipe", timeout: 6e5 });
39557
+ } catch {
39558
+ try {
39559
+ execSync27(`${JSON.stringify(venvPy)} -m pip install --quiet torch torchaudio`, { stdio: "pipe", timeout: 6e5 });
39560
+ } catch (err2) {
39561
+ throw new Error(`Failed to install PyTorch: ${err2 instanceof Error ? err2.message : String(err2)}`);
39562
+ }
39501
39563
  }
39502
39564
  }
39503
39565
  const repoDir = luxttsRepoDir();
@@ -39561,70 +39623,95 @@ Error: ${err2 instanceof Error ? err2.message : String(err2)}`);
39561
39623
  }
39562
39624
  }
39563
39625
  }
39564
- /** Write the LuxTTS inference helper Python script */
39626
+ /** Persistent LuxTTS daemon process keeps model warm in GPU memory */
39627
+ _luxttsDaemon = null;
39628
+ _luxttsPending = /* @__PURE__ */ new Map();
39629
+ _luxttsBuffer = "";
39630
+ /** Write the LuxTTS persistent daemon Python script */
39565
39631
  writeLuxttsInferScript() {
39566
39632
  const script = `#!/usr/bin/env python3
39567
- """LuxTTS inference helper for open-agents voice engine.
39568
- Called by voice.ts via execSync with JSON args.
39633
+ """LuxTTS persistent daemon for open-agents voice engine.
39634
+ Keeps model warm in GPU memory. Reads JSON requests from stdin, writes JSON responses to stdout.
39569
39635
  """
39570
- import sys, json, os, wave, struct
39636
+ import sys, json, os, wave, signal
39571
39637
  import numpy as np
39572
39638
 
39573
39639
  def main():
39574
- args = json.loads(sys.argv[1])
39575
- action = args.get('action', 'synthesize')
39576
-
39577
- repo_path = args.get('repo_path', '')
39640
+ repo_path = os.environ.get('LUXTTS_REPO_PATH', '')
39578
39641
  if repo_path:
39579
39642
  sys.path.insert(0, repo_path)
39580
39643
 
39581
- if action == 'check':
39644
+ import torch
39645
+ from zipvoice.luxvoice import LuxTTS
39646
+
39647
+ # Auto-detect best device
39648
+ if torch.cuda.is_available():
39649
+ device = 'cuda'
39650
+ elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
39651
+ device = 'mps'
39652
+ else:
39653
+ device = 'cpu'
39654
+
39655
+ # Load model ONCE \u2014 stays warm in GPU/CPU memory
39656
+ tts = LuxTTS(model_path='YatharthS/LuxTTS', device=device, threads=4)
39657
+ sys.stdout.write(json.dumps({'type': 'ready', 'device': device}) + '\\n')
39658
+ sys.stdout.flush()
39659
+
39660
+ # Cache encoded prompts by clone_ref path (avoid re-encoding same voice)
39661
+ prompt_cache = {}
39662
+
39663
+ for line in sys.stdin:
39664
+ line = line.strip()
39665
+ if not line:
39666
+ continue
39582
39667
  try:
39583
- from zipvoice.luxvoice import LuxTTS
39584
- print(json.dumps({'ok': True}))
39668
+ req = json.loads(line)
39585
39669
  except Exception as e:
39586
- print(json.dumps({'ok': False, 'error': str(e)}))
39587
- return
39588
-
39589
- if action == 'synthesize':
39590
- import torch
39591
- from zipvoice.luxvoice import LuxTTS
39592
-
39593
- text = args['text']
39594
- clone_ref = args['clone_ref']
39595
- output_path = args['output_path']
39596
- speed = args.get('speed', 1.0)
39597
-
39598
- # Auto-detect best device
39599
- if torch.cuda.is_available():
39600
- device = 'cuda'
39601
- elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
39602
- device = 'mps'
39603
- else:
39604
- device = 'cpu'
39605
-
39606
- threads = 4 if device == 'cpu' else 4
39607
- tts = LuxTTS(model_path='YatharthS/LuxTTS', device=device, threads=threads)
39608
- encoded = tts.encode_prompt(clone_ref, duration=5, rms=0.001)
39609
- wav_tensor = tts.generate_speech(text, encoded, num_steps=4, guidance_scale=3.0, t_shift=0.5, speed=speed)
39610
- wav_np = wav_tensor.cpu().numpy().squeeze()
39611
-
39612
- # Write as 48kHz 16-bit mono WAV using stdlib
39613
- int16_data = (np.clip(wav_np, -1.0, 1.0) * 32767).astype(np.int16)
39614
- with wave.open(output_path, 'wb') as wf:
39615
- wf.setnchannels(1)
39616
- wf.setsampwidth(2)
39617
- wf.setframerate(48000)
39618
- wf.writeframes(int16_data.tobytes())
39619
-
39620
- print(json.dumps({
39621
- 'ok': True,
39622
- 'path': output_path,
39623
- 'sample_rate': 48000,
39624
- 'device': device,
39625
- 'samples': len(int16_data),
39626
- }))
39627
- return
39670
+ sys.stdout.write(json.dumps({'type': 'error', 'id': '', 'error': str(e)}) + '\\n')
39671
+ sys.stdout.flush()
39672
+ continue
39673
+
39674
+ req_id = req.get('id', '')
39675
+
39676
+ if req.get('action') == 'check':
39677
+ sys.stdout.write(json.dumps({'type': 'result', 'id': req_id, 'ok': True}) + '\\n')
39678
+ sys.stdout.flush()
39679
+ continue
39680
+
39681
+ if req.get('action') == 'quit':
39682
+ break
39683
+
39684
+ if req.get('action') == 'synthesize':
39685
+ try:
39686
+ text = req['text']
39687
+ clone_ref = req['clone_ref']
39688
+ output_path = req['output_path']
39689
+ speed = req.get('speed', 1.0)
39690
+
39691
+ # Use cached prompt encoding if available
39692
+ if clone_ref not in prompt_cache:
39693
+ prompt_cache[clone_ref] = tts.encode_prompt(clone_ref, duration=5, rms=0.001)
39694
+ encoded = prompt_cache[clone_ref]
39695
+
39696
+ wav_tensor = tts.generate_speech(text, encoded, num_steps=4, guidance_scale=3.0, t_shift=0.5, speed=speed)
39697
+ wav_np = wav_tensor.cpu().numpy().squeeze()
39698
+
39699
+ int16_data = (np.clip(wav_np, -1.0, 1.0) * 32767).astype(np.int16)
39700
+ with wave.open(output_path, 'wb') as wf:
39701
+ wf.setnchannels(1)
39702
+ wf.setsampwidth(2)
39703
+ wf.setframerate(48000)
39704
+ wf.writeframes(int16_data.tobytes())
39705
+
39706
+ sys.stdout.write(json.dumps({
39707
+ 'type': 'result', 'id': req_id, 'ok': True,
39708
+ 'path': output_path, 'sample_rate': 48000,
39709
+ 'device': device, 'samples': len(int16_data),
39710
+ }) + '\\n')
39711
+ sys.stdout.flush()
39712
+ except Exception as e:
39713
+ sys.stdout.write(json.dumps({'type': 'error', 'id': req_id, 'error': str(e)}) + '\\n')
39714
+ sys.stdout.flush()
39628
39715
 
39629
39716
  if __name__ == '__main__':
39630
39717
  main()
@@ -39633,6 +39720,84 @@ if __name__ == '__main__':
39633
39720
  mkdirSync14(voiceDir(), { recursive: true });
39634
39721
  writeFileSync14(scriptPath2, script);
39635
39722
  }
39723
+ /** Ensure the LuxTTS daemon is running, spawn if needed */
39724
+ async ensureLuxttsDaemon() {
39725
+ if (this._luxttsDaemon && !this._luxttsDaemon.killed)
39726
+ return true;
39727
+ const venvPy = luxttsVenvPy();
39728
+ if (!existsSync34(venvPy))
39729
+ return false;
39730
+ return new Promise((resolve31) => {
39731
+ const env = { ...process.env, LUXTTS_REPO_PATH: luxttsRepoDir() };
39732
+ const daemon = nodeSpawn(venvPy, [luxttsInferScript()], {
39733
+ stdio: ["pipe", "pipe", "pipe"],
39734
+ cwd: tmpdir6(),
39735
+ env
39736
+ });
39737
+ this._luxttsDaemon = daemon;
39738
+ this._luxttsBuffer = "";
39739
+ daemon.stdout.on("data", (chunk) => {
39740
+ this._luxttsBuffer += chunk.toString();
39741
+ const lines = this._luxttsBuffer.split("\n");
39742
+ this._luxttsBuffer = lines.pop();
39743
+ for (const line of lines) {
39744
+ if (!line.trim())
39745
+ continue;
39746
+ try {
39747
+ const msg = JSON.parse(line);
39748
+ if (msg.type === "ready") {
39749
+ resolve31(true);
39750
+ } else if (msg.type === "result" || msg.type === "error") {
39751
+ const pending = this._luxttsPending.get(msg.id);
39752
+ if (pending) {
39753
+ this._luxttsPending.delete(msg.id);
39754
+ if (msg.type === "error")
39755
+ pending.reject(new Error(msg.error));
39756
+ else
39757
+ pending.resolve(line);
39758
+ }
39759
+ }
39760
+ } catch {
39761
+ }
39762
+ }
39763
+ });
39764
+ daemon.on("exit", () => {
39765
+ this._luxttsDaemon = null;
39766
+ for (const [, p] of this._luxttsPending) {
39767
+ p.reject(new Error("LuxTTS daemon exited"));
39768
+ }
39769
+ this._luxttsPending.clear();
39770
+ });
39771
+ daemon.on("error", () => {
39772
+ this._luxttsDaemon = null;
39773
+ resolve31(false);
39774
+ });
39775
+ setTimeout(() => {
39776
+ if (this._luxttsDaemon === daemon && !this._luxttsPending.has("__ready__")) {
39777
+ resolve31(false);
39778
+ }
39779
+ }, 6e4);
39780
+ });
39781
+ }
39782
+ /** Send a request to the LuxTTS daemon and await the response */
39783
+ luxttsRequest(req) {
39784
+ return new Promise((resolve31, reject) => {
39785
+ if (!this._luxttsDaemon || this._luxttsDaemon.killed) {
39786
+ reject(new Error("LuxTTS daemon not running"));
39787
+ return;
39788
+ }
39789
+ const id = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
39790
+ req.id = id;
39791
+ this._luxttsPending.set(id, { resolve: resolve31, reject });
39792
+ this._luxttsDaemon.stdin.write(JSON.stringify(req) + "\n");
39793
+ setTimeout(() => {
39794
+ if (this._luxttsPending.has(id)) {
39795
+ this._luxttsPending.delete(id);
39796
+ reject(new Error("LuxTTS synthesis timeout"));
39797
+ }
39798
+ }, 12e4);
39799
+ });
39800
+ }
39636
39801
  /**
39637
39802
  * Synthesize and play audio using LuxTTS voice cloning.
39638
39803
  * Speed is passed natively to LuxTTS. Pitch is post-processed via resampling.
@@ -39645,20 +39810,18 @@ if __name__ == '__main__':
39645
39810
  const cleaned = text.replace(/\*/g, "").trim();
39646
39811
  if (!cleaned)
39647
39812
  return;
39648
- const venvPy = luxttsVenvPy();
39649
- if (!existsSync34(venvPy))
39813
+ const ready = await this.ensureLuxttsDaemon();
39814
+ if (!ready)
39650
39815
  return;
39651
39816
  const wavPath = join43(tmpdir6(), `oa-luxtts-${Date.now()}.wav`);
39652
- const inferArgs = JSON.stringify({
39653
- action: "synthesize",
39654
- repo_path: luxttsRepoDir(),
39655
- text: cleaned,
39656
- clone_ref: this.luxttsCloneRef,
39657
- output_path: wavPath,
39658
- speed: speedFactor
39659
- });
39660
39817
  try {
39661
- execSync27(`${JSON.stringify(venvPy)} ${JSON.stringify(luxttsInferScript())} ${JSON.stringify(inferArgs)}`, { stdio: "pipe", timeout: 12e4, cwd: tmpdir6() });
39818
+ await this.luxttsRequest({
39819
+ action: "synthesize",
39820
+ text: cleaned,
39821
+ clone_ref: this.luxttsCloneRef,
39822
+ output_path: wavPath,
39823
+ speed: speedFactor
39824
+ });
39662
39825
  } catch {
39663
39826
  return;
39664
39827
  }
@@ -39721,20 +39884,18 @@ if __name__ == '__main__':
39721
39884
  const cleaned = text.replace(/\*/g, "").trim();
39722
39885
  if (!cleaned)
39723
39886
  return null;
39724
- const venvPy = luxttsVenvPy();
39725
- if (!existsSync34(venvPy))
39887
+ const ready = await this.ensureLuxttsDaemon();
39888
+ if (!ready)
39726
39889
  return null;
39727
39890
  const wavPath = join43(tmpdir6(), `oa-luxtts-buf-${Date.now()}.wav`);
39728
- const inferArgs = JSON.stringify({
39729
- action: "synthesize",
39730
- repo_path: luxttsRepoDir(),
39731
- text: cleaned,
39732
- clone_ref: this.luxttsCloneRef,
39733
- output_path: wavPath,
39734
- speed: 1
39735
- });
39736
39891
  try {
39737
- execSync27(`${JSON.stringify(venvPy)} ${JSON.stringify(luxttsInferScript())} ${JSON.stringify(inferArgs)}`, { stdio: "pipe", timeout: 12e4, cwd: tmpdir6() });
39892
+ await this.luxttsRequest({
39893
+ action: "synthesize",
39894
+ text: cleaned,
39895
+ clone_ref: this.luxttsCloneRef,
39896
+ output_path: wavPath,
39897
+ speed: 1
39898
+ });
39738
39899
  } catch {
39739
39900
  return null;
39740
39901
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.103.79",
3
+ "version": "0.103.81",
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",