Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3231,6 +3231,14 @@
"onExp"
]
},
"github.copilot.chat.tools.compressOutput.enabled": {
"type": "boolean",
"default": false,
"markdownDescription": "%github.copilot.config.tools.compressOutput.enabled%",
"tags": [
"preview"
]
},
"github.copilot.chat.backgroundCompaction": {
"type": "boolean",
"default": false,
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@
"copilot.tools.viewImage.name": "View Image",
"copilot.tools.viewImage.userDescription": "View the contents of an image file",
"github.copilot.config.tools.viewImage.enabled": "Enable the view image tool, which allows the agent to view image files such as png, jpg, jpeg, gif, and webp.",
"github.copilot.config.tools.compressOutput.enabled": "(Experimental) Post-process tool output (e.g. `git diff`, `ls -l`, `npm install`) to reduce token usage before it is sent to the model.",
"copilot.tools.listDirectory.name": "List Dir",
"copilot.tools.listDirectory.userDescription": "List the contents of a directory",
"copilot.tools.getTaskOutput.name": "Get Task Output",
Expand Down
2 changes: 2 additions & 0 deletions src/extension/extension/vscode-node/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ import { AgentMemoryService, IAgentMemoryService } from '../../tools/common/agen
import { IMemoryCleanupService, MemoryCleanupService } from '../../tools/common/memoryCleanupService';
import { IToolDeferralService } from '../../../platform/networking/common/toolDeferralService';
import { ToolDeferralService } from '../../tools/common/toolDeferralService';
import { IToolResultCompressor, ToolResultCompressorService } from '../../tools/common/toolResultCompressor';
import { IToolsService } from '../../tools/common/toolsService';
import { ToolsService } from '../../tools/vscode-node/toolsService';
import { LanguageContextServiceImpl } from '../../typescriptContext/vscode-node/languageContextService';
Expand Down Expand Up @@ -166,6 +167,7 @@ export function registerServices(builder: IInstantiationServiceBuilder, extensio
builder.define(ITokenizerProvider, new SyncDescriptor(TokenizerProvider, [true]));
builder.define(IToolsService, new SyncDescriptor(ToolsService));
builder.define(IToolDeferralService, new ToolDeferralService());
builder.define(IToolResultCompressor, new SyncDescriptor(ToolResultCompressorService));
builder.define(IAgentMemoryService, new SyncDescriptor(AgentMemoryService));
builder.define(IMemoryCleanupService, new SyncDescriptor(MemoryCleanupService));
builder.define(IChatDiskSessionResources, new SyncDescriptor(ChatDiskSessionResources));
Expand Down
165 changes: 165 additions & 0 deletions src/extension/tools/common/toolResultCompressor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type * as vscode from 'vscode';
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
import { ILogService } from '../../../platform/log/common/logService';
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
import { createServiceIdentifier } from '../../../util/common/services';
import { LanguageModelTextPart } from '../../../vscodeTypes';

export const IToolResultCompressor = createServiceIdentifier<IToolResultCompressor>('IToolResultCompressor');

/**
* Result of running a {@link IToolResultFilter}.
*
* `text` is the new text to substitute back into the corresponding text part.
* `compressed` is `true` if any compression actually happened — used purely
* for telemetry / accounting.
*/
export interface IToolResultFilterOutput {
readonly text: string;
readonly compressed: boolean;
}

/**
* A pure function that compresses a single text part of a tool result.
*
* Implementations MUST never make output worse than the input. If a filter
* cannot improve a piece of text, it should return the original `text` and
* `compressed: false`.
*/
export interface IToolResultFilter {
readonly id: string;
/** Tool names this filter applies to. */
readonly toolNames: readonly string[];
/**
* Decide whether this filter wants to handle the result. May inspect tool
* input (e.g. for `run_in_terminal`, the command being run).
*/
matches(toolName: string, input: unknown): boolean;
apply(text: string, input: unknown): IToolResultFilterOutput;
}

export interface IToolResultCompressor {
readonly _serviceBrand: undefined;
registerFilter(filter: IToolResultFilter): void;
/**
* Returns a possibly-compressed copy of `result`, or `undefined` if no
* compression was applied (caller should pass through the original).
*/
maybeCompress(toolName: string, input: unknown, result: vscode.LanguageModelToolResult | vscode.LanguageModelToolResult2): vscode.LanguageModelToolResult | undefined;
}

/**
* Outputs at or below this size are not worth compressing.
* Mirrors ztk's 80-byte minimum.
*/
const MIN_COMPRESSIBLE_LENGTH = 80;

Comment thread
meganrogge marked this conversation as resolved.
export class ToolResultCompressorService implements IToolResultCompressor {
declare readonly _serviceBrand: undefined;

private readonly _filters = new Map<string, IToolResultFilter[]>();

constructor(
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@ILogService private readonly _logService: ILogService,
) { }

registerFilter(filter: IToolResultFilter): void {
for (const name of filter.toolNames) {
let bucket = this._filters.get(name);
if (!bucket) {
bucket = [];
this._filters.set(name, bucket);
}
bucket.push(filter);
}
}

maybeCompress(toolName: string, input: unknown, result: vscode.LanguageModelToolResult | vscode.LanguageModelToolResult2): vscode.LanguageModelToolResult | undefined {
if (!this._configurationService.getConfig(ConfigKey.ToolResultCompressionEnabled)) {
return undefined;
}

const filters = this._filters.get(toolName);
if (!filters || filters.length === 0) {
return undefined;
}

const matchingFilters = filters.filter(f => f.matches(toolName, input));
if (matchingFilters.length === 0) {
return undefined;
}

let totalBefore = 0;
let totalAfter = 0;
let anyCompressed = false;
const usedFilterIds = new Set<string>();

const newContent = result.content.map(part => {
if (!(part instanceof LanguageModelTextPart)) {
return part;
}
const original = part.value;
if (original.length < MIN_COMPRESSIBLE_LENGTH) {
return part;
}

let current = original;
for (const filter of matchingFilters) {
try {
const out = filter.apply(current, input);
if (out.compressed && out.text.length < current.length) {
current = out.text;
usedFilterIds.add(filter.id);
}
} catch (err) {
// "Never make it worse." Drop the filter on error and keep going.
this._logService.warn(`[ToolResultCompressor] filter ${filter.id} threw on tool ${toolName}: ${err}`);
}
Comment thread
meganrogge marked this conversation as resolved.
}

totalBefore += original.length;
totalAfter += current.length;
if (current !== original) {
anyCompressed = true;
return new LanguageModelTextPart(current);
}
return part;
Comment thread
meganrogge marked this conversation as resolved.
});

if (!anyCompressed) {
return undefined;
}

this._sendTelemetry(toolName, [...usedFilterIds], totalBefore, totalAfter);

// Preserve `toolResultMessage`/`toolResultDetails` if present (ExtendedLanguageModelToolResult shape).
const compressed: vscode.LanguageModelToolResult & { toolResultMessage?: unknown; toolResultDetails?: unknown } =
Object.assign(Object.create(Object.getPrototypeOf(result)), result, { content: newContent });
return compressed as vscode.LanguageModelToolResult;
}
Comment thread
meganrogge marked this conversation as resolved.

private _sendTelemetry(toolName: string, filterIds: string[], beforeBytes: number, afterBytes: number) {
/* __GDPR__
"toolResultCompressed" : {
"owner": "meganrogge",
"comment": "Reports tool output compression savings.",
"toolName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The tool whose output was compressed." },
"filters": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Comma-separated filter ids that fired." },
"beforeBytes": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true, "comment": "Total text part bytes before compression." },
"afterBytes": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true, "comment": "Total text part bytes after compression." }
}
*/
this._telemetryService.sendMSFTTelemetryEvent(
'toolResultCompressed',
{ toolName, filters: filterIds.join(',') },
{ beforeBytes, afterBytes },
);
}
}
Loading
Loading