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
56 changes: 34 additions & 22 deletions agents/base2/base2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ export function createBase2(
isFree && 'code-reviewer-lite',
isDefault && 'code-reviewer',
isMax && 'code-reviewer-multi-prompt',
isFree && 'thinker-with-files-gemini',
'thinker-gpt',
'context-pruner',
),
Expand Down Expand Up @@ -144,7 +143,6 @@ Use the spawn_agents tool to spawn specialized agents to help you complete the u
${buildArray(
'- 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.',
isFree && `Spawn the thinker-with-files-gemini agent for complex problems — it's very smart. Skip it for routine edits and clearly-scoped changes. Pass the relevant filePaths since it has no conversation history.`,
isDefault &&
'- Spawn the editor agent to implement the changes after you have gathered all the context you need.',
(isDefault || isMax) &&
Expand Down Expand Up @@ -284,22 +282,40 @@ ${PLACEHOLDER.GIT_CHANGES_PROMPT}
noAskUser,
}),

handleSteps: function* ({ params }) {
while (true) {
// Run context-pruner before each step
yield {
toolName: 'spawn_agent_inline',
input: {
agent_type: 'context-pruner',
params: params ?? {},
},
includeToolCall: false,
} as any

const { stepsComplete } = yield 'STEP'
if (stepsComplete) break
}
},
// handleSteps is serialized via .toString() and re-eval'd, so closure
// variables like `isFree` are not in scope at runtime. Pick the right
// literal-baked function here instead.
handleSteps: isFree
? function* ({ params }) {
while (true) {
yield {
toolName: 'spawn_agent_inline',
input: {
agent_type: 'context-pruner',
params: { ...(params ?? {}), cacheExpiryMs: 10 * 60 * 1000 },
},
includeToolCall: false,
} as any

const { stepsComplete } = yield 'STEP'
if (stepsComplete) break
}
}
: function* ({ params }) {
while (true) {
yield {
toolName: 'spawn_agent_inline',
input: {
agent_type: 'context-pruner',
params: params ?? {},
},
includeToolCall: false,
} as any

const { stepsComplete } = yield 'STEP'
if (stepsComplete) break
}
},
}
}

Expand Down Expand Up @@ -336,8 +352,6 @@ ${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.`,
isFree &&
`- For complex problems, spawn the thinker-with-files-gemini agent after gathering context. Skip it for routine edits and clearly-scoped changes. Pass the relevant filePaths.`,
(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 @@ -382,8 +396,6 @@ function buildImplementationStepPrompt({
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.',
isFree &&
`Spawn the thinker-with-files-gemini agent for complex problems, not routine edits. Pass the relevant filePaths.`,
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
18 changes: 16 additions & 2 deletions agents/context-pruner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const definition: AgentDefinition = {
userBudget: {
type: 'number',
},
cacheExpiryMs: {
type: 'number',
},
},
required: [],
},
Expand Down Expand Up @@ -74,8 +77,8 @@ const definition: AgentDefinition = {
/** Fudge factor for token count threshold to trigger pruning earlier */
const TOKEN_COUNT_FUDGE_FACTOR = 1_000

/** Prompt cache expiry time (Anthropic caches for 5 minutes) */
const CACHE_EXPIRY_MS = 5 * 60 * 1000
/** Prompt cache expiry time (Anthropic caches for 5 minutes by default) */
const CACHE_EXPIRY_MS: number = params?.cacheExpiryMs ?? 5 * 60 * 1000

/** Header used in conversation summaries */
const SUMMARY_HEADER =
Expand Down Expand Up @@ -328,6 +331,17 @@ const definition: AgentDefinition = {
currentMessages.splice(lastSubagentSpawnIndex, 1)
}

// Also remove the params USER_PROMPT if params were provided to this agent
// (this is the message like <user_message>{"cacheExpiryMs": 600000}</user_message>)
if (params && Object.keys(params).length > 0) {
const lastUserPromptIndex = currentMessages.findLastIndex((message) =>
message.tags?.includes('USER_PROMPT'),
)
if (lastUserPromptIndex !== -1) {
currentMessages.splice(lastUserPromptIndex, 1)
}
}

// Check for prompt cache miss (>5 min gap before the USER_PROMPT message)
// The USER_PROMPT is the actual user message; INSTRUCTIONS_PROMPT comes after it
// We need to find the USER_PROMPT and check the gap between it and the last assistant message
Expand Down
176 changes: 176 additions & 0 deletions cli/release-staging/http.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
const http = require('http')
const https = require('https')
const tls = require('tls')

function createReleaseHttpClient({
env = process.env,
userAgent,
requestTimeout,
httpModule = http,
httpsModule = https,
tlsModule = tls,
}) {
function getProxyUrl() {
return (
env.HTTPS_PROXY ||
env.https_proxy ||
env.HTTP_PROXY ||
env.http_proxy ||
null
)
}

function shouldBypassProxy(hostname) {
const noProxy = env.NO_PROXY || env.no_proxy || ''
if (!noProxy) return false

const domains = noProxy
.split(',')
.map((domain) => domain.trim().toLowerCase().replace(/:\d+$/, ''))
const host = hostname.toLowerCase()

return domains.some((domain) => {
if (domain === '*') return true
if (domain.startsWith('.')) {
return host.endsWith(domain) || host === domain.slice(1)
}
return host === domain || host.endsWith(`.${domain}`)
})
}

function connectThroughProxy(proxyUrl, targetHost, targetPort) {
return new Promise((resolve, reject) => {
const proxy = new URL(proxyUrl)
const isHttpsProxy = proxy.protocol === 'https:'
const connectOptions = {
hostname: proxy.hostname,
port: proxy.port || (isHttpsProxy ? 443 : 80),
method: 'CONNECT',
path: `${targetHost}:${targetPort}`,
headers: {
Host: `${targetHost}:${targetPort}`,
},
}

if (proxy.username || proxy.password) {
const auth = Buffer.from(
`${decodeURIComponent(proxy.username || '')}:${decodeURIComponent(
proxy.password || '',
)}`,
).toString('base64')
connectOptions.headers['Proxy-Authorization'] = `Basic ${auth}`
}

const transport = isHttpsProxy ? httpsModule : httpModule
const req = transport.request(connectOptions)

req.on('connect', (res, socket) => {
if (res.statusCode === 200) {
resolve(socket)
return
}

socket.destroy()
reject(new Error(`Proxy CONNECT failed with status ${res.statusCode}`))
})

req.on('error', (error) => {
reject(new Error(`Proxy connection failed: ${error.message}`))
})

req.setTimeout(requestTimeout, () => {
req.destroy()
reject(new Error('Proxy connection timeout.'))
})

req.end()
})
}

async function buildRequestOptions(url, options = {}) {
const parsedUrl = new URL(url)
const reqOptions = {
hostname: parsedUrl.hostname,
port: parsedUrl.port || 443,
path: parsedUrl.pathname + parsedUrl.search,
headers: {
'User-Agent': userAgent,
...options.headers,
},
}

const proxyUrl = getProxyUrl()
if (!proxyUrl || shouldBypassProxy(parsedUrl.hostname)) {
return reqOptions
}

const tunnelSocket = await connectThroughProxy(
proxyUrl,
parsedUrl.hostname,
parsedUrl.port || 443,
)

class TunnelAgent extends httpsModule.Agent {
createConnection(_options, callback) {
const secureSocket = tlsModule.connect({
socket: tunnelSocket,
servername: parsedUrl.hostname,
})

if (typeof callback === 'function') {
if (typeof secureSocket.once === 'function') {
let settled = false
const finish = (error) => {
if (settled) return
settled = true
callback(error || null, error ? undefined : secureSocket)
}

secureSocket.once('secureConnect', () => finish(null))
secureSocket.once('error', (error) => finish(error))
} else {
callback(null, secureSocket)
}
}

return secureSocket
}
}

reqOptions.agent = new TunnelAgent({ keepAlive: false })
return reqOptions
}

async function httpGet(url, options = {}) {
const reqOptions = await buildRequestOptions(url, options)

return new Promise((resolve, reject) => {
const req = httpsModule.get(reqOptions, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
res.resume()
httpGet(new URL(res.headers.location, url).href, options)
.then(resolve)
.catch(reject)
return
}

resolve(res)
})

req.on('error', reject)
req.setTimeout(options.timeout || requestTimeout, () => {
req.destroy()
reject(new Error('Request timeout.'))
})
})
}

return {
getProxyUrl,
httpGet,
}
}

module.exports = {
createReleaseHttpClient,
}
Loading
Loading