open-agents-ai 0.77.0 → 0.78.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 +143 -32
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9550,7 +9550,18 @@ function installCronJob(task, workingDir) {
9550
9550
  const oaBin = findOaBinary();
9551
9551
  const logDir = resolve21(workingDir, ".oa", "scheduled", "logs");
9552
9552
  const logFile = join23(logDir, `${task.id}.log`);
9553
- const cronLine = `${task.schedule} cd ${JSON.stringify(workingDir)} && ${oaBin} ${JSON.stringify(task.task)} >> ${JSON.stringify(logFile)} 2>&1 ${CRON_MARKER}${task.id}`;
9553
+ const storeFile = resolve21(workingDir, ".oa", "scheduled", "tasks.json");
9554
+ const taskEscaped = task.task.replace(/'/g, "'\\''");
9555
+ const taskId = task.id;
9556
+ const wrapperCmd = [
9557
+ `cd ${JSON.stringify(workingDir)}`,
9558
+ `mkdir -p ${JSON.stringify(logDir)}`,
9559
+ // Run the task and capture exit code
9560
+ `${oaBin} '${taskEscaped}' >> ${JSON.stringify(logFile)} 2>&1; _oa_exit=$?`,
9561
+ // Update store: increment runCount, set lastRun, handle oneShot/maxRuns
9562
+ `node -e "const fs=require('fs'),p=${JSON.stringify(storeFile)};try{const s=JSON.parse(fs.readFileSync(p,'utf8'));const t=s.tasks.find(x=>x.id==='${taskId}');if(t){t.runCount=(t.runCount||0)+1;t.lastRun=new Date().toISOString();if(t.oneShot)t.enabled=false;if(t.maxRuns&&t.runCount>=t.maxRuns)t.enabled=false;s.updatedAt=new Date().toISOString();fs.writeFileSync(p,JSON.stringify(s,null,2));}}catch(e){}" 2>/dev/null`
9563
+ ].join("; ");
9564
+ const cronLine = `${task.schedule} ${wrapperCmd} ${CRON_MARKER}${task.id}`;
9554
9565
  const filtered = lines.filter((l) => !l.includes(`${CRON_MARKER}${task.id}`));
9555
9566
  filtered.push(cronLine);
9556
9567
  writeCrontab(filtered);
@@ -10815,8 +10826,21 @@ function installCronAgentJob(job, workingDir) {
10815
10826
  const oaBin = findOaBinary2();
10816
10827
  const logDir = resolve25(workingDir, ".oa", "cron-agents", "logs");
10817
10828
  const logFile = join27(logDir, `${job.id}.log`);
10829
+ const storeFile = resolve25(workingDir, ".oa", "cron-agents", "store.json");
10818
10830
  const taskEscaped = job.task.replace(/'/g, "'\\''");
10819
- const cronLine = `${job.schedule} cd ${JSON.stringify(workingDir)} && ${oaBin} '${taskEscaped}' >> ${JSON.stringify(logFile)} 2>&1 ${CRON_AGENT_MARKER}${job.id}`;
10831
+ const jobId = job.id;
10832
+ const wrapperCmd = [
10833
+ `cd ${JSON.stringify(workingDir)}`,
10834
+ `mkdir -p ${JSON.stringify(logDir)}`,
10835
+ `_oa_start=$(date +%s%3N 2>/dev/null || date +%s)`,
10836
+ // Run the task and capture exit code
10837
+ `${oaBin} '${taskEscaped}' >> ${JSON.stringify(logFile)} 2>&1; _oa_exit=$?`,
10838
+ `_oa_end=$(date +%s%3N 2>/dev/null || date +%s)`,
10839
+ // Update store: increment runCount, set lastRunAt, append to history
10840
+ // Pass shell vars as node CLI args: node -e "..." exitCode startMs endMs
10841
+ `node -e "const fs=require('fs'),p=${JSON.stringify(storeFile)};const[,,ex,st,en]=process.argv;try{const s=JSON.parse(fs.readFileSync(p,'utf8'));const j=s.jobs.find(x=>x.id==='${jobId}');if(j){j.runCount=(j.runCount||0)+1;j.lastRunAt=new Date().toISOString();const dur=Math.max(0,(+en||0)-(+st||0));j.history=(j.history||[]).concat({runAt:j.lastRunAt,exitCode:+(ex||0),outputSummary:'cron execution',durationMs:dur});if(j.history.length>20)j.history=j.history.slice(-20);if(j.maxRuns>0&&j.runCount>=j.maxRuns)j.status='completed';s.updatedAt=new Date().toISOString();fs.writeFileSync(p,JSON.stringify(s,null,2));}}catch(e){}" $_oa_exit $_oa_start $_oa_end 2>/dev/null`
10842
+ ].join("; ");
10843
+ const cronLine = `${job.schedule} ${wrapperCmd} ${CRON_AGENT_MARKER}${job.id}`;
10820
10844
  const filtered = lines.filter((l) => !l.includes(`${CRON_AGENT_MARKER}${job.id}`));
10821
10845
  filtered.push(cronLine);
10822
10846
  writeCrontab2(filtered);
@@ -30568,18 +30592,31 @@ var init_voice = __esm({
30568
30592
  return `Voice model set to ${VOICE_MODELS[key].label}. Enable with /voice.`;
30569
30593
  }
30570
30594
  /**
30571
- * Speak text asynchronously (non-blocking).
30595
+ * Speak text asynchronously (non-blocking) at full volume.
30572
30596
  * Long text is chunked on sentence/line boundaries for reliable TTS.
30573
30597
  * Chunks are queued FIFO and played back-to-back without cutoff.
30574
30598
  */
30575
30599
  speak(text) {
30600
+ this.enqueueSpeech(text, 1, 1);
30601
+ }
30602
+ /**
30603
+ * Speak text at reduced volume with slight pitch shift to indicate
30604
+ * subordinate/secondary output (sub-agent activity, tool narration, etc.).
30605
+ * Volume: 55% of primary. Pitch: shifted down slightly (0.92x).
30606
+ */
30607
+ speakSubordinate(text) {
30608
+ this.enqueueSpeech(text, 0.55, 0.92);
30609
+ }
30610
+ enqueueSpeech(text, volume, pitchFactor) {
30576
30611
  if (!this.enabled || !this.ready)
30577
30612
  return;
30578
30613
  const chunks = this.chunkText(text);
30579
30614
  if (this.speakQueue.length >= 30) {
30580
30615
  this.speakQueue.length = 0;
30581
30616
  }
30582
- this.speakQueue.push(...chunks);
30617
+ for (const chunk of chunks) {
30618
+ this.speakQueue.push({ text: chunk, volume, pitchFactor });
30619
+ }
30583
30620
  if (!this.speaking) {
30584
30621
  this.drainQueue().catch(() => {
30585
30622
  });
@@ -30704,33 +30741,98 @@ var init_voice = __esm({
30704
30741
  // -------------------------------------------------------------------------
30705
30742
  /**
30706
30743
  * Drain the speak queue FIFO — each item is synthesized and played to
30707
- * completion before the next starts. Items play back-to-back without
30708
- * cutting off previous audio.
30744
+ * completion before the next starts. A brief silence gap (250ms) is
30745
+ * inserted between queued items for natural pacing.
30709
30746
  */
30710
30747
  async drainQueue() {
30711
30748
  this.speaking = true;
30749
+ let isFirst = true;
30712
30750
  while (this.speakQueue.length > 0) {
30713
- const text = this.speakQueue.shift();
30751
+ const item = this.speakQueue.shift();
30752
+ if (!isFirst) {
30753
+ await this.sleep(250);
30754
+ }
30755
+ isFirst = false;
30714
30756
  try {
30715
- await this.synthesizeAndPlay(text);
30757
+ await this.synthesizeAndPlay(item.text, item.volume, item.pitchFactor);
30716
30758
  } catch {
30717
30759
  }
30718
30760
  }
30719
30761
  this.speaking = false;
30720
30762
  }
30763
+ sleep(ms) {
30764
+ return new Promise((resolve31) => setTimeout(resolve31, ms));
30765
+ }
30721
30766
  // -------------------------------------------------------------------------
30722
30767
  // Synthesis pipeline
30723
30768
  // -------------------------------------------------------------------------
30724
- async synthesizeAndPlay(text) {
30769
+ async synthesizeAndPlay(text, volume = 1, pitchFactor = 1) {
30725
30770
  if (!this.session || !this.config || !this.ort)
30726
30771
  return;
30727
30772
  const cleaned = text.replace(/\*/g, "");
30728
30773
  if (!cleaned.trim())
30729
30774
  return;
30730
- const textPhonemes = await this.textToPhonemes(cleaned);
30775
+ const sentences = cleaned.split(/(?<=[.!?])\s+/).filter((s) => s.trim());
30776
+ const sampleRate = this.config.audio.sample_rate;
30777
+ const sentenceSilenceSamples = Math.round(sampleRate * 0.18);
30778
+ const sentenceSilence = new Float32Array(sentenceSilenceSamples);
30779
+ const allSegments = [];
30780
+ for (let si = 0; si < sentences.length; si++) {
30781
+ const sentence = sentences[si].trim();
30782
+ if (!sentence)
30783
+ continue;
30784
+ const audioData2 = await this.synthesizeRaw(sentence);
30785
+ if (!audioData2 || audioData2.length === 0)
30786
+ continue;
30787
+ allSegments.push(audioData2);
30788
+ if (si < sentences.length - 1) {
30789
+ allSegments.push(sentenceSilence);
30790
+ }
30791
+ }
30792
+ if (allSegments.length === 0)
30793
+ return;
30794
+ const totalLen = allSegments.reduce((acc, seg) => acc + seg.length, 0);
30795
+ let audioData = new Float32Array(totalLen);
30796
+ let offset = 0;
30797
+ for (const seg of allSegments) {
30798
+ audioData.set(seg, offset);
30799
+ offset += seg.length;
30800
+ }
30801
+ if (volume !== 1) {
30802
+ for (let i = 0; i < audioData.length; i++) {
30803
+ audioData[i] = audioData[i] * volume;
30804
+ }
30805
+ }
30806
+ if (pitchFactor !== 1) {
30807
+ audioData = this.resamplePitch(audioData, pitchFactor);
30808
+ }
30809
+ if (this.onPCMOutput) {
30810
+ const int16 = new Int16Array(audioData.length);
30811
+ for (let i = 0; i < audioData.length; i++) {
30812
+ const s = Math.max(-1, Math.min(1, audioData[i]));
30813
+ int16[i] = s < 0 ? s * 32768 : s * 32767;
30814
+ }
30815
+ this.onPCMOutput(Buffer.from(int16.buffer), sampleRate);
30816
+ }
30817
+ const wavPath = join40(tmpdir6(), `oa-voice-${Date.now()}.wav`);
30818
+ this.writeWav(audioData, sampleRate, wavPath);
30819
+ await this.playWav(wavPath);
30820
+ try {
30821
+ unlinkSync4(wavPath);
30822
+ } catch {
30823
+ }
30824
+ }
30825
+ /**
30826
+ * Synthesize text to raw Float32 audio samples via ONNX inference.
30827
+ * Returns null if synthesis fails or produces no audio.
30828
+ */
30829
+ async synthesizeRaw(text) {
30830
+ if (!this.session || !this.config || !this.ort)
30831
+ return null;
30832
+ const textPhonemes = await this.textToPhonemes(text);
30731
30833
  const phonemeIds = this.phonemesToIds(textPhonemes);
30732
30834
  if (phonemeIds.length === 0)
30733
- return;
30835
+ return null;
30734
30836
  const inputLength = phonemeIds.length;
30735
30837
  const inputTensor = new this.ort.Tensor("int64", BigInt64Array.from(phonemeIds.map((id) => BigInt(id))), [1, inputLength]);
30736
30838
  const lengthTensor = new this.ort.Tensor("int64", BigInt64Array.from([BigInt(inputLength)]), [1]);
@@ -30745,23 +30847,28 @@ var init_voice = __esm({
30745
30847
  }
30746
30848
  const result = await this.session.run(feeds);
30747
30849
  const audioData = result["output"].data;
30748
- if (audioData.length === 0)
30749
- return;
30750
- if (this.onPCMOutput) {
30751
- const int16 = new Int16Array(audioData.length);
30752
- for (let i = 0; i < audioData.length; i++) {
30753
- const s = Math.max(-1, Math.min(1, audioData[i]));
30754
- int16[i] = s < 0 ? s * 32768 : s * 32767;
30755
- }
30756
- this.onPCMOutput(Buffer.from(int16.buffer), this.config.audio.sample_rate);
30757
- }
30758
- const wavPath = join40(tmpdir6(), `oa-voice-${Date.now()}.wav`);
30759
- this.writeWav(audioData, this.config.audio.sample_rate, wavPath);
30760
- await this.playWav(wavPath);
30761
- try {
30762
- unlinkSync4(wavPath);
30763
- } catch {
30764
- }
30850
+ return audioData.length > 0 ? audioData : null;
30851
+ }
30852
+ /**
30853
+ * Pitch-shift audio via linear-interpolation resampling.
30854
+ * Factor > 1.0 = higher pitch (shorter duration).
30855
+ * Factor < 1.0 = lower pitch (longer duration).
30856
+ * Duration is preserved by adjusting the output length accordingly
30857
+ * we resample to change pitch, then the WAV sample rate stays constant.
30858
+ */
30859
+ resamplePitch(samples, factor) {
30860
+ const newLen = Math.round(samples.length / factor);
30861
+ if (newLen <= 0)
30862
+ return samples;
30863
+ const out = new Float32Array(newLen);
30864
+ for (let i = 0; i < newLen; i++) {
30865
+ const srcIdx = i * factor;
30866
+ const lo = Math.floor(srcIdx);
30867
+ const hi = Math.min(lo + 1, samples.length - 1);
30868
+ const frac = srcIdx - lo;
30869
+ out[i] = samples[lo] * (1 - frac) + samples[hi] * frac;
30870
+ }
30871
+ return out;
30765
30872
  }
30766
30873
  // -------------------------------------------------------------------------
30767
30874
  // Phonemization
@@ -35745,8 +35852,12 @@ with summary "no_reply" to silently skip without responding.
35745
35852
  }
35746
35853
  const existing = this.subAgents.get(msg.chatId);
35747
35854
  if (existing && !existing.aborted) {
35748
- existing.runner.injectUserMessage(msg.text);
35749
- this.tuiWrite(() => renderTelegramSubAgentEvent(msg.username, "mid-conversation steering injected"));
35855
+ if (existing.runner) {
35856
+ existing.runner.injectUserMessage(msg.text);
35857
+ this.tuiWrite(() => renderTelegramSubAgentEvent(msg.username, "mid-conversation steering injected"));
35858
+ } else {
35859
+ this.tuiWrite(() => renderTelegramSubAgentEvent(msg.username, "queued (sub-agent still starting)"));
35860
+ }
35750
35861
  return;
35751
35862
  }
35752
35863
  const subAgent = {
@@ -38168,7 +38279,7 @@ ${entry.fullContent}`
38168
38279
  const emoCtx = emoState ? { valence: emoState.valence, arousal: emoState.arousal, label: emoState.label, emoji: emoState.emoji } : void 0;
38169
38280
  const desc = describeToolCall(event.toolName ?? "unknown", event.toolArgs ?? {}, vLevel, emoCtx);
38170
38281
  renderVoiceText(desc);
38171
- voice.speak(desc);
38282
+ voice.speakSubordinate(desc);
38172
38283
  }
38173
38284
  renderToolCallStart(event.toolName ?? "unknown", event.toolArgs ?? {}, config.verbose);
38174
38285
  });
@@ -38206,7 +38317,7 @@ ${entry.fullContent}`
38206
38317
  const desc = describeToolResult(event.toolName ?? "unknown", event.success ?? false, vLevel, event.content ?? void 0, emoCtx2);
38207
38318
  if (desc) {
38208
38319
  renderVoiceText(desc);
38209
- voice.speak(desc);
38320
+ voice.speakSubordinate(desc);
38210
38321
  }
38211
38322
  }
38212
38323
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.77.0",
3
+ "version": "0.78.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",