Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3c18b6c
add memfs to deps
pgayvallet Jan 21, 2026
e4879d6
work in progress
pgayvallet Jan 22, 2026
c1bca2f
I totally did that alone
pgayvallet Jan 22, 2026
b136796
move FS files, adapt tool result store and runner
pgayvallet Jan 22, 2026
0f22dcd
Merge remote-tracking branch 'upstream/main' into ab-12348-fs-store
pgayvallet Jan 22, 2026
9942e7e
preparing store interface
pgayvallet Jan 23, 2026
d0d69ff
implement fs_store, update read tool
pgayvallet Jan 23, 2026
710fe39
create base set of tools
pgayvallet Jan 23, 2026
f71fe46
move types to server package
pgayvallet Jan 23, 2026
97f1663
fix unit tests
pgayvallet Jan 26, 2026
5e92995
plug FS tools to agent
pgayvallet Jan 26, 2026
f97f223
Merge remote-tracking branch 'upstream/main' into ab-12348-fs-store
pgayvallet Jan 26, 2026
477cef4
remove memfs
pgayvallet Jan 26, 2026
0b029fb
use constants for tool names
pgayvallet Jan 26, 2026
c17e3e8
fix mocks
pgayvallet Jan 26, 2026
77bf6a0
export class to please TS
pgayvallet Jan 28, 2026
42402db
add folder tree util
pgayvallet Jan 28, 2026
5e51506
cleanup agent execution
pgayvallet Jan 28, 2026
5351aaf
move timestamp to prompt factory
pgayvallet Jan 28, 2026
0fd234b
clean exports
pgayvallet Jan 28, 2026
1b0f548
fix types again
pgayvallet Jan 28, 2026
8534c6d
Merge remote-tracking branch 'upstream/main' into ab-12348-fs-store
pgayvallet Jan 29, 2026
d716cd4
rename filesystem to filestore + cleanup
pgayvallet Jan 29, 2026
36dcac4
change namespace
pgayvallet Jan 29, 2026
72fbe61
remove duplicate instructions
pgayvallet Jan 29, 2026
4a92f7d
add kill switch
pgayvallet Jan 29, 2026
1a1e930
add filestore to descriptions
pgayvallet Jan 29, 2026
c7ffc30
update prompt
pgayvallet Jan 29, 2026
447200b
update prompt again
pgayvallet Jan 29, 2026
e9ce8e4
last time
pgayvallet Jan 29, 2026
701f784
Merge remote-tracking branch 'upstream/main' into ab-12348-fs-store
pgayvallet Jan 30, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
*/
export const internalNamespaces = {
platformCore: 'platform.core',
observability: 'observability',
platformDashboard: 'platform.dashboard',
filestore: 'filestore',
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started with platform.filestore, but then changed to just filestore for a few reasons

  1. the platform prefix is used for "real"/"selectable" tools, I don't think we need those "internal" tools to use the same top-level prefix
  2. I even think it's better to have a dedicated namespace, for better identification, in case we want to do specific events or UI display for those tools/calls (even if platform.filestore would have worked perfectly well too)
  3. it's shorter, which is always better

Now if someone is strongly opposed or have another idea, please state so.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep its a good idea. I very much doubt customers have already claimed this "namespace" but worth checking what happens in that case (who takes priority)

observability: 'observability',
security: 'security',
} as const;

Expand All @@ -21,6 +22,7 @@ export const internalNamespaces = {
*/
export const protectedNamespaces: string[] = [
internalNamespaces.platformCore,
internalNamespaces.filestore,
internalNamespaces.observability,
internalNamespaces.platformDashboard, // Owned by dashboard_agent plugin
internalNamespaces.security,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export {
type ToolDefinition,
type ToolDefinitionWithSchema,
platformCoreTools,
filestoreTools,
defaultAgentToolIds,
editableToolTypes,
isReservedToolId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ export const platformCoreTools = {
attachmentDiff: platformCoreTool('attachment_diff'),
} as const;

export const filestoreTools = {
read: `${internalNamespaces.filestore}.read`,
ls: `${internalNamespaces.filestore}.ls`,
grep: `${internalNamespaces.filestore}.grep`,
glob: `${internalNamespaces.filestore}.glob`,
};

/**
* List of tool types which can be created / edited by a user.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export { ToolType, type ToolDefinition, type ToolDefinitionWithSchema } from './
export { isReservedToolId, validateToolId, toolIdRegexp, toolIdMaxLength } from './tool_ids';
export {
platformCoreTools,
filestoreTools,
activeToolsCountWarningThreshold,
defaultAgentToolIds,
editableToolTypes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export {
export { processFieldCapsResponse, type FieldListFromFieldCapsResponse } from './field_caps';
export { generateXmlTree, type XmlNode } from './formatting';
export { errorResult, otherResult } from './results';
export { estimateTokens, truncateTokens } from './token_count';
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

/**
* Estimates token count for given string or arbitrary data.
* Uses a simple heuristic: ~4 characters per token.
*/
export const estimateTokens = (data: unknown): number => {
const str = typeof data === 'string' ? data : JSON.stringify(data);
return Math.ceil(str.length / 4);
};

/**
* Truncates a string to a given number of tokens.
* Uses a simple heuristic: ~4 characters per token.
*/
export const truncateTokens = (data: string, maxTokens: number): string => {
return data.slice(0, maxTokens * 4);
};
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
PromptManager,
ConversationStateManager,
} from '../runner';
import type { IFileStore } from '../runner/filestore';
import type { AttachmentStateManager } from '../attachments';

export type AgentHandlerFn = (
Expand Down Expand Up @@ -103,6 +104,10 @@ export interface AgentHandlerContext {
* Logger scoped to this execution
*/
logger: Logger;
/**
* File store to access data from the agent's virtual filesystem
*/
filestore: IFileStore;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export type {
InternalToolDefinition,
ToolReturnSummarizerFn,
} from './tools';
export { getToolResultId, createErrorResult, isToolResultId } from './tools';
export { getToolResultId, createErrorResult, createOtherResult, isToolResultId } from './tools';
export type {
AgentHandlerParams,
AgentHandlerContext,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

/**
* Possible types of entries stored in the store.
*/
export enum FileEntryType {
toolResult = 'tool_result',
attachment = 'attachment',
}

export type FileEntryMetadata<TExtraMeta extends object = {}> = {
/**
* Type of the entry (tool_result, attachment...)
*/
type: FileEntryType;
/**
* Unique identifier of the entry, (unique for its type)
*/
id: string;
/**
* Estimated length, in tokens, of the content of the raw content.
*/
token_count: number;
/**
* Defines if the entry can be modified or not.
*/
readonly: boolean;
} /** extra per-type metadata */ & TExtraMeta;

export interface FileEntryContent<TData extends object = object> {
/**
* Raw content of the file.
*/
raw: TData;
/**
* Plain text representation of the file content, which can be used for grep.
*/
plain_text?: string;
}

/**
* A file entry in the virtual filesystem.
*/
export interface FileEntry<TContent extends object = object, TMeta extends object = object> {
path: string;
type: 'file';
metadata: FileEntryMetadata<TMeta>;
content: FileEntryContent<TContent>;
}

/**
* A directory entry in the virtual filesystem.
*/
export interface DirEntry {
path: string;
type: 'dir';
}

/**
* Either a file or directory entry.
*/
export type FsEntry = FileEntry | DirEntry;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export {
FileEntryType,
type FileEntry,
type FsEntry,
type FileEntryContent,
type FileEntryMetadata,
type DirEntry,
} from './filesystem';
export type { IFileStore, IToolFileStore, LsEntry, DirEntryWithChildren, GrepMatch } from './store';
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { FileEntry, DirEntry } from './filesystem';

/**
* Main interface for the public API of the file store.
*/
export interface IFileStore {
/**
* Read a file entry from the store.
*
* @param path path of the file to read.
*/
read(path: string): Promise<FileEntry | undefined>;
/**
* List files and directories at the given path.
*
* @param path path of the directory to list.
* @param options.depth optional level of depth to include (default to 1).
*/
ls(path: string, options?: { depth?: number }): Promise<LsEntry[]>;
/**
* List files matching the given glob pattern.
*
* @param pattern glob pattern to match.
*/
glob(pattern: string): Promise<FileEntry[]>;
/**
* Search files with text matching the given pattern.
*
* @param pattern The pattern to search for.
* @param glob The glob pattern to match files against.
* @param options.context Optional number of lines of context to include before and after the match.
* @param options.fixed If true, treat pattern as literal text (like grep -F). Default: false (regex).
*/
grep(
pattern: string,
glob: string,
options?: { context?: number; fixed?: boolean }
): Promise<GrepMatch[]>;
}

/**
* Distinct interface for the API exposed to tool handlers.
* The same for now, but will change in the future.
*/
export type IToolFileStore = IFileStore;

export interface DirEntryWithChildren extends DirEntry {
children?: LsEntry[];
}

export type LsEntry = DirEntryWithChildren | FileEntry;

export interface GrepMatch {
/**
* Reference to the file entry that matched.
*/
entry: FileEntry;
/**
* Line number of the match.
*/
line: number;
/**
* The matched text, (with pre/post context lines depending on call options).
*/
match: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,20 @@ export type {
ModelProviderStats,
ModelCallInfo,
} from './model_provider';
export type { ToolResultStore, WritableToolResultStore } from './result_store';
export type { ToolResultStore, WritableToolResultStore, ToolResultWithMeta } from './result_store';
export type { AttachmentsService } from './attachments_service';
export type { PromptManager, ToolPromptManager, ConfirmationInfo } from './prompt_manager';
export type { ConversationStateManager, ToolStateManager } from './state_manager';
export { FileEntryType } from './filestore';
export type {
IToolFileStore,
IFileStore,
LsEntry,
FsEntry,
DirEntry,
DirEntryWithChildren,
GrepMatch,
FileEntry,
FileEntryMetadata,
FileEntryContent,
} from './filestore';
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ export interface ToolResultStore {
* Writable version of ToolResultStore, used internally by the runner/agent
*/
export interface WritableToolResultStore extends ToolResultStore {
add(result: ToolResult): void;
add(result: ToolResultWithMeta): void;
delete(resultId: string): boolean;
asReadonly(): ToolResultStore;
}

export interface ToolResultWithMeta {
tool_call_id: string;
tool_id: string;
result: ToolResult;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
ToolPromptManager,
ToolStateManager,
} from '../runner';
import type { IToolFileStore } from '../runner/filestore';
import type { AttachmentStateManager } from '../attachments';

/**
Expand Down Expand Up @@ -125,4 +126,8 @@ export interface ToolHandlerContext {
* Allows tools to create, read, update, and delete attachments that persist across conversation rounds.
*/
attachments: AttachmentStateManager;
/**
* File store to access data from the agent's virtual filesystem
*/
filestore: IToolFileStore;
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ export {
isToolHandlerInterruptReturn,
isToolHandlerStandardReturn,
} from './handler';
export { getToolResultId, createErrorResult, isToolResultId } from './utils';
export { getToolResultId, createErrorResult, createOtherResult, isToolResultId } from './utils';
export type { InternalToolDefinition, InternalToolAvailabilityHandler } from './internal';
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { randomInt } from 'crypto';
import type { ErrorResult } from '@kbn/agent-builder-common';
import type { ErrorResult, OtherResult } from '@kbn/agent-builder-common';
import { ToolResultType } from '@kbn/agent-builder-common';

const charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
Expand All @@ -33,3 +33,11 @@ export const createErrorResult = (message: string | ErrorResult['data']): ErrorR
data: typeof message === 'string' ? { message } : message,
};
};

export const createOtherResult = <T extends Object>(data: T): OtherResult<T> => {
return {
tool_result_id: getToolResultId(),
type: ToolResultType.other,
data,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
import type { AgentBuilderPluginSetup, AgentBuilderPluginStart } from './types';
import { createMockedExecutableTool, createToolRegistryMock } from './test_utils/tools';
import { createFormatContextMock } from './test_utils/attachments';
import { createToolHandlerContextMock } from './test_utils/runner';

export type { ToolHandlerContextMock } from './test_utils/runner';

const createSetupContractMock = (): jest.Mocked<AgentBuilderPluginSetup> => {
return {
Expand Down Expand Up @@ -42,4 +45,7 @@ export const agentBuilderMocks = {
attachments: {
createFormatContextMock,
},
tools: {
createHandlerContext: createToolHandlerContextMock,
},
};
Loading
Loading