Skip to content

Commit a953458

Browse files
committed
improvement(search): search several GitHub and GitLab kinds per account in one call and hide recovered search failures
1 parent 491740a commit a953458

14 files changed

Lines changed: 331 additions & 137 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/search-activity.test.tsx‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,27 @@ describe('search in shared tool activity', () => {
224224
expect(container.innerHTML).not.toContain('--text-error')
225225
})
226226

227+
it('replaces a failed search with the search that followed it', () => {
228+
const retry = completedSearch('two', 'Second query')
229+
render([retry])
230+
expand()
231+
const alone = { text: container.textContent, links: container.querySelectorAll('a').length }
232+
render([{ ...completedSearch('one', 'First query'), status: 'error' }, retry])
233+
expand()
234+
expect({ text: container.textContent, links: container.querySelectorAll('a').length }).toEqual(
235+
alone
236+
)
237+
})
238+
239+
it('keeps the last search visible when every search failed', () => {
240+
render([
241+
{ ...completedSearch('one', 'First query'), status: 'error' },
242+
{ ...completedSearch('two', 'Second query'), status: 'error' },
243+
])
244+
expect(headerText()).toBe('Searching documents')
245+
expect(container.textContent).not.toMatch(/failed/i)
246+
})
247+
227248
it.each([
228249
{ success: false, data: { results: [] } },
229250
{},

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.tsx‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
import { type ComponentType, Fragment, useState } from 'react'
44
import { ActivityStatus } from '@/components/ui/activity-status'
55
import type { ToolActivity } from '@/lib/mothership/generated/protocol'
6-
import { CallIntegrationTool, RunCode } from '@/lib/mothership/generated/tool-catalog-v1'
6+
import {
7+
CallIntegrationTool,
8+
RunCode,
9+
SearchWorkspace,
10+
} from '@/lib/mothership/generated/tool-catalog-v1'
711
import { extractStreamingStringArgument } from '@/lib/mothership/tools/streaming-args'
812
import {
913
getToolActivitySummaryActions,
@@ -31,6 +35,20 @@ function isFailedTool(tool: ToolCallData): boolean {
3135
return tool.status === ToolCallStatus.error || tool.status === ToolCallStatus.rejected
3236
}
3337

38+
/**
39+
* Drops a search that errored when another call follows it: the model corrected the query
40+
* and moved on, so the failed attempt is not part of what the user reads.
41+
* The last call always stays, so a run whose searches all failed still shows its outcome.
42+
*/
43+
function withoutRecoveredSearchFailures(tools: ToolCallData[]): ToolCallData[] {
44+
return tools.filter(
45+
(tool, index) =>
46+
index === tools.length - 1 ||
47+
tool.toolName !== SearchWorkspace.id ||
48+
tool.status !== ToolCallStatus.error
49+
)
50+
}
51+
3452
function toolCountLabel(tools: ToolCallData[]): string {
3553
return `${tools.length} tool ${tools.length === 1 ? 'call' : 'calls'}`
3654
}
@@ -192,12 +210,13 @@ interface ToolActivityGroupProps {
192210

193211
export function ToolActivityGroup({
194212
activity,
195-
tools,
213+
tools: calls,
196214
ToolCallComponent,
197215
autoScrollActivity = true,
198216
isLive = false,
199217
}: ToolActivityGroupProps) {
200218
const [expanded, setExpanded] = useState(false)
219+
const tools = withoutRecoveredSearchFailures(calls)
201220
const statusTool = getActivityStatusTool(tools)
202221
if (!statusTool) return null
203222
const headerTool = getActivityHeaderTool(tools, statusTool)

‎apps/sim/lib/api/contracts/mothership-assistant-tools.test.ts‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,38 @@ describe('Assistant execution contracts', () => {
7676
const nativeQueries = [{ provider: 'github', query: 'author:@me', kind: 'commits' }]
7777
expect(searchWorkspaceInputSchema.parse({ nativeQueries })).toMatchObject({ query: '' })
7878
expect(searchWorkspaceInputSchema.safeParse({}).success).toBe(false)
79+
})
80+
81+
it('accepts one native query per provider account and kind', () => {
82+
const accepts = (nativeQueries: Record<string, string>[]) =>
83+
searchWorkspaceInputSchema.safeParse({ query: 'launch', nativeQueries }).success
84+
const github = { provider: 'github', accountId: 'account', query: 'repo:org/repo launch' }
85+
expect(
86+
accepts([
87+
{ ...github, kind: 'issues' },
88+
{ ...github, kind: 'commits' },
89+
])
90+
).toBe(true)
91+
expect(
92+
accepts([
93+
{ ...github, kind: 'issues' },
94+
{ ...github, kind: 'issues' },
95+
])
96+
).toBe(false)
97+
expect(accepts([{ ...github, kind: 'issues' }, github])).toBe(false)
98+
expect(
99+
accepts([
100+
{ ...github, kind: 'issues' },
101+
{ ...github, accountId: 'other', kind: 'issues' },
102+
])
103+
).toBe(true)
104+
const gmail = { provider: 'gmail', query: 'subject:launch' }
105+
expect(
106+
accepts([
107+
{ ...gmail, kind: 'issues' },
108+
{ ...gmail, kind: 'code' },
109+
])
110+
).toBe(false)
79111
expect(
80112
searchWorkspaceInputSchema.safeParse({ nativeQueries: [{ provider: 'github', query: '' }] })
81113
.success

‎apps/sim/lib/api/contracts/mothership-assistant-tools.ts‎

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,25 @@ import { LIVE_SEARCH_PROVIDER_IDS } from '@/lib/sim-search/live/provider-catalog
44
export const liveSearchProviderSchema = z.enum(LIVE_SEARCH_PROVIDER_IDS)
55
export type LiveSearchProvider = z.output<typeof liveSearchProviderSchema>
66

7+
const nativeSearchKindSchema = z.enum([
8+
'issues',
9+
'code',
10+
'repositories',
11+
'commits',
12+
'merge_requests',
13+
'wiki',
14+
])
15+
16+
/** Providers whose `kind` selects a separate search endpoint; others ignore it. */
17+
const KIND_PROVIDERS: ReadonlySet<LiveSearchProvider> = new Set(['github', 'gitlab'])
18+
719
/** Queries are data for fixed read-only provider endpoints, never URLs or credentials. */
820
export const nativeSearchQuerySchema = z
921
.object({
1022
provider: liveSearchProviderSchema,
1123
query: z.string().trim().max(2000),
1224
accountId: z.string().min(1).max(200).optional(),
13-
kind: z
14-
.enum(['issues', 'code', 'repositories', 'commits', 'merge_requests', 'wiki'])
15-
.optional(),
25+
kind: nativeSearchKindSchema.optional(),
1626
project: z.string().min(1).max(300).optional(),
1727
cursor: z.string().max(4000).optional(),
1828
termClauses: z.array(z.string().max(500)).max(10).optional(),
@@ -27,28 +37,38 @@ export const nativeSearchQueriesSchema = z
2737
.min(1)
2838
.max(9)
2939
.superRefine((queries, context) => {
40+
/**
41+
* Each account runs one query per kind: GitHub and GitLab kinds are separate endpoints, so
42+
* one call can search several of them for the same account. A query without a kind covers
43+
* the provider's default kinds and conflicts with any other query for that account.
44+
*/
45+
const kindOf = (query: NativeSearchQuery) =>
46+
KIND_PROVIDERS.has(query.provider) ? query.kind : undefined
3047
for (const [index, query] of queries.entries()) {
3148
if (
3249
queries
3350
.slice(0, index)
3451
.some(
3552
(previous) =>
3653
previous.provider === query.provider &&
37-
(!previous.accountId || !query.accountId || previous.accountId === query.accountId)
54+
(!previous.accountId || !query.accountId || previous.accountId === query.accountId) &&
55+
(!kindOf(previous) || !kindOf(query) || kindOf(previous) === kindOf(query))
3856
)
3957
)
4058
context.addIssue({
4159
code: 'custom',
4260
path: [index],
4361
message:
44-
'Use one query per provider/account per call; refine in another call or combine native query clauses.',
62+
'Use one query per provider account and kind per call. Combine alternatives with OR in one query, or refine in another call.',
4563
})
4664
}
4765
})
4866

4967
export const liveSearchAccountStatusSchema = z.object({
5068
accountId: z.string(),
5169
provider: liveSearchProviderSchema,
70+
/** The native query kind this status and its cursor belong to. */
71+
kind: nativeSearchKindSchema.optional(),
5272
displayName: z.string(),
5373
status: z.enum(['ok', 'partial', 'reconnect', 'rate_limited', 'unavailable', 'timeout']),
5474
message: z.string().optional(),
@@ -117,7 +137,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
117137
nativeQueries: nativeSearchQueriesSchema
118138
.optional()
119139
.describe(
120-
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.'
140+
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Send one query per account, or one per kind for GitHub and GitLab (e.g. issues and commits together); combine alternatives with OR. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.'
121141
),
122142
query: z
123143
.string()

‎apps/sim/lib/mothership/generated/sim-assistant-tools.generated.ts‎

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,25 @@ export const liveSearchProviderSchema = z.enum([
1616
])
1717
export type LiveSearchProvider = z.output<typeof liveSearchProviderSchema>
1818

19+
const nativeSearchKindSchema = z.enum([
20+
'issues',
21+
'code',
22+
'repositories',
23+
'commits',
24+
'merge_requests',
25+
'wiki',
26+
])
27+
28+
/** Providers whose `kind` selects a separate search endpoint; others ignore it. */
29+
const KIND_PROVIDERS: ReadonlySet<LiveSearchProvider> = new Set(['github', 'gitlab'])
30+
1931
/** Queries are data for fixed read-only provider endpoints, never URLs or credentials. */
2032
export const nativeSearchQuerySchema = z
2133
.object({
2234
provider: liveSearchProviderSchema,
2335
query: z.string().trim().max(2000),
2436
accountId: z.string().min(1).max(200).optional(),
25-
kind: z
26-
.enum(['issues', 'code', 'repositories', 'commits', 'merge_requests', 'wiki'])
27-
.optional(),
37+
kind: nativeSearchKindSchema.optional(),
2838
project: z.string().min(1).max(300).optional(),
2939
cursor: z.string().max(4000).optional(),
3040
termClauses: z.array(z.string().max(500)).max(10).optional(),
@@ -39,28 +49,38 @@ export const nativeSearchQueriesSchema = z
3949
.min(1)
4050
.max(9)
4151
.superRefine((queries, context) => {
52+
/**
53+
* Each account runs one query per kind: GitHub and GitLab kinds are separate endpoints, so
54+
* one call can search several of them for the same account. A query without a kind covers
55+
* the provider's default kinds and conflicts with any other query for that account.
56+
*/
57+
const kindOf = (query: NativeSearchQuery) =>
58+
KIND_PROVIDERS.has(query.provider) ? query.kind : undefined
4259
for (const [index, query] of queries.entries()) {
4360
if (
4461
queries
4562
.slice(0, index)
4663
.some(
4764
(previous) =>
4865
previous.provider === query.provider &&
49-
(!previous.accountId || !query.accountId || previous.accountId === query.accountId)
66+
(!previous.accountId || !query.accountId || previous.accountId === query.accountId) &&
67+
(!kindOf(previous) || !kindOf(query) || kindOf(previous) === kindOf(query))
5068
)
5169
)
5270
context.addIssue({
5371
code: 'custom',
5472
path: [index],
5573
message:
56-
'Use one query per provider/account per call; refine in another call or combine native query clauses.',
74+
'Use one query per provider account and kind per call. Combine alternatives with OR in one query, or refine in another call.',
5775
})
5876
}
5977
})
6078

6179
export const liveSearchAccountStatusSchema = z.object({
6280
accountId: z.string(),
6381
provider: liveSearchProviderSchema,
82+
/** The native query kind this status and its cursor belong to. */
83+
kind: nativeSearchKindSchema.optional(),
6484
displayName: z.string(),
6585
status: z.enum(['ok', 'partial', 'reconnect', 'rate_limited', 'unavailable', 'timeout']),
6686
message: z.string().optional(),
@@ -129,7 +149,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
129149
nativeQueries: nativeSearchQueriesSchema
130150
.optional()
131151
.describe(
132-
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.'
152+
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Send one query per account, or one per kind for GitHub and GitLab (e.g. issues and commits together); combine alternatives with OR. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.'
133153
),
134154
query: z
135155
.string()

‎apps/sim/lib/mothership/generated/tool-catalog-v1.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6041,7 +6041,7 @@ export const SearchWorkspace: ToolCatalogEntry = {
60416041
},
60426042
nativeQueries: {
60436043
description:
6044-
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.',
6044+
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Send one query per account, or one per kind for GitHub and GitLab (e.g. issues and commits together); combine alternatives with OR. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.',
60456045
minItems: 1,
60466046
maxItems: 9,
60476047
type: 'array',

‎apps/sim/lib/mothership/generated/tool-schemas-v1.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5972,7 +5972,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
59725972
},
59735973
nativeQueries: {
59745974
description:
5975-
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.',
5975+
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Send one query per account, or one per kind for GitHub and GitLab (e.g. issues and commits together); combine alternatives with OR. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.',
59765976
minItems: 1,
59775977
maxItems: 9,
59785978
type: 'array',

‎apps/sim/lib/sim-search/live/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Member mode only. Search uses the connected user's Slack real-time search grant
5858

5959
### GitHub
6060

61-
Member mode searches issues, code, repositories, and commits permitted by the connected token. Commits are searched only with an explicit `commits` kind; they cover each repository's default branch, and a commit read lists up to 30 changed files. Explicit repository/organization/user qualifiers narrow the user's query. Default discovery is bounded to up to 100 affiliated repositories, sent as `repo:` qualifiers in at most four batches per search kind. Code batches stay under code search's 1,000-byte query limit, issue batches are larger, and a long query that cannot fit every repository reports the searched subset. Date bounds become one `updated:start..end` range (`author-date:` for commits), since GitHub ORs repeated qualifiers; a native query that already sets that qualifier keeps its own range, and results are still checked against the date filters. Provider pagination/search caps still apply. REST code search returns no file dates and accepts no date qualifier, so date-filtered searches cover issues and pull requests only.
61+
Member mode searches issues, code, repositories, and commits permitted by the connected token. One call may send one native query per GitHub or GitLab kind for the same account (for example issues and commits); the account opens one session, lists its affiliated repositories once, and reports each kind's status and cursor separately. Commits are searched only with an explicit `commits` kind; they cover each repository's default branch, and a commit read lists up to 30 changed files. Explicit repository/organization/user qualifiers narrow the user's query. Default discovery is bounded to up to 100 affiliated repositories, sent as `repo:` qualifiers in at most four batches per search kind. Code batches stay under code search's 1,000-byte query limit, issue batches are larger, and a long query that cannot fit every repository reports the searched subset. Date bounds become one `updated:start..end` range (`author-date:` for commits), since GitHub ORs repeated qualifiers; a native query that already sets that qualifier keeps its own range, and results are still checked against the date filters. Provider pagination/search caps still apply. REST code search returns no file dates and accepts no date qualifier, so date-filtered searches cover issues and pull requests only.
6262

6363
In service mode, an administrator connects a GitHub App installation and selects repositories one by one in Sources. Each source pins the provider-verified repository ID and may narrow code files by directory and extension. Search queries the member's own GitHub connection with `repo:` qualifiers drawn only from active sources. For each candidate, Sim checks that the current App installation still covers that repository, mints a repository-scoped read token, and compares repository and owner IDs returned under both the App and member tokens. It then checks the per-repository code filters. Reads use the member token and repeat these checks. A personal repository outside the selected sources is never searched, even if the member can access it. GitHub REST code search covers the default branch; live Sources therefore do not offer a branch setting.
6464

‎apps/sim/lib/sim-search/live/application.test.ts‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,45 @@ describe('authorized live retrieval', () => {
526526
).rejects.toThrow()
527527
expect(mocks.search).toHaveBeenCalledOnce()
528528
})
529+
it('searches each GitHub kind of one account through one session and reports it separately', async () => {
530+
const github = { ...account, id: 'github-account', provider: 'github', providerId: 'github' }
531+
mocks.accounts.mockResolvedValue([github])
532+
mocks.resolveAccount.mockResolvedValue({ account: github, accessToken: 'secret' })
533+
mocks.search.mockImplementation(async (_provider, _client, search) =>
534+
search.native.kind === 'commits'
535+
? { documents: [], nextCursor: '2' }
536+
: Promise.reject(new NativeSearchError('rate_limited', 'Slow down.', 30))
537+
)
538+
const nativeQueries = (['issues', 'commits'] as const).map((kind) => ({
539+
provider: 'github' as const,
540+
query: 'repo:org/repo launch',
541+
accountId: 'github-account',
542+
kind,
543+
}))
544+
const result = await searchLiveKnowledge.execute({
545+
principal,
546+
input: { ...input, query: '', nativeQueries },
547+
})
548+
expect(mocks.resolveAccount).toHaveBeenCalledOnce()
549+
expect(mocks.search.mock.calls.map(([, , search]) => search.native.kind)).toEqual([
550+
'issues',
551+
'commits',
552+
])
553+
expect(result.live?.accounts).toEqual([
554+
expect.objectContaining({
555+
accountId: 'github-account',
556+
kind: 'issues',
557+
status: 'rate_limited',
558+
retryAfterSeconds: 30,
559+
}),
560+
expect.objectContaining({
561+
accountId: 'github-account',
562+
kind: 'commits',
563+
status: 'partial',
564+
nextCursor: '2',
565+
}),
566+
])
567+
})
529568
it('rejects invalid dates before resolving provider credentials', async () => {
530569
await expect(
531570
searchLiveKnowledge.execute({

0 commit comments

Comments
 (0)