Remove text-turn session path and keep voice flow audio-only
This commit is contained in:
@@ -759,10 +759,6 @@ export class DaemonClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendUserMessage(text: string): void {
|
|
||||||
this.sendSessionMessage({ type: "user_text", text });
|
|
||||||
}
|
|
||||||
|
|
||||||
clearAgentAttention(agentId: string | string[]): void {
|
clearAgentAttention(agentId: string | string[]): void {
|
||||||
this.sendSessionMessage({ type: "clear_agent_attention", agentId });
|
this.sendSessionMessage({ type: "clear_agent_attention", agentId });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -586,8 +586,8 @@ describe("daemon client E2E", () => {
|
|||||||
120000
|
120000
|
||||||
);
|
);
|
||||||
|
|
||||||
test(
|
speechTest(
|
||||||
"does not process non-voice turns through the voice agent path",
|
"does not process non-voice audio through the voice agent path",
|
||||||
async () => {
|
async () => {
|
||||||
await ctx.client.setVoiceMode(false);
|
await ctx.client.setVoiceMode(false);
|
||||||
|
|
||||||
@@ -624,7 +624,16 @@ describe("daemon client E2E", () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
await ctx.client.sendUserMessage("Say 'hello' and nothing else");
|
const fixturePath = path.resolve(
|
||||||
|
process.cwd(),
|
||||||
|
"..",
|
||||||
|
"app",
|
||||||
|
"e2e",
|
||||||
|
"fixtures",
|
||||||
|
"recording.wav"
|
||||||
|
);
|
||||||
|
const wav = await import("node:fs/promises").then((fs) => fs.readFile(fixturePath));
|
||||||
|
await ctx.client.sendVoiceAudioChunk(wav.toString("base64"), "audio/wav", true);
|
||||||
await transcriptSeen;
|
await transcriptSeen;
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||||
|
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ function toAgentPersistenceHandle(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Session represents a single client conversation session.
|
* Session represents a single connected client session.
|
||||||
* It owns all state management, orchestration logic, and message processing.
|
* It owns all state management, orchestration logic, and message processing.
|
||||||
* Session has no knowledge of WebSockets - it only emits and receives messages.
|
* Session has no knowledge of WebSockets - it only emits and receives messages.
|
||||||
*/
|
*/
|
||||||
@@ -835,10 +835,6 @@ export class Session {
|
|||||||
public async handleMessage(msg: SessionInboundMessage): Promise<void> {
|
public async handleMessage(msg: SessionInboundMessage): Promise<void> {
|
||||||
try {
|
try {
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case "user_text":
|
|
||||||
await this.handleUserText(msg.text);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "voice_audio_chunk":
|
case "voice_audio_chunk":
|
||||||
await this.handleAudioChunk(msg);
|
await this.handleAudioChunk(msg);
|
||||||
break;
|
break;
|
||||||
@@ -4150,36 +4146,6 @@ export class Session {
|
|||||||
return timeline.length;
|
return timeline.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle text message from user
|
|
||||||
*/
|
|
||||||
private async handleUserText(text: string): Promise<void> {
|
|
||||||
// Abort any in-progress stream immediately
|
|
||||||
this.createAbortController();
|
|
||||||
|
|
||||||
// Wait for aborted stream to finish cleanup (save partial response)
|
|
||||||
if (this.currentStreamPromise) {
|
|
||||||
this.sessionLogger.debug("Waiting for aborted stream to finish cleanup");
|
|
||||||
await this.currentStreamPromise;
|
|
||||||
this.sessionLogger.debug("Aborted stream finished cleanup");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit user message activity log
|
|
||||||
this.emit({
|
|
||||||
type: "activity_log",
|
|
||||||
payload: {
|
|
||||||
id: uuidv4(),
|
|
||||||
timestamp: new Date(),
|
|
||||||
type: "transcript",
|
|
||||||
content: text,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process through LLM (voice path is agent-backed only)
|
|
||||||
this.currentStreamPromise = this.processVoiceTurn(this.isVoiceMode, text);
|
|
||||||
await this.currentStreamPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle audio chunk for buffering and transcription
|
* Handle audio chunk for buffering and transcription
|
||||||
*/
|
*/
|
||||||
@@ -4445,7 +4411,7 @@ export class Session {
|
|||||||
// Set phase to LLM and process (TTS enabled in voice mode for voice agents)
|
// Set phase to LLM and process (TTS enabled in voice mode for voice agents)
|
||||||
this.clearSpeechInProgress("transcription complete");
|
this.clearSpeechInProgress("transcription complete");
|
||||||
this.setPhase("llm");
|
this.setPhase("llm");
|
||||||
this.currentStreamPromise = this.processVoiceTurn(this.isVoiceMode, result.text);
|
this.currentStreamPromise = this.processVoiceTurn(result.text);
|
||||||
await this.currentStreamPromise;
|
await this.currentStreamPromise;
|
||||||
this.setPhase("idle");
|
this.setPhase("idle");
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -4655,10 +4621,10 @@ export class Session {
|
|||||||
/**
|
/**
|
||||||
* Process user message through LLM with streaming and tool execution
|
* Process user message through LLM with streaming and tool execution
|
||||||
*/
|
*/
|
||||||
private async processVoiceTurn(enableTTS: boolean, latestUserText?: string): Promise<void> {
|
private async processVoiceTurn(latestUserText?: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (!enableTTS) {
|
if (!this.isVoiceMode) {
|
||||||
this.sessionLogger.warn("Ignoring non-voice processVoiceTurn call; voice is agent-only");
|
this.sessionLogger.warn("Ignoring processVoiceTurn call while voice mode is disabled");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const normalized = (latestUserText ?? "").trim();
|
const normalized = (latestUserText ?? "").trim();
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
|
import path from "node:path";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
|
||||||
import { createDaemonTestContext, type DaemonTestContext } from "./test-utils/index.js";
|
import { createDaemonTestContext, type DaemonTestContext } from "./test-utils/index.js";
|
||||||
|
|
||||||
@@ -104,9 +106,16 @@ function waitForSignal<T>(
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.client.sendUserMessage(
|
const fixturePath = path.resolve(
|
||||||
"Use the speak tool and say exactly: local voice agent path is working."
|
process.cwd(),
|
||||||
|
"..",
|
||||||
|
"app",
|
||||||
|
"e2e",
|
||||||
|
"fixtures",
|
||||||
|
"recording.wav"
|
||||||
);
|
);
|
||||||
|
const wav = await readFile(fixturePath);
|
||||||
|
await ctx.client.sendVoiceAudioChunk(wav.toString("base64"), "audio/wav", true);
|
||||||
|
|
||||||
const [{ chunkId }, assistantText] = await Promise.all([
|
const [{ chunkId }, assistantText] = await Promise.all([
|
||||||
audioPromise,
|
audioPromise,
|
||||||
@@ -114,7 +123,7 @@ function waitForSignal<T>(
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
expect(chunkId.length).toBeGreaterThan(0);
|
expect(chunkId.length).toBeGreaterThan(0);
|
||||||
expect(assistantText.toLowerCase()).toContain("local voice agent path is working");
|
expect(assistantText.trim().length).toBeGreaterThan(0);
|
||||||
|
|
||||||
const agents = await ctx.client.fetchAgents();
|
const agents = await ctx.client.fetchAgents();
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -294,11 +294,6 @@ export type AgentStreamEventPayload = z.infer<
|
|||||||
// Session Inbound Messages (Session receives these)
|
// Session Inbound Messages (Session receives these)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export const UserTextMessageSchema = z.object({
|
|
||||||
type: z.literal("user_text"),
|
|
||||||
text: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const VoiceAudioChunkMessageSchema = z.object({
|
export const VoiceAudioChunkMessageSchema = z.object({
|
||||||
type: z.literal("voice_audio_chunk"),
|
type: z.literal("voice_audio_chunk"),
|
||||||
audio: z.string(), // base64 encoded
|
audio: z.string(), // base64 encoded
|
||||||
@@ -859,7 +854,6 @@ export const KillTerminalRequestSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
|
export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
|
||||||
UserTextMessageSchema,
|
|
||||||
VoiceAudioChunkMessageSchema,
|
VoiceAudioChunkMessageSchema,
|
||||||
AbortRequestMessageSchema,
|
AbortRequestMessageSchema,
|
||||||
AudioPlayedMessageSchema,
|
AudioPlayedMessageSchema,
|
||||||
@@ -1713,7 +1707,6 @@ export type InitializeAgentResponseMessage = z.infer<typeof InitializeAgentRespo
|
|||||||
export type ActivityLogPayload = z.infer<typeof ActivityLogPayloadSchema>;
|
export type ActivityLogPayload = z.infer<typeof ActivityLogPayloadSchema>;
|
||||||
|
|
||||||
// Type exports for inbound message types
|
// Type exports for inbound message types
|
||||||
export type UserTextMessage = z.infer<typeof UserTextMessageSchema>;
|
|
||||||
export type VoiceAudioChunkMessage = z.infer<typeof VoiceAudioChunkMessageSchema>;
|
export type VoiceAudioChunkMessage = z.infer<typeof VoiceAudioChunkMessageSchema>;
|
||||||
export type FetchAgentsRequestMessage = z.infer<typeof FetchAgentsRequestMessageSchema>;
|
export type FetchAgentsRequestMessage = z.infer<typeof FetchAgentsRequestMessageSchema>;
|
||||||
export type FetchAgentRequestMessage = z.infer<typeof FetchAgentRequestMessageSchema>;
|
export type FetchAgentRequestMessage = z.infer<typeof FetchAgentRequestMessageSchema>;
|
||||||
|
|||||||
Reference in New Issue
Block a user