mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
chore(telemetry): add ripgrep fallback telemetry (#1203)
Some checks are pending
CI / lint (push) Waiting to run
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Some checks are pending
CI / lint (push) Waiting to run
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
This commit is contained in:
parent
c82dcf9cd8
commit
53e79e4bae
8 changed files with 130 additions and 7 deletions
|
|
@ -481,8 +481,8 @@ export class ToolManager {
|
|||
new b.ReadTool(kaos, workspace),
|
||||
new b.WriteTool(kaos, workspace),
|
||||
new b.EditTool(kaos, workspace),
|
||||
new b.GrepTool(kaos, workspace),
|
||||
new b.GlobTool(kaos, workspace),
|
||||
new b.GrepTool(kaos, workspace, this.agent.telemetry),
|
||||
new b.GlobTool(kaos, workspace, this.agent.telemetry),
|
||||
new b.BashTool(kaos, cwd, background, {
|
||||
allowBackground,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { spawn } from 'node:child_process';
|
|||
import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { Disposable, InstantiationType, registerSingleton } from '../../di';
|
||||
import { Disposable, SyncDescriptor, registerSingleton } from '../../di';
|
||||
import type {
|
||||
FsGrepFileHit,
|
||||
FsGrepMatch,
|
||||
|
|
@ -19,6 +19,7 @@ import ignore, { type Ignore } from 'ignore';
|
|||
import { ISessionService } from '../session/session';
|
||||
|
||||
import { ILogService } from '../logger/logger';
|
||||
import { noopTelemetryClient, type TelemetryClient } from '../../telemetry';
|
||||
import { IFsSearchService, FsGrepTimeoutError } from './fsSearch';
|
||||
|
||||
const SEARCH_HARD_CAP = 500;
|
||||
|
|
@ -39,11 +40,15 @@ export class FsSearchService
|
|||
|
||||
protected rgMissingWarned = false;
|
||||
|
||||
protected readonly telemetry: TelemetryClient;
|
||||
|
||||
constructor(
|
||||
telemetry: TelemetryClient,
|
||||
@ISessionService protected readonly sessions: ISessionService,
|
||||
@ILogService protected readonly logger: ILogService,
|
||||
) {
|
||||
super();
|
||||
this.telemetry = telemetry;
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
|
|
@ -119,6 +124,7 @@ export class FsSearchService
|
|||
);
|
||||
return out;
|
||||
}
|
||||
this.telemetry.track('fs_grep_node_fallback', { reason: 'rg_missing' });
|
||||
const out = await this.grepWithNode(
|
||||
realCwd,
|
||||
req,
|
||||
|
|
@ -644,4 +650,7 @@ async function whichBinary(name: string): Promise<string | null> {
|
|||
return null;
|
||||
}
|
||||
|
||||
registerSingleton(IFsSearchService, FsSearchService, InstantiationType.Delayed);
|
||||
registerSingleton(
|
||||
IFsSearchService,
|
||||
new SyncDescriptor(FsSearchService, [noopTelemetryClient], true),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import type { BuiltinTool } from '../../../agent/tool';
|
|||
import { isAbortError } from '../../../loop/errors';
|
||||
import { ToolAccesses } from '../../../loop/tool-access';
|
||||
import type { ExecutableToolResult, ToolExecution } from '../../../loop/types';
|
||||
import { noopTelemetryClient, type TelemetryClient } from '../../../telemetry';
|
||||
import { isWithinDirectory, resolvePathAccessPath } from '../../policies/path-access';
|
||||
import type { PathClass } from '../../policies/path-access';
|
||||
import { isSensitiveFile } from '../../policies/sensitive';
|
||||
|
|
@ -104,10 +105,13 @@ export class GlobTool implements BuiltinTool<GlobInput> {
|
|||
readonly name = 'Glob' as const;
|
||||
readonly description: string;
|
||||
readonly parameters: Record<string, unknown> = toInputJsonSchema(GlobInputSchema);
|
||||
private readonly telemetry: TelemetryClient;
|
||||
constructor(
|
||||
private readonly kaos: Kaos,
|
||||
private readonly workspace: WorkspaceConfig,
|
||||
telemetry: TelemetryClient = noopTelemetryClient,
|
||||
) {
|
||||
this.telemetry = telemetry;
|
||||
this.description =
|
||||
this.kaos.pathClass() === 'win32'
|
||||
? GLOB_DESCRIPTION + WINDOWS_PATH_HINT
|
||||
|
|
@ -176,10 +180,17 @@ export class GlobTool implements BuiltinTool<GlobInput> {
|
|||
try {
|
||||
const resolution = await ensureRgPath({ signal });
|
||||
rgPath = resolution.path;
|
||||
if (resolution.source !== 'system-path') {
|
||||
this.telemetry.track('glob_tool_rg_fallback', {
|
||||
source: resolution.source,
|
||||
outcome: 'resolved',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (isAbortError(error)) {
|
||||
return { isError: true, output: 'Glob aborted' };
|
||||
}
|
||||
this.telemetry.track('glob_tool_rg_fallback', { outcome: 'failed' });
|
||||
return { isError: true, output: rgUnavailableMessage(error) };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import type { BuiltinTool } from '../../../agent/tool';
|
|||
import { isAbortError } from '../../../loop/errors';
|
||||
import { ToolAccesses } from '../../../loop/tool-access';
|
||||
import type { ExecutableToolResult, ToolExecution } from '../../../loop/types';
|
||||
import { noopTelemetryClient, type TelemetryClient } from '../../../telemetry';
|
||||
import { resolvePathAccessPath } from '../../policies/path-access';
|
||||
import type { PathClass } from '../../policies/path-access';
|
||||
import { isSensitiveFile } from '../../policies/sensitive';
|
||||
|
|
@ -158,10 +159,14 @@ export class GrepTool implements BuiltinTool<GrepInput> {
|
|||
readonly name = 'Grep' as const;
|
||||
readonly description = GREP_DESCRIPTION;
|
||||
readonly parameters: Record<string, unknown> = toInputJsonSchema(GrepInputSchema);
|
||||
private readonly telemetry: TelemetryClient;
|
||||
constructor(
|
||||
private readonly kaos: Kaos,
|
||||
private readonly workspace: WorkspaceConfig,
|
||||
) {}
|
||||
telemetry: TelemetryClient = noopTelemetryClient,
|
||||
) {
|
||||
this.telemetry = telemetry;
|
||||
}
|
||||
|
||||
resolveExecution(args: GrepInput): ToolExecution {
|
||||
let path: string | undefined;
|
||||
|
|
@ -199,10 +204,17 @@ export class GrepTool implements BuiltinTool<GrepInput> {
|
|||
try {
|
||||
const resolution = await ensureRgPath({ signal });
|
||||
rgPath = resolution.path;
|
||||
if (resolution.source !== 'system-path') {
|
||||
this.telemetry.track('grep_tool_rg_fallback', {
|
||||
source: resolution.source,
|
||||
outcome: 'resolved',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (isAbortError(error)) {
|
||||
return { isError: true, output: 'Grep aborted' };
|
||||
}
|
||||
this.telemetry.track('grep_tool_rg_fallback', { outcome: 'failed' });
|
||||
return { isError: true, output: rgUnavailableMessage(error) };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { ensureRgPath } from '../../src/tools/support/rg-locator';
|
|||
import type { WorkspaceConfig } from '../../src/tools/support/workspace';
|
||||
import { createFakeKaos } from './fixtures/fake-kaos';
|
||||
import { executeTool } from './fixtures/execute-tool';
|
||||
import { recordingTelemetry, type TelemetryRecord } from '../fixtures/telemetry';
|
||||
|
||||
vi.mock('../../src/tools/support/rg-locator', () => ({
|
||||
ensureRgPath: vi.fn(async () => ({ path: '/mock/rg', source: 'system-path' })),
|
||||
|
|
@ -108,6 +109,26 @@ describe('GlobTool', () => {
|
|||
expect(schema.properties['include_dirs']?.description?.toLowerCase()).toContain('deprecated');
|
||||
});
|
||||
|
||||
it('tracks when glob uses a non-system ripgrep fallback', async () => {
|
||||
vi.mocked(ensureRgPath).mockResolvedValueOnce({
|
||||
path: '/mock/rg',
|
||||
source: 'share-bin-downloaded',
|
||||
});
|
||||
const records: TelemetryRecord[] = [];
|
||||
const exec = execReturning('/workspace/src/a.ts\n');
|
||||
const tool = new GlobTool(kaosWithExec(exec), workspace, recordingTelemetry(records));
|
||||
|
||||
const result = await executeTool(tool, context({ pattern: 'src/**/*.ts', path: '/workspace' }));
|
||||
|
||||
expect(result.output).toBe('src/a.ts');
|
||||
expect(records).toEqual([
|
||||
{
|
||||
event: 'glob_tool_rg_fallback',
|
||||
properties: { source: 'share-bin-downloaded', outcome: 'resolved' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('injects the Windows path hint into the description on a win32 backend', () => {
|
||||
const tool = new GlobTool(createFakeKaos({ pathClass: () => 'win32' }), workspace);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { ensureRgPath } from '../../src/tools/support/rg-locator';
|
|||
import type { WorkspaceConfig } from '../../src/tools/support/workspace';
|
||||
import { createFakeKaos, toolContentString } from './fixtures/fake-kaos';
|
||||
import { executeTool } from './fixtures/execute-tool';
|
||||
import { recordingTelemetry, type TelemetryRecord } from '../fixtures/telemetry';
|
||||
|
||||
vi.mock('../../src/tools/support/rg-locator', () => ({
|
||||
ensureRgPath: vi.fn(async () => ({ path: '/mock/rg', source: 'system-path' })),
|
||||
|
|
@ -1391,6 +1392,26 @@ describe('GrepTool', () => {
|
|||
expect(exec).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('tracks when grep uses a non-system ripgrep fallback', async () => {
|
||||
vi.mocked(ensureRgPath).mockResolvedValueOnce({
|
||||
path: '/mock/rg',
|
||||
source: 'share-bin-downloaded',
|
||||
});
|
||||
const records: TelemetryRecord[] = [];
|
||||
const exec = vi.fn().mockResolvedValue(processWithOutput('/workspace/src/a.ts\n'));
|
||||
const tool = new GrepTool(createFakeKaos({ exec }), workspace, recordingTelemetry(records));
|
||||
|
||||
const result = await executeTool(tool, context({ pattern: 'hit' }));
|
||||
|
||||
expect(result.isError).not.toBe(true);
|
||||
expect(records).toEqual([
|
||||
{
|
||||
event: 'grep_tool_rg_fallback',
|
||||
properties: { source: 'share-bin-downloaded', outcome: 'resolved' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns an install hint when spawning the resolved ripgrep path hits ENOENT', async () => {
|
||||
const error = Object.assign(new Error('spawn /mock/rg ENOENT'), { code: 'ENOENT' });
|
||||
const exec = vi.fn().mockRejectedValue(error);
|
||||
|
|
|
|||
|
|
@ -52,6 +52,14 @@ export function createServerServiceCollection(
|
|||
}
|
||||
|
||||
services.set(Services.ILogService, new PinoLoggerAdapter(pinoLogger));
|
||||
services.set(
|
||||
Services.IFsSearchService,
|
||||
new SyncDescriptor(
|
||||
Services.FsSearchService,
|
||||
[server.coreProcessOptions?.telemetry ?? Services.noopTelemetryClient],
|
||||
true,
|
||||
),
|
||||
);
|
||||
services.set(IRestGateway, new FastifyRestGateway(app));
|
||||
services.set(Services.IEnvironmentService, envService);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,14 @@ import { join } from 'node:path';
|
|||
import { pino } from 'pino';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { ISessionService, FsSearchService, ILogService } from '@moonshot-ai/agent-core';
|
||||
import {
|
||||
ISessionService,
|
||||
FsSearchService,
|
||||
ILogService,
|
||||
noopTelemetryClient,
|
||||
type TelemetryClient,
|
||||
type TelemetryProperties,
|
||||
} from '@moonshot-ai/agent-core';
|
||||
|
||||
import { IRestGateway, startServer, type RunningServer } from '../src';
|
||||
import { fixedTokenAuth } from './helpers/serverHarness';
|
||||
|
|
@ -398,6 +405,14 @@ describe('FsSearchService direct: rg fallback + grep timeout (W11.1)', () => {
|
|||
}
|
||||
|
||||
class StubMissingRg extends FsSearchService {
|
||||
constructor(
|
||||
sessions: ISessionService,
|
||||
logger: ILogService,
|
||||
telemetry: TelemetryClient = noopTelemetryClient,
|
||||
) {
|
||||
super(telemetry, sessions, logger);
|
||||
}
|
||||
|
||||
public override probeRg(): Promise<string | null> {
|
||||
if (this.rgPath !== undefined) return Promise.resolve(this.rgPath);
|
||||
this.rgPath = null;
|
||||
|
|
@ -445,6 +460,32 @@ describe('FsSearchService direct: rg fallback + grep timeout (W11.1)', () => {
|
|||
svc.dispose();
|
||||
});
|
||||
|
||||
it('tracks fs grep node fallback when rg is missing', async () => {
|
||||
const sessions = makeStubSession(workspace);
|
||||
const logger = makeStubLogger();
|
||||
const events: Array<{ event: string; properties?: TelemetryProperties }> = [];
|
||||
const svc = new StubMissingRg(sessions, logger, {
|
||||
track: (event, properties) => events.push({ event, properties }),
|
||||
});
|
||||
writeFileSync(join(workspace, 'a.txt'), 'needle\n');
|
||||
|
||||
await svc.grep('sess_stub', {
|
||||
pattern: 'needle',
|
||||
regex: false,
|
||||
case_sensitive: true,
|
||||
follow_gitignore: true,
|
||||
max_files: 200,
|
||||
max_matches_per_file: 50,
|
||||
max_total_matches: 5000,
|
||||
context_lines: 0,
|
||||
});
|
||||
|
||||
expect(events).toEqual([
|
||||
{ event: 'fs_grep_node_fallback', properties: { reason: 'rg_missing' } },
|
||||
]);
|
||||
svc.dispose();
|
||||
});
|
||||
|
||||
it('grep timeout fires FsGrepTimeoutError → 41305', async () => {
|
||||
const sessions = makeStubSession(workspace);
|
||||
const logger = makeStubLogger();
|
||||
|
|
@ -467,7 +508,7 @@ describe('FsSearchService direct: rg fallback + grep timeout (W11.1)', () => {
|
|||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
const svc = new StubTimeout(sessions, logger);
|
||||
const svc = new StubTimeout(noopTelemetryClient, sessions, logger);
|
||||
writeFileSync(join(workspace, 'a.txt'), 'needle\n');
|
||||
await expect(
|
||||
svc.grep('sess_stub', {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue