Skip to content

Commit

Permalink
Merge branch 'main' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
PriNova committed Feb 9, 2025
2 parents bda2e5f + 25a431a commit 9835797
Show file tree
Hide file tree
Showing 41 changed files with 375 additions and 1,146 deletions.
Empty file.
11 changes: 8 additions & 3 deletions lib/shared/src/models/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ export function getExperimentalClientModelByFeatureFlag(flag: FeatureFlag): Serv
}
}

export const DeepCodyAgentID = 'deep-cody'

function getDeepCodyServerModel(): ServerModel {
return {
// This modelRef does not exist in the backend and is used to identify the model in the client.
modelRef: 'sourcegraph::2023-06-01::deep-cody',
displayName: 'Agentic chat',
modelName: 'deep-cody',
modelName: DeepCodyAgentID,
capabilities: ['chat'],
category: 'accuracy',
status: 'experimental' as ModelTag.Experimental,
Expand All @@ -33,10 +35,13 @@ function getDeepCodyServerModel(): ServerModel {
}
}

export const ToolCodyModelRef = 'sourcegraph::2024-12-31::tool-cody'
export const ToolCodyModelName = 'tool-cody'

export const TOOL_CODY_MODEL: ServerModel = {
modelRef: 'sourcegraph::2024-12-31::tool-cody',
modelRef: ToolCodyModelRef,
displayName: 'Tool Cody',
modelName: 'tool-cody',
modelName: ToolCodyModelName,
capabilities: ['chat'],
category: 'accuracy',
status: 'internal' as ModelTag.Internal,
Expand Down
4 changes: 2 additions & 2 deletions lib/shared/src/models/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { RestClient } from '../sourcegraph-api/rest/client'
import type { UserProductSubscription } from '../sourcegraph-api/userProductSubscription'
import { CHAT_INPUT_TOKEN_BUDGET } from '../token/constants'
import { isError } from '../utils'
import { TOOL_CODY_MODEL, getExperimentalClientModelByFeatureFlag } from './client'
import { TOOL_CODY_MODEL, ToolCodyModelName, getExperimentalClientModelByFeatureFlag } from './client'
import { type Model, type ServerModel, createModel, createModelFromServerModel } from './model'
import type {
DefaultsAndUserPreferencesForEndpoint,
Expand Down Expand Up @@ -265,7 +265,7 @@ export function syncModels({
}

const hasToolCody = data.primaryModels.some(m =>
m.id.includes('tool-cody')
m.id.includes(ToolCodyModelName)
)
if (!hasToolCody && isToolCodyEnabled) {
clientModels.push(TOOL_CODY_MODEL)
Expand Down
5 changes: 3 additions & 2 deletions vscode/src/chat/agentic/CodyToolProvider.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type ContextItem, ContextItemSource, ps } from '@sourcegraph/cody-shared'
import { DeepCodyAgentID } from '@sourcegraph/cody-shared/src/models/client'
import { Observable } from 'observable-fns'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { URI } from 'vscode-uri'
Expand Down Expand Up @@ -91,7 +92,7 @@ describe('CodyToolProvider', () => {

it('should not include CLI tool if shell is disabled', () => {
vi.spyOn(toolboxManager, 'getSettings').mockReturnValue({
agent: { name: 'deep-cody' },
agent: { name: DeepCodyAgentID },
shell: { enabled: false },
})
const tools = CodyToolProvider.getTools()
Expand All @@ -100,7 +101,7 @@ describe('CodyToolProvider', () => {

it('should include CLI tool if shell is enabled', () => {
vi.spyOn(toolboxManager, 'getSettings').mockReturnValue({
agent: { name: 'deep-cody' },
agent: { name: DeepCodyAgentID },
shell: { enabled: true },
})
const tools = CodyToolProvider.getTools()
Expand Down
3 changes: 2 additions & 1 deletion vscode/src/chat/agentic/DeepCody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
telemetryRecorder,
wrapInActiveSpan,
} from '@sourcegraph/cody-shared'
import { DeepCodyAgentID } from '@sourcegraph/cody-shared/src/models/client'
import { getContextFromRelativePath } from '../../commands/context/file-path'
import { forkSignal } from '../../completions/utils'
import { getCategorizedMentions, isUserAddedItem } from '../../prompt-builder/utils'
Expand Down Expand Up @@ -164,7 +165,7 @@ export class DeepCodyAgent {
requestID,
model: DeepCodyAgent.model,
traceId: span.spanContext().traceId,
chatAgent: 'deep-cody',
chatAgent: DeepCodyAgentID,
},
metadata: {
loop: this.stats.loop, // Number of loops run.
Expand Down
3 changes: 2 additions & 1 deletion vscode/src/chat/agentic/ToolboxManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
startWith,
userProductSubscription,
} from '@sourcegraph/cody-shared'
import { DeepCodyAgentID } from '@sourcegraph/cody-shared/src/models/client'
import { type Observable, Subject, map } from 'observable-fns'
import { env } from 'vscode'
import { DeepCodyAgent } from './DeepCody'
Expand Down Expand Up @@ -51,7 +52,7 @@ class ToolboxManager {
const shellError = this.getFeatureError('shell')
// TODO: Remove hard-coded agent once we have a proper agentic chat selection UI
return {
agent: { name: this.isRateLimited ? undefined : 'deep-cody' },
agent: { name: this.isRateLimited ? undefined : DeepCodyAgentID },
shell: {
enabled: shellError === undefined,
error: shellError,
Expand Down
42 changes: 29 additions & 13 deletions vscode/src/chat/chat-view/ChatController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ import { type Span, context } from '@opentelemetry/api'
import { captureException } from '@sentry/core'
import type { SubMessage } from '@sourcegraph/cody-shared/src/chat/transcript/messages'
import { resolveAuth } from '@sourcegraph/cody-shared/src/configuration/auth-resolver'
import {
DeepCodyAgentID,
ToolCodyModelName,
ToolCodyModelRef,
} from '@sourcegraph/cody-shared/src/models/client'
import type { TelemetryEventParameters } from '@sourcegraph/telemetry'
import { Subject, map } from 'observable-fns'
import type { URI } from 'vscode-uri'
Expand Down Expand Up @@ -289,6 +294,8 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv
break
}
case 'edit': {
await this.cancelSubmitOrEditOperation()

await this.handleEdit({
requestID: uuid.v4(),
text: PromptString.unsafe_fromUserQuery(message.text),
Expand Down Expand Up @@ -477,7 +484,10 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv
let auth: AuthCredentials | undefined = undefined

if (token) {
auth = { credentials: { token, source: 'paste' }, serverEndpoint: endpoint }
auth = {
credentials: { token, source: 'paste' },
serverEndpoint: endpoint,
}
} else {
const { configuration } = await currentResolvedConfig()
auth = await resolveAuth(endpoint, configuration, secretStorage)
Expand Down Expand Up @@ -702,7 +712,10 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv
firstResultFromOperation(ChatBuilder.resolvedModelForChat(this.chatBuilder))
)
this.chatBuilder.setSelectedModel(model)
const selectedAgent = model?.includes('deep-cody') ? 'deep-cody' : undefined
let selectedAgent = model?.includes(DeepCodyAgentID) ? DeepCodyAgentID : undefined
if (model?.includes(ToolCodyModelName)) {
selectedAgent = ToolCodyModelRef
}

this.chatBuilder.addHumanMessage({
text: inputText,
Expand Down Expand Up @@ -827,7 +840,10 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv
}

this.chatBuilder.setSelectedModel(model)
const chatAgent = model.includes('deep-cody') ? 'deep-cody' : undefined
let chatAgent = model.includes(DeepCodyAgentID) ? DeepCodyAgentID : undefined
if (model.includes(ToolCodyModelName)) {
chatAgent = ToolCodyModelRef
}

const recorder = await OmniboxTelemetry.create({
requestID,
Expand Down Expand Up @@ -931,7 +947,11 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv

// Now that we have the confirmation, proceed based on the user's choice

this.postViewTranscript({ speaker: 'assistant', processes: [step], model })
this.postViewTranscript({
speaker: 'assistant',
processes: [step],
model,
})
return confirmation
},
postDone: (op?: { abort: boolean }): void => {
Expand Down Expand Up @@ -1098,16 +1118,12 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv
}

private submitOrEditOperation: AbortController | undefined
public startNewSubmitOrEditOperation(): Promise<AbortSignal> {
public startNewSubmitOrEditOperation(): AbortSignal {
this.submitOrEditOperation?.abort()

return new Promise(resolve => {
setTimeout(() => {
this.submitOrEditOperation = new AbortController()
resolve(this.submitOrEditOperation.signal)
}, 500)
})
this.submitOrEditOperation = new AbortController()
return this.submitOrEditOperation.signal
}

private cancelSubmitOrEditOperation(): Promise<void> {
if (this.submitOrEditOperation) {
this.submitOrEditOperation.abort()
Expand Down Expand Up @@ -1239,7 +1255,7 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv
preDetectedIntentScores?: { intent: string; score: number }[] | undefined | null
manuallySelectedIntent?: ChatMessage['intent'] | undefined | null
}): Promise<void> {
const abortSignal = await this.startNewSubmitOrEditOperation()
const abortSignal = this.startNewSubmitOrEditOperation()

telemetryRecorder.recordEvent('cody.editChatButton', 'clicked', {
billingMetadata: {
Expand Down
148 changes: 148 additions & 0 deletions vscode/src/chat/chat-view/handlers/ToolHandler.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { spawn } from 'node:child_process'
import type { SpawnOptions } from 'node:child_process'
import type Anthropic from '@anthropic-ai/sdk'
import type { ContentBlock, MessageParam, Tool, ToolResultBlockParam } from '@anthropic-ai/sdk/resources'
import { ProcessType, PromptString } from '@sourcegraph/cody-shared'
Expand Down Expand Up @@ -52,6 +54,44 @@ const allTools: CodyTool[] = [
}
},
},
{
spec: {
name: 'run_terminal_command',
description: 'Run an arbitrary terminal command at the root of the users project. ',
input_schema: {
type: 'object',
properties: {
command: {
type: 'string',
description:
'The command to run in the root of the users project. Must be shell escaped.',
},
},
required: ['command'],
},
},
invoke: async (input: { command: string }) => {
if (typeof input.command !== 'string') {
throw new Error(
`run_terminal_command argument must be a string, value was ${JSON.stringify(input)}`
)
}

const workspaceFolder = vscode.workspace.workspaceFolders?.[0]
if (!workspaceFolder) {
throw new Error('No workspace folder found')
}

try {
const commandResult = await runShellCommand(input.command, {
cwd: workspaceFolder.uri.path,
})
return commandResult.stdout
} catch (error) {
throw new Error(`Failed to run terminal command: ${input.command}: ${error}`)
}
},
},
]

export class ExperimentalToolHandler implements AgentHandler {
Expand Down Expand Up @@ -169,3 +209,111 @@ export class ExperimentalToolHandler implements AgentHandler {
delegate.postDone()
}
}

interface CommandOptions {
cwd?: string
env?: Record<string, string>
}

interface CommandResult {
stdout: string
stderr: string
code: number | null
signal: NodeJS.Signals | null
}

class CommandError extends Error {
constructor(
message: string,
public readonly result: CommandResult
) {
super(message)
this.name = 'CommandError'
}
}

async function runShellCommand(command: string, options: CommandOptions = {}): Promise<CommandResult> {
const { cwd = process.cwd(), env = process.env } = options

const timeout = 10_000
const maxBuffer = 1024 * 1024 * 10
const encoding = 'utf8'
const spawnOptions: SpawnOptions = {
shell: true,
cwd,
env,
windowsHide: true,
}

return new Promise((resolve, reject) => {
const process = spawn(command, [], spawnOptions)

let stdout = ''
let stderr = ''
let killed = false
const timeoutId = setTimeout(() => {
killed = true
process.kill()
reject(new Error(`Command timed out after ${timeout}ms`))
}, timeout)

let stdoutLength = 0
let stderrLength = 0

if (process.stdout) {
process.stdout.on('data', (data: Buffer) => {
const chunk = data.toString(encoding)
stdoutLength += chunk.length
if (stdoutLength > maxBuffer) {
killed = true
process.kill()
reject(new Error('stdout maxBuffer exceeded'))
return
}
stdout += chunk
})
}

if (process.stderr) {
process.stderr.on('data', (data: Buffer) => {
const chunk = data.toString(encoding)
stderrLength += chunk.length
if (stderrLength > maxBuffer) {
killed = true
process.kill()
reject(new Error('stderr maxBuffer exceeded'))
return
}
stderr += chunk
})
}

process.on('error', (error: Error) => {
if (timeoutId) clearTimeout(timeoutId)
reject(new Error(`Failed to start process: ${error.message}`))
})

process.on('close', (code: number | null, signal: NodeJS.Signals | null) => {
if (timeoutId) clearTimeout(timeoutId)
if (killed) return

const result: CommandResult = {
stdout,
stderr,
code,
signal,
}

if (code === 0) {
resolve(result)
} else {
reject(
new CommandError(
`Command failed with exit code ${code}${stderr ? ': ' + stderr : ''}`,
result
)
)
}
})
})
}
6 changes: 3 additions & 3 deletions vscode/src/chat/chat-view/handlers/registry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Anthropic from '@anthropic-ai/sdk'
import { DeepCodyAgentID, ToolCodyModelRef } from '@sourcegraph/cody-shared/src/models/client'
import { getConfiguration } from '../../../configuration'
import { DeepCodyAgent } from '../../agentic/DeepCody'
import { ChatHandler } from './ChatHandler'
import { DeepCodyHandler } from './DeepCodyHandler'
import { EditHandler } from './EditHandler'
Expand All @@ -20,7 +20,7 @@ function registerAgent(id: string, ctr: (id: string, tools: AgentTools) => Agent

export function getAgent(id: string, modelId: string, tools: AgentTools): AgentHandler {
const { contextRetriever, editor, chatClient } = tools
if (id === DeepCodyAgent.id) {
if (id === DeepCodyAgentID) {
return new DeepCodyHandler(modelId, contextRetriever, editor, chatClient)
}
if (agentRegistry.has(id)) {
Expand All @@ -41,7 +41,7 @@ registerAgent(
(_id: string, { contextRetriever, editor }: AgentTools) =>
new EditHandler('insert', contextRetriever, editor)
)
registerAgent('sourcegraph::2024-12-31::tool-cody', (_id: string) => {
registerAgent(ToolCodyModelRef, (_id: string) => {
const config = getConfiguration()
const anthropicAPI = new Anthropic({
apiKey: config.experimentalMinionAnthropicKey,
Expand Down
Loading

0 comments on commit 9835797

Please sign in to comment.