Skip to content

Commit c60a128

Browse files
authored
fix(copilot): declare item schemas on table tool array params (#7458)
The catalog declared updates, rows, order, columnNames, and disabledTagIds as arrays with no items schema. The provider-path sanitizer fills a missing items with {type: "string"}, so the model was told batch_update_rows takes a list of strings and sent [] or ["rowId", "data"]; the executor then crashed on Object.entries(undefined) and masked it as a generic table failure. Sync the catalog with item schemas so the router's Ajv validation rejects a malformed call with an actionable error, validate element shape in the table tool as a last line, and match operation verbs as tokens in tool-call titles.
1 parent 0664c6f commit c60a128

7 files changed

Lines changed: 406 additions & 40 deletions

File tree

apps/sim/lib/copilot/generated/tool-catalog-v1.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3346,6 +3346,7 @@ export const ManageKnowledgeBase: ToolCatalogEntry = {
33463346
type: 'array',
33473347
description:
33483348
'Tag definition IDs to opt out of (optional for add_connector). See tagDefinitions in the connector schema.',
3349+
items: { type: 'string' },
33493350
},
33503351
documentId: { type: 'string', description: 'Document ID (required for update_document)' },
33513352
documentIds: {
@@ -4224,6 +4225,14 @@ export const QueryUserTable: ToolCatalogEntry = {
42244225
type: 'array',
42254226
description:
42264227
'Sort spec for query_rows (optional). Ordered list of {field, direction} where direction is asc or desc, e.g. [{"field":"wins","direction":"desc"},{"field":"name","direction":"asc"}].',
4228+
items: {
4229+
type: 'object',
4230+
properties: {
4231+
direction: { type: 'string', enum: ['asc', 'desc'] },
4232+
field: { type: 'string' },
4233+
},
4234+
required: ['field', 'direction'],
4235+
},
42274236
},
42284237
rowId: { type: 'string', description: 'Row ID (required for get_row)' },
42294238
tableId: { type: 'string', description: 'Table ID (required for all operations)' },
@@ -5484,6 +5493,7 @@ export const TableColumns: ToolCatalogEntry = {
54845493
type: 'array',
54855494
description:
54865495
'Array of column names to delete at once (preferred for multi-column delete_column)',
5496+
items: { type: 'string' },
54875497
},
54885498
multiple: {
54895499
type: 'boolean',
@@ -5751,12 +5761,18 @@ export const TableRows: ToolCatalogEntry = {
57515761
type: 'array',
57525762
description:
57535763
'Array of row data objects (required for batch_insert_rows). TTL cells take absolute whole Unix epoch seconds, never JavaScript milliseconds; a missing or null TTL means no expiration.',
5764+
items: { type: 'object' },
57545765
},
57555766
tableId: { type: 'string', description: 'Table ID (required for every operation)' },
57565767
updates: {
57575768
type: 'array',
57585769
description:
57595770
"Array of per-row updates: [{ rowId, data: { col: val } }] (batch_update_rows format a). TTL values are absolute whole Unix epoch seconds, never JavaScript milliseconds; omit a row's TTL key to preserve it or set it to null to clear the expiration.",
5771+
items: {
5772+
type: 'object',
5773+
properties: { data: { type: 'object' }, rowId: { type: 'string' } },
5774+
required: ['rowId', 'data'],
5775+
},
57605776
},
57615777
values: {
57625778
type: 'object',
@@ -6098,6 +6114,7 @@ export const UserTable: ToolCatalogEntry = {
60986114
type: 'array',
60996115
description:
61006116
'Array of column names to delete at once (for delete_column). Preferred over columnName when deleting multiple columns.',
6117+
items: { type: 'string' },
61016118
},
61026119
cursor: {
61036120
type: 'string',
@@ -6237,6 +6254,14 @@ export const UserTable: ToolCatalogEntry = {
62376254
type: 'array',
62386255
description:
62396256
'Sort spec for query_rows (optional). Ordered list of {field, direction} where direction is asc or desc, e.g. [{"field":"wins","direction":"desc"},{"field":"name","direction":"asc"}].',
6257+
items: {
6258+
type: 'object',
6259+
properties: {
6260+
direction: { type: 'string', enum: ['asc', 'desc'] },
6261+
field: { type: 'string' },
6262+
},
6263+
required: ['field', 'direction'],
6264+
},
62406265
},
62416266
outputColumnNames: {
62426267
type: 'object',
@@ -6305,6 +6330,7 @@ export const UserTable: ToolCatalogEntry = {
63056330
type: 'array',
63066331
description:
63076332
'Array of row data objects (required for batch_insert_rows). TTL cells take absolute whole Unix epoch seconds, never JavaScript milliseconds; a missing or null TTL means no expiration.',
6333+
items: { type: 'object' },
63086334
},
63096335
runMode: {
63106336
type: 'string',
@@ -6341,6 +6367,11 @@ export const UserTable: ToolCatalogEntry = {
63416367
type: 'array',
63426368
description:
63436369
"Array of per-row updates: [{ rowId, data: { col: val } }] (for batch_update_rows). TTL values are absolute whole Unix epoch seconds, never JavaScript milliseconds; omit a row's TTL key to preserve it or set it to null to clear the expiration.",
6370+
items: {
6371+
type: 'object',
6372+
properties: { data: { type: 'object' }, rowId: { type: 'string' } },
6373+
required: ['rowId', 'data'],
6374+
},
63446375
},
63456376
values: {
63466377
type: 'object',

apps/sim/lib/copilot/generated/tool-schemas-v1.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3227,6 +3227,9 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
32273227
type: 'array',
32283228
description:
32293229
'Tag definition IDs to opt out of (optional for add_connector). See tagDefinitions in the connector schema.',
3230+
items: {
3231+
type: 'string',
3232+
},
32303233
},
32313234
documentId: {
32323235
type: 'string',
@@ -4105,6 +4108,19 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
41054108
type: 'array',
41064109
description:
41074110
'Sort spec for query_rows (optional). Ordered list of {field, direction} where direction is asc or desc, e.g. [{"field":"wins","direction":"desc"},{"field":"name","direction":"asc"}].',
4111+
items: {
4112+
type: 'object',
4113+
properties: {
4114+
direction: {
4115+
type: 'string',
4116+
enum: ['asc', 'desc'],
4117+
},
4118+
field: {
4119+
type: 'string',
4120+
},
4121+
},
4122+
required: ['field', 'direction'],
4123+
},
41084124
},
41094125
rowId: {
41104126
type: 'string',
@@ -5359,6 +5375,9 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
53595375
type: 'array',
53605376
description:
53615377
'Array of column names to delete at once (preferred for multi-column delete_column)',
5378+
items: {
5379+
type: 'string',
5380+
},
53625381
},
53635382
multiple: {
53645383
type: 'boolean',
@@ -5665,6 +5684,9 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
56655684
type: 'array',
56665685
description:
56675686
'Array of row data objects (required for batch_insert_rows). TTL cells take absolute whole Unix epoch seconds, never JavaScript milliseconds; a missing or null TTL means no expiration.',
5687+
items: {
5688+
type: 'object',
5689+
},
56685690
},
56695691
tableId: {
56705692
type: 'string',
@@ -5674,6 +5696,18 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
56745696
type: 'array',
56755697
description:
56765698
"Array of per-row updates: [{ rowId, data: { col: val } }] (batch_update_rows format a). TTL values are absolute whole Unix epoch seconds, never JavaScript milliseconds; omit a row's TTL key to preserve it or set it to null to clear the expiration.",
5699+
items: {
5700+
type: 'object',
5701+
properties: {
5702+
data: {
5703+
type: 'object',
5704+
},
5705+
rowId: {
5706+
type: 'string',
5707+
},
5708+
},
5709+
required: ['rowId', 'data'],
5710+
},
56775711
},
56785712
values: {
56795713
type: 'object',
@@ -6032,6 +6066,9 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
60326066
type: 'array',
60336067
description:
60346068
'Array of column names to delete at once (for delete_column). Preferred over columnName when deleting multiple columns.',
6069+
items: {
6070+
type: 'string',
6071+
},
60356072
},
60366073
cursor: {
60376074
type: 'string',
@@ -6186,6 +6223,19 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
61866223
type: 'array',
61876224
description:
61886225
'Sort spec for query_rows (optional). Ordered list of {field, direction} where direction is asc or desc, e.g. [{"field":"wins","direction":"desc"},{"field":"name","direction":"asc"}].',
6226+
items: {
6227+
type: 'object',
6228+
properties: {
6229+
direction: {
6230+
type: 'string',
6231+
enum: ['asc', 'desc'],
6232+
},
6233+
field: {
6234+
type: 'string',
6235+
},
6236+
},
6237+
required: ['field', 'direction'],
6238+
},
61896239
},
61906240
outputColumnNames: {
61916241
type: 'object',
@@ -6262,6 +6312,9 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
62626312
type: 'array',
62636313
description:
62646314
'Array of row data objects (required for batch_insert_rows). TTL cells take absolute whole Unix epoch seconds, never JavaScript milliseconds; a missing or null TTL means no expiration.',
6315+
items: {
6316+
type: 'object',
6317+
},
62656318
},
62666319
runMode: {
62676320
type: 'string',
@@ -6300,6 +6353,18 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
63006353
type: 'array',
63016354
description:
63026355
"Array of per-row updates: [{ rowId, data: { col: val } }] (for batch_update_rows). TTL values are absolute whole Unix epoch seconds, never JavaScript milliseconds; omit a row's TTL key to preserve it or set it to null to clear the expiration.",
6356+
items: {
6357+
type: 'object',
6358+
properties: {
6359+
data: {
6360+
type: 'object',
6361+
},
6362+
rowId: {
6363+
type: 'string',
6364+
},
6365+
},
6366+
required: ['rowId', 'data'],
6367+
},
63036368
},
63046369
values: {
63056370
type: 'object',
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { validateGeneratedToolPayload } from '@/lib/copilot/tools/server/generated-schema'
6+
import { OrchestrationError } from '@/lib/core/orchestration/types'
7+
8+
/**
9+
* The shapes below are what an agent actually sent when the catalog advertised
10+
* `updates` as a bare array: the provider-path sanitizer filled the missing
11+
* `items` with {type: "string"}, so the model produced `["rowId", "data"]` and
12+
* `[]`, and the executor crashed on the strings. With the item schema synced
13+
* from the catalog, the router refuses them as a classified input error the
14+
* model can correct.
15+
*/
16+
describe('validateGeneratedToolPayload table_rows parameters', () => {
17+
it('rejects string elements in updates as the caller error they are', () => {
18+
expect(() =>
19+
validateGeneratedToolPayload('table_rows', 'parameters', {
20+
operation: 'batch_update_rows',
21+
args: { tableId: 'tbl_1', updates: ['rowId', 'data'] },
22+
})
23+
).toThrow(OrchestrationError)
24+
expect(() =>
25+
validateGeneratedToolPayload('table_rows', 'parameters', {
26+
operation: 'batch_update_rows',
27+
args: { tableId: 'tbl_1', updates: ['rowId', 'data'] },
28+
})
29+
).toThrow(/\/args\/updates\/0 must be object/)
30+
})
31+
32+
it('rejects an update patch that omits its data object', () => {
33+
expect(() =>
34+
validateGeneratedToolPayload('table_rows', 'parameters', {
35+
operation: 'batch_update_rows',
36+
args: { tableId: 'tbl_1', updates: [{ rowId: 'row-1' }] },
37+
})
38+
).toThrow(/\/args\/updates\/0 must have required property 'data'/)
39+
})
40+
41+
it('rejects a non-object row in batch_insert_rows', () => {
42+
expect(() =>
43+
validateGeneratedToolPayload('table_rows', 'parameters', {
44+
operation: 'batch_insert_rows',
45+
args: { tableId: 'tbl_1', rows: [{ name: 'Ada' }, 'Bob'] },
46+
})
47+
).toThrow(/\/args\/rows\/1 must be object/)
48+
})
49+
50+
it('accepts the documented per-row patch shape', () => {
51+
const payload = {
52+
operation: 'batch_update_rows',
53+
args: { tableId: 'tbl_1', updates: [{ rowId: 'row-1', data: { status: 'active' } }] },
54+
}
55+
expect(validateGeneratedToolPayload('table_rows', 'parameters', payload)).toBe(payload)
56+
})
57+
58+
it('accepts a sort spec on query_user_table order', () => {
59+
const payload = {
60+
operation: 'query_rows',
61+
args: { tableId: 'tbl_1', order: [{ field: 'age', direction: 'desc' }] },
62+
}
63+
expect(validateGeneratedToolPayload('query_user_table', 'parameters', payload)).toBe(payload)
64+
expect(() =>
65+
validateGeneratedToolPayload('query_user_table', 'parameters', {
66+
operation: 'query_rows',
67+
args: { tableId: 'tbl_1', order: ['age'] },
68+
})
69+
).toThrow(/\/args\/order\/0 must be object/)
70+
})
71+
})

0 commit comments

Comments
 (0)