Skip to content

Commit 251f5de

Browse files
FelixMalfaitclaude
andauthored
[breaking, deploy server first] fix(ai-chat): persist providerExecuted flag on tool parts (#20030)
## Summary Fixes Sentry errors of the form: > \`messages.3: \`tool_use\` ids were found without \`tool_result\` blocks immediately after: srvtoolu_…. Each \`tool_use\` block must have a corresponding \`tool_result\` block in the next message.\` ### Root cause When the model invokes a **provider-hosted tool** (e.g. Anthropic's native \`web_search\` — note the \`srvtoolu_\` ID prefix), the AI SDK marks the resulting \`UIMessagePart\` with \`providerExecuted: true\`. \`convertToModelMessages\` uses that flag to emit the tool_use/tool_result pair *inside the same assistant message* — the format Anthropic requires for server-side tools. Our \`AgentMessagePart\` persistence was dropping \`providerExecuted\` on the way to the DB (and re-hydration didn't know to set it). On the next turn, \`convertToModelMessages\` treated the rehydrated part as a client-side tool call, splitting it into \`assistant(tool_use)\` + \`user(tool_result)\` — which Anthropic then rejects with the error above. ### Fix - Add nullable \`providerExecuted BOOLEAN\` column on \`core.agentMessagePart\` via a fast instance command. - Surface the field on \`AgentMessagePartDTO\` (GraphQL). - Preserve it through \`mapUIMessagePartsToDBParts\` (server) and both \`mapDBPartToUIMessagePart\` mappers (server + frontend). - Include it in \`GET_CHAT_MESSAGES\` and \`GET_AGENT_TURNS\` selections. - Regenerate \`generated-metadata/graphql.ts\`. ### Backwards compatibility Existing rows have \`NULL providerExecuted\` and round-trip as the omitted flag — which is exactly the pre-fix behaviour for tool parts that were never provider-executed. Only *new* assistant messages using \`web_search\` (or other provider-hosted tools) will write \`true\`, and those are the only ones that were breaking. ## Test plan - [x] \`npx tsgo\` typecheck — server + front clean - [x] \`oxlint\` + \`prettier --check\` on all touched files — clean - [x] \`npx nx run twenty-server:database:migrate:prod\` runs the new instance command locally; \`providerExecuted\` column present on \`core.agentMessagePart\` - [x] Regenerated \`generated-metadata/graphql.ts\` — \`providerExecuted\` wired into both queries and \`AgentMessagePart\` type - [ ] Manual: start a chat with Anthropic web_search enabled, invoke the tool in turn 1, reply in turn 2 — should not throw the srvtoolu error 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5f604a5 commit 251f5de

20 files changed

Lines changed: 84 additions & 22 deletions

File tree

packages/twenty-client-sdk/src/metadata/generated/schema.graphql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2440,6 +2440,7 @@ type AgentMessagePart {
24402440
toolInput: JSON
24412441
toolOutput: JSON
24422442
state: String
2443+
providerExecuted: Boolean
24432444
errorMessage: String
24442445
errorDetails: JSON
24452446
sourceUrlSourceId: String

packages/twenty-client-sdk/src/metadata/generated/schema.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2116,6 +2116,7 @@ export interface AgentMessagePart {
21162116
toolInput?: Scalars['JSON']
21172117
toolOutput?: Scalars['JSON']
21182118
state?: Scalars['String']
2119+
providerExecuted?: Scalars['Boolean']
21192120
errorMessage?: Scalars['String']
21202121
errorDetails?: Scalars['JSON']
21212122
sourceUrlSourceId?: Scalars['String']
@@ -5110,6 +5111,7 @@ export interface AgentMessagePartGenqlSelection{
51105111
toolInput?: boolean | number
51115112
toolOutput?: boolean | number
51125113
state?: boolean | number
5114+
providerExecuted?: boolean | number
51135115
errorMessage?: boolean | number
51145116
errorDetails?: boolean | number
51155117
sourceUrlSourceId?: boolean | number

packages/twenty-client-sdk/src/metadata/generated/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4809,6 +4809,9 @@ export default {
48094809
"state": [
48104810
1
48114811
],
4812+
"providerExecuted": [
4813+
6
4814+
],
48124815
"errorMessage": [
48134816
1
48144817
],

packages/twenty-front/src/generated-metadata/graphql.ts

Lines changed: 5 additions & 4 deletions
Large diffs are not rendered by default.

packages/twenty-front/src/modules/ai/graphql/queries/getAgentTurns.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const GET_AGENT_TURNS = gql`
2828
toolOutput
2929
errorMessage
3030
state
31+
providerExecuted
3132
errorDetails
3233
sourceUrlSourceId
3334
sourceUrlUrl

packages/twenty-front/src/modules/ai/graphql/queries/getChatMessages.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const GET_CHAT_MESSAGES = gql`
2121
toolInput
2222
toolOutput
2323
state
24+
providerExecuted
2425
errorMessage
2526
errorDetails
2627
sourceUrlSourceId

packages/twenty-front/src/modules/ai/utils/mapDBPartToUIMessagePart.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ export const mapDBPartToUIMessagePart = (
7171
output: part.toolOutput,
7272
errorText: part.errorMessage!,
7373
state: part.state,
74+
...(part.providerExecuted != null && {
75+
providerExecuted: part.providerExecuted,
76+
}),
7477
} as ToolUIPart;
7578
}
7679
}

packages/twenty-server/src/database/commands/__tests__/__snapshots__/instance-command-generation.service.spec.ts.snap

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ export class TestFastInstanceCommand implements FastInstanceCommand {
2626
exports[`InstanceCommandGenerationService should escape backslashes in SQL queries 1`] = `
2727
{
2828
"className": "UpdatePathFastInstanceCommand",
29-
"fileName": "2-0-instance-command-fast-1775000000000-update-path.ts",
29+
"fileName": "2-1-instance-command-fast-1775000000000-update-path.ts",
3030
"fileTemplate": "import { QueryRunner } from 'typeorm';
3131
3232
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
3333
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
3434
35-
@RegisteredInstanceCommand('2.0.0', 1775000000000)
35+
@RegisteredInstanceCommand('2.1.0', 1775000000000)
3636
export class UpdatePathFastInstanceCommand implements FastInstanceCommand {
3737
public async up(queryRunner: QueryRunner): Promise<void> {
3838
await queryRunner.query('UPDATE "core"."config" SET "value" = E\\'path\\\\\\\\to\\\\\\\\file\\'');
@@ -49,13 +49,13 @@ export class UpdatePathFastInstanceCommand implements FastInstanceCommand {
4949
exports[`InstanceCommandGenerationService should escape single quotes in SQL queries 1`] = `
5050
{
5151
"className": "UpdateConfigFastInstanceCommand",
52-
"fileName": "2-0-instance-command-fast-1775000000000-update-config.ts",
52+
"fileName": "2-1-instance-command-fast-1775000000000-update-config.ts",
5353
"fileTemplate": "import { QueryRunner } from 'typeorm';
5454
5555
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
5656
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
5757
58-
@RegisteredInstanceCommand('2.0.0', 1775000000000)
58+
@RegisteredInstanceCommand('2.1.0', 1775000000000)
5959
export class UpdateConfigFastInstanceCommand implements FastInstanceCommand {
6060
public async up(queryRunner: QueryRunner): Promise<void> {
6161
await queryRunner.query('UPDATE "core"."config" SET "value" = \\'it\\'\\'s done\\'');
@@ -72,13 +72,13 @@ export class UpdateConfigFastInstanceCommand implements FastInstanceCommand {
7272
exports[`InstanceCommandGenerationService should generate a migration with a single up/down query 1`] = `
7373
{
7474
"className": "AddFooColumnFastInstanceCommand",
75-
"fileName": "2-0-instance-command-fast-1775000000000-add-foo-column.ts",
75+
"fileName": "2-1-instance-command-fast-1775000000000-add-foo-column.ts",
7676
"fileTemplate": "import { QueryRunner } from 'typeorm';
7777
7878
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
7979
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
8080
81-
@RegisteredInstanceCommand('2.0.0', 1775000000000)
81+
@RegisteredInstanceCommand('2.1.0', 1775000000000)
8282
export class AddFooColumnFastInstanceCommand implements FastInstanceCommand {
8383
public async up(queryRunner: QueryRunner): Promise<void> {
8484
await queryRunner.query('ALTER TABLE "core"."user" ADD "foo" varchar');
@@ -95,13 +95,13 @@ export class AddFooColumnFastInstanceCommand implements FastInstanceCommand {
9595
exports[`InstanceCommandGenerationService should generate a migration with multiple queries 1`] = `
9696
{
9797
"className": "CreateTaskTableFastInstanceCommand",
98-
"fileName": "2-0-instance-command-fast-1775000000000-create-task-table.ts",
98+
"fileName": "2-1-instance-command-fast-1775000000000-create-task-table.ts",
9999
"fileTemplate": "import { QueryRunner } from 'typeorm';
100100
101101
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
102102
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
103103
104-
@RegisteredInstanceCommand('2.0.0', 1775000000000)
104+
@RegisteredInstanceCommand('2.1.0', 1775000000000)
105105
export class CreateTaskTableFastInstanceCommand implements FastInstanceCommand {
106106
public async up(queryRunner: QueryRunner): Promise<void> {
107107
await queryRunner.query('CREATE TABLE "core"."task" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" varchar NOT NULL)');
@@ -120,13 +120,13 @@ export class CreateTaskTableFastInstanceCommand implements FastInstanceCommand {
120120
exports[`InstanceCommandGenerationService should generate a migration with query parameters 1`] = `
121121
{
122122
"className": "SeedSettingFastInstanceCommand",
123-
"fileName": "2-0-instance-command-fast-1775000000000-seed-setting.ts",
123+
"fileName": "2-1-instance-command-fast-1775000000000-seed-setting.ts",
124124
"fileTemplate": "import { QueryRunner } from 'typeorm';
125125
126126
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
127127
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
128128
129-
@RegisteredInstanceCommand('2.0.0', 1775000000000)
129+
@RegisteredInstanceCommand('2.1.0', 1775000000000)
130130
export class SeedSettingFastInstanceCommand implements FastInstanceCommand {
131131
public async up(queryRunner: QueryRunner): Promise<void> {
132132
await queryRunner.query('INSERT INTO "core"."setting" ("key", "value") VALUES ($1, $2)', ["theme","dark"]);
@@ -143,13 +143,13 @@ export class SeedSettingFastInstanceCommand implements FastInstanceCommand {
143143
exports[`InstanceCommandGenerationService should generate a slow instance command with populated up/down 1`] = `
144144
{
145145
"className": "MakeColumnNotNullableSlowInstanceCommand",
146-
"fileName": "2-0-instance-command-slow-1775000000000-make-column-not-nullable.ts",
146+
"fileName": "2-1-instance-command-slow-1775000000000-make-column-not-nullable.ts",
147147
"fileTemplate": "import { DataSource, QueryRunner } from 'typeorm';
148148
149149
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
150150
import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface';
151151
152-
@RegisteredInstanceCommand('2.0.0', 1775000000000, { type: 'slow' })
152+
@RegisteredInstanceCommand('2.1.0', 1775000000000, { type: 'slow' })
153153
export class MakeColumnNotNullableSlowInstanceCommand implements SlowInstanceCommand {
154154
async runDataMigration(dataSource: DataSource): Promise<void> {
155155
// TODO: implement data backfill before the DDL migration
@@ -170,13 +170,13 @@ export class MakeColumnNotNullableSlowInstanceCommand implements SlowInstanceCom
170170
exports[`InstanceCommandGenerationService should use default migration name in class and file names 1`] = `
171171
{
172172
"className": "AutoGeneratedFastInstanceCommand",
173-
"fileName": "2-0-instance-command-fast-1775000000000-auto-generated.ts",
173+
"fileName": "2-1-instance-command-fast-1775000000000-auto-generated.ts",
174174
"fileTemplate": "import { QueryRunner } from 'typeorm';
175175
176176
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
177177
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
178178
179-
@RegisteredInstanceCommand('2.0.0', 1775000000000)
179+
@RegisteredInstanceCommand('2.1.0', 1775000000000)
180180
export class AutoGeneratedFastInstanceCommand implements FastInstanceCommand {
181181
public async up(queryRunner: QueryRunner): Promise<void> {
182182
await queryRunner.query('ALTER TABLE "core"."user" ADD "bar" integer');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { QueryRunner } from 'typeorm';
2+
3+
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
4+
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
5+
6+
@RegisteredInstanceCommand('2.1.0', 1777012800000)
7+
export class AddProviderExecutedToAgentMessagePartFastInstanceCommand
8+
implements FastInstanceCommand
9+
{
10+
public async up(queryRunner: QueryRunner): Promise<void> {
11+
await queryRunner.query(
12+
'ALTER TABLE "core"."agentMessagePart" ADD "providerExecuted" boolean',
13+
);
14+
}
15+
16+
public async down(queryRunner: QueryRunner): Promise<void> {
17+
await queryRunner.query(
18+
'ALTER TABLE "core"."agentMessagePart" DROP COLUMN "providerExecuted"',
19+
);
20+
}
21+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Module } from '@nestjs/common';
2+
3+
@Module({
4+
imports: [],
5+
providers: [],
6+
})
7+
export class V2_2_UpgradeVersionCommandModule {}

0 commit comments

Comments
 (0)