( message: string, candidates: string[], aiService: AIService, /** Optional conversation turns context used for regenerate-title prompts. */ conversationContext?: string, /** Optional most recent user message; included as additional context only — not given precedence over older turns. */ latestUserMessage?: string )
| 179 | } |
| 180 | |
| 181 | export async function generateWorkspaceIdentity( |
| 182 | message: string, |
| 183 | candidates: string[], |
| 184 | aiService: AIService, |
| 185 | /** Optional conversation turns context used for regenerate-title prompts. */ |
| 186 | conversationContext?: string, |
| 187 | /** Optional most recent user message; included as additional context only — not given precedence over older turns. */ |
| 188 | latestUserMessage?: string |
| 189 | ): Promise<Result<GenerateWorkspaceIdentityResult, NameGenerationError>> { |
| 190 | if (candidates.length === 0) { |
| 191 | return Err({ type: "unknown", raw: "No model candidates provided for name generation" }); |
| 192 | } |
| 193 | |
| 194 | // Try up to 3 candidates |
| 195 | const maxAttempts = Math.min(candidates.length, 3); |
| 196 | |
| 197 | // Track the last classified error to return if all candidates fail |
| 198 | let lastError: NameGenerationError | null = null; |
| 199 | |
| 200 | for (let i = 0; i < maxAttempts; i++) { |
| 201 | const modelString = candidates[i]; |
| 202 | |
| 203 | const modelResult = await aiService.createModel(modelString, undefined, { |
| 204 | agentInitiated: true, |
| 205 | }); |
| 206 | if (!modelResult.success) { |
| 207 | lastError = mapModelCreationError(modelResult.error, modelString); |
| 208 | log.debug(`Name generation: skipping ${modelString} (${modelResult.error.type})`); |
| 209 | continue; |
| 210 | } |
| 211 | |
| 212 | try { |
| 213 | // Use streamText with a propose_name tool instead of Output.object(). |
| 214 | // Tool calls are universally supported across LLM APIs and far more |
| 215 | // reliable than structured JSON output, eliminating all the fragile |
| 216 | // regex fallback parsing that was previously needed. |
| 217 | // |
| 218 | // streamText (not generateText): the Codex OAuth endpoint requires |
| 219 | // stream:true in the request body; streamText sets it automatically. |
| 220 | // |
| 221 | // No toolChoice — forced tool choice (toolChoice: "required" / "any" / |
| 222 | // { type: "tool" }) is incompatible with extended thinking models. |
| 223 | // Instead, the prompt instructs the model to call the tool, and the |
| 224 | // name_workspace builtin agent declares tools.require: [propose_name] |
| 225 | // which the StreamManager enforces via stopWhen for full agent sessions. |
| 226 | // For this direct streamText path, the candidate retry loop handles the |
| 227 | // (rare) case where the model ignores the instruction. |
| 228 | const currentStream = streamText({ |
| 229 | model: modelResult.data, |
| 230 | prompt: buildWorkspaceIdentityPrompt(message, conversationContext, latestUserMessage), |
| 231 | tools: { |
| 232 | // Defined inline so TypeScript preserves full schema inference on |
| 233 | // toolResult.output (the propose_name tool is only used here). |
| 234 | propose_name: tool({ |
| 235 | description: TOOL_DEFINITIONS.propose_name.description, |
| 236 | inputSchema: ProposeNameToolArgsSchema, |
| 237 | // eslint-disable-next-line @typescript-eslint/require-await -- AI SDK Tool.execute must return a Promise |
| 238 | execute: async (args) => ({ success: true as const, ...args }), |
no test coverage detected