mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
* feat(codex): support /personality via in-session override Intercept /personality in the Codex slash layer, keep the value in CLI memory only, and forward it on thread/turn start when set. Unset means omit the field so Codex config/thread defaults apply—no Hub DB or web UI. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(codex): refuse fake /personality clear (sticky thread setting) turn/start.personality sticks for later turns; omitting the field does not restore config.toml. Drop default|auto|clear success paths and require an explicit friendly|pragmatic|none instead. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
305 lines
12 KiB
TypeScript
305 lines
12 KiB
TypeScript
import type { EnhancedMode } from '../loop';
|
|
import type { CodexCliOverrides } from './codexCliOverrides';
|
|
import type { McpServersConfig } from './buildHapiMcpBridge';
|
|
import { codexSystemPrompt } from './systemPrompt';
|
|
import type {
|
|
ApprovalPolicy,
|
|
SandboxMode,
|
|
SandboxPolicy,
|
|
ThreadStartParams,
|
|
TurnStartParams,
|
|
UserInput
|
|
} from '../appServerTypes';
|
|
import { resolveCodexPermissionModeConfig } from './permissionModeConfig';
|
|
|
|
export const codexCollaborationSpawnAgentInstructions = [
|
|
'Codex sub-agent spawning rules:',
|
|
'- Treat omitted fork_context the same as fork_context: true: a full-history fork inherits the parent agent type, model, and reasoning effort.',
|
|
'- If you call spawn_agent with fork_context omitted or true, do not set agent_type, model, or reasoning_effort.',
|
|
'- If you need a specific agent_type, model, or reasoning_effort, set fork_context: false and include only the necessary context in the message.',
|
|
'- Do not rely on parent turn reasoning settings for spawned agents; only set reasoning_effort on spawn_agent when the chosen child model supports it.'
|
|
].join('\n');
|
|
|
|
const MODELS_WITHOUT_REASONING_SUMMARY = new Set([
|
|
'gpt-5.3-codex-spark'
|
|
]);
|
|
|
|
const MCP_ELICITATION_ONLY_APPROVAL_POLICY = {
|
|
granular: {
|
|
sandbox_approval: false,
|
|
rules: false,
|
|
skill_approval: false,
|
|
request_permissions: false,
|
|
mcp_elicitations: true
|
|
}
|
|
} as const satisfies ApprovalPolicy;
|
|
|
|
function resolveApprovalPolicy(mode: EnhancedMode): ApprovalPolicy {
|
|
if (mode.permissionMode === 'yolo' || mode.permissionMode === 'read-only') {
|
|
// Codex's `never` policy auto-declines MCP elicitations before app-server
|
|
// can forward them. Keep command/sandbox prompts disabled for Yolo and
|
|
// read-only while allowing auth and structured input to reach HAPI's UI.
|
|
return MCP_ELICITATION_ONLY_APPROVAL_POLICY;
|
|
}
|
|
return resolveCodexPermissionModeConfig(mode.permissionMode).approvalPolicy;
|
|
}
|
|
|
|
function resolveSandbox(mode: EnhancedMode): SandboxMode {
|
|
return resolveCodexPermissionModeConfig(mode.permissionMode).sandbox;
|
|
}
|
|
|
|
function resolveSandboxPolicy(mode: EnhancedMode): SandboxPolicy {
|
|
return resolveCodexPermissionModeConfig(mode.permissionMode).sandboxPolicy;
|
|
}
|
|
|
|
function resolveSandboxPolicyOverride(value: CodexCliOverrides['sandbox'] | undefined): SandboxPolicy | undefined {
|
|
switch (value) {
|
|
case 'read-only':
|
|
return { type: 'readOnly' };
|
|
case 'workspace-write':
|
|
return { type: 'workspaceWrite' };
|
|
case 'danger-full-access':
|
|
return { type: 'dangerFullAccess' };
|
|
default:
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
// The Codex model catalog advertises the Fast tier with request id `'priority'`
|
|
// (display name "Fast"); OpenAI's docs confirm the legacy `service_tier = "fast"`
|
|
// maps to the request value `priority`. The app-server `serviceTier` override is
|
|
// a raw request value and does not validate unknown strings, so sending `'fast'`
|
|
// would be silently ignored — we must send the advertised `'priority'` id.
|
|
const APP_SERVER_FAST_TIER = 'priority';
|
|
|
|
/**
|
|
* Translate HAPI's stored service-tier representation into the Codex
|
|
* app-server `serviceTier` field for thread/turn params:
|
|
* - `'fast'` → `'priority'` (the advertised Fast tier request value)
|
|
* - `'standard'` → `null` (explicit Standard tier)
|
|
* - anything else / untouched → `undefined` (omit; use account default)
|
|
*/
|
|
function toAppServerServiceTier(stored: string | null | undefined): string | null | undefined {
|
|
if (stored === 'fast') {
|
|
return APP_SERVER_FAST_TIER;
|
|
}
|
|
if (stored === 'standard') {
|
|
return null;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function supportsReasoningSummary(model: string | undefined): boolean {
|
|
const normalized = model?.trim().toLowerCase();
|
|
if (!normalized) return true;
|
|
const modelName = normalized.split('/').pop() ?? normalized;
|
|
return !MODELS_WITHOUT_REASONING_SUMMARY.has(modelName);
|
|
}
|
|
|
|
function buildMcpServerConfig(mcpServers: McpServersConfig): Record<string, unknown> {
|
|
const config: Record<string, unknown> = {};
|
|
|
|
for (const [name, server] of Object.entries(mcpServers)) {
|
|
config[`mcp_servers.${name}`] = {
|
|
command: server.command,
|
|
args: server.args,
|
|
...(server.tools ? { tools: server.tools } : {})
|
|
};
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
function resolveInstructions(args: {
|
|
baseInstructions?: string;
|
|
developerInstructions?: string;
|
|
}): { baseInstructions: string; developerInstructions: string } {
|
|
const baseInstructions = args.baseInstructions ?? codexSystemPrompt;
|
|
const developerInstructions = args.developerInstructions
|
|
? `${baseInstructions}\n\n${args.developerInstructions}`
|
|
: baseInstructions;
|
|
return {
|
|
baseInstructions,
|
|
developerInstructions
|
|
};
|
|
}
|
|
|
|
function appendCollaborationInstructions(developerInstructions: string, proactiveMultiAgent?: boolean): string {
|
|
if (proactiveMultiAgent === undefined) {
|
|
return `${developerInstructions}\n\n${codexCollaborationSpawnAgentInstructions}`;
|
|
}
|
|
const multiAgentMode = proactiveMultiAgent
|
|
? 'Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Use sub-agents when parallel work would materially improve speed or quality. This mode remains active until a later multi-agent mode developer message changes it.'
|
|
: 'Any earlier instruction enabling proactive multi-agent delegation no longer applies. Do not spawn sub-agents unless the user or applicable AGENTS.md/skill instructions explicitly ask for sub-agents, delegation, or parallel agent work.';
|
|
return `${developerInstructions}\n\n${codexCollaborationSpawnAgentInstructions}\n\n<multi_agent_mode>${multiAgentMode}</multi_agent_mode>`;
|
|
}
|
|
|
|
function mentionNameFromPath(path: string): string {
|
|
const parts = path.split(/[\\/]/).filter(Boolean);
|
|
return parts[parts.length - 1] ?? path;
|
|
}
|
|
|
|
export function buildUserInputFromMessage(message: string): UserInput[] {
|
|
const inputs: UserInput[] = [];
|
|
const mentionPattern = /(^|\s)@"((?:\\.|[^"\\])*)"/g;
|
|
let lastIndex = 0;
|
|
let match: RegExpExecArray | null;
|
|
|
|
while ((match = mentionPattern.exec(message)) !== null) {
|
|
const prefix = match[1] ?? '';
|
|
const rawPath = match[2] ?? '';
|
|
const pathText = rawPath;
|
|
const path = pathText.replace(/\\(["\\])/g, '$1');
|
|
if (!path) continue;
|
|
|
|
const atIndex = match.index + prefix.length;
|
|
const textBeforeMention = message.slice(lastIndex, atIndex);
|
|
if (textBeforeMention) {
|
|
inputs.push({ type: 'text', text: textBeforeMention });
|
|
}
|
|
|
|
inputs.push({
|
|
type: 'mention',
|
|
name: mentionNameFromPath(path),
|
|
path
|
|
});
|
|
lastIndex = mentionPattern.lastIndex - (rawPath.length - pathText.length);
|
|
}
|
|
|
|
const remainder = message.slice(lastIndex);
|
|
if (remainder || inputs.length === 0) {
|
|
inputs.push({ type: 'text', text: remainder });
|
|
}
|
|
|
|
return inputs;
|
|
}
|
|
|
|
export function buildThreadStartParams(args: {
|
|
cwd: string;
|
|
mode: EnhancedMode;
|
|
mcpServers: McpServersConfig;
|
|
cliOverrides?: CodexCliOverrides;
|
|
baseInstructions?: string;
|
|
developerInstructions?: string;
|
|
}): ThreadStartParams {
|
|
const approvalPolicy = resolveApprovalPolicy(args.mode);
|
|
const sandbox = resolveSandbox(args.mode);
|
|
const allowCliOverrides = args.mode.permissionMode === 'default';
|
|
const cliOverrides = allowCliOverrides ? args.cliOverrides : undefined;
|
|
const resolvedApprovalPolicy = cliOverrides?.approvalPolicy ?? approvalPolicy;
|
|
const resolvedSandbox = cliOverrides?.sandbox ?? sandbox;
|
|
|
|
const config = buildMcpServerConfig(args.mcpServers);
|
|
const {
|
|
baseInstructions,
|
|
developerInstructions: resolvedDeveloperInstructions
|
|
} = resolveInstructions(args);
|
|
const configWithInstructions = {
|
|
...config,
|
|
developer_instructions: resolvedDeveloperInstructions,
|
|
...(args.mode.modelReasoningEffort ? { model_reasoning_effort: args.mode.modelReasoningEffort } : {})
|
|
};
|
|
|
|
const params: ThreadStartParams = {
|
|
cwd: args.cwd,
|
|
approvalPolicy: resolvedApprovalPolicy,
|
|
sandbox: resolvedSandbox,
|
|
baseInstructions,
|
|
developerInstructions: resolvedDeveloperInstructions,
|
|
...(Object.keys(configWithInstructions).length > 0 ? { config: configWithInstructions } : {})
|
|
};
|
|
|
|
if (args.mode.model) {
|
|
params.model = args.mode.model;
|
|
}
|
|
if (args.mode.personality) {
|
|
params.personality = args.mode.personality;
|
|
}
|
|
|
|
const threadServiceTier = toAppServerServiceTier(args.mode.serviceTier);
|
|
if (threadServiceTier !== undefined) {
|
|
params.serviceTier = threadServiceTier;
|
|
}
|
|
|
|
return params;
|
|
}
|
|
|
|
export function buildTurnStartParams(args: {
|
|
threadId: string;
|
|
message: string;
|
|
cwd: string;
|
|
mode?: EnhancedMode;
|
|
cliOverrides?: CodexCliOverrides;
|
|
baseInstructions?: string;
|
|
developerInstructions?: string;
|
|
overrides?: {
|
|
approvalPolicy?: TurnStartParams['approvalPolicy'];
|
|
sandboxPolicy?: TurnStartParams['sandboxPolicy'];
|
|
model?: string;
|
|
suppressCollaborationMode?: boolean;
|
|
};
|
|
}): TurnStartParams {
|
|
const params: TurnStartParams = {
|
|
threadId: args.threadId,
|
|
cwd: args.cwd,
|
|
input: buildUserInputFromMessage(args.message)
|
|
};
|
|
|
|
const allowCliOverrides = args.mode?.permissionMode === 'default';
|
|
const cliOverrides = allowCliOverrides ? args.cliOverrides : undefined;
|
|
const approvalPolicy = args.overrides?.approvalPolicy
|
|
?? cliOverrides?.approvalPolicy
|
|
?? (args.mode ? resolveApprovalPolicy(args.mode) : undefined);
|
|
if (approvalPolicy) {
|
|
params.approvalPolicy = approvalPolicy;
|
|
}
|
|
|
|
const sandboxPolicy = args.overrides?.sandboxPolicy
|
|
?? resolveSandboxPolicyOverride(cliOverrides?.sandbox)
|
|
?? (args.mode ? resolveSandboxPolicy(args.mode) : undefined);
|
|
if (sandboxPolicy) {
|
|
params.sandboxPolicy = sandboxPolicy;
|
|
}
|
|
|
|
const collaborationMode = args.overrides?.suppressCollaborationMode
|
|
? undefined
|
|
: args.mode?.collaborationMode;
|
|
const model = args.overrides?.model ?? args.mode?.model;
|
|
const modelReasoningEffort = args.mode?.modelReasoningEffort;
|
|
|
|
if (modelReasoningEffort) {
|
|
params.effort = modelReasoningEffort;
|
|
if (!collaborationMode && supportsReasoningSummary(model)) {
|
|
params.summary = 'detailed';
|
|
}
|
|
}
|
|
|
|
if (collaborationMode) {
|
|
if (!model) {
|
|
throw new Error(`Collaboration mode '${collaborationMode}' requires a resolved model`);
|
|
}
|
|
const { developerInstructions } = resolveInstructions(args);
|
|
params.collaborationMode = {
|
|
mode: collaborationMode,
|
|
settings: {
|
|
model,
|
|
...(modelReasoningEffort !== undefined ? { reasoning_effort: modelReasoningEffort } : {}),
|
|
developer_instructions: appendCollaborationInstructions(developerInstructions, args.mode?.proactiveMultiAgent)
|
|
}
|
|
};
|
|
} else if (model) {
|
|
params.model = model;
|
|
}
|
|
|
|
const turnServiceTier = toAppServerServiceTier(args.mode?.serviceTier);
|
|
if (turnServiceTier !== undefined) {
|
|
params.serviceTier = turnServiceTier;
|
|
}
|
|
|
|
if (args.mode?.personality) {
|
|
params.personality = args.mode.personality;
|
|
}
|
|
|
|
return params;
|
|
}
|