@teamlearners/clawops 0.16.0 → 0.16.3

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.
@@ -320,7 +320,7 @@ interface Session {
320
320
  /** Start the realtime session with the call session and tools. */
321
321
  start(callSession: CallSession, tools?: ToolRegistry): Promise<void>;
322
322
  /** Feed raw audio into the session. */
323
- feedAudio(audio: Buffer): void;
323
+ feedAudio(audio: Buffer, timestamp?: number): void;
324
324
  /** Feed DTMF digits into the LLM context and trigger a response. */
325
325
  feedDtmf?(digits: string): Promise<void>;
326
326
  /** Stop the session. */
@@ -430,6 +430,16 @@ interface ClawOpsAgentOptions {
430
430
  logger?: Logger;
431
431
  /** Tool 실행 관련 설정. */
432
432
  toolConfig?: ToolConfig;
433
+ /**
434
+ * Gain applied to inbound audio (caller → AI). 1.0 = pass-through (default), 0 = mute, 2.0 = 2x amplify.
435
+ * AI/STT receive the gained audio, and recording captures it post-gain.
436
+ */
437
+ rxGain?: number;
438
+ /**
439
+ * Gain applied to outbound audio (AI → caller). 1.0 = pass-through (default), 0 = mute, 2.0 = 2x amplify.
440
+ * The caller hears the gained audio, and recording captures it post-gain.
441
+ */
442
+ txGain?: number;
433
443
  }
434
444
  declare class ClawOpsAgent {
435
445
  private _apiKey;
@@ -454,7 +464,10 @@ declare class ClawOpsAgent {
454
464
  private _pipelineLog;
455
465
  private _isPipelineSession;
456
466
  private _holdAudioChunks;
467
+ private _rxGain;
468
+ private _txGain;
457
469
  constructor(options: ClawOpsAgentOptions);
470
+ private static _validateGain;
458
471
  /**
459
472
  * Register a function tool.
460
473
  *
@@ -584,17 +597,18 @@ declare class AudioRecorder {
584
597
  private _inWritten;
585
598
  private _outWritten;
586
599
  private _mixWritten;
587
- private _startTime;
588
600
  private _started;
601
+ private _baseTs;
602
+ private _outCursor;
589
603
  private _log;
590
604
  setLogger(logger: Logger): void;
591
605
  constructor(recordingPath: string, callId: string);
592
606
  start(): void;
593
- private _expectedBytes;
607
+ private _timestampToBytes;
594
608
  private _padSilence;
595
609
  private _writeToMix;
596
- writeInbound(pcm16_8k: Buffer): void;
597
- writeOutbound(pcm16_8k: Buffer): void;
610
+ writeInbound(pcm16_8k: Buffer, mediaTsMs?: number): void;
611
+ writeOutbound(pcm16_8k: Buffer, mediaTsMs?: number): void;
598
612
  stop(): void;
599
613
  }
600
614
 
@@ -679,7 +693,7 @@ interface OpenAIRealtimeOptions {
679
693
  apiKey?: string;
680
694
  /** System prompt / instructions for the AI. */
681
695
  systemPrompt?: string;
682
- /** Model to use. Default: 'gpt-realtime-1.5' */
696
+ /** Model to use. Default: 'gpt-realtime-2' */
683
697
  model?: string;
684
698
  /** Voice ID. Default: 'marin' */
685
699
  voice?: string;
@@ -728,7 +742,7 @@ declare class OpenAIRealtime implements Session {
728
742
  setHoldAudio(chunks: Buffer[]): void;
729
743
  start(callSession: CallSession, tools?: ToolRegistry): Promise<void>;
730
744
  feedDtmf(digits: string): Promise<void>;
731
- feedAudio(audio: Buffer): void;
745
+ feedAudio(audio: Buffer, timestamp?: number): void;
732
746
  stop(): Promise<void>;
733
747
  private _sendSessionUpdate;
734
748
  private _handleMessage;
@@ -320,7 +320,7 @@ interface Session {
320
320
  /** Start the realtime session with the call session and tools. */
321
321
  start(callSession: CallSession, tools?: ToolRegistry): Promise<void>;
322
322
  /** Feed raw audio into the session. */
323
- feedAudio(audio: Buffer): void;
323
+ feedAudio(audio: Buffer, timestamp?: number): void;
324
324
  /** Feed DTMF digits into the LLM context and trigger a response. */
325
325
  feedDtmf?(digits: string): Promise<void>;
326
326
  /** Stop the session. */
@@ -430,6 +430,16 @@ interface ClawOpsAgentOptions {
430
430
  logger?: Logger;
431
431
  /** Tool 실행 관련 설정. */
432
432
  toolConfig?: ToolConfig;
433
+ /**
434
+ * Gain applied to inbound audio (caller → AI). 1.0 = pass-through (default), 0 = mute, 2.0 = 2x amplify.
435
+ * AI/STT receive the gained audio, and recording captures it post-gain.
436
+ */
437
+ rxGain?: number;
438
+ /**
439
+ * Gain applied to outbound audio (AI → caller). 1.0 = pass-through (default), 0 = mute, 2.0 = 2x amplify.
440
+ * The caller hears the gained audio, and recording captures it post-gain.
441
+ */
442
+ txGain?: number;
433
443
  }
434
444
  declare class ClawOpsAgent {
435
445
  private _apiKey;
@@ -454,7 +464,10 @@ declare class ClawOpsAgent {
454
464
  private _pipelineLog;
455
465
  private _isPipelineSession;
456
466
  private _holdAudioChunks;
467
+ private _rxGain;
468
+ private _txGain;
457
469
  constructor(options: ClawOpsAgentOptions);
470
+ private static _validateGain;
458
471
  /**
459
472
  * Register a function tool.
460
473
  *
@@ -584,17 +597,18 @@ declare class AudioRecorder {
584
597
  private _inWritten;
585
598
  private _outWritten;
586
599
  private _mixWritten;
587
- private _startTime;
588
600
  private _started;
601
+ private _baseTs;
602
+ private _outCursor;
589
603
  private _log;
590
604
  setLogger(logger: Logger): void;
591
605
  constructor(recordingPath: string, callId: string);
592
606
  start(): void;
593
- private _expectedBytes;
607
+ private _timestampToBytes;
594
608
  private _padSilence;
595
609
  private _writeToMix;
596
- writeInbound(pcm16_8k: Buffer): void;
597
- writeOutbound(pcm16_8k: Buffer): void;
610
+ writeInbound(pcm16_8k: Buffer, mediaTsMs?: number): void;
611
+ writeOutbound(pcm16_8k: Buffer, mediaTsMs?: number): void;
598
612
  stop(): void;
599
613
  }
600
614
 
@@ -679,7 +693,7 @@ interface OpenAIRealtimeOptions {
679
693
  apiKey?: string;
680
694
  /** System prompt / instructions for the AI. */
681
695
  systemPrompt?: string;
682
- /** Model to use. Default: 'gpt-realtime-1.5' */
696
+ /** Model to use. Default: 'gpt-realtime-2' */
683
697
  model?: string;
684
698
  /** Voice ID. Default: 'marin' */
685
699
  voice?: string;
@@ -728,7 +742,7 @@ declare class OpenAIRealtime implements Session {
728
742
  setHoldAudio(chunks: Buffer[]): void;
729
743
  start(callSession: CallSession, tools?: ToolRegistry): Promise<void>;
730
744
  feedDtmf(digits: string): Promise<void>;
731
- feedAudio(audio: Buffer): void;
745
+ feedAudio(audio: Buffer, timestamp?: number): void;
732
746
  stop(): Promise<void>;
733
747
  private _sendSessionUpdate;
734
748
  private _handleMessage;
@@ -1,4 +1,4 @@
1
- import { DEFAULT_BASE_URL, AgentError, AgentConnectionError, VERSION } from '../chunk-N5EKKQVO.js';
1
+ import { DEFAULT_BASE_URL, AgentError, AgentConnectionError, VERSION } from '../chunk-5IYJOP2D.js';
2
2
  import pino from 'pino';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
@@ -304,6 +304,27 @@ function ulawToPcm16(ulaw) {
304
304
  }
305
305
  return out;
306
306
  }
307
+ function applyPcm16Gain(pcm, gain) {
308
+ if (!pcm.length) return Buffer.alloc(0);
309
+ if (gain === 1) return pcm;
310
+ if (!Number.isFinite(gain) || gain < 0) {
311
+ throw new Error("gain must be a finite number >= 0");
312
+ }
313
+ const nSamples = pcm.length >> 1;
314
+ const out = Buffer.alloc(nSamples * 2);
315
+ for (let i = 0; i < nSamples; i++) {
316
+ let value = Math.round(pcm.readInt16LE(i * 2) * gain);
317
+ if (value > 32767) value = 32767;
318
+ else if (value < -32768) value = -32768;
319
+ out.writeInt16LE(value, i * 2);
320
+ }
321
+ return out;
322
+ }
323
+ function applyUlawGain(ulaw, gain) {
324
+ if (!ulaw.length) return Buffer.alloc(0);
325
+ if (gain === 1) return ulaw;
326
+ return pcm16ToUlaw(applyPcm16Gain(ulawToPcm16(ulaw), gain));
327
+ }
307
328
  function resamplePcm16(pcm, fromRate, toRate) {
308
329
  if (fromRate === toRate || !pcm.length) return pcm;
309
330
  const nSamples = pcm.length >> 1;
@@ -892,8 +913,9 @@ var AudioRecorder = class {
892
913
  _inWritten = 0;
893
914
  _outWritten = 0;
894
915
  _mixWritten = 0;
895
- _startTime = 0;
896
916
  _started = false;
917
+ _baseTs = null;
918
+ _outCursor = 0;
897
919
  _log = NOOP_LOGGER;
898
920
  setLogger(logger) {
899
921
  this._log = logger;
@@ -910,17 +932,18 @@ var AudioRecorder = class {
910
932
  fs.writeSync(this._fdIn, header);
911
933
  fs.writeSync(this._fdOut, header);
912
934
  fs.writeSync(this._fdMix, header);
913
- this._startTime = performance.now();
914
935
  this._started = true;
915
936
  this._log.info("Recording started: %s", this._dir);
916
937
  }
917
- _expectedBytes() {
918
- const elapsed = (performance.now() - this._startTime) / 1e3;
919
- return Math.floor(elapsed * BYTES_PER_SECOND);
938
+ _timestampToBytes(mediaTsMs) {
939
+ if (this._baseTs === null) {
940
+ this._baseTs = mediaTsMs;
941
+ }
942
+ const target = Math.floor((mediaTsMs - this._baseTs) * BYTES_PER_SECOND / 1e3);
943
+ return Math.max(0, target - target % 2);
920
944
  }
921
- _padSilence(fd, written) {
922
- const expected = this._expectedBytes();
923
- let gap = expected - written;
945
+ _padSilence(fd, written, target) {
946
+ let gap = target - written;
924
947
  if (gap <= 0) return 0;
925
948
  gap = gap - gap % 2;
926
949
  if (gap > 0) {
@@ -930,9 +953,12 @@ var AudioRecorder = class {
930
953
  }
931
954
  _writeToMix(data, trackPos) {
932
955
  if (this._fdMix === null) return;
956
+ data = data.subarray(0, data.length - data.length % 2);
957
+ if (data.length === 0) return;
933
958
  const filePos = 44 + trackPos;
934
959
  if (trackPos < this._mixWritten) {
935
- const overlap = Math.min(data.length, this._mixWritten - trackPos);
960
+ let overlap = Math.min(data.length, this._mixWritten - trackPos);
961
+ overlap = overlap - overlap % 2;
936
962
  const existing = Buffer.alloc(overlap);
937
963
  fs.readSync(this._fdMix, existing, 0, overlap, filePos);
938
964
  const mixed = mixSamples(existing, data.subarray(0, overlap));
@@ -955,10 +981,13 @@ var AudioRecorder = class {
955
981
  this._mixWritten += data.length;
956
982
  }
957
983
  }
958
- writeInbound(pcm16_8k) {
984
+ writeInbound(pcm16_8k, mediaTsMs = 0) {
959
985
  if (!this._started || this._fdIn === null) return;
960
986
  try {
961
- const gap = this._padSilence(this._fdIn, this._inWritten);
987
+ pcm16_8k = pcm16_8k.subarray(0, pcm16_8k.length - pcm16_8k.length % 2);
988
+ if (pcm16_8k.length === 0) return;
989
+ const target = this._timestampToBytes(mediaTsMs);
990
+ const gap = this._padSilence(this._fdIn, this._inWritten, target);
962
991
  this._inWritten += gap;
963
992
  const posBefore = this._inWritten;
964
993
  fs.writeSync(this._fdIn, pcm16_8k);
@@ -968,14 +997,21 @@ var AudioRecorder = class {
968
997
  this._log.error({ err }, "Recording write error (inbound)");
969
998
  }
970
999
  }
971
- writeOutbound(pcm16_8k) {
1000
+ writeOutbound(pcm16_8k, mediaTsMs) {
972
1001
  if (!this._started || this._fdOut === null) return;
973
1002
  try {
974
- const gap = this._padSilence(this._fdOut, this._outWritten);
1003
+ pcm16_8k = pcm16_8k.subarray(0, pcm16_8k.length - pcm16_8k.length % 2);
1004
+ if (pcm16_8k.length === 0) return;
1005
+ let target = this._outCursor;
1006
+ if (mediaTsMs !== void 0) {
1007
+ target = Math.max(target, this._timestampToBytes(mediaTsMs));
1008
+ }
1009
+ const gap = this._padSilence(this._fdOut, this._outWritten, target);
975
1010
  this._outWritten += gap;
976
1011
  const posBefore = this._outWritten;
977
1012
  fs.writeSync(this._fdOut, pcm16_8k);
978
1013
  this._outWritten += pcm16_8k.length;
1014
+ this._outCursor = this._outWritten;
979
1015
  this._writeToMix(pcm16_8k, posBefore);
980
1016
  } catch (err) {
981
1017
  this._log.error({ err }, "Recording write error (outbound)");
@@ -1671,7 +1707,7 @@ var ATTR_CALL_DIRECTION = "clawops.call.direction";
1671
1707
  var ATTR_AGENT_ID = "clawops.agent.id";
1672
1708
 
1673
1709
  // src/agent/agent.ts
1674
- var ClawOpsAgent = class {
1710
+ var ClawOpsAgent = class _ClawOpsAgent {
1675
1711
  _apiKey;
1676
1712
  _accountId;
1677
1713
  _baseUrl;
@@ -1694,6 +1730,8 @@ var ClawOpsAgent = class {
1694
1730
  _pipelineLog;
1695
1731
  _isPipelineSession = false;
1696
1732
  _holdAudioChunks = null;
1733
+ _rxGain;
1734
+ _txGain;
1697
1735
  constructor(options) {
1698
1736
  this._apiKey = options.apiKey ?? process.env["CLAWOPS_API_KEY"] ?? "";
1699
1737
  this._accountId = options.accountId ?? process.env["CLAWOPS_ACCOUNT_ID"] ?? "";
@@ -1705,6 +1743,8 @@ var ClawOpsAgent = class {
1705
1743
  this._mcpServers = options.mcpServers ?? [];
1706
1744
  this._builtinTools = resolveBuiltinTools(options.builtinTools ?? "all" /* ALL */);
1707
1745
  this._passiveDtmfDebounceMs = options.passiveDtmfDebounceMs ?? 500;
1746
+ this._rxGain = _ClawOpsAgent._validateGain("rxGain", options.rxGain ?? 1);
1747
+ this._txGain = _ClawOpsAgent._validateGain("txGain", options.txGain ?? 1);
1708
1748
  if (options.tracing) {
1709
1749
  setTracingConfig(options.tracing);
1710
1750
  }
@@ -1715,6 +1755,12 @@ var ClawOpsAgent = class {
1715
1755
  this._holdAudioChunks = loadHoldAudio(options.toolConfig.holdAudio);
1716
1756
  }
1717
1757
  }
1758
+ static _validateGain(name, gain) {
1759
+ if (typeof gain !== "number" || !Number.isFinite(gain) || gain < 0) {
1760
+ throw new AgentError(`${name}=${gain} must be a finite number >= 0`);
1761
+ }
1762
+ return gain;
1763
+ }
1718
1764
  /**
1719
1765
  * Register a function tool.
1720
1766
  *
@@ -1997,9 +2043,14 @@ var ClawOpsAgent = class {
1997
2043
  }
1998
2044
  const mediaWs = new MediaWebSocket();
1999
2045
  mediaWs.setLogger(this._log);
2046
+ let latestMediaTs = 0;
2000
2047
  session._bindTransport(
2001
2048
  (audio) => {
2002
- mediaWs.sendAudio(audio.toString("base64"));
2049
+ const gained = applyUlawGain(audio, this._txGain);
2050
+ if (recorder) {
2051
+ recorder.writeOutbound(ulawToPcm16(gained), latestMediaTs);
2052
+ }
2053
+ mediaWs.sendAudio(gained.toString("base64"));
2003
2054
  session.recordFirstResponse();
2004
2055
  },
2005
2056
  () => {
@@ -2036,12 +2087,14 @@ var ClawOpsAgent = class {
2036
2087
  sessionHandler.setHoldAudio(this._holdAudioChunks);
2037
2088
  }
2038
2089
  this._callSessions.set(session.callId, sessionHandler);
2039
- mediaWs.onAudio((ulawAudio, _timestamp) => {
2040
- if (sessionHandler) {
2041
- sessionHandler.feedAudio(ulawAudio);
2042
- }
2090
+ mediaWs.onAudio((ulawAudio, timestamp) => {
2091
+ latestMediaTs = timestamp;
2092
+ const gained = applyUlawGain(ulawAudio, this._rxGain);
2043
2093
  if (recorder) {
2044
- recorder.writeInbound(ulawToPcm16(ulawAudio));
2094
+ recorder.writeInbound(ulawToPcm16(gained), timestamp);
2095
+ }
2096
+ if (sessionHandler) {
2097
+ sessionHandler.feedAudio(gained, timestamp);
2045
2098
  }
2046
2099
  });
2047
2100
  mediaWs.onDtmf((digit) => {
@@ -2512,10 +2565,6 @@ var PipelineSession = class {
2512
2565
  sampleRate: this._sampleRate
2513
2566
  })) {
2514
2567
  if (!this._running || !this._speaking) break;
2515
- if (this._recorder) {
2516
- const pcm8k2 = this._sampleRate !== 8e3 ? resamplePcm16(audioChunk, this._sampleRate, 8e3) : audioChunk;
2517
- this._recorder.writeOutbound(pcm8k2);
2518
- }
2519
2568
  const pcm8k = resamplePcm16(audioChunk, this._sampleRate, 8e3);
2520
2569
  const ulaw = pcm16ToUlaw(pcm8k);
2521
2570
  for (let off = 0; off < ulaw.length; off += 160) {
@@ -2585,7 +2634,7 @@ var OpenAIRealtime = class {
2585
2634
  constructor(options = {}) {
2586
2635
  this._apiKey = options.apiKey ?? process.env["OPENAI_API_KEY"] ?? "";
2587
2636
  this._systemPrompt = options.systemPrompt ?? "";
2588
- this._model = options.model ?? "gpt-realtime-1.5";
2637
+ this._model = options.model ?? "gpt-realtime-2";
2589
2638
  this._voice = options.voice ?? "marin";
2590
2639
  this._language = options.language ?? "ko";
2591
2640
  this._turnDetection = options.turnDetection !== void 0 ? options.turnDetection : { type: "semantic_vad", eagerness: "medium", interrupt_response: true };
@@ -2660,8 +2709,8 @@ var OpenAIRealtime = class {
2660
2709
  });
2661
2710
  this._send({ type: "response.create" });
2662
2711
  }
2663
- feedAudio(audio) {
2664
- this._latestMediaTs = Date.now();
2712
+ feedAudio(audio, timestamp) {
2713
+ this._latestMediaTs = timestamp ?? this._latestMediaTs;
2665
2714
  if (this._ws && this._ws.readyState === 1 && !this._closed) {
2666
2715
  this._send({
2667
2716
  type: "input_audio_buffer.append",
@@ -2772,7 +2821,7 @@ var OpenAIRealtime = class {
2772
2821
  if (this._playback === null) {
2773
2822
  this._playback = {
2774
2823
  itemId: msg["item_id"] || "",
2775
- startTs: this._latestMediaTs || Date.now(),
2824
+ startTs: this._latestMediaTs,
2776
2825
  sentChunks: 0,
2777
2826
  generating: true,
2778
2827
  audioRemainder: Buffer.alloc(0)
@@ -2782,9 +2831,6 @@ var OpenAIRealtime = class {
2782
2831
  }
2783
2832
  const pb = this._playback;
2784
2833
  const ulaw = Buffer.from(msg["delta"], "base64");
2785
- if (this._recorder) {
2786
- this._recorder.writeOutbound(ulawToPcm16(ulaw));
2787
- }
2788
2834
  const combined = Buffer.concat([pb.audioRemainder, ulaw]);
2789
2835
  const chunkSize = 160;
2790
2836
  const fullEnd = Math.floor(combined.length / chunkSize) * chunkSize;
@@ -2808,7 +2854,7 @@ var OpenAIRealtime = class {
2808
2854
  if (pb === null) {
2809
2855
  return;
2810
2856
  }
2811
- const playedMs = Math.max(0, (this._latestMediaTs || Date.now()) - pb.startTs);
2857
+ const playedMs = Math.max(0, this._latestMediaTs - pb.startTs);
2812
2858
  this._log.info(
2813
2859
  "[Interrupt] item=%s played=%dms total=%dms",
2814
2860
  pb.itemId,
@@ -3100,9 +3146,6 @@ var GeminiRealtime = class _GeminiRealtime {
3100
3146
  feedAudio(audio) {
3101
3147
  if (this._session && !this._closed) {
3102
3148
  const pcm8k = ulawToPcm16(audio);
3103
- if (this._recorder) {
3104
- this._recorder.writeInbound(pcm8k);
3105
- }
3106
3149
  const pcm16k = resamplePcm16(pcm8k, 8e3, 16e3);
3107
3150
  this._session.sendRealtimeInput({
3108
3151
  audio: {
@@ -3200,9 +3243,6 @@ var GeminiRealtime = class _GeminiRealtime {
3200
3243
  _handleAudioData(b64Data) {
3201
3244
  if (!this._call) return;
3202
3245
  const pcm24k = Buffer.from(b64Data, "base64");
3203
- if (this._recorder) {
3204
- this._recorder.writeOutbound(resamplePcm16(pcm24k, 24e3, 8e3));
3205
- }
3206
3246
  const pcm8k = resamplePcm16(pcm24k, 24e3, 8e3);
3207
3247
  const ulaw = pcm16ToUlaw(pcm8k);
3208
3248
  const combined = Buffer.concat([this._audioRemainder, ulaw]);