diff --git a/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts b/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts index 65c4144d0..065934f9b 100644 --- a/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts +++ b/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts @@ -539,6 +539,26 @@ describe("codex tool-call mapper", () => { }); }); + it("normalizes codex paseo_voice.speak mcp calls and extracts spoken text", () => { + const item = mapCodexToolCallFromThreadItem({ + type: "mcpToolCall", + id: "codex-speak-thread-2", + status: "completed", + server: "paseo_voice", + tool: "speak", + arguments: { text: "Voice response from Codex via paseo_voice." }, + result: { ok: true }, + }); + + expect(item).toBeTruthy(); + expect(item?.name).toBe("speak"); + expect(item?.detail).toEqual({ + type: "unknown", + input: "Voice response from Codex via paseo_voice.", + output: null, + }); + }); + it("normalizes codex paseo speak rollout names and extracts spoken text", () => { const item = expectMapped( mapCodexRolloutToolCall({ diff --git a/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts b/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts index a5fd872f2..8b0657c05 100644 --- a/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts +++ b/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import type { ToolCallTimelineItem } from "../../agent-sdk-types.js"; import { extractCodexShellOutput, truncateDiffText } from "../tool-call-mapper-utils.js"; import { deriveCodexToolDetail, normalizeCodexFilePath } from "./tool-call-detail-parser.js"; +import { isSpeakToolName } from "../../tool-name-normalization.js"; type CodexMapperOptions = { cwd?: string | null }; @@ -67,7 +68,7 @@ const CodexEditToolNameSchema = z.union([ z.literal("apply_diff"), ]); const CodexSearchToolNameSchema = z.union([z.literal("search"), z.literal("web_search")]); -const CodexSpeakToolNameSchema = z.literal("paseo.speak"); +const CodexSpeakToolNameSchema = z.string().min(1).refine((name) => isSpeakToolName(name.trim())); const CodexToolKindSchema = z.enum([ "shell", diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 813cf0e4d..b60151273 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -133,6 +133,7 @@ import { buildVoiceAgentMcpServerConfig, buildVoiceModeSystemPrompt, stripVoiceModeSystemPrompt, + wrapSpokenInput, } from "./voice-config.js"; import { isVoicePermissionAllowed } from "./voice-permission-policy.js"; import { @@ -2834,6 +2835,7 @@ export class Session { messageId?: string, images?: Array<{ data: string; mimeType: string }>, runOptions?: AgentRunOptions, + options?: { spokenInput?: boolean }, ): Promise { this.sessionLogger.info( { agentId, textPreview: text.substring(0, 50), imageCount: images?.length ?? 0 }, @@ -2861,7 +2863,8 @@ export class Session { ); } - const prompt = this.buildAgentPrompt(text, images); + const promptText = options?.spokenInput ? wrapSpokenInput(text) : text; + const prompt = this.buildAgentPrompt(promptText, images); this.startAgentStream(agentId, prompt, runOptions); } @@ -7019,9 +7022,14 @@ export class Session { return; } - // Route voice utterances through the same send path as regular text input: - // interrupt-if-running, record message, then start a new stream. - await this.handleSendAgentMessage(agentId, result.text); + await this.handleSendAgentMessage( + agentId, + result.text, + undefined, + undefined, + undefined, + { spokenInput: true }, + ); await this.flushPendingAudioSegments("transcription complete"); } diff --git a/packages/server/src/server/voice-config.ts b/packages/server/src/server/voice-config.ts index 482e8197f..77b3a698a 100644 --- a/packages/server/src/server/voice-config.ts +++ b/packages/server/src/server/voice-config.ts @@ -53,6 +53,10 @@ export function buildVoiceModeSystemPrompt(existing: string | undefined, enabled .join("\n\n"); } +export function wrapSpokenInput(text: string): string { + return `\n${text}\n`; +} + export function buildVoiceAgentMcpServerConfig(params: { command: string; baseArgs: string[];