Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions agents/base2/base2.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { buildArray } from '@codebuff/common/util/array'
import { FREEBUFF_KIMI_MODEL_ID } from '@codebuff/common/constants/freebuff-models'
import {
FREEBUFF_GEMINI_THINKER_AGENT_ID,
FREEBUFF_GEMINI_THINKER_INSTRUCTIONS_PROMPT,
FREEBUFF_GEMINI_THINKER_STEP_PROMPT,
FREEBUFF_GEMINI_THINKER_SYSTEM_INSTRUCTION,
} from '@codebuff/common/constants/freebuff-gemini-thinker'

import { publisher } from '../constants'
import {
Expand Down Expand Up @@ -32,6 +39,7 @@ export function createBase2(
const model =
modelOverride ??
(isFree ? 'moonshotai/kimi-k2.6' : 'anthropic/claude-opus-4.7')
const hasFreeGeminiThinker = isFree && model === FREEBUFF_KIMI_MODEL_ID
const defaultProviderOptions = isFree
? {
data_collection: 'deny' as const,
Expand Down Expand Up @@ -97,6 +105,7 @@ export function createBase2(
isFree && 'code-reviewer-lite',
isDefault && 'code-reviewer',
isMax && 'code-reviewer-multi-prompt',
hasFreeGeminiThinker && FREEBUFF_GEMINI_THINKER_AGENT_ID,
'thinker-gpt',
'context-pruner',
),
Expand Down Expand Up @@ -154,6 +163,7 @@ Use the spawn_agents tool to spawn specialized agents to help you complete the u
'- Spawn context-gathering agents (file pickers, code searchers, and web/docs researchers) before making edits. Use the list_directory and glob tools directly for searching and exploring the codebase.',
isFree &&
'Do not spawn the thinker-gpt agent, unless the user asks. Not everyone has connected their ChatGPT subscription to Codebuff to allow for it.',
hasFreeGeminiThinker && FREEBUFF_GEMINI_THINKER_SYSTEM_INSTRUCTION,
isDefault &&
'- Spawn the editor agent to implement the changes after you have gathered all the context you need.',
(isDefault || isMax) &&
Expand Down Expand Up @@ -280,6 +290,7 @@ ${PLACEHOLDER.GIT_CHANGES_PROMPT}
isDefault,
isMax,
isFree,
hasFreeGeminiThinker,
hasNoValidation,
noAskUser,
}),
Expand All @@ -292,6 +303,7 @@ ${PLACEHOLDER.GIT_CHANGES_PROMPT}
hasNoValidation,
isSonnet,
isFree,
hasFreeGeminiThinker,
noAskUser,
}),

Expand Down Expand Up @@ -340,6 +352,7 @@ function buildImplementationInstructionsPrompt({
isDefault,
isMax,
isFree,
hasFreeGeminiThinker,
hasNoValidation,
noAskUser,
}: {
Expand All @@ -348,6 +361,7 @@ function buildImplementationInstructionsPrompt({
isDefault: boolean
isMax: boolean
isFree: boolean
hasFreeGeminiThinker: boolean
hasNoValidation: boolean
noAskUser: boolean
}) {
Expand All @@ -365,6 +379,7 @@ ${buildArray(
'After getting context on the user request from the codebase or from research, use the ask_user tool to ask the user for important clarifications on their request or alternate implementation strategies. You should skip this step if the choice is obvious -- only ask the user if you need their help making the best choice.',
(isDefault || isMax || isFree) &&
`- For any task requiring 3+ steps, use the write_todos tool to write out your step-by-step implementation plan. Include ALL of the applicable tasks in the list.${isFast ? '' : ' You should include a step to review the changes after you have implemented the changes.'}:${hasNoValidation ? '' : ' You should include at least one step to validate/test your changes: be specific about whether to typecheck, run tests, run lints, etc.'} You may be able to do reviewing and validation in parallel in the same step. Skip write_todos for simple tasks like quick edits or answering questions.`,
hasFreeGeminiThinker && FREEBUFF_GEMINI_THINKER_INSTRUCTIONS_PROMPT,
(isDefault || isMax) &&
`- For quick problems, briefly explain your reasoning to the user. If you need to think longer, write your thoughts within the <think> tags. Finally, for complex problems, spawn the thinker agent to help find the best solution. (gpt-5-agent is a last resort for complex problems)`,
isDefault &&
Expand Down Expand Up @@ -395,6 +410,7 @@ function buildImplementationStepPrompt({
hasNoValidation,
isSonnet,
isFree,
hasFreeGeminiThinker,
noAskUser,
}: {
isDefault: boolean
Expand All @@ -403,12 +419,14 @@ function buildImplementationStepPrompt({
hasNoValidation: boolean
isSonnet: boolean
isFree: boolean
hasFreeGeminiThinker: boolean
noAskUser: boolean
}) {
return buildArray(
isMax &&
`Keep working until the user's request is completely satisfied${!hasNoValidation ? ' and validated' : ''}, or until you require more information from the user.`,
'Consider loading relevant skills with the skill tool if they might help with the current task. Do not reload skills that were already loaded earlier in this conversation.',
hasFreeGeminiThinker && FREEBUFF_GEMINI_THINKER_STEP_PROMPT,
isMax &&
`You must spawn the 'editor-multi-prompt' agent to implement code changes rather than using the str_replace or write_file tools, since it will generate the best code changes.`,
(isDefault || isMax) &&
Expand Down
87 changes: 78 additions & 9 deletions cli/src/__tests__/integration/local-agents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import path from 'path'

import { validateAgents } from '@codebuff/sdk'
import {
describe,
test,
expect,
beforeEach,
afterEach,
mock,
} from 'bun:test'
FREEBUFF_GEMINI_THINKER_AGENT_ID,
FREEBUFF_GEMINI_THINKER_INSTRUCTIONS_PROMPT,
FREEBUFF_GEMINI_THINKER_STEP_PROMPT,
FREEBUFF_GEMINI_THINKER_SYSTEM_INSTRUCTION,
} from '@codebuff/common/constants/freebuff-gemini-thinker'
import {
FREEBUFF_KIMI_MODEL_ID,
FREEBUFF_MINIMAX_MODEL_ID,
} from '@codebuff/common/constants/freebuff-models'
import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'

// Mock the logger to prevent analytics initialization errors in tests
mock.module('../../utils/logger', () => ({
Expand All @@ -27,6 +30,7 @@ import { setProjectRoot, getProjectRoot } from '../../project-files'
import {
loadAgentDefinitions,
loadLocalAgents,
configureFreebuffBaseAgentForModel,
initializeAgentRegistry,
findAgentsDirectory,
getLoadedAgentsData,
Expand All @@ -37,6 +41,67 @@ import {

const MODEL_NAME = 'anthropic/claude-sonnet-4'

describe('configureFreebuffBaseAgentForModel', () => {
const makeBase2Free = () => ({
id: 'base2-free',
spawnableAgents: ['file-picker', FREEBUFF_GEMINI_THINKER_AGENT_ID],
systemPrompt: [
'before',
FREEBUFF_GEMINI_THINKER_SYSTEM_INSTRUCTION,
'after',
].join('\n'),
instructionsPrompt: [
'before',
FREEBUFF_GEMINI_THINKER_INSTRUCTIONS_PROMPT,
'after',
].join('\n'),
stepPrompt: ['before', FREEBUFF_GEMINI_THINKER_STEP_PROMPT, 'after'].join(
'\n',
),
})

test('keeps the Gemini thinker and prompt guidance for Kimi', () => {
const definition = makeBase2Free()

configureFreebuffBaseAgentForModel(definition, FREEBUFF_KIMI_MODEL_ID)

expect(definition.spawnableAgents).toContain(
FREEBUFF_GEMINI_THINKER_AGENT_ID,
)
expect(definition.systemPrompt).toContain(
FREEBUFF_GEMINI_THINKER_SYSTEM_INSTRUCTION,
)
expect(definition.instructionsPrompt).toContain(
FREEBUFF_GEMINI_THINKER_INSTRUCTIONS_PROMPT,
)
expect(definition.stepPrompt).toContain(FREEBUFF_GEMINI_THINKER_STEP_PROMPT)
})

test('removes only exact Gemini thinker prompt guidance for MiniMax', () => {
const definition = makeBase2Free()
definition.systemPrompt +=
'\nUser text mentioning thinker-with-files-gemini should stay.'

configureFreebuffBaseAgentForModel(definition, FREEBUFF_MINIMAX_MODEL_ID)

expect(definition.spawnableAgents).not.toContain(
FREEBUFF_GEMINI_THINKER_AGENT_ID,
)
expect(definition.systemPrompt).not.toContain(
FREEBUFF_GEMINI_THINKER_SYSTEM_INSTRUCTION,
)
expect(definition.instructionsPrompt).not.toContain(
FREEBUFF_GEMINI_THINKER_INSTRUCTIONS_PROMPT,
)
expect(definition.stepPrompt).not.toContain(
FREEBUFF_GEMINI_THINKER_STEP_PROMPT,
)
expect(definition.systemPrompt).toContain(
'User text mentioning thinker-with-files-gemini should stay.',
)
})
})

const writeAgentFile = (
agentsDir: string,
fileName: string,
Expand Down Expand Up @@ -408,7 +473,9 @@ describe('Local Agent Integration', () => {
expect(uiAgent!.id).toBe('test-ui-agent')
// File path should be populated for "Open file" UI links
// Use realpathSync to normalize paths (on macOS, /var is a symlink to /private/var)
expect(realpathSync(uiAgent!.filePath!)).toBe(realpathSync(path.join(agentsDir, 'ui-agent.ts')))
expect(realpathSync(uiAgent!.filePath!)).toBe(
realpathSync(path.join(agentsDir, 'ui-agent.ts')),
)
})

test('loadLocalAgents sorts agents alphabetically by displayName', async () => {
Expand Down Expand Up @@ -735,7 +802,9 @@ describe('Local Agent Integration', () => {
const data = getLoadedAgentsData()
expect(data).not.toBeNull()
expect(data!.agents.some((a) => a.id === 'test-announce-agent')).toBe(true)
expect(data!.agents.some((a) => a.displayName === 'Announce Test Agent')).toBe(true)
expect(
data!.agents.some((a) => a.displayName === 'Announce Test Agent'),
).toBe(true)
})

// ============================================================================
Expand Down
22 changes: 3 additions & 19 deletions cli/src/components/freebuff-model-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { Button } from './button'
import {
FALLBACK_FREEBUFF_MODEL_ID,
FREEBUFF_GEMINI_PRO_MODEL_ID,
FREEBUFF_KIMI_MODEL_ID,
FREEBUFF_MODELS,
getFreebuffDeploymentAvailabilityLabel,
Expand All @@ -23,15 +22,8 @@ import { nextFreebuffModelId } from '../utils/freebuff-model-navigation'
import type { KeyEvent } from '@opentui/core'

const FREEBUFF_MODEL_SELECTOR_MODELS = [
...FREEBUFF_MODELS.filter(
(model) => model.id === FREEBUFF_GEMINI_PRO_MODEL_ID,
),
...FREEBUFF_MODELS.filter((model) => model.id === FREEBUFF_KIMI_MODEL_ID),
...FREEBUFF_MODELS.filter(
(model) =>
model.id !== FREEBUFF_GEMINI_PRO_MODEL_ID &&
model.id !== FREEBUFF_KIMI_MODEL_ID,
),
...FREEBUFF_MODELS.filter((model) => model.id !== FREEBUFF_KIMI_MODEL_ID),
]

/**
Expand Down Expand Up @@ -121,13 +113,7 @@ export const FreebuffModelSelector: React.FC = () => {
// when the user's selection moves between queues. The tagline is shown
// inline with the name now, so it's no longer part of this slot.
const hintWidth = useMemo(
() =>
Math.max(
'No wait'.length,
'999 ahead'.length,
'Used today'.length,
'Limit used'.length,
),
() => Math.max('No wait'.length, '999 ahead'.length, 'Limit used'.length),
[],
)

Expand Down Expand Up @@ -267,9 +253,7 @@ export const FreebuffModelSelector: React.FC = () => {
const hint = !isAvailable
? 'Closed'
: isQuotaExhausted
? model.id === FREEBUFF_GEMINI_PRO_MODEL_ID
? 'Used today'
: 'Limit used'
? 'Limit used'
: ahead === undefined
? ''
: ahead === 0
Expand Down
34 changes: 21 additions & 13 deletions cli/src/hooks/use-send-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { createStreamController } from './stream-state'
import { useChatStore } from '../state/chat-store'
import { getFreebuffInstanceId } from './use-freebuff-session'
import { getCodebuffClient } from '../utils/codebuff-client'
import { AGENT_MODE_TO_ID, AGENT_MODE_TO_COST_MODE, IS_FREEBUFF } from '../utils/constants'
import {
AGENT_MODE_TO_ID,
AGENT_MODE_TO_COST_MODE,
IS_FREEBUFF,
} from '../utils/constants'
import { createEventHandlerState } from '../utils/create-event-handler-state'
import { createRunConfig } from '../utils/create-run-config'
import { loadAgentDefinitions } from '../utils/local-agent-registry'
Expand Down Expand Up @@ -108,7 +112,7 @@ export const useSendMessage = ({
onBeforeMessageSend,
mainAgentTimer,
scrollToLatest,
onTimerEvent = () => { },
onTimerEvent = () => {},
isQueuePausedRef,
isProcessingQueueRef,
resumeQueue,
Expand Down Expand Up @@ -295,13 +299,13 @@ export const useSendMessage = ({
const errorsToAttach =
validationResult.errors.length === 0
? [
// Hide this for now, as validate endpoint may be flaky and we don't want to bother users.
// {
// id: NETWORK_ERROR_ID,
// message:
// 'Agent validation failed. This may be due to a network issue or temporary server problem. Please try again.',
// },
]
// Hide this for now, as validate endpoint may be flaky and we don't want to bother users.
// {
// id: NETWORK_ERROR_ID,
// message:
// 'Agent validation failed. This may be due to a network issue or temporary server problem. Please try again.',
// },
]
: validationResult.errors

setMessages((prev) =>
Expand Down Expand Up @@ -457,12 +461,16 @@ export const useSendMessage = ({
eventHandlerState,
signal: abortController.signal,
costMode: AGENT_MODE_TO_COST_MODE[agentMode],
extraCodebuffMetadata: freebuffInstanceId
? { freebuff_instance_id: freebuffInstanceId }
: undefined,
extraCodebuffMetadata:
IS_FREEBUFF && freebuffInstanceId
? { freebuff_instance_id: freebuffInstanceId }
: undefined,
})

logger.info({ runConfig }, '[send-message] Sending message with sdk run config')
logger.info(
{ runConfig },
'[send-message] Sending message with sdk run config',
)
const runState = await client.run(runConfig)

// Finalize: persist state and mark complete
Expand Down
Loading
Loading