mirror of
https://github.com/simstudioai/sim.git
synced 2026-11-03 15:22:02 +08:00
Support split table tools and option recovery
This commit is contained in:
+30
@@ -1330,3 +1330,33 @@ describe('parseSpecialTags sim_key placeholder', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('recoverTrailingBareOptions', () => {
|
||||
const bareOptions =
|
||||
'{"1": {"title": "Fix the tracker", "description": "debug"}, "2": {"title": "Inspect the miss", "description": "look"}}'
|
||||
|
||||
it('renders a trailing bare-JSON options payload as an options card', () => {
|
||||
const { segments } = parseSpecialTags(`Here they are.\n${bareOptions}`, false)
|
||||
const last = segments[segments.length - 1]
|
||||
expect(last.type).toBe('options')
|
||||
if (last.type === 'options') {
|
||||
expect(last.data['1']?.title).toBe('Fix the tracker')
|
||||
}
|
||||
expect(segments[0]).toEqual({ type: 'text', content: 'Here they are.' })
|
||||
})
|
||||
|
||||
it('never recovers mid-stream — a partial JSON tail must not flicker into a card', () => {
|
||||
const { segments } = parseSpecialTags(`Here they are.\n${bareOptions}`, true)
|
||||
expect(segments.every((segment) => segment.type === 'text')).toBe(true)
|
||||
})
|
||||
|
||||
it('leaves ordinary JSON prose alone', () => {
|
||||
const { segments } = parseSpecialTags('The config is {"retries": 3, "mode": "fast"}', false)
|
||||
expect(segments.every((segment) => segment.type === 'text')).toBe(true)
|
||||
})
|
||||
|
||||
it('does not double-render when a real options tag already parsed', () => {
|
||||
const { segments } = parseSpecialTags(`Pick one <options>${bareOptions}</options>`, false)
|
||||
expect(segments.filter((segment) => segment.type === 'options')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
+43
@@ -1442,8 +1442,51 @@ export function parseSpecialTags(content: string, isStreaming: boolean): ParsedS
|
||||
segments.push({ type: 'text', content })
|
||||
}
|
||||
|
||||
if (!isStreaming) {
|
||||
recoverTrailingBareOptions(segments)
|
||||
}
|
||||
|
||||
return { segments, hasPendingTag }
|
||||
}
|
||||
/**
|
||||
* Recovers a trailing bare-JSON options payload the model emitted WITHOUT the
|
||||
* `<options>` wrapper (observed when an automation prompt asks the model to
|
||||
* "(re)send suggested actions" and it answers with the JSON as content). The
|
||||
* shape check is strict — a non-empty object whose every value is
|
||||
* { title, description } with numeric-string keys — so ordinary JSON in prose
|
||||
* cannot false-positive. Only a message's FINAL text segment is considered,
|
||||
* mirroring the tag contract (options go last), and only when no options tag
|
||||
* already parsed. Never applied mid-stream: a partial JSON tail must not
|
||||
* flicker between prose and a card.
|
||||
*/
|
||||
function recoverTrailingBareOptions(segments: ContentSegment[]): void {
|
||||
const last = segments[segments.length - 1]
|
||||
if (!last || last.type !== 'text') return
|
||||
if (segments.some((segment) => segment.type === 'options')) return
|
||||
const text = last.content
|
||||
if (!text.trimEnd().endsWith('}')) return
|
||||
// The payload nests objects, so the START brace is the first one from which
|
||||
// the remainder parses — probe brace positions left to right (bounded).
|
||||
let start = -1
|
||||
let parsed: unknown
|
||||
let probe = text.indexOf('{')
|
||||
for (let attempts = 0; probe !== -1 && attempts < 20; attempts++) {
|
||||
try {
|
||||
parsed = JSON.parse(text.slice(probe).trim())
|
||||
start = probe
|
||||
break
|
||||
} catch {
|
||||
probe = text.indexOf('{', probe + 1)
|
||||
}
|
||||
}
|
||||
if (start === -1) return
|
||||
if (!isOptionsTagData(parsed) || Object.keys(parsed as object).length === 0) return
|
||||
if (!Object.keys(parsed as object).every((key) => /^\d+$/.test(key))) return
|
||||
const prefix = text.slice(0, start).replace(/\s+$/, '')
|
||||
segments.pop()
|
||||
if (prefix) segments.push({ type: 'text', content: prefix })
|
||||
segments.push({ type: 'options', data: parsed })
|
||||
}
|
||||
|
||||
interface SpecialTagsProps {
|
||||
segment: Exclude<ContentSegment, { type: 'text' }>
|
||||
|
||||
@@ -113,6 +113,11 @@ export interface ToolCatalogEntry {
|
||||
| 'set_global_workflow_variables'
|
||||
| 'share_file'
|
||||
| 'table'
|
||||
| 'table_automations'
|
||||
| 'table_columns'
|
||||
| 'table_enrichments'
|
||||
| 'table_manage'
|
||||
| 'table_rows'
|
||||
| 'terminal'
|
||||
| 'update_deployment_version'
|
||||
| 'update_workspace_mcp_server'
|
||||
@@ -229,6 +234,11 @@ export interface ToolCatalogEntry {
|
||||
| 'set_global_workflow_variables'
|
||||
| 'share_file'
|
||||
| 'table'
|
||||
| 'table_automations'
|
||||
| 'table_columns'
|
||||
| 'table_enrichments'
|
||||
| 'table_manage'
|
||||
| 'table_rows'
|
||||
| 'terminal'
|
||||
| 'update_deployment_version'
|
||||
| 'update_workspace_mcp_server'
|
||||
@@ -2067,7 +2077,7 @@ export const EnrichmentRun: ToolCatalogEntry = {
|
||||
enrichmentId: {
|
||||
type: 'string',
|
||||
description:
|
||||
"Which enrichment to run. Discover the full set and each one's inputs/outputs via user_table.list_enrichments.",
|
||||
"Which enrichment to run. Discover the full set and each one's inputs/outputs via table_enrichments.list_enrichments.",
|
||||
enum: [
|
||||
'work-email',
|
||||
'phone-number',
|
||||
@@ -4900,6 +4910,505 @@ export const Table: ToolCatalogEntry = {
|
||||
internal: true,
|
||||
}
|
||||
|
||||
export const TableAutomations: ToolCatalogEntry = {
|
||||
id: 'table_automations',
|
||||
name: 'table_automations',
|
||||
route: 'sim',
|
||||
mode: 'async',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
args: {
|
||||
type: 'object',
|
||||
description: 'Arguments for the operation',
|
||||
properties: {
|
||||
autoRun: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
"On add: true fires dep-satisfied rows immediately (only when the user explicitly asked); default false stages silently. On update: toggles the group's auto-fire on dep satisfaction.",
|
||||
},
|
||||
blockId: {
|
||||
type: 'string',
|
||||
description: 'Source block ID inside the workflow (add_workflow_group_output)',
|
||||
},
|
||||
columnName: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Target column name: required for delete_workflow_group_output (the bound column to drop); optional for add_workflow_group_output (auto-derived from path)',
|
||||
},
|
||||
dependencies: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Dependencies before a row runs: { columns?: string[] } of input column names that must be filled. Output columns of upstream groups are valid; a group cannot depend on its own outputs.',
|
||||
properties: {
|
||||
columns: {
|
||||
type: 'array',
|
||||
description: 'Input column names that must be filled before the group runs a row.',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
deploymentMode: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Which workflow version rows execute: "live" (default, editable draft — edits take effect immediately) or "deployed" (latest active deployment; fails if the workflow was never deployed).',
|
||||
enum: ['live', 'deployed'],
|
||||
},
|
||||
groupId: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Workflow group ID (required for update_workflow_group, delete_workflow_group, add_workflow_group_output, delete_workflow_group_output)',
|
||||
},
|
||||
groupIds: {
|
||||
type: 'array',
|
||||
description: 'Workflow group IDs to fire (required for run_column, non-empty)',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
mappingUpdates: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Surgical per-output remap for update_workflow_group: each entry repoints ONE existing output column to a new (blockId, path) without touching the rest. Stale cells clear and backfill from saved execution logs where possible. Discover valid pairs via list_workflow_outputs first.',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
blockId: { type: 'string', description: 'New source block ID for this column.' },
|
||||
columnName: {
|
||||
type: 'string',
|
||||
description: 'The existing output column to remap (must be bound to this group).',
|
||||
},
|
||||
path: { type: 'string', description: 'New dotted output path on the new block.' },
|
||||
},
|
||||
required: ['columnName', 'blockId', 'path'],
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Display name for the group (optional on add/update)',
|
||||
},
|
||||
outputPath: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Write this call\'s result to a NEW workspace file instead of returning it (e.g. "files/export.csv"). On success the tool result is REPLACED by a file receipt (fileId, vfsPath, size) — set it only when the file IS the goal. ".csv" serializes rows as a CSV table; ".json"/".txt"/".md"/".html" write pretty-printed JSON of the full result envelope. Missing parent folders are created; an existing path fails.',
|
||||
},
|
||||
outputs: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Outputs to surface as columns for add_workflow_group: each { blockId, path, columnName?, columnType? }; columnName auto-derives from path, columnType from the leaf type. Validated against list_workflow_outputs — invalid picks return the valid options. For update_workflow_group prefer add/delete_workflow_group_output and mappingUpdates; pass outputs only to restructure the whole set.',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
blockId: { type: 'string', description: 'Source block ID inside the workflow.' },
|
||||
columnName: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Optional target column name; auto-derived from the path when omitted.',
|
||||
},
|
||||
columnType: {
|
||||
type: 'string',
|
||||
description: 'Optional column type; defaults from the leaf type when omitted.',
|
||||
enum: ['string', 'number', 'boolean', 'date', 'json'],
|
||||
},
|
||||
path: { type: 'string', description: 'Dotted output path on the block.' },
|
||||
},
|
||||
required: ['blockId', 'path'],
|
||||
},
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'Dotted output path on the block (add_workflow_group_output)',
|
||||
},
|
||||
rowId: { type: 'string', description: 'Row ID for cancel_table_runs with scope "row".' },
|
||||
rowIds: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Optional row scope for run_column: only these rows are candidates (server eligibility still applies); omit for the whole table.',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
runMode: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Run mode for run_column: "incomplete" (default) re-runs only rows with no output or a last failure; "all" re-runs every dep-satisfied row.',
|
||||
enum: ['incomplete', 'all'],
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Cancellation scope for cancel_table_runs: "all" (whole table) or "row" (requires rowId).',
|
||||
enum: ['all', 'row'],
|
||||
},
|
||||
tableId: {
|
||||
type: 'string',
|
||||
description: 'Table ID (required for everything except list_workflow_outputs)',
|
||||
},
|
||||
workflowId: {
|
||||
type: 'string',
|
||||
description: 'Workflow ID (required for add_workflow_group and list_workflow_outputs)',
|
||||
},
|
||||
},
|
||||
},
|
||||
operation: {
|
||||
type: 'string',
|
||||
description: 'The automation operation to perform',
|
||||
enum: [
|
||||
'list_workflow_outputs',
|
||||
'add_workflow_group',
|
||||
'update_workflow_group',
|
||||
'delete_workflow_group',
|
||||
'add_workflow_group_output',
|
||||
'delete_workflow_group_output',
|
||||
'run_column',
|
||||
'cancel_table_runs',
|
||||
],
|
||||
},
|
||||
},
|
||||
required: ['operation', 'args'],
|
||||
},
|
||||
resultSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: { type: 'object', description: 'Operation-specific result payload.' },
|
||||
message: { type: 'string', description: 'Human-readable outcome summary.' },
|
||||
success: { type: 'boolean', description: 'Whether the operation succeeded.' },
|
||||
},
|
||||
required: ['success', 'message'],
|
||||
},
|
||||
}
|
||||
|
||||
export const TableColumns: ToolCatalogEntry = {
|
||||
id: 'table_columns',
|
||||
name: 'table_columns',
|
||||
route: 'sim',
|
||||
mode: 'async',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
args: {
|
||||
type: 'object',
|
||||
description: 'Arguments for the operation',
|
||||
properties: {
|
||||
column: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Column definition for add_column: { name, type, unique?, position? }; select (enum) columns also take { options: [names], multiple?: true } — options is required for select.',
|
||||
},
|
||||
columnName: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Column name (required for rename_column and update_column; single-column delete_column)',
|
||||
},
|
||||
columnNames: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Array of column names to delete at once (preferred for multi-column delete_column)',
|
||||
},
|
||||
multiple: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Whether a select cell may hold several options (default false). Switching true → false fails if any row has more than one selected.',
|
||||
},
|
||||
newName: { type: 'string', description: 'New column name (required for rename_column)' },
|
||||
newType: {
|
||||
type: 'string',
|
||||
description:
|
||||
'New column type for update_column: string, number, boolean, date, json, select. Converting to select also requires options; conversion fails if an existing cell value matches no option. A multiple select round-trips through text as a comma-separated cell.',
|
||||
},
|
||||
options: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Choices for a select (enum) column as display names, e.g. ["Open", "Closed"]. Required when creating or converting to select. On update_column this REPLACES the whole list, matched BY NAME — send the full list including options you keep; omitting one deletes it and clears its cells. Max 100.',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
outputPath: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Write this call\'s result to a NEW workspace file instead of returning it (e.g. "files/export.csv"). On success the tool result is REPLACED by a file receipt (fileId, vfsPath, size) — set it only when the file IS the goal. ".csv" serializes rows as a CSV table; ".json"/".txt"/".md"/".html" write pretty-printed JSON of the full result envelope. Missing parent folders are created; an existing path fails.',
|
||||
},
|
||||
tableId: { type: 'string', description: 'Table ID (required for every operation)' },
|
||||
unique: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Set or clear the column unique constraint (update_column; not supported on select columns)',
|
||||
},
|
||||
},
|
||||
required: ['tableId'],
|
||||
},
|
||||
operation: {
|
||||
type: 'string',
|
||||
description: 'The column operation to perform',
|
||||
enum: ['add_column', 'rename_column', 'delete_column', 'update_column'],
|
||||
},
|
||||
},
|
||||
required: ['operation', 'args'],
|
||||
},
|
||||
resultSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: { type: 'object', description: 'Operation-specific result payload.' },
|
||||
message: { type: 'string', description: 'Human-readable outcome summary.' },
|
||||
success: { type: 'boolean', description: 'Whether the operation succeeded.' },
|
||||
},
|
||||
required: ['success', 'message'],
|
||||
},
|
||||
}
|
||||
|
||||
export const TableEnrichments: ToolCatalogEntry = {
|
||||
id: 'table_enrichments',
|
||||
name: 'table_enrichments',
|
||||
route: 'sim',
|
||||
mode: 'async',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
args: {
|
||||
type: 'object',
|
||||
description: 'Arguments for the operation',
|
||||
properties: {
|
||||
autoRun: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'true fires dep-satisfied rows immediately on add (only when the user explicitly asked); default false stages silently — fire later via table_automations run_column.',
|
||||
},
|
||||
dependencies: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Optional dependency override: { columns?: string[] }; omit to default to the mapped input columns.',
|
||||
properties: {
|
||||
columns: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Input column names that must be filled before the enrichment runs a row.',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
enrichmentId: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Enrichment registry ID for add_enrichment — discover via list_enrichments (e.g. work-email, phone-number, company-domain, company-info).',
|
||||
},
|
||||
inputMappings: {
|
||||
type: 'array',
|
||||
description:
|
||||
'For add_enrichment: binds each enrichment input to an existing table column, as { inputName, columnName } where inputName is the enrichment input id from list_enrichments. Provide one for every required input.',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
columnName: {
|
||||
type: 'string',
|
||||
description: 'Existing table column that supplies this input.',
|
||||
},
|
||||
inputName: {
|
||||
type: 'string',
|
||||
description: 'Enrichment input id to bind (from list_enrichments).',
|
||||
},
|
||||
},
|
||||
required: ['inputName', 'columnName'],
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description:
|
||||
"Optional display name for the enrichment column group; defaults to the enrichment's registry name.",
|
||||
},
|
||||
outputColumnNames: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Optional output column name overrides, as { "<outputId>": "<columnName>" }; omit for defaults.',
|
||||
additionalProperties: {
|
||||
type: 'string',
|
||||
description: 'Target column name for this enrichment output id.',
|
||||
},
|
||||
},
|
||||
outputPath: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Write this call\'s result to a NEW workspace file instead of returning it (e.g. "files/export.csv"). On success the tool result is REPLACED by a file receipt (fileId, vfsPath, size) — set it only when the file IS the goal. ".csv" serializes rows as a CSV table; ".json"/".txt"/".md"/".html" write pretty-printed JSON of the full result envelope. Missing parent folders are created; an existing path fails.',
|
||||
},
|
||||
tableId: { type: 'string', description: 'Table ID (required for add_enrichment)' },
|
||||
},
|
||||
},
|
||||
operation: {
|
||||
type: 'string',
|
||||
description: 'The enrichment operation to perform',
|
||||
enum: ['list_enrichments', 'add_enrichment'],
|
||||
},
|
||||
},
|
||||
required: ['operation', 'args'],
|
||||
},
|
||||
resultSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: { type: 'object', description: 'Operation-specific result payload.' },
|
||||
message: { type: 'string', description: 'Human-readable outcome summary.' },
|
||||
success: { type: 'boolean', description: 'Whether the operation succeeded.' },
|
||||
},
|
||||
required: ['success', 'message'],
|
||||
},
|
||||
}
|
||||
|
||||
export const TableManage: ToolCatalogEntry = {
|
||||
id: 'table_manage',
|
||||
name: 'table_manage',
|
||||
route: 'sim',
|
||||
mode: 'async',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
args: {
|
||||
type: 'object',
|
||||
description: 'Arguments for the operation',
|
||||
properties: {
|
||||
description: { type: 'string', description: 'Table description (optional for create)' },
|
||||
filePath: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Canonical workspace file VFS path for create_from_file / import_file, e.g. files/{path}/{name}',
|
||||
},
|
||||
mapping: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Optional explicit CSV-header → table-column mapping for import_file, as { "csvHeader": "columnName" | null }. null skips that header; omit a header to auto-map by sanitized name.',
|
||||
additionalProperties: {
|
||||
type: ['string', 'null'],
|
||||
description: 'Target column name on the table; null skips that CSV header.',
|
||||
},
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Import mode for import_file: append (default) adds rows; replace truncates existing rows in a transaction first.',
|
||||
enum: ['append', 'replace'],
|
||||
},
|
||||
name: { type: 'string', description: 'Table name (required for create)' },
|
||||
newName: { type: 'string', description: 'New table name (required for rename)' },
|
||||
outputPath: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Write this call\'s result to a NEW workspace file instead of returning it (e.g. "files/export.csv"). On success the tool result is REPLACED by a file receipt (fileId, vfsPath, size) — set it only when the file IS the goal. ".csv" serializes rows as a CSV table; ".json"/".txt"/".md"/".html" write pretty-printed JSON of the full result envelope. Missing parent folders are created; an existing path fails.',
|
||||
},
|
||||
schema: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Table schema with a columns array (required for create). Each column: { name, type, unique? }; a select (enum) column also requires options (display names) and takes multiple?.',
|
||||
},
|
||||
tableId: {
|
||||
type: 'string',
|
||||
description: 'Table ID (required for import_file and rename)',
|
||||
},
|
||||
},
|
||||
},
|
||||
operation: {
|
||||
type: 'string',
|
||||
description: 'The lifecycle operation to perform',
|
||||
enum: ['create', 'create_from_file', 'import_file', 'rename'],
|
||||
},
|
||||
},
|
||||
required: ['operation', 'args'],
|
||||
},
|
||||
resultSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: { type: 'object', description: 'Operation-specific result payload.' },
|
||||
message: { type: 'string', description: 'Human-readable outcome summary.' },
|
||||
success: { type: 'boolean', description: 'Whether the operation succeeded.' },
|
||||
},
|
||||
required: ['success', 'message'],
|
||||
},
|
||||
}
|
||||
|
||||
export const TableRows: ToolCatalogEntry = {
|
||||
id: 'table_rows',
|
||||
name: 'table_rows',
|
||||
route: 'sim',
|
||||
mode: 'async',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
args: {
|
||||
type: 'object',
|
||||
description: 'Arguments for the operation',
|
||||
properties: {
|
||||
columnName: {
|
||||
type: 'string',
|
||||
description: 'Column to set when using the values map format of batch_update_rows',
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Row data as column → value pairs (required for insert_row, update_row; the patch object for update_rows_by_filter). Select (enum) cells take the option NAME.',
|
||||
},
|
||||
filter: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Predicate filter for update_rows_by_filter / delete_rows_by_filter: {"all":[...]} (AND) or {"any":[...]} (OR) of {field, op, value} leaves or nested groups. Ops: eq, ne, gt, gte, lt, lte, in, nin, like, ilike (* wildcard), nlike, nilike, contains, ncontains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. in/nin take a non-empty array. Single-select columns match by eq/ne/in/nin; multiple-select by contains/ncontains — values are option NAMES.',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description:
|
||||
'Optional cap on affected rows for the by-filter operations; omit to act on every match.',
|
||||
},
|
||||
outputPath: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Write this call\'s result to a NEW workspace file instead of returning it (e.g. "files/export.csv"). On success the tool result is REPLACED by a file receipt (fileId, vfsPath, size) — set it only when the file IS the goal. ".csv" serializes rows as a CSV table; ".json"/".txt"/".md"/".html" write pretty-printed JSON of the full result envelope. Missing parent folders are created; an existing path fails.',
|
||||
},
|
||||
position: {
|
||||
type: 'integer',
|
||||
description:
|
||||
'Zero-based index at which to insert the row (optional, insert_row only). Rows at and below shift down; omit to append.',
|
||||
},
|
||||
rowId: { type: 'string', description: 'Row ID (required for update_row, delete_row)' },
|
||||
rowIds: {
|
||||
type: 'array',
|
||||
description: 'Array of row IDs to delete (required for batch_delete_rows)',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
rows: {
|
||||
type: 'array',
|
||||
description: 'Array of row data objects (required for batch_insert_rows)',
|
||||
},
|
||||
tableId: { type: 'string', description: 'Table ID (required for every operation)' },
|
||||
updates: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Array of per-row updates: [{ rowId, data: { col: val } }] (batch_update_rows format a)',
|
||||
},
|
||||
values: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Map of rowId → value for single-column batch update (batch_update_rows format b, with columnName)',
|
||||
},
|
||||
},
|
||||
required: ['tableId'],
|
||||
},
|
||||
operation: {
|
||||
type: 'string',
|
||||
description: 'The row operation to perform',
|
||||
enum: [
|
||||
'insert_row',
|
||||
'batch_insert_rows',
|
||||
'update_row',
|
||||
'batch_update_rows',
|
||||
'delete_row',
|
||||
'batch_delete_rows',
|
||||
'update_rows_by_filter',
|
||||
'delete_rows_by_filter',
|
||||
],
|
||||
},
|
||||
},
|
||||
required: ['operation', 'args'],
|
||||
},
|
||||
resultSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: { type: 'object', description: 'Operation-specific result payload.' },
|
||||
message: { type: 'string', description: 'Human-readable outcome summary.' },
|
||||
success: { type: 'boolean', description: 'Whether the operation succeeded.' },
|
||||
},
|
||||
required: ['success', 'message'],
|
||||
},
|
||||
}
|
||||
|
||||
export const Terminal: ToolCatalogEntry = {
|
||||
id: 'terminal',
|
||||
name: 'terminal',
|
||||
@@ -5788,6 +6297,101 @@ export const SearchKnowledgeBaseOperationValues = [
|
||||
SearchKnowledgeBaseOperation.listTags,
|
||||
] as const
|
||||
|
||||
export const TableAutomationsOperation = {
|
||||
listWorkflowOutputs: 'list_workflow_outputs',
|
||||
addWorkflowGroup: 'add_workflow_group',
|
||||
updateWorkflowGroup: 'update_workflow_group',
|
||||
deleteWorkflowGroup: 'delete_workflow_group',
|
||||
addWorkflowGroupOutput: 'add_workflow_group_output',
|
||||
deleteWorkflowGroupOutput: 'delete_workflow_group_output',
|
||||
runColumn: 'run_column',
|
||||
cancelTableRuns: 'cancel_table_runs',
|
||||
} as const
|
||||
|
||||
export type TableAutomationsOperation =
|
||||
(typeof TableAutomationsOperation)[keyof typeof TableAutomationsOperation]
|
||||
|
||||
export const TableAutomationsOperationValues = [
|
||||
TableAutomationsOperation.listWorkflowOutputs,
|
||||
TableAutomationsOperation.addWorkflowGroup,
|
||||
TableAutomationsOperation.updateWorkflowGroup,
|
||||
TableAutomationsOperation.deleteWorkflowGroup,
|
||||
TableAutomationsOperation.addWorkflowGroupOutput,
|
||||
TableAutomationsOperation.deleteWorkflowGroupOutput,
|
||||
TableAutomationsOperation.runColumn,
|
||||
TableAutomationsOperation.cancelTableRuns,
|
||||
] as const
|
||||
|
||||
export const TableColumnsOperation = {
|
||||
addColumn: 'add_column',
|
||||
renameColumn: 'rename_column',
|
||||
deleteColumn: 'delete_column',
|
||||
updateColumn: 'update_column',
|
||||
} as const
|
||||
|
||||
export type TableColumnsOperation =
|
||||
(typeof TableColumnsOperation)[keyof typeof TableColumnsOperation]
|
||||
|
||||
export const TableColumnsOperationValues = [
|
||||
TableColumnsOperation.addColumn,
|
||||
TableColumnsOperation.renameColumn,
|
||||
TableColumnsOperation.deleteColumn,
|
||||
TableColumnsOperation.updateColumn,
|
||||
] as const
|
||||
|
||||
export const TableEnrichmentsOperation = {
|
||||
listEnrichments: 'list_enrichments',
|
||||
addEnrichment: 'add_enrichment',
|
||||
} as const
|
||||
|
||||
export type TableEnrichmentsOperation =
|
||||
(typeof TableEnrichmentsOperation)[keyof typeof TableEnrichmentsOperation]
|
||||
|
||||
export const TableEnrichmentsOperationValues = [
|
||||
TableEnrichmentsOperation.listEnrichments,
|
||||
TableEnrichmentsOperation.addEnrichment,
|
||||
] as const
|
||||
|
||||
export const TableManageOperation = {
|
||||
create: 'create',
|
||||
createFromFile: 'create_from_file',
|
||||
importFile: 'import_file',
|
||||
rename: 'rename',
|
||||
} as const
|
||||
|
||||
export type TableManageOperation = (typeof TableManageOperation)[keyof typeof TableManageOperation]
|
||||
|
||||
export const TableManageOperationValues = [
|
||||
TableManageOperation.create,
|
||||
TableManageOperation.createFromFile,
|
||||
TableManageOperation.importFile,
|
||||
TableManageOperation.rename,
|
||||
] as const
|
||||
|
||||
export const TableRowsOperation = {
|
||||
insertRow: 'insert_row',
|
||||
batchInsertRows: 'batch_insert_rows',
|
||||
updateRow: 'update_row',
|
||||
batchUpdateRows: 'batch_update_rows',
|
||||
deleteRow: 'delete_row',
|
||||
batchDeleteRows: 'batch_delete_rows',
|
||||
updateRowsByFilter: 'update_rows_by_filter',
|
||||
deleteRowsByFilter: 'delete_rows_by_filter',
|
||||
} as const
|
||||
|
||||
export type TableRowsOperation = (typeof TableRowsOperation)[keyof typeof TableRowsOperation]
|
||||
|
||||
export const TableRowsOperationValues = [
|
||||
TableRowsOperation.insertRow,
|
||||
TableRowsOperation.batchInsertRows,
|
||||
TableRowsOperation.updateRow,
|
||||
TableRowsOperation.batchUpdateRows,
|
||||
TableRowsOperation.deleteRow,
|
||||
TableRowsOperation.batchDeleteRows,
|
||||
TableRowsOperation.updateRowsByFilter,
|
||||
TableRowsOperation.deleteRowsByFilter,
|
||||
] as const
|
||||
|
||||
export const TerminalOperation = {
|
||||
run: 'run',
|
||||
read: 'read',
|
||||
@@ -6008,6 +6612,11 @@ export const TOOL_CATALOG: Record<string, ToolCatalogEntry> = {
|
||||
[SetGlobalWorkflowVariables.id]: SetGlobalWorkflowVariables,
|
||||
[ShareFile.id]: ShareFile,
|
||||
[Table.id]: Table,
|
||||
[TableAutomations.id]: TableAutomations,
|
||||
[TableColumns.id]: TableColumns,
|
||||
[TableEnrichments.id]: TableEnrichments,
|
||||
[TableManage.id]: TableManage,
|
||||
[TableRows.id]: TableRows,
|
||||
[Terminal.id]: Terminal,
|
||||
[UpdateDeploymentVersion.id]: UpdateDeploymentVersion,
|
||||
[UpdateWorkspaceMcpServer.id]: UpdateWorkspaceMcpServer,
|
||||
|
||||
@@ -1971,7 +1971,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
|
||||
enrichmentId: {
|
||||
type: 'string',
|
||||
description:
|
||||
"Which enrichment to run. Discover the full set and each one's inputs/outputs via user_table.list_enrichments.",
|
||||
"Which enrichment to run. Discover the full set and each one's inputs/outputs via table_enrichments.list_enrichments.",
|
||||
enum: [
|
||||
'work-email',
|
||||
'phone-number',
|
||||
@@ -4749,6 +4749,579 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
|
||||
},
|
||||
resultSchema: undefined,
|
||||
},
|
||||
table_automations: {
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
args: {
|
||||
type: 'object',
|
||||
description: 'Arguments for the operation',
|
||||
properties: {
|
||||
autoRun: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
"On add: true fires dep-satisfied rows immediately (only when the user explicitly asked); default false stages silently. On update: toggles the group's auto-fire on dep satisfaction.",
|
||||
},
|
||||
blockId: {
|
||||
type: 'string',
|
||||
description: 'Source block ID inside the workflow (add_workflow_group_output)',
|
||||
},
|
||||
columnName: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Target column name: required for delete_workflow_group_output (the bound column to drop); optional for add_workflow_group_output (auto-derived from path)',
|
||||
},
|
||||
dependencies: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Dependencies before a row runs: { columns?: string[] } of input column names that must be filled. Output columns of upstream groups are valid; a group cannot depend on its own outputs.',
|
||||
properties: {
|
||||
columns: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Input column names that must be filled before the group runs a row.',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
deploymentMode: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Which workflow version rows execute: "live" (default, editable draft — edits take effect immediately) or "deployed" (latest active deployment; fails if the workflow was never deployed).',
|
||||
enum: ['live', 'deployed'],
|
||||
},
|
||||
groupId: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Workflow group ID (required for update_workflow_group, delete_workflow_group, add_workflow_group_output, delete_workflow_group_output)',
|
||||
},
|
||||
groupIds: {
|
||||
type: 'array',
|
||||
description: 'Workflow group IDs to fire (required for run_column, non-empty)',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
mappingUpdates: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Surgical per-output remap for update_workflow_group: each entry repoints ONE existing output column to a new (blockId, path) without touching the rest. Stale cells clear and backfill from saved execution logs where possible. Discover valid pairs via list_workflow_outputs first.',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
blockId: {
|
||||
type: 'string',
|
||||
description: 'New source block ID for this column.',
|
||||
},
|
||||
columnName: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The existing output column to remap (must be bound to this group).',
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'New dotted output path on the new block.',
|
||||
},
|
||||
},
|
||||
required: ['columnName', 'blockId', 'path'],
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Display name for the group (optional on add/update)',
|
||||
},
|
||||
outputPath: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Write this call\'s result to a NEW workspace file instead of returning it (e.g. "files/export.csv"). On success the tool result is REPLACED by a file receipt (fileId, vfsPath, size) — set it only when the file IS the goal. ".csv" serializes rows as a CSV table; ".json"/".txt"/".md"/".html" write pretty-printed JSON of the full result envelope. Missing parent folders are created; an existing path fails.',
|
||||
},
|
||||
outputs: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Outputs to surface as columns for add_workflow_group: each { blockId, path, columnName?, columnType? }; columnName auto-derives from path, columnType from the leaf type. Validated against list_workflow_outputs — invalid picks return the valid options. For update_workflow_group prefer add/delete_workflow_group_output and mappingUpdates; pass outputs only to restructure the whole set.',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
blockId: {
|
||||
type: 'string',
|
||||
description: 'Source block ID inside the workflow.',
|
||||
},
|
||||
columnName: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Optional target column name; auto-derived from the path when omitted.',
|
||||
},
|
||||
columnType: {
|
||||
type: 'string',
|
||||
description: 'Optional column type; defaults from the leaf type when omitted.',
|
||||
enum: ['string', 'number', 'boolean', 'date', 'json'],
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'Dotted output path on the block.',
|
||||
},
|
||||
},
|
||||
required: ['blockId', 'path'],
|
||||
},
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'Dotted output path on the block (add_workflow_group_output)',
|
||||
},
|
||||
rowId: {
|
||||
type: 'string',
|
||||
description: 'Row ID for cancel_table_runs with scope "row".',
|
||||
},
|
||||
rowIds: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Optional row scope for run_column: only these rows are candidates (server eligibility still applies); omit for the whole table.',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
runMode: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Run mode for run_column: "incomplete" (default) re-runs only rows with no output or a last failure; "all" re-runs every dep-satisfied row.',
|
||||
enum: ['incomplete', 'all'],
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Cancellation scope for cancel_table_runs: "all" (whole table) or "row" (requires rowId).',
|
||||
enum: ['all', 'row'],
|
||||
},
|
||||
tableId: {
|
||||
type: 'string',
|
||||
description: 'Table ID (required for everything except list_workflow_outputs)',
|
||||
},
|
||||
workflowId: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Workflow ID (required for add_workflow_group and list_workflow_outputs)',
|
||||
},
|
||||
},
|
||||
},
|
||||
operation: {
|
||||
type: 'string',
|
||||
description: 'The automation operation to perform',
|
||||
enum: [
|
||||
'list_workflow_outputs',
|
||||
'add_workflow_group',
|
||||
'update_workflow_group',
|
||||
'delete_workflow_group',
|
||||
'add_workflow_group_output',
|
||||
'delete_workflow_group_output',
|
||||
'run_column',
|
||||
'cancel_table_runs',
|
||||
],
|
||||
},
|
||||
},
|
||||
required: ['operation', 'args'],
|
||||
},
|
||||
resultSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: {
|
||||
type: 'object',
|
||||
description: 'Operation-specific result payload.',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Human-readable outcome summary.',
|
||||
},
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the operation succeeded.',
|
||||
},
|
||||
},
|
||||
required: ['success', 'message'],
|
||||
},
|
||||
},
|
||||
table_columns: {
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
args: {
|
||||
type: 'object',
|
||||
description: 'Arguments for the operation',
|
||||
properties: {
|
||||
column: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Column definition for add_column: { name, type, unique?, position? }; select (enum) columns also take { options: [names], multiple?: true } — options is required for select.',
|
||||
},
|
||||
columnName: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Column name (required for rename_column and update_column; single-column delete_column)',
|
||||
},
|
||||
columnNames: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Array of column names to delete at once (preferred for multi-column delete_column)',
|
||||
},
|
||||
multiple: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Whether a select cell may hold several options (default false). Switching true → false fails if any row has more than one selected.',
|
||||
},
|
||||
newName: {
|
||||
type: 'string',
|
||||
description: 'New column name (required for rename_column)',
|
||||
},
|
||||
newType: {
|
||||
type: 'string',
|
||||
description:
|
||||
'New column type for update_column: string, number, boolean, date, json, select. Converting to select also requires options; conversion fails if an existing cell value matches no option. A multiple select round-trips through text as a comma-separated cell.',
|
||||
},
|
||||
options: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Choices for a select (enum) column as display names, e.g. ["Open", "Closed"]. Required when creating or converting to select. On update_column this REPLACES the whole list, matched BY NAME — send the full list including options you keep; omitting one deletes it and clears its cells. Max 100.',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
outputPath: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Write this call\'s result to a NEW workspace file instead of returning it (e.g. "files/export.csv"). On success the tool result is REPLACED by a file receipt (fileId, vfsPath, size) — set it only when the file IS the goal. ".csv" serializes rows as a CSV table; ".json"/".txt"/".md"/".html" write pretty-printed JSON of the full result envelope. Missing parent folders are created; an existing path fails.',
|
||||
},
|
||||
tableId: {
|
||||
type: 'string',
|
||||
description: 'Table ID (required for every operation)',
|
||||
},
|
||||
unique: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Set or clear the column unique constraint (update_column; not supported on select columns)',
|
||||
},
|
||||
},
|
||||
required: ['tableId'],
|
||||
},
|
||||
operation: {
|
||||
type: 'string',
|
||||
description: 'The column operation to perform',
|
||||
enum: ['add_column', 'rename_column', 'delete_column', 'update_column'],
|
||||
},
|
||||
},
|
||||
required: ['operation', 'args'],
|
||||
},
|
||||
resultSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: {
|
||||
type: 'object',
|
||||
description: 'Operation-specific result payload.',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Human-readable outcome summary.',
|
||||
},
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the operation succeeded.',
|
||||
},
|
||||
},
|
||||
required: ['success', 'message'],
|
||||
},
|
||||
},
|
||||
table_enrichments: {
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
args: {
|
||||
type: 'object',
|
||||
description: 'Arguments for the operation',
|
||||
properties: {
|
||||
autoRun: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'true fires dep-satisfied rows immediately on add (only when the user explicitly asked); default false stages silently — fire later via table_automations run_column.',
|
||||
},
|
||||
dependencies: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Optional dependency override: { columns?: string[] }; omit to default to the mapped input columns.',
|
||||
properties: {
|
||||
columns: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Input column names that must be filled before the enrichment runs a row.',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
enrichmentId: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Enrichment registry ID for add_enrichment — discover via list_enrichments (e.g. work-email, phone-number, company-domain, company-info).',
|
||||
},
|
||||
inputMappings: {
|
||||
type: 'array',
|
||||
description:
|
||||
'For add_enrichment: binds each enrichment input to an existing table column, as { inputName, columnName } where inputName is the enrichment input id from list_enrichments. Provide one for every required input.',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
columnName: {
|
||||
type: 'string',
|
||||
description: 'Existing table column that supplies this input.',
|
||||
},
|
||||
inputName: {
|
||||
type: 'string',
|
||||
description: 'Enrichment input id to bind (from list_enrichments).',
|
||||
},
|
||||
},
|
||||
required: ['inputName', 'columnName'],
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description:
|
||||
"Optional display name for the enrichment column group; defaults to the enrichment's registry name.",
|
||||
},
|
||||
outputColumnNames: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Optional output column name overrides, as { "<outputId>": "<columnName>" }; omit for defaults.',
|
||||
additionalProperties: {
|
||||
type: 'string',
|
||||
description: 'Target column name for this enrichment output id.',
|
||||
},
|
||||
},
|
||||
outputPath: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Write this call\'s result to a NEW workspace file instead of returning it (e.g. "files/export.csv"). On success the tool result is REPLACED by a file receipt (fileId, vfsPath, size) — set it only when the file IS the goal. ".csv" serializes rows as a CSV table; ".json"/".txt"/".md"/".html" write pretty-printed JSON of the full result envelope. Missing parent folders are created; an existing path fails.',
|
||||
},
|
||||
tableId: {
|
||||
type: 'string',
|
||||
description: 'Table ID (required for add_enrichment)',
|
||||
},
|
||||
},
|
||||
},
|
||||
operation: {
|
||||
type: 'string',
|
||||
description: 'The enrichment operation to perform',
|
||||
enum: ['list_enrichments', 'add_enrichment'],
|
||||
},
|
||||
},
|
||||
required: ['operation', 'args'],
|
||||
},
|
||||
resultSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: {
|
||||
type: 'object',
|
||||
description: 'Operation-specific result payload.',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Human-readable outcome summary.',
|
||||
},
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the operation succeeded.',
|
||||
},
|
||||
},
|
||||
required: ['success', 'message'],
|
||||
},
|
||||
},
|
||||
table_manage: {
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
args: {
|
||||
type: 'object',
|
||||
description: 'Arguments for the operation',
|
||||
properties: {
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Table description (optional for create)',
|
||||
},
|
||||
filePath: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Canonical workspace file VFS path for create_from_file / import_file, e.g. files/{path}/{name}',
|
||||
},
|
||||
mapping: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Optional explicit CSV-header → table-column mapping for import_file, as { "csvHeader": "columnName" | null }. null skips that header; omit a header to auto-map by sanitized name.',
|
||||
additionalProperties: {
|
||||
type: ['string', 'null'],
|
||||
description: 'Target column name on the table; null skips that CSV header.',
|
||||
},
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Import mode for import_file: append (default) adds rows; replace truncates existing rows in a transaction first.',
|
||||
enum: ['append', 'replace'],
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Table name (required for create)',
|
||||
},
|
||||
newName: {
|
||||
type: 'string',
|
||||
description: 'New table name (required for rename)',
|
||||
},
|
||||
outputPath: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Write this call\'s result to a NEW workspace file instead of returning it (e.g. "files/export.csv"). On success the tool result is REPLACED by a file receipt (fileId, vfsPath, size) — set it only when the file IS the goal. ".csv" serializes rows as a CSV table; ".json"/".txt"/".md"/".html" write pretty-printed JSON of the full result envelope. Missing parent folders are created; an existing path fails.',
|
||||
},
|
||||
schema: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Table schema with a columns array (required for create). Each column: { name, type, unique? }; a select (enum) column also requires options (display names) and takes multiple?.',
|
||||
},
|
||||
tableId: {
|
||||
type: 'string',
|
||||
description: 'Table ID (required for import_file and rename)',
|
||||
},
|
||||
},
|
||||
},
|
||||
operation: {
|
||||
type: 'string',
|
||||
description: 'The lifecycle operation to perform',
|
||||
enum: ['create', 'create_from_file', 'import_file', 'rename'],
|
||||
},
|
||||
},
|
||||
required: ['operation', 'args'],
|
||||
},
|
||||
resultSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: {
|
||||
type: 'object',
|
||||
description: 'Operation-specific result payload.',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Human-readable outcome summary.',
|
||||
},
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the operation succeeded.',
|
||||
},
|
||||
},
|
||||
required: ['success', 'message'],
|
||||
},
|
||||
},
|
||||
table_rows: {
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
args: {
|
||||
type: 'object',
|
||||
description: 'Arguments for the operation',
|
||||
properties: {
|
||||
columnName: {
|
||||
type: 'string',
|
||||
description: 'Column to set when using the values map format of batch_update_rows',
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Row data as column → value pairs (required for insert_row, update_row; the patch object for update_rows_by_filter). Select (enum) cells take the option NAME.',
|
||||
},
|
||||
filter: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Predicate filter for update_rows_by_filter / delete_rows_by_filter: {"all":[...]} (AND) or {"any":[...]} (OR) of {field, op, value} leaves or nested groups. Ops: eq, ne, gt, gte, lt, lte, in, nin, like, ilike (* wildcard), nlike, nilike, contains, ncontains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. in/nin take a non-empty array. Single-select columns match by eq/ne/in/nin; multiple-select by contains/ncontains — values are option NAMES.',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description:
|
||||
'Optional cap on affected rows for the by-filter operations; omit to act on every match.',
|
||||
},
|
||||
outputPath: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Write this call\'s result to a NEW workspace file instead of returning it (e.g. "files/export.csv"). On success the tool result is REPLACED by a file receipt (fileId, vfsPath, size) — set it only when the file IS the goal. ".csv" serializes rows as a CSV table; ".json"/".txt"/".md"/".html" write pretty-printed JSON of the full result envelope. Missing parent folders are created; an existing path fails.',
|
||||
},
|
||||
position: {
|
||||
type: 'integer',
|
||||
description:
|
||||
'Zero-based index at which to insert the row (optional, insert_row only). Rows at and below shift down; omit to append.',
|
||||
},
|
||||
rowId: {
|
||||
type: 'string',
|
||||
description: 'Row ID (required for update_row, delete_row)',
|
||||
},
|
||||
rowIds: {
|
||||
type: 'array',
|
||||
description: 'Array of row IDs to delete (required for batch_delete_rows)',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
rows: {
|
||||
type: 'array',
|
||||
description: 'Array of row data objects (required for batch_insert_rows)',
|
||||
},
|
||||
tableId: {
|
||||
type: 'string',
|
||||
description: 'Table ID (required for every operation)',
|
||||
},
|
||||
updates: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Array of per-row updates: [{ rowId, data: { col: val } }] (batch_update_rows format a)',
|
||||
},
|
||||
values: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Map of rowId → value for single-column batch update (batch_update_rows format b, with columnName)',
|
||||
},
|
||||
},
|
||||
required: ['tableId'],
|
||||
},
|
||||
operation: {
|
||||
type: 'string',
|
||||
description: 'The row operation to perform',
|
||||
enum: [
|
||||
'insert_row',
|
||||
'batch_insert_rows',
|
||||
'update_row',
|
||||
'batch_update_rows',
|
||||
'delete_row',
|
||||
'batch_delete_rows',
|
||||
'update_rows_by_filter',
|
||||
'delete_rows_by_filter',
|
||||
],
|
||||
},
|
||||
},
|
||||
required: ['operation', 'args'],
|
||||
},
|
||||
resultSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: {
|
||||
type: 'object',
|
||||
description: 'Operation-specific result payload.',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Human-readable outcome summary.',
|
||||
},
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the operation succeeded.',
|
||||
},
|
||||
},
|
||||
required: ['success', 'message'],
|
||||
},
|
||||
},
|
||||
terminal: {
|
||||
parameters: {
|
||||
type: 'object',
|
||||
|
||||
@@ -48,6 +48,11 @@ import { generateAudioServerTool } from '@/lib/copilot/tools/server/media/genera
|
||||
import { generateVideoServerTool } from '@/lib/copilot/tools/server/media/generate-video'
|
||||
import { searchOnlineServerTool } from '@/lib/copilot/tools/server/other/search-online'
|
||||
import { queryUserTableServerTool } from '@/lib/copilot/tools/server/table/query-user-table'
|
||||
import { tableAutomationsServerTool } from '@/lib/copilot/tools/server/table/table-automations'
|
||||
import { tableColumnsServerTool } from '@/lib/copilot/tools/server/table/table-columns'
|
||||
import { tableEnrichmentsServerTool } from '@/lib/copilot/tools/server/table/table-enrichments'
|
||||
import { tableManageServerTool } from '@/lib/copilot/tools/server/table/table-manage'
|
||||
import { tableRowsServerTool } from '@/lib/copilot/tools/server/table/table-rows'
|
||||
import { userTableServerTool } from '@/lib/copilot/tools/server/table/user-table'
|
||||
import { getCredentialsServerTool } from '@/lib/copilot/tools/server/user/get-credentials'
|
||||
import { setEnvironmentVariablesServerTool } from '@/lib/copilot/tools/server/user/set-environment-variables'
|
||||
@@ -172,6 +177,11 @@ const baseServerToolRegistry: Record<string, BaseServerTool> = {
|
||||
[enrichmentRunServerTool.name]: enrichmentRunServerTool,
|
||||
[userTableServerTool.name]: userTableServerTool,
|
||||
[queryUserTableServerTool.name]: queryUserTableServerTool,
|
||||
[tableManageServerTool.name]: tableManageServerTool,
|
||||
[tableRowsServerTool.name]: tableRowsServerTool,
|
||||
[tableColumnsServerTool.name]: tableColumnsServerTool,
|
||||
[tableAutomationsServerTool.name]: tableAutomationsServerTool,
|
||||
[tableEnrichmentsServerTool.name]: tableEnrichmentsServerTool,
|
||||
[workspaceFileServerTool.name]: workspaceFileServerTool,
|
||||
[editContentServerTool.name]: editContentServerTool,
|
||||
[createFileServerTool.name]: createFileServerTool,
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { TableAutomations } from '@/lib/copilot/generated/tool-catalog-v1'
|
||||
import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool'
|
||||
import { userTableServerTool } from '@/lib/copilot/tools/server/table/user-table'
|
||||
|
||||
type TableAutomationsArgs = {
|
||||
operation: string
|
||||
args?: Record<string, any>
|
||||
}
|
||||
|
||||
type TableAutomationsResult = {
|
||||
success: boolean
|
||||
message: string
|
||||
data?: any
|
||||
}
|
||||
|
||||
const ALLOWED_OPERATIONS = new Set([
|
||||
'list_workflow_outputs',
|
||||
'add_workflow_group',
|
||||
'update_workflow_group',
|
||||
'delete_workflow_group',
|
||||
'add_workflow_group_output',
|
||||
'delete_workflow_group_output',
|
||||
'run_column',
|
||||
'cancel_table_runs',
|
||||
])
|
||||
|
||||
/**
|
||||
* per-row workflow automations slice of the split user_table surface. Copilot access control is a
|
||||
* per-agent tool allowlist, so each slice gets its own tool name with its own
|
||||
* operation contract — enforced here (where execution happens) on top of the
|
||||
* schema enum in the Go catalog. Delegates to the shared user_table executor,
|
||||
* so argument semantics stay identical by construction.
|
||||
*/
|
||||
export const tableAutomationsServerTool: BaseServerTool<
|
||||
TableAutomationsArgs,
|
||||
TableAutomationsResult
|
||||
> = {
|
||||
name: TableAutomations.id,
|
||||
async execute(params: TableAutomationsArgs, context?: ServerToolContext) {
|
||||
const operation = params?.operation
|
||||
if (!ALLOWED_OPERATIONS.has(operation)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `table_automations does not support operation '${operation}' (allowed: list_workflow_outputs, add_workflow_group, update_workflow_group, delete_workflow_group, add_workflow_group_output, delete_workflow_group_output, run_column, cancel_table_runs); other table operations live on their own table_* tools`,
|
||||
}
|
||||
}
|
||||
return userTableServerTool.execute(params, context)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { TableColumns } from '@/lib/copilot/generated/tool-catalog-v1'
|
||||
import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool'
|
||||
import { userTableServerTool } from '@/lib/copilot/tools/server/table/user-table'
|
||||
|
||||
type TableColumnsArgs = {
|
||||
operation: string
|
||||
args?: Record<string, any>
|
||||
}
|
||||
|
||||
type TableColumnsResult = {
|
||||
success: boolean
|
||||
message: string
|
||||
data?: any
|
||||
}
|
||||
|
||||
const ALLOWED_OPERATIONS = new Set([
|
||||
'add_column',
|
||||
'rename_column',
|
||||
'delete_column',
|
||||
'update_column',
|
||||
])
|
||||
|
||||
/**
|
||||
* column DDL (add/rename/retype/delete) slice of the split user_table surface. Copilot access control is a
|
||||
* per-agent tool allowlist, so each slice gets its own tool name with its own
|
||||
* operation contract — enforced here (where execution happens) on top of the
|
||||
* schema enum in the Go catalog. Delegates to the shared user_table executor,
|
||||
* so argument semantics stay identical by construction.
|
||||
*/
|
||||
export const tableColumnsServerTool: BaseServerTool<TableColumnsArgs, TableColumnsResult> = {
|
||||
name: TableColumns.id,
|
||||
async execute(params: TableColumnsArgs, context?: ServerToolContext) {
|
||||
const operation = params?.operation
|
||||
if (!ALLOWED_OPERATIONS.has(operation)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `table_columns does not support operation '${operation}' (allowed: add_column, rename_column, delete_column, update_column); other table operations live on their own table_* tools`,
|
||||
}
|
||||
}
|
||||
return userTableServerTool.execute(params, context)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { TableEnrichments } from '@/lib/copilot/generated/tool-catalog-v1'
|
||||
import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool'
|
||||
import { userTableServerTool } from '@/lib/copilot/tools/server/table/user-table'
|
||||
|
||||
type TableEnrichmentsArgs = {
|
||||
operation: string
|
||||
args?: Record<string, any>
|
||||
}
|
||||
|
||||
type TableEnrichmentsResult = {
|
||||
success: boolean
|
||||
message: string
|
||||
data?: any
|
||||
}
|
||||
|
||||
const ALLOWED_OPERATIONS = new Set(['list_enrichments', 'add_enrichment'])
|
||||
|
||||
/**
|
||||
* prebuilt per-row enrichments slice of the split user_table surface. Copilot access control is a
|
||||
* per-agent tool allowlist, so each slice gets its own tool name with its own
|
||||
* operation contract — enforced here (where execution happens) on top of the
|
||||
* schema enum in the Go catalog. Delegates to the shared user_table executor,
|
||||
* so argument semantics stay identical by construction.
|
||||
*/
|
||||
export const tableEnrichmentsServerTool: BaseServerTool<
|
||||
TableEnrichmentsArgs,
|
||||
TableEnrichmentsResult
|
||||
> = {
|
||||
name: TableEnrichments.id,
|
||||
async execute(params: TableEnrichmentsArgs, context?: ServerToolContext) {
|
||||
const operation = params?.operation
|
||||
if (!ALLOWED_OPERATIONS.has(operation)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `table_enrichments does not support operation '${operation}' (allowed: list_enrichments, add_enrichment); other table operations live on their own table_* tools`,
|
||||
}
|
||||
}
|
||||
return userTableServerTool.execute(params, context)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { TableManage } from '@/lib/copilot/generated/tool-catalog-v1'
|
||||
import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool'
|
||||
import { userTableServerTool } from '@/lib/copilot/tools/server/table/user-table'
|
||||
|
||||
type TableManageArgs = {
|
||||
operation: string
|
||||
args?: Record<string, any>
|
||||
}
|
||||
|
||||
type TableManageResult = {
|
||||
success: boolean
|
||||
message: string
|
||||
data?: any
|
||||
}
|
||||
|
||||
const ALLOWED_OPERATIONS = new Set(['create', 'create_from_file', 'import_file', 'rename'])
|
||||
|
||||
/**
|
||||
* table lifecycle (create, create_from_file, import_file, rename) slice of the split user_table surface. Copilot access control is a
|
||||
* per-agent tool allowlist, so each slice gets its own tool name with its own
|
||||
* operation contract — enforced here (where execution happens) on top of the
|
||||
* schema enum in the Go catalog. Delegates to the shared user_table executor,
|
||||
* so argument semantics stay identical by construction.
|
||||
*/
|
||||
export const tableManageServerTool: BaseServerTool<TableManageArgs, TableManageResult> = {
|
||||
name: TableManage.id,
|
||||
async execute(params: TableManageArgs, context?: ServerToolContext) {
|
||||
const operation = params?.operation
|
||||
if (!ALLOWED_OPERATIONS.has(operation)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `table_manage does not support operation '${operation}' (allowed: create, create_from_file, import_file, rename); other table operations live on their own table_* tools`,
|
||||
}
|
||||
}
|
||||
return userTableServerTool.execute(params, context)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { TableRows } from '@/lib/copilot/generated/tool-catalog-v1'
|
||||
import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool'
|
||||
import { userTableServerTool } from '@/lib/copilot/tools/server/table/user-table'
|
||||
|
||||
type TableRowsArgs = {
|
||||
operation: string
|
||||
args?: Record<string, any>
|
||||
}
|
||||
|
||||
type TableRowsResult = {
|
||||
success: boolean
|
||||
message: string
|
||||
data?: any
|
||||
}
|
||||
|
||||
const ALLOWED_OPERATIONS = new Set([
|
||||
'insert_row',
|
||||
'batch_insert_rows',
|
||||
'update_row',
|
||||
'batch_update_rows',
|
||||
'delete_row',
|
||||
'batch_delete_rows',
|
||||
'update_rows_by_filter',
|
||||
'delete_rows_by_filter',
|
||||
])
|
||||
|
||||
/**
|
||||
* row data (insert/update/delete, batch and by-filter) slice of the split user_table surface. Copilot access control is a
|
||||
* per-agent tool allowlist, so each slice gets its own tool name with its own
|
||||
* operation contract — enforced here (where execution happens) on top of the
|
||||
* schema enum in the Go catalog. Delegates to the shared user_table executor,
|
||||
* so argument semantics stay identical by construction.
|
||||
*/
|
||||
export const tableRowsServerTool: BaseServerTool<TableRowsArgs, TableRowsResult> = {
|
||||
name: TableRows.id,
|
||||
async execute(params: TableRowsArgs, context?: ServerToolContext) {
|
||||
const operation = params?.operation
|
||||
if (!ALLOWED_OPERATIONS.has(operation)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `table_rows does not support operation '${operation}' (allowed: insert_row, batch_insert_rows, update_row, batch_update_rows, delete_row, batch_delete_rows, update_rows_by_filter, delete_rows_by_filter); other table operations live on their own table_* tools`,
|
||||
}
|
||||
}
|
||||
return userTableServerTool.execute(params, context)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const executeUserTable = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/lib/copilot/tools/server/table/user-table', () => ({
|
||||
userTableServerTool: { execute: executeUserTable },
|
||||
}))
|
||||
|
||||
import { tableAutomationsServerTool } from '@/lib/copilot/tools/server/table/table-automations'
|
||||
import { tableColumnsServerTool } from '@/lib/copilot/tools/server/table/table-columns'
|
||||
import { tableEnrichmentsServerTool } from '@/lib/copilot/tools/server/table/table-enrichments'
|
||||
import { tableManageServerTool } from '@/lib/copilot/tools/server/table/table-manage'
|
||||
import { tableRowsServerTool } from '@/lib/copilot/tools/server/table/table-rows'
|
||||
|
||||
/**
|
||||
* Every split tool delegates its own operations to the shared user_table
|
||||
* executor untouched, and rejects operations that belong to a sibling slice
|
||||
* without ever invoking it — the per-slice allowlist is the access contract.
|
||||
*/
|
||||
describe('split table tools', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
executeUserTable.mockResolvedValue({ success: true, message: 'ok' })
|
||||
})
|
||||
|
||||
const cases = [
|
||||
{ tool: tableManageServerTool, own: 'create', foreign: 'insert_row' },
|
||||
{ tool: tableRowsServerTool, own: 'batch_update_rows', foreign: 'add_column' },
|
||||
{ tool: tableColumnsServerTool, own: 'update_column', foreign: 'create' },
|
||||
{ tool: tableAutomationsServerTool, own: 'run_column', foreign: 'add_enrichment' },
|
||||
{ tool: tableEnrichmentsServerTool, own: 'add_enrichment', foreign: 'run_column' },
|
||||
] as const
|
||||
|
||||
it.each(cases)(
|
||||
'$tool.name delegates $own and rejects $foreign',
|
||||
async ({ tool, own, foreign }) => {
|
||||
const context = { userId: 'user-1', workspaceId: 'workspace-1', copilotToolExecution: true }
|
||||
const params = { operation: own, args: { tableId: 'table-1' } }
|
||||
|
||||
await expect(tool.execute(params as never, context as never)).resolves.toEqual({
|
||||
success: true,
|
||||
message: 'ok',
|
||||
})
|
||||
expect(executeUserTable).toHaveBeenCalledWith(params, context)
|
||||
|
||||
executeUserTable.mockClear()
|
||||
await expect(
|
||||
tool.execute({ operation: foreign, args: { tableId: 'table-1' } } as never)
|
||||
).resolves.toMatchObject({
|
||||
success: false,
|
||||
message: expect.stringContaining(foreign),
|
||||
})
|
||||
expect(executeUserTable).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -444,6 +444,11 @@ const TOOL_TITLES: Record<string, string> = {
|
||||
user_table: 'Managing table',
|
||||
run_code: 'Running code',
|
||||
query_user_table: 'Querying table',
|
||||
table_manage: 'Managing table',
|
||||
table_rows: 'Editing table rows',
|
||||
table_columns: 'Editing table columns',
|
||||
table_automations: 'Managing table automations',
|
||||
table_enrichments: 'Managing table enrichments',
|
||||
workspace_file: 'Editing file',
|
||||
edit_content: 'Applying file content',
|
||||
create_workflow: 'Creating workflow',
|
||||
@@ -682,6 +687,11 @@ export function getToolDisplayTitle(name: string, args?: Record<string, unknown>
|
||||
case 'knowledge_base':
|
||||
return knowledgeBaseTitle(args)
|
||||
case 'query_user_table':
|
||||
case 'table_manage':
|
||||
case 'table_rows':
|
||||
case 'table_columns':
|
||||
case 'table_automations':
|
||||
case 'table_enrichments':
|
||||
return queryUserTableTitle(args)
|
||||
case 'search_knowledge_base':
|
||||
return searchKnowledgeBaseTitle(args)
|
||||
|
||||
Reference in New Issue
Block a user