Skip to content

Commit

Permalink
feat(agentic context): add agentic context component (#6598)
Browse files Browse the repository at this point in the history
CLOSE https://linear.app/sourcegraph/issue/CODY-4634 &&
https://linear.app/sourcegraph/issue/CODY-4688

Implemented https://www.figma.com/design/ttPjrGfWIEvPwIlPw8FwG1

- Moved ProcessList into an independent component called
`AgenticChatCell` to display the status of agentic chat processing
- Includes indicators for pending, successful, and failed processes
- Integrates the new cell into the `Transcript` component
- Adds Storybook stories to showcase the different states of the cell

More changes merged from #6634

### Latest

Updated copies from aravind


![image](https://github.com/user-attachments/assets/730a7dba-b1b6-496e-92b4-584364481d69)


![image](https://github.com/user-attachments/assets/e72a5c55-8bc3-4c32-a6a7-c1907db4031d)


![image](https://github.com/user-attachments/assets/5bc4fb83-2d21-4de6-abdc-913032cd1d1e)


### Outdated

<img width="641" alt="image"
src="https://github.com/user-attachments/assets/32ba7197-3686-409f-af6b-016b4b9df9f2"
/>

<img width="626" alt="image"
src="https://github.com/user-attachments/assets/2b2df982-dccc-442a-b7c0-4888bb02eff6"
/>

<img width="624" alt="image"
src="https://github.com/user-attachments/assets/6abb1071-a987-43c9-b630-6ffc9bbd4f4a"
/>

## Test plan

<!-- Required. See
https://docs-legacy.sourcegraph.com/dev/background-information/testing_principles.
-->

UI update - Updated storybook

---------

Co-authored-by: Ara <arafat.da.khan@gmail.com>
  • Loading branch information
abeatrix and arafatkatze authored Jan 15, 2025
1 parent 8a34b3b commit d3a6f7e
Show file tree
Hide file tree
Showing 29 changed files with 839 additions and 340 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ object Constants {
const val chat = "chat"
const val `class` = "class"
const val `client-managed` = "client-managed"
const val confirmation = "confirmation"
const val `create-file` = "create-file"
const val debug = "debug"
const val default = "default"
Expand Down Expand Up @@ -91,6 +92,7 @@ object Constants {
const val symbol = "symbol"
const val system = "system"
const val terminal = "terminal"
const val tool = "tool"
const val trace = "trace"
const val tree = "tree"
const val `tree-sitter` = "tree-sitter"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@file:Suppress("FunctionName", "ClassName", "unused", "EnumEntryName", "UnusedImport")
package com.sourcegraph.cody.agent.protocol_generated;

typealias ProcessType = String // One of: tool, confirmation

Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ package com.sourcegraph.cody.agent.protocol_generated;
import com.google.gson.annotations.SerializedName;

data class ProcessingStep(
val type: ProcessType? = null, // Oneof: tool, confirmation
val id: String,
val title: String? = null,
val description: String? = null,
val content: String,
val status: StatusEnum, // Oneof: pending, success, error
val step: Long? = null,
val state: StateEnum, // Oneof: pending, success, error
val error: ChatError? = null,
) {

enum class StatusEnum {
enum class StateEnum {
@SerializedName("pending") Pending,
@SerializedName("success") Success,
@SerializedName("error") Error,
Expand Down
47 changes: 29 additions & 18 deletions lib/shared/src/chat/transcript/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,44 @@ export interface ChatMessage extends Message {
subMessages?: SubMessage[]
}

export enum ProcessType {
/**
* A process initiated by a tool.
*/
Tool = 'tool',
/**
* A process that prompts the user confirmation.
*/
Confirmation = 'confirmation',
}

/**
* Represents an individual step in a chat message processing pipeline, typically used
* to track and display the progress of context fetching and analysis operations.
*/
export interface ProcessingStep {
/**
* The type of the step
*/
type?: ProcessType | undefined | null

/**
* Unique identifier or name for the processing step
*/
id: string

/**
* Description of what the step is doing or has completed
* The title of the step
*/
title?: string | undefined | null

/**
* Description of the step
*/
description?: string | undefined | null

/**
* Content for the step
*/
content: string

Expand All @@ -78,18 +104,12 @@ export interface ProcessingStep {
* - 'success': Step completed successfully
* - 'error': Step failed to complete
*/
status: 'pending' | 'success' | 'error'

/**
* Optional numerical order of the step in the sequence.
* Used to display the steps in the correct order.
*/
step?: number
state: 'pending' | 'success' | 'error'

/**
* Error information if the step failed
*/
error?: ChatError
error?: ChatError | undefined | null
}

export type ChatMessageWithSearch = ChatMessage & { search: ChatMessageSearch }
Expand Down Expand Up @@ -209,12 +229,3 @@ export function errorToChatError(error: Error): ChatError {
name: error.name,
}
}

export function createProcessingStep(data: Partial<ProcessingStep>): ProcessingStep {
return {
id: data.id ?? '',
content: data.content ?? '',
status: data.status ?? 'pending',
step: data.step ?? 0,
}
}
1 change: 1 addition & 0 deletions lib/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export type {
ChatMessageSearch,
ProcessingStep,
} from './chat/transcript/messages'
export { ProcessType } from './chat/transcript/messages'
export {
CODY_PASSTHROUGH_VSCODE_OPEN_COMMAND_ID,
webviewOpenURIForContextItem,
Expand Down
16 changes: 13 additions & 3 deletions vscode/src/chat/agentic/CodyTool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,20 @@ import { CodyToolProvider, TestToolFactory, type ToolStatusCallback } from './Co
import { toolboxManager } from './ToolboxManager'

const mockCallback: ToolStatusCallback = {
onStart: vi.fn(),
onUpdate: vi.fn(),
onStream: vi.fn(),
onComplete: vi.fn(),
onConfirmationNeeded: vi.fn(),
}

class TestTool extends CodyTool {
public async execute(span: Span, queries: string[]): Promise<ContextItem[]> {
if (queries.length) {
mockCallback?.onStream(this.config.title, queries.join(', '))
mockCallback?.onStream({
id: this.config.tags.tag.toString(),
title: this.config.title,
content: queries.join(', '),
})
// Return mock context items based on queries
return queries.map(query => ({
type: 'file',
Expand Down Expand Up @@ -152,7 +157,12 @@ describe('CodyTool', () => {
testTool?.stream('<TOOLTEST><test>test content</test></TOOLTEST>')
await testTool?.run(mockSpan, mockCallback)

expect(mockCallback.onStream).toHaveBeenCalledWith('TestTool', 'test content')
expect(mockCallback.onStream).toHaveBeenCalledWith({
content: 'test content',
id: 'TOOLTEST',
title: 'TestTool',
type: 'tool',
})
})

it('should not call callback when running tool with empty content', async () => {
Expand Down
54 changes: 45 additions & 9 deletions vscode/src/chat/agentic/CodyTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ContextItemSource,
type ContextItemWithContent,
type ContextMentionProviderMetadata,
ProcessType,
PromptString,
firstValueFrom,
logDebug,
Expand All @@ -12,6 +13,7 @@ import {
pendingOperation,
ps,
} from '@sourcegraph/cody-shared'
import * as uuid from 'uuid'
import { URI } from 'vscode-uri'
import { getContextFromRelativePath } from '../../commands/context/file-path'
import { getContextFileFromShell } from '../../commands/context/shell'
Expand Down Expand Up @@ -106,12 +108,22 @@ export abstract class CodyTool {
*
* Abstract method to be implemented by subclasses for executing the tool.
*/
protected abstract execute(span: Span, queries: string[]): Promise<ContextItem[]>
public async run(span: Span, callback?: ToolStatusCallback): Promise<ContextItem[]> {
public abstract execute(
span: Span,
queries: string[],
callback?: ToolStatusCallback
): Promise<ContextItem[]>
public async run(span: Span, cb?: ToolStatusCallback): Promise<ContextItem[]> {
const toolID = this.config.tags.tag.toString()
try {
const queries = this.parse()
if (queries.length) {
callback?.onStream(this.config.title, queries.join(', '))
cb?.onStream({
id: toolID,
title: this.config.title,
content: queries.join(', '),
type: ProcessType.Tool,
})
// Create a timeout promise
const timeoutPromise = new Promise<ContextItem[]>((_, reject) => {
setTimeout(() => {
Expand All @@ -123,13 +135,13 @@ export abstract class CodyTool {
}, CodyTool.EXECUTION_TIMEOUT_MS)
})
// Race between execution and timeout
const results = await Promise.race([this.execute(span, queries), timeoutPromise])
const results = await Promise.race([this.execute(span, queries, cb), timeoutPromise])
// Notify that tool execution is complete
callback?.onComplete(this.config.title)
cb?.onComplete(toolID)
return results
}
} catch (error) {
callback?.onComplete(this.config.title, error as Error)
cb?.onComplete(toolID, error as Error)
}
return Promise.resolve([])
}
Expand Down Expand Up @@ -158,11 +170,35 @@ class CliTool extends CodyTool {
})
}

public async execute(span: Span, commands: string[]): Promise<ContextItem[]> {
public async execute(
span: Span,
commands: string[],
callback: ToolStatusCallback
): Promise<ContextItem[]> {
span.addEvent('executeCliTool')
if (commands.length === 0) return []
logDebug('CodyTool', `executing ${commands.length} commands...`)
return Promise.all(commands.map(getContextFileFromShell)).then(results => results.flat())
const toolID = this.config.tags.tag.toString()
const approvedCommands = new Set<string>()
for (const command of commands) {
const stepId = `${toolID}-${uuid.v4()}`
const apporval = await callback?.onConfirmationNeeded(stepId, {
title: this.config.title,
content: command,
})
if (apporval) {
approvedCommands.add(command)
} else {
callback.onComplete(stepId, new Error('Command rejected'))
}
}
if (!approvedCommands.size) {
throw new Error('No commands approved for execution')
}
callback.onUpdate(toolID, [...approvedCommands].join(', '))
logDebug('CodyTool', `executing ${approvedCommands.size} commands...`)
return Promise.all([...approvedCommands].map(getContextFileFromShell)).then(results =>
results.flat()
)
}
}

Expand Down
2 changes: 1 addition & 1 deletion vscode/src/chat/agentic/CodyToolProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ describe('ToolFactory', () => {
let factory: TestToolFactory

class TestCodyTool extends CodyTool {
protected async execute(): Promise<ContextItem[]> {
public async execute(): Promise<ContextItem[]> {
return Promise.resolve([])
}
}
Expand Down
11 changes: 8 additions & 3 deletions vscode/src/chat/agentic/CodyToolProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
type ContextMentionProviderMetadata,
type ProcessingStep,
PromptString,
type Unsubscribable,
isDefined,
Expand All @@ -20,9 +21,13 @@ type Retriever = Pick<ContextRetriever, 'retrieveContext'>
* Used to track and report tool execution progress.
*/
export interface ToolStatusCallback {
onStart(): void
onStream(tool: string, content: string): void
onComplete(tool?: string, error?: Error): void
onUpdate(id: string, content: string): void
onStream(step: Partial<ProcessingStep>): void
onComplete(id?: string, error?: Error): void
onConfirmationNeeded(
id: string,
step: Omit<ProcessingStep, 'id' | 'type' | 'state'>
): Promise<boolean>
}

/**
Expand Down
30 changes: 26 additions & 4 deletions vscode/src/chat/agentic/DeepCody.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe('DeepCody', () => {
let mockCodyToolProvider: typeof CodyToolProvider
let localStorageData: { [key: string]: unknown } = {}
let mockStatusCallback: (steps: ProcessingStep[]) => void
let mockRequestCallback: (steps: ProcessingStep) => Promise<boolean>

mockLocalStorage({
get: (key: string) => localStorageData[key],
Expand Down Expand Up @@ -99,6 +100,7 @@ describe('DeepCody', () => {
]

mockStatusCallback = vi.fn()
mockRequestCallback = vi.fn().mockResolvedValue(true)

vi.spyOn(featureFlagProvider, 'evaluatedFeatureFlag').mockReturnValue(Observable.of(false))
vi.spyOn(modelsService, 'isStreamDisabled').mockReturnValue(false)
Expand Down Expand Up @@ -130,7 +132,12 @@ describe('DeepCody', () => {
})

it('initializes correctly when invoked', async () => {
const agent = new DeepCodyAgent(mockChatBuilder, mockChatClient, mockStatusCallback)
const agent = new DeepCodyAgent(
mockChatBuilder,
mockChatClient,
mockStatusCallback,
mockRequestCallback
)

expect(agent).toBeDefined()
})
Expand Down Expand Up @@ -159,7 +166,12 @@ describe('DeepCody', () => {
])
)

const agent = new DeepCodyAgent(mockChatBuilder, mockChatClient, mockStatusCallback)
const agent = new DeepCodyAgent(
mockChatBuilder,
mockChatClient,
mockStatusCallback,
mockRequestCallback
)

const result = await agent.getContext(
'deep-cody-test-interaction-id',
Expand Down Expand Up @@ -200,7 +212,12 @@ describe('DeepCody', () => {
])
)

const agent = new DeepCodyAgent(mockChatBuilder, mockChatClient, mockStatusCallback)
const agent = new DeepCodyAgent(
mockChatBuilder,
mockChatClient,
mockStatusCallback,
mockRequestCallback
)

const result = await agent.getContext(
'deep-cody-test-interaction-id',
Expand Down Expand Up @@ -251,7 +268,12 @@ describe('DeepCody', () => {
mockChatClient.chat = vi.fn().mockReturnValue(mockStreamResponse)

// Create agent and run context retrieval
const agent = new DeepCodyAgent(mockChatBuilder, mockChatClient, mockStatusCallback)
const agent = new DeepCodyAgent(
mockChatBuilder,
mockChatClient,
mockStatusCallback,
mockRequestCallback
)
const result = await agent.getContext(
'deep-cody-test-validation-id',
new AbortController().signal,
Expand Down
Loading

0 comments on commit d3a6f7e

Please sign in to comment.