@absolutejs/voice 0.0.22-beta.95 → 0.0.22-beta.97

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.
package/dist/index.js CHANGED
@@ -3596,7 +3596,7 @@ var createVoiceSession = (options) => {
3596
3596
  } : undefined;
3597
3597
  const appendTrace = async (input) => {
3598
3598
  await options.trace?.append({
3599
- at: Date.now(),
3599
+ at: input.at ?? Date.now(),
3600
3600
  metadata: input.metadata,
3601
3601
  payload: input.payload,
3602
3602
  scenarioId: input.session?.scenarioId ?? options.scenarioId,
@@ -3605,6 +3605,13 @@ var createVoiceSession = (options) => {
3605
3605
  type: input.type
3606
3606
  });
3607
3607
  };
3608
+ const appendTurnLatencyStage = async (input) => appendTrace({
3609
+ at: input.at,
3610
+ payload: { stage: input.stage },
3611
+ session: input.session,
3612
+ turnId: input.turnId,
3613
+ type: "turn_latency.stage"
3614
+ });
3608
3615
  const phraseHints = options.phraseHints ?? [];
3609
3616
  const lexicon = options.lexicon ?? [];
3610
3617
  let socket = options.socket;
@@ -4555,6 +4562,13 @@ var createVoiceSession = (options) => {
4555
4562
  turnId: activeTTSTurnId,
4556
4563
  type: "audio"
4557
4564
  });
4565
+ if (activeTTSTurnId) {
4566
+ await appendTurnLatencyStage({
4567
+ at: receivedAt,
4568
+ stage: "assistant_audio_received",
4569
+ turnId: activeTTSTurnId
4570
+ });
4571
+ }
4558
4572
  });
4559
4573
  });
4560
4574
  openedSession.on("error", (event) => {
@@ -4613,6 +4627,7 @@ var createVoiceSession = (options) => {
4613
4627
  voicemail: committedOutput?.voicemail
4614
4628
  };
4615
4629
  if (output?.assistantText) {
4630
+ const assistantTextStartedAt = Date.now();
4616
4631
  await writeSession((currentSession) => {
4617
4632
  setTurnResult(currentSession, turn.id, {
4618
4633
  assistantText: output.assistantText
@@ -4623,6 +4638,12 @@ var createVoiceSession = (options) => {
4623
4638
  turnId: turn.id,
4624
4639
  type: "assistant"
4625
4640
  });
4641
+ await appendTurnLatencyStage({
4642
+ at: assistantTextStartedAt,
4643
+ session,
4644
+ stage: "assistant_text_started",
4645
+ turnId: turn.id
4646
+ });
4626
4647
  await appendTrace({
4627
4648
  payload: {
4628
4649
  text: output.assistantText,
@@ -4637,7 +4658,18 @@ var createVoiceSession = (options) => {
4637
4658
  if (activeTTSSession) {
4638
4659
  const ttsStartedAt = Date.now();
4639
4660
  activeTTSTurnId = turn.id;
4661
+ await appendTurnLatencyStage({
4662
+ at: ttsStartedAt,
4663
+ session,
4664
+ stage: "tts_send_started",
4665
+ turnId: turn.id
4666
+ });
4640
4667
  await activeTTSSession.send(output.assistantText);
4668
+ await appendTurnLatencyStage({
4669
+ session,
4670
+ stage: "tts_send_completed",
4671
+ turnId: turn.id
4672
+ });
4641
4673
  await appendTrace({
4642
4674
  payload: {
4643
4675
  elapsedMs: Date.now() - ttsStartedAt,
@@ -4834,6 +4866,30 @@ var createVoiceSession = (options) => {
4834
4866
  turnId: turn.id,
4835
4867
  type: "turn.cost"
4836
4868
  });
4869
+ const firstTranscriptAt = turn.transcripts.map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs).filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
4870
+ const finalTranscriptAt = turn.transcripts.filter((transcript) => transcript.isFinal).map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs).filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
4871
+ if (firstTranscriptAt !== undefined) {
4872
+ await appendTurnLatencyStage({
4873
+ at: firstTranscriptAt,
4874
+ session: updatedSession,
4875
+ stage: "speech_detected",
4876
+ turnId: turn.id
4877
+ });
4878
+ }
4879
+ if (finalTranscriptAt !== undefined) {
4880
+ await appendTurnLatencyStage({
4881
+ at: finalTranscriptAt,
4882
+ session: updatedSession,
4883
+ stage: "final_transcript",
4884
+ turnId: turn.id
4885
+ });
4886
+ }
4887
+ await appendTurnLatencyStage({
4888
+ at: turn.committedAt,
4889
+ session: updatedSession,
4890
+ stage: "turn_committed",
4891
+ turnId: turn.id
4892
+ });
4837
4893
  await send({
4838
4894
  turn,
4839
4895
  type: "turn"
@@ -9965,6 +10021,8 @@ var timelineLabel = (event) => {
9965
10021
  return `Error${getString9(event.payload.error) ? `: ${getString9(event.payload.error)}` : ""}`;
9966
10022
  case "turn.cost":
9967
10023
  return "Cost telemetry";
10024
+ case "turn_latency.stage":
10025
+ return `Latency ${getString9(event.payload.stage) ?? "stage"}`;
9968
10026
  case "workflow.contract":
9969
10027
  return `Workflow contract ${eventStatus(event) ?? ""}`.trim();
9970
10028
  default:
@@ -11174,17 +11232,44 @@ var DEFAULT_WARN_AFTER_MS = 1800;
11174
11232
  var DEFAULT_FAIL_AFTER_MS = 3200;
11175
11233
  var escapeHtml19 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11176
11234
  var firstNumber2 = (values) => values.filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
11235
+ var getString10 = (value) => typeof value === "string" && value.trim() ? value : undefined;
11236
+ var createTraceStageIndex = (events) => {
11237
+ const index = new Map;
11238
+ for (const event of events) {
11239
+ if (event.type !== "turn_latency.stage" || !event.turnId) {
11240
+ continue;
11241
+ }
11242
+ const stage = getString10(event.payload.stage);
11243
+ if (!stage) {
11244
+ continue;
11245
+ }
11246
+ const key = `${event.sessionId}:${event.turnId}`;
11247
+ const stages = index.get(key) ?? new Map;
11248
+ const previous = stages.get(stage);
11249
+ if (previous === undefined || event.at < previous) {
11250
+ stages.set(stage, event.at);
11251
+ }
11252
+ index.set(key, stages);
11253
+ }
11254
+ return index;
11255
+ };
11177
11256
  var summarizeTurn = (sessionId, turn, options) => {
11178
- const firstTranscriptAt = firstNumber2(turn.transcripts.map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs));
11179
- const finalTranscriptAt = firstNumber2(turn.transcripts.filter((transcript) => transcript.isFinal).map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs));
11180
- const commitAfterFirstMs = firstTranscriptAt === undefined ? undefined : Math.max(0, turn.committedAt - firstTranscriptAt);
11181
- const commitAfterFinalMs = finalTranscriptAt === undefined ? undefined : Math.max(0, turn.committedAt - finalTranscriptAt);
11182
- const assistantTextStartedAt = turn.assistantText ? turn.committedAt : undefined;
11183
- const totalMs = commitAfterFirstMs;
11257
+ const traceStages = options.stageIndex?.get(`${sessionId}:${turn.id}`);
11258
+ const firstTranscriptAt = traceStages?.get("speech_detected") ?? firstNumber2(turn.transcripts.map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs));
11259
+ const finalTranscriptAt = traceStages?.get("final_transcript") ?? firstNumber2(turn.transcripts.filter((transcript) => transcript.isFinal).map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs));
11260
+ const committedAt = traceStages?.get("turn_committed") ?? turn.committedAt;
11261
+ const assistantTextStartedAt = traceStages?.get("assistant_text_started") ?? (turn.assistantText ? committedAt : undefined);
11262
+ const ttsSendStartedAt = traceStages?.get("tts_send_started");
11263
+ const ttsSendCompletedAt = traceStages?.get("tts_send_completed");
11264
+ const assistantAudioReceivedAt = traceStages?.get("assistant_audio_received");
11265
+ const commitAfterFirstMs = firstTranscriptAt === undefined ? undefined : Math.max(0, committedAt - firstTranscriptAt);
11266
+ const commitAfterFinalMs = finalTranscriptAt === undefined ? undefined : Math.max(0, committedAt - finalTranscriptAt);
11267
+ const totalEndAt = assistantAudioReceivedAt ?? assistantTextStartedAt ?? committedAt;
11268
+ const totalMs = firstTranscriptAt === undefined ? commitAfterFirstMs : Math.max(0, totalEndAt - firstTranscriptAt);
11184
11269
  const status = totalMs === undefined ? "warn" : totalMs > options.failAfterMs ? "fail" : totalMs > options.warnAfterMs ? "warn" : "pass";
11185
11270
  return {
11186
11271
  assistantTextStartedAt,
11187
- committedAt: turn.committedAt,
11272
+ committedAt,
11188
11273
  finalTranscriptAt,
11189
11274
  firstTranscriptAt,
11190
11275
  sessionId,
@@ -11193,7 +11278,19 @@ var summarizeTurn = (sessionId, turn, options) => {
11193
11278
  { label: "Final transcript to commit", valueMs: commitAfterFinalMs },
11194
11279
  {
11195
11280
  label: "Commit to assistant text",
11196
- valueMs: assistantTextStartedAt === undefined ? undefined : 0
11281
+ valueMs: assistantTextStartedAt === undefined ? undefined : Math.max(0, assistantTextStartedAt - committedAt)
11282
+ },
11283
+ {
11284
+ label: "Assistant text to TTS send",
11285
+ valueMs: ttsSendStartedAt === undefined || assistantTextStartedAt === undefined ? undefined : Math.max(0, ttsSendStartedAt - assistantTextStartedAt)
11286
+ },
11287
+ {
11288
+ label: "TTS send duration",
11289
+ valueMs: ttsSendCompletedAt === undefined || ttsSendStartedAt === undefined ? undefined : Math.max(0, ttsSendCompletedAt - ttsSendStartedAt)
11290
+ },
11291
+ {
11292
+ label: "TTS to first audio",
11293
+ valueMs: assistantAudioReceivedAt === undefined || ttsSendCompletedAt === undefined ? undefined : Math.max(0, assistantAudioReceivedAt - ttsSendCompletedAt)
11197
11294
  }
11198
11295
  ],
11199
11296
  status,
@@ -11222,9 +11319,14 @@ var resolveSessions = async (options) => {
11222
11319
  };
11223
11320
  var summarizeVoiceTurnLatency = async (options) => {
11224
11321
  const sessions = await resolveSessions(options);
11322
+ const traceEvents = options.traceStore ? await options.traceStore.list({
11323
+ limit: 1000,
11324
+ type: "turn_latency.stage"
11325
+ }) : [];
11326
+ const stageIndex = createTraceStageIndex(traceEvents);
11225
11327
  const warnAfterMs = options.warnAfterMs ?? DEFAULT_WARN_AFTER_MS;
11226
11328
  const failAfterMs = options.failAfterMs ?? DEFAULT_FAIL_AFTER_MS;
11227
- const turns = sessions.flatMap((session) => session.turns.map((turn) => summarizeTurn(session.id, turn, { failAfterMs, warnAfterMs }))).sort((left, right) => right.committedAt - left.committedAt);
11329
+ const turns = sessions.flatMap((session) => session.turns.map((turn) => summarizeTurn(session.id, turn, { failAfterMs, stageIndex, warnAfterMs }))).sort((left, right) => right.committedAt - left.committedAt);
11228
11330
  const totals = turns.map((turn) => turn.totalMs).filter((value) => typeof value === "number");
11229
11331
  const failed = turns.filter((turn) => turn.status === "fail").length;
11230
11332
  const warnings = turns.filter((turn) => turn.status === "warn").length;
@@ -5216,7 +5216,7 @@ var createVoiceSession = (options) => {
5216
5216
  } : undefined;
5217
5217
  const appendTrace = async (input) => {
5218
5218
  await options.trace?.append({
5219
- at: Date.now(),
5219
+ at: input.at ?? Date.now(),
5220
5220
  metadata: input.metadata,
5221
5221
  payload: input.payload,
5222
5222
  scenarioId: input.session?.scenarioId ?? options.scenarioId,
@@ -5225,6 +5225,13 @@ var createVoiceSession = (options) => {
5225
5225
  type: input.type
5226
5226
  });
5227
5227
  };
5228
+ const appendTurnLatencyStage = async (input) => appendTrace({
5229
+ at: input.at,
5230
+ payload: { stage: input.stage },
5231
+ session: input.session,
5232
+ turnId: input.turnId,
5233
+ type: "turn_latency.stage"
5234
+ });
5228
5235
  const phraseHints = options.phraseHints ?? [];
5229
5236
  const lexicon = options.lexicon ?? [];
5230
5237
  let socket = options.socket;
@@ -6175,6 +6182,13 @@ var createVoiceSession = (options) => {
6175
6182
  turnId: activeTTSTurnId,
6176
6183
  type: "audio"
6177
6184
  });
6185
+ if (activeTTSTurnId) {
6186
+ await appendTurnLatencyStage({
6187
+ at: receivedAt,
6188
+ stage: "assistant_audio_received",
6189
+ turnId: activeTTSTurnId
6190
+ });
6191
+ }
6178
6192
  });
6179
6193
  });
6180
6194
  openedSession.on("error", (event) => {
@@ -6233,6 +6247,7 @@ var createVoiceSession = (options) => {
6233
6247
  voicemail: committedOutput?.voicemail
6234
6248
  };
6235
6249
  if (output?.assistantText) {
6250
+ const assistantTextStartedAt = Date.now();
6236
6251
  await writeSession((currentSession) => {
6237
6252
  setTurnResult(currentSession, turn.id, {
6238
6253
  assistantText: output.assistantText
@@ -6243,6 +6258,12 @@ var createVoiceSession = (options) => {
6243
6258
  turnId: turn.id,
6244
6259
  type: "assistant"
6245
6260
  });
6261
+ await appendTurnLatencyStage({
6262
+ at: assistantTextStartedAt,
6263
+ session,
6264
+ stage: "assistant_text_started",
6265
+ turnId: turn.id
6266
+ });
6246
6267
  await appendTrace({
6247
6268
  payload: {
6248
6269
  text: output.assistantText,
@@ -6257,7 +6278,18 @@ var createVoiceSession = (options) => {
6257
6278
  if (activeTTSSession) {
6258
6279
  const ttsStartedAt = Date.now();
6259
6280
  activeTTSTurnId = turn.id;
6281
+ await appendTurnLatencyStage({
6282
+ at: ttsStartedAt,
6283
+ session,
6284
+ stage: "tts_send_started",
6285
+ turnId: turn.id
6286
+ });
6260
6287
  await activeTTSSession.send(output.assistantText);
6288
+ await appendTurnLatencyStage({
6289
+ session,
6290
+ stage: "tts_send_completed",
6291
+ turnId: turn.id
6292
+ });
6261
6293
  await appendTrace({
6262
6294
  payload: {
6263
6295
  elapsedMs: Date.now() - ttsStartedAt,
@@ -6454,6 +6486,30 @@ var createVoiceSession = (options) => {
6454
6486
  turnId: turn.id,
6455
6487
  type: "turn.cost"
6456
6488
  });
6489
+ const firstTranscriptAt = turn.transcripts.map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs).filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
6490
+ const finalTranscriptAt = turn.transcripts.filter((transcript) => transcript.isFinal).map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs).filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
6491
+ if (firstTranscriptAt !== undefined) {
6492
+ await appendTurnLatencyStage({
6493
+ at: firstTranscriptAt,
6494
+ session: updatedSession,
6495
+ stage: "speech_detected",
6496
+ turnId: turn.id
6497
+ });
6498
+ }
6499
+ if (finalTranscriptAt !== undefined) {
6500
+ await appendTurnLatencyStage({
6501
+ at: finalTranscriptAt,
6502
+ session: updatedSession,
6503
+ stage: "final_transcript",
6504
+ turnId: turn.id
6505
+ });
6506
+ }
6507
+ await appendTurnLatencyStage({
6508
+ at: turn.committedAt,
6509
+ session: updatedSession,
6510
+ stage: "turn_committed",
6511
+ turnId: turn.id
6512
+ });
6457
6513
  await send({
6458
6514
  turn,
6459
6515
  type: "turn"
package/dist/trace.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type VoiceTraceEventType = 'assistant.guardrail' | 'assistant.memory' | 'assistant.run' | 'agent.handoff' | 'agent.model' | 'agent.result' | 'agent.tool' | 'call.handoff' | 'call.lifecycle' | 'client.barge_in' | 'session.error' | 'turn.assistant' | 'turn.committed' | 'turn.cost' | 'turn.transcript' | 'workflow.contract';
1
+ export type VoiceTraceEventType = 'assistant.guardrail' | 'assistant.memory' | 'assistant.run' | 'agent.handoff' | 'agent.model' | 'agent.result' | 'agent.tool' | 'call.handoff' | 'call.lifecycle' | 'client.barge_in' | 'session.error' | 'turn.assistant' | 'turn.committed' | 'turn.cost' | 'turn_latency.stage' | 'turn.transcript' | 'workflow.contract';
2
2
  export type VoiceTraceEvent<TPayload extends Record<string, unknown> = Record<string, unknown>> = {
3
3
  at: number;
4
4
  id?: string;
@@ -1,4 +1,5 @@
1
1
  import { Elysia } from 'elysia';
2
+ import type { VoiceTraceEventStore } from './trace';
2
3
  import type { VoiceSessionRecord, VoiceSessionStore } from './types';
3
4
  export type VoiceTurnLatencyStatus = 'pass' | 'warn' | 'fail' | 'empty';
4
5
  export type VoiceTurnLatencyStage = {
@@ -32,6 +33,7 @@ export type VoiceTurnLatencyOptions<TSession extends VoiceSessionRecord = VoiceS
32
33
  sessionIds?: string[];
33
34
  sessions?: TSession[];
34
35
  store?: VoiceSessionStore<TSession>;
36
+ traceStore?: VoiceTraceEventStore;
35
37
  warnAfterMs?: number;
36
38
  failAfterMs?: number;
37
39
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.95",
3
+ "version": "0.0.22-beta.97",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",