Skip to content

Commit 1f96a0e

Browse files
authored
Merge pull request #28 from fancyboi999/codex/fix-language-enforcement
fix: enforce response language by settings
2 parents 4582112 + 5896598 commit 1f96a0e

9 files changed

Lines changed: 150 additions & 19 deletions

File tree

‎src-api/src/app/api/agent.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ agent.post('/plan', async (c) => {
6767

6868
const session = createSession('plan');
6969
const readable = createSSEStream(
70-
runPlanningPhase(body.prompt, session, body.modelConfig)
70+
runPlanningPhase(body.prompt, session, body.modelConfig, body.language)
7171
);
7272

7373
return new Response(readable, { headers: SSE_HEADERS });
@@ -94,6 +94,7 @@ agent.post('/execute', async (c) => {
9494
appDirEnabled: boolean;
9595
mcpConfigPath?: string;
9696
};
97+
language?: string;
9798
}>();
9899

99100
console.log('[AgentAPI] POST /execute received:', {
@@ -129,7 +130,8 @@ agent.post('/execute', async (c) => {
129130
body.modelConfig,
130131
body.sandboxConfig,
131132
body.skillsConfig,
132-
body.mcpConfig
133+
body.mcpConfig,
134+
body.language
133135
)
134136
);
135137

@@ -189,7 +191,8 @@ agent.post('/', async (c) => {
189191
body.sandboxConfig,
190192
body.images,
191193
body.skillsConfig,
192-
body.mcpConfig
194+
body.mcpConfig,
195+
body.language
193196
)
194197
);
195198

‎src-api/src/core/agent/base.ts‎

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -514,13 +514,66 @@ sandbox_run_script:
514514
return instruction;
515515
}
516516

517+
export type ResolvedLanguage = 'en-US' | 'zh-CN';
518+
519+
const LANGUAGE_ALIASES: Record<string, ResolvedLanguage> = {
520+
'en': 'en-US',
521+
'en-us': 'en-US',
522+
'english': 'en-US',
523+
'zh': 'zh-CN',
524+
'zh-cn': 'zh-CN',
525+
'zh-hans': 'zh-CN',
526+
'chinese': 'zh-CN',
527+
'cn': 'zh-CN',
528+
};
529+
530+
export function detectLanguageFromText(text?: string): ResolvedLanguage {
531+
if (!text) return 'en-US';
532+
const hasCjk = /[\u3400-\u9FFF]/.test(text);
533+
return hasCjk ? 'zh-CN' : 'en-US';
534+
}
535+
536+
export function resolveLanguage(
537+
language?: string,
538+
prompt?: string
539+
): ResolvedLanguage {
540+
const normalized = language?.trim().toLowerCase();
541+
if (normalized) {
542+
const mapped = LANGUAGE_ALIASES[normalized];
543+
if (mapped) return mapped;
544+
if (normalized.startsWith('zh')) return 'zh-CN';
545+
if (normalized.startsWith('en')) return 'en-US';
546+
}
547+
return detectLanguageFromText(prompt);
548+
}
549+
550+
export function buildLanguageInstruction(
551+
language?: string,
552+
prompt?: string
553+
): string {
554+
const resolved = resolveLanguage(language, prompt);
555+
const label =
556+
resolved === 'zh-CN' ? 'Chinese (Simplified)' : 'English';
557+
const rule =
558+
resolved === 'zh-CN'
559+
? '请仅使用简体中文回答,不要夹杂英文或其他语言。'
560+
: 'Respond only in English. Do not use Chinese or any other language.';
561+
return `
562+
## LANGUAGE REQUIREMENT
563+
- Output language: ${label}
564+
- ${rule}
565+
`;
566+
}
567+
517568
/**
518569
* Format a plan for execution phase
519570
*/
520571
export function formatPlanForExecution(
521572
plan: TaskPlan,
522573
workDir?: string,
523-
sandbox?: SandboxOptions
574+
sandbox?: SandboxOptions,
575+
language?: string,
576+
originalPrompt?: string
524577
): string {
525578
const stepsText = plan.steps
526579
.map((step, index) => `${index + 1}. ${step.description}`)
@@ -529,9 +582,10 @@ export function formatPlanForExecution(
529582
const workspaceNote = workDir
530583
? getWorkspaceInstruction(workDir, sandbox)
531584
: '';
585+
const languageNote = buildLanguageInstruction(language, originalPrompt);
532586

533587
return `You are executing a pre-approved plan. Follow these steps in order:
534-
${workspaceNote}
588+
${workspaceNote}${languageNote}
535589
Goal: ${plan.goal}
536590
537591
Steps:

‎src-api/src/core/agent/types.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ export interface AgentOptions {
139139
sessionId?: string;
140140
/** Conversation history */
141141
conversation?: ConversationMessage[];
142+
/** Preferred response language (e.g., en-US, zh-CN) */
143+
language?: string;
142144
/** Working directory */
143145
cwd?: string;
144146
/** Allowed tools */

‎src-api/src/extensions/agent/claude/index.ts‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { z } from 'zod';
1919

2020
import {
2121
BaseAgent,
22+
buildLanguageInstruction,
2223
formatPlanForExecution,
2324
getWorkspaceInstruction,
2425
parsePlanFromResponse,
@@ -1494,17 +1495,23 @@ User's request (answer this AFTER reading the images):
14941495
const conversationContext = this.formatConversationHistory(
14951496
options?.conversation
14961497
);
1498+
const languageInstruction = buildLanguageInstruction(
1499+
options?.language,
1500+
prompt
1501+
);
14971502

14981503
// Add workspace instruction to prompt so skills know where to save files
14991504
// If images are attached, put image instruction FIRST (highest priority)
15001505
const enhancedPrompt = imageInstruction
15011506
? imageInstruction +
1507+
languageInstruction +
15021508
prompt +
15031509
'\n\n' +
15041510
getWorkspaceInstruction(sessionCwd, sandboxOpts) +
15051511
conversationContext
15061512
: getWorkspaceInstruction(sessionCwd, sandboxOpts) +
15071513
conversationContext +
1514+
languageInstruction +
15081515
prompt;
15091516

15101517
// Ensure Claude Code is installed
@@ -1727,7 +1734,12 @@ User's request (answer this AFTER reading the images):
17271734
**ALL files must be saved to: ${sessionCwd}**
17281735
If you need to create any files during planning, use this directory.
17291736
`;
1730-
const planningPrompt = workspaceInstruction + PLANNING_INSTRUCTION + prompt;
1737+
const languageInstruction = buildLanguageInstruction(
1738+
options?.language,
1739+
prompt
1740+
);
1741+
const planningPrompt =
1742+
workspaceInstruction + PLANNING_INSTRUCTION + languageInstruction + prompt;
17311743

17321744
let fullResponse = '';
17331745

@@ -1876,7 +1888,13 @@ If you need to create any files during planning, use this directory.
18761888

18771889
// Pass workDir and sandbox to formatPlanForExecution so skills know where to save files
18781890
const executionPrompt =
1879-
formatPlanForExecution(plan, sessionCwd, sandboxOpts) +
1891+
formatPlanForExecution(
1892+
plan,
1893+
sessionCwd,
1894+
sandboxOpts,
1895+
options.language,
1896+
options.originalPrompt
1897+
) +
18801898
'\n\nOriginal request: ' +
18811899
options.originalPrompt;
18821900
logger.info(

‎src-api/src/extensions/agent/codex/index.ts‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { join } from 'path';
1313

1414
import {
1515
BaseAgent,
16+
buildLanguageInstruction,
1617
formatPlanForExecution,
1718
getWorkspaceInstruction,
1819
parsePlanFromResponse,
@@ -322,9 +323,15 @@ export class CodexAgent extends BaseAgent {
322323
}
323324
: undefined;
324325

326+
const languageInstruction = buildLanguageInstruction(
327+
options?.language,
328+
prompt
329+
);
325330
// Add workspace instruction to prompt
326331
const enhancedPrompt =
327-
getWorkspaceInstruction(sessionCwd, sandboxOpts) + prompt;
332+
getWorkspaceInstruction(sessionCwd, sandboxOpts) +
333+
languageInstruction +
334+
prompt;
328335

329336
// Ensure Codex CLI is installed
330337
const codexPath = await ensureCodex();
@@ -401,7 +408,11 @@ export class CodexAgent extends BaseAgent {
401408

402409
// For Codex, we'll use a simplified planning approach
403410
// Since Codex doesn't have a native planning mode, we'll ask it to generate a plan
404-
const planningPrompt = `${PLANNING_INSTRUCTION}${prompt}
411+
const languageInstruction = buildLanguageInstruction(
412+
options?.language,
413+
prompt
414+
);
415+
const planningPrompt = `${PLANNING_INSTRUCTION}${languageInstruction}${prompt}
405416
406417
Please respond ONLY with JSON in this exact format, no other text:
407418
{
@@ -523,7 +534,13 @@ Please respond ONLY with JSON in this exact format, no other text:
523534
: undefined;
524535

525536
const executionPrompt =
526-
formatPlanForExecution(plan, sessionCwd, sandboxOpts) +
537+
formatPlanForExecution(
538+
plan,
539+
sessionCwd,
540+
sandboxOpts,
541+
options.language,
542+
options.originalPrompt
543+
) +
527544
'\n\nOriginal request: ' +
528545
options.originalPrompt;
529546

‎src-api/src/extensions/agent/deepagents/index.ts‎

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
* the implementation according to the actual DeepAgents.js API.
99
*/
1010

11-
import { BaseAgent, parsePlanFromResponse } from '@/core/agent/base';
11+
import {
12+
BaseAgent,
13+
parsePlanFromResponse,
14+
resolveLanguage,
15+
} from '@/core/agent/base';
1216
// Import plugin definition helpers
1317
import { DEEPAGENTS_METADATA, defineAgentPlugin } from '@/core/agent/plugin';
1418
import type { AgentPlugin } from '@/core/agent/plugin';
@@ -192,7 +196,7 @@ export class DeepAgentsAdapter extends BaseAgent {
192196
*/
193197
async *plan(
194198
prompt: string,
195-
_options?: PlanOptions
199+
options?: PlanOptions
196200
): AsyncGenerator<AgentMessage> {
197201
const session = this.createSession('planning', {
198202
id: _options?.sessionId,
@@ -209,23 +213,38 @@ export class DeepAgentsAdapter extends BaseAgent {
209213

210214
if (!agent) {
211215
// Fallback: create a simple plan without LLM
216+
const resolvedLanguage = resolveLanguage(options?.language, prompt);
217+
const planText =
218+
resolvedLanguage === 'zh-CN'
219+
? {
220+
goal: '完成用户请求',
221+
steps: ['分析需求', '执行必要操作', '验证并汇报结果'],
222+
}
223+
: {
224+
goal: 'Complete the user request',
225+
steps: [
226+
'Analyze the request',
227+
'Execute required actions',
228+
'Verify and report results',
229+
],
230+
};
212231
const simplePlan: TaskPlan = {
213232
id: `plan-${Date.now()}`,
214-
goal: prompt.slice(0, 100),
233+
goal: planText.goal,
215234
steps: [
216235
{
217236
id: '1',
218-
description: 'Analyze the request',
237+
description: planText.steps[0],
219238
status: 'pending',
220239
},
221240
{
222241
id: '2',
223-
description: 'Execute the required actions',
242+
description: planText.steps[1],
224243
status: 'pending',
225244
},
226245
{
227246
id: '3',
228-
description: 'Verify and report results',
247+
description: planText.steps[2],
229248
status: 'pending',
230249
},
231250
],

‎src-api/src/shared/services/agent.ts‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,13 +149,15 @@ export function deletePlan(planId: string): boolean {
149149
export async function* runPlanningPhase(
150150
prompt: string,
151151
session: AgentSession,
152-
modelConfig?: { apiKey?: string; baseUrl?: string; model?: string }
152+
modelConfig?: { apiKey?: string; baseUrl?: string; model?: string },
153+
language?: string
153154
): AsyncGenerator<AgentMessage> {
154155
const agent = getAgent(modelConfig);
155156

156157
for await (const message of agent.plan(prompt, {
157158
sessionId: session.id,
158159
abortController: session.abortController,
160+
language,
159161
})) {
160162
// Intercept plan messages and save to global store
161163
if (message.type === 'plan' && message.plan) {
@@ -177,7 +179,8 @@ export async function* runExecutionPhase(
177179
modelConfig?: { apiKey?: string; baseUrl?: string; model?: string },
178180
sandboxConfig?: SandboxConfig,
179181
skillsConfig?: SkillsConfig,
180-
mcpConfig?: McpConfig
182+
mcpConfig?: McpConfig,
183+
language?: string
181184
): AsyncGenerator<AgentMessage> {
182185
const agent = getAgent(modelConfig);
183186

@@ -212,6 +215,7 @@ export async function* runExecutionPhase(
212215
sandbox: sandboxConfig,
213216
skillsConfig,
214217
mcpConfig,
218+
language,
215219
})) {
216220
yield message;
217221
}
@@ -230,7 +234,8 @@ export async function* runAgent(
230234
sandboxConfig?: SandboxConfig,
231235
images?: ImageAttachment[],
232236
skillsConfig?: SkillsConfig,
233-
mcpConfig?: McpConfig
237+
mcpConfig?: McpConfig,
238+
language?: string
234239
): AsyncGenerator<AgentMessage> {
235240
const agent = getAgent(modelConfig);
236241

@@ -254,6 +259,7 @@ export async function* runAgent(
254259
images,
255260
skillsConfig,
256261
mcpConfig,
262+
language,
257263
})) {
258264
yield message;
259265
}

‎src-api/src/shared/types/agent.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ export interface AgentRequest {
7979
role: 'user' | 'assistant';
8080
content: string;
8181
}>;
82+
/** Preferred response language (e.g., en-US, zh-CN) */
83+
language?: string;
8284
// Two-phase execution control
8385
phase?: 'plan' | 'execute';
8486
planId?: string; // Reference to approved plan

0 commit comments

Comments
 (0)