feat: improve AI chat - system prompt, tool output, context window display (#17769)

⚠️ **AI-generated PR — not ready for review** ⚠️

cc @FelixMalfait

---

## Changes

### System prompt improvements
- Explicit skill-before-tools workflow to prevent the model from calling
tools without loading the matching skill first
- Data efficiency guidance (default small limits, use filters)
- Pluralized `load_skill` → `load_skills` for consistency with
`load_tools`

### Token usage reduction
- Output serialization layer: strips null/undefined/empty values from
tool results
- Lowered default `find_*` limit from 100 → 10, max from 1000 → 100

### System object tool generation
- System objects (calendar events, messages, etc.) now generate AI tools
- Only workflow-related and favorite-related objects are excluded

### Context window display fix
- **Bug**: UI compared cumulative tokens (sum of all turns) against
single-request context window → showed 100% after a few turns
- **Fix**: Track `conversationSize` (last step's `inputTokens`) which
represents the actual conversation history size sent to the model
- New `conversationSize` column on thread entity with migration

### Workspace AI instructions
- Support for custom workspace-level AI instructions

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
This commit is contained in:
Félix Malfait
2026-02-09 14:26:02 +01:00
committed by GitHub
co-authored by claude[bot] <41898282+claude[bot]@users.noreply.github.com>
parent 6c7c389785
commit 3216b634a3
105 changed files with 3245 additions and 1714 deletions
@@ -4,8 +4,10 @@ import { type UIMessage } from 'ai';
export type AIChatUsageMetadata = {
inputTokens: number;
outputTokens: number;
cachedInputTokens: number;
inputCredits: number;
outputCredits: number;
conversationSize: number;
};
export type AIChatModelMetadata = {
@@ -1,4 +1,4 @@
import { IsSerializedRelation } from "@/types/IsSerializedRelation.type";
import { type IsSerializedRelation } from '@/types/IsSerializedRelation.type';
export type ExtractSerializedRelationProperties<T> = T extends unknown
? T extends object
@@ -1,4 +1,4 @@
import { SERIALIZED_RELATION_BRAND } from '@/types/SerializedRelation.type';
import { type SERIALIZED_RELATION_BRAND } from '@/types/SerializedRelation.type';
export type IsSerializedRelation<T> =
typeof SERIALIZED_RELATION_BRAND extends keyof T ? true : false;
@@ -28,6 +28,7 @@ export enum SettingsPath {
EmailingDomainDetail = 'domains/emailing-domain/:domainId',
Updates = 'updates',
AI = 'ai',
AIPrompts = 'ai/prompts',
AINewAgent = 'ai/new-agent',
AIAgentDetail = 'ai/agents/:agentId',
AIAgentTurnDetail = 'ai/agents/:agentId/turns/:turnId',
@@ -81,5 +81,3 @@ type Assertions = [
>
>,
];
@@ -145,6 +145,7 @@ export { safeParseRelativeDateFilterJSONStringified } from './safeParseRelativeD
export { getGenericOperationName } from './sentry/getGenericOperationName';
export { getHumanReadableNameFromCode } from './sentry/getHumanReadableNameFromCode';
export { appendCopySuffix } from './strings/appendCopySuffix';
export { camelToSnakeCase } from './strings/camelToSnakeCase';
export { capitalize } from './strings/capitalize';
export { pascalCase } from './strings/pascalCase';
export { stringifySafely } from './strings/stringifySafely';
@@ -0,0 +1,25 @@
import { camelToSnakeCase } from '../camelToSnakeCase';
describe('camelToSnakeCase', () => {
it('should convert camelCase to snake_case', () => {
expect(camelToSnakeCase('cloudUserWorkspaces')).toBe(
'cloud_user_workspaces',
);
});
it('should convert single-word camelCase', () => {
expect(camelToSnakeCase('people')).toBe('people');
});
it('should handle two-word camelCase', () => {
expect(camelToSnakeCase('selfHostingUser')).toBe('self_hosting_user');
});
it('should handle already snake_case', () => {
expect(camelToSnakeCase('already_snake')).toBe('already_snake');
});
it('should handle single word', () => {
expect(camelToSnakeCase('company')).toBe('company');
});
});
@@ -0,0 +1,2 @@
export const camelToSnakeCase = (str: string): string =>
str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);