diff --git a/apps/sim/lib/api/contracts/mothership-assistant-tools.test.ts b/apps/sim/lib/api/contracts/mothership-assistant-tools.test.ts index 87e8da2e513..aef3a59c2af 100644 --- a/apps/sim/lib/api/contracts/mothership-assistant-tools.test.ts +++ b/apps/sim/lib/api/contracts/mothership-assistant-tools.test.ts @@ -57,13 +57,12 @@ describe('Assistant execution contracts', () => { expect(accepts([trip, { ...trip, accountId: 'other' }])).toBe(true) }) - it('takes one GitHub or GitLab query per account and kind', () => { + it('refuses a kindless GitHub query alongside kinded queries on one account', () => { const accepts = (nativeQueries: Record[]) => searchWorkspaceInputSchema.safeParse({ query: 'launch', nativeQueries }).success const github = { provider: 'github', accountId: 'account', query: 'repo:org/repo launch' } const kinds = ['issues', 'commits', 'code', 'repositories'].map((kind) => ({ ...github, kind })) expect(accepts(kinds)).toBe(true) - expect(accepts([kinds[0]!, { ...kinds[0]!, query: 'repo:org/repo deploy' }])).toBe(false) expect(accepts([kinds[0]!, github])).toBe(false) expect( searchWorkspaceInputSchema.safeParse({ nativeQueries: [{ provider: 'github', query: '' }] }) diff --git a/apps/sim/lib/api/contracts/mothership-assistant-tools.ts b/apps/sim/lib/api/contracts/mothership-assistant-tools.ts index 70289cee3b0..109948291a6 100644 --- a/apps/sim/lib/api/contracts/mothership-assistant-tools.ts +++ b/apps/sim/lib/api/contracts/mothership-assistant-tools.ts @@ -16,8 +16,9 @@ export const NOTION_SEARCH_TERMS_REQUIRED = export const MAX_NATIVE_QUERIES_PER_ACCOUNT = 4 /** - * Providers whose kind selects a distinct search collection. One query per kind bounds fanout - * while retaining independently searchable collections within the per-account request limit. + * Providers whose kind selects a distinct search collection. A query without a kind fans out + * across its provider's default collections, so it cannot share an account with kinded queries; + * the per-account request limit bounds the rest. */ const PROVIDER_KIND_SCHEMAS = { github: z.enum(['issues', 'code', 'repositories', 'commits']), @@ -102,10 +103,10 @@ export const nativeSearchQueriesSchema = z addIssue('Duplicate native query.') else if ( hasSearchKinds(query.provider) && - earlier.some((previous) => !previous.kind || !query.kind || previous.kind === query.kind) + earlier.some((previous) => !previous.kind || !query.kind) ) addIssue( - 'Send one query per account and kind for GitHub, GitLab, or HubSpot. Use provider-supported operators for alternatives, or send another call.' + 'A GitHub, GitLab, or HubSpot query without a kind already searches every kind; give each query on this account a kind.' ) else if (busiestAccountLoad(earlier) >= MAX_NATIVE_QUERIES_PER_ACCOUNT) addIssue( @@ -187,7 +188,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema nativeQueries: nativeSearchQueriesSchema .optional() .describe( - `Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies/HubSpot terms, Granola natural-language questions, Notion keywords or AI questions when available). Blank queries require a date bound or sortBy newest/oldest; Notion always requires search terms. Up to ${MAX_NATIVE_QUERIES_PER_ACCOUNT} per account run separately and merge; GitHub, GitLab, and HubSpot take one per kind. HubSpot kinds are contacts, companies, deals, and tickets; ownership filters are unsupported. Write queries from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.` + `Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies/HubSpot terms, Granola natural-language questions, Notion keywords or AI questions when available). Blank queries require a date bound or sortBy newest/oldest; Notion always requires search terms. Up to ${MAX_NATIVE_QUERIES_PER_ACCOUNT} per account run separately and merge; one GitHub, GitLab, or HubSpot query without a kind searches GitHub issues (plus code when the query has no date bound or boolean operators, as its status message says), GitLab issues, merge requests, and code, or every HubSpot CRM kind; other collections, and multiple queries on one account, each need a kind, which may repeat. HubSpot kinds are contacts, companies, deals, and tickets; ownership filters are unsupported. Write queries from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.` ), query: z .string() @@ -246,11 +247,12 @@ export const readDocumentInputSchema = z.object({ .min(1) .max(4000) .describe('Canonical document ID returned by search or selected document context.'), + /** A larger request is capped rather than refused: the server returns at most 8 chunks anyway. */ limit: z - .number() - .int() - .min(1) - .max(8) + .preprocess( + (limit) => (typeof limit === 'number' && limit > 8 ? 8 : limit), + z.number().int().min(1).max(8) + ) .default(3) .describe( 'Maximum number of chunks, from 1 to 8 (default 3); the server may return fewer to fit its text budget. Follow next for more context.' diff --git a/apps/sim/lib/mothership/generated/sim-assistant-tools.generated.ts b/apps/sim/lib/mothership/generated/sim-assistant-tools.generated.ts index a41b1ea417a..e9f86f3e8a2 100644 --- a/apps/sim/lib/mothership/generated/sim-assistant-tools.generated.ts +++ b/apps/sim/lib/mothership/generated/sim-assistant-tools.generated.ts @@ -33,8 +33,9 @@ export const NOTION_SEARCH_TERMS_REQUIRED = export const MAX_NATIVE_QUERIES_PER_ACCOUNT = 4 /** - * Providers whose kind selects a distinct search collection. One query per kind bounds fanout - * while retaining independently searchable collections within the per-account request limit. + * Providers whose kind selects a distinct search collection. A query without a kind fans out + * across its provider's default collections, so it cannot share an account with kinded queries; + * the per-account request limit bounds the rest. */ const PROVIDER_KIND_SCHEMAS = { github: z.enum(['issues', 'code', 'repositories', 'commits']), @@ -119,10 +120,10 @@ export const nativeSearchQueriesSchema = z addIssue('Duplicate native query.') else if ( hasSearchKinds(query.provider) && - earlier.some((previous) => !previous.kind || !query.kind || previous.kind === query.kind) + earlier.some((previous) => !previous.kind || !query.kind) ) addIssue( - 'Send one query per account and kind for GitHub, GitLab, or HubSpot. Use provider-supported operators for alternatives, or send another call.' + 'A GitHub, GitLab, or HubSpot query without a kind already searches every kind; give each query on this account a kind.' ) else if (busiestAccountLoad(earlier) >= MAX_NATIVE_QUERIES_PER_ACCOUNT) addIssue( @@ -204,7 +205,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema nativeQueries: nativeSearchQueriesSchema .optional() .describe( - `Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies/HubSpot terms, Granola natural-language questions, Notion keywords or AI questions when available). Blank queries require a date bound or sortBy newest/oldest; Notion always requires search terms. Up to ${MAX_NATIVE_QUERIES_PER_ACCOUNT} per account run separately and merge; GitHub, GitLab, and HubSpot take one per kind. HubSpot kinds are contacts, companies, deals, and tickets; ownership filters are unsupported. Write queries from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.` + `Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies/HubSpot terms, Granola natural-language questions, Notion keywords or AI questions when available). Blank queries require a date bound or sortBy newest/oldest; Notion always requires search terms. Up to ${MAX_NATIVE_QUERIES_PER_ACCOUNT} per account run separately and merge; one GitHub, GitLab, or HubSpot query without a kind searches GitHub issues (plus code when the query has no date bound or boolean operators, as its status message says), GitLab issues, merge requests, and code, or every HubSpot CRM kind; other collections, and multiple queries on one account, each need a kind, which may repeat. HubSpot kinds are contacts, companies, deals, and tickets; ownership filters are unsupported. Write queries from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.` ), query: z .string() @@ -263,11 +264,12 @@ export const readDocumentInputSchema = z.object({ .min(1) .max(4000) .describe('Canonical document ID returned by search or selected document context.'), + /** A larger request is capped rather than refused: the server returns at most 8 chunks anyway. */ limit: z - .number() - .int() - .min(1) - .max(8) + .preprocess( + (limit) => (typeof limit === 'number' && limit > 8 ? 8 : limit), + z.number().int().min(1).max(8) + ) .default(3) .describe( 'Maximum number of chunks, from 1 to 8 (default 3); the server may return fewer to fit its text budget. Follow next for more context.' diff --git a/apps/sim/lib/mothership/generated/tool-catalog-v1.ts b/apps/sim/lib/mothership/generated/tool-catalog-v1.ts index 2d05e9fb9b5..35b58f4b7d5 100644 --- a/apps/sim/lib/mothership/generated/tool-catalog-v1.ts +++ b/apps/sim/lib/mothership/generated/tool-catalog-v1.ts @@ -5196,7 +5196,6 @@ export const ReadDocument: ToolCatalogEntry = { description: 'Canonical document ID returned by search or selected document context.', }, limit: { - default: 3, description: 'Maximum number of chunks, from 1 to 8 (default 3); the server may return fewer to fit its text budget. Follow next for more context.', type: 'integer', @@ -6096,7 +6095,7 @@ export const SearchWorkspace: ToolCatalogEntry = { }, nativeQueries: { description: - "Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies/HubSpot terms, Granola natural-language questions, Notion keywords or AI questions when available). Blank queries require a date bound or sortBy newest/oldest; Notion always requires search terms. Up to 4 per account run separately and merge; GitHub, GitLab, and HubSpot take one per kind. HubSpot kinds are contacts, companies, deals, and tickets; ownership filters are unsupported. Write queries from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.", + "Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies/HubSpot terms, Granola natural-language questions, Notion keywords or AI questions when available). Blank queries require a date bound or sortBy newest/oldest; Notion always requires search terms. Up to 4 per account run separately and merge; one GitHub, GitLab, or HubSpot query without a kind searches GitHub issues (plus code when the query has no date bound or boolean operators, as its status message says), GitLab issues, merge requests, and code, or every HubSpot CRM kind; other collections, and multiple queries on one account, each need a kind, which may repeat. HubSpot kinds are contacts, companies, deals, and tickets; ownership filters are unsupported. Write queries from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.", minItems: 1, maxItems: 9, type: 'array', diff --git a/apps/sim/lib/mothership/generated/tool-schemas-v1.ts b/apps/sim/lib/mothership/generated/tool-schemas-v1.ts index 1ec5eafcda1..51b94ea62ce 100644 --- a/apps/sim/lib/mothership/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/mothership/generated/tool-schemas-v1.ts @@ -5184,7 +5184,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { description: 'Canonical document ID returned by search or selected document context.', }, limit: { - default: 3, description: 'Maximum number of chunks, from 1 to 8 (default 3); the server may return fewer to fit its text budget. Follow next for more context.', type: 'integer', @@ -6044,7 +6043,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, nativeQueries: { description: - "Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies/HubSpot terms, Granola natural-language questions, Notion keywords or AI questions when available). Blank queries require a date bound or sortBy newest/oldest; Notion always requires search terms. Up to 4 per account run separately and merge; GitHub, GitLab, and HubSpot take one per kind. HubSpot kinds are contacts, companies, deals, and tickets; ownership filters are unsupported. Write queries from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.", + "Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies/HubSpot terms, Granola natural-language questions, Notion keywords or AI questions when available). Blank queries require a date bound or sortBy newest/oldest; Notion always requires search terms. Up to 4 per account run separately and merge; one GitHub, GitLab, or HubSpot query without a kind searches GitHub issues (plus code when the query has no date bound or boolean operators, as its status message says), GitLab issues, merge requests, and code, or every HubSpot CRM kind; other collections, and multiple queries on one account, each need a kind, which may repeat. HubSpot kinds are contacts, companies, deals, and tickets; ownership filters are unsupported. Write queries from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.", minItems: 1, maxItems: 9, type: 'array', diff --git a/apps/sim/lib/mothership/tools/server/knowledge/workspace-search.test.ts b/apps/sim/lib/mothership/tools/server/knowledge/workspace-search.test.ts index 2a44b3e3608..00e482484c4 100644 --- a/apps/sim/lib/mothership/tools/server/knowledge/workspace-search.test.ts +++ b/apps/sim/lib/mothership/tools/server/knowledge/workspace-search.test.ts @@ -376,7 +376,7 @@ describe('Assistant retrieval tools', () => { expect(mocks.search).not.toHaveBeenCalled() expect(mocks.read).not.toHaveBeenCalled() }) - it('reads a selected document through the shared use case and rejects unbounded pages', async () => { + it('reads a selected document through the shared use case and caps oversized pages', async () => { expect( await readDocumentServerTool.execute({ documentId: 'doc', startChunkIndex: 20 }, context) ).toMatchObject({ success: true }) @@ -390,9 +390,20 @@ describe('Assistant retrieval tools', () => { }), }) ) - expect( - await readDocumentServerTool.execute({ documentId: 'doc', limit: 9 }, context) - ).toMatchObject({ success: false }) - expect(mocks.read).toHaveBeenCalledOnce() + mocks.read.mockImplementationOnce(async ({ input }: { input: { limit: number } }) => ({ + knowledgeBaseId: 'index', + documentId: 'doc', + documentName: 'Title', + sourceUrl: 'https://source.test/doc', + chunks: Array.from({ length: input.limit }, (_, chunkIndex) => ({ + content: 'body', + chunkIndex, + })), + hasMore: true, + next: null, + })) + const capped = await readDocumentServerTool.execute({ documentId: 'doc', limit: 9 }, context) + expect(capped).toMatchObject({ success: true }) + expect((capped as { data: { chunks: unknown[] } }).data.chunks).toHaveLength(8) }) })