Merge branch 'v0.9.0rc' into feat/subagents-skills

Resolved conflict in agent-service.ts by keeping both:
- agents parameter for custom subagents (from our branch)
- thinkingLevel and reasoningEffort parameters (from v0.9.0rc)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Kacper
2026-01-08 22:57:09 +01:00
46 changed files with 1570 additions and 525 deletions

View File

@@ -10,7 +10,7 @@ import { BaseProvider } from './base-provider.js';
import { classifyError, getUserFriendlyErrorMessage, createLogger } from '@automaker/utils';
const logger = createLogger('ClaudeProvider');
import { getThinkingTokenBudget } from '@automaker/types';
import { getThinkingTokenBudget, validateBareModelId } from '@automaker/types';
import type {
ExecuteOptions,
ProviderMessage,
@@ -53,6 +53,10 @@ export class ClaudeProvider extends BaseProvider {
* Execute a query using Claude Agent SDK
*/
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
// Validate that model doesn't have a provider prefix
// AgentService should strip prefixes before passing to providers
validateBareModelId(options.model, 'ClaudeProvider');
const {
prompt,
model,

View File

@@ -7,16 +7,17 @@
import { CODEX_MODEL_MAP } from '@automaker/types';
import type { ModelDefinition } from './types.js';
const CONTEXT_WINDOW_200K = 200000;
const CONTEXT_WINDOW_256K = 256000;
const CONTEXT_WINDOW_128K = 128000;
const MAX_OUTPUT_32K = 32000;
const MAX_OUTPUT_16K = 16000;
/**
* All available Codex models with their specifications
* Based on https://developers.openai.com/codex/models/
*/
export const CODEX_MODELS: ModelDefinition[] = [
// ========== Codex-Specific Models ==========
// ========== Recommended Codex Models ==========
{
id: CODEX_MODEL_MAP.gpt52Codex,
name: 'GPT-5.2-Codex',
@@ -24,7 +25,7 @@ export const CODEX_MODELS: ModelDefinition[] = [
provider: 'openai',
description:
'Most advanced agentic coding model for complex software engineering (default for ChatGPT users).',
contextWindow: CONTEXT_WINDOW_200K,
contextWindow: CONTEXT_WINDOW_256K,
maxOutputTokens: MAX_OUTPUT_32K,
supportsVision: true,
supportsTools: true,
@@ -33,38 +34,12 @@ export const CODEX_MODELS: ModelDefinition[] = [
hasReasoning: true,
},
{
id: CODEX_MODEL_MAP.gpt5Codex,
name: 'GPT-5-Codex',
modelString: CODEX_MODEL_MAP.gpt5Codex,
id: CODEX_MODEL_MAP.gpt51CodexMax,
name: 'GPT-5.1-Codex-Max',
modelString: CODEX_MODEL_MAP.gpt51CodexMax,
provider: 'openai',
description: 'Purpose-built for Codex CLI with versatile tool use (default for CLI users).',
contextWindow: CONTEXT_WINDOW_200K,
maxOutputTokens: MAX_OUTPUT_32K,
supportsVision: true,
supportsTools: true,
tier: 'standard' as const,
hasReasoning: true,
},
{
id: CODEX_MODEL_MAP.gpt5CodexMini,
name: 'GPT-5-Codex-Mini',
modelString: CODEX_MODEL_MAP.gpt5CodexMini,
provider: 'openai',
description: 'Faster workflows optimized for low-latency code Q&A and editing.',
contextWindow: CONTEXT_WINDOW_128K,
maxOutputTokens: MAX_OUTPUT_16K,
supportsVision: false,
supportsTools: true,
tier: 'basic' as const,
hasReasoning: false,
},
{
id: CODEX_MODEL_MAP.codex1,
name: 'Codex-1',
modelString: CODEX_MODEL_MAP.codex1,
provider: 'openai',
description: 'Version of o3 optimized for software engineering with advanced reasoning.',
contextWindow: CONTEXT_WINDOW_200K,
description: 'Optimized for long-horizon, agentic coding tasks in Codex.',
contextWindow: CONTEXT_WINDOW_256K,
maxOutputTokens: MAX_OUTPUT_32K,
supportsVision: true,
supportsTools: true,
@@ -72,27 +47,40 @@ export const CODEX_MODELS: ModelDefinition[] = [
hasReasoning: true,
},
{
id: CODEX_MODEL_MAP.codexMiniLatest,
name: 'Codex-Mini-Latest',
modelString: CODEX_MODEL_MAP.codexMiniLatest,
id: CODEX_MODEL_MAP.gpt51CodexMini,
name: 'GPT-5.1-Codex-Mini',
modelString: CODEX_MODEL_MAP.gpt51CodexMini,
provider: 'openai',
description: 'Version of o4-mini designed for Codex with faster workflows.',
description: 'Smaller, more cost-effective version for faster workflows.',
contextWindow: CONTEXT_WINDOW_128K,
maxOutputTokens: MAX_OUTPUT_16K,
supportsVision: true,
supportsTools: true,
tier: 'standard' as const,
tier: 'basic' as const,
hasReasoning: false,
},
// ========== Base GPT-5 Model ==========
// ========== General-Purpose GPT Models ==========
{
id: CODEX_MODEL_MAP.gpt5,
name: 'GPT-5',
modelString: CODEX_MODEL_MAP.gpt5,
id: CODEX_MODEL_MAP.gpt52,
name: 'GPT-5.2',
modelString: CODEX_MODEL_MAP.gpt52,
provider: 'openai',
description: 'GPT-5 base flagship model with strong general-purpose capabilities.',
contextWindow: CONTEXT_WINDOW_200K,
description: 'Best general agentic model for tasks across industries and domains.',
contextWindow: CONTEXT_WINDOW_256K,
maxOutputTokens: MAX_OUTPUT_32K,
supportsVision: true,
supportsTools: true,
tier: 'standard' as const,
hasReasoning: true,
},
{
id: CODEX_MODEL_MAP.gpt51,
name: 'GPT-5.1',
modelString: CODEX_MODEL_MAP.gpt51,
provider: 'openai',
description: 'Great for coding and agentic tasks across domains.',
contextWindow: CONTEXT_WINDOW_256K,
maxOutputTokens: MAX_OUTPUT_32K,
supportsVision: true,
supportsTools: true,

View File

@@ -31,6 +31,7 @@ import type {
import {
CODEX_MODEL_MAP,
supportsReasoningEffort,
validateBareModelId,
type CodexApprovalPolicy,
type CodexSandboxMode,
type CodexAuthStatus,
@@ -61,6 +62,7 @@ const CODEX_ADD_DIR_FLAG = '--add-dir';
const CODEX_SKIP_GIT_REPO_CHECK_FLAG = '--skip-git-repo-check';
const CODEX_RESUME_FLAG = 'resume';
const CODEX_REASONING_EFFORT_KEY = 'reasoning_effort';
const CODEX_YOLO_FLAG = '--dangerously-bypass-approvals-and-sandbox';
const OPENAI_API_KEY_ENV = 'OPENAI_API_KEY';
const CODEX_EXECUTION_MODE_CLI = 'cli';
const CODEX_EXECUTION_MODE_SDK = 'sdk';
@@ -662,6 +664,10 @@ export class CodexProvider extends BaseProvider {
}
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
// Validate that model doesn't have a provider prefix
// AgentService should strip prefixes before passing to providers
validateBareModelId(options.model, 'CodexProvider');
try {
const mcpServers = options.mcpServers ?? {};
const hasMcpServers = Object.keys(mcpServers).length > 0;
@@ -759,18 +765,15 @@ export class CodexProvider extends BaseProvider {
}
}
// Model is already bare (no prefix) - validated by executeQuery
const args = [
CODEX_EXEC_SUBCOMMAND,
CODEX_YOLO_FLAG,
CODEX_SKIP_GIT_REPO_CHECK_FLAG,
...preExecArgs,
CODEX_MODEL_FLAG,
options.model,
CODEX_JSON_FLAG,
CODEX_SANDBOX_FLAG,
resolvedSandboxMode,
...(outputSchemaPath ? [CODEX_OUTPUT_SCHEMA_FLAG, outputSchemaPath] : []),
...(imagePaths.length > 0 ? [CODEX_IMAGE_FLAG, imagePaths.join(',')] : []),
...configOverrides,
'-', // Read prompt from stdin to avoid shell escaping issues
];

View File

@@ -28,7 +28,7 @@ import type {
ModelDefinition,
ContentBlock,
} from './types.js';
import { stripProviderPrefix } from '@automaker/types';
import { validateBareModelId } from '@automaker/types';
import { validateApiKey } from '../lib/auth-utils.js';
import { getEffectivePermissions } from '../services/cursor-config-service.js';
import {
@@ -317,8 +317,8 @@ export class CursorProvider extends CliProvider {
}
buildCliArgs(options: ExecuteOptions): string[] {
// Extract model (strip 'cursor-' prefix if present)
const model = stripProviderPrefix(options.model || 'auto');
// Model is already bare (no prefix) - validated by executeQuery
const model = options.model || 'auto';
// Build CLI arguments for cursor-agent
// NOTE: Prompt is NOT included here - it's passed via stdin to avoid
@@ -649,6 +649,10 @@ export class CursorProvider extends CliProvider {
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
this.ensureCliDetected();
// Validate that model doesn't have a provider prefix
// AgentService should strip prefixes before passing to providers
validateBareModelId(options.model, 'CursorProvider');
if (!this.cliPath) {
throw this.createError(
CursorErrorCode.NOT_INSTALLED,

View File

@@ -6,7 +6,8 @@
import path from 'path';
import * as secureFs from '../lib/secure-fs.js';
import type { EventEmitter } from '../lib/events.js';
import type { ExecuteOptions, ThinkingLevel } from '@automaker/types';
import type { ExecuteOptions, ThinkingLevel, ReasoningEffort } from '@automaker/types';
import { stripProviderPrefix } from '@automaker/types';
import {
readImageAsBase64,
buildPromptWithImages,
@@ -59,6 +60,7 @@ interface Session {
workingDirectory: string;
model?: string;
thinkingLevel?: ThinkingLevel; // Thinking level for Claude models
reasoningEffort?: ReasoningEffort; // Reasoning effort for Codex models
sdkSessionId?: string; // Claude SDK session ID for conversation continuity
promptQueue: QueuedPrompt[]; // Queue of prompts to auto-run after current task
}
@@ -148,6 +150,7 @@ export class AgentService {
imagePaths,
model,
thinkingLevel,
reasoningEffort,
}: {
sessionId: string;
message: string;
@@ -155,6 +158,7 @@ export class AgentService {
imagePaths?: string[];
model?: string;
thinkingLevel?: ThinkingLevel;
reasoningEffort?: ReasoningEffort;
}) {
const session = this.sessions.get(sessionId);
if (!session) {
@@ -167,7 +171,7 @@ export class AgentService {
throw new Error('Agent is already processing a message');
}
// Update session model and thinking level if provided
// Update session model, thinking level, and reasoning effort if provided
if (model) {
session.model = model;
await this.updateSession(sessionId, { model });
@@ -175,6 +179,9 @@ export class AgentService {
if (thinkingLevel !== undefined) {
session.thinkingLevel = thinkingLevel;
}
if (reasoningEffort !== undefined) {
session.reasoningEffort = reasoningEffort;
}
// Validate vision support before processing images
const effectiveModel = model || session.model;
@@ -284,8 +291,9 @@ export class AgentService {
: baseSystemPrompt;
// Build SDK options using centralized configuration
// Use thinking level from request, or fall back to session's stored thinking level
// Use thinking level and reasoning effort from request, or fall back to session's stored values
const effectiveThinkingLevel = thinkingLevel ?? session.thinkingLevel;
const effectiveReasoningEffort = reasoningEffort ?? session.reasoningEffort;
const sdkOptions = createChatOptions({
cwd: effectiveWorkDir,
model: model,
@@ -342,13 +350,17 @@ export class AgentService {
}
}
// Get provider for this model
// Get provider for this model (with prefix)
const provider = ProviderFactory.getProviderForModel(effectiveModel);
// Strip provider prefix - providers should receive bare model IDs
const bareModel = stripProviderPrefix(effectiveModel);
// Build options for provider
const options: ExecuteOptions = {
prompt: '', // Will be set below based on images
model: effectiveModel,
model: bareModel, // Bare model ID (e.g., "gpt-5.1-codex-max", "composer-1")
originalModel: effectiveModel, // Original with prefix for logging (e.g., "codex-gpt-5.1-codex-max")
cwd: effectiveWorkDir,
systemPrompt: sdkOptions.systemPrompt,
maxTurns: maxTurns,
@@ -359,6 +371,8 @@ export class AgentService {
sdkSessionId: session.sdkSessionId, // Pass SDK session ID for resuming
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined, // Pass MCP servers configuration
agents: customSubagents, // Pass custom subagents for task delegation
thinkingLevel: effectiveThinkingLevel, // Pass thinking level for Claude models
reasoningEffort: effectiveReasoningEffort, // Pass reasoning effort for Codex models
};
// Build prompt content with images

View File

@@ -1,6 +1,9 @@
import * as os from 'os';
import { findCodexCliPath } from '@automaker/platform';
import { checkCodexAuthentication } from '../lib/codex-auth.js';
import { spawnProcess } from '@automaker/platform';
import * as fs from 'fs';
import * as path from 'path';
export interface CodexRateLimitWindow {
limit: number;
@@ -32,11 +35,10 @@ export interface CodexUsageData {
/**
* Codex Usage Service
*
* Unlike Claude Code CLI which provides a `/usage` command, Codex CLI
* does not expose usage statistics directly. This service returns a
* clear message explaining this limitation.
*
* Future enhancement: Could query OpenAI API headers for rate limit info.
* Attempts to fetch usage data from Codex CLI and OpenAI API.
* Codex CLI doesn't provide a direct usage command, but we can:
* 1. Parse usage info from error responses (rate limit errors contain plan info)
* 2. Check for OpenAI API usage if API key is available
*/
export class CodexUsageService {
private codexBinary = 'codex';
@@ -47,8 +49,6 @@ export class CodexUsageService {
* Check if Codex CLI is available on the system
*/
async isAvailable(): Promise<boolean> {
// Prefer our platform-aware resolver over `which/where` because the server
// process PATH may not include npm global bins (nvm/fnm/volta/pnpm).
this.cachedCliPath = await findCodexCliPath();
return Boolean(this.cachedCliPath);
}
@@ -56,31 +56,241 @@ export class CodexUsageService {
/**
* Attempt to fetch usage data
*
* Note: Codex CLI doesn't provide usage statistics like Claude Code does.
* This method returns an error explaining this limitation.
* Tries multiple approaches:
* 1. Check for OpenAI API key in environment
* 2. Make a test request to capture rate limit headers
* 3. Parse usage info from error responses
*/
async fetchUsageData(): Promise<CodexUsageData> {
// Check authentication status first
const isAuthenticated = await this.checkAuthentication();
const cliPath = this.cachedCliPath || (await findCodexCliPath());
if (!isAuthenticated) {
throw new Error("Codex is not authenticated. Please run 'codex login' to authenticate.");
if (!cliPath) {
throw new Error('Codex CLI not found. Please install it with: npm install -g @openai/codex');
}
// Codex CLI doesn't provide a usage command
// Return an error that will be caught and displayed
// Check if user has an API key that we can use
const hasApiKey = !!process.env.OPENAI_API_KEY;
if (hasApiKey) {
// Try to get usage from OpenAI API
const openaiUsage = await this.fetchOpenAIUsage();
if (openaiUsage) {
return openaiUsage;
}
}
// Try to get usage from Codex CLI by making a simple request
const codexUsage = await this.fetchCodexUsage(cliPath);
if (codexUsage) {
return codexUsage;
}
// Fallback: try to parse usage from auth file
const authUsage = await this.fetchFromAuthFile();
if (authUsage) {
return authUsage;
}
// If all else fails, return a message with helpful information
throw new Error(
'Codex usage statistics are not available. Unlike Claude Code, the Codex CLI does not provide a built-in usage command. ' +
'Usage limits are enforced by OpenAI but cannot be queried via the CLI. ' +
'Check your OpenAI dashboard at https://platform.openai.com/usage for detailed usage information.'
'Codex usage statistics require additional configuration. ' +
'To enable usage tracking:\n\n' +
'1. Set your OpenAI API key in the environment:\n' +
' export OPENAI_API_KEY=sk-...\n\n' +
'2. Or check your usage at:\n' +
' https://platform.openai.com/usage\n\n' +
'Note: If using Codex CLI with ChatGPT OAuth authentication, ' +
'usage data must be queried through your OpenAI account.'
);
}
/**
* Try to fetch usage from OpenAI API using the API key
*/
private async fetchOpenAIUsage(): Promise<CodexUsageData | null> {
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) return null;
try {
const endTime = Math.floor(Date.now() / 1000);
const startTime = endTime - 7 * 24 * 60 * 60; // Last 7 days
const response = await fetch(
`https://api.openai.com/v1/organization/usage/completions?start_time=${startTime}&end_time=${endTime}&limit=1`,
{
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
}
);
if (response.ok) {
const data = await response.json();
return this.parseOpenAIUsage(data);
}
} catch (error) {
console.log('[CodexUsage] Failed to fetch from OpenAI API:', error);
}
return null;
}
/**
* Parse OpenAI usage API response
*/
private parseOpenAIUsage(data: any): CodexUsageData {
let totalInputTokens = 0;
let totalOutputTokens = 0;
if (data.data && Array.isArray(data.data)) {
for (const bucket of data.data) {
if (bucket.results && Array.isArray(bucket.results)) {
for (const result of bucket.results) {
totalInputTokens += result.input_tokens || 0;
totalOutputTokens += result.output_tokens || 0;
}
}
}
}
return {
rateLimits: {
planType: 'unknown',
credits: {
hasCredits: true,
},
},
lastUpdated: new Date().toISOString(),
};
}
/**
* Try to fetch usage by making a test request to Codex CLI
* and parsing rate limit information from the response
*/
private async fetchCodexUsage(cliPath: string): Promise<CodexUsageData | null> {
try {
// Make a simple request to trigger rate limit info if at limit
const result = await spawnProcess({
command: cliPath,
args: ['exec', '--', 'echo', 'test'],
cwd: process.cwd(),
env: {
...process.env,
TERM: 'dumb',
},
timeout: 10000,
});
// Parse the output for rate limit information
const combinedOutput = (result.stdout + result.stderr).toLowerCase();
// Check if we got a rate limit error
const rateLimitMatch = combinedOutput.match(
/usage_limit_reached.*?"plan_type":"([^"]+)".*?"resets_at":(\d+).*?"resets_in_seconds":(\d+)/
);
if (rateLimitMatch) {
const planType = rateLimitMatch[1] as CodexPlanType;
const resetsAt = parseInt(rateLimitMatch[2], 10);
const resetsInSeconds = parseInt(rateLimitMatch[3], 10);
return {
rateLimits: {
planType,
primary: {
limit: 0,
used: 0,
remaining: 0,
usedPercent: 100,
windowDurationMins: Math.ceil(resetsInSeconds / 60),
resetsAt,
},
},
lastUpdated: new Date().toISOString(),
};
}
// If no rate limit, return basic info
return {
rateLimits: {
planType: 'plus',
credits: {
hasCredits: true,
unlimited: false,
},
},
lastUpdated: new Date().toISOString(),
};
} catch (error) {
console.log('[CodexUsage] Failed to fetch from Codex CLI:', error);
}
return null;
}
/**
* Try to extract usage info from the Codex auth file
*/
private async fetchFromAuthFile(): Promise<CodexUsageData | null> {
try {
const authFilePath = path.join(os.homedir(), '.codex', 'auth.json');
if (fs.existsSync(authFilePath)) {
const authContent = fs.readFileSync(authFilePath, 'utf-8');
const authData = JSON.parse(authContent);
// Extract plan type from the ID token claims
if (authData.tokens?.id_token) {
const idToken = authData.tokens.id_token;
const claims = this.parseJwt(idToken);
const planType = claims?.['https://chatgpt.com/account_type'] || 'unknown';
const isPlus = planType === 'plus';
return {
rateLimits: {
planType: planType as CodexPlanType,
credits: {
hasCredits: true,
unlimited: !isPlus,
},
},
lastUpdated: new Date().toISOString(),
};
}
}
} catch (error) {
console.log('[CodexUsage] Failed to parse auth file:', error);
}
return null;
}
/**
* Parse JWT token to extract claims
*/
private parseJwt(token: string): any {
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
);
return JSON.parse(jsonPayload);
} catch {
return null;
}
}
/**
* Check if Codex is authenticated
*/
private async checkAuthentication(): Promise<boolean> {
// Use the cached CLI path if available, otherwise fall back to finding it
const cliPath = this.cachedCliPath || (await findCodexCliPath());
const authCheck = await checkCodexAuthentication(cliPath);
return authCheck.authenticated;

View File

@@ -40,6 +40,7 @@ import type { SettingsService } from './settings-service.js';
import type { FeatureLoader } from './feature-loader.js';
import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
import { resolveModelString } from '@automaker/model-resolver';
import { stripProviderPrefix } from '@automaker/types';
const logger = createLogger('IdeationService');
@@ -201,7 +202,7 @@ export class IdeationService {
existingWorkContext
);
// Resolve model alias to canonical identifier
// Resolve model alias to canonical identifier (with prefix)
const modelId = resolveModelString(options?.model ?? 'sonnet');
// Create SDK options
@@ -214,9 +215,13 @@ export class IdeationService {
const provider = ProviderFactory.getProviderForModel(modelId);
// Strip provider prefix - providers need bare model IDs
const bareModel = stripProviderPrefix(modelId);
const executeOptions: ExecuteOptions = {
prompt: message,
model: modelId,
model: bareModel,
originalModel: modelId,
cwd: projectPath,
systemPrompt: sdkOptions.systemPrompt,
maxTurns: 1, // Single turn for ideation
@@ -648,7 +653,7 @@ export class IdeationService {
existingWorkContext
);
// Resolve model alias to canonical identifier
// Resolve model alias to canonical identifier (with prefix)
const modelId = resolveModelString('sonnet');
// Create SDK options
@@ -661,9 +666,13 @@ export class IdeationService {
const provider = ProviderFactory.getProviderForModel(modelId);
// Strip provider prefix - providers need bare model IDs
const bareModel = stripProviderPrefix(modelId);
const executeOptions: ExecuteOptions = {
prompt: prompt.prompt,
model: modelId,
model: bareModel,
originalModel: modelId,
cwd: projectPath,
systemPrompt: sdkOptions.systemPrompt,
maxTurns: 1,