open-agents-ai 0.77.0 → 0.79.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 +161 -36
  2. package/package.json +2 -2
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);
@@ -11409,7 +11433,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
11409
11433
  ];
11410
11434
  NexusTool = class {
11411
11435
  name = "nexus";
11412
- description = "Decentralized agent-to-agent communication via open-agents-nexus v1.2.1. Spawns a background Node.js process with real network sockets for libp2p + NATS P2P mesh. Simple flow: connect \u2192 join room \u2192 send messages. Auto-installs nexus if needed. Also supports direct peer invoke (streaming inference), IPFS content storage, direct messages, peer discovery, x402 micropayments, and inference proofs.";
11436
+ description = "Decentralized agent-to-agent communication via open-agents-nexus v1.3.0. Spawns a background Node.js process with real network sockets for libp2p + NATS P2P mesh. Simple flow: connect \u2192 join room \u2192 send messages. Auto-installs nexus if needed. Also supports direct peer invoke (streaming inference), IPFS content storage, direct messages, peer discovery, x402 micropayments, and inference proofs. Onboarding: curl openagents.nexus/llms.txt for full instructions, /.well-known/agent.json for machine-readable manifest.";
11413
11437
  parameters = {
11414
11438
  type: "object",
11415
11439
  properties: {
@@ -30079,6 +30103,10 @@ function modelOnnxPath(id) {
30079
30103
  function modelConfigPath(id) {
30080
30104
  return join40(modelDir(id), "config.json");
30081
30105
  }
30106
+ function emotionToPitchBias(emotion) {
30107
+ const raw = emotion.valence * 0.6 + (emotion.arousal - 0.5) * 0.4;
30108
+ return Math.max(-0.1, Math.min(0.1, raw * 0.1));
30109
+ }
30082
30110
  function resetNarrationContext() {
30083
30111
  narration.toolCount = 0;
30084
30112
  narration.toolCounts = {};
@@ -30568,18 +30596,37 @@ var init_voice = __esm({
30568
30596
  return `Voice model set to ${VOICE_MODELS[key].label}. Enable with /voice.`;
30569
30597
  }
30570
30598
  /**
30571
- * Speak text asynchronously (non-blocking).
30599
+ * Speak text asynchronously (non-blocking) at full volume.
30572
30600
  * Long text is chunked on sentence/line boundaries for reliable TTS.
30573
30601
  * Chunks are queued FIFO and played back-to-back without cutoff.
30602
+ * If emotion context is provided, pitch is biased by valence/arousal:
30603
+ * excited (high valence + high arousal) → higher pitch
30604
+ * dejected (low valence + low arousal) → lower pitch
30605
+ */
30606
+ speak(text, emotion) {
30607
+ const pitchBias = emotion ? emotionToPitchBias(emotion) : 0;
30608
+ this.enqueueSpeech(text, 1, 1 + pitchBias);
30609
+ }
30610
+ /**
30611
+ * Speak text at reduced volume with pitch shift to indicate
30612
+ * subordinate/secondary output (sub-agent activity, tool narration, etc.).
30613
+ * Volume: 55% of primary. Base pitch: 0.92x (lower).
30614
+ * Emotion further modulates pitch on top of the subordinate base.
30574
30615
  */
30575
- speak(text) {
30616
+ speakSubordinate(text, emotion) {
30617
+ const pitchBias = emotion ? emotionToPitchBias(emotion) : 0;
30618
+ this.enqueueSpeech(text, 0.55, 0.92 + pitchBias);
30619
+ }
30620
+ enqueueSpeech(text, volume, pitchFactor) {
30576
30621
  if (!this.enabled || !this.ready)
30577
30622
  return;
30578
30623
  const chunks = this.chunkText(text);
30579
30624
  if (this.speakQueue.length >= 30) {
30580
30625
  this.speakQueue.length = 0;
30581
30626
  }
30582
- this.speakQueue.push(...chunks);
30627
+ for (const chunk of chunks) {
30628
+ this.speakQueue.push({ text: chunk, volume, pitchFactor });
30629
+ }
30583
30630
  if (!this.speaking) {
30584
30631
  this.drainQueue().catch(() => {
30585
30632
  });
@@ -30704,33 +30751,98 @@ var init_voice = __esm({
30704
30751
  // -------------------------------------------------------------------------
30705
30752
  /**
30706
30753
  * 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.
30754
+ * completion before the next starts. A brief silence gap (250ms) is
30755
+ * inserted between queued items for natural pacing.
30709
30756
  */
30710
30757
  async drainQueue() {
30711
30758
  this.speaking = true;
30759
+ let isFirst = true;
30712
30760
  while (this.speakQueue.length > 0) {
30713
- const text = this.speakQueue.shift();
30761
+ const item = this.speakQueue.shift();
30762
+ if (!isFirst) {
30763
+ await this.sleep(250);
30764
+ }
30765
+ isFirst = false;
30714
30766
  try {
30715
- await this.synthesizeAndPlay(text);
30767
+ await this.synthesizeAndPlay(item.text, item.volume, item.pitchFactor);
30716
30768
  } catch {
30717
30769
  }
30718
30770
  }
30719
30771
  this.speaking = false;
30720
30772
  }
30773
+ sleep(ms) {
30774
+ return new Promise((resolve31) => setTimeout(resolve31, ms));
30775
+ }
30721
30776
  // -------------------------------------------------------------------------
30722
30777
  // Synthesis pipeline
30723
30778
  // -------------------------------------------------------------------------
30724
- async synthesizeAndPlay(text) {
30779
+ async synthesizeAndPlay(text, volume = 1, pitchFactor = 1) {
30725
30780
  if (!this.session || !this.config || !this.ort)
30726
30781
  return;
30727
30782
  const cleaned = text.replace(/\*/g, "");
30728
30783
  if (!cleaned.trim())
30729
30784
  return;
30730
- const textPhonemes = await this.textToPhonemes(cleaned);
30785
+ const sentences = cleaned.split(/(?<=[.!?])\s+/).filter((s) => s.trim());
30786
+ const sampleRate = this.config.audio.sample_rate;
30787
+ const sentenceSilenceSamples = Math.round(sampleRate * 0.18);
30788
+ const sentenceSilence = new Float32Array(sentenceSilenceSamples);
30789
+ const allSegments = [];
30790
+ for (let si = 0; si < sentences.length; si++) {
30791
+ const sentence = sentences[si].trim();
30792
+ if (!sentence)
30793
+ continue;
30794
+ const audioData2 = await this.synthesizeRaw(sentence);
30795
+ if (!audioData2 || audioData2.length === 0)
30796
+ continue;
30797
+ allSegments.push(audioData2);
30798
+ if (si < sentences.length - 1) {
30799
+ allSegments.push(sentenceSilence);
30800
+ }
30801
+ }
30802
+ if (allSegments.length === 0)
30803
+ return;
30804
+ const totalLen = allSegments.reduce((acc, seg) => acc + seg.length, 0);
30805
+ let audioData = new Float32Array(totalLen);
30806
+ let offset = 0;
30807
+ for (const seg of allSegments) {
30808
+ audioData.set(seg, offset);
30809
+ offset += seg.length;
30810
+ }
30811
+ if (volume !== 1) {
30812
+ for (let i = 0; i < audioData.length; i++) {
30813
+ audioData[i] = audioData[i] * volume;
30814
+ }
30815
+ }
30816
+ if (pitchFactor !== 1) {
30817
+ audioData = this.resamplePitch(audioData, pitchFactor);
30818
+ }
30819
+ if (this.onPCMOutput) {
30820
+ const int16 = new Int16Array(audioData.length);
30821
+ for (let i = 0; i < audioData.length; i++) {
30822
+ const s = Math.max(-1, Math.min(1, audioData[i]));
30823
+ int16[i] = s < 0 ? s * 32768 : s * 32767;
30824
+ }
30825
+ this.onPCMOutput(Buffer.from(int16.buffer), sampleRate);
30826
+ }
30827
+ const wavPath = join40(tmpdir6(), `oa-voice-${Date.now()}.wav`);
30828
+ this.writeWav(audioData, sampleRate, wavPath);
30829
+ await this.playWav(wavPath);
30830
+ try {
30831
+ unlinkSync4(wavPath);
30832
+ } catch {
30833
+ }
30834
+ }
30835
+ /**
30836
+ * Synthesize text to raw Float32 audio samples via ONNX inference.
30837
+ * Returns null if synthesis fails or produces no audio.
30838
+ */
30839
+ async synthesizeRaw(text) {
30840
+ if (!this.session || !this.config || !this.ort)
30841
+ return null;
30842
+ const textPhonemes = await this.textToPhonemes(text);
30731
30843
  const phonemeIds = this.phonemesToIds(textPhonemes);
30732
30844
  if (phonemeIds.length === 0)
30733
- return;
30845
+ return null;
30734
30846
  const inputLength = phonemeIds.length;
30735
30847
  const inputTensor = new this.ort.Tensor("int64", BigInt64Array.from(phonemeIds.map((id) => BigInt(id))), [1, inputLength]);
30736
30848
  const lengthTensor = new this.ort.Tensor("int64", BigInt64Array.from([BigInt(inputLength)]), [1]);
@@ -30745,23 +30857,28 @@ var init_voice = __esm({
30745
30857
  }
30746
30858
  const result = await this.session.run(feeds);
30747
30859
  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
- }
30860
+ return audioData.length > 0 ? audioData : null;
30861
+ }
30862
+ /**
30863
+ * Pitch-shift audio via linear-interpolation resampling.
30864
+ * Factor > 1.0 = higher pitch (shorter duration).
30865
+ * Factor < 1.0 = lower pitch (longer duration).
30866
+ * Duration is preserved by adjusting the output length accordingly
30867
+ * we resample to change pitch, then the WAV sample rate stays constant.
30868
+ */
30869
+ resamplePitch(samples, factor) {
30870
+ const newLen = Math.round(samples.length / factor);
30871
+ if (newLen <= 0)
30872
+ return samples;
30873
+ const out = new Float32Array(newLen);
30874
+ for (let i = 0; i < newLen; i++) {
30875
+ const srcIdx = i * factor;
30876
+ const lo = Math.floor(srcIdx);
30877
+ const hi = Math.min(lo + 1, samples.length - 1);
30878
+ const frac = srcIdx - lo;
30879
+ out[i] = samples[lo] * (1 - frac) + samples[hi] * frac;
30880
+ }
30881
+ return out;
30765
30882
  }
30766
30883
  // -------------------------------------------------------------------------
30767
30884
  // Phonemization
@@ -35745,8 +35862,12 @@ with summary "no_reply" to silently skip without responding.
35745
35862
  }
35746
35863
  const existing = this.subAgents.get(msg.chatId);
35747
35864
  if (existing && !existing.aborted) {
35748
- existing.runner.injectUserMessage(msg.text);
35749
- this.tuiWrite(() => renderTelegramSubAgentEvent(msg.username, "mid-conversation steering injected"));
35865
+ if (existing.runner) {
35866
+ existing.runner.injectUserMessage(msg.text);
35867
+ this.tuiWrite(() => renderTelegramSubAgentEvent(msg.username, "mid-conversation steering injected"));
35868
+ } else {
35869
+ this.tuiWrite(() => renderTelegramSubAgentEvent(msg.username, "queued (sub-agent still starting)"));
35870
+ }
35750
35871
  return;
35751
35872
  }
35752
35873
  const subAgent = {
@@ -38168,7 +38289,7 @@ ${entry.fullContent}`
38168
38289
  const emoCtx = emoState ? { valence: emoState.valence, arousal: emoState.arousal, label: emoState.label, emoji: emoState.emoji } : void 0;
38169
38290
  const desc = describeToolCall(event.toolName ?? "unknown", event.toolArgs ?? {}, vLevel, emoCtx);
38170
38291
  renderVoiceText(desc);
38171
- voice.speak(desc);
38292
+ voice.speakSubordinate(desc, emoCtx);
38172
38293
  }
38173
38294
  renderToolCallStart(event.toolName ?? "unknown", event.toolArgs ?? {}, config.verbose);
38174
38295
  });
@@ -38206,7 +38327,7 @@ ${entry.fullContent}`
38206
38327
  const desc = describeToolResult(event.toolName ?? "unknown", event.success ?? false, vLevel, event.content ?? void 0, emoCtx2);
38207
38328
  if (desc) {
38208
38329
  renderVoiceText(desc);
38209
- voice.speak(desc);
38330
+ voice.speakSubordinate(desc, emoCtx2);
38210
38331
  }
38211
38332
  }
38212
38333
  });
@@ -38308,12 +38429,16 @@ ${emotionContext}` : `Working directory: ${repoRoot}`;
38308
38429
  if (onComplete)
38309
38430
  onComplete(result.summary, { turns: result.turns, toolCalls: result.toolCalls, durationMs: result.durationMs, model: config.model });
38310
38431
  if (voice?.enabled && result.summary) {
38311
- voice.speak(describeTaskComplete(result.summary, true, vLevel));
38432
+ const emoFinal = emotionEngine?.getState();
38433
+ const emoCtxFinal = emoFinal ? { valence: emoFinal.valence, arousal: emoFinal.arousal, label: emoFinal.label, emoji: emoFinal.emoji } : void 0;
38434
+ voice.speak(describeTaskComplete(result.summary, true, vLevel), emoCtxFinal);
38312
38435
  }
38313
38436
  } else {
38314
38437
  renderTaskIncomplete(result.turns, result.toolCalls, result.durationMs, tokens);
38315
38438
  if (voice?.enabled) {
38316
- voice.speak(describeTaskComplete("", false, vLevel));
38439
+ const emoFinal2 = emotionEngine?.getState();
38440
+ const emoCtxFinal2 = emoFinal2 ? { valence: emoFinal2.valence, arousal: emoFinal2.arousal, label: emoFinal2.label, emoji: emoFinal2.emoji } : void 0;
38441
+ voice.speak(describeTaskComplete("", false, vLevel), emoCtxFinal2);
38317
38442
  }
38318
38443
  }
38319
38444
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.77.0",
3
+ "version": "0.79.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",
@@ -70,6 +70,6 @@
70
70
  },
71
71
  "optionalDependencies": {
72
72
  "moondream": "^0.2.0",
73
- "open-agents-nexus": "^1.2.1"
73
+ "open-agents-nexus": "^1.3.0"
74
74
  }
75
75
  }